mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-15 00:04:59 +08:00
typescript src/room
(#437)
This commit is contained in:
parent
c723fae0e2
commit
2d99acabe2
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
@ -24,7 +24,7 @@ declare global {
|
||||
|
||||
// TypeScript doesn't know about the experimental setSinkId method, so we
|
||||
// declare it ourselves
|
||||
interface MediaElement extends HTMLMediaElement {
|
||||
interface MediaElement extends HTMLVideoElement {
|
||||
setSinkId: (id: string) => void;
|
||||
}
|
||||
}
|
||||
|
@ -132,6 +132,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
isAuthenticated: Boolean(client),
|
||||
isPasswordlessUser,
|
||||
userName: client?.getUserIdLocalpart(),
|
||||
error: undefined,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
@ -141,6 +142,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: false,
|
||||
userName: null,
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
@ -170,6 +172,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
isAuthenticated: true,
|
||||
isPasswordlessUser: false,
|
||||
userName: client.getUserIdLocalpart(),
|
||||
error: undefined,
|
||||
});
|
||||
},
|
||||
[client]
|
||||
@ -190,6 +193,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
isAuthenticated: true,
|
||||
isPasswordlessUser: session.passwordlessUser,
|
||||
userName: newClient.getUserIdLocalpart(),
|
||||
error: undefined,
|
||||
});
|
||||
} else {
|
||||
clearSession();
|
||||
@ -200,6 +204,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
isAuthenticated: false,
|
||||
isPasswordlessUser: false,
|
||||
userName: null,
|
||||
error: undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
@ -258,6 +263,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
logout,
|
||||
userName,
|
||||
setClient,
|
||||
error: undefined,
|
||||
}),
|
||||
[
|
||||
loading,
|
||||
|
12
src/Menu.tsx
12
src/Menu.tsx
@ -1,4 +1,4 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { Key, useRef, useState } from "react";
|
||||
import { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu";
|
||||
import { TreeState, useTreeState } from "@react-stately/tree";
|
||||
import { mergeProps } from "@react-aria/utils";
|
||||
@ -9,15 +9,17 @@ import { Node } from "@react-types/shared";
|
||||
import styles from "./Menu.module.css";
|
||||
|
||||
interface MenuProps<T> extends AriaMenuOptions<T> {
|
||||
className: String;
|
||||
onAction: () => void;
|
||||
onClose: () => void;
|
||||
className?: String;
|
||||
onClose?: () => void;
|
||||
onAction: (value: Key) => void;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function Menu<T extends object>({
|
||||
className,
|
||||
onAction,
|
||||
onClose,
|
||||
label,
|
||||
...rest
|
||||
}: MenuProps<T>) {
|
||||
const state = useTreeState<T>({ ...rest, selectionMode: "none" });
|
||||
@ -46,7 +48,7 @@ export function Menu<T extends object>({
|
||||
interface MenuItemProps<T> {
|
||||
item: Node<T>;
|
||||
state: TreeState<T>;
|
||||
onAction: () => void;
|
||||
onAction: (value: Key) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
@ -16,13 +16,16 @@ limitations under the License.
|
||||
|
||||
import React, { useCallback, useState } from "react";
|
||||
|
||||
import { SequenceDiagramViewer } from "./room/GroupCallInspector";
|
||||
import {
|
||||
SequenceDiagramViewer,
|
||||
SequenceDiagramMatrixEvent,
|
||||
} from "./room/GroupCallInspector";
|
||||
import { FieldRow, InputField } from "./input/Input";
|
||||
import { usePageTitle } from "./usePageTitle";
|
||||
|
||||
interface DebugLog {
|
||||
localUserId: string;
|
||||
eventsByUserId: Record<string, {}>;
|
||||
eventsByUserId: { [userId: string]: SequenceDiagramMatrixEvent[] };
|
||||
remoteUserIds: string[];
|
||||
}
|
||||
|
||||
@ -33,7 +36,7 @@ export function SequenceDiagramViewerPage() {
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>();
|
||||
const onChangeDebugLog = useCallback((e) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
e.target.files[0].text().then((text) => {
|
||||
e.target.files[0].text().then((text: string) => {
|
||||
setDebugLog(JSON.parse(text));
|
||||
});
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ interface Props {
|
||||
preventNavigation?: boolean;
|
||||
}
|
||||
|
||||
export function UserMenuContainer({ preventNavigation }: Props) {
|
||||
export function UserMenuContainer({ preventNavigation = false }: Props) {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
|
||||
|
@ -100,7 +100,7 @@ export const RegisterPage: FC = () => {
|
||||
submit()
|
||||
.then(() => {
|
||||
if (location.state?.from) {
|
||||
history.push(location.state.from);
|
||||
history.push(location.state?.from);
|
||||
} else {
|
||||
history.push("/");
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ interface Props {
|
||||
children: Element[];
|
||||
onPress: (e: PressEvent) => void;
|
||||
onPressStart: (e: PressEvent) => void;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}
|
||||
export const Button = forwardRef<HTMLButtonElement, Props>(
|
||||
@ -136,6 +137,7 @@ export function MicButton({
|
||||
...rest
|
||||
}: {
|
||||
muted: boolean;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
return (
|
||||
@ -154,6 +156,7 @@ export function VideoButton({
|
||||
...rest
|
||||
}: {
|
||||
muted: boolean;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
return (
|
||||
@ -174,6 +177,7 @@ export function ScreenshareButton({
|
||||
}: {
|
||||
enabled: boolean;
|
||||
className?: string;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
return (
|
||||
@ -192,6 +196,7 @@ export function HangupButton({
|
||||
...rest
|
||||
}: {
|
||||
className?: string;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
return (
|
||||
@ -212,6 +217,7 @@ export function SettingsButton({
|
||||
...rest
|
||||
}: {
|
||||
className?: string;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
return (
|
||||
@ -228,6 +234,7 @@ export function InviteButton({
|
||||
...rest
|
||||
}: {
|
||||
className?: string;
|
||||
// TODO: add all props for <Button>
|
||||
[index: string]: unknown;
|
||||
}) {
|
||||
return (
|
||||
|
@ -23,9 +23,9 @@ import { Button, ButtonVariant } from "./Button";
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
children?: JSX.Element;
|
||||
className: string;
|
||||
variant: ButtonVariant;
|
||||
children?: JSX.Element | string;
|
||||
className?: string;
|
||||
variant?: ButtonVariant;
|
||||
copiedMessage?: string;
|
||||
}
|
||||
export function CopyButton({
|
||||
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode } from "react";
|
||||
import React, { HTMLAttributes } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import classNames from "classnames";
|
||||
import * as H from "history";
|
||||
|
||||
import {
|
||||
variantToClassName,
|
||||
@ -24,19 +25,21 @@ import {
|
||||
ButtonVariant,
|
||||
ButtonSize,
|
||||
} from "./Button";
|
||||
interface Props {
|
||||
className?: string;
|
||||
variant?: ButtonVariant;
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLAnchorElement> {
|
||||
children: JSX.Element | string;
|
||||
to: H.LocationDescriptor | ((location: H.Location) => H.LocationDescriptor);
|
||||
size?: ButtonSize;
|
||||
children: ReactNode;
|
||||
[index: string]: unknown;
|
||||
variant?: ButtonVariant;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LinkButton({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
children,
|
||||
to,
|
||||
size,
|
||||
variant,
|
||||
className,
|
||||
...rest
|
||||
}: Props) {
|
||||
return (
|
||||
@ -46,6 +49,7 @@ export function LinkButton({
|
||||
sizeToClassName[size],
|
||||
className
|
||||
)}
|
||||
to={to}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
@ -46,7 +46,7 @@ export const CallTypeDropdown: FC<Props> = ({ callType, setCallType }) => {
|
||||
{callType === CallType.Video ? "Video call" : "Walkie-talkie call"}
|
||||
</Headline>
|
||||
</Button>
|
||||
{(props) => (
|
||||
{(props: JSX.IntrinsicAttributes) => (
|
||||
<Menu {...props} label="Call type menu" onAction={setCallType}>
|
||||
<Item key={CallType.Video} textValue="Video call">
|
||||
<VideoIcon />
|
||||
|
@ -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>
|
@ -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 (
|
@ -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}>
|
@ -15,6 +15,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Item } from "@react-stately/collections";
|
||||
|
||||
import { Button } from "../button";
|
||||
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
|
||||
import { ReactComponent as SpotlightIcon } from "../icons/Spotlight.svg";
|
||||
@ -22,10 +24,14 @@ import { ReactComponent as FreedomIcon } from "../icons/Freedom.svg";
|
||||
import { ReactComponent as CheckIcon } from "../icons/Check.svg";
|
||||
import menuStyles from "../Menu.module.css";
|
||||
import { Menu } from "../Menu";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { Tooltip, TooltipTrigger } from "../Tooltip";
|
||||
import { TooltipTrigger } from "../Tooltip";
|
||||
|
||||
export function GridLayoutMenu({ layout, setLayout }) {
|
||||
type Layout = "freedom" | "spotlight";
|
||||
interface Props {
|
||||
layout: Layout;
|
||||
setLayout: (layout: Layout) => void;
|
||||
}
|
||||
export function GridLayoutMenu({ layout, setLayout }: Props) {
|
||||
return (
|
||||
<PopoverMenuTrigger placement="bottom right">
|
||||
<TooltipTrigger tooltip={() => "Layout Type"}>
|
||||
@ -33,7 +39,7 @@ export function GridLayoutMenu({ layout, setLayout }) {
|
||||
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{(props) => (
|
||||
{(props: JSX.IntrinsicAttributes) => (
|
||||
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
||||
<Item key="freedom" textValue="Freedom">
|
||||
<FreedomIcon />
|
@ -22,40 +22,26 @@ import React, {
|
||||
useRef,
|
||||
createContext,
|
||||
useContext,
|
||||
Dispatch,
|
||||
} from "react";
|
||||
import ReactJson from "react-json-view";
|
||||
import ReactJson, { CollapsedFieldProps } from "react-json-view";
|
||||
import mermaid from "mermaid";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { MatrixEvent, GroupCall, IContent } from "matrix-js-sdk";
|
||||
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { CallEvent } from "matrix-js-sdk/src/webrtc/call";
|
||||
|
||||
import styles from "./GroupCallInspector.module.css";
|
||||
import { SelectInput } from "../input/SelectInput";
|
||||
import { Item } from "@react-stately/collections";
|
||||
|
||||
function getCallUserId(call) {
|
||||
return call.getOpponentMember()?.userId || call.invitee || null;
|
||||
interface InspectorContextState {
|
||||
eventsByUserId?: { [userId: string]: SequenceDiagramMatrixEvent[] };
|
||||
remoteUserIds?: string[];
|
||||
localUserId?: string;
|
||||
localSessionId?: string;
|
||||
}
|
||||
|
||||
function getCallState(call) {
|
||||
return {
|
||||
id: call.callId,
|
||||
opponentMemberId: getCallUserId(call),
|
||||
state: call.state,
|
||||
direction: call.direction,
|
||||
};
|
||||
}
|
||||
|
||||
function getHangupCallState(call) {
|
||||
return {
|
||||
...getCallState(call),
|
||||
hangupReason: call.hangupReason,
|
||||
};
|
||||
}
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
fractionalSecondDigits: 3,
|
||||
});
|
||||
|
||||
const defaultCollapsedFields = [
|
||||
"org.matrix.msc3401.call",
|
||||
"org.matrix.msc3401.call.member",
|
||||
@ -67,19 +53,19 @@ const defaultCollapsedFields = [
|
||||
"content",
|
||||
];
|
||||
|
||||
function shouldCollapse({ name, src, type, namespace }) {
|
||||
function shouldCollapse({ name }: CollapsedFieldProps) {
|
||||
return defaultCollapsedFields.includes(name);
|
||||
}
|
||||
|
||||
function getUserName(userId) {
|
||||
const match = userId.match(/@([^\:]+):/);
|
||||
function getUserName(userId: string) {
|
||||
const match = userId.match(/@([^:]+):/);
|
||||
|
||||
return match && match.length > 0
|
||||
? match[1].replace("-", " ").replace(/\W/g, "")
|
||||
: userId.replace(/\W/g, "");
|
||||
}
|
||||
|
||||
function formatContent(type, content) {
|
||||
function formatContent(type: string, content: CallEventContent) {
|
||||
if (type === "m.call.hangup") {
|
||||
return `callId: ${content.call_id.slice(-4)} reason: ${
|
||||
content.reason
|
||||
@ -109,14 +95,35 @@ function formatContent(type, content) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
const dateFormatter = new Intl.DateTimeFormat([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore the linter does not know about this property of the DataTimeFormatOptions
|
||||
fractionalSecondDigits: 3,
|
||||
});
|
||||
|
||||
function formatTimestamp(timestamp: number | Date) {
|
||||
return dateFormatter.format(timestamp);
|
||||
}
|
||||
|
||||
export const InspectorContext = createContext();
|
||||
export const InspectorContext =
|
||||
createContext<
|
||||
[
|
||||
InspectorContextState,
|
||||
React.Dispatch<React.SetStateAction<InspectorContextState>>
|
||||
]
|
||||
>(undefined);
|
||||
|
||||
export function InspectorContextProvider({ children }) {
|
||||
const context = useState({});
|
||||
export function InspectorContextProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
// The context will be initialized empty.
|
||||
// It is then set from within GroupCallInspector.
|
||||
const context = useState<InspectorContextState>({});
|
||||
return (
|
||||
<InspectorContext.Provider value={context}>
|
||||
{children}
|
||||
@ -124,14 +131,43 @@ export function InspectorContextProvider({ children }) {
|
||||
);
|
||||
}
|
||||
|
||||
type CallEventContent = {
|
||||
["m.calls"]: {
|
||||
["m.devices"]: { session_id: string; [x: string]: unknown }[];
|
||||
["m.call_id"]: string;
|
||||
}[];
|
||||
} & {
|
||||
call_id: string;
|
||||
reason: string;
|
||||
sender_session_id: string;
|
||||
dest_session_id: string;
|
||||
} & IContent;
|
||||
|
||||
export type SequenceDiagramMatrixEvent = {
|
||||
to: string;
|
||||
from: string;
|
||||
timestamp: number;
|
||||
type: string;
|
||||
content: CallEventContent;
|
||||
ignored: boolean;
|
||||
};
|
||||
|
||||
interface SequenceDiagramViewerProps {
|
||||
localUserId: string;
|
||||
remoteUserIds: string[];
|
||||
selectedUserId: string;
|
||||
onSelectUserId: Dispatch<(prevState: undefined) => undefined>;
|
||||
events: SequenceDiagramMatrixEvent[];
|
||||
}
|
||||
|
||||
export function SequenceDiagramViewer({
|
||||
localUserId,
|
||||
remoteUserIds,
|
||||
selectedUserId,
|
||||
onSelectUserId,
|
||||
events,
|
||||
}) {
|
||||
const mermaidElRef = useRef();
|
||||
}: SequenceDiagramViewerProps) {
|
||||
const mermaidElRef = useRef<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
mermaid.initialize({
|
||||
@ -165,7 +201,7 @@ export function SequenceDiagramViewer({
|
||||
}
|
||||
`;
|
||||
|
||||
mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode) => {
|
||||
mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode: string) => {
|
||||
mermaidElRef.current.innerHTML = svgCode;
|
||||
});
|
||||
}, [events, localUserId, selectedUserId]);
|
||||
@ -190,9 +226,17 @@ export function SequenceDiagramViewer({
|
||||
);
|
||||
}
|
||||
|
||||
function reducer(state, action) {
|
||||
function reducer(
|
||||
state: InspectorContextState,
|
||||
action: {
|
||||
type?: CallEvent | ClientEvent | RoomStateEvent;
|
||||
event: MatrixEvent;
|
||||
callStateEvent?: MatrixEvent;
|
||||
memberStateEvents?: MatrixEvent[];
|
||||
}
|
||||
) {
|
||||
switch (action.type) {
|
||||
case "receive_room_state_event": {
|
||||
case RoomStateEvent.Events: {
|
||||
const { event, callStateEvent, memberStateEvents } = action;
|
||||
|
||||
let eventsByUserId = state.eventsByUserId;
|
||||
@ -247,12 +291,12 @@ function reducer(state, action) {
|
||||
),
|
||||
};
|
||||
}
|
||||
case "received_voip_event": {
|
||||
case ClientEvent.ReceivedVoipEvent: {
|
||||
const event = action.event;
|
||||
const eventsByUserId = { ...state.eventsByUserId };
|
||||
const fromId = event.getSender();
|
||||
const toId = state.localUserId;
|
||||
const content = event.getContent();
|
||||
const content = event.getContent<CallEventContent>();
|
||||
|
||||
const remoteUserIds = eventsByUserId[fromId]
|
||||
? state.remoteUserIds
|
||||
@ -272,11 +316,11 @@ function reducer(state, action) {
|
||||
|
||||
return { ...state, eventsByUserId, remoteUserIds };
|
||||
}
|
||||
case "send_voip_event": {
|
||||
case CallEvent.SendVoipEvent: {
|
||||
const event = action.event;
|
||||
const eventsByUserId = { ...state.eventsByUserId };
|
||||
const fromId = state.localUserId;
|
||||
const toId = event.userId;
|
||||
const toId = event.target.userId; // was .user
|
||||
|
||||
const remoteUserIds = eventsByUserId[toId]
|
||||
? state.remoteUserIds
|
||||
@ -287,8 +331,8 @@ function reducer(state, action) {
|
||||
{
|
||||
from: fromId,
|
||||
to: toId,
|
||||
type: event.eventType,
|
||||
content: event.content,
|
||||
type: event.getType(),
|
||||
content: event.getContent(),
|
||||
timestamp: Date.now(),
|
||||
ignored: false,
|
||||
},
|
||||
@ -301,7 +345,11 @@ function reducer(state, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function useGroupCallState(client, groupCall, pollCallStats) {
|
||||
function useGroupCallState(
|
||||
client: MatrixClient,
|
||||
groupCall: GroupCall,
|
||||
showPollCallStats: boolean
|
||||
) {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
localUserId: client.getUserId(),
|
||||
localSessionId: client.getSessionId(),
|
||||
@ -312,7 +360,7 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
function onUpdateRoomState(event) {
|
||||
function onUpdateRoomState(event?: MatrixEvent) {
|
||||
const callStateEvent = groupCall.room.currentState.getStateEvents(
|
||||
"org.matrix.msc3401.call",
|
||||
groupCall.groupCallId
|
||||
@ -323,120 +371,60 @@ function useGroupCallState(client, groupCall, pollCallStats) {
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: "receive_room_state_event",
|
||||
type: RoomStateEvent.Events,
|
||||
event,
|
||||
callStateEvent,
|
||||
memberStateEvents,
|
||||
});
|
||||
}
|
||||
|
||||
// function onCallsChanged() {
|
||||
// const calls = groupCall.calls.reduce((obj, call) => {
|
||||
// obj[
|
||||
// `${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
|
||||
// ] = getCallState(call);
|
||||
// return obj;
|
||||
// }, {});
|
||||
|
||||
// updateState({ calls });
|
||||
// }
|
||||
|
||||
// function onCallHangup(call) {
|
||||
// setState(({ hangupCalls, ...rest }) => ({
|
||||
// ...rest,
|
||||
// hangupCalls: {
|
||||
// ...hangupCalls,
|
||||
// [`${call.callId} (${
|
||||
// call.getOpponentMember()?.userId || call.sender
|
||||
// })`]: getHangupCallState(call),
|
||||
// },
|
||||
// }));
|
||||
// dispatch({ type: "call_hangup", call });
|
||||
// }
|
||||
|
||||
function onReceivedVoipEvent(event) {
|
||||
dispatch({ type: "received_voip_event", event });
|
||||
function onReceivedVoipEvent(event: MatrixEvent) {
|
||||
dispatch({ type: ClientEvent.ReceivedVoipEvent, event });
|
||||
}
|
||||
|
||||
function onSendVoipEvent(event) {
|
||||
dispatch({ type: "send_voip_event", event });
|
||||
function onSendVoipEvent(event: MatrixEvent) {
|
||||
dispatch({ type: CallEvent.SendVoipEvent, event });
|
||||
}
|
||||
|
||||
client.on("RoomState.events", onUpdateRoomState);
|
||||
client.on(RoomStateEvent.Events, onUpdateRoomState);
|
||||
//groupCall.on("calls_changed", onCallsChanged);
|
||||
groupCall.on("send_voip_event", onSendVoipEvent);
|
||||
groupCall.on(CallEvent.SendVoipEvent, onSendVoipEvent);
|
||||
//client.on("state", onCallsChanged);
|
||||
//client.on("hangup", onCallHangup);
|
||||
client.on("received_voip_event", onReceivedVoipEvent);
|
||||
client.on(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
|
||||
|
||||
onUpdateRoomState();
|
||||
|
||||
return () => {
|
||||
client.removeListener("RoomState.events", onUpdateRoomState);
|
||||
client.removeListener(RoomStateEvent.Events, onUpdateRoomState);
|
||||
//groupCall.removeListener("calls_changed", onCallsChanged);
|
||||
groupCall.removeListener("send_voip_event", onSendVoipEvent);
|
||||
groupCall.removeListener(CallEvent.SendVoipEvent, onSendVoipEvent);
|
||||
//client.removeListener("state", onCallsChanged);
|
||||
//client.removeListener("hangup", onCallHangup);
|
||||
client.removeListener("received_voip_event", onReceivedVoipEvent);
|
||||
client.removeListener(ClientEvent.ReceivedVoipEvent, onReceivedVoipEvent);
|
||||
};
|
||||
}, [client, groupCall]);
|
||||
|
||||
// useEffect(() => {
|
||||
// let timeout;
|
||||
|
||||
// async function updateCallStats() {
|
||||
// const callIds = groupCall.calls.map(
|
||||
// (call) =>
|
||||
// `${call.callId} (${call.getOpponentMember()?.userId || call.sender})`
|
||||
// );
|
||||
// const stats = await Promise.all(
|
||||
// groupCall.calls.map((call) =>
|
||||
// call.peerConn
|
||||
// ? call.peerConn
|
||||
// .getStats(null)
|
||||
// .then((stats) =>
|
||||
// Object.fromEntries(
|
||||
// Array.from(stats).map(([_id, report], i) => [
|
||||
// report.type + i,
|
||||
// report,
|
||||
// ])
|
||||
// )
|
||||
// )
|
||||
// : Promise.resolve(null)
|
||||
// )
|
||||
// );
|
||||
|
||||
// const callStats = {};
|
||||
|
||||
// for (let i = 0; i < groupCall.calls.length; i++) {
|
||||
// callStats[callIds[i]] = stats[i];
|
||||
// }
|
||||
|
||||
// dispatch({ type: "callStats", callStats });
|
||||
// timeout = setTimeout(updateCallStats, 1000);
|
||||
// }
|
||||
|
||||
// if (pollCallStats) {
|
||||
// updateCallStats();
|
||||
// }
|
||||
|
||||
// return () => {
|
||||
// clearTimeout(timeout);
|
||||
// };
|
||||
// }, [pollCallStats]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export function GroupCallInspector({ client, groupCall, show }) {
|
||||
interface GroupCallInspectorProps {
|
||||
client: MatrixClient;
|
||||
groupCall: GroupCall;
|
||||
show: boolean;
|
||||
}
|
||||
export function GroupCallInspector({
|
||||
client,
|
||||
groupCall,
|
||||
show,
|
||||
}: GroupCallInspectorProps) {
|
||||
const [currentTab, setCurrentTab] = useState("sequence-diagrams");
|
||||
const [selectedUserId, setSelectedUserId] = useState();
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>();
|
||||
const state = useGroupCallState(client, groupCall, show);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [_, setState] = useContext(InspectorContext);
|
||||
|
||||
useEffect(() => {
|
||||
setState({ json: state });
|
||||
setState(state);
|
||||
}, [setState, state]);
|
||||
|
||||
if (!show) {
|
||||
@ -446,7 +434,7 @@ export function GroupCallInspector({ client, groupCall, show }) {
|
||||
return (
|
||||
<Resizable
|
||||
enable={{ top: true }}
|
||||
defaultSize={{ height: 200 }}
|
||||
defaultSize={{ height: 200, width: undefined }}
|
||||
className={styles.inspector}
|
||||
>
|
||||
<div className={styles.toolbar}>
|
@ -14,18 +14,28 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { GroupCall, MatrixClient } from "matrix-js-sdk";
|
||||
|
||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
roomId: string;
|
||||
viaServers: string[];
|
||||
children: (groupCall: GroupCall) => ReactNode;
|
||||
createPtt: boolean;
|
||||
}
|
||||
|
||||
export function GroupCallLoader({
|
||||
client,
|
||||
roomId,
|
||||
viaServers,
|
||||
createPtt,
|
||||
children,
|
||||
}) {
|
||||
createPtt,
|
||||
}: Props): JSX.Element {
|
||||
const { loading, error, groupCall } = useLoadGroupCall(
|
||||
client,
|
||||
roomId,
|
||||
@ -47,5 +57,5 @@ export function GroupCallLoader({
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
return children(groupCall);
|
||||
return <>{children(groupCall)}</>;
|
||||
}
|
@ -16,7 +16,9 @@ limitations under the License.
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { MatrixClient } from "matrix-js-sdk";
|
||||
|
||||
import { useGroupCall } from "./useGroupCall";
|
||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
import { LobbyView } from "./LobbyView";
|
||||
@ -26,14 +28,25 @@ import { CallEndedView } from "./CallEndedView";
|
||||
import { useRoomAvatar } from "./useRoomAvatar";
|
||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||
import { useLocationNavigation } from "../useLocationNavigation";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
groupCall: GroupCall;
|
||||
}
|
||||
}
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
isPasswordlessUser: boolean;
|
||||
isEmbedded: boolean;
|
||||
roomId: string;
|
||||
groupCall: GroupCall;
|
||||
}
|
||||
export function GroupCallView({
|
||||
client,
|
||||
isPasswordlessUser,
|
||||
isEmbedded,
|
||||
roomId,
|
||||
groupCall,
|
||||
}) {
|
||||
}: Props) {
|
||||
const {
|
||||
state,
|
||||
error,
|
||||
@ -52,7 +65,6 @@ export function GroupCallView({
|
||||
isScreensharing,
|
||||
localScreenshareFeed,
|
||||
screenshareFeeds,
|
||||
hasLocalParticipant,
|
||||
participants,
|
||||
unencryptedEventsFromUsers,
|
||||
} = useGroupCall(groupCall);
|
||||
@ -80,7 +92,7 @@ export function GroupCallView({
|
||||
if (!isPasswordlessUser) {
|
||||
history.push("/");
|
||||
}
|
||||
}, [leave, history]);
|
||||
}, [leave, isPasswordlessUser, history]);
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
@ -142,7 +154,6 @@ export function GroupCallView({
|
||||
<LobbyView
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
hasLocalParticipant={hasLocalParticipant}
|
||||
roomName={groupCall.room.name}
|
||||
avatarUrl={avatarUrl}
|
||||
state={state}
|
@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useRef } from "react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { usePreventScroll } from "@react-aria/overlays";
|
||||
import { GroupCall, MatrixClient } from "matrix-js-sdk";
|
||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||
|
||||
import styles from "./InCallView.module.css";
|
||||
import {
|
||||
HangupButton,
|
||||
@ -38,7 +42,6 @@ import { Avatar } from "../Avatar";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||
import { usePreventScroll } from "@react-aria/overlays";
|
||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||
import { useShowInspector } from "../settings/useSetting";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
@ -50,6 +53,33 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
// For now we can disable screensharing in Safari.
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
groupCall: GroupCall;
|
||||
roomName: string;
|
||||
avatarUrl: string;
|
||||
microphoneMuted: boolean;
|
||||
localVideoMuted: boolean;
|
||||
toggleLocalVideoMuted: () => void;
|
||||
toggleMicrophoneMuted: () => void;
|
||||
toggleScreensharing: () => void;
|
||||
userMediaFeeds: CallFeed[];
|
||||
activeSpeaker: string;
|
||||
onLeave: () => void;
|
||||
isScreensharing: boolean;
|
||||
screenshareFeeds: CallFeed[];
|
||||
localScreenshareFeed: CallFeed;
|
||||
roomId: string;
|
||||
unencryptedEventsFromUsers: Set<string>;
|
||||
}
|
||||
interface Participant {
|
||||
id: string;
|
||||
callFeed: CallFeed;
|
||||
focused: boolean;
|
||||
isLocal: boolean;
|
||||
presenter: boolean;
|
||||
}
|
||||
|
||||
export function InCallView({
|
||||
client,
|
||||
groupCall,
|
||||
@ -65,9 +95,10 @@ export function InCallView({
|
||||
toggleScreensharing,
|
||||
isScreensharing,
|
||||
screenshareFeeds,
|
||||
localScreenshareFeed,
|
||||
roomId,
|
||||
unencryptedEventsFromUsers,
|
||||
}) {
|
||||
}: Props) {
|
||||
usePreventScroll();
|
||||
const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0);
|
||||
|
||||
@ -79,7 +110,7 @@ export function InCallView({
|
||||
useModalTriggerState();
|
||||
|
||||
const items = useMemo(() => {
|
||||
const participants = [];
|
||||
const participants: Participant[] = [];
|
||||
|
||||
for (const callFeed of userMediaFeeds) {
|
||||
participants.push({
|
||||
@ -90,6 +121,7 @@ export function InCallView({
|
||||
? callFeed.userId === activeSpeaker
|
||||
: false,
|
||||
isLocal: callFeed.isLocal(),
|
||||
presenter: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -107,29 +139,27 @@ export function InCallView({
|
||||
callFeed,
|
||||
focused: true,
|
||||
isLocal: callFeed.isLocal(),
|
||||
presenter: false,
|
||||
});
|
||||
}
|
||||
|
||||
return participants;
|
||||
}, [userMediaFeeds, activeSpeaker, screenshareFeeds, layout]);
|
||||
|
||||
const renderAvatar = useCallback(
|
||||
(roomMember, width, height) => {
|
||||
const avatarUrl = roomMember.user?.avatarUrl;
|
||||
const size = Math.round(Math.min(width, height) / 2);
|
||||
const renderAvatar = useCallback((roomMember, width, height) => {
|
||||
const avatarUrl = roomMember.user?.avatarUrl;
|
||||
const size = Math.round(Math.min(width, height) / 2);
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
key={roomMember.userId}
|
||||
size={size}
|
||||
src={avatarUrl}
|
||||
fallback={roomMember.name.slice(0, 1).toUpperCase()}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
return (
|
||||
<Avatar
|
||||
key={roomMember.userId}
|
||||
size={size}
|
||||
src={avatarUrl}
|
||||
fallback={roomMember.name.slice(0, 1).toUpperCase()}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
modalState: rageshakeRequestModalState,
|
||||
@ -158,7 +188,7 @@ export function InCallView({
|
||||
</div>
|
||||
) : (
|
||||
<VideoGrid items={items} layout={layout} disableAnimations={isSafari}>
|
||||
{({ item, ...rest }) => (
|
||||
{({ item, ...rest }: { item: Participant; [x: string]: unknown }) => (
|
||||
<VideoTileContainer
|
||||
key={item.id}
|
||||
item={item}
|
||||
@ -185,7 +215,6 @@ export function InCallView({
|
||||
<OverflowMenu
|
||||
inCall
|
||||
roomId={roomId}
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
showInvite={true}
|
||||
feedbackModalState={feedbackModalState}
|
@ -15,12 +15,19 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
import { CopyButton } from "../button";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import styles from "./InviteModal.module.css";
|
||||
|
||||
export function InviteModal({ roomId, ...rest }) {
|
||||
export function InviteModal({
|
||||
roomId,
|
||||
...rest
|
||||
}: {
|
||||
roomId: string;
|
||||
[x: string]: unknown;
|
||||
}) {
|
||||
return (
|
||||
<Modal
|
||||
title="Invite People"
|
@ -15,10 +15,14 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { PressEvent } from "@react-types/shared";
|
||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||
|
||||
import styles from "./LobbyView.module.css";
|
||||
import { Button, CopyButton } from "../button";
|
||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { useCallFeed } from "../video-grid/useCallFeed";
|
||||
import { getRoomUrl } from "../matrix-utils";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
@ -28,6 +32,22 @@ import { useMediaHandler } from "../settings/useMediaHandler";
|
||||
import { VideoPreview } from "./VideoPreview";
|
||||
import { AudioPreview } from "./AudioPreview";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
groupCall: GroupCall;
|
||||
roomName: string;
|
||||
avatarUrl: string;
|
||||
state: GroupCallState;
|
||||
onInitLocalCallFeed: () => void;
|
||||
onEnter: (e: PressEvent) => void;
|
||||
localCallFeed: CallFeed;
|
||||
microphoneMuted: boolean;
|
||||
toggleLocalVideoMuted: () => void;
|
||||
toggleMicrophoneMuted: () => void;
|
||||
localVideoMuted: boolean;
|
||||
roomId: string;
|
||||
isEmbedded: boolean;
|
||||
}
|
||||
export function LobbyView({
|
||||
client,
|
||||
groupCall,
|
||||
@ -43,7 +63,7 @@ export function LobbyView({
|
||||
toggleMicrophoneMuted,
|
||||
roomId,
|
||||
isEmbedded,
|
||||
}) {
|
||||
}: Props) {
|
||||
const { stream } = useCallFeed(localCallFeed);
|
||||
const {
|
||||
audioInput,
|
||||
@ -60,7 +80,7 @@ export function LobbyView({
|
||||
|
||||
useLocationNavigation(state === GroupCallState.InitializingLocalCallFeed);
|
||||
|
||||
const joinCallButtonRef = useRef();
|
||||
const joinCallButtonRef = useRef<HTMLButtonElement>();
|
||||
|
||||
useEffect(() => {
|
||||
if (state === GroupCallState.LocalCallFeedInitialized) {
|
@ -15,10 +15,13 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from "react";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||
|
||||
import { Button } from "../button";
|
||||
import { Menu } from "../Menu";
|
||||
import { PopoverMenuTrigger } from "../popover/PopoverMenu";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { ReactComponent as SettingsIcon } from "../icons/Settings.svg";
|
||||
import { ReactComponent as AddUserIcon } from "../icons/AddUser.svg";
|
||||
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
|
||||
@ -28,7 +31,17 @@ import { SettingsModal } from "../settings/SettingsModal";
|
||||
import { InviteModal } from "./InviteModal";
|
||||
import { TooltipTrigger } from "../Tooltip";
|
||||
import { FeedbackModal } from "./FeedbackModal";
|
||||
|
||||
interface Props {
|
||||
roomId: string;
|
||||
inCall: boolean;
|
||||
groupCall: GroupCall;
|
||||
showInvite: boolean;
|
||||
feedbackModalState: OverlayTriggerState;
|
||||
feedbackModalProps: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
}
|
||||
export function OverflowMenu({
|
||||
roomId,
|
||||
inCall,
|
||||
@ -36,27 +49,46 @@ export function OverflowMenu({
|
||||
showInvite,
|
||||
feedbackModalState,
|
||||
feedbackModalProps,
|
||||
}) {
|
||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||
useModalTriggerState();
|
||||
const { modalState: settingsModalState, modalProps: settingsModalProps } =
|
||||
useModalTriggerState();
|
||||
}: Props) {
|
||||
const {
|
||||
modalState: inviteModalState,
|
||||
modalProps: inviteModalProps,
|
||||
}: {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
} = useModalTriggerState();
|
||||
const {
|
||||
modalState: settingsModalState,
|
||||
modalProps: settingsModalProps,
|
||||
}: {
|
||||
modalState: OverlayTriggerState;
|
||||
modalProps: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
} = useModalTriggerState();
|
||||
|
||||
// TODO: On closing modal, focus should be restored to the trigger button
|
||||
// https://github.com/adobe/react-spectrum/issues/2444
|
||||
const onAction = useCallback((key) => {
|
||||
switch (key) {
|
||||
case "invite":
|
||||
inviteModalState.open();
|
||||
break;
|
||||
case "settings":
|
||||
settingsModalState.open();
|
||||
break;
|
||||
case "feedback":
|
||||
feedbackModalState.open();
|
||||
break;
|
||||
}
|
||||
});
|
||||
const onAction = useCallback(
|
||||
(key) => {
|
||||
switch (key) {
|
||||
case "invite":
|
||||
inviteModalState.open();
|
||||
break;
|
||||
case "settings":
|
||||
settingsModalState.open();
|
||||
break;
|
||||
case "feedback":
|
||||
feedbackModalState.open();
|
||||
break;
|
||||
}
|
||||
},
|
||||
[feedbackModalState, inviteModalState, settingsModalState]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -66,8 +98,8 @@ export function OverflowMenu({
|
||||
<OverflowIcon />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{(props) => (
|
||||
<Menu {...props} label="More menu" onAction={onAction}>
|
||||
{(props: JSX.IntrinsicAttributes) => (
|
||||
<Menu {...props} label="more menu" onAction={onAction}>
|
||||
{showInvite && (
|
||||
<Item key="invite" textValue="Invite people">
|
||||
<AddUserIcon />
|
@ -206,7 +206,6 @@ export const PTTCallView: React.FC<Props> = ({
|
||||
<OverflowMenu
|
||||
inCall
|
||||
roomId={roomId}
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
showInvite={false}
|
||||
feedbackModalState={feedbackModalState}
|
||||
|
@ -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 />;
|
@ -15,20 +15,30 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
import { Button } from "../button";
|
||||
import { FieldRow, ErrorMessage } from "../input/Input";
|
||||
import { useSubmitRageshake } from "../settings/submit-rageshake";
|
||||
import { Body } from "../typography/Typography";
|
||||
|
||||
export function RageshakeRequestModal({ rageshakeRequestId, roomId, ...rest }) {
|
||||
export function RageshakeRequestModal({
|
||||
rageshakeRequestId,
|
||||
roomId,
|
||||
...rest
|
||||
}: {
|
||||
rageshakeRequestId: string;
|
||||
roomId: string;
|
||||
onClose: () => void;
|
||||
[x: string]: unknown;
|
||||
}) {
|
||||
const { submitRageshake, sending, sent, error } = useSubmitRageshake();
|
||||
|
||||
useEffect(() => {
|
||||
if (sent) {
|
||||
rest.onClose();
|
||||
}
|
||||
}, [sent, rest.onClose]);
|
||||
}, [sent, rest]);
|
||||
|
||||
return (
|
||||
<Modal title="Debug Log Request" isDismissable {...rest}>
|
@ -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);
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
|
||||
import { useClient } from "../ClientContext";
|
||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||
import { RoomAuthView } from "./RoomAuthView";
|
||||
@ -29,7 +30,7 @@ export function RoomPage() {
|
||||
useClient();
|
||||
|
||||
const { roomId: maybeRoomId } = useParams();
|
||||
const { hash, search } = useLocation();
|
||||
const { hash, search }: { hash: string; search: string } = useLocation();
|
||||
const [viaServers, isEmbedded, isPtt, displayName] = useMemo(() => {
|
||||
const params = new URLSearchParams(search);
|
||||
return [
|
||||
@ -40,8 +41,7 @@ export function RoomPage() {
|
||||
];
|
||||
}, [search]);
|
||||
const roomId = (maybeRoomId || hash || "").toLowerCase();
|
||||
const { registerPasswordlessUser, recaptchaId } =
|
||||
useRegisterPasswordlessUser();
|
||||
const { registerPasswordlessUser } = useRegisterPasswordlessUser();
|
||||
const [isRegistering, setIsRegistering] = useState(false);
|
||||
|
||||
useEffect(() => {
|
@ -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";
|
||||
|
@ -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);
|
@ -15,18 +15,31 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { ResizeObserver } from "@juggle/resize-observer";
|
||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { MicButton, VideoButton } from "../button";
|
||||
import { useMediaStream } from "../video-grid/useMediaStream";
|
||||
import { OverflowMenu } from "./OverflowMenu";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { useProfile } from "../profile/useProfile";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { ResizeObserver } from "@juggle/resize-observer";
|
||||
import { GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import styles from "./VideoPreview.module.css";
|
||||
import { Body } from "../typography/Typography";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
state: GroupCallState;
|
||||
roomId: string;
|
||||
microphoneMuted: boolean;
|
||||
localVideoMuted: boolean;
|
||||
toggleLocalVideoMuted: () => void;
|
||||
toggleMicrophoneMuted: () => void;
|
||||
audioOutput: string;
|
||||
stream: MediaStream;
|
||||
}
|
||||
export function VideoPreview({
|
||||
client,
|
||||
state,
|
||||
@ -37,7 +50,7 @@ export function VideoPreview({
|
||||
toggleMicrophoneMuted,
|
||||
audioOutput,
|
||||
stream,
|
||||
}) {
|
||||
}: Props) {
|
||||
const videoRef = useMediaStream(stream, audioOutput, true);
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver });
|
||||
@ -81,9 +94,11 @@ export function VideoPreview({
|
||||
/>
|
||||
<OverflowMenu
|
||||
roomId={roomId}
|
||||
client={client}
|
||||
feedbackModalState={feedbackModalState}
|
||||
feedbackModalProps={feedbackModalProps}
|
||||
inCall={false}
|
||||
groupCall={undefined}
|
||||
showInvite={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
@ -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,
|
||||
|
@ -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);
|
@ -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]);
|
@ -32,9 +32,8 @@ import { useDownloadDebugLog } from "./submit-rageshake";
|
||||
import { Body } from "../typography/Typography";
|
||||
|
||||
interface Props {
|
||||
setShowInspector: boolean;
|
||||
showInspector: boolean;
|
||||
[rest: string]: unknown;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const SettingsModal = (props: Props) => {
|
||||
|
@ -26,11 +26,11 @@ import { InspectorContext } from "../room/GroupCallInspector";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
|
||||
interface RageShakeSubmitOptions {
|
||||
description: string;
|
||||
roomId?: string;
|
||||
label?: string;
|
||||
sendLogs: boolean;
|
||||
rageshakeRequestId?: string;
|
||||
description?: string;
|
||||
roomId?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export function useSubmitRageshake(): {
|
||||
@ -40,7 +40,7 @@ export function useSubmitRageshake(): {
|
||||
error: Error;
|
||||
} {
|
||||
const client: MatrixClient = useClient().client;
|
||||
const [{ json }] = useContext(InspectorContext);
|
||||
const json = useContext(InspectorContext);
|
||||
|
||||
const [{ sending, sent, error }, setState] = useState({
|
||||
sending: false,
|
||||
@ -274,7 +274,7 @@ export function useSubmitRageshake(): {
|
||||
}
|
||||
|
||||
export function useDownloadDebugLog(): () => void {
|
||||
const [{ json }] = useContext(InspectorContext);
|
||||
const json = useContext(InspectorContext);
|
||||
|
||||
const downloadDebugLog = useCallback(() => {
|
||||
const blob = new Blob([JSON.stringify(json)], { type: "application/json" });
|
||||
|
@ -24,6 +24,7 @@ import React, {
|
||||
useMemo,
|
||||
useContext,
|
||||
createContext,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
|
||||
export interface MediaHandlerContextInterface {
|
||||
@ -73,7 +74,7 @@ function updateMediaPreferences(newPreferences: MediaPreferences): void {
|
||||
}
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
children: JSX.Element[];
|
||||
children: ReactNode;
|
||||
}
|
||||
export function MediaHandlerProvider({ client, children }: Props): JSX.Element {
|
||||
const [
|
||||
|
@ -34,7 +34,7 @@ export const useMediaStream = (
|
||||
stream: MediaStream,
|
||||
audioOutputDevice: string,
|
||||
mute = false,
|
||||
localVolume: number
|
||||
localVolume?: number
|
||||
): RefObject<MediaElement> => {
|
||||
const mediaRef = useRef<MediaElement>();
|
||||
|
||||
@ -196,7 +196,7 @@ export const useSpatialMediaStream = (
|
||||
audioContext: AudioContext,
|
||||
audioDestination: AudioNode,
|
||||
mute = false,
|
||||
localVolume: number
|
||||
localVolume?: number
|
||||
): [RefObject<Element>, RefObject<MediaElement>] => {
|
||||
const tileRef = useRef<Element>();
|
||||
const [spatialAudio] = useSpatialAudio();
|
||||
|
@ -8,14 +8,7 @@
|
||||
"noImplicitAny": false,
|
||||
"noUnusedLocals": true,
|
||||
"jsx": "preserve",
|
||||
"lib": [
|
||||
"es2020",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
],
|
||||
"lib": ["es2020", "dom", "dom.iterable"]
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx",
|
||||
],
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user