Settings improvements
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
		
					parent
					
						
							
								515e00b763
							
						
					
				
			
			
				commit
				
					
						0269753f59
					
				
			
		
					 15 changed files with 380 additions and 470 deletions
				
			
		
							
								
								
									
										57
									
								
								src/App.tsx
									
										
									
									
									
								
							
							
						
						
									
										57
									
								
								src/App.tsx
									
										
									
									
									
								
							|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2021 New Vector Ltd | Copyright 2021 - 2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -30,6 +30,7 @@ import { SequenceDiagramViewerPage } from "./SequenceDiagramViewerPage"; | ||||||
| import { InspectorContextProvider } from "./room/GroupCallInspector"; | import { InspectorContextProvider } from "./room/GroupCallInspector"; | ||||||
| import { CrashView, LoadingView } from "./FullScreenView"; | import { CrashView, LoadingView } from "./FullScreenView"; | ||||||
| import { Initializer } from "./initializer"; | import { Initializer } from "./initializer"; | ||||||
|  | import { MediaHandlerProvider } from "./settings/useMediaHandler"; | ||||||
| 
 | 
 | ||||||
| const SentryRoute = Sentry.withSentryRouting(Route); | const SentryRoute = Sentry.withSentryRouting(Route); | ||||||
| 
 | 
 | ||||||
