Merge branch 'main' into matroska

This commit is contained in:
Robin Townsend
2022-08-05 16:16:59 -04:00
79 changed files with 1416 additions and 748 deletions

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,19 +24,22 @@ import { ReactComponent as FreedomIcon } from "../icons/Freedom.svg";
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
import menuStyles from "../Menu.module.css";
import { Menu } from "../Menu";
import { Item } from "@react-stately/collections";
import { Tooltip, TooltipTrigger } from "../Tooltip";
import { TooltipTrigger } from "../Tooltip";
export function GridLayoutMenu({ layout, setLayout }) {
type Layout = "freedom" | "spotlight";
interface Props {
layout: Layout;
setLayout: (layout: Layout) => void;
}
export function GridLayoutMenu({ layout, setLayout }: Props) {
return (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger>
<TooltipTrigger tooltip={() => "Layout Type"}>
<Button variant="icon">
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</Button>
{() => "Layout Type"}
</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,18 @@ export function SequenceDiagramViewer({
);
}
function reducer(state, action) {
function reducer(
state: InspectorContextState,
action: {
type?: CallEvent | ClientEvent | RoomStateEvent;
event?: MatrixEvent;
rawEvent?: Record<string, unknown>;
callStateEvent?: MatrixEvent;
memberStateEvents?: MatrixEvent[];
}
) {
switch (action.type) {
case "receive_room_state_event": {
case RoomStateEvent.Events: {
const { event, callStateEvent, memberStateEvents } = action;
let eventsByUserId = state.eventsByUserId;
@@ -247,12 +292,12 @@ function reducer(state, action) {
),
};
}
case "received_voip_event": {
case ClientEvent.ReceivedVoipEvent: {
const event = action.event;
const eventsByUserId = { ...state.eventsByUserId };
const fromId = event.getSender();
const toId = state.localUserId;
const content = event.getContent();
const content = event.getContent<CallEventContent>();
const remoteUserIds = eventsByUserId[fromId]
? state.remoteUserIds
@@ -272,11 +317,11 @@ function reducer(state, action) {
return { ...state, eventsByUserId, remoteUserIds };
}
case "send_voip_event": {
const event = action.event;
case CallEvent.SendVoipEvent: {
const event = action.rawEvent;
const eventsByUserId = { ...state.eventsByUserId };
const fromId = state.localUserId;
const toId = event.userId;
const toId = event.userId as string;
const remoteUserIds = eventsByUserId[toId]
? state.remoteUserIds
@@ -287,8 +332,8 @@ function reducer(state, action) {
{
from: fromId,
to: toId,
type: event.eventType,
content: event.content,
type: event.eventType as string,
content: event.content as CallEventContent,
timestamp: Date.now(),
ignored: false,
},
@@ -301,7 +346,11 @@ function reducer(state, action) {
}
}
function useGroupCallState(client, groupCall, pollCallStats) {
function useGroupCallState(
client: MatrixClient,
groupCall: GroupCall,
showPollCallStats: boolean
) {
const [state, dispatch] = useReducer(reducer, {
localUserId: client.getUserId(),
localSessionId: client.getSessionId(),
@@ -312,7 +361,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
});
useEffect(() => {
function onUpdateRoomState(event) {
function onUpdateRoomState(event?: MatrixEvent) {
const callStateEvent = groupCall.room.currentState.getStateEvents(
"org.matrix.msc3401.call",
groupCall.groupCallId
@@ -323,120 +372,60 @@ function useGroupCallState(client, groupCall, pollCallStats) {
);
dispatch({
type: "receive_room_state_event",
type: RoomStateEvent.Events,
event,
callStateEvent,
memberStateEvents,
});
}
// function onCallsChanged() {
// const calls = groupCall.calls.reduce((obj, call) => {
// obj[
// `${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
// ] = getCallState(call);
// return obj;
// }, {});
// updateState({ calls });
// }
// function onCallHangup(call) {
// setState(({ hangupCalls, ...rest }) => ({
// ...rest,
// hangupCalls: {
// ...hangupCalls,
// [`${call.callId} (${
// call.getOpponentMember()?.userId || call.sender
// })`]: getHangupCallState(call),
// },
// }));
// dispatch({ type: "call_hangup", call });
// }
function onReceivedVoipEvent(event) {
dispatch({ type: "received_voip_event", event });
function onReceivedVoipEvent(event: MatrixEvent) {
dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
}
function onSendVoipEvent(event) {
dispatch({ type: "send_voip_event", event });
function onSendVoipEvent(event: Record<string, unknown>) {
dispatch({ type: CallEvent.SendVoipEvent, rawEvent: event });
}
client.on("RoomState.events", onUpdateRoomState);
client.on(RoomStateEvent.Events, onUpdateRoomState);
//groupCall.on("calls_changed", onCallsChanged);
groupCall.on("send_voip_event", onSendVoipEvent);
groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent);
//client.on("state", onCallsChanged);
//client.on("hangup", onCallHangup);
client.on("received_voip_event", onReceivedVoipEvent);
client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
onUpdateRoomState();
return () => {
client.removeListener("RoomState.events", onUpdateRoomState);
client.removeListener(RoomStateEvent.Events, onUpdateRoomState);
//groupCall.removeListener("calls_changed", onCallsChanged);
groupCall.removeListener("send_voip_event", onSendVoipEvent);
groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent);
//client.removeListener("state", onCallsChanged);
//client.removeListener("hangup", onCallHangup);
client.removeListener("received_voip_event", onReceivedVoipEvent);
client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
};
}, [client, groupCall]);
// useEffect(() => {
// let timeout;
// async function updateCallStats() {
// const callIds = groupCall.calls.map(
// (call) =>
// `${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
// );
// const stats = await Promise.all(
// groupCall.calls.map((call) =>
// call.peerConn
// ? call.peerConn
// .getStats(null)
// .then((stats) =>
// Object.fromEntries(
// Array.from(stats).map(([_id, report], i) => [
// report.type + i,
// report,
// ])
// )
// )
// : Promise.resolve(null)
// )
// );
// const callStats = {};
// for (let i = 0; i < groupCall.calls.length; i++) {
// callStats[callIds[i]] = stats[i];
// }
// dispatch({ type: "callStats", callStats });
// timeout = setTimeout(updateCallStats, 1000);
// }
// if (pollCallStats) {
// updateCallStats();
// }
// return () => {
// clearTimeout(timeout);
// };
// }, [pollCallStats]);
return state;
}
export function GroupCallInspector({ client, groupCall, show }) {
interface GroupCallInspectorProps {
client: MatrixClient;
groupCall: GroupCall;
show: boolean;
}
export function GroupCallInspector({
client,
groupCall,
show,
}: GroupCallInspectorProps) {
const [currentTab, setCurrentTab] = useState("sequence-diagrams");
const [selectedUserId, setSelectedUserId] = useState();
const [selectedUserId, setSelectedUserId] = useState<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 +435,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;
roomIdOrAlias: string;
viaServers: string[];
children: (groupCall: GroupCall) => ReactNode;
createPtt: boolean;
}
export function GroupCallLoader({
client,
roomIdOrAlias,
viaServers,
createPtt,
children,
}) {
createPtt,
}: Props): JSX.Element {
const { loading, error, groupCall } = useLoadGroupCall(
client,
roomIdOrAlias,
@@ -47,5 +57,5 @@ export function GroupCallLoader({
return <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;
roomIdOrAlias: string;
groupCall: GroupCall;
}
export function GroupCallView({
client,
isPasswordlessUser,
isEmbedded,
roomIdOrAlias,
groupCall,
}) {
}: Props) {
const {
state,
error,
@@ -52,7 +65,6 @@ export function GroupCallView({
isScreensharing,
localScreenshareFeed,
screenshareFeeds,
hasLocalParticipant,
participants,
unencryptedEventsFromUsers,
} = useGroupCall(groupCall);
@@ -80,7 +92,7 @@ export function GroupCallView({
if (!isPasswordlessUser) {
history.push("/");
}
}, [leave, history]);
}, [leave, isPasswordlessUser, history]);
if (error) {
return <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;
roomIdOrAlias: 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,
roomIdOrAlias,
unencryptedEventsFromUsers,
}) {
}: Props) {
usePreventScroll();
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
@@ -79,7 +110,7 @@ export function InCallView({
useModalTriggerState();
const items = useMemo(() => {
const participants = [];
const participants: Participant[] = [];
for (const callFeed of userMediaFeeds) {
participants.push({
@@ -90,6 +121,7 @@ export function InCallView({
? callFeed.userId === activeSpeaker
: false,
isLocal: callFeed.isLocal(),
presenter: false,
});
}
@@ -107,29 +139,27 @@ export function InCallView({
callFeed,
focused: true,
isLocal: callFeed.isLocal(),
presenter: false,
});
}
return participants;
}, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
const renderAvatar = useCallback(
(roomMember, width, height) => {
const avatarUrl = roomMember.user?.avatarUrl;
const size = Math.round(Math.min(width, height) / 2);
const renderAvatar = useCallback((roomMember, width, height) => {
const avatarUrl = roomMember.user?.avatarUrl;
const size = Math.round(Math.min(width, height) / 2);
return (
<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
roomIdOrAlias={roomIdOrAlias}
client={client}
groupCall={groupCall}
showInvite={true}
feedbackModalState={feedbackModalState}

View File

@@ -14,27 +14,30 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { Modal, ModalContent } from "../Modal";
import React, { FC } from "react";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { CopyButton } from "../button";
import { getRoomUrl } from "../matrix-utils";
import styles from "./InviteModal.module.css";
export function InviteModal({ roomIdOrAlias, ...rest }) {
return (
<Modal
title="Invite People"
isDismissable
className={styles.inviteModal}
{...rest}
>
<ModalContent>
<p>Copy and share this meeting link</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
/>
</ModalContent>
</Modal>
);
interface Props extends Omit<ModalProps, "title" | "children"> {
roomIdOrAlias: string;
}
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => (
<Modal
title="Invite People"
isDismissable
className={styles.inviteModal}
{...rest}
>
<ModalContent>
<p>Copy and share this meeting link</p>
<CopyButton
className={styles.copyButton}
value={getRoomUrl(roomIdOrAlias)}
/>
</ModalContent>
</Modal>
);

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;
roomIdOrAlias: string;
isEmbedded: boolean;
}
export function LobbyView({
client,
groupCall,
@@ -43,7 +63,7 @@ export function LobbyView({
toggleMicrophoneMuted,
roomIdOrAlias,
isEmbedded,
}) {
}: Props) {
const { stream } = useCallFeed(localCallFeed);
const {
audioInput,
@@ -60,7 +80,7 @@ export function LobbyView({
useLocationNavigation(state === GroupCallState.InitializingLocalCallFeed);
const joinCallButtonRef = useRef();
const joinCallButtonRef = useRef<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 {
roomIdOrAlias: string;
inCall: boolean;
groupCall: GroupCall;
showInvite: boolean;
feedbackModalState: OverlayTriggerState;
feedbackModalProps: {
isOpen: boolean;
onClose: () => void;
};
}
export function OverflowMenu({
roomIdOrAlias,
inCall,
@@ -36,39 +49,57 @@ export function OverflowMenu({
showInvite,
feedbackModalState,
feedbackModalProps,
}) {
const { modalState: inviteModalState, modalProps: inviteModalProps } =
useModalTriggerState();
const { modalState: settingsModalState, modalProps: settingsModalProps } =
useModalTriggerState();
}: Props) {
const {
modalState: inviteModalState,
modalProps: inviteModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
const {
modalState: settingsModalState,
modalProps: settingsModalProps,
}: {
modalState: OverlayTriggerState;
modalProps: {
isOpen: boolean;
onClose: () => void;
};
} = useModalTriggerState();
// TODO: On closing modal, focus should be restored to the trigger button
// https://github.com/adobe/react-spectrum/issues/2444
const onAction = useCallback((key) => {
switch (key) {
case "invite":
inviteModalState.open();
break;
case "settings":
settingsModalState.open();
break;
case "feedback":
feedbackModalState.open();
break;
}
});
const onAction = useCallback(
(key) => {
switch (key) {
case "invite":
inviteModalState.open();
break;
case "settings":
settingsModalState.open();
break;
case "feedback":
feedbackModalState.open();
break;
}
},
[feedbackModalState, inviteModalState, settingsModalState]
);
return (
<>
<PopoverMenuTrigger disableOnState>
<TooltipTrigger placement="top">
<TooltipTrigger tooltip={() => "More"} placement="top">
<Button variant="toolbar">
<OverflowIcon />
</Button>
{() => "More"}
</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

@@ -38,6 +38,7 @@ import { usePTTSounds } from "../sound/usePttSounds";
import { PTTClips } from "../sound/PTTClips";
import { GroupCallInspector } from "./GroupCallInspector";
import { OverflowMenu } from "./OverflowMenu";
import { Size } from "../Avatar";
function getPromptText(
networkWaiting: boolean,
@@ -112,7 +113,7 @@ export const PTTCallView: React.FC<Props> = ({
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
useModalTriggerState();
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
const facepileSize = bounds.width < 800 ? "sm" : "md";
const facepileSize = bounds.width < 800 ? Size.SM : Size.MD;
const showControls = bounds.height > 500;
const pttButtonSize = 232;
@@ -205,7 +206,6 @@ export const PTTCallView: React.FC<Props> = ({
<OverflowMenu
inCall
roomIdOrAlias={roomIdOrAlias}
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

@@ -14,25 +14,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useEffect } from "react";
import { Modal, ModalContent } from "../Modal";
import React, { FC, useEffect } from "react";
import { Modal, ModalContent, ModalProps } from "../Modal";
import { Button } from "../button";
import { FieldRow, ErrorMessage } from "../input/Input";
import { useSubmitRageshake } from "../settings/submit-rageshake";
import { Body } from "../typography/Typography";
export function RageshakeRequestModal({
interface Props extends Omit<ModalProps, "title" | "children"> {
rageshakeRequestId: string;
roomIdOrAlias: string;
onClose: () => void;
}
export const RageshakeRequestModal: FC<Props> = ({
rageshakeRequestId,
roomIdOrAlias,
...rest
}) {
}) => {
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
useEffect(() => {
if (sent) {
rest.onClose();
}
}, [sent, rest.onClose]);
}, [sent, rest]);
return (
<Modal title="Debug Log Request" isDismissable {...rest}>
@@ -47,7 +54,7 @@ export function RageshakeRequestModal({
submitRageshake({
sendLogs: true,
rageshakeRequestId,
roomIdOrAlias, // Possibly not a room ID, but oh well
roomId: roomIdOrAlias, // Possibly not a room ID, but oh well
})
}
disabled={sending}
@@ -63,4 +70,4 @@ export function RageshakeRequestModal({
</ModalContent>
</Modal>
);
}
};

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

@@ -15,6 +15,7 @@ limitations under the License.
*/
import React, { FC, useEffect, useState } from "react";
import { useClient } from "../ClientContext";
import { ErrorView, LoadingView } from "../FullScreenView";
import { RoomAuthView } from "./RoomAuthView";
@@ -24,7 +25,7 @@ import { useRoomParams } from "./useRoomParams";
import { MediaHandlerProvider } from "../settings/useMediaHandler";
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
export function RoomPage() {
export const RoomPage: FC = () => {
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
useClient();
@@ -84,4 +85,4 @@ export function RoomPage() {
</GroupCallLoader>
</MediaHandlerProvider>
);
}
};

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;
roomIdOrAlias: string;
microphoneMuted: boolean;
localVideoMuted: boolean;
toggleLocalVideoMuted: () => void;
toggleMicrophoneMuted: () => void;
audioOutput: string;
stream: MediaStream;
}
export function VideoPreview({
client,
state,
@@ -37,7 +50,7 @@ export function VideoPreview({
toggleMicrophoneMuted,
audioOutput,
stream,
}) {
}: Props) {
const videoRef = useMediaStream(stream, audioOutput, true);
const { displayName, avatarUrl } = useProfile(client);
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
@@ -81,9 +94,11 @@ export function VideoPreview({
/>
<OverflowMenu
roomIdOrAlias={roomIdOrAlias}
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]);