typescript src/room (#437)

This commit is contained in:
Timo 2022-08-02 00:46:16 +02:00 committed by GitHub
parent c723fae0e2
commit 2d99acabe2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 465 additions and 284 deletions

View File

@ -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;
}
}

View File

@ -132,6 +132,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isAuthenticated: Boolean(client),
isPasswordlessUser,
userName: client?.getUserIdLocalpart(),
error: undefined,
});
})
.catch(() => {
@ -141,6 +142,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isAuthenticated: false,
isPasswordlessUser: false,
userName: null,
error: undefined,
});
});
}, []);
@ -170,6 +172,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isAuthenticated: true,
isPasswordlessUser: false,
userName: client.getUserIdLocalpart(),
error: undefined,
});
},
[client]
@ -190,6 +193,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isAuthenticated: true,
isPasswordlessUser: session.passwordlessUser,
userName: newClient.getUserIdLocalpart(),
error: undefined,
});
} else {
clearSession();
@ -200,6 +204,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isAuthenticated: false,
isPasswordlessUser: false,
userName: null,
error: undefined,
});
}
},
@ -258,6 +263,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
logout,
userName,
setClient,
error: undefined,
}),
[
loading,

View File

@ -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<T> extends AriaMenuOptions<T> {
className: String;
onAction: () => void;
onClose: () => void;
className?: String;
onClose?: () => void;
onAction: (value: Key) => void;
label?: string;
}
export function Menu<T extends object>({
className,
onAction,
onClose,
label,
...rest
}: MenuProps<T>) {
const state = useTreeState<T>({ ...rest, selectionMode: "none" });
@ -46,7 +48,7 @@ export function Menu<T extends object>({
interface MenuItemProps<T> {
item: Node<T>;
state: TreeState<T>;
onAction: () => void;
onAction: (value: Key) => void;
onClose: () => void;
}

View File

@ -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<string, {}>;
eventsByUserId: { [userId: string]: SequenceDiagramMatrixEvent[] };
remoteUserIds: string[];
}
@ -33,7 +36,7 @@ export function SequenceDiagramViewerPage() {
const [selectedUserId, setSelectedUserId] = useState<string>();
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));
});
}

View File

@ -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 } =

View File

@ -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("/");
}

View File

