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 // TypeScript doesn't know about the experimental setSinkId method, so we
// declare it ourselves // declare it ourselves
interface MediaElement extends HTMLMediaElement { interface MediaElement extends HTMLVideoElement {
setSinkId: (id: string) => void; setSinkId: (id: string) => void;
} }
} }

View file

@ -132,6 +132,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isAuthenticated: Boolean(client), isAuthenticated: Boolean(client),
isPasswordlessUser, isPasswordlessUser,
userName: client?.getUserIdLocalpart(), userName: client?.getUserIdLocalpart(),
error: undefined,
}); });
}) })
.catch(() => { .catch(() => {
@ -141,6 +142,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isAuthenticated: false, isAuthenticated: false,
isPasswordlessUser: false, isPasswordlessUser: false,
userName: null, userName: null,
error: undefined,
}); });
}); });
}, []); }, []);
@ -170,6 +172,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isAuthenticated: true, isAuthenticated: true,
isPasswordlessUser: false, isPasswordlessUser: false,
userName: client.getUserIdLocalpart(), userName: client.getUserIdLocalpart(),
error: undefined,
}); });
}, },
[client] [client]
@ -190,6 +193,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isAuthenticated: true, isAuthenticated: true,
isPasswordlessUser: session.passwordlessUser, isPasswordlessUser: session.passwordlessUser,
userName: newClient.getUserIdLocalpart(), userName: newClient.getUserIdLocalpart(),
error: undefined,
}); });
} else { } else {
clearSession(); clearSession();
@ -200,6 +204,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
isAuthenticated: false, isAuthenticated: false,
isPasswordlessUser: false, isPasswordlessUser: false,
userName: null, userName: null,
error: undefined,
}); });
} }
}, },
@ -258,6 +263,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
logout, logout,
userName, userName,
setClient, setClient,
error: undefined,
}), }),
[ [
loading, 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 { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu";
import { TreeState, useTreeState } from "@react-stately/tree"; import { TreeState, useTreeState } from "@react-stately/tree";
import { mergeProps } from "@react-aria/utils"; import { mergeProps } from "@react-aria/utils";
@ -9,15 +9,17 @@ import { Node } from "@react-types/shared";
import styles from "./Menu.module.css"; import styles from "./Menu.module.css";
interface MenuProps<T> extends AriaMenuOptions<T> { interface MenuProps<T> extends AriaMenuOptions<T> {
className: String; className?: String;
onAction: () => void; onClose?: () => void;
onClose: () => void; onAction: (value: Key) => void;
label?: string;
} }
export function Menu<T extends object>({ export function Menu<T extends object>({
className, className,
onAction, onAction,
onClose, onClose,
label,
...rest ...rest
}: MenuProps<T>) { }: MenuProps<T>) {
const state = useTreeState<T>({ ...rest, selectionMode: "none" }); const state = useTreeState<T>({ ...rest, selectionMode: "none" });
@ -46,7 +48,7 @@ export function Menu<T extends object>({
interface MenuItemProps<T> { interface MenuItemProps<T> {
item: Node<T>; item: Node<T>;
state: TreeState<T>; state: TreeState<T>;
onAction: () => void; onAction: (value: Key) => void;
onClose: () => void; onClose: () => void;
} }

View file

@ -16,13 +16,16 @@ limitations under the License.
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { SequenceDiagramViewer } from "./room/GroupCallInspector"; import {
SequenceDiagramViewer,
SequenceDiagramMatrixEvent,
} from "./room/GroupCallInspector";
import { FieldRow, InputField } from "./input/Input"; import { FieldRow, InputField } from "./input/Input";
import { usePageTitle } from "./usePageTitle"; import { usePageTitle } from "./usePageTitle";
interface DebugLog { interface DebugLog {
localUserId: string; localUserId: string;
eventsByUserId: Record<string, {}>; eventsByUserId: { [userId: string]: SequenceDiagramMatrixEvent[] };
remoteUserIds: string[]; remoteUserIds: string[];
} }
@ -33,7 +36,7 @@ export function SequenceDiagramViewerPage() {
const [selectedUserId, setSelectedUserId] = useState<string>(); const [selectedUserId, setSelectedUserId] = useState<string>();
const onChangeDebugLog = useCallback((e) => { const onChangeDebugLog = useCallback((e) => {
if (e.target.files && e.target.files.length > 0) { 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)); setDebugLog(JSON.parse(text));
}); });
} }

View file

@ -11,7 +11,7 @@ interface Props {
preventNavigation?: boolean; preventNavigation?: boolean;
} }
export function UserMenuContainer({ preventNavigation }: Props) { export function UserMenuContainer({ preventNavigation = false }: Props) {
const location = useLocation(); const location = useLocation();
const history = useHistory(); const history = useHistory();
const { isAuthenticated, isPasswordlessUser, logout, userName, client } = const { isAuthenticated, isPasswordlessUser, logout, userName, client } =

View file

@ -100,7 +100,7 @@ export const RegisterPage: FC = () => {
submit() submit()
.then(() => { .then(() => {
if (location.state?.from) { if (location.state?.from) {
history.push(location.state.from); history.push(location.state?.from);
} else { } else {
history.push("/"); history.push("/");
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -15,12 +15,24 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import styles from "./AudioPreview.module.css";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall"; import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { SelectInput } from "../input/SelectInput";
import { Item } from "@react-stately/collections"; import { Item } from "@react-stately/collections";
import styles from "./AudioPreview.module.css";
import { SelectInput } from "../input/SelectInput";
import { Body } from "../typography/Typography"; 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({ export function AudioPreview({
state, state,
roomName, roomName,
@ -30,7 +42,7 @@ export function AudioPreview({
audioOutput, audioOutput,
audioOutputs, audioOutputs,
setAudioOutput, setAudioOutput,
}) { }: Props) {
return ( return (
<> <>
<h1>{`${roomName} - Walkie-talkie call`}</h1> <h1>{`${roomName} - Walkie-talkie call`}</h1>

View file

@ -15,13 +15,15 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { MatrixClient } from "matrix-js-sdk";
import styles from "./CallEndedView.module.css"; import styles from "./CallEndedView.module.css";
import { LinkButton } from "../button"; import { LinkButton } from "../button";
import { useProfile } from "../profile/useProfile"; import { useProfile } from "../profile/useProfile";
import { Subtitle, Body, Link, Headline } from "../typography/Typography"; import { Subtitle, Body, Link, Headline } from "../typography/Typography";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header"; import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
export function CallEndedView({ client }) { export function CallEndedView({ client }: { client: MatrixClient }) {
const { displayName } = useProfile(client); const { displayName } = useProfile(client);
return ( return (

View file

@ -15,6 +15,8 @@ limitations under the License.
*/ */
import React, { useCallback, useEffect } from "react"; import React, { useCallback, useEffect } from "react";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { Modal, ModalContent } from "../Modal"; import { Modal, ModalContent } from "../Modal";
import { Button } from "../button"; import { Button } from "../button";
import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { FieldRow, InputField, ErrorMessage } from "../input/Input";
@ -23,9 +25,14 @@ import {
useRageshakeRequest, useRageshakeRequest,
} from "../settings/submit-rageshake"; } from "../settings/submit-rageshake";
import { Body } from "../typography/Typography"; import { Body } from "../typography/Typography";
import { randomString } from "matrix-js-sdk/src/randomstring"; interface Props {
inCall: boolean;
export function FeedbackModal({ inCall, roomId, ...rest }) { 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 { submitRageshake, sending, sent, error } = useSubmitRageshake();
const sendRageshakeRequest = useRageshakeRequest(); const sendRageshakeRequest = useRageshakeRequest();
@ -33,8 +40,12 @@ export function FeedbackModal({ inCall, roomId, ...rest }) {
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
const data = new FormData(e.target); const data = new FormData(e.target);
const description = data.get("description"); const descriptionData = data.get("description");
const sendLogs = data.get("sendLogs"); const description =
typeof descriptionData === "string" ? descriptionData : "";
const sendLogsData = data.get("sendLogs");
const sendLogs =
typeof sendLogsData === "string" ? sendLogsData === "true" : false;
const rageshakeRequestId = randomString(16); const rageshakeRequestId = randomString(16);
submitRageshake({ submitRageshake({
@ -53,9 +64,9 @@ export function FeedbackModal({ inCall, roomId, ...rest }) {
useEffect(() => { useEffect(() => {
if (sent) { if (sent) {
rest.onClose(); onClose();
} }
}, [sent, rest.onClose]); }, [sent, onClose]);
return ( return (
<Modal title="Submit Feedback" isDismissable {...rest}> <Modal title="Submit Feedback" isDismissable {...rest}>

View file

@ -15,6 +15,8 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { Item } from "@react-stately/collections";
import { Button } from "../button"; import { Button } from "../button";
import { PopoverMenuTrigger } from "../popover/PopoverMenu"; import { PopoverMenuTrigger } from "../popover/PopoverMenu";
import { ReactComponent as SpotlightIcon } from "../icons/Spotlight.svg"; 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 { ReactComponent as CheckIcon } from "../icons/Check.svg";
import menuStyles from "../Menu.module.css"; import menuStyles from "../Menu.module.css";
import { Menu } from "../Menu"; import { Menu } from "../Menu";
import { Item } from "@react-stately/collections"; import { TooltipTrigger } from "../Tooltip";
import { Tooltip, 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 ( return (
<PopoverMenuTrigger placement="bottom right"> <PopoverMenuTrigger placement="bottom right">
<TooltipTrigger tooltip={() => "Layout Type"}> <TooltipTrigger tooltip={() => "Layout Type"}>
@ -33,7 +39,7 @@ export function GridLayoutMenu({ layout, setLayout }) {
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />} {layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
{(props) => ( {(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label="Grid layout menu" onAction={setLayout}> <Menu {...props} label="Grid layout menu" onAction={setLayout}>
<Item key="freedom" textValue="Freedom"> <Item key="freedom" textValue="Freedom">
<FreedomIcon /> <FreedomIcon />

View file

@ -22,40 +22,26 @@ import React, {
useRef, useRef,
createContext, createContext,
useContext, useContext,
Dispatch,
} from "react"; } from "react";
import ReactJson from "react-json-view"; import ReactJson, { CollapsedFieldProps } from "react-json-view";
import mermaid from "mermaid"; 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 styles from "./GroupCallInspector.module.css";
import { SelectInput } from "../input/SelectInput"; import { SelectInput } from "../input/SelectInput";
import { Item } from "@react-stately/collections";
function getCallUserId(call) { interface InspectorContextState {
return call.getOpponentMember()?.userId || call.invitee || null; 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 = [ const defaultCollapsedFields = [
"org.matrix.msc3401.call", "org.matrix.msc3401.call",
"org.matrix.msc3401.call.member", "org.matrix.msc3401.call.member",
@ -67,19 +53,19 @@ const defaultCollapsedFields = [
"content", "content",
]; ];
function shouldCollapse({ name, src, type, namespace }) { function shouldCollapse({ name }: CollapsedFieldProps) {
return defaultCollapsedFields.includes(name); return defaultCollapsedFields.includes(name);
} }
function getUserName(userId) { function getUserName(userId: string) {
const match = userId.match(/@([^\:]+):/); const match = userId.match(/@([^:]+):/);
return match && match.length > 0 return match && match.length > 0
? match[1].replace("-", " ").replace(/\W/g, "") ? match[1].replace("-", " ").replace(/\W/g, "")
: userId.replace(/\W/g, ""); : userId.replace(/\W/g, "");
} }
function formatContent(type, content) { function formatContent(type: string, content: CallEventContent) {
if (type === "m.call.hangup") { if (type === "m.call.hangup") {
return `callId: ${content.call_id.slice(-4)} reason: ${ return `callId: ${content.call_id.slice(-4)} reason: ${
content.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); return dateFormatter.format(timestamp);
} }
export const InspectorContext = createContext(); export const InspectorContext =
createContext<
[
InspectorContextState,
React.Dispatch<React.SetStateAction<InspectorContextState>>
]
>(undefined);
export function InspectorContextProvider({ children }) { export function InspectorContextProvider({
const context = useState({}); children,
}: {
children: React.ReactNode;
}) {
// The context will be initialized empty.
// It is then set from within GroupCallInspector.
const context = useState<InspectorContextState>({});
return ( return (
<InspectorContext.Provider value={context}> <InspectorContext.Provider value={context}>
{children} {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({ export function SequenceDiagramViewer({
localUserId, localUserId,
remoteUserIds, remoteUserIds,
selectedUserId, selectedUserId,
onSelectUserId, onSelectUserId,
events, events,
}) { }: SequenceDiagramViewerProps) {
const mermaidElRef = useRef(); const mermaidElRef = useRef<HTMLDivElement>();
useEffect(() => { useEffect(() => {
mermaid.initialize({ 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; mermaidElRef.current.innerHTML = svgCode;
}); });
}, [events, localUserId, selectedUserId]); }, [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) { switch (action.type) {
case "receive_room_state_event": { case RoomStateEvent.Events: {
const { event, callStateEvent, memberStateEvents } = action; const { event, callStateEvent, memberStateEvents } = action;
let eventsByUserId = state.eventsByUserId; let eventsByUserId = state.eventsByUserId;
@ -247,12 +291,12 @@ function reducer(state, action) {
), ),
}; };
} }
case "received_voip_event": { case ClientEvent.ReceivedVoipEvent: {
const event = action.event; const event = action.event;
const eventsByUserId = { ...state.eventsByUserId }; const eventsByUserId = { ...state.eventsByUserId };
const fromId = event.getSender(); const fromId = event.getSender();
const toId = state.localUserId; const toId = state.localUserId;
const content = event.getContent(); const content = event.getContent<CallEventContent>();
const remoteUserIds = eventsByUserId[fromId] const remoteUserIds = eventsByUserId[fromId]
? state.remoteUserIds ? state.remoteUserIds
@ -272,11 +316,11 @@ function reducer(state, action) {
return { ...state, eventsByUserId, remoteUserIds }; return { ...state, eventsByUserId, remoteUserIds };
} }
case "send_voip_event": { case CallEvent.SendVoipEvent: {
const event = action.event; const event = action.event;
const eventsByUserId = { ...state.eventsByUserId }; const eventsByUserId = { ...state.eventsByUserId };
const fromId = state.localUserId; const fromId = state.localUserId;
const toId = event.userId; const toId = event.target.userId; // was .user
const remoteUserIds = eventsByUserId[toId] const remoteUserIds = eventsByUserId[toId]
? state.remoteUserIds ? state.remoteUserIds
@ -287,8 +331,8 @@ function reducer(state, action) {
{ {
from: fromId, from: fromId,
to: toId, to: toId,
type: event.eventType, type: event.getType(),
content: event.content, content: event.getContent(),
timestamp: Date.now(), timestamp: Date.now(),
ignored: false, 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, { const [state, dispatch] = useReducer(reducer, {
localUserId: client.getUserId(), localUserId: client.getUserId(),
localSessionId: client.getSessionId(), localSessionId: client.getSessionId(),
@ -312,7 +360,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
}); });
useEffect(() => { useEffect(() => {
function onUpdateRoomState(event) { function onUpdateRoomState(event?: MatrixEvent) {
const callStateEvent = groupCall.room.currentState.getStateEvents( const callStateEvent = groupCall.room.currentState.getStateEvents(
"org.matrix.msc3401.call", "org.matrix.msc3401.call",
groupCall.groupCallId groupCall.groupCallId
@ -323,120 +371,60 @@ function useGroupCallState(client, groupCall, pollCallStats) {
); );
dispatch({ dispatch({
type: "receive_room_state_event", type: RoomStateEvent.Events,
event, event,
callStateEvent, callStateEvent,
memberStateEvents, memberStateEvents,
}); });
} }
// function onCallsChanged() { function onReceivedVoipEvent(event: MatrixEvent) {
// const calls = groupCall.calls.reduce((obj, call) => { dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
// 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 onSendVoipEvent(event) { function onSendVoipEvent(event: MatrixEvent) {
dispatch({ type: "send_voip_event", event }); dispatch({ type: CallEvent.SendVoipEvent, event });
} }
client.on(RoomStateEvent.Events, onUpdateRoomState);
client.on("RoomState.events", onUpdateRoomState);
//groupCall.on("calls_changed", onCallsChanged); //groupCall.on("calls_changed", onCallsChanged);
groupCall.on("send_voip_event", onSendVoipEvent); groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent);
//client.on("state", onCallsChanged); //client.on("state", onCallsChanged);
//client.on("hangup", onCallHangup); //client.on("hangup", onCallHangup);
client.on("received_voip_event", onReceivedVoipEvent); client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
onUpdateRoomState(); onUpdateRoomState();
return () => { return () => {
client.removeListener("RoomState.events", onUpdateRoomState); client.removeListener(RoomStateEvent.Events, onUpdateRoomState);
//groupCall.removeListener("calls_changed", onCallsChanged); //groupCall.removeListener("calls_changed", onCallsChanged);
groupCall.removeListener("send_voip_event", onSendVoipEvent); groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent);
//client.removeListener("state", onCallsChanged); //client.removeListener("state", onCallsChanged);
//client.removeListener("hangup", onCallHangup); //client.removeListener("hangup", onCallHangup);
client.removeListener("received_voip_event", onReceivedVoipEvent); client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
}; };
}, [client, groupCall]); }, [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; return state;
} }
interface GroupCallInspectorProps {
export function GroupCallInspector({ client, groupCall, show }) { client: MatrixClient;
groupCall: GroupCall;
show: boolean;
}
export function GroupCallInspector({
client,
groupCall,
show,
}: GroupCallInspectorProps) {
const [currentTab, setCurrentTab] = useState("sequence-diagrams"); const [currentTab, setCurrentTab] = useState("sequence-diagrams");
const [selectedUserId, setSelectedUserId] = useState(); const [selectedUserId, setSelectedUserId] = useState<string>();
const state = useGroupCallState(client, groupCall, show); const state = useGroupCallState(client, groupCall, show);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, setState] = useContext(InspectorContext); const [_, setState] = useContext(InspectorContext);
useEffect(() => { useEffect(() => {
setState({ json: state }); setState(state);
}, [setState, state]); }, [setState, state]);
if (!show) { if (!show) {
@ -446,7 +434,7 @@ export function GroupCallInspector({ client, groupCall, show }) {
return ( return (
<Resizable <Resizable
enable={{ top: true }} enable={{ top: true }}
defaultSize={{ height: 200 }} defaultSize={{ height: 200, width: undefined }}
className={styles.inspector} className={styles.inspector}
> >
<div className={styles.toolbar}> <div className={styles.toolbar}>

View file

@ -14,18 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { useLoadGroupCall } from "./useLoadGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView"; import { ErrorView, FullScreenView } from "../FullScreenView";
import { usePageTitle } from "../usePageTitle"; import { usePageTitle } from "../usePageTitle";
interface Props {
client: MatrixClient;
roomId: string;
viaServers: string[];
children: (groupCall: GroupCall) => ReactNode;
createPtt: boolean;
}
export function GroupCallLoader({ export function GroupCallLoader({
client, client,
roomId, roomId,
viaServers, viaServers,
createPtt,
children, children,
}) { createPtt,
}: Props): JSX.Element {
const { loading, error, groupCall } = useLoadGroupCall( const { loading, error, groupCall } = useLoadGroupCall(
client, client,
roomId, roomId,
@ -47,5 +57,5 @@ export function GroupCallLoader({
return <ErrorView error={error} />; 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 React, { useCallback, useEffect, useState } from "react";
import { useHistory } from "react-router-dom"; 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 { useGroupCall } from "./useGroupCall";
import { ErrorView, FullScreenView } from "../FullScreenView"; import { ErrorView, FullScreenView } from "../FullScreenView";
import { LobbyView } from "./LobbyView"; import { LobbyView } from "./LobbyView";
@ -26,14 +28,25 @@ import { CallEndedView } from "./CallEndedView";
import { useRoomAvatar } from "./useRoomAvatar"; import { useRoomAvatar } from "./useRoomAvatar";
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler"; import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
import { useLocationNavigation } from "../useLocationNavigation"; 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({ export function GroupCallView({
client, client,
isPasswordlessUser, isPasswordlessUser,
isEmbedded, isEmbedded,
roomId, roomId,
groupCall, groupCall,
}) { }: Props) {
const { const {
state, state,
error, error,
@ -52,7 +65,6 @@ export function GroupCallView({
isScreensharing, isScreensharing,
localScreenshareFeed, localScreenshareFeed,
screenshareFeeds, screenshareFeeds,
hasLocalParticipant,
participants, participants,
unencryptedEventsFromUsers, unencryptedEventsFromUsers,
} = useGroupCall(groupCall); } = useGroupCall(groupCall);
@ -80,7 +92,7 @@ export function GroupCallView({
if (!isPasswordlessUser) { if (!isPasswordlessUser) {
history.push("/"); history.push("/");
} }
}, [leave, history]); }, [leave, isPasswordlessUser, history]);
if (error) { if (error) {
return <ErrorView error={error} />; return <ErrorView error={error} />;
@ -142,7 +154,6 @@ export function GroupCallView({
<LobbyView <LobbyView
client={client} client={client}
groupCall={groupCall} groupCall={groupCall}
hasLocalParticipant={hasLocalParticipant}
roomName={groupCall.room.name} roomName={groupCall.room.name}
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
state={state} state={state}

View file

@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. 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 styles from "./InCallView.module.css";
import { import {
HangupButton, HangupButton,
@ -38,7 +42,6 @@ import { Avatar } from "../Avatar";
import { UserMenuContainer } from "../UserMenuContainer"; import { UserMenuContainer } from "../UserMenuContainer";
import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal"; import { RageshakeRequestModal } from "./RageshakeRequestModal";
import { usePreventScroll } from "@react-aria/overlays";
import { useMediaHandler } from "../settings/useMediaHandler"; import { useMediaHandler } from "../settings/useMediaHandler";
import { useShowInspector } from "../settings/useSetting"; import { useShowInspector } from "../settings/useSetting";
import { useModalTriggerState } from "../Modal"; import { useModalTriggerState } from "../Modal";
@ -50,6 +53,33 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
// For now we can disable screensharing in Safari. // For now we can disable screensharing in Safari.
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 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({ export function InCallView({
client, client,
groupCall, groupCall,
@ -65,9 +95,10 @@ export function InCallView({
toggleScreensharing, toggleScreensharing,
isScreensharing, isScreensharing,
screenshareFeeds, screenshareFeeds,
localScreenshareFeed,
roomId, roomId,
unencryptedEventsFromUsers, unencryptedEventsFromUsers,
}) { }: Props) {
usePreventScroll(); usePreventScroll();
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0); const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
@ -79,7 +110,7 @@ export function InCallView({
useModalTriggerState(); useModalTriggerState();
const items = useMemo(() => { const items = useMemo(() => {
const participants = []; const participants: Participant[] = [];
for (const callFeed of userMediaFeeds) { for (const callFeed of userMediaFeeds) {
participants.push({ participants.push({
@ -90,6 +121,7 @@ export function InCallView({
? callFeed.userId === activeSpeaker ? callFeed.userId === activeSpeaker
: false, : false,
isLocal: callFeed.isLocal(), isLocal: callFeed.isLocal(),
presenter: false,
}); });
} }
@ -107,29 +139,27 @@ export function InCallView({
callFeed, callFeed,
focused: true, focused: true,
isLocal: callFeed.isLocal(), isLocal: callFeed.isLocal(),
presenter: false,
}); });
} }
return participants; return participants;
}, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]); }, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
const renderAvatar = useCallback( const renderAvatar = useCallback((roomMember, width, height) => {
(roomMember, width, height) => { const avatarUrl = roomMember.user?.avatarUrl;
const avatarUrl = roomMember.user?.avatarUrl; const size = Math.round(Math.min(width, height) / 2);
const size = Math.round(Math.min(width, height) / 2);
return ( return (
<Avatar <Avatar
key={roomMember.userId} key={roomMember.userId}
size={size} size={size}
src={avatarUrl} src={avatarUrl}
fallback={roomMember.name.slice(0, 1).toUpperCase()} fallback={roomMember.name.slice(0, 1).toUpperCase()}
className={styles.avatar} className={styles.avatar}
/> />
); );
}, }, []);
[client]
);
const { const {
modalState: rageshakeRequestModalState, modalState: rageshakeRequestModalState,
@ -158,7 +188,7 @@ export function InCallView({
</div> </div>
) : ( ) : (
<VideoGrid items={items} layout={layout} disableAnimations={isSafari}> <VideoGrid items={items} layout={layout} disableAnimations={isSafari}>
{({ item, ...rest }) => ( {({ item, ...rest }: { item: Participant; [x: string]: unknown }) => (
<VideoTileContainer <VideoTileContainer
key={item.id} key={item.id}
item={item} item={item}
@ -185,7 +215,6 @@ export function InCallView({
<OverflowMenu <OverflowMenu
inCall inCall
roomId={roomId} roomId={roomId}
client={client}
groupCall={groupCall} groupCall={groupCall}
showInvite={true} showInvite={true}
feedbackModalState={feedbackModalState} feedbackModalState={feedbackModalState}

View file

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

View file

@ -15,10 +15,14 @@ limitations under the License.
*/ */
import React, { useEffect, useRef } from "react"; 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 styles from "./LobbyView.module.css";
import { Button, CopyButton } from "../button"; import { Button, CopyButton } from "../button";
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header"; import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
import { useCallFeed } from "../video-grid/useCallFeed"; import { useCallFeed } from "../video-grid/useCallFeed";
import { getRoomUrl } from "../matrix-utils"; import { getRoomUrl } from "../matrix-utils";
import { UserMenuContainer } from "../UserMenuContainer"; import { UserMenuContainer } from "../UserMenuContainer";
@ -28,6 +32,22 @@ import { useMediaHandler } from "../settings/useMediaHandler";
import { VideoPreview } from "./VideoPreview"; import { VideoPreview } from "./VideoPreview";
import { AudioPreview } from "./AudioPreview"; 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({ export function LobbyView({
client, client,
groupCall, groupCall,
@ -43,7 +63,7 @@ export function LobbyView({
toggleMicrophoneMuted, toggleMicrophoneMuted,
roomId, roomId,
isEmbedded, isEmbedded,
}) { }: Props) {
const { stream } = useCallFeed(localCallFeed); const { stream } = useCallFeed(localCallFeed);
const { const {
audioInput, audioInput,
@ -60,7 +80,7 @@ export function LobbyView({
useLocationNavigation(state === GroupCallState.InitializingLocalCallFeed); useLocationNavigation(state === GroupCallState.InitializingLocalCallFeed);
const joinCallButtonRef = useRef(); const joinCallButtonRef = useRef<HTMLButtonElement>();
useEffect(() => { useEffect(() => {
if (state === GroupCallState.LocalCallFeedInitialized) { if (state === GroupCallState.LocalCallFeedInitialized) {

View file

@ -15,10 +15,13 @@ limitations under the License.
*/ */
import React, { useCallback } from "react"; 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 { Button } from "../button";
import { Menu } from "../Menu"; import { Menu } from "../Menu";
import { PopoverMenuTrigger } from "../popover/PopoverMenu"; import { PopoverMenuTrigger } from "../popover/PopoverMenu";
import { Item } from "@react-stately/collections";
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg"; import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg"; import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg"; import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
@ -28,7 +31,17 @@ import { SettingsModal } from "../settings/SettingsModal";
import { InviteModal } from "./InviteModal"; import { InviteModal } from "./InviteModal";
import { TooltipTrigger } from "../Tooltip"; import { TooltipTrigger } from "../Tooltip";
import { FeedbackModal } from "./FeedbackModal"; import { FeedbackModal } from "./FeedbackModal";
interface Props {
roomId: string;
inCall: boolean;
groupCall: GroupCall;
showInvite: boolean;
feedbackModalState: OverlayTriggerState;
feedbackModalProps: {
isOpen: boolean;
onClose: () => void;
};
}
export function OverflowMenu({ export function OverflowMenu({
roomId, roomId,
inCall, inCall,
@ -36,27 +49,46 @@ export function OverflowMenu({
showInvite, showInvite,
feedbackModalState, feedbackModalState,
feedbackModalProps, feedbackModalProps,
}) { }: Props) {
const { modalState: inviteModalState, modalProps: inviteModalProps } = const {
useModalTriggerState(); modalState: inviteModalState,
const { modalState: settingsModalState, modalProps: settingsModalProps } = modalProps: inviteModalProps,
useModalTriggerState(); }: {
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 // TODO: On closing modal, focus should be restored to the trigger button
// https://github.com/adobe/react-spectrum/issues/2444 // https://github.com/adobe/react-spectrum/issues/2444
const onAction = useCallback((key) => { const onAction = useCallback(
switch (key) { (key) => {
case "invite": switch (key) {
inviteModalState.open(); case "invite":
break; inviteModalState.open();
case "settings": break;
settingsModalState.open(); case "settings":
break; settingsModalState.open();
case "feedback": break;
feedbackModalState.open(); case "feedback":
break; feedbackModalState.open();
} break;
}); }
},
[feedbackModalState, inviteModalState, settingsModalState]
);
return ( return (
<> <>
@ -66,8 +98,8 @@ export function OverflowMenu({
<OverflowIcon /> <OverflowIcon />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
{(props) => ( {(props: JSX.IntrinsicAttributes) => (
<Menu {...props} label="More menu" onAction={onAction}> <Menu {...props} label="more menu" onAction={onAction}>
{showInvite && ( {showInvite && (
<Item key="invite" textValue="Invite people"> <Item key="invite" textValue="Invite people">
<AddUserIcon /> <AddUserIcon />

View file

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

View file

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

View file

@ -15,20 +15,30 @@ limitations under the License.
*/ */
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Modal, ModalContent } from "../Modal"; import { Modal, ModalContent } from "../Modal";
import { Button } from "../button"; import { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input"; import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake"; import { useSubmitRageshake } from "../settings/submit-rageshake";
import { Body } from "../typography/Typography"; 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(); const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => { useEffect(() => {
if (sent) { if (sent) {
rest.onClose(); rest.onClose();
} }
}, [sent, rest.onClose]); }, [sent, rest]);
return ( return (
<Modal title="Debug Log Request" isDismissable {...rest}> <Modal title="Debug Log Request" isDismissable {...rest}>

View file

@ -15,11 +15,12 @@ limitations under the License.
*/ */
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { useLocation } from "react-router-dom";
import styles from "./RoomAuthView.module.css"; import styles from "./RoomAuthView.module.css";
import { Button } from "../button"; import { Button } from "../button";
import { Body, Caption, Link, Headline } from "../typography/Typography"; import { Body, Caption, Link, Headline } from "../typography/Typography";
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header"; import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
import { useLocation } from "react-router-dom";
import { FieldRow, InputField, ErrorMessage } from "../input/Input"; import { FieldRow, InputField, ErrorMessage } from "../input/Input";
import { Form } from "../form/Form"; import { Form } from "../form/Form";
import { UserMenuContainer } from "../UserMenuContainer"; import { UserMenuContainer } from "../UserMenuContainer";
@ -27,7 +28,7 @@ import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser
export function RoomAuthView() { export function RoomAuthView() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(); const [error, setError] = useState<Error>();
const { registerPasswordlessUser, recaptchaId, privacyPolicyUrl } = const { registerPasswordlessUser, recaptchaId, privacyPolicyUrl } =
useRegisterPasswordlessUser(); useRegisterPasswordlessUser();
@ -36,7 +37,9 @@ export function RoomAuthView() {
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
const data = new FormData(e.target); 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) => { registerPasswordlessUser(displayName).catch((error) => {
console.error("Failed to register passwordless user", e); 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 React, { useEffect, useMemo, useState } from "react";
import { useLocation, useParams } from "react-router-dom"; import { useLocation, useParams } from "react-router-dom";
import { useClient } from "../ClientContext"; import { useClient } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView"; import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView"; import { RoomAuthView } from "./RoomAuthView";
@ -29,7 +30,7 @@ export function RoomPage() {
useClient(); useClient();
const { roomId: maybeRoomId } = useParams(); const { roomId: maybeRoomId } = useParams();
const { hash, search } = useLocation(); const { hash, search }: { hash: string; search: string } = useLocation();
const [viaServers, isEmbedded, isPtt, displayName] = useMemo(() => { const [viaServers, isEmbedded, isPtt, displayName] = useMemo(() => {
const params = new URLSearchParams(search); const params = new URLSearchParams(search);
return [ return [
@ -40,8 +41,7 @@ export function RoomPage() {
]; ];
}, [search]); }, [search]);
const roomId = (maybeRoomId || hash || "").toLowerCase(); const roomId = (maybeRoomId || hash || "").toLowerCase();
const { registerPasswordlessUser, recaptchaId } = const { registerPasswordlessUser } = useRegisterPasswordlessUser();
useRegisterPasswordlessUser();
const [isRegistering, setIsRegistering] = useState(false); const [isRegistering, setIsRegistering] = useState(false);
useEffect(() => { useEffect(() => {

View file

@ -16,6 +16,7 @@ limitations under the License.
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { useLocation, useHistory } from "react-router-dom"; import { useLocation, useHistory } from "react-router-dom";
import { defaultHomeserverHost } from "../matrix-utils"; import { defaultHomeserverHost } from "../matrix-utils";
import { LoadingView } from "../FullScreenView"; import { LoadingView } from "../FullScreenView";

View file

@ -16,11 +16,11 @@ limitations under the License.
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
function leftPad(value) { function leftPad(value: number): string {
return value < 10 ? "0" + value : value; return value < 10 ? "0" + value : "" + value;
} }
function formatTime(msElapsed) { function formatTime(msElapsed: number): string {
const secondsElapsed = msElapsed / 1000; const secondsElapsed = msElapsed / 1000;
const hours = Math.floor(secondsElapsed / 3600); const hours = Math.floor(secondsElapsed / 3600);
const minutes = Math.floor(secondsElapsed / 60) - hours * 60; const minutes = Math.floor(secondsElapsed / 60) - hours * 60;
@ -28,15 +28,15 @@ function formatTime(msElapsed) {
return `${leftPad(hours)}:${leftPad(minutes)}:${leftPad(seconds)}`; return `${leftPad(hours)}:${leftPad(minutes)}:${leftPad(seconds)}`;
} }
export function Timer({ value }) { export function Timer({ value }: { value: string }) {
const [timestamp, setTimestamp] = useState(); const [timestamp, setTimestamp] = useState<string>();
useEffect(() => { useEffect(() => {
const startTimeMs = performance.now(); const startTimeMs = performance.now();
let animationFrame; let animationFrame: number;
function onUpdate(curTimeMs) { function onUpdate(curTimeMs: number) {
const msElapsed = curTimeMs - startTimeMs; const msElapsed = curTimeMs - startTimeMs;
setTimestamp(formatTime(msElapsed)); setTimestamp(formatTime(msElapsed));
animationFrame = requestAnimationFrame(onUpdate); animationFrame = requestAnimationFrame(onUpdate);

View file

@ -15,18 +15,31 @@ limitations under the License.
*/ */
import React from "react"; 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 { MicButton, VideoButton } from "../button";
import { useMediaStream } from "../video-grid/useMediaStream"; import { useMediaStream } from "../video-grid/useMediaStream";
import { OverflowMenu } from "./OverflowMenu"; import { OverflowMenu } from "./OverflowMenu";
import { Avatar } from "../Avatar"; import { Avatar } from "../Avatar";
import { useProfile } from "../profile/useProfile"; 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 styles from "./VideoPreview.module.css";
import { Body } from "../typography/Typography"; import { Body } from "../typography/Typography";
import { useModalTriggerState } from "../Modal"; 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({ export function VideoPreview({
client, client,
state, state,
@ -37,7 +50,7 @@ export function VideoPreview({
toggleMicrophoneMuted, toggleMicrophoneMuted,
audioOutput, audioOutput,
stream, stream,
}) { }: Props) {
const videoRef = useMediaStream(stream, audioOutput, true); const videoRef = useMediaStream(stream, audioOutput, true);
const { displayName, avatarUrl } = useProfile(client); const { displayName, avatarUrl } = useProfile(client);
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver }); const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
@ -81,9 +94,11 @@ export function VideoPreview({
/> />
<OverflowMenu <OverflowMenu
roomId={roomId} roomId={roomId}
client={client}
feedbackModalState={feedbackModalState} feedbackModalState={feedbackModalState}
feedbackModalProps={feedbackModalProps} feedbackModalProps={feedbackModalProps}
inCall={false}
groupCall={undefined}
showInvite={false}
/> />
</div> </div>
</> </>

View file

@ -29,7 +29,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { usePageUnload } from "./usePageUnload"; import { usePageUnload } from "./usePageUnload";
export interface UseGroupCallType { export interface UseGroupCallReturnType {
state: GroupCallState; state: GroupCallState;
calls: MatrixCall[]; calls: MatrixCall[];
localCallFeed: CallFeed; localCallFeed: CallFeed;
@ -72,7 +72,7 @@ interface State {
hasLocalParticipant: boolean; hasLocalParticipant: boolean;
} }
export function useGroupCall(groupCall: GroupCall): UseGroupCallType { export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
const [ const [
{ {
state, state,

View file

@ -32,11 +32,11 @@ function isIOS() {
); );
} }
export function usePageUnload(callback) { export function usePageUnload(callback: () => void) {
useEffect(() => { useEffect(() => {
let pageVisibilityTimeout; let pageVisibilityTimeout: number;
function onBeforeUnload(event) { function onBeforeUnload(event: PageTransitionEvent) {
if (event.type === "visibilitychange") { if (event.type === "visibilitychange") {
if (document.visibilityState === "visible") { if (document.visibilityState === "visible") {
clearTimeout(pageVisibilityTimeout); clearTimeout(pageVisibilityTimeout);

View file

@ -16,28 +16,30 @@ limitations under the License.
import { useEffect } from "react"; import { useEffect } from "react";
import * as Sentry from "@sentry/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(() => { useEffect(() => {
function onHangup(call) { function onHangup(call: MatrixCall) {
if (call.hangupReason === "ice_failed") { if (call.hangupReason === "ice_failed") {
Sentry.captureException(new Error("Call hangup due to ICE failure.")); Sentry.captureException(new Error("Call hangup due to ICE failure."));
} }
} }
function onError(error) { function onError(error: Error) {
Sentry.captureException(error); Sentry.captureException(error);
} }
if (groupCall) { if (groupCall) {
groupCall.on("hangup", onHangup); groupCall.on(CallEvent.Hangup, onHangup);
groupCall.on("error", onError); groupCall.on(GroupCallEvent.Error, onError);
} }
return () => { return () => {
if (groupCall) { if (groupCall) {
groupCall.removeListener("hangup", onHangup); groupCall.removeListener(CallEvent.Hangup, onHangup);
groupCall.removeListener("error", onError); groupCall.removeListener(GroupCallEvent.Error, onError);
} }
}; };
}, [groupCall]); }, [groupCall]);

View file

@ -32,9 +32,8 @@ import { useDownloadDebugLog } from "./submit-rageshake";
import { Body } from "../typography/Typography"; import { Body } from "../typography/Typography";
interface Props { interface Props {
setShowInspector: boolean; isOpen: boolean;
showInspector: boolean; onClose: () => void;
[rest: string]: unknown;
} }
export const SettingsModal = (props: Props) => { export const SettingsModal = (props: Props) => {

View file

@ -26,11 +26,11 @@ import { InspectorContext } from "../room/GroupCallInspector";
import { useModalTriggerState } from "../Modal"; import { useModalTriggerState } from "../Modal";
interface RageShakeSubmitOptions { interface RageShakeSubmitOptions {
description: string;
roomId?: string;
label?: string;
sendLogs: boolean; sendLogs: boolean;
rageshakeRequestId?: string; rageshakeRequestId?: string;
description?: string;
roomId?: string;
label?: string;
} }
export function useSubmitRageshake(): { export function useSubmitRageshake(): {
@ -40,7 +40,7 @@ export function useSubmitRageshake(): {
error: Error; error: Error;
} { } {
const client: MatrixClient = useClient().client; const client: MatrixClient = useClient().client;
const [{ json }] = useContext(InspectorContext); const json = useContext(InspectorContext);
const [{ sending, sent, error }, setState] = useState({ const [{ sending, sent, error }, setState] = useState({
sending: false, sending: false,
@ -274,7 +274,7 @@ export function useSubmitRageshake(): {
} }
export function useDownloadDebugLog(): () => void { export function useDownloadDebugLog(): () => void {
const [{ json }] = useContext(InspectorContext); const json = useContext(InspectorContext);
const downloadDebugLog = useCallback(() => { const downloadDebugLog = useCallback(() => {
const blob = new Blob([JSON.stringify(json)], { type: "application/json" }); const blob = new Blob([JSON.stringify(json)], { type: "application/json" });

View file

@ -24,6 +24,7 @@ import React, {
useMemo, useMemo,
useContext, useContext,
createContext, createContext,
ReactNode,
} from "react"; } from "react";
export interface MediaHandlerContextInterface { export interface MediaHandlerContextInterface {
@ -73,7 +74,7 @@ function updateMediaPreferences(newPreferences: MediaPreferences): void {
} }
interface Props { interface Props {
client: MatrixClient; client: MatrixClient;
children: JSX.Element[]; children: ReactNode;
} }
export function MediaHandlerProvider({ client, children }: Props): JSX.Element { export function MediaHandlerProvider({ client, children }: Props): JSX.Element {
const [ const [

View file

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

View file

@ -8,14 +8,7 @@
"noImplicitAny": false, "noImplicitAny": false,
"noUnusedLocals": true, "noUnusedLocals": true,
"jsx": "preserve", "jsx": "preserve",
"lib": [ "lib": ["es2020", "dom", "dom.iterable"]
"es2020",
"dom",
"dom.iterable"
],
}, },
"include": [ "include": ["./src/**/*.ts", "./src/**/*.tsx"]
"./src/**/*.ts",
"./src/**/*.tsx",
],
} }