@@ -48,7 +56,14 @@ export function CallList({ rooms, client, disableFacepile }) {
>
);
}
-
+interface CallTileProps {
+ name: string;
+ avatarUrl: string;
+ roomId: string;
+ participants: RoomMember[];
+ client: MatrixClient;
+ disableFacepile?: boolean;
+}
function CallTile({
name,
avatarUrl,
@@ -56,12 +71,12 @@ function CallTile({
participants,
client,
disableFacepile,
-}) {
+}: CallTileProps) {
return (
= ({ callType, setCallType }) => {
{callType === CallType.Video ? "Video call" : "Walkie-talkie call"}
- {(props) => (
+ {(props: JSX.IntrinsicAttributes) => (
-
diff --git a/src/home/HomePage.jsx b/src/home/HomePage.tsx
similarity index 99%
rename from src/home/HomePage.jsx
rename to src/home/HomePage.tsx
index 89c9ece..00f770f 100644
--- a/src/home/HomePage.jsx
+++ b/src/home/HomePage.tsx
@@ -15,6 +15,7 @@ limitations under the License.
*/
import React from "react";
+
import { useClient } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView";
import { UnauthenticatedView } from "./UnauthenticatedView";
diff --git a/src/home/JoinExistingCallModal.jsx b/src/home/JoinExistingCallModal.tsx
similarity index 76%
rename from src/home/JoinExistingCallModal.jsx
rename to src/home/JoinExistingCallModal.tsx
index 515baca..b26c45d 100644
--- a/src/home/JoinExistingCallModal.jsx
+++ b/src/home/JoinExistingCallModal.tsx
@@ -15,18 +15,26 @@ limitations under the License.
*/
import React from "react";
+import { PressEvent } from "@react-types/shared";
+
import { Modal, ModalContent } from "../Modal";
import { Button } from "../button";
import { FieldRow } from "../input/Input";
import styles from "./JoinExistingCallModal.module.css";
-export function JoinExistingCallModal({ onJoin, ...rest }) {
+interface Props {
+ onJoin: (e: PressEvent) => void;
+ onClose: (e: PressEvent) => void;
+ // TODO: add used parameters for
+ [index: string]: unknown;
+}
+export function JoinExistingCallModal({ onJoin, onClose, ...rest }: Props) {
return (
This call already exists, would you like to join?
- No
+ No
Yes, join call
diff --git a/src/home/RegisteredView.jsx b/src/home/RegisteredView.tsx
similarity index 84%
rename from src/home/RegisteredView.jsx
rename to src/home/RegisteredView.tsx
index b5fef74..4d190dd 100644
--- a/src/home/RegisteredView.jsx
+++ b/src/home/RegisteredView.tsx
@@ -14,7 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useState, useCallback } from "react";
+import React, {
+ useState,
+ useCallback,
+ FormEvent,
+ FormEventHandler,
+} from "react";
+import { useHistory } from "react-router-dom";
+import { MatrixClient } from "matrix-js-sdk";
+
import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils";
import { useGroupCallRooms } from "./useGroupCallRooms";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
@@ -26,28 +34,35 @@ import { CallList } from "./CallList";
import { UserMenuContainer } from "../UserMenuContainer";
import { useModalTriggerState } from "../Modal";
import { JoinExistingCallModal } from "./JoinExistingCallModal";
-import { useHistory } from "react-router-dom";
import { Title } from "../typography/Typography";
import { Form } from "../form/Form";
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
-export function RegisteredView({ client }) {
+interface Props {
+ client: MatrixClient;
+ isPasswordlessUser: boolean;
+}
+
+export function RegisteredView({ client, isPasswordlessUser }: Props) {
const [callType, setCallType] = useState(CallType.Video);
const [loading, setLoading] = useState(false);
- const [error, setError] = useState();
+ const [error, setError] = useState();
const history = useHistory();
- const onSubmit = useCallback(
- (e) => {
+ const { modalState, modalProps } = useModalTriggerState();
+
+ const onSubmit: FormEventHandler = useCallback(
+ (e: FormEvent) => {
e.preventDefault();
- const data = new FormData(e.target);
- const roomName = data.get("callName");
- const ptt = callType === CallType.Radio;
+ const data = new FormData(e.target as HTMLFormElement);
+ const roomNameData = data.get("callName");
+ const roomName = typeof roomNameData === "string" ? roomNameData : "";
+ // const ptt = callType === CallType.Radio;
async function submit() {
setError(undefined);
setLoading(true);
- const [roomIdOrAlias] = await createRoom(client, roomName, ptt);
+ const [roomIdOrAlias] = await createRoom(client, roomName);
if (roomIdOrAlias) {
history.push(`/room/${roomIdOrAlias}`);
@@ -64,17 +79,15 @@ export function RegisteredView({ client }) {
console.error(error);
setLoading(false);
setError(error);
- reset();
}
});
},
- [client, callType]
+ [client, history, modalState]
);
const recentRooms = useGroupCallRooms(client);
- const { modalState, modalProps } = useModalTriggerState();
- const [existingRoomId, setExistingRoomId] = useState();
+ const [existingRoomId, setExistingRoomId] = useState();
const onJoinExistingRoom = useCallback(() => {
history.push(`/${existingRoomId}`);
}, [history, existingRoomId]);
diff --git a/src/home/useGroupCallRooms.js b/src/home/useGroupCallRooms.ts
similarity index 72%
rename from src/home/useGroupCallRooms.js
rename to src/home/useGroupCallRooms.ts
index 4177a5f..cf15a26 100644
--- a/src/home/useGroupCallRooms.js
+++ b/src/home/useGroupCallRooms.ts
@@ -14,11 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { GroupCall, MatrixClient, Room, RoomMember } from "matrix-js-sdk";
+import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler";
import { useState, useEffect } from "react";
-const tsCache = {};
+export interface GroupCallRoom {
+ roomId: string;
+ roomName: string;
+ avatarUrl: string;
+ room: Room;
+ groupCall: GroupCall;
+ participants: RoomMember[];
+}
+const tsCache: { [index: string]: number } = {};
-function getLastTs(client, r) {
+function getLastTs(client: MatrixClient, r: Room) {
if (tsCache[r.roomId]) {
return tsCache[r.roomId];
}
@@ -59,13 +69,13 @@ function getLastTs(client, r) {
return ts;
}
-function sortRooms(client, rooms) {
+function sortRooms(client: MatrixClient, rooms: Room[]): Room[] {
return rooms.sort((a, b) => {
return getLastTs(client, b) - getLastTs(client, a);
});
}
-export function useGroupCallRooms(client) {
+export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
const [rooms, setRooms] = useState([]);
useEffect(() => {
@@ -90,12 +100,15 @@ export function useGroupCallRooms(client) {
updateRooms();
- client.on("GroupCall.incoming", updateRooms);
- client.on("GroupCall.participants", updateRooms);
+ client.on(GroupCallEventHandlerEvent.Incoming, updateRooms);
+ client.on(GroupCallEventHandlerEvent.Participants, updateRooms);
return () => {
- client.removeListener("GroupCall.incoming", updateRooms);
- client.removeListener("GroupCall.participants", updateRooms);
+ client.removeListener(GroupCallEventHandlerEvent.Incoming, updateRooms);
+ client.removeListener(
+ GroupCallEventHandlerEvent.Participants,
+ updateRooms
+ );
};
}, [client]);
diff --git a/src/icons/Audio.svg b/src/icons/Audio.svg
index f541ebc..0be0da9 100644
--- a/src/icons/Audio.svg
+++ b/src/icons/Audio.svg
@@ -1,5 +1,5 @@
-
-
-
-
+
+
+
+
diff --git a/src/icons/AudioLow.svg b/src/icons/AudioLow.svg
new file mode 100644
index 0000000..bfa80ce
--- /dev/null
+++ b/src/icons/AudioLow.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/icons/AudioMuted.svg b/src/icons/AudioMuted.svg
new file mode 100644
index 0000000..372ece5
--- /dev/null
+++ b/src/icons/AudioMuted.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/index.css b/src/index.css
index 7ec1e9c..6e9a912 100644
--- a/src/index.css
+++ b/src/index.css
@@ -38,6 +38,7 @@ limitations under the License.
--quinary-content: #394049;
--system: #21262c;
--background: #15191e;
+ --background-85: rgba(23, 25, 28, 0.85);
--bgColor3: #444; /* This isn't found anywhere in the designs or Compound */
}
diff --git a/src/main.tsx b/src/main.tsx
index 2330bed..a1739c7 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -84,6 +84,10 @@ if (import.meta.env.VITE_CUSTOM_THEME) {
"--background",
import.meta.env.VITE_THEME_BACKGROUND as string
);
+ style.setProperty(
+ "--background-85",
+ import.meta.env.VITE_THEME_BACKGROUND_85 as string
+ );
}
const history = createBrowserHistory();
diff --git a/src/popover/Popover.jsx b/src/popover/Popover.tsx
similarity index 84%
rename from src/popover/Popover.jsx
rename to src/popover/Popover.tsx
index 09c73aa..b02fb58 100644
--- a/src/popover/Popover.jsx
+++ b/src/popover/Popover.tsx
@@ -14,14 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { forwardRef, useRef } from "react";
+import React, { forwardRef, HTMLAttributes } from "react";
import { DismissButton, useOverlay } from "@react-aria/overlays";
import { FocusScope } from "@react-aria/focus";
import classNames from "classnames";
-import styles from "./Popover.module.css";
import { useObjectRef } from "@react-aria/utils";
-export const Popover = forwardRef(
+import styles from "./Popover.module.css";
+
+interface Props extends HTMLAttributes {
+ isOpen: boolean;
+ onClose: () => void;
+ className?: string;
+ children?: JSX.Element;
+}
+
+export const Popover = forwardRef(
({ isOpen = true, onClose, className, children, ...rest }, ref) => {
const popoverRef = useObjectRef(ref);
diff --git a/src/popover/PopoverMenu.jsx b/src/popover/PopoverMenu.jsx
deleted file mode 100644
index fe7ee10..0000000
--- a/src/popover/PopoverMenu.jsx
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
-Copyright 2022 Matrix.org Foundation C.I.C.
-
-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, { forwardRef, useRef } from "react";
-import styles from "./PopoverMenu.module.css";
-import { useMenuTriggerState } from "@react-stately/menu";
-import { useMenuTrigger } from "@react-aria/menu";
-import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
-import { mergeProps, useObjectRef } from "@react-aria/utils";
-import classNames from "classnames";
-import { Popover } from "./Popover";
-
-export const PopoverMenuTrigger = forwardRef(
- ({ children, placement, className, disableOnState, ...rest }, ref) => {
- const popoverMenuState = useMenuTriggerState(rest);
- const buttonRef = useObjectRef(ref);
- const { menuTriggerProps, menuProps } = useMenuTrigger(
- {},
- popoverMenuState,
- buttonRef
- );
-
- const popoverRef = useRef();
-
- const { overlayProps } = useOverlayPosition({
- targetRef: buttonRef,
- overlayRef: popoverRef,
- placement: placement || "top",
- offset: 5,
- isOpen: popoverMenuState.isOpen,
- });
-
- if (
- !Array.isArray(children) ||
- children.length > 2 ||
- typeof children[1] !== "function"
- ) {
- throw new Error(
- "PopoverMenu must have two props. The first being a button and the second being a render prop."
- );
- }
-
- const [popoverTrigger, popoverMenu] = children;
-
- return (
-
-
- {popoverMenuState.isOpen && (
-
-
- {popoverMenu({
- ...menuProps,
- autoFocus: popoverMenuState.focusStrategy,
- onClose: popoverMenuState.close,
- })}
-
-
- )}
-
- );
- }
-);
diff --git a/src/popover/PopoverMenu.tsx b/src/popover/PopoverMenu.tsx
new file mode 100644
index 0000000..2b4555e
--- /dev/null
+++ b/src/popover/PopoverMenu.tsx
@@ -0,0 +1,96 @@
+/*
+Copyright 2022 Matrix.org Foundation C.I.C.
+
+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, { forwardRef, useRef } from "react";
+import { useMenuTriggerState } from "@react-stately/menu";
+import { useMenuTrigger } from "@react-aria/menu";
+import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
+import { mergeProps, useObjectRef } from "@react-aria/utils";
+import classNames from "classnames";
+import { MenuTriggerProps } from "@react-types/menu";
+import { Placement } from "@react-types/overlays";
+
+import styles from "./PopoverMenu.module.css";
+import { Popover } from "./Popover";
+
+interface PopoverMenuTriggerProps extends MenuTriggerProps {
+ children: JSX.Element;
+ placement: Placement;
+ className: string;
+ disableOnState: boolean;
+ [index: string]: unknown;
+}
+
+export const PopoverMenuTrigger = forwardRef<
+ HTMLDivElement,
+ PopoverMenuTriggerProps
+>(({ children, placement, className, disableOnState, ...rest }, ref) => {
+ const popoverMenuState = useMenuTriggerState(rest);
+ const buttonRef = useObjectRef(ref);
+ const { menuTriggerProps, menuProps } = useMenuTrigger(
+ {},
+ popoverMenuState,
+ buttonRef
+ );
+
+ const popoverRef = useRef();
+
+ const { overlayProps } = useOverlayPosition({
+ targetRef: buttonRef,
+ overlayRef: popoverRef,
+ placement: placement || "top",
+ offset: 5,
+ isOpen: popoverMenuState.isOpen,
+ });
+
+ if (
+ !Array.isArray(children) ||
+ children.length > 2 ||
+ typeof children[1] !== "function"
+ ) {
+ throw new Error(
+ "PopoverMenu must have two props. The first being a button and the second being a render prop."
+ );
+ }
+
+ const [popoverTrigger, popoverMenu] = children;
+
+ return (
+
+
+ {popoverMenuState.isOpen && (
+
+
+ {popoverMenu({
+ ...menuProps,
+ autoFocus: popoverMenuState.focusStrategy,
+ onClose: popoverMenuState.close,
+ })}
+
+
+ )}
+
+ );
+});
diff --git a/src/profile/ProfileModal.tsx b/src/profile/ProfileModal.tsx
index 26b99e9..3b3033a 100644
--- a/src/profile/ProfileModal.tsx
+++ b/src/profile/ProfileModal.tsx
@@ -26,7 +26,7 @@ import styles from "./ProfileModal.module.css";
interface Props {
client: MatrixClient;
- onClose: () => {};
+ onClose: () => void;
[rest: string]: unknown;
}
export function ProfileModal({ client, ...rest }: Props) {
diff --git a/src/room/AudioPreview.jsx b/src/room/AudioPreview.tsx
similarity index 90%
rename from src/room/AudioPreview.jsx
rename to src/room/AudioPreview.tsx
index f0b4bdf..c0e2e8b 100644
--- a/src/room/AudioPreview.jsx
+++ b/src/room/AudioPreview.tsx
@@ -15,12 +15,24 @@ limitations under the License.
*/
import React from "react";
-import styles from "./AudioPreview.module.css";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
-import { SelectInput } from "../input/SelectInput";
import { Item } from "@react-stately/collections";
+
+import styles from "./AudioPreview.module.css";
+import { SelectInput } from "../input/SelectInput";
import { Body } from "../typography/Typography";
+interface Props {
+ state: GroupCallState;
+ roomName: string;
+ audioInput: string;
+ audioInputs: MediaDeviceInfo[];
+ setAudioInput: (deviceId: string) => void;
+ audioOutput: string;
+ audioOutputs: MediaDeviceInfo[];
+ setAudioOutput: (deviceId: string) => void;
+}
+
export function AudioPreview({
state,
roomName,
@@ -30,7 +42,7 @@ export function AudioPreview({
audioOutput,
audioOutputs,
setAudioOutput,
-}) {
+}: Props) {
return (
<>
{`${roomName} - Walkie-talkie call`}
diff --git a/src/room/CallEndedView.jsx b/src/room/CallEndedView.tsx
similarity index 94%
rename from src/room/CallEndedView.jsx
rename to src/room/CallEndedView.tsx
index 5bbd8b8..7cb7cc2 100644
--- a/src/room/CallEndedView.jsx
+++ b/src/room/CallEndedView.tsx
@@ -15,13 +15,15 @@ limitations under the License.
*/
import React from "react";
+import { MatrixClient } from "matrix-js-sdk";
+
import styles from "./CallEndedView.module.css";
import { LinkButton } from "../button";
import { useProfile } from "../profile/useProfile";
import { Subtitle, Body, Link, Headline } from "../typography/Typography";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
-export function CallEndedView({ client }) {
+export function CallEndedView({ client }: { client: MatrixClient }) {
const { displayName } = useProfile(client);
return (
diff --git a/src/room/FeedbackModal.jsx b/src/room/FeedbackModal.tsx
similarity index 82%
rename from src/room/FeedbackModal.jsx
rename to src/room/FeedbackModal.tsx
index 3bf8517..c4af871 100644
--- a/src/room/FeedbackModal.jsx
+++ b/src/room/FeedbackModal.tsx
@@ -15,6 +15,8 @@ limitations under the License.
*/
import React, { useCallback, useEffect } from "react";
+import { randomString } from "matrix-js-sdk/src/randomstring";
+
import { Modal, ModalContent } from "../Modal";
import { Button } from "../button";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
@@ -23,9 +25,14 @@ import {
useRageshakeRequest,
} from "../settings/submit-rageshake";
import { Body } from "../typography/Typography";
-import { randomString } from "matrix-js-sdk/src/randomstring";
-
-export function FeedbackModal({ inCall, roomId, ...rest }) {
+interface Props {
+ inCall: boolean;
+ roomId: string;
+ onClose?: () => void;
+ // TODO: add all props for for
+ [index: string]: unknown;
+}
+export function FeedbackModal({ inCall, roomId, onClose, ...rest }: Props) {
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
const sendRageshakeRequest = useRageshakeRequest();
@@ -33,8 +40,12 @@ export function FeedbackModal({ inCall, roomId, ...rest }) {
(e) => {
e.preventDefault();
const data = new FormData(e.target);
- const description = data.get("description");
- const sendLogs = data.get("sendLogs");
+ const descriptionData = data.get("description");
+ const description =
+ typeof descriptionData === "string" ? descriptionData : "";
+ const sendLogsData = data.get("sendLogs");
+ const sendLogs =
+ typeof sendLogsData === "string" ? sendLogsData === "true" : false;
const rageshakeRequestId = randomString(16);
submitRageshake({
@@ -53,9 +64,9 @@ export function FeedbackModal({ inCall, roomId, ...rest }) {
useEffect(() => {
if (sent) {
- rest.onClose();
+ onClose();
}
- }, [sent, rest.onClose]);
+ }, [sent, onClose]);
return (
diff --git a/src/room/GridLayoutMenu.jsx b/src/room/GridLayoutMenu.tsx
similarity index 85%
rename from src/room/GridLayoutMenu.jsx
rename to src/room/GridLayoutMenu.tsx
index 02f324b..6c9fa5a 100644
--- a/src/room/GridLayoutMenu.jsx
+++ b/src/room/GridLayoutMenu.tsx
@@ -15,6 +15,8 @@ limitations under the License.
*/
import React from "react";
+import { Item } from "@react-stately/collections";
+
import { Button } from "../button";
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
import { ReactComponent as SpotlightIcon } from "../icons/Spotlight.svg";
@@ -22,19 +24,22 @@ import { ReactComponent as FreedomIcon } from "../icons/Freedom.svg";
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
import menuStyles from "../Menu.module.css";
import { Menu } from "../Menu";
-import { Item } from "@react-stately/collections";
-import { Tooltip, TooltipTrigger } from "../Tooltip";
+import { TooltipTrigger } from "../Tooltip";
-export function GridLayoutMenu({ layout, setLayout }) {
+type Layout = "freedom" | "spotlight";
+interface Props {
+ layout: Layout;
+ setLayout: (layout: Layout) => void;
+}
+export function GridLayoutMenu({ layout, setLayout }: Props) {
return (
-
+ "Layout Type"}>
{layout === "spotlight" ? : }
- {() => "Layout Type"}
- {(props) => (
+ {(props: JSX.IntrinsicAttributes) => (
-
diff --git a/src/room/GroupCallInspector.jsx b/src/room/GroupCallInspector.tsx
similarity index 70%
rename from src/room/GroupCallInspector.jsx
rename to src/room/GroupCallInspector.tsx
index c3188e8..4afd171 100644
--- a/src/room/GroupCallInspector.jsx
+++ b/src/room/GroupCallInspector.tsx
@@ -22,40 +22,26 @@ import React, {
useRef,
createContext,
useContext,
+ Dispatch,
} from "react";
-import ReactJson from "react-json-view";
+import ReactJson, { CollapsedFieldProps } from "react-json-view";
import mermaid from "mermaid";
+import { Item } from "@react-stately/collections";
+import { MatrixEvent, GroupCall, IContent } from "matrix-js-sdk";
+import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
+import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
+import { CallEvent } from "matrix-js-sdk/src/webrtc/call";
+
import styles from "./GroupCallInspector.module.css";
import { SelectInput } from "../input/SelectInput";
-import { Item } from "@react-stately/collections";
-function getCallUserId(call) {
- return call.getOpponentMember()?.userId || call.invitee || null;
+interface InspectorContextState {
+ eventsByUserId?: { [userId: string]: SequenceDiagramMatrixEvent[] };
+ remoteUserIds?: string[];
+ localUserId?: string;
+ localSessionId?: string;
}
-function getCallState(call) {
- return {
- id: call.callId,
- opponentMemberId: getCallUserId(call),
- state: call.state,
- direction: call.direction,
- };
-}
-
-function getHangupCallState(call) {
- return {
- ...getCallState(call),
- hangupReason: call.hangupReason,
- };
-}
-
-const dateFormatter = new Intl.DateTimeFormat([], {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- fractionalSecondDigits: 3,
-});
-
const defaultCollapsedFields = [
"org.matrix.msc3401.call",
"org.matrix.msc3401.call.member",
@@ -67,19 +53,19 @@ const defaultCollapsedFields = [
"content",
];
-function shouldCollapse({ name, src, type, namespace }) {
+function shouldCollapse({ name }: CollapsedFieldProps) {
return defaultCollapsedFields.includes(name);
}
-function getUserName(userId) {
- const match = userId.match(/@([^\:]+):/);
+function getUserName(userId: string) {
+ const match = userId.match(/@([^:]+):/);
return match && match.length > 0
? match[1].replace("-", " ").replace(/\W/g, "")
: userId.replace(/\W/g, "");
}
-function formatContent(type, content) {
+function formatContent(type: string, content: CallEventContent) {
if (type === "m.call.hangup") {
return `callId: ${content.call_id.slice(-4)} reason: ${
content.reason
@@ -109,14 +95,35 @@ function formatContent(type, content) {
}
}
-function formatTimestamp(timestamp) {
+const dateFormatter = new Intl.DateTimeFormat([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore the linter does not know about this property of the DataTimeFormatOptions
+ fractionalSecondDigits: 3,
+});
+
+function formatTimestamp(timestamp: number | Date) {
return dateFormatter.format(timestamp);
}
-export const InspectorContext = createContext();
+export const InspectorContext =
+ createContext<
+ [
+ InspectorContextState,
+ React.Dispatch>
+ ]
+ >(undefined);
-export function InspectorContextProvider({ children }) {
- const context = useState({});
+export function InspectorContextProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ // The context will be initialized empty.
+ // It is then set from within GroupCallInspector.
+ const context = useState({});
return (
{children}
@@ -124,14 +131,43 @@ export function InspectorContextProvider({ children }) {
);
}
+type CallEventContent = {
+ ["m.calls"]: {
+ ["m.devices"]: { session_id: string; [x: string]: unknown }[];
+ ["m.call_id"]: string;
+ }[];
+} & {
+ call_id: string;
+ reason: string;
+ sender_session_id: string;
+ dest_session_id: string;
+} & IContent;
+
+export type SequenceDiagramMatrixEvent = {
+ to: string;
+ from: string;
+ timestamp: number;
+ type: string;
+ content: CallEventContent;
+ ignored: boolean;
+};
+
+interface SequenceDiagramViewerProps {
+ localUserId: string;
+ remoteUserIds: string[];
+ selectedUserId: string;
+ onSelectUserId: Dispatch<(prevState: undefined) => undefined>;
+ events: SequenceDiagramMatrixEvent[];
+}
+
export function SequenceDiagramViewer({
localUserId,
remoteUserIds,
selectedUserId,
onSelectUserId,
events,
-}) {
- const mermaidElRef = useRef();
+}: SequenceDiagramViewerProps) {
+ const mermaidElRef = useRef();
useEffect(() => {
mermaid.initialize({
@@ -165,7 +201,7 @@ export function SequenceDiagramViewer({
}
`;
- mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode) => {
+ mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode: string) => {
mermaidElRef.current.innerHTML = svgCode;
});
}, [events, localUserId, selectedUserId]);
@@ -190,9 +226,18 @@ export function SequenceDiagramViewer({
);
}
-function reducer(state, action) {
+function reducer(
+ state: InspectorContextState,
+ action: {
+ type?: CallEvent | ClientEvent | RoomStateEvent;
+ event?: MatrixEvent;
+ rawEvent?: Record;
+ callStateEvent?: MatrixEvent;
+ memberStateEvents?: MatrixEvent[];
+ }
+) {
switch (action.type) {
- case "receive_room_state_event": {
+ case RoomStateEvent.Events: {
const { event, callStateEvent, memberStateEvents } = action;
let eventsByUserId = state.eventsByUserId;
@@ -247,12 +292,12 @@ function reducer(state, action) {
),
};
}
- case "received_voip_event": {
+ case ClientEvent.ReceivedVoipEvent: {
const event = action.event;
const eventsByUserId = { ...state.eventsByUserId };
const fromId = event.getSender();
const toId = state.localUserId;
- const content = event.getContent();
+ const content = event.getContent();
const remoteUserIds = eventsByUserId[fromId]
? state.remoteUserIds
@@ -272,11 +317,11 @@ function reducer(state, action) {
return { ...state, eventsByUserId, remoteUserIds };
}
- case "send_voip_event": {
- const event = action.event;
+ case CallEvent.SendVoipEvent: {
+ const event = action.rawEvent;
const eventsByUserId = { ...state.eventsByUserId };
const fromId = state.localUserId;
- const toId = event.userId;
+ const toId = event.userId as string;
const remoteUserIds = eventsByUserId[toId]
? state.remoteUserIds
@@ -287,8 +332,8 @@ function reducer(state, action) {
{
from: fromId,
to: toId,
- type: event.eventType,
- content: event.content,
+ type: event.eventType as string,
+ content: event.content as CallEventContent,
timestamp: Date.now(),
ignored: false,
},
@@ -301,7 +346,11 @@ function reducer(state, action) {
}
}
-function useGroupCallState(client, groupCall, pollCallStats) {
+function useGroupCallState(
+ client: MatrixClient,
+ groupCall: GroupCall,
+ showPollCallStats: boolean
+) {
const [state, dispatch] = useReducer(reducer, {
localUserId: client.getUserId(),
localSessionId: client.getSessionId(),
@@ -312,7 +361,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
});
useEffect(() => {
- function onUpdateRoomState(event) {
+ function onUpdateRoomState(event?: MatrixEvent) {
const callStateEvent = groupCall.room.currentState.getStateEvents(
"org.matrix.msc3401.call",
groupCall.groupCallId
@@ -323,120 +372,60 @@ function useGroupCallState(client, groupCall, pollCallStats) {
);
dispatch({
- type: "receive_room_state_event",
+ type: RoomStateEvent.Events,
event,
callStateEvent,
memberStateEvents,
});
}
- // function onCallsChanged() {
- // const calls = groupCall.calls.reduce((obj, call) => {
- // obj[
- // `${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
- // ] = getCallState(call);
- // return obj;
- // }, {});
-
- // updateState({ calls });
- // }
-
- // function onCallHangup(call) {
- // setState(({ hangupCalls, ...rest }) => ({
- // ...rest,
- // hangupCalls: {
- // ...hangupCalls,
- // [`${call.callId} (${
- // call.getOpponentMember()?.userId || call.sender
- // })`]: getHangupCallState(call),
- // },
- // }));
- // dispatch({ type: "call_hangup", call });
- // }
-
- function onReceivedVoipEvent(event) {
- dispatch({ type: "received_voip_event", event });
+ function onReceivedVoipEvent(event: MatrixEvent) {
+ dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
}
- function onSendVoipEvent(event) {
- dispatch({ type: "send_voip_event", event });
+ function onSendVoipEvent(event: Record) {
+ dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event });
}
-
- client.on("RoomState.events", onUpdateRoomState);
+ client.on(RoomStateEvent.Events, onUpdateRoomState);
//groupCall.on("calls_changed", onCallsChanged);
- groupCall.on("send_voip_event", onSendVoipEvent);
+ groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent);
//client.on("state", onCallsChanged);
//client.on("hangup", onCallHangup);
- client.on("received_voip_event", onReceivedVoipEvent);
+ client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
onUpdateRoomState();
return () => {
- client.removeListener("RoomState.events", onUpdateRoomState);
+ client.removeListener(RoomStateEvent.Events, onUpdateRoomState);
//groupCall.removeListener("calls_changed", onCallsChanged);
- groupCall.removeListener("send_voip_event", onSendVoipEvent);
+ groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent);
//client.removeListener("state", onCallsChanged);
//client.removeListener("hangup", onCallHangup);
- client.removeListener("received_voip_event", onReceivedVoipEvent);
+ client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
};
}, [client, groupCall]);
- // useEffect(() => {
- // let timeout;
-
- // async function updateCallStats() {
- // const callIds = groupCall.calls.map(
- // (call) =>
- // `${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
- // );
- // const stats = await Promise.all(
- // groupCall.calls.map((call) =>
- // call.peerConn
- // ? call.peerConn
- // .getStats(null)
- // .then((stats) =>
- // Object.fromEntries(
- // Array.from(stats).map(([_id, report], i) => [
- // report.type + i,
- // report,
- // ])
- // )
- // )
- // : Promise.resolve(null)
- // )
- // );
-
- // const callStats = {};
-
- // for (let i = 0; i < groupCall.calls.length; i++) {
- // callStats[callIds[i]] = stats[i];
- // }
-
- // dispatch({ type: "callStats", callStats });
- // timeout = setTimeout(updateCallStats, 1000);
- // }
-
- // if (pollCallStats) {
- // updateCallStats();
- // }
-
- // return () => {
- // clearTimeout(timeout);
- // };
- // }, [pollCallStats]);
-
return state;
}
-
-export function GroupCallInspector({ client, groupCall, show }) {
+interface GroupCallInspectorProps {
+ client: MatrixClient;
+ groupCall: GroupCall;
+ show: boolean;
+}
+export function GroupCallInspector({
+ client,
+ groupCall,
+ show,
+}: GroupCallInspectorProps) {
const [currentTab, setCurrentTab] = useState("sequence-diagrams");
- const [selectedUserId, setSelectedUserId] = useState();
+ const [selectedUserId, setSelectedUserId] = useState();
const state = useGroupCallState(client, groupCall, show);
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setState] = useContext(InspectorContext);
useEffect(() => {
- setState({ json: state });
+ setState(state);
}, [setState, state]);
if (!show) {
@@ -446,7 +435,7 @@ export function GroupCallInspector({ client, groupCall, show }) {
return (
diff --git a/src/room/GroupCallLoader.jsx b/src/room/GroupCallLoader.tsx
similarity index 78%
rename from src/room/GroupCallLoader.jsx
rename to src/room/GroupCallLoader.tsx
index 5b7f481..dcfbf48 100644
--- a/src/room/GroupCallLoader.jsx
+++ b/src/room/GroupCallLoader.tsx
@@ -14,18 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { ReactNode } from "react";
+import { GroupCall, MatrixClient } from "matrix-js-sdk";
+
import { useLoadGroupCall } from "./useLoadGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
import { usePageTitle } from "../usePageTitle";
+interface Props {
+ client: MatrixClient;
+ roomIdOrAlias: string;
+ viaServers: string[];
+ children: (groupCall: GroupCall) => ReactNode;
+ createPtt: boolean;
+}
+
export function GroupCallLoader({
client,
roomIdOrAlias,
viaServers,
- createPtt,
children,
-}) {
+ createPtt,
+}: Props): JSX.Element {
const { loading, error, groupCall } = useLoadGroupCall(
client,
roomIdOrAlias,
@@ -47,5 +57,5 @@ export function GroupCallLoader({
return
;
}
- return children(groupCall);
+ return <>{children(groupCall)}>;
}
diff --git a/src/room/GroupCallView.jsx b/src/room/GroupCallView.tsx
similarity index 91%
rename from src/room/GroupCallView.jsx
rename to src/room/GroupCallView.tsx
index 445a507..2d5ee92 100644
--- a/src/room/GroupCallView.jsx
+++ b/src/room/GroupCallView.tsx
@@ -16,7 +16,9 @@ limitations under the License.
import React, { useCallback, useEffect, useState } from "react";
import { useHistory } from "react-router-dom";
-import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
+import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
+import { MatrixClient } from "matrix-js-sdk";
+
import { useGroupCall } from "./useGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView";
import { LobbyView } from "./LobbyView";
@@ -26,14 +28,25 @@ import { CallEndedView } from "./CallEndedView";
import { useRoomAvatar } from "./useRoomAvatar";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { useLocationNavigation } from "../useLocationNavigation";
-
+declare global {
+ interface Window {
+ groupCall: GroupCall;
+ }
+}
+interface Props {
+ client: MatrixClient;
+ isPasswordlessUser: boolean;
+ isEmbedded: boolean;
+ roomIdOrAlias: string;
+ groupCall: GroupCall;
+}
export function GroupCallView({
client,
isPasswordlessUser,
isEmbedded,
roomIdOrAlias,
groupCall,
-}) {
+}: Props) {
const {
state,
error,
@@ -52,7 +65,6 @@ export function GroupCallView({
isScreensharing,
localScreenshareFeed,
screenshareFeeds,
- hasLocalParticipant,
participants,
unencryptedEventsFromUsers,
} = useGroupCall(groupCall);
@@ -80,7 +92,7 @@ export function GroupCallView({
if (!isPasswordlessUser) {
history.push("/");
}
- }, [leave, history]);
+ }, [leave, isPasswordlessUser, history]);
if (error) {
return
;
@@ -142,7 +154,6 @@ export function GroupCallView({
void;
+ toggleMicrophoneMuted: () => void;
+ toggleScreensharing: () => void;
+ userMediaFeeds: CallFeed[];
+ activeSpeaker: string;
+ onLeave: () => void;
+ isScreensharing: boolean;
+ screenshareFeeds: CallFeed[];
+ localScreenshareFeed: CallFeed;
+ roomIdOrAlias: string;
+ unencryptedEventsFromUsers: Set;
+}
+interface Participant {
+ id: string;
+ callFeed: CallFeed;
+ focused: boolean;
+ isLocal: boolean;
+ presenter: boolean;
+}
+
export function InCallView({
client,
groupCall,
@@ -65,9 +95,10 @@ export function InCallView({
toggleScreensharing,
isScreensharing,
screenshareFeeds,
+ localScreenshareFeed,
roomIdOrAlias,
unencryptedEventsFromUsers,
-}) {
+}: Props) {
usePreventScroll();
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
@@ -79,7 +110,7 @@ export function InCallView({
useModalTriggerState();
const items = useMemo(() => {
- const participants = [];
+ const participants: Participant[] = [];
for (const callFeed of userMediaFeeds) {
participants.push({
@@ -90,6 +121,7 @@ export function InCallView({
? callFeed.userId === activeSpeaker
: false,
isLocal: callFeed.isLocal(),
+ presenter: false,
});
}
@@ -107,29 +139,27 @@ export function InCallView({
callFeed,
focused: true,
isLocal: callFeed.isLocal(),
+ presenter: false,
});
}
return participants;
}, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
- const renderAvatar = useCallback(
- (roomMember, width, height) => {
- const avatarUrl = roomMember.user?.avatarUrl;
- const size = Math.round(Math.min(width, height) / 2);
+ const renderAvatar = useCallback((roomMember, width, height) => {
+ const avatarUrl = roomMember.user?.avatarUrl;
+ const size = Math.round(Math.min(width, height) / 2);
- return (
-
- );
- },
- [client]
- );
+ return (
+
+ );
+ }, []);
const {
modalState: rageshakeRequestModalState,
@@ -158,7 +188,7 @@ export function InCallView({
) : (
- {({ item, ...rest }) => (
+ {({ item, ...rest }: { item: Participant; [x: string]: unknown }) => (
-
- Copy and share this meeting link
-
-
-
- );
+interface Props extends Omit {
+ roomIdOrAlias: string;
}
+
+export const InviteModal: FC = ({ roomIdOrAlias, ...rest }) => (
+
+
+ Copy and share this meeting link
+
+
+
+);
diff --git a/src/room/LobbyView.jsx b/src/room/LobbyView.tsx
similarity index 84%
rename from src/room/LobbyView.jsx
rename to src/room/LobbyView.tsx
index fd43560..9075aac 100644
--- a/src/room/LobbyView.jsx
+++ b/src/room/LobbyView.tsx
@@ -15,10 +15,14 @@ limitations under the License.
*/
import React, { useEffect, useRef } from "react";
+import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { PressEvent } from "@react-types/shared";
+import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
+
import styles from "./LobbyView.module.css";
import { Button, CopyButton } from "../button";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
-import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { useCallFeed } from "../video-grid/useCallFeed";
import { getRoomUrl } from "../matrix-utils";
import { UserMenuContainer } from "../UserMenuContainer";
@@ -28,6 +32,22 @@ import { useMediaHandler } from "../settings/useMediaHandler";
import { VideoPreview } from "./VideoPreview";
import { AudioPreview } from "./AudioPreview";
+interface Props {
+ client: MatrixClient;
+ groupCall: GroupCall;
+ roomName: string;
+ avatarUrl: string;
+ state: GroupCallState;
+ onInitLocalCallFeed: () => void;
+ onEnter: (e: PressEvent) => void;
+ localCallFeed: CallFeed;
+ microphoneMuted: boolean;
+ toggleLocalVideoMuted: () => void;
+ toggleMicrophoneMuted: () => void;
+ localVideoMuted: boolean;
+ roomIdOrAlias: string;
+ isEmbedded: boolean;
+}
export function LobbyView({
client,
groupCall,
@@ -43,7 +63,7 @@ export function LobbyView({
toggleMicrophoneMuted,
roomIdOrAlias,
isEmbedded,
-}) {
+}: Props) {
const { stream } = useCallFeed(localCallFeed);
const {
audioInput,
@@ -60,7 +80,7 @@ export function LobbyView({
useLocationNavigation(state === GroupCallState.InitializingLocalCallFeed);
- const joinCallButtonRef = useRef();
+ const joinCallButtonRef = useRef();
useEffect(() => {
if (state === GroupCallState.LocalCallFeedInitialized) {
diff --git a/src/room/OverflowMenu.jsx b/src/room/OverflowMenu.tsx
similarity index 66%
rename from src/room/OverflowMenu.jsx
rename to src/room/OverflowMenu.tsx
index 39c9ffe..f973f8b 100644
--- a/src/room/OverflowMenu.jsx
+++ b/src/room/OverflowMenu.tsx
@@ -15,10 +15,13 @@ 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 { Button } from "../button";
import { Menu } from "../Menu";
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
-import { Item } from "@react-stately/collections";
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
@@ -28,7 +31,17 @@ import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal";
import { TooltipTrigger } from "../Tooltip";
import { FeedbackModal } from "./FeedbackModal";
-
+interface Props {
+ roomIdOrAlias: string;
+ inCall: boolean;
+ groupCall: GroupCall;
+ showInvite: boolean;
+ feedbackModalState: OverlayTriggerState;
+ feedbackModalProps: {
+ isOpen: boolean;
+ onClose: () => void;
+ };
+}
export function OverflowMenu({
roomIdOrAlias,
inCall,
@@ -36,39 +49,57 @@ export function OverflowMenu({
showInvite,
feedbackModalState,
feedbackModalProps,
-}) {
- const { modalState: inviteModalState, modalProps: inviteModalProps } =
- useModalTriggerState();
- const { modalState: settingsModalState, modalProps: settingsModalProps } =
- useModalTriggerState();
+}: Props) {
+ 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;
- }
- });
+ const onAction = useCallback(
+ (key) => {
+ switch (key) {
+ case "invite":
+ inviteModalState.open();
+ break;
+ case "settings":
+ settingsModalState.open();
+ break;
+ case "feedback":
+ feedbackModalState.open();
+ break;
+ }
+ },
+ [feedbackModalState, inviteModalState, settingsModalState]
+ );
return (
<>
-
+ "More"} placement="top">
- {() => "More"}
- {(props) => (
-
+ {(props: JSX.IntrinsicAttributes) => (
+
{showInvite && (
-
diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx
index 36b7638..dbdf90d 100644
--- a/src/room/PTTCallView.tsx
+++ b/src/room/PTTCallView.tsx
@@ -38,6 +38,7 @@ import { usePTTSounds } from "../sound/usePttSounds";
import { PTTClips } from "../sound/PTTClips";
import { GroupCallInspector } from "./GroupCallInspector";
import { OverflowMenu } from "./OverflowMenu";
+import { Size } from "../Avatar";
function getPromptText(
networkWaiting: boolean,
@@ -112,7 +113,7 @@ export const PTTCallView: React.FC = ({
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
useModalTriggerState();
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
- const facepileSize = bounds.width < 800 ? "sm" : "md";
+ const facepileSize = bounds.width < 800 ? Size.SM : Size.MD;
const showControls = bounds.height > 500;
const pttButtonSize = 232;
@@ -205,7 +206,6 @@ export const PTTCallView: React.FC = ({
;
diff --git a/src/room/RageshakeRequestModal.jsx b/src/room/RageshakeRequestModal.tsx
similarity index 81%
rename from src/room/RageshakeRequestModal.jsx
rename to src/room/RageshakeRequestModal.tsx
index 29fbe9e..59b74b3 100644
--- a/src/room/RageshakeRequestModal.jsx
+++ b/src/room/RageshakeRequestModal.tsx
@@ -14,25 +14,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { useEffect } from "react";
-import { Modal, ModalContent } from "../Modal";
+import React, { FC, useEffect } from "react";
+
+import { Modal, ModalContent, ModalProps } from "../Modal";
import { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake";
import { Body } from "../typography/Typography";
-export function RageshakeRequestModal({
+interface Props extends Omit {
+ rageshakeRequestId: string;
+ roomIdOrAlias: string;
+ onClose: () => void;
+}
+
+export const RageshakeRequestModal: FC = ({
rageshakeRequestId,
roomIdOrAlias,
...rest
-}) {
+}) => {
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => {
if (sent) {
rest.onClose();
}
- }, [sent, rest.onClose]);
+ }, [sent, rest]);
return (
@@ -47,7 +54,7 @@ export function RageshakeRequestModal({
submitRageshake({
sendLogs: true,
rageshakeRequestId,
- roomIdOrAlias, // Possibly not a room ID, but oh well
+ roomId: roomIdOrAlias, // Possibly not a room ID, but oh well
})
}
disabled={sending}
@@ -63,4 +70,4 @@ export function RageshakeRequestModal({
);
-}
+};
diff --git a/src/room/RoomAuthView.jsx b/src/room/RoomAuthView.tsx
similarity index 94%
rename from src/room/RoomAuthView.jsx
rename to src/room/RoomAuthView.tsx
index 0561314..ff2e8f0 100644
--- a/src/room/RoomAuthView.jsx
+++ b/src/room/RoomAuthView.tsx
@@ -15,11 +15,12 @@ limitations under the License.
*/
import React, { useCallback, useState } from "react";
+import { useLocation } from "react-router-dom";
+
import styles from "./RoomAuthView.module.css";
import { Button } from "../button";
import { Body, Caption, Link, Headline } from "../typography/Typography";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
-import { useLocation } from "react-router-dom";
import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Form } from "../form/Form";
import { UserMenuContainer } from "../UserMenuContainer";
@@ -27,7 +28,7 @@ import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser
export function RoomAuthView() {
const [loading, setLoading] = useState(false);
- const [error, setError] = useState();
+ const [error, setError] = useState();
const { registerPasswordlessUser, recaptchaId, privacyPolicyUrl } =
useRegisterPasswordlessUser();
@@ -36,7 +37,9 @@ export function RoomAuthView() {
(e) => {
e.preventDefault();
const data = new FormData(e.target);
- const displayName = data.get("displayName");
+ const dataForDisplayName = data.get("displayName");
+ const displayName =
+ typeof dataForDisplayName === "string" ? dataForDisplayName : "";
registerPasswordlessUser(displayName).catch((error) => {
console.error("Failed to register passwordless user", e);
diff --git a/src/room/RoomPage.jsx b/src/room/RoomPage.tsx
similarity index 98%
rename from src/room/RoomPage.jsx
rename to src/room/RoomPage.tsx
index 931eae1..c41ac6a 100644
--- a/src/room/RoomPage.jsx
+++ b/src/room/RoomPage.tsx
@@ -15,6 +15,7 @@ limitations under the License.
*/
import React, { FC, useEffect, useState } from "react";
+
import { useClient } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView";
@@ -24,7 +25,7 @@ import { useRoomParams } from "./useRoomParams";
import { MediaHandlerProvider } from "../settings/useMediaHandler";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
-export function RoomPage() {
+export const RoomPage: FC = () => {
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
useClient();
@@ -84,4 +85,4 @@ export function RoomPage() {
);
-}
+};
diff --git a/src/room/RoomRedirect.jsx b/src/room/RoomRedirect.tsx
similarity index 99%
rename from src/room/RoomRedirect.jsx
rename to src/room/RoomRedirect.tsx
index 1b3d2d3..eeef083 100644
--- a/src/room/RoomRedirect.jsx
+++ b/src/room/RoomRedirect.tsx
@@ -16,6 +16,7 @@ limitations under the License.
import React, { useEffect } from "react";
import { useLocation, useHistory } from "react-router-dom";
+
import { defaultHomeserverHost } from "../matrix-utils";
import { LoadingView } from "../FullScreenView";
diff --git a/src/room/Timer.jsx b/src/room/Timer.tsx
similarity index 80%
rename from src/room/Timer.jsx
rename to src/room/Timer.tsx
index c814b9f..8dd5694 100644
--- a/src/room/Timer.jsx
+++ b/src/room/Timer.tsx
@@ -16,11 +16,11 @@ limitations under the License.
import React, { useEffect, useState } from "react";
-function leftPad(value) {
- return value < 10 ? "0" + value : value;
+function leftPad(value: number): string {
+ return value < 10 ? "0" + value : "" + value;
}
-function formatTime(msElapsed) {
+function formatTime(msElapsed: number): string {
const secondsElapsed = msElapsed / 1000;
const hours = Math.floor(secondsElapsed / 3600);
const minutes = Math.floor(secondsElapsed / 60) - hours * 60;
@@ -28,15 +28,15 @@ function formatTime(msElapsed) {
return `${leftPad(hours)}:${leftPad(minutes)}:${leftPad(seconds)}`;
}
-export function Timer({ value }) {
- const [timestamp, setTimestamp] = useState();
+export function Timer({ value }: { value: string }) {
+ const [timestamp, setTimestamp] = useState();
useEffect(() => {
const startTimeMs = performance.now();
- let animationFrame;
+ let animationFrame: number;
- function onUpdate(curTimeMs) {
+ function onUpdate(curTimeMs: number) {
const msElapsed = curTimeMs - startTimeMs;
setTimestamp(formatTime(msElapsed));
animationFrame = requestAnimationFrame(onUpdate);
diff --git a/src/room/VideoPreview.jsx b/src/room/VideoPreview.tsx
similarity index 87%
rename from src/room/VideoPreview.jsx
rename to src/room/VideoPreview.tsx
index 91e2478..21b559d 100644
--- a/src/room/VideoPreview.jsx
+++ b/src/room/VideoPreview.tsx
@@ -15,18 +15,31 @@ limitations under the License.
*/
import React 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 { MicButton, VideoButton } from "../button";
import { useMediaStream } from "../video-grid/useMediaStream";
import { OverflowMenu } from "./OverflowMenu";
import { Avatar } from "../Avatar";
import { useProfile } from "../profile/useProfile";
-import useMeasure from "react-use-measure";
-import { ResizeObserver } from "@juggle/resize-observer";
-import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import styles from "./VideoPreview.module.css";
import { Body } from "../typography/Typography";
import { useModalTriggerState } from "../Modal";
+interface Props {
+ client: MatrixClient;
+ state: GroupCallState;
+ roomIdOrAlias: string;
+ microphoneMuted: boolean;
+ localVideoMuted: boolean;
+ toggleLocalVideoMuted: () => void;
+ toggleMicrophoneMuted: () => void;
+ audioOutput: string;
+ stream: MediaStream;
+}
export function VideoPreview({
client,
state,
@@ -37,7 +50,7 @@ export function VideoPreview({
toggleMicrophoneMuted,
audioOutput,
stream,
-}) {
+}: Props) {
const videoRef = useMediaStream(stream, audioOutput, true);
const { displayName, avatarUrl } = useProfile(client);
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
@@ -81,9 +94,11 @@ export function VideoPreview({
/>
>
diff --git a/src/room/useGroupCall.ts b/src/room/useGroupCall.ts
index 1e01ad5..7f3232a 100644
--- a/src/room/useGroupCall.ts
+++ b/src/room/useGroupCall.ts
@@ -29,7 +29,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { usePageUnload } from "./usePageUnload";
-export interface UseGroupCallType {
+export interface UseGroupCallReturnType {
state: GroupCallState;
calls: MatrixCall[];
localCallFeed: CallFeed;
@@ -72,7 +72,7 @@ interface State {
hasLocalParticipant: boolean;
}
-export function useGroupCall(groupCall: GroupCall): UseGroupCallType {
+export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
const [
{
state,
diff --git a/src/room/usePageUnload.js b/src/room/usePageUnload.ts
similarity index 93%
rename from src/room/usePageUnload.js
rename to src/room/usePageUnload.ts
index 8fd8220..b49c778 100644
--- a/src/room/usePageUnload.js
+++ b/src/room/usePageUnload.ts
@@ -32,11 +32,11 @@ function isIOS() {
);
}
-export function usePageUnload(callback) {
+export function usePageUnload(callback: () => void) {
useEffect(() => {
- let pageVisibilityTimeout;
+ let pageVisibilityTimeout: number;
- function onBeforeUnload(event) {
+ function onBeforeUnload(event: PageTransitionEvent) {
if (event.type === "visibilitychange") {
if (document.visibilityState === "visible") {
clearTimeout(pageVisibilityTimeout);
diff --git a/src/room/useSentryGroupCallHandler.js b/src/room/useSentryGroupCallHandler.ts
similarity index 64%
rename from src/room/useSentryGroupCallHandler.js
rename to src/room/useSentryGroupCallHandler.ts
index 4520384..4afb7f7 100644
--- a/src/room/useSentryGroupCallHandler.js
+++ b/src/room/useSentryGroupCallHandler.ts
@@ -16,28 +16,30 @@ limitations under the License.
import { useEffect } from "react";
import * as Sentry from "@sentry/react";
+import { GroupCall, GroupCallEvent } from "matrix-js-sdk/src/webrtc/groupCall";
+import { CallEvent, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
-export function useSentryGroupCallHandler(groupCall) {
+export function useSentryGroupCallHandler(groupCall: GroupCall) {
useEffect(() => {
- function onHangup(call) {
+ function onHangup(call: MatrixCall) {
if (call.hangupReason === "ice_failed") {
Sentry.captureException(new Error("Call hangup due to ICE failure."));
}
}
- function onError(error) {
+ function onError(error: Error) {
Sentry.captureException(error);
}
if (groupCall) {
- groupCall.on("hangup", onHangup);
- groupCall.on("error", onError);
+ groupCall.on(CallEvent.Hangup, onHangup);
+ groupCall.on(GroupCallEvent.Error, onError);
}
return () => {
if (groupCall) {
- groupCall.removeListener("hangup", onHangup);
- groupCall.removeListener("error", onError);
+ groupCall.removeListener(CallEvent.Hangup, onHangup);
+ groupCall.removeListener(GroupCallEvent.Error, onError);
}
};
}, [groupCall]);
diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx
index 7613f0d..53764e3 100644
--- a/src/settings/SettingsModal.tsx
+++ b/src/settings/SettingsModal.tsx
@@ -32,9 +32,8 @@ import { useDownloadDebugLog } from "./submit-rageshake";
import { Body } from "../typography/Typography";
interface Props {
- setShowInspector: boolean;
- showInspector: boolean;
- [rest: string]: unknown;
+ isOpen: boolean;
+ onClose: () => void;
}
export const SettingsModal = (props: Props) => {
diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts
index 20b0785..8d3ce6d 100644
--- a/src/settings/submit-rageshake.ts
+++ b/src/settings/submit-rageshake.ts
@@ -26,11 +26,11 @@ import { InspectorContext } from "../room/GroupCallInspector";
import { useModalTriggerState } from "../Modal";
interface RageShakeSubmitOptions {
- description: string;
- roomId: string;
- label: string;
sendLogs: boolean;
- rageshakeRequestId: string;
+ rageshakeRequestId?: string;
+ description?: string;
+ roomId?: string;
+ label?: string;
}
export function useSubmitRageshake(): {
@@ -40,7 +40,7 @@ export function useSubmitRageshake(): {
error: Error;
} {
const client: MatrixClient = useClient().client;
- const [{ json }] = useContext(InspectorContext);
+ const json = useContext(InspectorContext);
const [{ sending, sent, error }, setState] = useState({
sending: false,
@@ -274,7 +274,7 @@ export function useSubmitRageshake(): {
}
export function useDownloadDebugLog(): () => void {
- const [{ json }] = useContext(InspectorContext);
+ const json = useContext(InspectorContext);
const downloadDebugLog = useCallback(() => {
const blob = new Blob([JSON.stringify(json)], { type: "application/json" });
diff --git a/src/settings/useMediaHandler.tsx b/src/settings/useMediaHandler.tsx
index bd2acd3..05e8795 100644
--- a/src/settings/useMediaHandler.tsx
+++ b/src/settings/useMediaHandler.tsx
@@ -24,6 +24,7 @@ import React, {
useMemo,
useContext,
createContext,
+ ReactNode,
} from "react";
export interface MediaHandlerContextInterface {
@@ -73,7 +74,7 @@ function updateMediaPreferences(newPreferences: MediaPreferences): void {
}
interface Props {
client: MatrixClient;
- children: JSX.Element[];
+ children: ReactNode;
}
export function MediaHandlerProvider({ client, children }: Props): JSX.Element {
const [
diff --git a/src/useLocationNavigation.js b/src/useLocationNavigation.ts
similarity index 81%
rename from src/useLocationNavigation.js
rename to src/useLocationNavigation.ts
index 2ae5234..81ecdbc 100644
--- a/src/useLocationNavigation.js
+++ b/src/useLocationNavigation.ts
@@ -1,7 +1,7 @@
import { useEffect } from "react";
import { useHistory } from "react-router-dom";
-export function useLocationNavigation(enabled = false) {
+export function useLocationNavigation(enabled = false): void {
const history = useHistory();
useEffect(() => {
@@ -12,7 +12,7 @@ export function useLocationNavigation(enabled = false) {
const url = new URL(tx.pathname, window.location.href);
url.search = tx.search;
url.hash = tx.hash;
- window.location = url.href;
+ window.location.href = url.href;
});
}
diff --git a/src/usePageFocusStyle.js b/src/usePageFocusStyle.ts
similarity index 92%
rename from src/usePageFocusStyle.js
rename to src/usePageFocusStyle.ts
index c7ec75f..542cffd 100644
--- a/src/usePageFocusStyle.js
+++ b/src/usePageFocusStyle.ts
@@ -1,8 +1,9 @@
import { useEffect } from "react";
import { useFocusVisible } from "@react-aria/interactions";
+
import styles from "./usePageFocusStyle.module.css";
-export function usePageFocusStyle() {
+export function usePageFocusStyle(): void {
const { isFocusVisible } = useFocusVisible();
useEffect(() => {
diff --git a/src/usePageTitle.js b/src/usePageTitle.js
deleted file mode 100644
index e7b5d9f..0000000
--- a/src/usePageTitle.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { useEffect } from "react";
-
-export function usePageTitle(title) {
- useEffect(() => {
- const productName =
- import.meta.env.VITE_PRODUCT_NAME || "Matrix Video Chat";
- document.title = title ? `${productName} | ${title}` : productName;
- }, [title]);
-}
diff --git a/src/form/Form.jsx b/src/usePageTitle.ts
similarity index 59%
rename from src/form/Form.jsx
rename to src/usePageTitle.ts
index 011fbb2..1516ecc 100644
--- a/src/form/Form.jsx
+++ b/src/usePageTitle.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2022 Matrix.org Foundation C.I.C.
+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.
@@ -14,14 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import classNames from "classnames";
-import React, { forwardRef } from "react";
-import styles from "./Form.module.css";
+import { useEffect } from "react";
-export const Form = forwardRef(({ children, className, ...rest }, ref) => {
- return (
-
- );
-});
+export function usePageTitle(title: string): void {
+ useEffect(() => {
+ const productName =
+ import.meta.env.VITE_PRODUCT_NAME || "Matrix Video Chat";
+ document.title = title ? `${productName} | ${title}` : productName;
+ }, [title]);
+}
diff --git a/src/video-grid/VideoTile.jsx b/src/video-grid/VideoTile.jsx
index 206f8a4..c1f842a 100644
--- a/src/video-grid/VideoTile.jsx
+++ b/src/video-grid/VideoTile.jsx
@@ -20,7 +20,7 @@ import classNames from "classnames";
import styles from "./VideoTile.module.css";
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
-import { OptionsButton } from "../button/Button";
+import { AudioButton } from "../button/Button";
export const VideoTile = forwardRef(
(
@@ -38,6 +38,7 @@ export const VideoTile = forwardRef(
mediaRef,
onOptionsPress,
showOptions,
+ localVolume,
...rest
},
ref
@@ -53,6 +54,15 @@ export const VideoTile = forwardRef(
ref={ref}
{...rest}
>
+ {showOptions && (
+
+ )}
{(videoMuted || noVideo) && (
<>
@@ -72,11 +82,7 @@ export const VideoTile = forwardRef(
)
)}
- {showOptions && (
-