@ -74,6 +74,7 @@ interface Props {
children: Element[];
onPress: (e: PressEvent) => void;
onPressStart: (e: PressEvent) => void;
// TODO: add all props for <Button>
[index: string]: unknown;
}
export const Button = forwardRef<HTMLButtonElement, Props>(
@ -136,6 +137,7 @@ export function MicButton({
...rest
}: {
muted: boolean;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
return (
@ -154,6 +156,7 @@ export function VideoButton({
...rest
}: {
muted: boolean;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
return (
@ -174,6 +177,7 @@ export function ScreenshareButton({
}: {
enabled: boolean;
className?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
return (
@ -192,6 +196,7 @@ export function HangupButton({
...rest
}: {
className?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
return (
@ -212,6 +217,7 @@ export function SettingsButton({
...rest
}: {
className?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
return (
@ -228,6 +234,7 @@ export function InviteButton({
...rest
}: {
className?: string;
// TODO: add all props for <Button>
[index: string]: unknown;
}) {
return (

View File

@ -23,9 +23,9 @@ import { Button, ButtonVariant } from "./Button";
interface Props {
value: string;
children?: JSX.Element;
className: string;
variant: ButtonVariant;
children?: JSX.Element | string;
className?: string;
variant?: ButtonVariant;
copiedMessage?: string;
}
export function CopyButton({

View File

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactNode } from "react";
import React, { HTMLAttributes } from "react";
import { Link } from "react-router-dom";
import classNames from "classnames";
import * as H from "history";
import {
variantToClassName,
@ -24,19 +25,21 @@ import {
ButtonVariant,
ButtonSize,
} from "./Button";
interface Props {
className?: string;
variant?: ButtonVariant;
interface Props extends HTMLAttributes<HTMLAnchorElement> {
children: JSX.Element | string;
to: H.LocationDescriptor | ((location: H.Location) => H.LocationDescriptor);
size?: ButtonSize;
children: ReactNode;
[index: string]: unknown;
variant?: ButtonVariant;
className?: string;
}
export function LinkButton({
className,
variant,
size,
children,
to,
size,
variant,
className,
...rest
}: Props) {
return (
@ -46,6 +49,7 @@ export function LinkButton({
sizeToClassName[size],
className
)}
to={to}
{...rest}
>
{children}

View File

@ -46,7 +46,7 @@ export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
{callType === CallType.Video ? "Video call" : "Walkie-talkie call"}
</Headline>
</Button>
{(props) => (
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label="Call type menu" onAction={setCallType}>
<Item key={CallType.Video} textValue="Video call">
<VideoIcon />

View File

@ -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 (
<>
<h1>{`${roomName} - Walkie-talkie call`}</h1>

View File

@ -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 (

View File

@ -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 <Modal>
[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 (
<Modal title="Submit Feedback" isDismissable {...rest}>

View File

@ -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 (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger tooltip={() => "Layout Type"}>
@ -33,7 +39,7 @@ export function GridLayoutMenu({ layout, setLayout }) {
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</Button>
</TooltipTrigger>
{(props) => (
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
<Item key="freedom" textValue="Freedom">
<FreedomIcon />

View File

@ -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<React.SetStateAction<InspectorContextState>>
]
>(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<InspectorContextState>({});
return (
<InspectorContext.Provider value={context}>
{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<HTMLDivElement>();
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<CallEventContent>();
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<string>();
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 (
<Resizable
enable={{ top: true }}
defaultSize={{ height: 200 }}
defaultSize={{ height: 200, width: undefined }}
className={styles.inspector}
>
<div className={styles.toolbar}>

View File

@ -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 <ErrorView error={error} />;
}
return children(groupCall);
return <>{children(groupCall)}</>;
}

View File

@ -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 <ErrorView error={error} />;
@ -142,7 +154,6 @@ export function GroupCallView({
<LobbyView
client={client}
groupCall={groupCall}
hasLocalParticipant={hasLocalParticipant}
roomName={groupCall.room.name}
avatarUrl={avatarUrl}
state={state}

View File

@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useCallback, useMemo, useRef } from "react";
import React, { useCallback, useMemo } from "react";
import { usePreventScroll } from "@react-aria/overlays";
import { GroupCall, MatrixClient } from "matrix-js-sdk";
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import styles from "./InCallView.module.css";
import {
HangupButton,
@ -38,7 +42,6 @@ import { Avatar } from "../Avatar";
import { UserMenuContainer } from "../UserMenuContainer";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { usePreventScroll } from "@react-aria/overlays";
import { useMediaHandler } from "../settings/useMediaHandler";
import { useShowInspector } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal";
@ -50,6 +53,33 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// For now we can disable screensharing in Safari.
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
interface Props {
client: MatrixClient;
groupCall: GroupCall;
roomName: string;
avatarUrl: string;
microphoneMuted: boolean;
localVideoMuted: boolean;
toggleLocalVideoMuted: () => void;
toggleMicrophoneMuted: () => void;
toggleScreensharing: () => void;
userMediaFeeds: CallFeed[];
activeSpeaker: string;
onLeave: () => void;
isScreensharing: boolean;
screenshareFeeds: CallFeed[];
localScreenshareFeed: CallFeed;
roomId: string;
unencryptedEventsFromUsers: Set<string>;
}
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 (
<Avatar
key={roomMember.userId}
size={size}
src={avatarUrl}
fallback={roomMember.name.slice(0, 1).toUpperCase()}
className={styles.avatar}
/>
);
},
[client]
);
return (
<Avatar
key={roomMember.userId}
size={size}
src={avatarUrl}
fallback={roomMember.name.slice(0, 1).toUpperCase()}
className={styles.avatar}
/>
);
}, []);
const {
modalState: rageshakeRequestModalState,
@ -158,7 +188,7 @@ export function InCallView({
</div>
) : (
<VideoGrid items={items} layout={layout} disableAnimations={isSafari}>
{({ item, ...rest }) => (
{({ item, ...rest }: { item: Participant; [x: string]: unknown }) => (
<VideoTileContainer
key={item.id}
item={item}
@ -185,7 +215,6 @@ export function InCallView({
<OverflowMenu
inCall
roomId={roomId}
client={client}
groupCall={groupCall}
showInvite={true}
feedbackModalState={feedbackModalState}

View File

@ -15,12 +15,19 @@ limitations under the License.
*/
import React from "react";
import { Modal, ModalContent } from "../Modal";
import { CopyButton } from "../button";
import { getRoomUrl } from "../matrix-utils";
import styles from "./InviteModal.module.css";
export function InviteModal({ roomId, ...rest }) {
export function InviteModal({
roomId,
...rest
}: {
roomId: string;
[x: string]: unknown;
}) {
return (
<Modal
title="Invite People"

View File

@ -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;
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<HTMLButtonElement>();
useEffect(() => {
if (state === GroupCallState.LocalCallFeedInitialized) {

View File

@ -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({
<OverflowIcon />
</Button>
</TooltipTrigger>
{(props) => (
<Menu {...props} label="More menu" onAction={onAction}>
{(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label="more menu" onAction={onAction}>
{showInvite && (
<Item key="invite" textValue="Invite people">
<AddUserIcon />

View File

@ -206,7 +206,6 @@ export const PTTCallView: React.FC<Props> = ({
<OverflowMenu
inCall
roomId={roomId}
client={client}
groupCall={groupCall}
showInvite={false}
feedbackModalState={feedbackModalState}

View File

@ -14,12 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import React from "react";
import { useCallFeed } from "../video-grid/useCallFeed";
import { useMediaStream } from "../video-grid/useMediaStream";
import styles from "./PTTFeed.module.css";
export function PTTFeed({ callFeed, audioOutputDevice }) {
export function PTTFeed({
callFeed,
audioOutputDevice,
}: {
callFeed: CallFeed;
audioOutputDevice: string;
}) {
const { isLocal, stream } = useCallFeed(callFeed);
const mediaRef = useMediaStream(stream, audioOutputDevice, isLocal);
return <audio ref={mediaRef} className={styles.audioFeed} playsInline />;

View File

@ -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 (
<Modal title="Debug Log Request" isDismissable {...rest}>

View File

@ -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<Error>();
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);

View File

@ -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(() => {

View File

@ -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";

View File

@ -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<string>();
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);

View File

@ -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({
/>
<OverflowMenu
roomId={roomId}
client={client}
feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps}
inCall={false}
groupCall={undefined}
showInvite={false}
/>
</div>
</>

View File

@ -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,

View File

@ -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);

View File

@ -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]);

View File

@ -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) => {

View File

@ -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" });

View File

@ -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 [

View File

@ -34,7 +34,7 @@ export const useMediaStream = (
stream: MediaStream,
audioOutputDevice: string,
mute = false,
localVolume: number
localVolume?: number
): RefObject<MediaElement> => {
const mediaRef = useRef<MediaElement>();
@ -196,7 +196,7 @@ export const useSpatialMediaStream = (
audioContext: AudioContext,
audioDestination: AudioNode,
mute = false,
localVolume: number
localVolume?: number
): [RefObject<Element>, RefObject<MediaElement>] => {
const tileRef = useRef<Element>();
const [spatialAudio] = useSpatialAudio();

View File

@ -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"]
}