|  | @ -55,32 +56,34 @@ export default function App({ history }: AppProps) { | ||||||
|       {loaded ? ( |       {loaded ? ( | ||||||
|         <Suspense fallback={null}> |         <Suspense fallback={null}> | ||||||
|           <ClientProvider> |           <ClientProvider> | ||||||
|             <InspectorContextProvider> |             <MediaHandlerProvider> | ||||||
|               <Sentry.ErrorBoundary fallback={errorPage}> |               <InspectorContextProvider> | ||||||
|                 <OverlayProvider> |                 <Sentry.ErrorBoundary fallback={errorPage}> | ||||||
|                   <Switch> |                   <OverlayProvider> | ||||||
|                     <SentryRoute exact path="/"> |                     <Switch> | ||||||
|                       <HomePage /> |                       <SentryRoute exact path="/"> | ||||||
|                     </SentryRoute> |                         <HomePage /> | ||||||
|                     <SentryRoute exact path="/login"> |                       </SentryRoute> | ||||||
|                       <LoginPage /> |                       <SentryRoute exact path="/login"> | ||||||
|                     </SentryRoute> |                         <LoginPage /> | ||||||
|                     <SentryRoute exact path="/register"> |                       </SentryRoute> | ||||||
|                       <RegisterPage /> |                       <SentryRoute exact path="/register"> | ||||||
|                     </SentryRoute> |                         <RegisterPage /> | ||||||
|                     <SentryRoute path="/room/:roomId?"> |                       </SentryRoute> | ||||||
|                       <RoomPage /> |                       <SentryRoute path="/room/:roomId?"> | ||||||
|                     </SentryRoute> |                         <RoomPage /> | ||||||
|                     <SentryRoute path="/inspector"> |                       </SentryRoute> | ||||||
|                       <SequenceDiagramViewerPage /> |                       <SentryRoute path="/inspector"> | ||||||
|                     </SentryRoute> |                         <SequenceDiagramViewerPage /> | ||||||
|                     <SentryRoute path="*"> |                       </SentryRoute> | ||||||
|                       <RoomRedirect /> |                       <SentryRoute path="*"> | ||||||
|                     </SentryRoute> |                         <RoomRedirect /> | ||||||
|                   </Switch> |                       </SentryRoute> | ||||||
|                 </OverlayProvider> |                     </Switch> | ||||||
|               </Sentry.ErrorBoundary> |                   </OverlayProvider> | ||||||
|             </InspectorContextProvider> |                 </Sentry.ErrorBoundary> | ||||||
|  |               </InspectorContextProvider> | ||||||
|  |             </MediaHandlerProvider> | ||||||
|           </ClientProvider> |           </ClientProvider> | ||||||
|         </Suspense> |         </Suspense> | ||||||
|       ) : ( |       ) : ( | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2022 New Vector Ltd | Copyright 2022 - 2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -25,6 +25,7 @@ import { Menu } from "./Menu"; | ||||||
| import { TooltipTrigger } from "./Tooltip"; | import { TooltipTrigger } from "./Tooltip"; | ||||||
| import { Avatar, Size } from "./Avatar"; | import { Avatar, Size } from "./Avatar"; | ||||||
| import { ReactComponent as UserIcon } from "./icons/User.svg"; | import { ReactComponent as UserIcon } from "./icons/User.svg"; | ||||||
|  | import { ReactComponent as SettingsIcon } from "./icons/Settings.svg"; | ||||||
| import { ReactComponent as LoginIcon } from "./icons/Login.svg"; | import { ReactComponent as LoginIcon } from "./icons/Login.svg"; | ||||||
| import { ReactComponent as LogoutIcon } from "./icons/Logout.svg"; | import { ReactComponent as LogoutIcon } from "./icons/Logout.svg"; | ||||||
| import { Body } from "./typography/Typography"; | import { Body } from "./typography/Typography"; | ||||||
|  | @ -59,6 +60,11 @@ export function UserMenu({ | ||||||
|         icon: UserIcon, |         icon: UserIcon, | ||||||
|         label: displayName, |         label: displayName, | ||||||
|       }); |       }); | ||||||
|  |       arr.push({ | ||||||
|  |         key: "settings", | ||||||
|  |         icon: SettingsIcon, | ||||||
|  |         label: t("Settings"), | ||||||
|  |       }); | ||||||
| 
 | 
 | ||||||
|       if (isPasswordlessUser && !preventNavigation) { |       if (isPasswordlessUser && !preventNavigation) { | ||||||
|         arr.push({ |         arr.push({ | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2022 New Vector Ltd | Copyright 2022 - 2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -14,13 +14,13 @@ See the License for the specific language governing permissions and | ||||||
| limitations under the License. | limitations under the License. | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| import React, { useCallback } from "react"; | import React, { useCallback, useState } from "react"; | ||||||
| import { useHistory, useLocation } from "react-router-dom"; | import { useHistory, useLocation } from "react-router-dom"; | ||||||
| 
 | 
 | ||||||
| import { useClient } from "./ClientContext"; | import { useClient } from "./ClientContext"; | ||||||
| import { useProfile } from "./profile/useProfile"; | import { useProfile } from "./profile/useProfile"; | ||||||
| import { useModalTriggerState } from "./Modal"; | import { useModalTriggerState } from "./Modal"; | ||||||
| import { ProfileModal } from "./profile/ProfileModal"; | import { SettingsModal } from "./settings/SettingsModal"; | ||||||
| import { UserMenu } from "./UserMenu"; | import { UserMenu } from "./UserMenu"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|  | @ -35,10 +35,17 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { | ||||||
|   const { displayName, avatarUrl } = useProfile(client); |   const { displayName, avatarUrl } = useProfile(client); | ||||||
|   const { modalState, modalProps } = useModalTriggerState(); |   const { modalState, modalProps } = useModalTriggerState(); | ||||||
| 
 | 
 | ||||||
|  |   const [defaultSettingsTab, setDefaultSettingsTab] = useState<string>(); | ||||||
|  | 
 | ||||||
|   const onAction = useCallback( |   const onAction = useCallback( | ||||||
|     (value: string) => { |     async (value: string) => { | ||||||
|       switch (value) { |       switch (value) { | ||||||
|         case "user": |         case "user": | ||||||
|  |           setDefaultSettingsTab("profile"); | ||||||
|  |           modalState.open(); | ||||||
|  |           break; | ||||||
|  |         case "settings": | ||||||
|  |           setDefaultSettingsTab("audio"); | ||||||
|           modalState.open(); |           modalState.open(); | ||||||
|           break; |           break; | ||||||
|         case "logout": |         case "logout": | ||||||
|  | @ -64,7 +71,13 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { | ||||||
|           displayName || (userName ? userName.replace("@", "") : undefined) |           displayName || (userName ? userName.replace("@", "") : undefined) | ||||||
|         } |         } | ||||||
|       /> |       /> | ||||||
|       {modalState.isOpen && <ProfileModal client={client} {...modalProps} />} |       {modalState.isOpen && ( | ||||||
|  |         <SettingsModal | ||||||
|  |           client={client} | ||||||
|  |           defaultTab={defaultSettingsTab} | ||||||
|  |           {...modalProps} | ||||||
|  |         /> | ||||||
|  |       )} | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2022 New Vector Ltd | Copyright 2022 - 2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -246,9 +246,11 @@ export function SettingsButton({ | ||||||
| 
 | 
 | ||||||
| export function InviteButton({ | export function InviteButton({ | ||||||
|   className, |   className, | ||||||
|  |   variant = "toolbar", | ||||||
|   ...rest |   ...rest | ||||||
| }: { | }: { | ||||||
|   className?: string; |   className?: string; | ||||||
|  |   variant?: string; | ||||||
|   // TODO: add all props for <Button>
 |   // TODO: add all props for <Button>
 | ||||||
|   [index: string]: unknown; |   [index: string]: unknown; | ||||||
| }) { | }) { | ||||||
|  | @ -257,7 +259,7 @@ export function InviteButton({ | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <TooltipTrigger tooltip={tooltip}> |     <TooltipTrigger tooltip={tooltip}> | ||||||
|       <Button variant="toolbar" {...rest}> |       <Button variant={variant} {...rest}> | ||||||
|         <AddUserIcon /> |         <AddUserIcon /> | ||||||
|       </Button> |       </Button> | ||||||
|     </TooltipTrigger> |     </TooltipTrigger> | ||||||
|  |  | ||||||
|  | @ -1,114 +0,0 @@ | ||||||
| /* |  | ||||||
| Copyright 2022 New Vector Ltd |  | ||||||
| 
 |  | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
| you may not use this file except in compliance with the License. |  | ||||||
| You may obtain a copy of the License at |  | ||||||
| 
 |  | ||||||
|     http://www.apache.org/licenses/LICENSE-2.0
 |  | ||||||
| 
 |  | ||||||
| Unless required by applicable law or agreed to in writing, software |  | ||||||
| distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
| See the License for the specific language governing permissions and |  | ||||||
| limitations under the License. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| import React, { useCallback, useEffect } from "react"; |  | ||||||
| import { randomString } from "matrix-js-sdk/src/randomstring"; |  | ||||||
| import { useTranslation } from "react-i18next"; |  | ||||||
| 
 |  | ||||||
| import { Modal, ModalContent } from "../Modal"; |  | ||||||
| import { Button } from "../button"; |  | ||||||
| import { FieldRow, InputField, ErrorMessage } from "../input/Input"; |  | ||||||
| import { |  | ||||||
|   useSubmitRageshake, |  | ||||||
|   useRageshakeRequest, |  | ||||||
| } from "../settings/submit-rageshake"; |  | ||||||
| import { Body } from "../typography/Typography"; |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   inCall: boolean; |  | ||||||
|   roomId: string; |  | ||||||
|   onClose?: () => void; |  | ||||||
|   // TODO: add all props for for <Modal>
 |  | ||||||
|   [index: string]: unknown; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
|   const { submitRageshake, sending, sent, error } = useSubmitRageshake(); |  | ||||||
|   const sendRageshakeRequest = useRageshakeRequest(); |  | ||||||
| 
 |  | ||||||
|   const onSubmitFeedback = useCallback( |  | ||||||
|     (e) => { |  | ||||||
|       e.preventDefault(); |  | ||||||
|       const data = new FormData(e.target); |  | ||||||
|       const descriptionData = data.get("description"); |  | ||||||
|       const description = |  | ||||||
|         typeof descriptionData === "string" ? descriptionData : ""; |  | ||||||
|       const sendLogs = Boolean(data.get("sendLogs")); |  | ||||||
|       const rageshakeRequestId = randomString(16); |  | ||||||
| 
 |  | ||||||
|       submitRageshake({ |  | ||||||
|         description, |  | ||||||
|         sendLogs, |  | ||||||
|         rageshakeRequestId, |  | ||||||
|         roomId, |  | ||||||
|       }); |  | ||||||
| 
 |  | ||||||
|       if (inCall && sendLogs) { |  | ||||||
|         sendRageshakeRequest(roomId, rageshakeRequestId); |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     [inCall, submitRageshake, roomId, sendRageshakeRequest] |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   useEffect(() => { |  | ||||||
|     if (sent) { |  | ||||||
|       onClose(); |  | ||||||
|     } |  | ||||||
|   }, [sent, onClose]); |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <Modal |  | ||||||
|       title={t("Submit feedback")} |  | ||||||
|       isDismissable |  | ||||||
|       onClose={onClose} |  | ||||||
|       {...rest} |  | ||||||
|     > |  | ||||||
|       <ModalContent> |  | ||||||
|         <Body>{t("Having trouble? Help us fix it.")}</Body> |  | ||||||
|         <form onSubmit={onSubmitFeedback}> |  | ||||||
|           <FieldRow> |  | ||||||
|             <InputField |  | ||||||
|               id="description" |  | ||||||
|               name="description" |  | ||||||
|               label={t("Description (optional)")} |  | ||||||
|               type="textarea" |  | ||||||
|             /> |  | ||||||
|           </FieldRow> |  | ||||||
|           <FieldRow> |  | ||||||
|             <InputField |  | ||||||
|               id="sendLogs" |  | ||||||
|               name="sendLogs" |  | ||||||
|               label={t("Include debug logs")} |  | ||||||
|               type="checkbox" |  | ||||||
|               defaultChecked |  | ||||||
|             /> |  | ||||||
|           </FieldRow> |  | ||||||
|           {error && ( |  | ||||||
|             <FieldRow> |  | ||||||
|               <ErrorMessage error={error} /> |  | ||||||
|             </FieldRow> |  | ||||||
|           )} |  | ||||||
|           <FieldRow> |  | ||||||
|             <Button type="submit" disabled={sending}> |  | ||||||
|               {sending ? t("Submitting feedback…") : t("Submit feedback")} |  | ||||||
|             </Button> |  | ||||||
|           </FieldRow> |  | ||||||
|         </form> |  | ||||||
|       </ModalContent> |  | ||||||
|     </Modal> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2022 New Vector Ltd | Copyright 2022 - 2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -24,6 +24,7 @@ import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; | ||||||
| import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed"; | import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed"; | ||||||
| import classNames from "classnames"; | import classNames from "classnames"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
|  | import { OverlayTriggerState } from "@react-stately/overlays"; | ||||||
| import { JoinRule } from "matrix-js-sdk/src/@types/partials"; | import { JoinRule } from "matrix-js-sdk/src/@types/partials"; | ||||||
| 
 | 
 | ||||||
| import type { IWidgetApiRequest } from "matrix-widget-api"; | import type { IWidgetApiRequest } from "matrix-widget-api"; | ||||||
|  | @ -33,6 +34,8 @@ import { | ||||||
|   MicButton, |   MicButton, | ||||||
|   VideoButton, |   VideoButton, | ||||||
|   ScreenshareButton, |   ScreenshareButton, | ||||||
|  |   SettingsButton, | ||||||
|  |   InviteButton, | ||||||
| } from "../button"; | } from "../button"; | ||||||
| import { | import { | ||||||
|   Header, |   Header, | ||||||
|  | @ -48,12 +51,8 @@ import { | ||||||
| } from "../video-grid/VideoGrid"; | } from "../video-grid/VideoGrid"; | ||||||
| import { VideoTileContainer } from "../video-grid/VideoTileContainer"; | import { VideoTileContainer } from "../video-grid/VideoTileContainer"; | ||||||
| import { GroupCallInspector } from "./GroupCallInspector"; | import { GroupCallInspector } from "./GroupCallInspector"; | ||||||
| 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 { useRageshakeRequestModal } from "../settings/submit-rageshake"; |  | ||||||
| import { RageshakeRequestModal } from "./RageshakeRequestModal"; |  | ||||||
| import { useMediaHandler } from "../settings/useMediaHandler"; | import { useMediaHandler } from "../settings/useMediaHandler"; | ||||||
| import { | import { | ||||||
|   useNewGrid, |   useNewGrid, | ||||||
|  | @ -74,6 +73,8 @@ import { AudioSink } from "../video-grid/AudioSink"; | ||||||
| import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; | import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; | ||||||
| import { NewVideoGrid } from "../video-grid/NewVideoGrid"; | import { NewVideoGrid } from "../video-grid/NewVideoGrid"; | ||||||
| import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; | import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; | ||||||
|  | import { SettingsModal } from "../settings/SettingsModal"; | ||||||
|  | import { InviteModal } from "./InviteModal"; | ||||||
| 
 | 
 | ||||||
| const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); | const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); | ||||||
| // There is currently a bug in Safari our our code with cloning and sending MediaStreams
 | // There is currently a bug in Safari our our code with cloning and sending MediaStreams
 | ||||||
|  | @ -128,7 +129,6 @@ export function InCallView({ | ||||||
| }: Props) { | }: Props) { | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   usePreventScroll(); |   usePreventScroll(); | ||||||
|   const joinRule = useJoinRule(groupCall.room); |  | ||||||
| 
 | 
 | ||||||
|   const containerRef1 = useRef<HTMLDivElement | null>(null); |   const containerRef1 = useRef<HTMLDivElement | null>(null); | ||||||
|   const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver }); |   const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver }); | ||||||
|  | @ -151,11 +151,10 @@ export function InCallView({ | ||||||
|   const [audioContext, audioDestination] = useAudioContext(); |   const [audioContext, audioDestination] = useAudioContext(); | ||||||
|   const [showInspector] = useShowInspector(); |   const [showInspector] = useShowInspector(); | ||||||
| 
 | 
 | ||||||
|   const { modalState: feedbackModalState, modalProps: feedbackModalProps } = |  | ||||||
|     useModalTriggerState(); |  | ||||||
| 
 |  | ||||||
|   const { hideScreensharing } = useUrlParams(); |   const { hideScreensharing } = useUrlParams(); | ||||||
| 
 | 
 | ||||||
|  |   const joinRule = useJoinRule(groupCall.room); | ||||||
|  | 
 | ||||||
|   useCallViewKeyboardShortcuts( |   useCallViewKeyboardShortcuts( | ||||||
|     containerRef1, |     containerRef1, | ||||||
|     toggleMicrophoneMuted, |     toggleMicrophoneMuted, | ||||||
|  | @ -342,9 +341,34 @@ export function InCallView({ | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const { |   const { | ||||||
|     modalState: rageshakeRequestModalState, |     modalState: settingsModalState, | ||||||
|     modalProps: rageshakeRequestModalProps, |     modalProps: settingsModalProps, | ||||||
|   } = useRageshakeRequestModal(groupCall.room.roomId); |   }: { | ||||||
|  |     modalState: OverlayTriggerState; | ||||||
|  |     modalProps: { | ||||||
|  |       isOpen: boolean; | ||||||
|  |       onClose: () => void; | ||||||
|  |     }; | ||||||
|  |   } = useModalTriggerState(); | ||||||
|  | 
 | ||||||
|  |   const openSettings = useCallback(() => { | ||||||
|  |     settingsModalState.open(); | ||||||
|  |   }, [settingsModalState]); | ||||||
|  | 
 | ||||||
|  |   const { | ||||||
|  |     modalState: inviteModalState, | ||||||
|  |     modalProps: inviteModalProps, | ||||||
|  |   }: { | ||||||
|  |     modalState: OverlayTriggerState; | ||||||
|  |     modalProps: { | ||||||
|  |       isOpen: boolean; | ||||||
|  |       onClose: () => void; | ||||||
|  |     }; | ||||||
|  |   } = useModalTriggerState(); | ||||||
|  | 
 | ||||||
|  |   const openInvite = useCallback(() => { | ||||||
|  |     inviteModalState.open(); | ||||||
|  |   }, [inviteModalState]); | ||||||
| 
 | 
 | ||||||
|   const containerClasses = classNames(styles.inRoom, { |   const containerClasses = classNames(styles.inRoom, { | ||||||
|     [styles.maximised]: maximisedParticipant, |     [styles.maximised]: maximisedParticipant, | ||||||
|  | @ -402,17 +426,7 @@ export function InCallView({ | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|       if (!maximisedParticipant) { |       if (!maximisedParticipant) { | ||||||
|         buttons.push( |         buttons.push(<SettingsButton key="4" onPress={openSettings} />); | ||||||
|           <OverflowMenu |  | ||||||
|             key="4" |  | ||||||
|             inCall |  | ||||||
|             roomIdOrAlias={roomIdOrAlias} |  | ||||||
|             groupCall={groupCall} |  | ||||||
|             showInvite={joinRule === JoinRule.Public} |  | ||||||
|             feedbackModalState={feedbackModalState} |  | ||||||
|             feedbackModalProps={feedbackModalProps} |  | ||||||
|           /> |  | ||||||
|         ); |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -434,7 +448,9 @@ export function InCallView({ | ||||||
|           </LeftNav> |           </LeftNav> | ||||||
|           <RightNav> |           <RightNav> | ||||||
|             <GridLayoutMenu layout={layout} setLayout={setLayout} /> |             <GridLayoutMenu layout={layout} setLayout={setLayout} /> | ||||||
|             <UserMenuContainer preventNavigation /> |             {joinRule === JoinRule.Public && ( | ||||||
|  |               <InviteButton variant="icon" onClick={openInvite} /> | ||||||
|  |             )} | ||||||
|           </RightNav> |           </RightNav> | ||||||
|         </Header> |         </Header> | ||||||
|       )} |       )} | ||||||
|  | @ -448,12 +464,16 @@ export function InCallView({ | ||||||
|         otelGroupCallMembership={otelGroupCallMembership} |         otelGroupCallMembership={otelGroupCallMembership} | ||||||
|         show={showInspector} |         show={showInspector} | ||||||
|       /> |       /> | ||||||
|       {rageshakeRequestModalState.isOpen && ( |       {settingsModalState.isOpen && ( | ||||||
|         <RageshakeRequestModal |         <SettingsModal | ||||||
|           {...rageshakeRequestModalProps} |           client={client} | ||||||
|           roomIdOrAlias={roomIdOrAlias} |           roomId={roomIdOrAlias} | ||||||
|  |           {...settingsModalProps} | ||||||
|         /> |         /> | ||||||
|       )} |       )} | ||||||
|  |       {inviteModalState.isOpen && ( | ||||||
|  |         <InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} /> | ||||||
|  |       )} | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,143 +0,0 @@ | ||||||
| /* |  | ||||||
| Copyright 2022 New Vector Ltd |  | ||||||
| 
 |  | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
| you may not use this file except in compliance with the License. |  | ||||||
| You may obtain a copy of the License at |  | ||||||
| 
 |  | ||||||
|     http://www.apache.org/licenses/LICENSE-2.0
 |  | ||||||
| 
 |  | ||||||
| Unless required by applicable law or agreed to in writing, software |  | ||||||
| distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
| See the License for the specific language governing permissions and |  | ||||||
| limitations under the License. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| import React, { useCallback } from "react"; |  | ||||||
| import { Item } from "@react-stately/collections"; |  | ||||||
| import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall"; |  | ||||||
| import { OverlayTriggerState } from "@react-stately/overlays"; |  | ||||||
| import { useTranslation } from "react-i18next"; |  | ||||||
| 
 |  | ||||||
| import { Button } from "../button"; |  | ||||||
| import { Menu } from "../Menu"; |  | ||||||
| import { PopoverMenuTrigger } from "../popover/PopoverMenu"; |  | ||||||
| import { ReactComponent as SettingsIcon } from "../icons/Settings.svg"; |  | ||||||
| import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg"; |  | ||||||
| import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg"; |  | ||||||
| import { ReactComponent as FeedbackIcon } from "../icons/Feedback.svg"; |  | ||||||
| import { useModalTriggerState } from "../Modal"; |  | ||||||
| import { SettingsModal } from "../settings/SettingsModal"; |  | ||||||
| import { InviteModal } from "./InviteModal"; |  | ||||||
| import { TooltipTrigger } from "../Tooltip"; |  | ||||||
| import { FeedbackModal } from "./FeedbackModal"; |  | ||||||
| import { Config } from "../config/Config"; |  | ||||||
| 
 |  | ||||||
| interface Props { |  | ||||||
|   roomIdOrAlias: string; |  | ||||||
|   inCall: boolean; |  | ||||||
|   groupCall: GroupCall; |  | ||||||
|   showInvite: boolean; |  | ||||||
|   feedbackModalState: OverlayTriggerState; |  | ||||||
|   feedbackModalProps: { |  | ||||||
|     isOpen: boolean; |  | ||||||
|     onClose: () => void; |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function OverflowMenu({ |  | ||||||
|   roomIdOrAlias, |  | ||||||
|   inCall, |  | ||||||
|   groupCall, |  | ||||||
|   showInvite, |  | ||||||
|   feedbackModalState, |  | ||||||
|   feedbackModalProps, |  | ||||||
| }: Props) { |  | ||||||
|   const { t } = useTranslation(); |  | ||||||
| 
 |  | ||||||
|   const { |  | ||||||
|     modalState: inviteModalState, |  | ||||||
|     modalProps: inviteModalProps, |  | ||||||
|   }: { |  | ||||||
|     modalState: OverlayTriggerState; |  | ||||||
|     modalProps: { |  | ||||||
|       isOpen: boolean; |  | ||||||
|       onClose: () => void; |  | ||||||
|     }; |  | ||||||
|   } = useModalTriggerState(); |  | ||||||
|   const { |  | ||||||
|     modalState: settingsModalState, |  | ||||||
|     modalProps: settingsModalProps, |  | ||||||
|   }: { |  | ||||||
|     modalState: OverlayTriggerState; |  | ||||||
|     modalProps: { |  | ||||||
|       isOpen: boolean; |  | ||||||
|       onClose: () => void; |  | ||||||
|     }; |  | ||||||
|   } = useModalTriggerState(); |  | ||||||
| 
 |  | ||||||
|   // TODO: On closing modal, focus should be restored to the trigger button
 |  | ||||||
|   // https://github.com/adobe/react-spectrum/issues/2444
 |  | ||||||
|   const onAction = useCallback( |  | ||||||
|     (key) => { |  | ||||||
|       switch (key) { |  | ||||||
|         case "invite": |  | ||||||
|           inviteModalState.open(); |  | ||||||
|           break; |  | ||||||
|         case "settings": |  | ||||||
|           settingsModalState.open(); |  | ||||||
|           break; |  | ||||||
|         case "feedback": |  | ||||||
|           feedbackModalState.open(); |  | ||||||
|           break; |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     [feedbackModalState, inviteModalState, settingsModalState] |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   const tooltip = useCallback(() => t("More"), [t]); |  | ||||||
| 
 |  | ||||||
|   return ( |  | ||||||
|     <> |  | ||||||
|       <PopoverMenuTrigger disableOnState> |  | ||||||
|         <TooltipTrigger tooltip={tooltip} placement="top"> |  | ||||||
|           <Button variant="toolbar"> |  | ||||||
|             <OverflowIcon /> |  | ||||||
|           </Button> |  | ||||||
|         </TooltipTrigger> |  | ||||||
|         {(props: JSX.IntrinsicAttributes) => ( |  | ||||||
|           <Menu {...props} label={t("More menu")} onAction={onAction}> |  | ||||||
|             {showInvite && ( |  | ||||||
|               <Item key="invite" textValue={t("Invite people")}> |  | ||||||
|                 <AddUserIcon /> |  | ||||||
|                 <span>{t("Invite people")}</span> |  | ||||||
|               </Item> |  | ||||||
|             )} |  | ||||||
|             <Item key="settings" textValue={t("Settings")}> |  | ||||||
|               <SettingsIcon /> |  | ||||||
|               <span>{t("Settings")}</span> |  | ||||||
|             </Item> |  | ||||||
|             {Config.get().rageshake?.submit_url && ( |  | ||||||
|               <Item key="feedback" textValue={t("Submit feedback")}> |  | ||||||
|                 <FeedbackIcon /> |  | ||||||
|                 <span>{t("Submit feedback")}</span> |  | ||||||
|               </Item> |  | ||||||
|             )} |  | ||||||
|           </Menu> |  | ||||||
|         )} |  | ||||||
|       </PopoverMenuTrigger> |  | ||||||
|       {settingsModalState.isOpen && <SettingsModal {...settingsModalProps} />} |  | ||||||
|       {inviteModalState.isOpen && ( |  | ||||||
|         <InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} /> |  | ||||||
|       )} |  | ||||||
|       {feedbackModalState.isOpen && ( |  | ||||||
|         <FeedbackModal |  | ||||||
|           {...feedbackModalProps} |  | ||||||
|           roomId={groupCall?.room.roomId} |  | ||||||
|           inCall={inCall} |  | ||||||
|         /> |  | ||||||
|       )} |  | ||||||
|     </> |  | ||||||
|   ); |  | ||||||
| } |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2022 New Vector Ltd | Copyright 2022 - 2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -27,7 +27,7 @@ import { useTranslation } from "react-i18next"; | ||||||
| import { useDelayedState } from "../useDelayedState"; | import { useDelayedState } from "../useDelayedState"; | ||||||
| import { useModalTriggerState } from "../Modal"; | import { useModalTriggerState } from "../Modal"; | ||||||
| import { InviteModal } from "./InviteModal"; | import { InviteModal } from "./InviteModal"; | ||||||
| import { HangupButton, InviteButton } from "../button"; | import { HangupButton, InviteButton, SettingsButton } from "../button"; | ||||||
| import { Header, LeftNav, RightNav, RoomSetupHeaderInfo } from "../Header"; | import { Header, LeftNav, RightNav, RoomSetupHeaderInfo } from "../Header"; | ||||||
| import styles from "./PTTCallView.module.css"; | import styles from "./PTTCallView.module.css"; | ||||||
| import { Facepile } from "../Facepile"; | import { Facepile } from "../Facepile"; | ||||||
|  | @ -41,10 +41,10 @@ import { ReactComponent as AudioIcon } from "../icons/Audio.svg"; | ||||||
| import { usePTTSounds } from "../sound/usePttSounds"; | import { usePTTSounds } from "../sound/usePttSounds"; | ||||||
| import { PTTClips } from "../sound/PTTClips"; | import { PTTClips } from "../sound/PTTClips"; | ||||||
| import { GroupCallInspector } from "./GroupCallInspector"; | import { GroupCallInspector } from "./GroupCallInspector"; | ||||||
| import { OverflowMenu } from "./OverflowMenu"; |  | ||||||
| import { Size } from "../Avatar"; | import { Size } from "../Avatar"; | ||||||
| import { ParticipantInfo } from "./useGroupCall"; | import { ParticipantInfo } from "./useGroupCall"; | ||||||
| import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; | import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; | ||||||
|  | import { SettingsModal } from "../settings/SettingsModal"; | ||||||
| 
 | 
 | ||||||
| function getPromptText( | function getPromptText( | ||||||
|   networkWaiting: boolean, |   networkWaiting: boolean, | ||||||
|  | @ -126,8 +126,9 @@ export const PTTCallView: React.FC<Props> = ({ | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { modalState: inviteModalState, modalProps: inviteModalProps } = |   const { modalState: inviteModalState, modalProps: inviteModalProps } = | ||||||
|     useModalTriggerState(); |     useModalTriggerState(); | ||||||
|   const { modalState: feedbackModalState, modalProps: feedbackModalProps } = |   const { modalState: settingsModalState, modalProps: settingsModalProps } = | ||||||
|     useModalTriggerState(); |     useModalTriggerState(); | ||||||
|  | 
 | ||||||
|   const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver }); |   const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver }); | ||||||
|   const facepileSize = bounds.width < 800 ? Size.SM : Size.MD; |   const facepileSize = bounds.width < 800 ? Size.SM : Size.MD; | ||||||
|   const showControls = bounds.height > 500; |   const showControls = bounds.height > 500; | ||||||
|  | @ -232,14 +233,7 @@ export const PTTCallView: React.FC<Props> = ({ | ||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|           <div className={styles.footer}> |           <div className={styles.footer}> | ||||||
|             <OverflowMenu |             <SettingsButton onPress={() => settingsModalState.open()} /> | ||||||
|               inCall |  | ||||||
|               roomIdOrAlias={roomIdOrAlias} |  | ||||||
|               groupCall={groupCall} |  | ||||||
|               showInvite={false} |  | ||||||
|               feedbackModalState={feedbackModalState} |  | ||||||
|               feedbackModalProps={feedbackModalProps} |  | ||||||
|             /> |  | ||||||
|             {!isEmbedded && <HangupButton onPress={onLeave} />} |             {!isEmbedded && <HangupButton onPress={onLeave} />} | ||||||
|             <InviteButton onPress={() => inviteModalState.open()} /> |             <InviteButton onPress={() => inviteModalState.open()} /> | ||||||
|           </div> |           </div> | ||||||
|  | @ -265,7 +259,7 @@ export const PTTCallView: React.FC<Props> = ({ | ||||||
|               <div className={styles.talkingInfo} /> |               <div className={styles.talkingInfo} /> | ||||||
|             ))} |             ))} | ||||||
|           <PTTButton |           <PTTButton | ||||||
|             enabled={!feedbackModalState.isOpen} |             enabled={!inviteModalState.isOpen && !settingsModalState.isOpen} | ||||||
|             showTalkOverError={showTalkOverError} |             showTalkOverError={showTalkOverError} | ||||||
|             activeSpeakerUserId={activeSpeakerUserId} |             activeSpeakerUserId={activeSpeakerUserId} | ||||||
|             activeSpeakerDisplayName={activeSpeakerDisplayName} |             activeSpeakerDisplayName={activeSpeakerDisplayName} | ||||||
|  | @ -312,6 +306,13 @@ export const PTTCallView: React.FC<Props> = ({ | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|  |       {settingsModalState.isOpen && ( | ||||||
|  |         <SettingsModal | ||||||
|  |           client={client} | ||||||
|  |           roomId={roomIdOrAlias} | ||||||
|  |           {...settingsModalProps} | ||||||
|  |         /> | ||||||
|  |       )} | ||||||
|       {inviteModalState.isOpen && showControls && ( |       {inviteModalState.isOpen && showControls && ( | ||||||
|         <InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} /> |         <InviteModal roomIdOrAlias={roomIdOrAlias} {...inviteModalProps} /> | ||||||
|       )} |       )} | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2021-2022 New Vector Ltd | Copyright 2021-2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -24,7 +24,6 @@ import { RoomAuthView } from "./RoomAuthView"; | ||||||
| import { GroupCallLoader } from "./GroupCallLoader"; | import { GroupCallLoader } from "./GroupCallLoader"; | ||||||
| import { GroupCallView } from "./GroupCallView"; | import { GroupCallView } from "./GroupCallView"; | ||||||
| import { useUrlParams } from "../UrlParams"; | import { useUrlParams } from "../UrlParams"; | ||||||
| import { MediaHandlerProvider } from "../settings/useMediaHandler"; |  | ||||||
| import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; | import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; | ||||||
| import { translatedError } from "../TranslatedError"; | import { translatedError } from "../TranslatedError"; | ||||||
| import { useOptInAnalytics } from "../settings/useSetting"; | import { useOptInAnalytics } from "../settings/useSetting"; | ||||||
|  | @ -101,15 +100,13 @@ export const RoomPage: FC = () => { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <MediaHandlerProvider client={client}> |     <GroupCallLoader | ||||||
|       <GroupCallLoader |       client={client} | ||||||
|         client={client} |       roomIdOrAlias={roomIdOrAlias} | ||||||
|         roomIdOrAlias={roomIdOrAlias} |       viaServers={viaServers} | ||||||
|         viaServers={viaServers} |       createPtt={isPtt} | ||||||
|         createPtt={isPtt} |     > | ||||||
|       > |       {groupCallView} | ||||||
|         {groupCallView} |     </GroupCallLoader> | ||||||
|       </GroupCallLoader> |  | ||||||
|     </MediaHandlerProvider> |  | ||||||
|   ); |   ); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2022 New Vector Ltd | Copyright 2022 - 2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -14,21 +14,22 @@ See the License for the specific language governing permissions and | ||||||
| limitations under the License. | limitations under the License. | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| import React from "react"; | import React, { useCallback } from "react"; | ||||||
| import useMeasure from "react-use-measure"; | import useMeasure from "react-use-measure"; | ||||||
| import { ResizeObserver } from "@juggle/resize-observer"; | import { ResizeObserver } from "@juggle/resize-observer"; | ||||||
| import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall"; | import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall"; | ||||||
| import { MatrixClient } from "matrix-js-sdk/src/client"; | import { MatrixClient } from "matrix-js-sdk/src/client"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
|  | import { OverlayTriggerState } from "@react-stately/overlays"; | ||||||
| 
 | 
 | ||||||
| import { MicButton, VideoButton } from "../button"; | import { MicButton, SettingsButton, VideoButton } from "../button"; | ||||||
| import { useMediaStream } from "../video-grid/useMediaStream"; | import { useMediaStream } from "../video-grid/useMediaStream"; | ||||||
| import { OverflowMenu } from "./OverflowMenu"; |  | ||||||
| import { Avatar } from "../Avatar"; | import { Avatar } from "../Avatar"; | ||||||
| import { useProfile } from "../profile/useProfile"; | import { useProfile } from "../profile/useProfile"; | ||||||
| import styles from "./VideoPreview.module.css"; | import styles from "./VideoPreview.module.css"; | ||||||
| import { Body } from "../typography/Typography"; | import { Body } from "../typography/Typography"; | ||||||
| import { useModalTriggerState } from "../Modal"; | import { useModalTriggerState } from "../Modal"; | ||||||
|  | import { SettingsModal } from "../settings/SettingsModal"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   client: MatrixClient; |   client: MatrixClient; | ||||||
|  | @ -59,8 +60,20 @@ export function VideoPreview({ | ||||||
|   const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver }); |   const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver }); | ||||||
|   const avatarSize = (previewBounds.height - 66) / 2; |   const avatarSize = (previewBounds.height - 66) / 2; | ||||||
| 
 | 
 | ||||||
|   const { modalState: feedbackModalState, modalProps: feedbackModalProps } = |   const { | ||||||
|     useModalTriggerState(); |     modalState: settingsModalState, | ||||||
|  |     modalProps: settingsModalProps, | ||||||
|  |   }: { | ||||||
|  |     modalState: OverlayTriggerState; | ||||||
|  |     modalProps: { | ||||||
|  |       isOpen: boolean; | ||||||
|  |       onClose: () => void; | ||||||
|  |     }; | ||||||
|  |   } = useModalTriggerState(); | ||||||
|  | 
 | ||||||
|  |   const openSettings = useCallback(() => { | ||||||
|  |     settingsModalState.open(); | ||||||
|  |   }, [settingsModalState]); | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className={styles.preview} ref={previewRef}> |     <div className={styles.preview} ref={previewRef}> | ||||||
|  | @ -95,17 +108,13 @@ export function VideoPreview({ | ||||||
|               muted={localVideoMuted} |               muted={localVideoMuted} | ||||||
|               onPress={toggleLocalVideoMuted} |               onPress={toggleLocalVideoMuted} | ||||||
|             /> |             /> | ||||||
|             <OverflowMenu |             <SettingsButton onPress={openSettings} /> | ||||||
|               roomIdOrAlias={roomIdOrAlias} |  | ||||||
|               feedbackModalState={feedbackModalState} |  | ||||||
|               feedbackModalProps={feedbackModalProps} |  | ||||||
|               inCall={false} |  | ||||||
|               groupCall={undefined} |  | ||||||
|               showInvite={false} |  | ||||||
|             /> |  | ||||||
|           </div> |           </div> | ||||||
|         </> |         </> | ||||||
|       )} |       )} | ||||||
|  |       {settingsModalState.isOpen && ( | ||||||
|  |         <SettingsModal client={client} {...settingsModalProps} /> | ||||||
|  |       )} | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										93
									
								
								src/settings/FeedbackSettingsTab.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/settings/FeedbackSettingsTab.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,93 @@ | ||||||
|  | /* | ||||||
|  | Copyright 2022 - 2023 New Vector Ltd | ||||||
|  | 
 | ||||||
|  | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  | you may not use this file except in compliance with the License. | ||||||
|  | You may obtain a copy of the License at | ||||||
|  | 
 | ||||||
|  |     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | 
 | ||||||
|  | Unless required by applicable law or agreed to in writing, software | ||||||
|  | distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  | See the License for the specific language governing permissions and | ||||||
|  | limitations under the License. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | import React, { useCallback } from "react"; | ||||||
|  | import { randomString } from "matrix-js-sdk/src/randomstring"; | ||||||
|  | import { useTranslation } from "react-i18next"; | ||||||
|  | 
 | ||||||
|  | import { Button } from "../button"; | ||||||
|  | import { FieldRow, InputField, ErrorMessage } from "../input/Input"; | ||||||
|  | import { useSubmitRageshake, useRageshakeRequest } from "./submit-rageshake"; | ||||||
|  | import { Body } from "../typography/Typography"; | ||||||
|  | 
 | ||||||
|  | interface Props { | ||||||
|  |   roomId?: string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function FeedbackSettingsTab({ roomId }: Props) { | ||||||
|  |   const { t } = useTranslation(); | ||||||
|  |   const { submitRageshake, sending, error } = useSubmitRageshake(); | ||||||
|  |   const sendRageshakeRequest = useRageshakeRequest(); | ||||||
|  | 
 | ||||||
|  |   const onSubmitFeedback = useCallback( | ||||||
|  |     (e) => { | ||||||
|  |       e.preventDefault(); | ||||||
|  |       const data = new FormData(e.target); | ||||||
|  |       const descriptionData = data.get("description"); | ||||||
|  |       const description = | ||||||
|  |         typeof descriptionData === "string" ? descriptionData : ""; | ||||||
|  |       const sendLogs = Boolean(data.get("sendLogs")); | ||||||
|  |       const rageshakeRequestId = randomString(16); | ||||||
|  | 
 | ||||||
|  |       submitRageshake({ | ||||||
|  |         description, | ||||||
|  |         sendLogs, | ||||||
|  |         rageshakeRequestId, | ||||||
|  |         roomId, | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       if (roomId && sendLogs) { | ||||||
|  |         sendRageshakeRequest(roomId, rageshakeRequestId); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     [submitRageshake, roomId, sendRageshakeRequest] | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div> | ||||||
|  |       <Body>{t("Having trouble? Help us fix it.")}</Body> | ||||||
|  |       <form onSubmit={onSubmitFeedback}> | ||||||
|  |         <FieldRow> | ||||||
|  |           <InputField | ||||||
|  |             id="description" | ||||||
|  |             name="description" | ||||||
|  |             label={t("Description (optional)")} | ||||||
|  |             type="textarea" | ||||||
|  |           /> | ||||||
|  |         </FieldRow> | ||||||
|  |         <FieldRow> | ||||||
|  |           <InputField | ||||||
|  |             id="sendLogs" | ||||||
|  |             name="sendLogs" | ||||||
|  |             label={t("Include debug logs")} | ||||||
|  |             type="checkbox" | ||||||
|  |             defaultChecked | ||||||
|  |           /> | ||||||
|  |         </FieldRow> | ||||||
|  |         {error && ( | ||||||
|  |           <FieldRow> | ||||||
|  |             <ErrorMessage error={error} /> | ||||||
|  |           </FieldRow> | ||||||
|  |         )} | ||||||
|  |         <FieldRow> | ||||||
|  |           <Button type="submit" disabled={sending}> | ||||||
|  |             {sending ? t("Submitting feedback…") : t("Submit feedback")} | ||||||
|  |           </Button> | ||||||
|  |         </FieldRow> | ||||||
|  |       </form> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2022 New Vector Ltd | Copyright 2022 - 2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2022 New Vector Ltd | Copyright 2022 - 2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -14,27 +14,22 @@ See the License for the specific language governing permissions and | ||||||
| limitations under the License. | limitations under the License. | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| import React, { ChangeEvent, useCallback, useEffect, useState } from "react"; | import React, { ChangeEvent, useCallback, useState } from "react"; | ||||||
| import { MatrixClient } from "matrix-js-sdk/src/client"; | import { MatrixClient } from "matrix-js-sdk/src/client"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| 
 | 
 | ||||||
| import { Button } from "../button"; | import { Button } from "../button"; | ||||||
| import { useProfile } from "./useProfile"; | import { useProfile } from "../profile/useProfile"; | ||||||
| import { FieldRow, InputField, ErrorMessage } from "../input/Input"; | import { FieldRow, InputField, ErrorMessage } from "../input/Input"; | ||||||
| import { Modal, ModalContent } from "../Modal"; |  | ||||||
| import { AvatarInputField } from "../input/AvatarInputField"; | import { AvatarInputField } from "../input/AvatarInputField"; | ||||||
| import styles from "./ProfileModal.module.css"; | import styles from "./ProfileSettingsTab.module.css"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   client: MatrixClient; |   client: MatrixClient; | ||||||
|   onClose: () => void; |  | ||||||
|   [rest: string]: unknown; |  | ||||||
| } | } | ||||||
| export function ProfileModal({ client, ...rest }: Props) { | export function ProfileSettingsTab({ client }: Props) { | ||||||
|   const { onClose } = rest; |  | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const { |   const { | ||||||
|     success, |  | ||||||
|     error, |     error, | ||||||
|     loading, |     loading, | ||||||
|     displayName: initialDisplayName, |     displayName: initialDisplayName, | ||||||
|  | @ -78,64 +73,51 @@ export function ProfileModal({ client, ...rest }: Props) { | ||||||
|     [saveProfile, removeAvatar] |     [saveProfile, removeAvatar] | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |  | ||||||
|     if (success) { |  | ||||||
|       onClose(); |  | ||||||
|     } |  | ||||||
|   }, [success, onClose]); |  | ||||||
| 
 |  | ||||||
|   return ( |   return ( | ||||||
|     <Modal title={t("Profile")} isDismissable {...rest}> |     <form onSubmit={onSubmit}> | ||||||
|       <ModalContent> |       <FieldRow className={styles.avatarFieldRow}> | ||||||
|         <form onSubmit={onSubmit}> |         <AvatarInputField | ||||||
|           <FieldRow className={styles.avatarFieldRow}> |           id="avatar" | ||||||
|             <AvatarInputField |           name="avatar" | ||||||
|               id="avatar" |           label={t("Avatar")} | ||||||
|               name="avatar" |           avatarUrl={avatarUrl} | ||||||
|               label={t("Avatar")} |           displayName={displayName} | ||||||
|               avatarUrl={avatarUrl} |           onRemoveAvatar={onRemoveAvatar} | ||||||
|               displayName={displayName} |         /> | ||||||
|               onRemoveAvatar={onRemoveAvatar} |       </FieldRow> | ||||||
|             /> |       <FieldRow> | ||||||
|           </FieldRow> |         <InputField | ||||||
|           <FieldRow> |           id="userId" | ||||||
|             <InputField |           name="userId" | ||||||
|               id="userId" |           label={t("User ID")} | ||||||
|               name="userId" |           type="text" | ||||||
|               label={t("User ID")} |           disabled | ||||||
|               type="text" |           value={client.getUserId()} | ||||||
|               disabled |         /> | ||||||
|               value={client.getUserId()} |       </FieldRow> | ||||||
|             /> |       <FieldRow> | ||||||
|           </FieldRow> |         <InputField | ||||||
|           <FieldRow> |           id="displayName" | ||||||
|             <InputField |           name="displayName" | ||||||
|               id="displayName" |           label={t("Display name")} | ||||||
|               name="displayName" |           type="text" | ||||||
|               label={t("Display name")} |           required | ||||||
|               type="text" |           autoComplete="off" | ||||||
|               required |           placeholder={t("Display name")} | ||||||
|               autoComplete="off" |           value={displayName} | ||||||
|               placeholder={t("Display name")} |           onChange={onChangeDisplayName} | ||||||
|               value={displayName} |         /> | ||||||
|               onChange={onChangeDisplayName} |       </FieldRow> | ||||||
|             /> |       {error && ( | ||||||
|           </FieldRow> |         <FieldRow> | ||||||
|           {error && ( |           <ErrorMessage error={error} /> | ||||||
|             <FieldRow> |         </FieldRow> | ||||||
|               <ErrorMessage error={error} /> |       )} | ||||||
|             </FieldRow> |       <FieldRow rightAlign> | ||||||
|           )} |         <Button type="submit" disabled={loading}> | ||||||
|           <FieldRow rightAlign> |           {loading ? t("Saving…") : t("Save")} | ||||||
|             <Button type="button" variant="secondary" onPress={onClose}> |         </Button> | ||||||
|               Cancel |       </FieldRow> | ||||||
|             </Button> |     </form> | ||||||
|             <Button type="submit" disabled={loading}> |  | ||||||
|               {loading ? t("Saving…") : t("Save")} |  | ||||||
|             </Button> |  | ||||||
|           </FieldRow> |  | ||||||
|         </form> |  | ||||||
|       </ModalContent> |  | ||||||
|     </Modal> |  | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2022 New Vector Ltd | Copyright 2022 - 2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -14,9 +14,10 @@ See the License for the specific language governing permissions and | ||||||
| limitations under the License. | limitations under the License. | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| import React from "react"; | import React, { useCallback, useState } from "react"; | ||||||
| import { Item } from "@react-stately/collections"; | import { Item } from "@react-stately/collections"; | ||||||
| import { Trans, useTranslation } from "react-i18next"; | import { Trans, useTranslation } from "react-i18next"; | ||||||
|  | import { MatrixClient } from "matrix-js-sdk"; | ||||||
| 
 | 
 | ||||||
| import { Modal } from "../Modal"; | import { Modal } from "../Modal"; | ||||||
| import styles from "./SettingsModal.module.css"; | import styles from "./SettingsModal.module.css"; | ||||||
|  | @ -25,6 +26,8 @@ import { ReactComponent as AudioIcon } from "../icons/Audio.svg"; | ||||||
| import { ReactComponent as VideoIcon } from "../icons/Video.svg"; | import { ReactComponent as VideoIcon } from "../icons/Video.svg"; | ||||||
| import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg"; | import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg"; | ||||||
| import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg"; | import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg"; | ||||||
|  | import { ReactComponent as UserIcon } from "../icons/User.svg"; | ||||||
|  | import { ReactComponent as FeedbackIcon } from "../icons/Feedback.svg"; | ||||||
| import { SelectInput } from "../input/SelectInput"; | import { SelectInput } from "../input/SelectInput"; | ||||||
| import { useMediaHandler } from "./useMediaHandler"; | import { useMediaHandler } from "./useMediaHandler"; | ||||||
| import { | import { | ||||||
|  | @ -39,9 +42,14 @@ import { Button } from "../button"; | ||||||
| import { useDownloadDebugLog } from "./submit-rageshake"; | import { useDownloadDebugLog } from "./submit-rageshake"; | ||||||
| import { Body, Caption } from "../typography/Typography"; | import { Body, Caption } from "../typography/Typography"; | ||||||
| import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; | import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; | ||||||
|  | import { ProfileSettingsTab } from "./ProfileSettingsTab"; | ||||||
|  | import { FeedbackSettingsTab } from "./FeedbackSettingsTab"; | ||||||
| 
 | 
 | ||||||
| interface Props { | interface Props { | ||||||
|   isOpen: boolean; |   isOpen: boolean; | ||||||
|  |   client: MatrixClient; | ||||||
|  |   roomId?: string; | ||||||
|  |   defaultTab?: string; | ||||||
|   onClose: () => void; |   onClose: () => void; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -68,6 +76,15 @@ export const SettingsModal = (props: Props) => { | ||||||
| 
 | 
 | ||||||
|   const downloadDebugLog = useDownloadDebugLog(); |   const downloadDebugLog = useDownloadDebugLog(); | ||||||
| 
 | 
 | ||||||
|  |   const [selectedTab, setSelectedTab] = useState<string | undefined>(); | ||||||
|  | 
 | ||||||
|  |   const onSelectedTabChanged = useCallback( | ||||||
|  |     (tab) => { | ||||||
|  |       setSelectedTab(tab); | ||||||
|  |     }, | ||||||
|  |     [setSelectedTab] | ||||||
|  |   ); | ||||||
|  | 
 | ||||||
|   const optInDescription = ( |   const optInDescription = ( | ||||||
|     <Caption> |     <Caption> | ||||||
|       <Trans> |       <Trans> | ||||||
|  | @ -87,8 +104,13 @@ export const SettingsModal = (props: Props) => { | ||||||
|       className={styles.settingsModal} |       className={styles.settingsModal} | ||||||
|       {...props} |       {...props} | ||||||
|     > |     > | ||||||
|       <TabContainer className={styles.tabContainer}> |       <TabContainer | ||||||
|  |         onSelectionChange={onSelectedTabChanged} | ||||||
|  |         selectedKey={selectedTab ?? props.defaultTab ?? "audio"} | ||||||
|  |         className={styles.tabContainer} | ||||||
|  |       > | ||||||
|         <TabItem |         <TabItem | ||||||
|  |           key="audio" | ||||||
|           title={ |           title={ | ||||||
|             <> |             <> | ||||||
|               <AudioIcon width={16} height={16} /> |               <AudioIcon width={16} height={16} /> | ||||||
|  | @ -145,6 +167,7 @@ export const SettingsModal = (props: Props) => { | ||||||
|           </FieldRow> |           </FieldRow> | ||||||
|         </TabItem> |         </TabItem> | ||||||
|         <TabItem |         <TabItem | ||||||
|  |           key="video" | ||||||
|           title={ |           title={ | ||||||
|             <> |             <> | ||||||
|               <VideoIcon width={16} height={16} /> |               <VideoIcon width={16} height={16} /> | ||||||
|  | @ -167,6 +190,29 @@ export const SettingsModal = (props: Props) => { | ||||||
|           </SelectInput> |           </SelectInput> | ||||||
|         </TabItem> |         </TabItem> | ||||||
|         <TabItem |         <TabItem | ||||||
|  |           key="profile" | ||||||
|  |           title={ | ||||||
|  |             <> | ||||||
|  |               <UserIcon width={16} height={16} /> | ||||||
|  |               <span>{t("Profile")}</span> | ||||||
|  |             </> | ||||||
|  |           } | ||||||
|  |         > | ||||||
|  |           <ProfileSettingsTab client={props.client} /> | ||||||
|  |         </TabItem> | ||||||
|  |         <TabItem | ||||||
|  |           key="feedback" | ||||||
|  |           title={ | ||||||
|  |             <> | ||||||
|  |               <FeedbackIcon width={16} height={16} /> | ||||||
|  |               <span>{t("feedback")}</span> | ||||||
|  |             </> | ||||||
|  |           } | ||||||
|  |         > | ||||||
|  |           <FeedbackSettingsTab roomId={props.roomId} /> | ||||||
|  |         </TabItem> | ||||||
|  |         <TabItem | ||||||
|  |           key="more" | ||||||
|           title={ |           title={ | ||||||
|             <> |             <> | ||||||
|               <OverflowIcon width={16} height={16} /> |               <OverflowIcon width={16} height={16} /> | ||||||
|  | @ -174,18 +220,10 @@ export const SettingsModal = (props: Props) => { | ||||||
|             </> |             </> | ||||||
|           } |           } | ||||||
|         > |         > | ||||||
|           <h4>Analytics</h4> |           <h4>Developer</h4> | ||||||
|           <FieldRow> |           <p> | ||||||
|             <InputField |             Version: {(import.meta.env.VITE_APP_VERSION as string) || "dev"} | ||||||
|               id="optInAnalytics" |           </p> | ||||||
|               type="checkbox" |  | ||||||
|               checked={optInAnalytics} |  | ||||||
|               description={optInDescription} |  | ||||||
|               onChange={(event: React.ChangeEvent<HTMLInputElement>) => |  | ||||||
|                 setOptInAnalytics(event.target.checked) |  | ||||||
|               } |  | ||||||
|             /> |  | ||||||
|           </FieldRow> |  | ||||||
|           <FieldRow> |           <FieldRow> | ||||||
|             <InputField |             <InputField | ||||||
|               id="developerSettingsTab" |               id="developerSettingsTab" | ||||||
|  | @ -200,9 +238,22 @@ export const SettingsModal = (props: Props) => { | ||||||
|               } |               } | ||||||
|             /> |             /> | ||||||
|           </FieldRow> |           </FieldRow> | ||||||
|  |           <h4>Analytics</h4> | ||||||
|  |           <FieldRow> | ||||||
|  |             <InputField | ||||||
|  |               id="optInAnalytics" | ||||||
|  |               type="checkbox" | ||||||
|  |               checked={optInAnalytics} | ||||||
|  |               description={optInDescription} | ||||||
|  |               onChange={(event: React.ChangeEvent<HTMLInputElement>) => | ||||||
|  |                 setOptInAnalytics(event.target.checked) | ||||||
|  |               } | ||||||
|  |             /> | ||||||
|  |           </FieldRow> | ||||||
|         </TabItem> |         </TabItem> | ||||||
|         {developerSettingsTab && ( |         {developerSettingsTab && ( | ||||||
|           <TabItem |           <TabItem | ||||||
|  |             key="developer" | ||||||
|             title={ |             title={ | ||||||
|               <> |               <> | ||||||
|                 <DeveloperIcon width={16} height={16} /> |                 <DeveloperIcon width={16} height={16} /> | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| /* | /* | ||||||
| Copyright 2022 New Vector Ltd | Copyright 2022 - 2023 New Vector Ltd | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); | Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| you may not use this file except in compliance with the License. | you may not use this file except in compliance with the License. | ||||||
|  | @ -15,23 +15,7 @@ limitations under the License. | ||||||
| */ | */ | ||||||
| 
 | 
 | ||||||
| /* eslint-disable @typescript-eslint/ban-ts-comment */ | /* eslint-disable @typescript-eslint/ban-ts-comment */ | ||||||
| /* |  | ||||||
| Copyright 2022 New Vector Ltd |  | ||||||
| 
 | 
 | ||||||
| Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
| you may not use this file except in compliance with the License. |  | ||||||
| You may obtain a copy of the License at |  | ||||||
| 
 |  | ||||||
|     http://www.apache.org/licenses/LICENSE-2.0
 |  | ||||||
| 
 |  | ||||||
| Unless required by applicable law or agreed to in writing, software |  | ||||||
| distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
| See the License for the specific language governing permissions and |  | ||||||
| limitations under the License. |  | ||||||
| */ |  | ||||||
| 
 |  | ||||||
| import { MatrixClient } from "matrix-js-sdk/src/client"; |  | ||||||
| import { MediaHandlerEvent } from "matrix-js-sdk/src/webrtc/mediaHandler"; | import { MediaHandlerEvent } from "matrix-js-sdk/src/webrtc/mediaHandler"; | ||||||
| import React, { | import React, { | ||||||
|   useState, |   useState, | ||||||
|  | @ -43,6 +27,8 @@ import React, { | ||||||
|   ReactNode, |   ReactNode, | ||||||
| } from "react"; | } from "react"; | ||||||
| 
 | 
 | ||||||
|  | import { useClient } from "../ClientContext"; | ||||||
|  | 
 | ||||||
| export interface MediaHandlerContextInterface { | export interface MediaHandlerContextInterface { | ||||||
|   audioInput: string; |   audioInput: string; | ||||||
|   audioInputs: MediaDeviceInfo[]; |   audioInputs: MediaDeviceInfo[]; | ||||||
|  | @ -89,10 +75,10 @@ function updateMediaPreferences(newPreferences: MediaPreferences): void { | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| interface Props { | interface Props { | ||||||
|   client: MatrixClient; |  | ||||||
|   children: ReactNode; |   children: ReactNode; | ||||||
| } | } | ||||||
| export function MediaHandlerProvider({ client, children }: Props): JSX.Element { | export function MediaHandlerProvider({ children }: Props): JSX.Element { | ||||||
|  |   const { client } = useClient(); | ||||||
|   const [ |   const [ | ||||||
|     { |     { | ||||||
|       audioInput, |       audioInput, | ||||||
|  | @ -104,19 +90,21 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { | ||||||
|     }, |     }, | ||||||
|     setState, |     setState, | ||||||
|   ] = useState(() => { |   ] = useState(() => { | ||||||
|     const mediaPreferences = getMediaPreferences(); |     const mediaHandler = client?.getMediaHandler(); | ||||||
|     const mediaHandler = client.getMediaHandler(); |  | ||||||
| 
 | 
 | ||||||
|     mediaHandler.restoreMediaSettings( |     if (mediaHandler) { | ||||||
|       mediaPreferences?.audioInput, |       const mediaPreferences = getMediaPreferences(); | ||||||
|       mediaPreferences?.videoInput |       mediaHandler?.restoreMediaSettings( | ||||||
|     ); |         mediaPreferences?.audioInput, | ||||||
|  |         mediaPreferences?.videoInput | ||||||
|  |       ); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|       // @ts-ignore, ignore that audioInput is a private members of mediaHandler
 |       // @ts-ignore, ignore that audioInput is a private members of mediaHandler
 | ||||||
|       audioInput: mediaHandler.audioInput, |       audioInput: mediaHandler?.audioInput, | ||||||
|       // @ts-ignore, ignore that videoInput is a private members of mediaHandler
 |       // @ts-ignore, ignore that videoInput is a private members of mediaHandler
 | ||||||
|       videoInput: mediaHandler.videoInput, |       videoInput: mediaHandler?.videoInput, | ||||||
|       audioOutput: undefined, |       audioOutput: undefined, | ||||||
|       audioInputs: [], |       audioInputs: [], | ||||||
|       videoInputs: [], |       videoInputs: [], | ||||||
|  | @ -125,6 +113,8 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|  |     if (!client) return; | ||||||
|  | 
 | ||||||
|     const mediaHandler = client.getMediaHandler(); |     const mediaHandler = client.getMediaHandler(); | ||||||
| 
 | 
 | ||||||
|     function updateDevices(): void { |     function updateDevices(): void { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue