diff --git a/public/locales/en-GB/app.json b/public/locales/en-GB/app.json index 24678c6..ccd111b 100644 --- a/public/locales/en-GB/app.json +++ b/public/locales/en-GB/app.json @@ -38,7 +38,6 @@ "Create account": "Create account", "Debug log": "Debug log", "Debug log request": "Debug log request", - "Description (optional)": "Description (optional)", "Details": "Details", "Developer": "Developer", "Developer Settings": "Developer Settings", @@ -47,13 +46,14 @@ "Element Call Home": "Element Call Home", "Exit full screen": "Exit full screen", "Expose developer settings in the settings window.": "Expose developer settings in the settings window.", + "Feedback": "Feedback", "Fetching group call timed out.": "Fetching group call timed out.", "Freedom": "Freedom", "Full screen": "Full screen", "Go": "Go", "Grid layout menu": "Grid layout menu", - "Having trouble? Help us fix it.": "Having trouble? Help us fix it.", "Home": "Home", + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.", "Include debug logs": "Include debug logs", "Incompatible versions": "Incompatible versions", "Incompatible versions!": "Incompatible versions!", @@ -73,7 +73,6 @@ "Microphone {{n}}": "Microphone {{n}}", "Microphone permissions needed to join the call.": "Microphone permissions needed to join the call.", "More": "More", - "More menu": "More menu", "Mute microphone": "Mute microphone", "No": "No", "Not now, return to home screen": "Not now, return to home screen", @@ -94,8 +93,6 @@ "Release to stop": "Release to stop", "Remove": "Remove", "Return to home screen": "Return to home screen", - "Save": "Save", - "Saving…": "Saving…", "Select an option": "Select an option", "Send debug logs": "Send debug logs", "Sending debug logs…": "Sending debug logs…", @@ -110,11 +107,13 @@ "Speaker {{n}}": "Speaker {{n}}", "Spotlight": "Spotlight", "Stop sharing screen": "Stop sharing screen", + "Submit": "Submit", "Submit feedback": "Submit feedback", - "Submitting feedback…": "Submitting feedback…", + "Submitting…": "Submitting…", "Take me Home": "Take me Home", "Talk over speaker": "Talk over speaker", "Talking…": "Talking…", + "Thanks, we received your feedback!": "Thanks, we received your feedback!", "Thanks! We'll get right on it.": "Thanks! We'll get right on it.", "This call already exists, would you like to join?": "This call already exists, would you like to join?", "This feature is only supported on Firefox.": "This feature is only supported on Firefox.", @@ -124,7 +123,6 @@ "Turn on camera": "Turn on camera", "Unmute microphone": "Unmute microphone", "Use the upcoming grid system": "Use the upcoming grid system", - "User ID": "User ID", "User menu": "User menu", "Username": "Username", "Version: {{version}}": "Version: {{version}}", @@ -138,5 +136,6 @@ "WebRTC is not supported or is being blocked in this browser.": "WebRTC is not supported or is being blocked in this browser.", "Yes, join call": "Yes, join call", "You can't talk at the same time": "You can't talk at the same time", + "Your feedback": "Your feedback", "Your recent calls": "Your recent calls" } 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/Modal.module.css b/src/Modal.module.css index 30c8af0..5143f8b 100644 --- a/src/Modal.module.css +++ b/src/Modal.module.css @@ -40,7 +40,7 @@ limitations under the License. .modalHeader { display: flex; justify-content: space-between; - padding: 34px 34px 0 34px; + padding: 34px 32px 0 32px; } .modalHeader h3 { @@ -72,7 +72,7 @@ limitations under the License. .modalHeader { display: flex; justify-content: space-between; - padding: 24px 24px 0 24px; + padding: 32px 20px 0 20px; } .modal.mobileFullScreen { diff --git a/src/UserMenu.tsx b/src/UserMenu.tsx index 55fc05d..afb4248 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"; @@ -60,6 +61,11 @@ export function UserMenu({ label: displayName, dataTestid: "usermenu_user", }); + 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.module.css b/src/button/Button.module.css index eb8f0b1..b19db46 100644 --- a/src/button/Button.module.css +++ b/src/button/Button.module.css @@ -39,10 +39,10 @@ limitations under the License. .secondaryHangup, .button, .copyButton { - padding: 7px 15px; + padding: 8px 20px; border-radius: 8px; font-size: var(--font-size-body); - font-weight: 700; + font-weight: 600; } .button { diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 78b8299..289f2b9 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. @@ -238,7 +238,7 @@ export function SettingsButton({ return ( ); @@ -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/index.css b/src/index.css index fbc9d1a..f9e00ef 100644 --- a/src/index.css +++ b/src/index.css @@ -180,10 +180,16 @@ h2 { /* Subtitle */ h3 { - font-weight: 400; + font-weight: 600; font-size: var(--font-size-subtitle); } +/* Body Semi Bold */ +h4 { + font-weight: 600; + font-size: var(--font-size-body); +} + h1, h2, h3 { diff --git a/src/input/AvatarInputField.module.css b/src/input/AvatarInputField.module.css index 1462042..626d110 100644 --- a/src/input/AvatarInputField.module.css +++ b/src/input/AvatarInputField.module.css @@ -54,4 +54,6 @@ limitations under the License. .removeButton { color: var(--accent); + font-size: var(--font-size-caption); + padding: 6px 0; } diff --git a/src/input/Input.tsx b/src/input/Input.tsx index afecd6a..95d3fc5 100644 --- a/src/input/Input.tsx +++ b/src/input/Input.tsx @@ -72,6 +72,7 @@ interface InputFieldProps { autoCorrect?: string; autoCapitalize?: string; value?: string; + defaultValue?: string; placeholder?: string; defaultChecked?: boolean; onChange?: (event: ChangeEvent) => void; diff --git a/src/input/SelectInput.module.css b/src/input/SelectInput.module.css index 727ede0..086be82 100644 --- a/src/input/SelectInput.module.css +++ b/src/input/SelectInput.module.css @@ -22,8 +22,6 @@ limitations under the License. } .label { - font-weight: 600; - font-size: var(--font-size-subtitle); margin-top: 0; margin-bottom: 12px; } diff --git a/src/profile/ProfileModal.tsx b/src/profile/ProfileModal.tsx deleted file mode 100644 index 49f78ca..0000000 --- a/src/profile/ProfileModal.tsx +++ /dev/null @@ -1,146 +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, { ChangeEvent, useCallback, useEffect, 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 { FieldRow, InputField, ErrorMessage } from "../input/Input"; -import { Modal, ModalContent } from "../Modal"; -import { AvatarInputField } from "../input/AvatarInputField"; -import styles from "./ProfileModal.module.css"; - -interface Props { - client: MatrixClient; - onClose: () => void; - [rest: string]: unknown; -} -export function ProfileModal({ client, ...rest }: Props) { - const { onClose } = rest; - const { t } = useTranslation(); - const { - success, - error, - loading, - displayName: initialDisplayName, - avatarUrl, - saveProfile, - } = useProfile(client); - const [displayName, setDisplayName] = useState(initialDisplayName || ""); - const [removeAvatar, setRemoveAvatar] = useState(false); - - const onRemoveAvatar = useCallback(() => { - setRemoveAvatar(true); - }, []); - - const onChangeDisplayName = useCallback( - (e: ChangeEvent) => { - setDisplayName(e.target.value); - }, - [setDisplayName] - ); - - const onSubmit = useCallback( - (e) => { - e.preventDefault(); - const data = new FormData(e.target); - const displayNameDataEntry = data.get("displayName"); - const avatar: File | string = data.get("avatar"); - - const avatarSize = - typeof avatar == "string" ? avatar.length : avatar.size; - const displayName = - typeof displayNameDataEntry == "string" - ? displayNameDataEntry - : displayNameDataEntry.name; - - saveProfile({ - displayName, - avatar: avatar && avatarSize > 0 ? avatar : undefined, - removeAvatar: removeAvatar && (!avatar || avatarSize === 0), - }); - }, - [saveProfile, removeAvatar] - ); - - useEffect(() => { - if (success) { - onClose(); - } - }, [success, onClose]); - - return ( - - -
- - - - - - - - - - {error && ( - - - - )} - - - - -
-
-
- ); -} diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index ac44e9d..3446d4a 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,10 @@ 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"; +import { useRageshakeRequestModal } from "../settings/submit-rageshake"; +import { RageshakeRequestModal } from "./RageshakeRequestModal"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -128,7 +131,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 +153,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, @@ -346,6 +347,36 @@ export function InCallView({ modalProps: rageshakeRequestModalProps, } = useRageshakeRequestModal(groupCall.room.roomId); + const { + 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, }); @@ -405,17 +436,7 @@ export function InCallView({ ); } if (!maximisedParticipant) { - buttons.push( - - ); + buttons.push(); } } @@ -439,7 +460,9 @@ export function InCallView({ - + {joinRule === JoinRule.Public && ( + + )} )} @@ -459,6 +482,16 @@ export function InCallView({ roomIdOrAlias={roomIdOrAlias} /> )} + {settingsModalState.isOpen && ( + + )} + {inviteModalState.isOpen && ( + + )} ); } diff --git a/src/room/OverflowMenu.tsx b/src/room/OverflowMenu.tsx deleted file mode 100644 index cfd5fb9..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..44b4d74 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 aaf9df5..af1b18a 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 (
@@ -101,17 +114,13 @@ export function VideoPreview({ muted={localVideoMuted} onPress={toggleLocalVideoMuted} /> - +
)} + {settingsModalState.isOpen && ( + + )} ); } diff --git a/src/room/FeedbackModal.tsx b/src/settings/FeedbackSettingsTab.tsx similarity index 58% rename from src/room/FeedbackModal.tsx rename to src/settings/FeedbackSettingsTab.tsx index 537132c..da40678 100644 --- a/src/room/FeedbackModal.tsx +++ b/src/settings/FeedbackSettingsTab.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,28 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useEffect } from "react"; +import React, { useCallback } 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 { useSubmitRageshake, useRageshakeRequest } from "./submit-rageshake"; import { Body } from "../typography/Typography"; +import styles from "../input/SelectInput.module.css"; interface Props { - inCall: boolean; - roomId: string; - onClose?: () => void; - // TODO: add all props for for - [index: string]: unknown; + roomId?: string; } -export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) { +export function FeedbackSettingsTab({ roomId }: Props) { const { t } = useTranslation(); const { submitRageshake, sending, sent, error } = useSubmitRageshake(); const sendRageshakeRequest = useRageshakeRequest(); @@ -57,37 +50,34 @@ export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) { roomId, }); - if (inCall && sendLogs) { + if (roomId && sendLogs) { sendRageshakeRequest(roomId, rageshakeRequestId); } }, - [inCall, submitRageshake, roomId, sendRageshakeRequest] + [submitRageshake, roomId, sendRageshakeRequest] ); - useEffect(() => { - if (sent) { - onClose(); - } - }, [sent, onClose]); - return ( - - - {t("Having trouble? Help us fix it.")} -
- - - +
+

{t("Submit feedback")}

+ + {t( + "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below." + )} + + + + + + {sent ? ( + {t("Thanks, we received your feedback!")} + ) : ( - - {error && ( - - - - )} - + {error && ( + + + + )} - - - + )} + +
); } diff --git a/src/profile/ProfileModal.module.css b/src/settings/ProfileSettingsTab.module.css similarity index 84% rename from src/profile/ProfileModal.module.css rename to src/settings/ProfileSettingsTab.module.css index 268fa93..e85a00a 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. @@ -14,6 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +.content { + width: 100%; + max-width: 350px; + align-self: center; +} + .avatarFieldRow { justify-content: center; } diff --git a/src/settings/ProfileSettingsTab.tsx b/src/settings/ProfileSettingsTab.tsx new file mode 100644 index 0000000..409c0e6 --- /dev/null +++ b/src/settings/ProfileSettingsTab.tsx @@ -0,0 +1,113 @@ +/* +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, useEffect, useRef } from "react"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { useTranslation } from "react-i18next"; + +import { useProfile } from "../profile/useProfile"; +import { FieldRow, InputField, ErrorMessage } from "../input/Input"; +import { AvatarInputField } from "../input/AvatarInputField"; +import styles from "./ProfileSettingsTab.module.css"; + +interface Props { + client: MatrixClient; +} +export function ProfileSettingsTab({ client }: Props) { + const { t } = useTranslation(); + const { error, displayName, avatarUrl, saveProfile } = useProfile(client); + + const formRef = useRef(null); + + const formChanged = useRef(false); + const onFormChange = useCallback(() => { + formChanged.current = true; + }, []); + + const removeAvatar = useRef(false); + const onRemoveAvatar = useCallback(() => { + removeAvatar.current = true; + formChanged.current = true; + }, []); + + useEffect(() => { + const form = formRef.current!; + // Auto-save when the user dismisses this component + return () => { + if (formChanged.current) { + const data = new FormData(form); + const displayNameDataEntry = data.get("displayName"); + const avatar = data.get("avatar"); + + const avatarSize = + typeof avatar == "string" ? avatar.length : avatar?.size ?? 0; + const displayName = + typeof displayNameDataEntry == "string" + ? displayNameDataEntry + : displayNameDataEntry?.name ?? null; + + saveProfile({ + displayName, + avatar: avatar && avatarSize > 0 ? avatar : undefined, + removeAvatar: removeAvatar.current && (!avatar || avatarSize === 0), + }); + } + }; + }, [saveProfile]); + + return ( +
+ + + + + + + + + + {error && ( + + + + )} +
+ ); +} diff --git a/src/settings/SettingsModal.module.css b/src/settings/SettingsModal.module.css index 1e44dad..864d795 100644 --- a/src/settings/SettingsModal.module.css +++ b/src/settings/SettingsModal.module.css @@ -19,6 +19,10 @@ limitations under the License. height: 480px; } +.settingsModal p { + color: var(--secondary-content); +} + .tabContainer { padding: 27px 20px; } @@ -26,12 +30,3 @@ limitations under the License. .fieldRowText { margin-bottom: 0; } - -/* -This style guarantees a fixed width of the tab bar in the settings window. -The "Developer" item in the tab bar can be toggled. -Without a defined width activating the developer tab makes the tab container jump to the right. -*/ -.tabLabel { - min-width: 80px; -} diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 3b9db2e..03dd2ac 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; } @@ -70,6 +78,15 @@ export const SettingsModal = (props: Props) => { const downloadDebugLog = useDownloadDebugLog(); + const [selectedTab, setSelectedTab] = useState(); + + const onSelectedTabChanged = useCallback( + (tab) => { + setSelectedTab(tab); + }, + [setSelectedTab] + ); + const optInDescription = ( @@ -89,8 +106,13 @@ export const SettingsModal = (props: Props) => { className={styles.settingsModal} {...props} > - + @@ -147,6 +169,7 @@ export const SettingsModal = (props: Props) => { @@ -169,6 +192,29 @@ export const SettingsModal = (props: Props) => { + + {t("Profile")} + + } + > + + + + + {t("Feedback")} + + } + > + + + @@ -176,18 +222,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 6bc3e56..0d84996 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. @@ -14,23 +14,6 @@ See the License for the specific language governing permissions and 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 React, { useState, @@ -43,6 +26,7 @@ import React, { useRef, } from "react"; +import { useClient } from "../ClientContext"; import { getNamedDevices } from "../media-utils"; export interface MediaHandlerContextInterface { @@ -70,6 +54,7 @@ interface MediaPreferences { videoInput?: string; audioOutput?: string; } + function getMediaPreferences(): MediaPreferences { const mediaPreferences = localStorage.getItem("matrix-media-preferences"); @@ -95,11 +80,13 @@ 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, @@ -124,7 +111,7 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { const numComponentsWantingNames = useRef(0); const updateDevices = useCallback( - async (initial: boolean) => { + async (client: MatrixClient, initial: boolean) => { // Only request device names if components actually want them, because it // could trigger an extra permission pop-up const devices = await (numComponentsWantingNames.current > 0 @@ -175,7 +162,7 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { client.getMediaHandler().setMediaInputs(audioInput, videoInput); } }, - [client, setState] + [setState] ); const useDeviceNames = useCallback(() => { @@ -183,31 +170,36 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { // dynamic hook, but it works // eslint-disable-next-line react-hooks/rules-of-hooks useEffect(() => { - numComponentsWantingNames.current++; - if (numComponentsWantingNames.current === 1) updateDevices(false); - return () => void numComponentsWantingNames.current--; + if (client) { + numComponentsWantingNames.current++; + if (numComponentsWantingNames.current === 1) + updateDevices(client, false); + return () => void numComponentsWantingNames.current--; + } }, []); - }, [updateDevices]); + }, [client, updateDevices]); useEffect(() => { - updateDevices(true); - const onDeviceChange = () => updateDevices(false); - navigator.mediaDevices.addEventListener("devicechange", onDeviceChange); + if (client) { + updateDevices(client, true); + const onDeviceChange = () => updateDevices(client, false); + navigator.mediaDevices.addEventListener("devicechange", onDeviceChange); - return () => { - navigator.mediaDevices.removeEventListener( - "devicechange", - onDeviceChange - ); - client.getMediaHandler().stopAllStreams(); - }; + return () => { + navigator.mediaDevices.removeEventListener( + "devicechange", + onDeviceChange + ); + client.getMediaHandler().stopAllStreams(); + }; + } }, [client, updateDevices]); const setAudioInput: (deviceId: string) => void = useCallback( (deviceId: string) => { updateMediaPreferences({ audioInput: deviceId }); setState((prevState) => ({ ...prevState, audioInput: deviceId })); - client.getMediaHandler().setAudioInput(deviceId); + client?.getMediaHandler().setAudioInput(deviceId); }, [client] ); diff --git a/src/tabs/Tabs.module.css b/src/tabs/Tabs.module.css index 188747c..9ba6104 100644 --- a/src/tabs/Tabs.module.css +++ b/src/tabs/Tabs.module.css @@ -25,12 +25,14 @@ limitations under the License. list-style: none; padding: 0; margin: 0 auto 24px auto; + gap: 16px; + overflow: scroll; + max-width: 100%; } .tab { - max-width: 190px; - min-width: fit-content; height: 32px; + box-sizing: border-box; border-radius: 8px; background-color: transparent; display: flex; @@ -38,6 +40,7 @@ limitations under the License. padding: 0 8px; border: none; cursor: pointer; + font-size: var(--font-size-body); } .tab > * { @@ -78,17 +81,18 @@ limitations under the License. @media (min-width: 800px) { .tab { + width: 200px; padding: 0 16px; } .tab > * { - margin: 0 16px 0 0; + margin: 0 12px 0 0; } .tabContainer { width: 100%; flex-direction: row; - padding: 27px 20px; + padding: 20px 18px; box-sizing: border-box; overflow: hidden; } @@ -96,6 +100,7 @@ limitations under the License. .tabList { flex-direction: column; margin-bottom: 0; + gap: 0; } .tabPanel {