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
		Add a link
		
	
		Reference in a new issue