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