diff --git a/src/room/FeedbackModal.jsx b/src/room/FeedbackModal.jsx new file mode 100644 index 0000000..ba07e2f --- /dev/null +++ b/src/room/FeedbackModal.jsx @@ -0,0 +1,69 @@ +import React, { useCallback, useEffect } from "react"; +import { Modal, ModalContent } from "../Modal"; +import { Button } from "../button"; +import { FieldRow, InputField, ErrorMessage } from "../input/Input"; +import { useSubmitRageshake, useRageshakeRequest } from "../settings/rageshake"; +import { Body } from "../typography/Typography"; + +export function FeedbackModal({ inCall, roomId, ...rest }) { + const { submitRageshake, sending, sent, error } = useSubmitRageshake(); + const sendRageshakeRequest = useRageshakeRequest(); + + const onSubmitFeedback = useCallback( + (e) => { + e.preventDefault(); + const data = new FormData(e.target); + const description = data.get("description"); + const sendLogs = data.get("sendLogs"); + submitRageshake({ description, sendLogs }); + + if (inCall && sendLogs) { + sendRageshakeRequest(roomId); + } + }, + [inCall, submitRageshake, roomId, sendRageshakeRequest] + ); + + useEffect(() => { + if (sent) { + rest.onClose(); + } + }, [sent, rest.onClose]); + + return ( + <Modal title="Submit Feedback" isDismissable {...rest}> + <ModalContent> + <Body>Having trouble on this call? Help us fix it.</Body> + <form onSubmit={onSubmitFeedback}> + <FieldRow> + <InputField + id="description" + name="description" + label="Description (optional)" + type="text" + /> + </FieldRow> + <FieldRow> + <InputField + id="sendLogs" + name="sendLogs" + label="Include Debug Logs" + type="checkbox" + defaultChecked + /> + </FieldRow> + {error && ( + <FieldRow> + <ErrorMessage>{error.message}</ErrorMessage> + </FieldRow> + )} + <FieldRow> + <Button type="submit" disabled={sending}> + {sending ? "Submitting feedback..." : "Submit Feedback"} + </Button> + </FieldRow> + </form> + </ModalContent> + </Modal> + ); +} diff --git a/src/room/InCallView.jsx b/src/room/InCallView.jsx index 5a7db8b..e28c771 100644 --- a/src/room/InCallView.jsx +++ b/src/room/InCallView.jsx @@ -19,6 +19,8 @@ import { OverflowMenu } from "./OverflowMenu"; import { GridLayoutMenu } from "./GridLayoutMenu"; import { Avatar } from "../Avatar"; import { UserMenuContainer } from "../UserMenuContainer"; +import { useRageshakeRequestModal } from "../settings/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 @@ -120,6 +122,11 @@ export function InCallView({ [client] ); + const { + modalState: rageshakeRequestModalState, + modalProps: rageshakeRequestModalProps, + } = useRageshakeRequestModal(groupCall.room.roomId); + return ( <div className={styles.inRoom}> <Header> @@ -164,10 +171,12 @@ export function InCallView({ /> )} <OverflowMenu + inCall roomId={roomId} setShowInspector={setShowInspector} showInspector={showInspector} client={client} + groupCall={groupCall} /> <HangupButton onPress={onLeave} /> </div> @@ -176,6 +185,9 @@ export function InCallView({ groupCall={groupCall} show={showInspector} /> + {rageshakeRequestModalState.isOpen && ( + <RageshakeRequestModal {...rageshakeRequestModalProps} /> + )} </div> ); } diff --git a/src/room/OverflowMenu.jsx b/src/room/OverflowMenu.jsx index e35538d..b507449 100644 --- a/src/room/OverflowMenu.jsx +++ b/src/room/OverflowMenu.jsx @@ -9,18 +9,23 @@ import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg"; import { useModalTriggerState } from "../Modal"; import { SettingsModal } from "../settings/SettingsModal"; import { InviteModal } from "./InviteModal"; -import { Tooltip, TooltipTrigger } from "../Tooltip"; +import { TooltipTrigger } from "../Tooltip"; +import { FeedbackModal } from "./FeedbackModal"; export function OverflowMenu({ roomId, setShowInspector, showInspector, client, + inCall, + groupCall, }) { const { modalState: inviteModalState, modalProps: inviteModalProps } = useModalTriggerState(); const { modalState: settingsModalState, modalProps: settingsModalProps } = useModalTriggerState(); + const { modalState: feedbackModalState, modalProps: feedbackModalProps } = + useModalTriggerState(); // TODO: On closing modal, focus should be restored to the trigger button // https://github.com/adobe/react-spectrum/issues/2444 @@ -32,6 +37,9 @@ export function OverflowMenu({ case "settings": settingsModalState.open(); break; + case "feedback": + feedbackModalState.open(); + break; } }); @@ -54,6 +62,10 @@ export function OverflowMenu({ <SettingsIcon /> <span>Settings</span> </Item> + <Item key="feedback" textValue="Submit Feedback"> + <SettingsIcon /> + <span>Submit Feedback</span> + </Item> </Menu> )} </PopoverMenuTrigger> @@ -68,6 +80,13 @@ export function OverflowMenu({ {inviteModalState.isOpen && ( <InviteModal roomId={roomId} {...inviteModalProps} /> )} + {feedbackModalState.isOpen && ( + <FeedbackModal + {...feedbackModalProps} + roomId={groupCall?.room.roomId} + inCall={inCall} + /> + )} </> ); } diff --git a/src/room/RageshakeRequestModal.jsx b/src/room/RageshakeRequestModal.jsx new file mode 100644 index 0000000..2aeb544 --- /dev/null +++ b/src/room/RageshakeRequestModal.jsx @@ -0,0 +1,40 @@ +import React, { useEffect } from "react"; +import { Modal, ModalContent } from "../Modal"; +import { Button } from "../button"; +import { FieldRow, ErrorMessage } from "../input/Input"; +import { useSubmitRageshake } from "../settings/rageshake"; +import { Body } from "../typography/Typography"; + +export function RageshakeRequestModal(props) { + const { submitRageshake, sending, sent, error } = useSubmitRageshake(); + + useEffect(() => { + if (sent) { + props.onClose(); + } + }, [sent, props.onClose]); + + return ( + <Modal title="Debug Log Request" isDismissable {...props}> + <ModalContent> + <Body> + Another user on this call is having an issue. In order to better + diagnose these issues we'd like to collect a debug log. + </Body> + <FieldRow> + <Button + onPress={() => submitRageshake({ sendLogs: true })} + disabled={sending} + > + {sending ? "Sending debug log..." : "Send debug log"} + </Button> + </FieldRow> + {error && ( + <FieldRow> + <ErrorMessage>{error.message}</ErrorMessage> + </FieldRow> + )} + </ModalContent> + </Modal> + ); +} diff --git a/src/settings/SettingsModal.jsx b/src/settings/SettingsModal.jsx index 014f92a..e456cff 100644 --- a/src/settings/SettingsModal.jsx +++ b/src/settings/SettingsModal.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; import { Modal } from "../Modal"; import styles from "./SettingsModal.module.css"; import { TabContainer, TabItem } from "../tabs/Tabs"; @@ -8,10 +8,9 @@ import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg"; import { SelectInput } from "../input/SelectInput"; import { Item } from "@react-stately/collections"; import { useMediaHandler } from "./useMediaHandler"; -import { FieldRow, InputField, ErrorMessage } from "../input/Input"; +import { FieldRow, InputField } from "../input/Input"; import { Button } from "../button"; -import { useSubmitRageshake } from "./useSubmitRageshake"; -import { Subtitle } from "../typography/Typography"; +import { useDownloadDebugLog } from "./rageshake"; export function SettingsModal({ client, @@ -28,10 +27,7 @@ export function SettingsModal({ setVideoInput, } = useMediaHandler(client); - const [description, setDescription] = useState(""); - - const { submitRageshake, sending, sent, error, downloadDebugLog } = - useSubmitRageshake(); + const downloadDebugLog = useDownloadDebugLog(); return ( <Modal @@ -96,31 +92,6 @@ export function SettingsModal({ onChange={(e) => setShowInspector(e.target.checked)} /> </FieldRow> - <Subtitle>Feedback</Subtitle> - <FieldRow> - <InputField - id="description" - name="description" - label="Description" - type="text" - value={description} - onChange={(e) => setDescription(e.target.value)} - /> - </FieldRow> - <FieldRow> - <Button onPress={() => submitRageshake({ description })}> - {sent - ? "Debug Logs Sent" - : sending - ? "Sending Debug Logs..." - : "Send Debug Logs"} - </Button> - </FieldRow> - {error && ( - <FieldRow> - <ErrorMessage>{error.message}</ErrorMessage> - </FieldRow> - )} <FieldRow> <Button onPress={downloadDebugLog}>Download Debug Logs</Button> </FieldRow> diff --git a/src/settings/useSubmitRageshake.js b/src/settings/rageshake.js similarity index 78% rename from src/settings/useSubmitRageshake.js rename to src/settings/rageshake.js index 0c32b8b..d1d69c8 100644 --- a/src/settings/useSubmitRageshake.js +++ b/src/settings/rageshake.js @@ -1,8 +1,9 @@ -import { useCallback, useContext, useState } from "react"; +import { useCallback, useContext, useEffect, useState } from "react"; import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake"; import pako from "pako"; import { useClient } from "../ClientContext"; import { InspectorContext } from "../room/GroupCallInspector"; +import { useModalTriggerState } from "../Modal"; export function useSubmitRageshake() { const { client } = useClient(); @@ -171,24 +172,26 @@ export function useSubmitRageshake() { } catch (e) {} } - const logs = await rageshake.getLogsForReport(); + if (opts.sendLogs) { + const logs = await rageshake.getLogsForReport(); - for (const entry of logs) { - // encode as UTF-8 - let buf = new TextEncoder().encode(entry.lines); + for (const entry of logs) { + // encode as UTF-8 + let buf = new TextEncoder().encode(entry.lines); - // compress - buf = pako.gzip(buf); + // compress + buf = pako.gzip(buf); - body.append("compressed-log", new Blob([buf]), entry.id); - } + body.append("compressed-log", new Blob([buf]), entry.id); + } - if (json) { - body.append( - "file", - new Blob([JSON.stringify(json)], { type: "text/plain" }), - "groupcall.txt" - ); + if (json) { + body.append( + "file", + new Blob([JSON.stringify(json)], { type: "text/plain" }), + "groupcall.txt" + ); + } } await fetch( @@ -209,6 +212,17 @@ export function useSubmitRageshake() { [client] ); + return { + submitRageshake, + sending, + sent, + error, + }; +} + +export function useDownloadDebugLog() { + const [{ json }] = useContext(InspectorContext); + const downloadDebugLog = useCallback(() => { const blob = new Blob([JSON.stringify(json)], { type: "application/json" }); const url = URL.createObjectURL(blob); @@ -222,7 +236,47 @@ export function useSubmitRageshake() { URL.revokeObjectURL(url); el.parentNode.removeChild(el); }, 0); - }); + }, [json]); - return { submitRageshake, sending, sent, error, downloadDebugLog }; + return downloadDebugLog; +} + +export function useRageshakeRequest() { + const { client } = useClient(); + + const sendRageshakeRequest = useCallback( + (roomId) => { + client.sendEvent(roomId, "org.matrix.rageshake_request", {}); + }, + [client] + ); + + return sendRageshakeRequest; +} + +export function useRageshakeRequestModal(roomId) { + const { modalState, modalProps } = useModalTriggerState(); + const { client } = useClient(); + + useEffect(() => { + const onEvent = (event) => { + const type = event.getType(); + + if ( + type === "org.matrix.rageshake_request" && + roomId === event.getRoomId() && + client.getUserId() !== event.getSender() + ) { + modalState.open(); + } + }; + + client.on("event", onEvent); + + return () => { + client.removeListener("event", onEvent); + }; + }, [modalState.open, roomId]); + + return { modalState, modalProps }; }