diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 7b5995f..97c5eff 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -24,7 +24,7 @@ declare global { // TypeScript doesn't know about the experimental setSinkId method, so we // declare it ourselves - interface MediaElement extends HTMLMediaElement { + interface MediaElement extends HTMLVideoElement { setSinkId: (id: string) => void; } } diff --git a/src/ClientContext.tsx b/src/ClientContext.tsx index 5e0c3c1..a1e5813 100644 --- a/src/ClientContext.tsx +++ b/src/ClientContext.tsx @@ -132,6 +132,7 @@ export const ClientProvider: FC = ({ children }) => { isAuthenticated: Boolean(client), isPasswordlessUser, userName: client?.getUserIdLocalpart(), + error: undefined, }); }) .catch(() => { @@ -141,6 +142,7 @@ export const ClientProvider: FC = ({ children }) => { isAuthenticated: false, isPasswordlessUser: false, userName: null, + error: undefined, }); }); }, []); @@ -170,6 +172,7 @@ export const ClientProvider: FC = ({ children }) => { isAuthenticated: true, isPasswordlessUser: false, userName: client.getUserIdLocalpart(), + error: undefined, }); }, [client] @@ -190,6 +193,7 @@ export const ClientProvider: FC = ({ children }) => { isAuthenticated: true, isPasswordlessUser: session.passwordlessUser, userName: newClient.getUserIdLocalpart(), + error: undefined, }); } else { clearSession(); @@ -200,6 +204,7 @@ export const ClientProvider: FC = ({ children }) => { isAuthenticated: false, isPasswordlessUser: false, userName: null, + error: undefined, }); } }, @@ -258,6 +263,7 @@ export const ClientProvider: FC = ({ children }) => { logout, userName, setClient, + error: undefined, }), [ loading, diff --git a/src/Menu.tsx b/src/Menu.tsx index 2b16ecf..82159bd 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { Key, useRef, useState } from "react"; import { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu"; import { TreeState, useTreeState } from "@react-stately/tree"; import { mergeProps } from "@react-aria/utils"; @@ -9,15 +9,17 @@ import { Node } from "@react-types/shared"; import styles from "./Menu.module.css"; interface MenuProps extends AriaMenuOptions { - className: String; - onAction: () => void; - onClose: () => void; + className?: String; + onClose?: () => void; + onAction: (value: Key) => void; + label?: string; } export function Menu({ className, onAction, onClose, + label, ...rest }: MenuProps) { const state = useTreeState({ ...rest, selectionMode: "none" }); @@ -46,7 +48,7 @@ export function Menu({ interface MenuItemProps { item: Node; state: TreeState; - onAction: () => void; + onAction: (value: Key) => void; onClose: () => void; } diff --git a/src/SequenceDiagramViewerPage.tsx b/src/SequenceDiagramViewerPage.tsx index f06d3ca..a6473cc 100644 --- a/src/SequenceDiagramViewerPage.tsx +++ b/src/SequenceDiagramViewerPage.tsx @@ -16,13 +16,16 @@ limitations under the License. import React, { useCallback, useState } from "react"; -import { SequenceDiagramViewer } from "./room/GroupCallInspector"; +import { + SequenceDiagramViewer, + SequenceDiagramMatrixEvent, +} from "./room/GroupCallInspector"; import { FieldRow, InputField } from "./input/Input"; import { usePageTitle } from "./usePageTitle"; interface DebugLog { localUserId: string; - eventsByUserId: Record; + eventsByUserId: { [userId: string]: SequenceDiagramMatrixEvent[] }; remoteUserIds: string[]; } @@ -33,7 +36,7 @@ export function SequenceDiagramViewerPage() { const [selectedUserId, setSelectedUserId] = useState(); const onChangeDebugLog = useCallback((e) => { if (e.target.files && e.target.files.length > 0) { - e.target.files[0].text().then((text) => { + e.target.files[0].text().then((text: string) => { setDebugLog(JSON.parse(text)); }); } diff --git a/src/UserMenuContainer.tsx b/src/UserMenuContainer.tsx index 5818536..437d3b7 100644 --- a/src/UserMenuContainer.tsx +++ b/src/UserMenuContainer.tsx @@ -11,7 +11,7 @@ interface Props { preventNavigation?: boolean; } -export function UserMenuContainer({ preventNavigation }: Props) { +export function UserMenuContainer({ preventNavigation = false }: Props) { const location = useLocation(); const history = useHistory(); const { isAuthenticated, isPasswordlessUser, logout, userName, client } = diff --git a/src/auth/RegisterPage.tsx b/src/auth/RegisterPage.tsx index fbae083..768c3c1 100644 --- a/src/auth/RegisterPage.tsx +++ b/src/auth/RegisterPage.tsx @@ -100,7 +100,7 @@ export const RegisterPage: FC = () => { submit() .then(() => { if (location.state?.from) { - history.push(location.state.from); + history.push(location.state?.from); } else { history.push("/"); } diff --git a/src/button/Button.tsx b/src/button/Button.tsx index e993661..008cb01 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -74,6 +74,7 @@ interface Props { children: Element[]; onPress: (e: PressEvent) => void; onPressStart: (e: PressEvent) => void; + // TODO: add all props for - {(props) => ( + {(props: JSX.IntrinsicAttributes) => ( 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 87% rename from src/room/GridLayoutMenu.jsx rename to src/room/GridLayoutMenu.tsx index 05d12a4..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,10 +24,14 @@ 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"}> @@ -33,7 +39,7 @@ export function GridLayoutMenu({ layout, setLayout }) { {layout === "spotlight" ? : } - {(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..813168e 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,17 @@ export function SequenceDiagramViewer({ ); } -function reducer(state, action) { +function reducer( + state: InspectorContextState, + action: { + type?: CallEvent | ClientEvent | RoomStateEvent; + event: MatrixEvent; + 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 +291,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 +316,11 @@ function reducer(state, action) { return { ...state, eventsByUserId, remoteUserIds }; } - case "send_voip_event": { + case CallEvent.SendVoipEvent: { const event = action.event; const eventsByUserId = { ...state.eventsByUserId }; const fromId = state.localUserId; - const toId = event.userId; + const toId = event.target.userId; // was .user const remoteUserIds = eventsByUserId[toId] ? state.remoteUserIds @@ -287,8 +331,8 @@ function reducer(state, action) { { from: fromId, to: toId, - type: event.eventType, - content: event.content, + type: event.getType(), + content: event.getContent(), timestamp: Date.now(), ignored: false, }, @@ -301,7 +345,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 +360,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 +371,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: MatrixEvent) { + dispatch({ type: CallEvent.SendVoipEvent, 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 +434,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 70791a2..4cc62f1 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; + roomId: string; + viaServers: string[]; + children: (groupCall: GroupCall) => ReactNode; + createPtt: boolean; +} + export function GroupCallLoader({ client, roomId, viaServers, - createPtt, children, -}) { + createPtt, +}: Props): JSX.Element { const { loading, error, groupCall } = useLoadGroupCall( client, roomId, @@ -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 92% rename from src/room/GroupCallView.jsx rename to src/room/GroupCallView.tsx index 9794a00..92e8ef1 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; + roomId: string; + groupCall: GroupCall; +} export function GroupCallView({ client, isPasswordlessUser, isEmbedded, roomId, 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; + roomId: 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, roomId, 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 }) => ( void; + onEnter: (e: PressEvent) => void; + localCallFeed: CallFeed; + microphoneMuted: boolean; + toggleLocalVideoMuted: () => void; + toggleMicrophoneMuted: () => void; + localVideoMuted: boolean; + roomId: string; + isEmbedded: boolean; +} export function LobbyView({ client, groupCall, @@ -43,7 +63,7 @@ export function LobbyView({ toggleMicrophoneMuted, roomId, 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 67% rename from src/room/OverflowMenu.jsx rename to src/room/OverflowMenu.tsx index 69334c7..e58f8f0 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 { + roomId: string; + inCall: boolean; + groupCall: GroupCall; + showInvite: boolean; + feedbackModalState: OverlayTriggerState; + feedbackModalProps: { + isOpen: boolean; + onClose: () => void; + }; +} export function OverflowMenu({ roomId, inCall, @@ -36,27 +49,46 @@ 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 ( <> @@ -66,8 +98,8 @@ export function OverflowMenu({ - {(props) => ( - + {(props: JSX.IntrinsicAttributes) => ( + {showInvite && ( diff --git a/src/room/PTTCallView.tsx b/src/room/PTTCallView.tsx index aff42ba..9f56b0d 100644 --- a/src/room/PTTCallView.tsx +++ b/src/room/PTTCallView.tsx @@ -206,7 +206,6 @@ export const PTTCallView: React.FC = ({ ; diff --git a/src/room/RageshakeRequestModal.jsx b/src/room/RageshakeRequestModal.tsx similarity index 89% rename from src/room/RageshakeRequestModal.jsx rename to src/room/RageshakeRequestModal.tsx index 201b308..3313bd1 100644 --- a/src/room/RageshakeRequestModal.jsx +++ b/src/room/RageshakeRequestModal.tsx @@ -15,20 +15,30 @@ limitations under the License. */ import React, { useEffect } from "react"; + import { Modal, ModalContent } 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({ rageshakeRequestId, roomId, ...rest }) { +export function RageshakeRequestModal({ + rageshakeRequestId, + roomId, + ...rest +}: { + rageshakeRequestId: string; + roomId: string; + onClose: () => void; + [x: string]: unknown; +}) { const { submitRageshake, sending, sent, error } = useSubmitRageshake(); useEffect(() => { if (sent) { rest.onClose(); } - }, [sent, rest.onClose]); + }, [sent, rest]); return ( 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 94% rename from src/room/RoomPage.jsx rename to src/room/RoomPage.tsx index 72f6bf4..fa9f9c0 100644 --- a/src/room/RoomPage.jsx +++ b/src/room/RoomPage.tsx @@ -16,6 +16,7 @@ limitations under the License. import React, { useEffect, useMemo, useState } from "react"; import { useLocation, useParams } from "react-router-dom"; + import { useClient } from "../ClientContext"; import { ErrorView, LoadingView } from "../FullScreenView"; import { RoomAuthView } from "./RoomAuthView"; @@ -29,7 +30,7 @@ export function RoomPage() { useClient(); const { roomId: maybeRoomId } = useParams(); - const { hash, search } = useLocation(); + const { hash, search }: { hash: string; search: string } = useLocation(); const [viaServers, isEmbedded, isPtt, displayName] = useMemo(() => { const params = new URLSearchParams(search); return [ @@ -40,8 +41,7 @@ export function RoomPage() { ]; }, [search]); const roomId = (maybeRoomId || hash || "").toLowerCase(); - const { registerPasswordlessUser, recaptchaId } = - useRegisterPasswordlessUser(); + const { registerPasswordlessUser } = useRegisterPasswordlessUser(); const [isRegistering, setIsRegistering] = useState(false); useEffect(() => { 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 88% rename from src/room/VideoPreview.jsx rename to src/room/VideoPreview.tsx index c6f685b..58f2c92 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; + roomId: 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 f34b066..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; + 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/video-grid/useMediaStream.ts b/src/video-grid/useMediaStream.ts index d3df237..f9c5e47 100644 --- a/src/video-grid/useMediaStream.ts +++ b/src/video-grid/useMediaStream.ts @@ -34,7 +34,7 @@ export const useMediaStream = ( stream: MediaStream, audioOutputDevice: string, mute = false, - localVolume: number + localVolume?: number ): RefObject => { const mediaRef = useRef(); @@ -196,7 +196,7 @@ export const useSpatialMediaStream = ( audioContext: AudioContext, audioDestination: AudioNode, mute = false, - localVolume: number + localVolume?: number ): [RefObject, RefObject] => { const tileRef = useRef(); const [spatialAudio] = useSpatialAudio(); diff --git a/tsconfig.json b/tsconfig.json index 437a94f..bbbe0f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,14 +8,7 @@ "noImplicitAny": false, "noUnusedLocals": true, "jsx": "preserve", - "lib": [ - "es2020", - "dom", - "dom.iterable" - ], + "lib": ["es2020", "dom", "dom.iterable"] }, - "include": [ - "./src/**/*.ts", - "./src/**/*.tsx", - ], + "include": ["./src/**/*.ts", "./src/**/*.tsx"] }