Remove dependency on matrix-react-sdk
This commit is contained in:
parent
46bcb8ac75
commit
72197c1a0a
30 changed files with 2610 additions and 1211 deletions
|
@ -18,11 +18,6 @@ module.exports = {
|
||||||
);
|
);
|
||||||
config.plugins.push(svgrPlugin());
|
config.plugins.push(svgrPlugin());
|
||||||
config.resolve = config.resolve || {};
|
config.resolve = config.resolve || {};
|
||||||
config.resolve.alias = config.resolve.alias || {};
|
|
||||||
config.resolve.alias["$(res)"] = path.resolve(
|
|
||||||
__dirname,
|
|
||||||
"../node_modules/matrix-react-sdk/res"
|
|
||||||
);
|
|
||||||
config.resolve.dedupe = config.resolve.dedupe || [];
|
config.resolve.dedupe = config.resolve.dedupe || [];
|
||||||
config.resolve.dedupe.push("react", "react-dom", "matrix-js-sdk");
|
config.resolve.dedupe.push("react", "react-dom", "matrix-js-sdk");
|
||||||
return config;
|
return config;
|
||||||
|
|
|
@ -2,13 +2,13 @@ FROM node:16-buster as builder
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY . /src/matrix-video-chat
|
COPY . /src/element-call
|
||||||
RUN matrix-video-chat/scripts/dockerbuild.sh
|
RUN element-call/scripts/dockerbuild.sh
|
||||||
|
|
||||||
# App
|
# App
|
||||||
FROM nginxinc/nginx-unprivileged:alpine
|
FROM nginxinc/nginx-unprivileged:alpine
|
||||||
|
|
||||||
COPY --from=builder /src/matrix-video-chat/dist /app
|
COPY --from=builder /src/element-call/dist /app
|
||||||
COPY scripts/default.conf /etc/nginx/conf.d/
|
COPY scripts/default.conf /etc/nginx/conf.d/
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
14
README.md
14
README.md
|
@ -6,7 +6,7 @@ Discussion in [#webrtc:matrix.org:  and [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/pull/6848). Because of how these packages are configured and Vite's requirements, you will need to clone them locally and use `yarn link` to stich things together.
|
`element-call` is built against the `robertlong/group-call` branch of [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk/pull/1902). Because of how this package is configured and Vite's requirements, you will need to clone it locally and use `yarn link` to stich things together.
|
||||||
|
|
||||||
First clone, install, and link `matrix-js-sdk`
|
First clone, install, and link `matrix-js-sdk`
|
||||||
|
|
||||||
|
@ -18,17 +18,6 @@ yarn
|
||||||
yarn link
|
yarn link
|
||||||
```
|
```
|
||||||
|
|
||||||
Then clone, install, link `matrix-js-sdk` into `matrix-react-sdk`, and link `matrix-react-sdk`
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/matrix-org/matrix-react-sdk.git
|
|
||||||
cd matrix-react-sdk
|
|
||||||
git checkout robertlong/group-call
|
|
||||||
yarn
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn link
|
|
||||||
```
|
|
||||||
|
|
||||||
Next you'll also need [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008.
|
Next you'll also need [Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) installed locally and running on port 8008.
|
||||||
|
|
||||||
Finally we can set up this project.
|
Finally we can set up this project.
|
||||||
|
@ -38,7 +27,6 @@ git clone https://github.com/vector-im/element-call.git
|
||||||
cd element-call
|
cd element-call
|
||||||
yarn
|
yarn
|
||||||
yarn link matrix-js-sdk
|
yarn link matrix-js-sdk
|
||||||
yarn link matrix-react-sdk
|
|
||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"@react-aria/tabs": "^3.1.0",
|
"@react-aria/tabs": "^3.1.0",
|
||||||
"@react-aria/tooltip": "^3.1.3",
|
"@react-aria/tooltip": "^3.1.3",
|
||||||
"@react-aria/utils": "^3.10.0",
|
"@react-aria/utils": "^3.10.0",
|
||||||
|
"@react-spring/web": "^9.4.4",
|
||||||
"@react-stately/collections": "^3.3.4",
|
"@react-stately/collections": "^3.3.4",
|
||||||
"@react-stately/overlays": "^3.1.3",
|
"@react-stately/overlays": "^3.1.3",
|
||||||
"@react-stately/select": "^3.1.3",
|
"@react-stately/select": "^3.1.3",
|
||||||
|
@ -25,11 +26,12 @@
|
||||||
"@react-stately/tree": "^3.2.0",
|
"@react-stately/tree": "^3.2.0",
|
||||||
"@sentry/react": "^6.13.3",
|
"@sentry/react": "^6.13.3",
|
||||||
"@sentry/tracing": "^6.13.3",
|
"@sentry/tracing": "^6.13.3",
|
||||||
|
"@use-gesture/react": "^10.2.11",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"color-hash": "^2.0.1",
|
"color-hash": "^2.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
|
"lodash-move": "^1.1.1",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#robertlong/group-call",
|
||||||
"matrix-react-sdk": "github:matrix-org/matrix-react-sdk#robertlong/group-call",
|
|
||||||
"mermaid": "^8.13.8",
|
"mermaid": "^8.13.8",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pako": "^2.0.4",
|
"pako": "^2.0.4",
|
||||||
|
|
|
@ -13,22 +13,11 @@ git checkout robertlong/group-call
|
||||||
yarn install
|
yarn install
|
||||||
yarn run build
|
yarn run build
|
||||||
yarn link
|
yarn link
|
||||||
cd ..
|
|
||||||
|
|
||||||
git clone https://github.com/matrix-org/matrix-react-sdk.git
|
cd ../element-call
|
||||||
cd matrix-react-sdk
|
|
||||||
git checkout robertlong/group-call
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn install
|
|
||||||
yarn run build
|
|
||||||
yarn link
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
cd matrix-video-chat
|
|
||||||
|
|
||||||
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
export VITE_APP_VERSION=$(git describe --tags --abbrev=0)
|
||||||
|
|
||||||
yarn link matrix-js-sdk
|
yarn link matrix-js-sdk
|
||||||
yarn link matrix-react-sdk
|
|
||||||
yarn install
|
yarn install
|
||||||
yarn run build
|
yarn run build
|
||||||
|
|
5
src/icons/MicMuted.svg
Normal file
5
src/icons/MicMuted.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.9206 1.0544C1.68141 0.815201 1.29359 0.815201 1.0544 1.0544C0.815201 1.29359 0.815201 1.68141 1.0544 1.9206L4.55 5.41621V7C4.55 8.3531 5.6469 9.45 7 9.45C7.45436 9.45 7.87983 9.32632 8.24458 9.11079L9.12938 9.99558C8.52863 10.4234 7.7937 10.675 7 10.675C4.97035 10.675 3.325 9.02965 3.325 7C3.325 6.66173 3.05077 6.3875 2.7125 6.3875C2.37423 6.3875 2.1 6.66173 2.1 7C2.1 9.49877 3.97038 11.5607 6.3875 11.8621V12.5125C6.3875 12.8508 6.66173 13.125 7 13.125C7.33827 13.125 7.6125 12.8508 7.6125 12.5125V11.8621C8.50718 11.7505 9.32696 11.3978 10.0047 10.8709L12.0794 12.9456C12.3186 13.1848 12.7064 13.1848 12.9456 12.9456C13.1848 12.7064 13.1848 12.3186 12.9456 12.0794L1.9206 1.0544Z" fill="white"/>
|
||||||
|
<path d="M10.5474 7.96338L11.5073 8.92525C11.7601 8.33424 11.9 7.68346 11.9 7C11.9 6.66173 11.6258 6.3875 11.2875 6.3875C10.9492 6.3875 10.675 6.66173 10.675 7C10.675 7.33336 10.6306 7.65634 10.5474 7.96338Z" fill="white"/>
|
||||||
|
<path d="M4.81385 2.21784L9.45 6.86366V3.325C9.45 1.9719 8.3531 0.875 7 0.875C6.04532 0.875 5.21818 1.42104 4.81385 2.21784Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
6
src/icons/VideoMuted.svg
Normal file
6
src/icons/VideoMuted.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.20333 0.963373C0.474437 0.690007 0.913989 0.690007 1.1851 0.963373L11.5983 11.4633C11.8694 11.7367 11.8694 12.1799 11.5983 12.4533C11.3272 12.7267 10.8876 12.7267 10.6165 12.4533L0.20333 1.95332C-0.0677768 1.67995 -0.0677768 1.23674 0.20333 0.963373Z" fill="white"/>
|
||||||
|
<path d="M0.418261 3.63429C0.226267 3.95219 0.115674 4.32557 0.115674 4.725V9.85832C0.115674 11.0181 1.0481 11.9583 2.19831 11.9583H8.65411L0.447396 3.66596C0.437225 3.65568 0.427513 3.64511 0.418261 3.63429Z" fill="white"/>
|
||||||
|
<path d="M9.95036 4.725V8.33212L4.30219 2.625H7.86772C9.01793 2.625 9.95036 3.5652 9.95036 4.725Z" fill="white"/>
|
||||||
|
<path d="M12.8721 4.11817L11.1074 5.54167V9.04166L12.8721 10.4652C13.3266 10.8318 14 10.5055 14 9.91855V4.66478C14 4.07782 13.3266 3.7515 12.8721 4.11817Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 892 B |
|
@ -22,10 +22,10 @@ import App from "./App";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { Integrations } from "@sentry/tracing";
|
import { Integrations } from "@sentry/tracing";
|
||||||
import { ErrorView } from "./FullScreenView";
|
import { ErrorView } from "./FullScreenView";
|
||||||
import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake";
|
import { init as initRageshake } from "./settings/rageshake";
|
||||||
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
import { InspectorContextProvider } from "./room/GroupCallInspector";
|
||||||
|
|
||||||
rageshake.init();
|
initRageshake();
|
||||||
|
|
||||||
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
|
console.info(`matrix-video-chat ${import.meta.env.VITE_APP_VERSION || "dev"}`);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,10 @@ import React, { useCallback, useEffect } from "react";
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||||
import { useSubmitRageshake, useRageshakeRequest } from "../settings/rageshake";
|
import {
|
||||||
|
useSubmitRageshake,
|
||||||
|
useRageshakeRequest,
|
||||||
|
} from "../settings/submit-rageshake";
|
||||||
import { Body } from "../typography/Typography";
|
import { Body } from "../typography/Typography";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { useGroupCall } from "matrix-react-sdk/src/hooks/useGroupCall";
|
import { useGroupCall } from "./useGroupCall";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
import { LobbyView } from "./LobbyView";
|
import { LobbyView } from "./LobbyView";
|
||||||
import { InCallView } from "./InCallView";
|
import { InCallView } from "./InCallView";
|
||||||
|
@ -14,7 +14,6 @@ export function GroupCallView({
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
roomId,
|
roomId,
|
||||||
groupCall,
|
groupCall,
|
||||||
simpleGrid,
|
|
||||||
}) {
|
}) {
|
||||||
const [showInspector, setShowInspector] = useState(
|
const [showInspector, setShowInspector] = useState(
|
||||||
() => !!localStorage.getItem("matrix-group-call-inspector")
|
() => !!localStorage.getItem("matrix-group-call-inspector")
|
||||||
|
@ -89,7 +88,6 @@ export function GroupCallView({
|
||||||
isScreensharing={isScreensharing}
|
isScreensharing={isScreensharing}
|
||||||
localScreenshareFeed={localScreenshareFeed}
|
localScreenshareFeed={localScreenshareFeed}
|
||||||
screenshareFeeds={screenshareFeeds}
|
screenshareFeeds={screenshareFeeds}
|
||||||
simpleGrid={simpleGrid}
|
|
||||||
setShowInspector={onChangeShowInspector}
|
setShowInspector={onChangeShowInspector}
|
||||||
showInspector={showInspector}
|
showInspector={showInspector}
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
|
|
|
@ -7,19 +7,15 @@ import {
|
||||||
ScreenshareButton,
|
ScreenshareButton,
|
||||||
} from "../button";
|
} from "../button";
|
||||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||||
import VideoGrid, {
|
import { VideoGrid, useVideoGridLayout } from "../video-grid/VideoGrid";
|
||||||
useVideoGridLayout,
|
import { VideoTileContainer } from "../video-grid/VideoTileContainer";
|
||||||
} from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
|
|
||||||
import { VideoTileContainer } from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoTileContainer";
|
|
||||||
import SimpleVideoGrid from "matrix-react-sdk/src/components/views/voip/GroupCallView/SimpleVideoGrid";
|
|
||||||
import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
|
|
||||||
import { getAvatarUrl } from "../matrix-utils";
|
import { getAvatarUrl } from "../matrix-utils";
|
||||||
import { GroupCallInspector } from "./GroupCallInspector";
|
import { GroupCallInspector } from "./GroupCallInspector";
|
||||||
import { OverflowMenu } from "./OverflowMenu";
|
import { OverflowMenu } from "./OverflowMenu";
|
||||||
import { GridLayoutMenu } from "./GridLayoutMenu";
|
import { GridLayoutMenu } from "./GridLayoutMenu";
|
||||||
import { Avatar } from "../Avatar";
|
import { Avatar } from "../Avatar";
|
||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { useRageshakeRequestModal } from "../settings/rageshake";
|
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
||||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||||
import { usePreventScroll } from "@react-aria/overlays";
|
import { usePreventScroll } from "@react-aria/overlays";
|
||||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
|
@ -44,7 +40,6 @@ export function InCallView({
|
||||||
toggleScreensharing,
|
toggleScreensharing,
|
||||||
isScreensharing,
|
isScreensharing,
|
||||||
screenshareFeeds,
|
screenshareFeeds,
|
||||||
simpleGrid,
|
|
||||||
setShowInspector,
|
setShowInspector,
|
||||||
showInspector,
|
showInspector,
|
||||||
roomId,
|
roomId,
|
||||||
|
@ -149,8 +144,6 @@ export function InCallView({
|
||||||
<div className={styles.centerMessage}>
|
<div className={styles.centerMessage}>
|
||||||
<p>Waiting for other participants...</p>
|
<p>Waiting for other participants...</p>
|
||||||
</div>
|
</div>
|
||||||
) : simpleGrid ? (
|
|
||||||
<SimpleVideoGrid items={items} />
|
|
||||||
) : (
|
) : (
|
||||||
<VideoGrid
|
<VideoGrid
|
||||||
items={items}
|
items={items}
|
||||||
|
|
|
@ -3,8 +3,8 @@ import styles from "./LobbyView.module.css";
|
||||||
import { Button, CopyButton, MicButton, VideoButton } from "../button";
|
import { Button, CopyButton, MicButton, VideoButton } from "../button";
|
||||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { useCallFeed } from "matrix-react-sdk/src/hooks/useCallFeed";
|
import { useCallFeed } from "../video-grid/useCallFeed";
|
||||||
import { useMediaStream } from "matrix-react-sdk/src/hooks/useMediaStream";
|
import { useMediaStream } from "../video-grid/useMediaStream";
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
import { OverflowMenu } from "./OverflowMenu";
|
import { OverflowMenu } from "./OverflowMenu";
|
||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { useEffect } from "react";
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { FieldRow, ErrorMessage } from "../input/Input";
|
import { FieldRow, ErrorMessage } from "../input/Input";
|
||||||
import { useSubmitRageshake } from "../settings/rageshake";
|
import { useSubmitRageshake } from "../settings/submit-rageshake";
|
||||||
import { Body } from "../typography/Typography";
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) {
|
export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) {
|
||||||
|
|
|
@ -29,9 +29,9 @@ export function RoomPage() {
|
||||||
|
|
||||||
const { roomId: maybeRoomId } = useParams();
|
const { roomId: maybeRoomId } = useParams();
|
||||||
const { hash, search } = useLocation();
|
const { hash, search } = useLocation();
|
||||||
const [simpleGrid, viaServers] = useMemo(() => {
|
const [viaServers] = useMemo(() => {
|
||||||
const params = new URLSearchParams(search);
|
const params = new URLSearchParams(search);
|
||||||
return [params.has("simple"), params.getAll("via")];
|
return [params.getAll("via")];
|
||||||
}, [search]);
|
}, [search]);
|
||||||
const roomId = (maybeRoomId || hash || "").toLowerCase();
|
const roomId = (maybeRoomId || hash || "").toLowerCase();
|
||||||
|
|
||||||
|
@ -56,7 +56,6 @@ export function RoomPage() {
|
||||||
roomId={roomId}
|
roomId={roomId}
|
||||||
groupCall={groupCall}
|
groupCall={groupCall}
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
isPasswordlessUser={isPasswordlessUser}
|
||||||
simpleGrid={simpleGrid}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</GroupCallLoader>
|
</GroupCallLoader>
|
||||||
|
|
252
src/room/useGroupCall.js
Normal file
252
src/room/useGroupCall.js
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
GroupCallEvent,
|
||||||
|
GroupCallState,
|
||||||
|
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
|
import { usePageUnload } from "./usePageUnload";
|
||||||
|
|
||||||
|
export function useGroupCall(groupCall) {
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
state,
|
||||||
|
calls,
|
||||||
|
localCallFeed,
|
||||||
|
activeSpeaker,
|
||||||
|
userMediaFeeds,
|
||||||
|
error,
|
||||||
|
microphoneMuted,
|
||||||
|
localVideoMuted,
|
||||||
|
isScreensharing,
|
||||||
|
screenshareFeeds,
|
||||||
|
localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId,
|
||||||
|
participants,
|
||||||
|
hasLocalParticipant,
|
||||||
|
requestingScreenshare,
|
||||||
|
},
|
||||||
|
setState,
|
||||||
|
] = useState({
|
||||||
|
state: GroupCallState.LocalCallFeedUninitialized,
|
||||||
|
calls: [],
|
||||||
|
userMediaFeeds: [],
|
||||||
|
microphoneMuted: false,
|
||||||
|
localVideoMuted: false,
|
||||||
|
screenshareFeeds: [],
|
||||||
|
isScreensharing: false,
|
||||||
|
requestingScreenshare: false,
|
||||||
|
participants: [],
|
||||||
|
hasLocalParticipant: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateState = (state) =>
|
||||||
|
setState((prevState) => ({ ...prevState, ...state }));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onGroupCallStateChanged() {
|
||||||
|
updateState({
|
||||||
|
state: groupCall.state,
|
||||||
|
calls: [...groupCall.calls],
|
||||||
|
localCallFeed: groupCall.localCallFeed,
|
||||||
|
activeSpeaker: groupCall.activeSpeaker,
|
||||||
|
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||||
|
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||||
|
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||||
|
isScreensharing: groupCall.isScreensharing(),
|
||||||
|
localScreenshareFeed: groupCall.localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||||
|
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||||
|
participants: [...groupCall.participants],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUserMediaFeedsChanged(userMediaFeeds) {
|
||||||
|
updateState({
|
||||||
|
userMediaFeeds: [...userMediaFeeds],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScreenshareFeedsChanged(screenshareFeeds) {
|
||||||
|
updateState({
|
||||||
|
screenshareFeeds: [...screenshareFeeds],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onActiveSpeakerChanged(activeSpeaker) {
|
||||||
|
updateState({
|
||||||
|
activeSpeaker: activeSpeaker,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLocalMuteStateChanged(microphoneMuted, localVideoMuted) {
|
||||||
|
updateState({
|
||||||
|
microphoneMuted,
|
||||||
|
localVideoMuted,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLocalScreenshareStateChanged(
|
||||||
|
isScreensharing,
|
||||||
|
localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId
|
||||||
|
) {
|
||||||
|
updateState({
|
||||||
|
isScreensharing,
|
||||||
|
localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCallsChanged(calls) {
|
||||||
|
updateState({
|
||||||
|
calls: [...calls],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onParticipantsChanged(participants) {
|
||||||
|
updateState({
|
||||||
|
participants: [...participants],
|
||||||
|
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
groupCall.on(GroupCallEvent.GroupCallStateChanged, onGroupCallStateChanged);
|
||||||
|
groupCall.on(GroupCallEvent.UserMediaFeedsChanged, onUserMediaFeedsChanged);
|
||||||
|
groupCall.on(
|
||||||
|
GroupCallEvent.ScreenshareFeedsChanged,
|
||||||
|
onScreenshareFeedsChanged
|
||||||
|
);
|
||||||
|
groupCall.on(GroupCallEvent.ActiveSpeakerChanged, onActiveSpeakerChanged);
|
||||||
|
groupCall.on(GroupCallEvent.LocalMuteStateChanged, onLocalMuteStateChanged);
|
||||||
|
groupCall.on(
|
||||||
|
GroupCallEvent.LocalScreenshareStateChanged,
|
||||||
|
onLocalScreenshareStateChanged
|
||||||
|
);
|
||||||
|
groupCall.on(GroupCallEvent.CallsChanged, onCallsChanged);
|
||||||
|
groupCall.on(GroupCallEvent.ParticipantsChanged, onParticipantsChanged);
|
||||||
|
|
||||||
|
updateState({
|
||||||
|
error: null,
|
||||||
|
state: groupCall.state,
|
||||||
|
calls: [...groupCall.calls],
|
||||||
|
localCallFeed: groupCall.localCallFeed,
|
||||||
|
activeSpeaker: groupCall.activeSpeaker,
|
||||||
|
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||||
|
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||||
|
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||||
|
isScreensharing: groupCall.isScreensharing(),
|
||||||
|
localScreenshareFeed: groupCall.localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||||
|
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||||
|
participants: [...groupCall.participants],
|
||||||
|
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.GroupCallStateChanged,
|
||||||
|
onGroupCallStateChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.UserMediaFeedsChanged,
|
||||||
|
onUserMediaFeedsChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.ScreenshareFeedsChanged,
|
||||||
|
onScreenshareFeedsChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.ActiveSpeakerChanged,
|
||||||
|
onActiveSpeakerChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.LocalMuteStateChanged,
|
||||||
|
onLocalMuteStateChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.LocalScreenshareStateChanged,
|
||||||
|
onLocalScreenshareStateChanged
|
||||||
|
);
|
||||||
|
groupCall.removeListener(GroupCallEvent.CallsChanged, onCallsChanged);
|
||||||
|
groupCall.removeListener(
|
||||||
|
GroupCallEvent.ParticipantsChanged,
|
||||||
|
onParticipantsChanged
|
||||||
|
);
|
||||||
|
groupCall.leave();
|
||||||
|
};
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
|
usePageUnload(() => {
|
||||||
|
groupCall.leave();
|
||||||
|
});
|
||||||
|
|
||||||
|
const initLocalCallFeed = useCallback(
|
||||||
|
() => groupCall.initLocalCallFeed(),
|
||||||
|
[groupCall]
|
||||||
|
);
|
||||||
|
|
||||||
|
const enter = useCallback(() => {
|
||||||
|
if (
|
||||||
|
groupCall.state !== GroupCallState.LocalCallFeedUninitialized &&
|
||||||
|
groupCall.state !== GroupCallState.LocalCallFeedInitialized
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
groupCall.enter().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
updateState({ error });
|
||||||
|
});
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
|
const leave = useCallback(() => groupCall.leave(), [groupCall]);
|
||||||
|
|
||||||
|
const toggleLocalVideoMuted = useCallback(() => {
|
||||||
|
groupCall.setLocalVideoMuted(!groupCall.isLocalVideoMuted());
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
|
const toggleMicrophoneMuted = useCallback(() => {
|
||||||
|
groupCall.setMicrophoneMuted(!groupCall.isMicrophoneMuted());
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
|
const toggleScreensharing = useCallback(() => {
|
||||||
|
updateState({ requestingScreenshare: true });
|
||||||
|
|
||||||
|
groupCall.setScreensharingEnabled(!groupCall.isScreensharing()).then(() => {
|
||||||
|
updateState({ requestingScreenshare: false });
|
||||||
|
});
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (window.RTCPeerConnection === undefined) {
|
||||||
|
const error = new Error(
|
||||||
|
"WebRTC is not supported or is being blocked in this browser."
|
||||||
|
);
|
||||||
|
console.error(error);
|
||||||
|
updateState({ error });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
calls,
|
||||||
|
localCallFeed,
|
||||||
|
activeSpeaker,
|
||||||
|
userMediaFeeds,
|
||||||
|
microphoneMuted,
|
||||||
|
localVideoMuted,
|
||||||
|
error,
|
||||||
|
initLocalCallFeed,
|
||||||
|
enter,
|
||||||
|
leave,
|
||||||
|
toggleLocalVideoMuted,
|
||||||
|
toggleMicrophoneMuted,
|
||||||
|
toggleScreensharing,
|
||||||
|
requestingScreenshare,
|
||||||
|
isScreensharing,
|
||||||
|
screenshareFeeds,
|
||||||
|
localScreenshareFeed,
|
||||||
|
localDesktopCapturerSourceId,
|
||||||
|
participants,
|
||||||
|
hasLocalParticipant,
|
||||||
|
};
|
||||||
|
}
|
54
src/room/usePageUnload.js
Normal file
54
src/room/usePageUnload.js
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/9039885
|
||||||
|
function isIOS() {
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
"iPad Simulator",
|
||||||
|
"iPhone Simulator",
|
||||||
|
"iPod Simulator",
|
||||||
|
"iPad",
|
||||||
|
"iPhone",
|
||||||
|
"iPod",
|
||||||
|
].includes(navigator.platform) ||
|
||||||
|
// iPad on iOS 13 detection
|
||||||
|
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePageUnload(callback) {
|
||||||
|
useEffect(() => {
|
||||||
|
let pageVisibilityTimeout;
|
||||||
|
|
||||||
|
function onBeforeUnload(event) {
|
||||||
|
if (event.type === "visibilitychange") {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
clearTimeout(pageVisibilityTimeout);
|
||||||
|
} else {
|
||||||
|
// Wait 5 seconds before closing the page to avoid accidentally leaving
|
||||||
|
// TODO: Make this configurable?
|
||||||
|
pageVisibilityTimeout = setTimeout(() => {
|
||||||
|
callback();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS doesn't fire beforeunload event, so leave the call when you hide the page.
|
||||||
|
if (isIOS()) {
|
||||||
|
window.addEventListener("pagehide", onBeforeUnload);
|
||||||
|
document.addEventListener("visibilitychange", onBeforeUnload);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", onBeforeUnload);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("pagehide", onBeforeUnload);
|
||||||
|
document.removeEventListener("visibilitychange", onBeforeUnload);
|
||||||
|
window.removeEventListener("beforeunload", onBeforeUnload);
|
||||||
|
clearTimeout(pageVisibilityTimeout);
|
||||||
|
};
|
||||||
|
}, [callback]);
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ import { Item } from "@react-stately/collections";
|
||||||
import { useMediaHandler } from "./useMediaHandler";
|
import { useMediaHandler } from "./useMediaHandler";
|
||||||
import { FieldRow, InputField } from "../input/Input";
|
import { FieldRow, InputField } from "../input/Input";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { useDownloadDebugLog } from "./rageshake";
|
import { useDownloadDebugLog } from "./submit-rageshake";
|
||||||
import { Body } from "../typography/Typography";
|
import { Body } from "../typography/Typography";
|
||||||
|
|
||||||
export function SettingsModal({ setShowInspector, showInspector, ...rest }) {
|
export function SettingsModal({ setShowInspector, showInspector, ...rest }) {
|
||||||
|
|
|
@ -1,300 +1,535 @@
|
||||||
import { useCallback, useContext, useEffect, useState } from "react";
|
/*
|
||||||
import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake";
|
Copyright 2017 OpenMarket Ltd
|
||||||
import pako from "pako";
|
Copyright 2018 New Vector Ltd
|
||||||
import { useClient } from "../ClientContext";
|
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||||
import { InspectorContext } from "../room/GroupCallInspector";
|
|
||||||
import { useModalTriggerState } from "../Modal";
|
|
||||||
|
|
||||||
export function useSubmitRageshake() {
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
const { client } = useClient();
|
you may not use this file except in compliance with the License.
|
||||||
const [{ json }] = useContext(InspectorContext);
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
const [{ sending, sent, error }, setState] = useState({
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
sending: false,
|
|
||||||
sent: false,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const submitRageshake = useCallback(
|
Unless required by applicable law or agreed to in writing, software
|
||||||
async (opts) => {
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
if (sending) {
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This module contains all the code needed to log the console, persist it to
|
||||||
|
// disk and submit bug reports. Rationale is as follows:
|
||||||
|
// - Monkey-patching the console is preferable to having a log library because
|
||||||
|
// we can catch logs by other libraries more easily, without having to all
|
||||||
|
// depend on the same log framework / pass the logger around.
|
||||||
|
// - We use IndexedDB to persists logs because it has generous disk space
|
||||||
|
// limits compared to local storage. IndexedDB does not work in incognito
|
||||||
|
// mode, in which case this module will not be able to write logs to disk.
|
||||||
|
// However, the logs will still be stored in-memory, so can still be
|
||||||
|
// submitted in a bug report should the user wish to: we can also store more
|
||||||
|
// logs in-memory than in local storage, which does work in incognito mode.
|
||||||
|
// We also need to handle the case where there are 2+ tabs. Each JS runtime
|
||||||
|
// generates a random string which serves as the "ID" for that tab/session.
|
||||||
|
// These IDs are stored along with the log lines.
|
||||||
|
// - Bug reports are sent as a POST over HTTPS: it purposefully does not use
|
||||||
|
// Matrix as bug reports may be made when Matrix is not responsive (which may
|
||||||
|
// be the cause of the bug). We send the most recent N MB of UTF-8 log data,
|
||||||
|
// starting with the most recent, which we know because the "ID"s are
|
||||||
|
// actually timestamps. We then purge the remaining logs. We also do this
|
||||||
|
// purge on startup to prevent logs from accumulating.
|
||||||
|
|
||||||
|
// the frequency with which we flush to indexeddb
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
const FLUSH_RATE_MS = 30 * 1000;
|
||||||
|
|
||||||
|
// the length of log data we keep in indexeddb (and include in the reports)
|
||||||
|
const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
|
||||||
|
|
||||||
|
// A class which monkey-patches the global console and stores log lines.
|
||||||
|
export class ConsoleLogger {
|
||||||
|
logs = "";
|
||||||
|
|
||||||
|
monkeyPatch(consoleObj) {
|
||||||
|
// Monkey-patch console logging
|
||||||
|
const consoleFunctionsToLevels = {
|
||||||
|
log: "I",
|
||||||
|
info: "I",
|
||||||
|
warn: "W",
|
||||||
|
error: "E",
|
||||||
|
};
|
||||||
|
Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
|
||||||
|
const level = consoleFunctionsToLevels[fnName];
|
||||||
|
const originalFn = consoleObj[fnName].bind(consoleObj);
|
||||||
|
consoleObj[fnName] = (...args) => {
|
||||||
|
this.log(level, ...args);
|
||||||
|
originalFn(...args);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log(level, ...args) {
|
||||||
|
// We don't know what locale the user may be running so use ISO strings
|
||||||
|
const ts = new Date().toISOString();
|
||||||
|
|
||||||
|
// Convert objects and errors to helpful things
|
||||||
|
args = args.map((arg) => {
|
||||||
|
if (arg instanceof DOMException) {
|
||||||
|
return arg.message + ` (${arg.name} | ${arg.code})`;
|
||||||
|
} else if (arg instanceof Error) {
|
||||||
|
return arg.message + (arg.stack ? `\n${arg.stack}` : "");
|
||||||
|
} else if (typeof arg === "object") {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(arg);
|
||||||
|
} catch (e) {
|
||||||
|
// In development, it can be useful to log complex cyclic
|
||||||
|
// objects to the console for inspection. This is fine for
|
||||||
|
// the console, but default `stringify` can't handle that.
|
||||||
|
// We workaround this by using a special replacer function
|
||||||
|
// to only log values of the root object and avoid cycles.
|
||||||
|
return JSON.stringify(arg, (key, value) => {
|
||||||
|
if (key && typeof value === "object") {
|
||||||
|
return "<object>";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Some browsers support string formatting which we're not doing here
|
||||||
|
// so the lines are a little more ugly but easy to implement / quick to
|
||||||
|
// run.
|
||||||
|
// Example line:
|
||||||
|
// 2017-01-18T11:23:53.214Z W Failed to set badge count
|
||||||
|
let line = `${ts} ${level} ${args.join(" ")}\n`;
|
||||||
|
// Do some cleanup
|
||||||
|
line = line.replace(/token=[a-zA-Z0-9-]+/gm, "token=xxxxx");
|
||||||
|
// Using + really is the quickest way in JS
|
||||||
|
// http://jsperf.com/concat-vs-plus-vs-join
|
||||||
|
this.logs += line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve log lines to flush to disk.
|
||||||
|
* @param {boolean} keepLogs True to not delete logs after flushing.
|
||||||
|
* @return {string} \n delimited log lines to flush.
|
||||||
|
*/
|
||||||
|
flush(keepLogs) {
|
||||||
|
// The ConsoleLogger doesn't care how these end up on disk, it just
|
||||||
|
// flushes them to the caller.
|
||||||
|
if (keepLogs) {
|
||||||
|
return this.logs;
|
||||||
|
}
|
||||||
|
const logsToFlush = this.logs;
|
||||||
|
this.logs = "";
|
||||||
|
return logsToFlush;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A class which stores log lines in an IndexedDB instance.
|
||||||
|
export class IndexedDBLogStore {
|
||||||
|
index = 0;
|
||||||
|
db = null;
|
||||||
|
flushPromise = null;
|
||||||
|
flushAgainPromise = null;
|
||||||
|
|
||||||
|
constructor(indexedDB, logger) {
|
||||||
|
this.indexedDB = indexedDB;
|
||||||
|
this.logger = logger;
|
||||||
|
this.id = "instance-" + Math.random() + Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {Promise} Resolves when the store is ready.
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
const req = this.indexedDB.open("logs");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
req.onsuccess = (event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
this.db = event.target.result;
|
||||||
|
// Periodically flush logs to local storage / indexeddb
|
||||||
|
setInterval(this.flush.bind(this), FLUSH_RATE_MS);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
req.onerror = (event) => {
|
||||||
|
const err =
|
||||||
|
// @ts-ignore
|
||||||
|
"Failed to open log database: " + event.target.error.name;
|
||||||
|
logger.error(err);
|
||||||
|
reject(new Error(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// First time: Setup the object store
|
||||||
|
req.onupgradeneeded = (event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const db = event.target.result;
|
||||||
|
const logObjStore = db.createObjectStore("logs", {
|
||||||
|
keyPath: ["id", "index"],
|
||||||
|
});
|
||||||
|
// Keys in the database look like: [ "instance-148938490", 0 ]
|
||||||
|
// Later on we need to query everything based on an instance id.
|
||||||
|
// In order to do this, we need to set up indexes "id".
|
||||||
|
logObjStore.createIndex("id", "id", { unique: false });
|
||||||
|
|
||||||
|
logObjStore.add(
|
||||||
|
this.generateLogEntry(new Date() + " ::: Log database was created.")
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastModifiedStore = db.createObjectStore("logslastmod", {
|
||||||
|
keyPath: "id",
|
||||||
|
});
|
||||||
|
lastModifiedStore.add(this.generateLastModifiedTime());
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flush logs to disk.
|
||||||
|
*
|
||||||
|
* There are guards to protect against race conditions in order to ensure
|
||||||
|
* that all previous flushes have completed before the most recent flush.
|
||||||
|
* Consider without guards:
|
||||||
|
* - A calls flush() periodically.
|
||||||
|
* - B calls flush() and wants to send logs immediately afterwards.
|
||||||
|
* - If B doesn't wait for A's flush to complete, B will be missing the
|
||||||
|
* contents of A's flush.
|
||||||
|
* To protect against this, we set 'flushPromise' when a flush is ongoing.
|
||||||
|
* Subsequent calls to flush() during this period will chain another flush,
|
||||||
|
* then keep returning that same chained flush.
|
||||||
|
*
|
||||||
|
* This guarantees that we will always eventually do a flush when flush() is
|
||||||
|
* called.
|
||||||
|
*
|
||||||
|
* @return {Promise} Resolved when the logs have been flushed.
|
||||||
|
*/
|
||||||
|
flush() {
|
||||||
|
// check if a flush() operation is ongoing
|
||||||
|
if (this.flushPromise) {
|
||||||
|
if (this.flushAgainPromise) {
|
||||||
|
// this is the 3rd+ time we've called flush() : return the same promise.
|
||||||
|
return this.flushAgainPromise;
|
||||||
|
}
|
||||||
|
// queue up a flush to occur immediately after the pending one completes.
|
||||||
|
this.flushAgainPromise = this.flushPromise
|
||||||
|
.then(() => {
|
||||||
|
return this.flush();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.flushAgainPromise = null;
|
||||||
|
});
|
||||||
|
return this.flushAgainPromise;
|
||||||
|
}
|
||||||
|
// there is no flush promise or there was but it has finished, so do
|
||||||
|
// a brand new one, destroying the chain which may have been built up.
|
||||||
|
this.flushPromise = new Promise((resolve, reject) => {
|
||||||
|
if (!this.db) {
|
||||||
|
// not connected yet or user rejected access for us to r/w to the db.
|
||||||
|
reject(new Error("No connected database"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const lines = this.logger.flush();
|
||||||
try {
|
if (lines.length === 0) {
|
||||||
setState({ sending: true, sent: false, error: null });
|
resolve();
|
||||||
|
return;
|
||||||
let userAgent = "UNKNOWN";
|
|
||||||
if (window.navigator && window.navigator.userAgent) {
|
|
||||||
userAgent = window.navigator.userAgent;
|
|
||||||
}
|
|
||||||
|
|
||||||
let touchInput = "UNKNOWN";
|
|
||||||
try {
|
|
||||||
// MDN claims broad support across browsers
|
|
||||||
touchInput = String(window.matchMedia("(pointer: coarse)").matches);
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
const body = new FormData();
|
|
||||||
body.append(
|
|
||||||
"text",
|
|
||||||
opts.description || "User did not supply any additional text."
|
|
||||||
);
|
|
||||||
body.append("app", "matrix-video-chat");
|
|
||||||
body.append("version", import.meta.env.VITE_APP_VERSION || "dev");
|
|
||||||
body.append("user_agent", userAgent);
|
|
||||||
body.append("installed_pwa", false);
|
|
||||||
body.append("touch_input", touchInput);
|
|
||||||
|
|
||||||
if (client) {
|
|
||||||
const userId = client.getUserId();
|
|
||||||
const user = client.getUser(userId);
|
|
||||||
body.append("display_name", user?.displayName);
|
|
||||||
body.append("user_id", client.credentials.userId);
|
|
||||||
body.append("device_id", client.deviceId);
|
|
||||||
|
|
||||||
if (opts.roomId) {
|
|
||||||
body.append("room_id", opts.roomId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client.isCryptoEnabled()) {
|
|
||||||
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
|
|
||||||
if (client.getDeviceCurve25519Key) {
|
|
||||||
keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
|
|
||||||
}
|
|
||||||
body.append("device_keys", keys.join(", "));
|
|
||||||
body.append("cross_signing_key", client.getCrossSigningId());
|
|
||||||
|
|
||||||
// add cross-signing status information
|
|
||||||
const crossSigning = client.crypto.crossSigningInfo;
|
|
||||||
const secretStorage = client.crypto.secretStorage;
|
|
||||||
|
|
||||||
body.append(
|
|
||||||
"cross_signing_ready",
|
|
||||||
String(await client.isCrossSigningReady())
|
|
||||||
);
|
|
||||||
body.append(
|
|
||||||
"cross_signing_supported_by_hs",
|
|
||||||
String(
|
|
||||||
await client.doesServerSupportUnstableFeature(
|
|
||||||
"org.matrix.e2e_cross_signing"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
body.append("cross_signing_key", crossSigning.getId());
|
|
||||||
body.append(
|
|
||||||
"cross_signing_privkey_in_secret_storage",
|
|
||||||
String(
|
|
||||||
!!(await crossSigning.isStoredInSecretStorage(secretStorage))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const pkCache = client.getCrossSigningCacheCallbacks();
|
|
||||||
body.append(
|
|
||||||
"cross_signing_master_privkey_cached",
|
|
||||||
String(
|
|
||||||
!!(pkCache && (await pkCache.getCrossSigningKeyCache("master")))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
body.append(
|
|
||||||
"cross_signing_self_signing_privkey_cached",
|
|
||||||
String(
|
|
||||||
!!(
|
|
||||||
pkCache &&
|
|
||||||
(await pkCache.getCrossSigningKeyCache("self_signing"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
body.append(
|
|
||||||
"cross_signing_user_signing_privkey_cached",
|
|
||||||
String(
|
|
||||||
!!(
|
|
||||||
pkCache &&
|
|
||||||
(await pkCache.getCrossSigningKeyCache("user_signing"))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
body.append(
|
|
||||||
"secret_storage_ready",
|
|
||||||
String(await client.isSecretStorageReady())
|
|
||||||
);
|
|
||||||
body.append(
|
|
||||||
"secret_storage_key_in_account",
|
|
||||||
String(!!(await secretStorage.hasKey()))
|
|
||||||
);
|
|
||||||
|
|
||||||
body.append(
|
|
||||||
"session_backup_key_in_secret_storage",
|
|
||||||
String(!!(await client.isKeyBackupKeyStored()))
|
|
||||||
);
|
|
||||||
const sessionBackupKeyFromCache =
|
|
||||||
await client.crypto.getSessionBackupPrivateKey();
|
|
||||||
body.append(
|
|
||||||
"session_backup_key_cached",
|
|
||||||
String(!!sessionBackupKeyFromCache)
|
|
||||||
);
|
|
||||||
body.append(
|
|
||||||
"session_backup_key_well_formed",
|
|
||||||
String(sessionBackupKeyFromCache instanceof Uint8Array)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.label) {
|
|
||||||
body.append("label", opts.label);
|
|
||||||
}
|
|
||||||
|
|
||||||
// add storage persistence/quota information
|
|
||||||
if (navigator.storage && navigator.storage.persisted) {
|
|
||||||
try {
|
|
||||||
body.append(
|
|
||||||
"storageManager_persisted",
|
|
||||||
String(await navigator.storage.persisted())
|
|
||||||
);
|
|
||||||
} catch (e) {}
|
|
||||||
} else if (document.hasStorageAccess) {
|
|
||||||
// Safari
|
|
||||||
try {
|
|
||||||
body.append(
|
|
||||||
"storageManager_persisted",
|
|
||||||
String(await document.hasStorageAccess())
|
|
||||||
);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (navigator.storage && navigator.storage.estimate) {
|
|
||||||
try {
|
|
||||||
const estimate = await navigator.storage.estimate();
|
|
||||||
body.append("storageManager_quota", String(estimate.quota));
|
|
||||||
body.append("storageManager_usage", String(estimate.usage));
|
|
||||||
if (estimate.usageDetails) {
|
|
||||||
Object.keys(estimate.usageDetails).forEach((k) => {
|
|
||||||
body.append(
|
|
||||||
`storageManager_usage_${k}`,
|
|
||||||
String(estimate.usageDetails[k])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.sendLogs) {
|
|
||||||
const logs = await rageshake.getLogsForReport();
|
|
||||||
|
|
||||||
for (const entry of logs) {
|
|
||||||
// encode as UTF-8
|
|
||||||
let buf = new TextEncoder().encode(entry.lines);
|
|
||||||
|
|
||||||
// compress
|
|
||||||
buf = pako.gzip(buf);
|
|
||||||
|
|
||||||
body.append("compressed-log", new Blob([buf]), entry.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json) {
|
|
||||||
body.append(
|
|
||||||
"file",
|
|
||||||
new Blob([JSON.stringify(json)], { type: "text/plain" }),
|
|
||||||
"groupcall.txt"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.rageshakeRequestId) {
|
|
||||||
body.append(
|
|
||||||
"group_call_rageshake_request_id",
|
|
||||||
opts.rageshakeRequestId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fetch(
|
|
||||||
import.meta.env.VITE_RAGESHAKE_SUBMIT_URL ||
|
|
||||||
"https://element.io/bugreports/submit",
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
setState({ sending: false, sent: true, error: null });
|
|
||||||
} catch (error) {
|
|
||||||
setState({ sending: false, sent: false, error });
|
|
||||||
console.error(error);
|
|
||||||
}
|
}
|
||||||
},
|
const txn = this.db.transaction(["logs", "logslastmod"], "readwrite");
|
||||||
[client]
|
const objStore = txn.objectStore("logs");
|
||||||
);
|
txn.oncomplete = (event) => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
txn.onerror = (event) => {
|
||||||
|
logger.error("Failed to flush logs : ", event);
|
||||||
|
reject(new Error("Failed to write logs: " + event.target.errorCode));
|
||||||
|
};
|
||||||
|
objStore.add(this.generateLogEntry(lines));
|
||||||
|
const lastModStore = txn.objectStore("logslastmod");
|
||||||
|
lastModStore.put(this.generateLastModifiedTime());
|
||||||
|
}).then(() => {
|
||||||
|
this.flushPromise = null;
|
||||||
|
});
|
||||||
|
return this.flushPromise;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
/**
|
||||||
submitRageshake,
|
* Consume the most recent logs and return them. Older logs which are not
|
||||||
sending,
|
* returned are deleted at the same time, so this can be called at startup
|
||||||
sent,
|
* to do house-keeping to keep the logs from growing too large.
|
||||||
error,
|
*
|
||||||
};
|
* @return {Promise<Object[]>} Resolves to an array of objects. The array is
|
||||||
}
|
* sorted in time (oldest first) based on when the log file was created (the
|
||||||
|
* log ID). The objects have said log ID in an "id" field and "lines" which
|
||||||
|
* is a big string with all the new-line delimited logs.
|
||||||
|
*/
|
||||||
|
async consume() {
|
||||||
|
const db = this.db;
|
||||||
|
|
||||||
export function useDownloadDebugLog() {
|
// Returns: a string representing the concatenated logs for this ID.
|
||||||
const [{ json }] = useContext(InspectorContext);
|
// Stops adding log fragments when the size exceeds maxSize
|
||||||
|
function fetchLogs(id, maxSize) {
|
||||||
|
const objectStore = db
|
||||||
|
.transaction("logs", "readonly")
|
||||||
|
.objectStore("logs");
|
||||||
|
|
||||||
const downloadDebugLog = useCallback(() => {
|
return new Promise((resolve, reject) => {
|
||||||
const blob = new Blob([JSON.stringify(json)], { type: "application/json" });
|
const query = objectStore
|
||||||
const url = URL.createObjectURL(blob);
|
.index("id")
|
||||||
const el = document.createElement("a");
|
.openCursor(IDBKeyRange.only(id), "prev");
|
||||||
el.href = url;
|
let lines = "";
|
||||||
el.download = "groupcall.json";
|
query.onerror = (event) => {
|
||||||
el.style.display = "none";
|
reject(new Error("Query failed: " + event.target.errorCode));
|
||||||
document.body.appendChild(el);
|
};
|
||||||
el.click();
|
query.onsuccess = (event) => {
|
||||||
setTimeout(() => {
|
const cursor = event.target.result;
|
||||||
URL.revokeObjectURL(url);
|
if (!cursor) {
|
||||||
el.parentNode.removeChild(el);
|
resolve(lines);
|
||||||
}, 0);
|
return; // end of results
|
||||||
}, [json]);
|
}
|
||||||
|
lines = cursor.value.lines + lines;
|
||||||
return downloadDebugLog;
|
if (lines.length >= maxSize) {
|
||||||
}
|
resolve(lines);
|
||||||
|
} else {
|
||||||
export function useRageshakeRequest() {
|
cursor.continue();
|
||||||
const { client } = useClient();
|
}
|
||||||
|
};
|
||||||
const sendRageshakeRequest = useCallback(
|
|
||||||
(roomId, rageshakeRequestId) => {
|
|
||||||
client.sendEvent(roomId, "org.matrix.rageshake_request", {
|
|
||||||
request_id: rageshakeRequestId,
|
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
[client]
|
|
||||||
);
|
|
||||||
|
|
||||||
return sendRageshakeRequest;
|
// Returns: A sorted array of log IDs. (newest first)
|
||||||
}
|
function fetchLogIds() {
|
||||||
|
// To gather all the log IDs, query for all records in logslastmod.
|
||||||
|
const o = db
|
||||||
|
.transaction("logslastmod", "readonly")
|
||||||
|
.objectStore("logslastmod");
|
||||||
|
return selectQuery(o, undefined, (cursor) => {
|
||||||
|
return {
|
||||||
|
id: cursor.value.id,
|
||||||
|
ts: cursor.value.ts,
|
||||||
|
};
|
||||||
|
}).then((res) => {
|
||||||
|
// Sort IDs by timestamp (newest first)
|
||||||
|
return res
|
||||||
|
.sort((a, b) => {
|
||||||
|
return b.ts - a.ts;
|
||||||
|
})
|
||||||
|
.map((a) => a.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useRageshakeRequestModal(roomId) {
|
function deleteLogs(id) {
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
return new Promise((resolve, reject) => {
|
||||||
const { client } = useClient();
|
const txn = db.transaction(["logs", "logslastmod"], "readwrite");
|
||||||
const [rageshakeRequestId, setRageshakeRequestId] = useState();
|
const o = txn.objectStore("logs");
|
||||||
|
// only load the key path, not the data which may be huge
|
||||||
|
const query = o.index("id").openKeyCursor(IDBKeyRange.only(id));
|
||||||
|
query.onsuccess = (event) => {
|
||||||
|
const cursor = event.target.result;
|
||||||
|
if (!cursor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
o.delete(cursor.primaryKey);
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
txn.oncomplete = () => {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
txn.onerror = (event) => {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
"Failed to delete logs for " +
|
||||||
|
`'${id}' : ${event.target.errorCode}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
// delete last modified entries
|
||||||
|
const lastModStore = txn.objectStore("logslastmod");
|
||||||
|
lastModStore.delete(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const allLogIds = await fetchLogIds();
|
||||||
const onEvent = (event) => {
|
let removeLogIds = [];
|
||||||
const type = event.getType();
|
const logs = [];
|
||||||
|
let size = 0;
|
||||||
|
for (let i = 0; i < allLogIds.length; i++) {
|
||||||
|
const lines = await fetchLogs(allLogIds[i], MAX_LOG_SIZE - size);
|
||||||
|
|
||||||
if (
|
// always add the log file: fetchLogs will truncate once the maxSize we give it is
|
||||||
type === "org.matrix.rageshake_request" &&
|
// exceeded, so we'll go over the max but only by one fragment's worth.
|
||||||
roomId === event.getRoomId() &&
|
logs.push({
|
||||||
client.getUserId() !== event.getSender()
|
lines: lines,
|
||||||
) {
|
id: allLogIds[i],
|
||||||
setRageshakeRequestId(event.getContent().request_id);
|
});
|
||||||
modalState.open();
|
size += lines.length;
|
||||||
|
|
||||||
|
// If fetchLogs truncated we'll now be at or over the size limit,
|
||||||
|
// in which case we should stop and remove the rest of the log files.
|
||||||
|
if (size >= MAX_LOG_SIZE) {
|
||||||
|
// the remaining log IDs should be removed. If we go out of
|
||||||
|
// bounds this is just []
|
||||||
|
removeLogIds = allLogIds.slice(i + 1);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (removeLogIds.length > 0) {
|
||||||
|
logger.log("Removing logs: ", removeLogIds);
|
||||||
|
// Don't await this because it's non-fatal if we can't clean up
|
||||||
|
// logs.
|
||||||
|
Promise.all(removeLogIds.map((id) => deleteLogs(id))).then(
|
||||||
|
() => {
|
||||||
|
logger.log(`Removed ${removeLogIds.length} old logs.`);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateLogEntry(lines) {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
lines: lines,
|
||||||
|
index: this.index++,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
client.on("event", onEvent);
|
generateLastModifiedTime() {
|
||||||
|
return {
|
||||||
return () => {
|
id: this.id,
|
||||||
client.removeListener("event", onEvent);
|
ts: Date.now(),
|
||||||
};
|
};
|
||||||
}, [modalState.open, roomId]);
|
}
|
||||||
|
}
|
||||||
return { modalState, modalProps: { ...modalProps, rageshakeRequestId } };
|
|
||||||
|
/**
|
||||||
|
* Helper method to collect results from a Cursor and promiseify it.
|
||||||
|
* @param {ObjectStore|Index} store The store to perform openCursor on.
|
||||||
|
* @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor.
|
||||||
|
* @param {Function} resultMapper A function which is repeatedly called with a
|
||||||
|
* Cursor.
|
||||||
|
* Return the data you want to keep.
|
||||||
|
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
|
||||||
|
* resultMapper.
|
||||||
|
*/
|
||||||
|
function selectQuery(store, keyRange, resultMapper) {
|
||||||
|
const query = store.openCursor(keyRange);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const results = [];
|
||||||
|
query.onerror = (event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
reject(new Error("Query failed: " + event.target.errorCode));
|
||||||
|
};
|
||||||
|
// collect results
|
||||||
|
query.onsuccess = (event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const cursor = event.target.result;
|
||||||
|
if (!cursor) {
|
||||||
|
resolve(results);
|
||||||
|
return; // end of results
|
||||||
|
}
|
||||||
|
results.push(resultMapper(cursor));
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure rage shaking support for sending bug reports.
|
||||||
|
* Modifies globals.
|
||||||
|
* @param {boolean} setUpPersistence When true (default), the persistence will
|
||||||
|
* be set up immediately for the logs.
|
||||||
|
* @return {Promise} Resolves when set up.
|
||||||
|
*/
|
||||||
|
export function init(setUpPersistence = true) {
|
||||||
|
if (global.mx_rage_initPromise) {
|
||||||
|
return global.mx_rage_initPromise;
|
||||||
|
}
|
||||||
|
global.mx_rage_logger = new ConsoleLogger();
|
||||||
|
global.mx_rage_logger.monkeyPatch(window.console);
|
||||||
|
|
||||||
|
if (setUpPersistence) {
|
||||||
|
return tryInitStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
global.mx_rage_initPromise = Promise.resolve();
|
||||||
|
return global.mx_rage_initPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to start up the rageshake storage for logs. If not possible (client unsupported)
|
||||||
|
* then this no-ops.
|
||||||
|
* @return {Promise} Resolves when complete.
|
||||||
|
*/
|
||||||
|
export function tryInitStorage() {
|
||||||
|
if (global.mx_rage_initStoragePromise) {
|
||||||
|
return global.mx_rage_initStoragePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("Configuring rageshake persistence...");
|
||||||
|
|
||||||
|
// just *accessing* indexedDB throws an exception in firefox with
|
||||||
|
// indexeddb disabled.
|
||||||
|
let indexedDB;
|
||||||
|
try {
|
||||||
|
indexedDB = window.indexedDB;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (indexedDB) {
|
||||||
|
global.mx_rage_store = new IndexedDBLogStore(
|
||||||
|
indexedDB,
|
||||||
|
global.mx_rage_logger
|
||||||
|
);
|
||||||
|
global.mx_rage_initStoragePromise = global.mx_rage_store.connect();
|
||||||
|
return global.mx_rage_initStoragePromise;
|
||||||
|
}
|
||||||
|
global.mx_rage_initStoragePromise = Promise.resolve();
|
||||||
|
return global.mx_rage_initStoragePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flush() {
|
||||||
|
if (!global.mx_rage_store) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
global.mx_rage_store.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old logs.
|
||||||
|
* @return {Promise} Resolves if cleaned logs.
|
||||||
|
*/
|
||||||
|
export async function cleanup() {
|
||||||
|
if (!global.mx_rage_store) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await global.mx_rage_store.consume();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a recent snapshot of the logs, ready for attaching to a bug report
|
||||||
|
*
|
||||||
|
* @return {Array<{lines: string, id, string}>} list of log data
|
||||||
|
*/
|
||||||
|
export async function getLogsForReport() {
|
||||||
|
if (!global.mx_rage_logger) {
|
||||||
|
throw new Error("No console logger, did you forget to call init()?");
|
||||||
|
}
|
||||||
|
// If in incognito mode, store is null, but we still want bug report
|
||||||
|
// sending to work going off the in-memory console logs.
|
||||||
|
if (global.mx_rage_store) {
|
||||||
|
// flush most recent logs
|
||||||
|
await global.mx_rage_store.flush();
|
||||||
|
return await global.mx_rage_store.consume();
|
||||||
|
} else {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
lines: global.mx_rage_logger.flush(true),
|
||||||
|
id: "-",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
300
src/settings/submit-rageshake.js
Normal file
300
src/settings/submit-rageshake.js
Normal file
|
@ -0,0 +1,300 @@
|
||||||
|
import { useCallback, useContext, useEffect, useState } from "react";
|
||||||
|
import { getLogsForReport } from "./rageshake";
|
||||||
|
import pako from "pako";
|
||||||
|
import { useClient } from "../ClientContext";
|
||||||
|
import { InspectorContext } from "../room/GroupCallInspector";
|
||||||
|
import { useModalTriggerState } from "../Modal";
|
||||||
|
|
||||||
|
export function useSubmitRageshake() {
|
||||||
|
const { client } = useClient();
|
||||||
|
const [{ json }] = useContext(InspectorContext);
|
||||||
|
|
||||||
|
const [{ sending, sent, error }, setState] = useState({
|
||||||
|
sending: false,
|
||||||
|
sent: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const submitRageshake = useCallback(
|
||||||
|
async (opts) => {
|
||||||
|
if (sending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState({ sending: true, sent: false, error: null });
|
||||||
|
|
||||||
|
let userAgent = "UNKNOWN";
|
||||||
|
if (window.navigator && window.navigator.userAgent) {
|
||||||
|
userAgent = window.navigator.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
let touchInput = "UNKNOWN";
|
||||||
|
try {
|
||||||
|
// MDN claims broad support across browsers
|
||||||
|
touchInput = String(window.matchMedia("(pointer: coarse)").matches);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const body = new FormData();
|
||||||
|
body.append(
|
||||||
|
"text",
|
||||||
|
opts.description || "User did not supply any additional text."
|
||||||
|
);
|
||||||
|
body.append("app", "matrix-video-chat");
|
||||||
|
body.append("version", import.meta.env.VITE_APP_VERSION || "dev");
|
||||||
|
body.append("user_agent", userAgent);
|
||||||
|
body.append("installed_pwa", false);
|
||||||
|
body.append("touch_input", touchInput);
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
const userId = client.getUserId();
|
||||||
|
const user = client.getUser(userId);
|
||||||
|
body.append("display_name", user?.displayName);
|
||||||
|
body.append("user_id", client.credentials.userId);
|
||||||
|
body.append("device_id", client.deviceId);
|
||||||
|
|
||||||
|
if (opts.roomId) {
|
||||||
|
body.append("room_id", opts.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.isCryptoEnabled()) {
|
||||||
|
const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
|
||||||
|
if (client.getDeviceCurve25519Key) {
|
||||||
|
keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
|
||||||
|
}
|
||||||
|
body.append("device_keys", keys.join(", "));
|
||||||
|
body.append("cross_signing_key", client.getCrossSigningId());
|
||||||
|
|
||||||
|
// add cross-signing status information
|
||||||
|
const crossSigning = client.crypto.crossSigningInfo;
|
||||||
|
const secretStorage = client.crypto.secretStorage;
|
||||||
|
|
||||||
|
body.append(
|
||||||
|
"cross_signing_ready",
|
||||||
|
String(await client.isCrossSigningReady())
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"cross_signing_supported_by_hs",
|
||||||
|
String(
|
||||||
|
await client.doesServerSupportUnstableFeature(
|
||||||
|
"org.matrix.e2e_cross_signing"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
body.append("cross_signing_key", crossSigning.getId());
|
||||||
|
body.append(
|
||||||
|
"cross_signing_privkey_in_secret_storage",
|
||||||
|
String(
|
||||||
|
!!(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const pkCache = client.getCrossSigningCacheCallbacks();
|
||||||
|
body.append(
|
||||||
|
"cross_signing_master_privkey_cached",
|
||||||
|
String(
|
||||||
|
!!(pkCache && (await pkCache.getCrossSigningKeyCache("master")))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"cross_signing_self_signing_privkey_cached",
|
||||||
|
String(
|
||||||
|
!!(
|
||||||
|
pkCache &&
|
||||||
|
(await pkCache.getCrossSigningKeyCache("self_signing"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"cross_signing_user_signing_privkey_cached",
|
||||||
|
String(
|
||||||
|
!!(
|
||||||
|
pkCache &&
|
||||||
|
(await pkCache.getCrossSigningKeyCache("user_signing"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
body.append(
|
||||||
|
"secret_storage_ready",
|
||||||
|
String(await client.isSecretStorageReady())
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"secret_storage_key_in_account",
|
||||||
|
String(!!(await secretStorage.hasKey()))
|
||||||
|
);
|
||||||
|
|
||||||
|
body.append(
|
||||||
|
"session_backup_key_in_secret_storage",
|
||||||
|
String(!!(await client.isKeyBackupKeyStored()))
|
||||||
|
);
|
||||||
|
const sessionBackupKeyFromCache =
|
||||||
|
await client.crypto.getSessionBackupPrivateKey();
|
||||||
|
body.append(
|
||||||
|
"session_backup_key_cached",
|
||||||
|
String(!!sessionBackupKeyFromCache)
|
||||||
|
);
|
||||||
|
body.append(
|
||||||
|
"session_backup_key_well_formed",
|
||||||
|
String(sessionBackupKeyFromCache instanceof Uint8Array)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.label) {
|
||||||
|
body.append("label", opts.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add storage persistence/quota information
|
||||||
|
if (navigator.storage && navigator.storage.persisted) {
|
||||||
|
try {
|
||||||
|
body.append(
|
||||||
|
"storageManager_persisted",
|
||||||
|
String(await navigator.storage.persisted())
|
||||||
|
);
|
||||||
|
} catch (e) {}
|
||||||
|
} else if (document.hasStorageAccess) {
|
||||||
|
// Safari
|
||||||
|
try {
|
||||||
|
body.append(
|
||||||
|
"storageManager_persisted",
|
||||||
|
String(await document.hasStorageAccess())
|
||||||
|
);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (navigator.storage && navigator.storage.estimate) {
|
||||||
|
try {
|
||||||
|
const estimate = await navigator.storage.estimate();
|
||||||
|
body.append("storageManager_quota", String(estimate.quota));
|
||||||
|
body.append("storageManager_usage", String(estimate.usage));
|
||||||
|
if (estimate.usageDetails) {
|
||||||
|
Object.keys(estimate.usageDetails).forEach((k) => {
|
||||||
|
body.append(
|
||||||
|
`storageManager_usage_${k}`,
|
||||||
|
String(estimate.usageDetails[k])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.sendLogs) {
|
||||||
|
const logs = await getLogsForReport();
|
||||||
|
|
||||||
|
for (const entry of logs) {
|
||||||
|
// encode as UTF-8
|
||||||
|
let buf = new TextEncoder().encode(entry.lines);
|
||||||
|
|
||||||
|
// compress
|
||||||
|
buf = pako.gzip(buf);
|
||||||
|
|
||||||
|
body.append("compressed-log", new Blob([buf]), entry.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json) {
|
||||||
|
body.append(
|
||||||
|
"file",
|
||||||
|
new Blob([JSON.stringify(json)], { type: "text/plain" }),
|
||||||
|
"groupcall.txt"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.rageshakeRequestId) {
|
||||||
|
body.append(
|
||||||
|
"group_call_rageshake_request_id",
|
||||||
|
opts.rageshakeRequestId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetch(
|
||||||
|
import.meta.env.VITE_RAGESHAKE_SUBMIT_URL ||
|
||||||
|
"https://element.io/bugreports/submit",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setState({ sending: false, sent: true, error: null });
|
||||||
|
} catch (error) {
|
||||||
|
setState({ sending: false, sent: false, error });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
submitRageshake,
|
||||||
|
sending,
|
||||||
|
sent,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDownloadDebugLog() {
|
||||||
|
const [{ json }] = useContext(InspectorContext);
|
||||||
|
|
||||||
|
const downloadDebugLog = useCallback(() => {
|
||||||
|
const blob = new Blob([JSON.stringify(json)], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const el = document.createElement("a");
|
||||||
|
el.href = url;
|
||||||
|
el.download = "groupcall.json";
|
||||||
|
el.style.display = "none";
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.click();
|
||||||
|
setTimeout(() => {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
}, 0);
|
||||||
|
}, [json]);
|
||||||
|
|
||||||
|
return downloadDebugLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRageshakeRequest() {
|
||||||
|
const { client } = useClient();
|
||||||
|
|
||||||
|
const sendRageshakeRequest = useCallback(
|
||||||
|
(roomId, rageshakeRequestId) => {
|
||||||
|
client.sendEvent(roomId, "org.matrix.rageshake_request", {
|
||||||
|
request_id: rageshakeRequestId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[client]
|
||||||
|
);
|
||||||
|
|
||||||
|
return sendRageshakeRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRageshakeRequestModal(roomId) {
|
||||||
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
const { client } = useClient();
|
||||||
|
const [rageshakeRequestId, setRageshakeRequestId] = useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onEvent = (event) => {
|
||||||
|
const type = event.getType();
|
||||||
|
|
||||||
|
if (
|
||||||
|
type === "org.matrix.rageshake_request" &&
|
||||||
|
roomId === event.getRoomId() &&
|
||||||
|
client.getUserId() !== event.getSender()
|
||||||
|
) {
|
||||||
|
setRageshakeRequestId(event.getContent().request_id);
|
||||||
|
modalState.open();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
client.on("event", onEvent);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
client.removeListener("event", onEvent);
|
||||||
|
};
|
||||||
|
}, [modalState.open, roomId]);
|
||||||
|
|
||||||
|
return { modalState, modalProps: { ...modalProps, rageshakeRequestId } };
|
||||||
|
}
|
1017
src/video-grid/VideoGrid.jsx
Normal file
1017
src/video-grid/VideoGrid.jsx
Normal file
File diff suppressed because it is too large
Load diff
7
src/video-grid/VideoGrid.module.css
Normal file
7
src/video-grid/VideoGrid.module.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.videoGrid {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import VideoGrid, {
|
import { VideoGrid, useVideoGridLayout } from "./VideoGrid";
|
||||||
useVideoGridLayout,
|
import { VideoTile } from "./VideoTile";
|
||||||
} from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoGrid";
|
|
||||||
import VideoTile from "matrix-react-sdk/src/components/views/voip/GroupCallView/VideoTile";
|
|
||||||
import "matrix-react-sdk/res/css/views/voip/GroupCallView/_VideoGrid.scss";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
|
|
||||||
|
|
54
src/video-grid/VideoTile.jsx
Normal file
54
src/video-grid/VideoTile.jsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import React from "react";
|
||||||
|
import { animated } from "@react-spring/web";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import styles from "./VideoTile.module.css";
|
||||||
|
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
|
||||||
|
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
|
||||||
|
|
||||||
|
export function VideoTile({
|
||||||
|
className,
|
||||||
|
isLocal,
|
||||||
|
speaking,
|
||||||
|
audioMuted,
|
||||||
|
noVideo,
|
||||||
|
videoMuted,
|
||||||
|
screenshare,
|
||||||
|
avatar,
|
||||||
|
name,
|
||||||
|
showName,
|
||||||
|
mediaRef,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<animated.div
|
||||||
|
className={classNames(styles.videoTile, className, {
|
||||||
|
[styles.isLocal]: isLocal,
|
||||||
|
[styles.speaking]: speaking,
|
||||||
|
[styles.muted]: audioMuted,
|
||||||
|
[styles.screenshare]: screenshare,
|
||||||
|
})}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{(videoMuted || noVideo) && (
|
||||||
|
<>
|
||||||
|
<div className={styles.videoMutedOverlay} />
|
||||||
|
{avatar}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{screenshare ? (
|
||||||
|
<div className={styles.presenterLabel}>
|
||||||
|
<span>{`${name} is presenting`}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
(showName || audioMuted || (videoMuted && !noVideo)) && (
|
||||||
|
<div className={styles.memberName}>
|
||||||
|
{audioMuted && !(videoMuted && !noVideo) && <MicMutedIcon />}
|
||||||
|
{videoMuted && !noVideo && <VideoMutedIcon />}
|
||||||
|
{showName && <span title={name}>{name}</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<video ref={mediaRef} playsInline disablePictureInPicture />
|
||||||
|
</animated.div>
|
||||||
|
);
|
||||||
|
}
|
113
src/video-grid/VideoTile.module.css
Normal file
113
src/video-grid/VideoTile.module.css
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
.videoTile {
|
||||||
|
position: absolute;
|
||||||
|
will-change: transform, width, height, opacity, box-shadow;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTile * {
|
||||||
|
touch-action: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTile video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTile.isLocal:not(.screenshare) video {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTile.speaking::after {
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
left: -1px;
|
||||||
|
right: -1px;
|
||||||
|
bottom: -1px;
|
||||||
|
content: "";
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: inset 0 0 0 4px #0dbd8b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTile.screenshare > video {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memberName {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 16px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 8px;
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(23, 25, 28, 0.85);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
user-select: none;
|
||||||
|
max-width: calc(100% - 48px);
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memberName > * {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memberName > :last-child {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memberName span {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoMutedAvatar {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoMutedOverlay {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #21262C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presenterLabel {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: #17191C;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screensharePIP {
|
||||||
|
bottom: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 25%;
|
||||||
|
max-width: 360px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
49
src/video-grid/VideoTileContainer.jsx
Normal file
49
src/video-grid/VideoTileContainer.jsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
|
||||||
|
import React from "react";
|
||||||
|
import { useCallFeed } from "./useCallFeed";
|
||||||
|
import { useMediaStream } from "./useMediaStream";
|
||||||
|
import { useRoomMemberName } from "./useRoomMemberName";
|
||||||
|
import { VideoTile } from "./VideoTile";
|
||||||
|
|
||||||
|
export function VideoTileContainer({
|
||||||
|
item,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
getAvatar,
|
||||||
|
showName,
|
||||||
|
audioOutputDevice,
|
||||||
|
disableSpeakingIndicator,
|
||||||
|
...rest
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
isLocal,
|
||||||
|
audioMuted,
|
||||||
|
videoMuted,
|
||||||
|
noVideo,
|
||||||
|
speaking,
|
||||||
|
stream,
|
||||||
|
purpose,
|
||||||
|
member,
|
||||||
|
} = useCallFeed(item.callFeed);
|
||||||
|
const { rawDisplayName } = useRoomMemberName(member);
|
||||||
|
const mediaRef = useMediaStream(stream, audioOutputDevice, isLocal);
|
||||||
|
|
||||||
|
// Firefox doesn't respect the disablePictureInPicture attribute
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1611831
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoTile
|
||||||
|
isLocal={isLocal}
|
||||||
|
speaking={speaking && !disableSpeakingIndicator}
|
||||||
|
audioMuted={audioMuted}
|
||||||
|
noVideo={noVideo}
|
||||||
|
videoMuted={videoMuted}
|
||||||
|
screenshare={purpose === SDPStreamMetadataPurpose.Screenshare}
|
||||||
|
name={rawDisplayName}
|
||||||
|
showName={showName}
|
||||||
|
mediaRef={mediaRef}
|
||||||
|
avatar={getAvatar && getAvatar(member, width, height)}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
56
src/video-grid/useCallFeed.js
Normal file
56
src/video-grid/useCallFeed.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { CallFeedEvent } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
|
|
||||||
|
function getCallFeedState(callFeed) {
|
||||||
|
return {
|
||||||
|
member: callFeed ? callFeed.getMember() : null,
|
||||||
|
isLocal: callFeed ? callFeed.isLocal() : false,
|
||||||
|
speaking: callFeed ? callFeed.isSpeaking() : false,
|
||||||
|
noVideo: callFeed
|
||||||
|
? !callFeed.stream || callFeed.stream.getVideoTracks().length === 0
|
||||||
|
: true,
|
||||||
|
videoMuted: callFeed ? callFeed.isVideoMuted() : true,
|
||||||
|
audioMuted: callFeed ? callFeed.isAudioMuted() : true,
|
||||||
|
stream: callFeed ? callFeed.stream : undefined,
|
||||||
|
purpose: callFeed ? callFeed.purpose : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCallFeed(callFeed) {
|
||||||
|
const [state, setState] = useState(() => getCallFeedState(callFeed));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onSpeaking(speaking) {
|
||||||
|
setState((prevState) => ({ ...prevState, speaking }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMuteStateChanged(audioMuted, videoMuted) {
|
||||||
|
setState((prevState) => ({ ...prevState, audioMuted, videoMuted }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpdateCallFeed() {
|
||||||
|
setState(getCallFeedState(callFeed));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callFeed) {
|
||||||
|
callFeed.on(CallFeedEvent.Speaking, onSpeaking);
|
||||||
|
callFeed.on(CallFeedEvent.MuteStateChanged, onMuteStateChanged);
|
||||||
|
callFeed.on(CallFeedEvent.NewStream, onUpdateCallFeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdateCallFeed();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (callFeed) {
|
||||||
|
callFeed.removeListener(CallFeedEvent.Speaking, onSpeaking);
|
||||||
|
callFeed.removeListener(
|
||||||
|
CallFeedEvent.MuteStateChanged,
|
||||||
|
onMuteStateChanged
|
||||||
|
);
|
||||||
|
callFeed.removeListener(CallFeedEvent.NewStream, onUpdateCallFeed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [callFeed]);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
48
src/video-grid/useMediaStream.js
Normal file
48
src/video-grid/useMediaStream.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { useRef, useEffect } from "react";
|
||||||
|
|
||||||
|
export function useMediaStream(stream, audioOutputDevice, mute = false) {
|
||||||
|
const mediaRef = useRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(
|
||||||
|
`useMediaStream update stream mediaRef.current ${!!mediaRef.current} stream ${
|
||||||
|
stream && stream.id
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mediaRef.current) {
|
||||||
|
if (stream) {
|
||||||
|
mediaRef.current.muted = mute;
|
||||||
|
mediaRef.current.srcObject = stream;
|
||||||
|
mediaRef.current.play();
|
||||||
|
} else {
|
||||||
|
mediaRef.current.srcObject = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [stream, mute]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
mediaRef.current &&
|
||||||
|
audioOutputDevice &&
|
||||||
|
mediaRef.current !== undefined
|
||||||
|
) {
|
||||||
|
console.log(`useMediaStream setSinkId ${audioOutputDevice}`);
|
||||||
|
mediaRef.current.setSinkId(audioOutputDevice);
|
||||||
|
}
|
||||||
|
}, [audioOutputDevice]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaEl = mediaRef.current;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (mediaEl) {
|
||||||
|
// Ensure we set srcObject to null before unmounting to prevent memory leak
|
||||||
|
// https://webrtchacks.com/srcobject-intervention/
|
||||||
|
mediaEl.srcObject = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return mediaRef;
|
||||||
|
}
|
24
src/video-grid/useRoomMemberName.js
Normal file
24
src/video-grid/useRoomMemberName.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export function useRoomMemberName(member) {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
name: member.name,
|
||||||
|
rawDisplayName: member.rawDisplayName,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function updateName() {
|
||||||
|
setState({ name: member.name, rawDisplayName: member.rawDisplayName });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateName();
|
||||||
|
|
||||||
|
member.on("RoomMember.name", updateName);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
member.removeListener("RoomMember.name", updateName);
|
||||||
|
};
|
||||||
|
}, [member]);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
|
@ -36,16 +36,8 @@ export default defineConfig(({ mode }) => {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/_matrix": env.VITE_DEFAULT_HOMESERVER || "http://localhost:8008",
|
"/_matrix": env.VITE_DEFAULT_HOMESERVER || "http://localhost:8008",
|
||||||
},
|
},
|
||||||
fs: {
|
|
||||||
// Current we're bundling files linked in from matrix-react-sdk
|
|
||||||
// We should re-enable this if we plan to run Vite outside the dev server mode
|
|
||||||
strict: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
|
||||||
"$(res)": path.resolve(__dirname, "node_modules/matrix-react-sdk/res"),
|
|
||||||
},
|
|
||||||
dedupe: [
|
dedupe: [
|
||||||
"react",
|
"react",
|
||||||
"react-dom",
|
"react-dom",
|
||||||
|
|
Loading…
Add table
Reference in a new issue