diff --git a/src/App.tsx b/src/App.tsx index 5fc0414..2ae27b7 100644 --- a/src/App.tsx +++ b/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"); 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 { CrashView, LoadingView } from "./FullScreenView"; import { Initializer } from "./initializer"; +import { MediaHandlerProvider } from "./settings/useMediaHandler"; const SentryRoute = Sentry.withSentryRouting(Route); @@ -55,32 +56,34 @@ export default function App({ history }: AppProps) { {loaded ? ( - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( diff --git a/src/UserMenu.tsx b/src/UserMenu.tsx index 36cad0d..0bf2a79 100644 --- a/src/UserMenu.tsx +++ b/src/UserMenu.tsx @@ -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"); 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 { Avatar, Size } from "./Avatar"; 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 LogoutIcon } from "./icons/Logout.svg"; import { Body } from "./typography/Typography"; @@ -59,6 +60,11 @@ export function UserMenu({ icon: UserIcon, label: displayName, }); + arr.push({ + key: "settings", + icon: SettingsIcon, + label: t("Settings"), + }); if (isPasswordlessUser && !preventNavigation) { arr.push({ diff --git a/src/UserMenuContainer.tsx b/src/UserMenuContainer.tsx index e2c57ca..0a116d9 100644 --- a/src/UserMenuContainer.tsx +++ b/src/UserMenuContainer.tsx @@ -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"); 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. */ -import React, { useCallback } from "react"; +import React, { useCallback, useState } from "react"; import { useHistory, useLocation } from "react-router-dom"; import { useClient } from "./ClientContext"; import { useProfile } from "./profile/useProfile"; import { useModalTriggerState } from "./Modal"; -import { ProfileModal } from "./profile/ProfileModal"; +import { SettingsModal } from "./settings/SettingsModal"; import { UserMenu } from "./UserMenu"; interface Props { @@ -35,10 +35,17 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { const { displayName, avatarUrl } = useProfile(client); const { modalState, modalProps } = useModalTriggerState(); + const [defaultSettingsTab, setDefaultSettingsTab] = useState(); + const onAction = useCallback( - (value: string) => { + async (value: string) => { switch (value) { case "user": + setDefaultSettingsTab("profile"); + modalState.open(); + break; + case "settings": + setDefaultSettingsTab("audio"); modalState.open(); break; case "logout": @@ -64,7 +71,13 @@ export function UserMenuContainer({ preventNavigation = false }: Props) { displayName || (userName ? userName.replace("@", "") : undefined) } /> - {modalState.isOpen && } + {modalState.isOpen && ( + + )} ); } diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 78b8299..1ac7921 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -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"); you may not use this file except in compliance with the License. @@ -246,9 +246,11 @@ export function SettingsButton({ export function InviteButton({ className, + variant = "toolbar", ...rest }: { className?: string; + variant?: string; // TODO: add all props for diff --git a/src/room/FeedbackModal.tsx b/src/room/FeedbackModal.tsx deleted file mode 100644 index 537132c..0000000 --- a/src/room/FeedbackModal.tsx +++ /dev/null @@ -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 - [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 ( - - - {t("Having trouble? Help us fix it.")} -
- - - - - - - {error && ( - - - - )} - - - -
-
-
- ); -} diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index b3fdaa3..41658c0 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -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"); 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 classNames from "classnames"; import { useTranslation } from "react-i18next"; +import { OverlayTriggerState } from "@react-stately/overlays"; import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import type { IWidgetApiRequest } from "matrix-widget-api"; @@ -33,6 +34,8 @@ import { MicButton, VideoButton, ScreenshareButton, + SettingsButton, + InviteButton, } from "../button"; import { Header, @@ -48,12 +51,8 @@ import { } from "../video-grid/VideoGrid"; import { VideoTileContainer } from "../video-grid/VideoTileContainer"; import { GroupCallInspector } from "./GroupCallInspector"; -import { OverflowMenu } from "./OverflowMenu"; import { GridLayoutMenu } from "./GridLayoutMenu"; import { Avatar } from "../Avatar"; -import { UserMenuContainer } from "../UserMenuContainer"; -import { useRageshakeRequestModal } from "../settings/submit-rageshake"; -import { RageshakeRequestModal } from "./RageshakeRequestModal"; import { useMediaHandler } from "../settings/useMediaHandler"; import { useNewGrid, @@ -74,6 +73,8 @@ import { AudioSink } from "../video-grid/AudioSink"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { NewVideoGrid } from "../video-grid/NewVideoGrid"; import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; +import { SettingsModal } from "../settings/SettingsModal"; +import { InviteModal } from "./InviteModal"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -128,7 +129,6 @@ export function InCallView({ }: Props) { const { t } = useTranslation(); usePreventScroll(); - const joinRule = useJoinRule(groupCall.room); const containerRef1 = useRef(null); const [containerRef2, bounds] = useMeasure({ polyfill: ResizeObserver }); @@ -151,11 +151,10 @@ export function InCallView({ const [audioContext, audioDestination] = useAudioContext(); const [showInspector] = useShowInspector(); - const { modalState: feedbackModalState, modalProps: feedbackModalProps } = - useModalTriggerState(); - const { hideScreensharing } = useUrlParams(); + const joinRule = useJoinRule(groupCall.room); + useCallViewKeyboardShortcuts( containerRef1, toggleMicrophoneMuted, @@ -342,9 +341,34 @@ export function InCallView({ }; const { - modalState: rageshakeRequestModalState, - modalProps: rageshakeRequestModalProps, - } = useRageshakeRequestModal(groupCall.room.roomId); + modalState: settingsModalState, + modalProps: settingsModalProps, + }: { + 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, { [styles.maximised]: maximisedParticipant, @@ -402,17 +426,7 @@ export function InCallView({ ); } if (!maximisedParticipant) { - buttons.push( - - ); + buttons.push(); } } @@ -434,7 +448,9 @@ export function InCallView({ - + {joinRule === JoinRule.Public && ( + + )} )} @@ -448,12 +464,16 @@ export function InCallView({ otelGroupCallMembership={otelGroupCallMembership} show={showInspector} /> - {rageshakeRequestModalState.isOpen && ( - )} + {inviteModalState.isOpen && ( + + )} ); } diff --git a/src/room/OverflowMenu.tsx b/src/room/OverflowMenu.tsx deleted file mode 100644 index 0920f2d..0000000 --- a/src/room/OverflowMenu.tsx +++ /dev/null @@ -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 ( - <> - - - - - {(props: JSX.IntrinsicAttributes) => ( - - {showInvite && ( - - - {t("Invite people")} - - )} - - - {t("Settings")} - - {Config.get().rageshake?.submit_url && ( - - - {t("Submit feedback")} - - )} - - )} - - {settingsModalState.isOpen && } - {inviteModalState.isOpen && ( - - )} - {feedbackModalState.isOpen && ( - - )} - - ); -} diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index 2781640..af5437d 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -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"); 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 { useModalTriggerState } from "../Modal"; import { InviteModal } from "./InviteModal"; -import { HangupButton, InviteButton } from "../button"; +import { HangupButton, InviteButton, SettingsButton } from "../button"; import { Header, LeftNav, RightNav, RoomSetupHeaderInfo } from "../Header"; import styles from "./PTTCallView.module.css"; import { Facepile } from "../Facepile"; @@ -41,10 +41,10 @@ import { ReactComponent as AudioIcon } from "../icons/Audio.svg"; import { usePTTSounds } from "../sound/usePttSounds"; import { PTTClips } from "../sound/PTTClips"; import { GroupCallInspector } from "./GroupCallInspector"; -import { OverflowMenu } from "./OverflowMenu"; import { Size } from "../Avatar"; import { ParticipantInfo } from "./useGroupCall"; import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; +import { SettingsModal } from "../settings/SettingsModal"; function getPromptText( networkWaiting: boolean, @@ -126,8 +126,9 @@ export const PTTCallView: React.FC = ({ const { t } = useTranslation(); const { modalState: inviteModalState, modalProps: inviteModalProps } = useModalTriggerState(); - const { modalState: feedbackModalState, modalProps: feedbackModalProps } = + const { modalState: settingsModalState, modalProps: settingsModalProps } = useModalTriggerState(); + const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver }); const facepileSize = bounds.width < 800 ? Size.SM : Size.MD; const showControls = bounds.height > 500; @@ -232,14 +233,7 @@ export const PTTCallView: React.FC = ({ />
- + settingsModalState.open()} /> {!isEmbedded && } inviteModalState.open()} />
@@ -265,7 +259,7 @@ export const PTTCallView: React.FC = ({
))} = ({
+ {settingsModalState.isOpen && ( + + )} {inviteModalState.isOpen && showControls && ( )} diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index fe91861..21ebc63 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -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"); 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 { GroupCallView } from "./GroupCallView"; import { useUrlParams } from "../UrlParams"; -import { MediaHandlerProvider } from "../settings/useMediaHandler"; import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser"; import { translatedError } from "../TranslatedError"; import { useOptInAnalytics } from "../settings/useSetting"; @@ -101,15 +100,13 @@ export const RoomPage: FC = () => { } return ( - - - {groupCallView} - - + + {groupCallView} + ); }; diff --git a/src/room/VideoPreview.tsx b/src/room/VideoPreview.tsx index 0e92cc5..a7fc498 100644 --- a/src/room/VideoPreview.tsx +++ b/src/room/VideoPreview.tsx @@ -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"); 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. */ -import React from "react"; +import React, { useCallback } from "react"; import useMeasure from "react-use-measure"; import { ResizeObserver } from "@juggle/resize-observer"; import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall"; import { MatrixClient } from "matrix-js-sdk/src/client"; 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 { OverflowMenu } from "./OverflowMenu"; import { Avatar } from "../Avatar"; import { useProfile } from "../profile/useProfile"; import styles from "./VideoPreview.module.css"; import { Body } from "../typography/Typography"; import { useModalTriggerState } from "../Modal"; +import { SettingsModal } from "../settings/SettingsModal"; interface Props { client: MatrixClient; @@ -59,8 +60,20 @@ export function VideoPreview({ const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver }); const avatarSize = (previewBounds.height - 66) / 2; - const { modalState: feedbackModalState, modalProps: feedbackModalProps } = - useModalTriggerState(); + const { + modalState: settingsModalState, + modalProps: settingsModalProps, + }: { + modalState: OverlayTriggerState; + modalProps: { + isOpen: boolean; + onClose: () => void; + }; + } = useModalTriggerState(); + + const openSettings = useCallback(() => { + settingsModalState.open(); + }, [settingsModalState]); return (
@@ -95,17 +108,13 @@ export function VideoPreview({ muted={localVideoMuted} onPress={toggleLocalVideoMuted} /> - +
)} + {settingsModalState.isOpen && ( + + )} ); } diff --git a/src/settings/FeedbackSettingsTab.tsx b/src/settings/FeedbackSettingsTab.tsx new file mode 100644 index 0000000..1383490 --- /dev/null +++ b/src/settings/FeedbackSettingsTab.tsx @@ -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 ( +
+ {t("Having trouble? Help us fix it.")} +
+ + + + + + + {error && ( + + + + )} + + + +
+
+ ); +} diff --git a/src/profile/ProfileModal.module.css b/src/settings/ProfileSettingsTab.module.css similarity index 93% rename from src/profile/ProfileModal.module.css rename to src/settings/ProfileSettingsTab.module.css index 268fa93..4b80e49 100644 --- a/src/profile/ProfileModal.module.css +++ b/src/settings/ProfileSettingsTab.module.css @@ -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"); you may not use this file except in compliance with the License. diff --git a/src/profile/ProfileModal.tsx b/src/settings/ProfileSettingsTab.tsx similarity index 50% rename from src/profile/ProfileModal.tsx rename to src/settings/ProfileSettingsTab.tsx index adb1e2b..3afd753 100644 --- a/src/profile/ProfileModal.tsx +++ b/src/settings/ProfileSettingsTab.tsx @@ -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"); 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. */ -import React, { ChangeEvent, useCallback, useEffect, useState } from "react"; +import React, { ChangeEvent, useCallback, useState } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { useTranslation } from "react-i18next"; import { Button } from "../button"; -import { useProfile } from "./useProfile"; +import { useProfile } from "../profile/useProfile"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; -import { Modal, ModalContent } from "../Modal"; import { AvatarInputField } from "../input/AvatarInputField"; -import styles from "./ProfileModal.module.css"; +import styles from "./ProfileSettingsTab.module.css"; interface Props { client: MatrixClient; - onClose: () => void; - [rest: string]: unknown; } -export function ProfileModal({ client, ...rest }: Props) { - const { onClose } = rest; +export function ProfileSettingsTab({ client }: Props) { const { t } = useTranslation(); const { - success, error, loading, displayName: initialDisplayName, @@ -78,64 +73,51 @@ export function ProfileModal({ client, ...rest }: Props) { [saveProfile, removeAvatar] ); - useEffect(() => { - if (success) { - onClose(); - } - }, [success, onClose]); - return ( - - -
- - - - - - - - - - {error && ( - - - - )} - - - - -
-
-
+
+ + + + + + + + + + {error && ( + + + + )} + + + +
); } diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index b126bb8..d2b5996 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -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"); 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. */ -import React from "react"; +import React, { useCallback, useState } from "react"; import { Item } from "@react-stately/collections"; import { Trans, useTranslation } from "react-i18next"; +import { MatrixClient } from "matrix-js-sdk"; import { Modal } from "../Modal"; 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 DeveloperIcon } from "../icons/Developer.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 { useMediaHandler } from "./useMediaHandler"; import { @@ -39,9 +42,14 @@ import { Button } from "../button"; import { useDownloadDebugLog } from "./submit-rageshake"; import { Body, Caption } from "../typography/Typography"; import { AnalyticsNotice } from "../analytics/AnalyticsNotice"; +import { ProfileSettingsTab } from "./ProfileSettingsTab"; +import { FeedbackSettingsTab } from "./FeedbackSettingsTab"; interface Props { isOpen: boolean; + client: MatrixClient; + roomId?: string; + defaultTab?: string; onClose: () => void; } @@ -68,6 +76,15 @@ export const SettingsModal = (props: Props) => { const downloadDebugLog = useDownloadDebugLog(); + const [selectedTab, setSelectedTab] = useState(); + + const onSelectedTabChanged = useCallback( + (tab) => { + setSelectedTab(tab); + }, + [setSelectedTab] + ); + const optInDescription = ( @@ -87,8 +104,13 @@ export const SettingsModal = (props: Props) => { className={styles.settingsModal} {...props} > - + @@ -145,6 +167,7 @@ export const SettingsModal = (props: Props) => { @@ -167,6 +190,29 @@ export const SettingsModal = (props: Props) => { + + {t("Profile")} + + } + > + + + + + {t("feedback")} + + } + > + + + @@ -174,18 +220,10 @@ export const SettingsModal = (props: Props) => { } > -

Analytics

- - ) => - setOptInAnalytics(event.target.checked) - } - /> - +

Developer

+

+ Version: {(import.meta.env.VITE_APP_VERSION as string) || "dev"} +

{ } /> +

Analytics

+ + ) => + setOptInAnalytics(event.target.checked) + } + /> +
{developerSettingsTab && ( diff --git a/src/settings/useMediaHandler.tsx b/src/settings/useMediaHandler.tsx index ce3953e..6f491c8 100644 --- a/src/settings/useMediaHandler.tsx +++ b/src/settings/useMediaHandler.tsx @@ -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"); 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 */ -/* -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 React, { useState, @@ -43,6 +27,8 @@ import React, { ReactNode, } from "react"; +import { useClient } from "../ClientContext"; + export interface MediaHandlerContextInterface { audioInput: string; audioInputs: MediaDeviceInfo[]; @@ -89,10 +75,10 @@ function updateMediaPreferences(newPreferences: MediaPreferences): void { ); } interface Props { - client: MatrixClient; children: ReactNode; } -export function MediaHandlerProvider({ client, children }: Props): JSX.Element { +export function MediaHandlerProvider({ children }: Props): JSX.Element { + const { client } = useClient(); const [ { audioInput, @@ -104,19 +90,21 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { }, setState, ] = useState(() => { - const mediaPreferences = getMediaPreferences(); - const mediaHandler = client.getMediaHandler(); + const mediaHandler = client?.getMediaHandler(); - mediaHandler.restoreMediaSettings( - mediaPreferences?.audioInput, - mediaPreferences?.videoInput - ); + if (mediaHandler) { + const mediaPreferences = getMediaPreferences(); + mediaHandler?.restoreMediaSettings( + mediaPreferences?.audioInput, + mediaPreferences?.videoInput + ); + } return { // @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 - videoInput: mediaHandler.videoInput, + videoInput: mediaHandler?.videoInput, audioOutput: undefined, audioInputs: [], videoInputs: [], @@ -125,6 +113,8 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { }); useEffect(() => { + if (!client) return; + const mediaHandler = client.getMediaHandler(); function updateDevices(): void {