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