Merge branch 'main' into matroska
This commit is contained in:
commit
eb43b96a1b
79 changed files with 1416 additions and 748 deletions
|
@ -27,3 +27,4 @@
|
||||||
# VITE_THEME_QUINARY_CONTENT=#394049
|
# VITE_THEME_QUINARY_CONTENT=#394049
|
||||||
# VITE_THEME_SYSTEM=#21262c
|
# VITE_THEME_SYSTEM=#21262c
|
||||||
# VITE_THEME_BACKGROUND=#15191e
|
# VITE_THEME_BACKGROUND=#15191e
|
||||||
|
# VITE_THEME_BACKGROUND_85=#15191ed9
|
||||||
|
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from "react";
|
||||||
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { OverlayProvider } from "@react-aria/overlays";
|
import { OverlayProvider } from "@react-aria/overlays";
|
||||||
|
|
||||||
import { HomePage } from "./home/HomePage";
|
import { HomePage } from "./home/HomePage";
|
||||||
import { LoginPage } from "./auth/LoginPage";
|
import { LoginPage } from "./auth/LoginPage";
|
||||||
import { RegisterPage } from "./auth/RegisterPage";
|
import { RegisterPage } from "./auth/RegisterPage";
|
||||||
|
@ -31,7 +32,11 @@ import { CrashView } from "./FullScreenView";
|
||||||
|
|
||||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||||
|
|
||||||
export default function App({ history }) {
|
interface AppProps {
|
||||||
|
history: History;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App({ history }: AppProps) {
|
||||||
usePageFocusStyle();
|
usePageFocusStyle();
|
||||||
|
|
||||||
const errorPage = <CrashView />;
|
const errorPage = <CrashView />;
|
|
@ -48,11 +48,11 @@ const resolveAvatarSrc = (client: MatrixClient, src: string, size: number) =>
|
||||||
|
|
||||||
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
bgKey?: string;
|
bgKey?: string;
|
||||||
src: string;
|
src?: string;
|
||||||
fallback: string;
|
|
||||||
size?: Size | number;
|
size?: Size | number;
|
||||||
className: string;
|
className?: string;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
|
fallback: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Avatar: React.FC<Props> = ({
|
export const Avatar: React.FC<Props> = ({
|
||||||
|
|
|
@ -67,6 +67,7 @@ interface ClientState {
|
||||||
changePassword: (password: string) => Promise<void>;
|
changePassword: (password: string) => Promise<void>;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
setClient: (client: MatrixClient, session: Session) => void;
|
setClient: (client: MatrixClient, session: Session) => void;
|
||||||
|
error?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClientContext = createContext<ClientState>(null);
|
const ClientContext = createContext<ClientState>(null);
|
||||||
|
@ -151,6 +152,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
isAuthenticated: Boolean(client),
|
isAuthenticated: Boolean(client),
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
userName: client?.getUserIdLocalpart(),
|
userName: client?.getUserIdLocalpart(),
|
||||||
|
error: undefined,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
@ -161,6 +163,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isPasswordlessUser: false,
|
isPasswordlessUser: false,
|
||||||
userName: null,
|
userName: null,
|
||||||
|
error: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -190,6 +193,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]
|
||||||
|
@ -210,6 +214,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();
|
||||||
|
@ -220,6 +225,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
isPasswordlessUser: false,
|
isPasswordlessUser: false,
|
||||||
userName: null,
|
userName: null,
|
||||||
|
error: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -278,6 +284,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
logout,
|
logout,
|
||||||
userName,
|
userName,
|
||||||
setClient,
|
setClient,
|
||||||
|
error: undefined,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
loading,
|
loading,
|
||||||
|
|
|
@ -1,22 +1,48 @@
|
||||||
import React from "react";
|
/*
|
||||||
import styles from "./Facepile.module.css";
|
Copyright 2022 New Vector Ltd
|
||||||
import classNames from "classnames";
|
|
||||||
import { Avatar, sizes } from "./Avatar";
|
|
||||||
|
|
||||||
const overlapMap = {
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
xs: 2,
|
you may not use this file except in compliance with the License.
|
||||||
sm: 4,
|
You may obtain a copy of the License at
|
||||||
md: 8,
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { HTMLAttributes } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { MatrixClient, RoomMember } from "matrix-js-sdk";
|
||||||
|
|
||||||
|
import styles from "./Facepile.module.css";
|
||||||
|
import { Avatar, Size, sizes } from "./Avatar";
|
||||||
|
|
||||||
|
const overlapMap: Partial<Record<Size, number>> = {
|
||||||
|
[Size.XS]: 2,
|
||||||
|
[Size.SM]: 4,
|
||||||
|
[Size.MD]: 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
className: string;
|
||||||
|
client: MatrixClient;
|
||||||
|
participants: RoomMember[];
|
||||||
|
max?: number;
|
||||||
|
size?: Size;
|
||||||
|
}
|
||||||
|
|
||||||
export function Facepile({
|
export function Facepile({
|
||||||
className,
|
className,
|
||||||
client,
|
client,
|
||||||
participants,
|
participants,
|
||||||
max,
|
max = 3,
|
||||||
size,
|
size = Size.XS,
|
||||||
...rest
|
...rest
|
||||||
}) {
|
}: Props) {
|
||||||
const _size = sizes.get(size);
|
const _size = sizes.get(size);
|
||||||
const _overlap = overlapMap[size];
|
const _overlap = overlapMap[size];
|
||||||
|
|
||||||
|
@ -56,8 +82,3 @@ export function Facepile({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Facepile.defaultProps = {
|
|
||||||
max: 3,
|
|
||||||
size: "xs",
|
|
||||||
};
|
|
|
@ -1,13 +1,19 @@
|
||||||
import React, { useCallback, useEffect } from "react";
|
import React, { ReactNode, useCallback, useEffect } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import styles from "./FullScreenView.module.css";
|
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
|
||||||
import { LinkButton, Button } from "./button";
|
import { LinkButton, Button } from "./button";
|
||||||
import { useSubmitRageshake } from "./settings/submit-rageshake";
|
import { useSubmitRageshake } from "./settings/submit-rageshake";
|
||||||
import { ErrorMessage } from "./input/Input";
|
import { ErrorMessage } from "./input/Input";
|
||||||
|
import styles from "./FullScreenView.module.css";
|
||||||
|
|
||||||
export function FullScreenView({ className, children }) {
|
interface FullScreenViewProps {
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FullScreenView({ className, children }: FullScreenViewProps) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.page, className)}>
|
<div className={classNames(styles.page, className)}>
|
||||||
<Header>
|
<Header>
|
||||||
|
@ -23,7 +29,11 @@ export function FullScreenView({ className, children }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorView({ error }) {
|
interface ErrorViewProps {
|
||||||
|
error: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorView({ error }: ErrorViewProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -31,7 +41,7 @@ export function ErrorView({ error }) {
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
const onReload = useCallback(() => {
|
const onReload = useCallback(() => {
|
||||||
window.location = "/";
|
window.location.href = "/";
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -72,7 +82,7 @@ export function CrashView() {
|
||||||
}, [submitRageshake]);
|
}, [submitRageshake]);
|
||||||
|
|
||||||
const onReload = useCallback(() => {
|
const onReload = useCallback(() => {
|
||||||
window.location = "/";
|
window.location.href = "/";
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
let logsComponent;
|
let logsComponent;
|
|
@ -1,18 +1,26 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { useCallback, useRef } from "react";
|
import React, { HTMLAttributes, ReactNode, useCallback, useRef } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import styles from "./Header.module.css";
|
|
||||||
import { ReactComponent as Logo } from "./icons/Logo.svg";
|
|
||||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
|
||||||
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
|
||||||
import { useButton } from "@react-aria/button";
|
import { useButton } from "@react-aria/button";
|
||||||
import { Subtitle } from "./typography/Typography";
|
import { AriaButtonProps } from "@react-types/button";
|
||||||
import { Avatar } from "./Avatar";
|
import { Room } from "matrix-js-sdk";
|
||||||
import { IncompatibleVersionModal } from "./IncompatibleVersionModal";
|
|
||||||
|
import styles from "./Header.module.css";
|
||||||
import { useModalTriggerState } from "./Modal";
|
import { useModalTriggerState } from "./Modal";
|
||||||
import { Button } from "./button";
|
import { Button } from "./button";
|
||||||
|
import { ReactComponent as Logo } from "./icons/Logo.svg";
|
||||||
|
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||||
|
import { Subtitle } from "./typography/Typography";
|
||||||
|
import { Avatar, Size } from "./Avatar";
|
||||||
|
import { IncompatibleVersionModal } from "./IncompatibleVersionModal";
|
||||||
|
import { ReactComponent as ArrowLeftIcon } from "./icons/ArrowLeft.svg";
|
||||||
|
|
||||||
export function Header({ children, className, ...rest }) {
|
interface HeaderProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({ children, className, ...rest }: HeaderProps) {
|
||||||
return (
|
return (
|
||||||
<header className={classNames(styles.header, className)} {...rest}>
|
<header className={classNames(styles.header, className)} {...rest}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -20,7 +28,18 @@ export function Header({ children, className, ...rest }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LeftNav({ children, className, hideMobile, ...rest }) {
|
interface LeftNavProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
hideMobile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LeftNav({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
hideMobile,
|
||||||
|
...rest
|
||||||
|
}: LeftNavProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -36,7 +55,18 @@ export function LeftNav({ children, className, hideMobile, ...rest }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RightNav({ children, className, hideMobile, ...rest }) {
|
interface RightNavProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
hideMobile?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RightNav({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
hideMobile,
|
||||||
|
...rest
|
||||||
|
}: RightNavProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
@ -52,7 +82,11 @@ export function RightNav({ children, className, hideMobile, ...rest }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeaderLogo({ className }) {
|
interface HeaderLogoProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HeaderLogo({ className }: HeaderLogoProps) {
|
||||||
return (
|
return (
|
||||||
<Link className={classNames(styles.headerLogo, className)} to="/">
|
<Link className={classNames(styles.headerLogo, className)} to="/">
|
||||||
<Logo />
|
<Logo />
|
||||||
|
@ -60,12 +94,17 @@ export function HeaderLogo({ className }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RoomHeaderInfo({ roomName, avatarUrl }) {
|
interface RoomHeaderInfo {
|
||||||
|
roomName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoomHeaderInfo({ roomName, avatarUrl }: RoomHeaderInfo) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.roomAvatar}>
|
<div className={styles.roomAvatar}>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="md"
|
size={Size.MD}
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
bgKey={roomName}
|
bgKey={roomName}
|
||||||
fallback={roomName.slice(0, 1).toUpperCase()}
|
fallback={roomName.slice(0, 1).toUpperCase()}
|
||||||
|
@ -77,12 +116,18 @@ export function RoomHeaderInfo({ roomName, avatarUrl }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RoomSetupHeaderInfoProps extends AriaButtonProps<"button"> {
|
||||||
|
roomName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
isEmbedded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function RoomSetupHeaderInfo({
|
export function RoomSetupHeaderInfo({
|
||||||
roomName,
|
roomName,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
...rest
|
...rest
|
||||||
}) {
|
}: RoomSetupHeaderInfoProps) {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
const { buttonProps } = useButton(rest, ref);
|
const { buttonProps } = useButton(rest, ref);
|
||||||
|
|
||||||
|
@ -102,7 +147,15 @@ export function RoomSetupHeaderInfo({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VersionMismatchWarning({ users, room }) {
|
interface VersionMismatchWarningProps {
|
||||||
|
users: Set<string>;
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VersionMismatchWarning({
|
||||||
|
users,
|
||||||
|
room,
|
||||||
|
}: VersionMismatchWarningProps) {
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
|
||||||
const onDetailsClick = useCallback(() => {
|
const onDetailsClick = useCallback(() => {
|
|
@ -1,5 +0,0 @@
|
||||||
import { IndexedDBStoreWorker } from "matrix-js-sdk/src/indexeddb-worker";
|
|
||||||
|
|
||||||
const remoteWorker = new IndexedDBStoreWorker(self.postMessage);
|
|
||||||
|
|
||||||
self.onmessage = remoteWorker.onMessage;
|
|
6
src/IndexedDBWorker.ts
Normal file
6
src/IndexedDBWorker.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { IndexedDBStoreWorker } from "matrix-js-sdk/src/indexeddb-worker";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const remoteWorker = new IndexedDBStoreWorker((self as any).postMessage);
|
||||||
|
|
||||||
|
self.onmessage = remoteWorker.onMessage;
|
|
@ -1,50 +0,0 @@
|
||||||
import React, { useRef } from "react";
|
|
||||||
import { useListBox, useOption } from "@react-aria/listbox";
|
|
||||||
import styles from "./ListBox.module.css";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
export function ListBox(props) {
|
|
||||||
const ref = useRef();
|
|
||||||
let { listBoxRef = ref, state } = props;
|
|
||||||
const { listBoxProps } = useListBox(props, state, listBoxRef);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul
|
|
||||||
{...listBoxProps}
|
|
||||||
ref={listBoxRef}
|
|
||||||
className={classNames(styles.listBox, props.className)}
|
|
||||||
>
|
|
||||||
{[...state.collection].map((item) => (
|
|
||||||
<Option
|
|
||||||
key={item.key}
|
|
||||||
item={item}
|
|
||||||
state={state}
|
|
||||||
className={props.optionClassName}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Option({ item, state, className }) {
|
|
||||||
const ref = useRef();
|
|
||||||
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
|
|
||||||
{ key: item.key },
|
|
||||||
state,
|
|
||||||
ref
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
{...optionProps}
|
|
||||||
ref={ref}
|
|
||||||
className={classNames(styles.option, className, {
|
|
||||||
[styles.selected]: isSelected,
|
|
||||||
[styles.focused]: isFocused,
|
|
||||||
[styles.disables]: isDisabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item.rendered}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
89
src/ListBox.tsx
Normal file
89
src/ListBox.tsx
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
import { useListBox, useOption, AriaListBoxOptions } from "@react-aria/listbox";
|
||||||
|
import { ListState } from "@react-stately/list";
|
||||||
|
import { Node } from "@react-types/shared";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import styles from "./ListBox.module.css";
|
||||||
|
|
||||||
|
interface ListBoxProps<T> extends AriaListBoxOptions<T> {
|
||||||
|
className: string;
|
||||||
|
optionClassName: string;
|
||||||
|
listBoxRef: React.MutableRefObject<HTMLUListElement>;
|
||||||
|
state: ListState<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListBox<T>({
|
||||||
|
state,
|
||||||
|
optionClassName,
|
||||||
|
className,
|
||||||
|
listBoxRef,
|
||||||
|
...rest
|
||||||
|
}: ListBoxProps<T>) {
|
||||||
|
const ref = useRef<HTMLUListElement>();
|
||||||
|
if (!listBoxRef) listBoxRef = ref;
|
||||||
|
|
||||||
|
const { listBoxProps } = useListBox(rest, state, listBoxRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
{...listBoxProps}
|
||||||
|
ref={listBoxRef}
|
||||||
|
className={classNames(styles.listBox, className)}
|
||||||
|
>
|
||||||
|
{[...state.collection].map((item) => (
|
||||||
|
<Option
|
||||||
|
key={item.key}
|
||||||
|
item={item}
|
||||||
|
state={state}
|
||||||
|
className={optionClassName}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OptionProps<T> {
|
||||||
|
className: string;
|
||||||
|
state: ListState<T>;
|
||||||
|
item: Node<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Option<T>({ item, state, className }: OptionProps<T>) {
|
||||||
|
const ref = useRef();
|
||||||
|
const { optionProps, isSelected, isFocused, isDisabled } = useOption(
|
||||||
|
{ key: item.key },
|
||||||
|
state,
|
||||||
|
ref
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
{...optionProps}
|
||||||
|
ref={ref}
|
||||||
|
className={classNames(styles.option, className, {
|
||||||
|
[styles.selected]: isSelected,
|
||||||
|
[styles.focused]: isFocused,
|
||||||
|
[styles.disables]: isDisabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item.rendered}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,15 +1,30 @@
|
||||||
import React, { useRef, useState } from "react";
|
import React, { Key, useRef, useState } from "react";
|
||||||
import styles from "./Menu.module.css";
|
import { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu";
|
||||||
import { useMenu, useMenuItem } from "@react-aria/menu";
|
import { TreeState, useTreeState } from "@react-stately/tree";
|
||||||
import { useTreeState } from "@react-stately/tree";
|
|
||||||
import { mergeProps } from "@react-aria/utils";
|
import { mergeProps } from "@react-aria/utils";
|
||||||
import { useFocus } from "@react-aria/interactions";
|
import { useFocus } from "@react-aria/interactions";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { Node } from "@react-types/shared";
|
||||||
|
|
||||||
export function Menu({ className, onAction, ...rest }) {
|
import styles from "./Menu.module.css";
|
||||||
const state = useTreeState({ ...rest, selectionMode: "none" });
|
|
||||||
|
interface MenuProps<T> extends AriaMenuOptions<T> {
|
||||||
|
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" });
|
||||||
const menuRef = useRef();
|
const menuRef = useRef();
|
||||||
const { menuProps } = useMenu(rest, state, menuRef);
|
const { menuProps } = useMenu<T>(rest, state, menuRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul
|
<ul
|
||||||
|
@ -23,19 +38,25 @@ export function Menu({ className, onAction, ...rest }) {
|
||||||
item={item}
|
item={item}
|
||||||
state={state}
|
state={state}
|
||||||
onAction={onAction}
|
onAction={onAction}
|
||||||
onClose={rest.onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuItem({ item, state, onAction, onClose }) {
|
interface MenuItemProps<T> {
|
||||||
|
item: Node<T>;
|
||||||
|
state: TreeState<T>;
|
||||||
|
onAction: (value: Key) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItem<T>({ item, state, onAction, onClose }: MenuItemProps<T>) {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
const { menuItemProps } = useMenuItem(
|
const { menuItemProps } = useMenuItem(
|
||||||
{
|
{
|
||||||
key: item.key,
|
key: item.key,
|
||||||
isDisabled: item.isDisabled,
|
|
||||||
onAction,
|
onAction,
|
||||||
onClose,
|
onClose,
|
||||||
},
|
},
|
|
@ -28,6 +28,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.modalHeader h3 {
|
.modalHeader h3 {
|
||||||
|
font-weight: 600;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,73 @@
|
||||||
import React, { useRef, useMemo } from "react";
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable jsx-a11y/no-autofocus */
|
||||||
|
|
||||||
|
import React, { useRef, useMemo, ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
useOverlay,
|
useOverlay,
|
||||||
usePreventScroll,
|
usePreventScroll,
|
||||||
useModal,
|
useModal,
|
||||||
OverlayContainer,
|
OverlayContainer,
|
||||||
|
OverlayProps,
|
||||||
} from "@react-aria/overlays";
|
} from "@react-aria/overlays";
|
||||||
import { useOverlayTriggerState } from "@react-stately/overlays";
|
import {
|
||||||
|
OverlayTriggerState,
|
||||||
|
useOverlayTriggerState,
|
||||||
|
} from "@react-stately/overlays";
|
||||||
import { useDialog } from "@react-aria/dialog";
|
import { useDialog } from "@react-aria/dialog";
|
||||||
import { FocusScope } from "@react-aria/focus";
|
import { FocusScope } from "@react-aria/focus";
|
||||||
import { useButton } from "@react-aria/button";
|
import { ButtonAria, useButton } from "@react-aria/button";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { AriaDialogProps } from "@react-types/dialog";
|
||||||
|
|
||||||
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
import { ReactComponent as CloseIcon } from "./icons/Close.svg";
|
||||||
import styles from "./Modal.module.css";
|
import styles from "./Modal.module.css";
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
export function Modal(props) {
|
export interface ModalProps extends OverlayProps, AriaDialogProps {
|
||||||
const { title, children, className, mobileFullScreen } = props;
|
title: string;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
mobileFullScreen?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Modal({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
mobileFullScreen,
|
||||||
|
onClose,
|
||||||
|
...rest
|
||||||
|
}: ModalProps) {
|
||||||
const modalRef = useRef();
|
const modalRef = useRef();
|
||||||
const { overlayProps, underlayProps } = useOverlay(props, modalRef);
|
const { overlayProps, underlayProps } = useOverlay(
|
||||||
|
{ ...rest, onClose },
|
||||||
|
modalRef
|
||||||
|
);
|
||||||
usePreventScroll();
|
usePreventScroll();
|
||||||
const { modalProps } = useModal();
|
const { modalProps } = useModal();
|
||||||
const { dialogProps, titleProps } = useDialog(props, modalRef);
|
const { dialogProps, titleProps } = useDialog(rest, modalRef);
|
||||||
const closeButtonRef = useRef();
|
const closeButtonRef = useRef();
|
||||||
const { buttonProps: closeButtonProps } = useButton({
|
const { buttonProps: closeButtonProps } = useButton(
|
||||||
onPress: () => props.onClose(),
|
{
|
||||||
});
|
onPress: () => onClose(),
|
||||||
|
},
|
||||||
|
closeButtonRef
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OverlayContainer>
|
<OverlayContainer>
|
||||||
|
@ -58,7 +102,16 @@ export function Modal(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModalContent({ children, className, ...rest }) {
|
interface ModalContentProps {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalContent({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: ModalContentProps) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.content, className)} {...rest}>
|
<div className={classNames(styles.content, className)} {...rest}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -66,7 +119,10 @@ export function ModalContent({ children, className, ...rest }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useModalTriggerState() {
|
export function useModalTriggerState(): {
|
||||||
|
modalState: OverlayTriggerState;
|
||||||
|
modalProps: { isOpen: boolean; onClose: () => void };
|
||||||
|
} {
|
||||||
const modalState = useOverlayTriggerState({});
|
const modalState = useOverlayTriggerState({});
|
||||||
const modalProps = useMemo(
|
const modalProps = useMemo(
|
||||||
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
|
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
|
||||||
|
@ -75,7 +131,10 @@ export function useModalTriggerState() {
|
||||||
return { modalState, modalProps };
|
return { modalState, modalProps };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useToggleModalButton(modalState, ref) {
|
export function useToggleModalButton(
|
||||||
|
modalState: OverlayTriggerState,
|
||||||
|
ref: React.RefObject<HTMLButtonElement>
|
||||||
|
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||||
return useButton(
|
return useButton(
|
||||||
{
|
{
|
||||||
onPress: () => modalState.toggle(),
|
onPress: () => modalState.toggle(),
|
||||||
|
@ -84,7 +143,10 @@ export function useToggleModalButton(modalState, ref) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOpenModalButton(modalState, ref) {
|
export function useOpenModalButton(
|
||||||
|
modalState: OverlayTriggerState,
|
||||||
|
ref: React.RefObject<HTMLButtonElement>
|
||||||
|
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||||
return useButton(
|
return useButton(
|
||||||
{
|
{
|
||||||
onPress: () => modalState.open(),
|
onPress: () => modalState.open(),
|
||||||
|
@ -93,7 +155,10 @@ export function useOpenModalButton(modalState, ref) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCloseModalButton(modalState, ref) {
|
export function useCloseModalButton(
|
||||||
|
modalState: OverlayTriggerState,
|
||||||
|
ref: React.RefObject<HTMLButtonElement>
|
||||||
|
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
|
||||||
return useButton(
|
return useButton(
|
||||||
{
|
{
|
||||||
onPress: () => modalState.close(),
|
onPress: () => modalState.close(),
|
||||||
|
@ -102,8 +167,12 @@ export function useCloseModalButton(modalState, ref) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModalTrigger({ children }) {
|
interface ModalTriggerProps {
|
||||||
const { modalState, modalProps } = useModalState();
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ModalTrigger({ children }: ModalTriggerProps) {
|
||||||
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
const buttonRef = useRef();
|
const buttonRef = useRef();
|
||||||
const { buttonProps } = useToggleModalButton(modalState, buttonRef);
|
const { buttonProps } = useToggleModalButton(modalState, buttonRef);
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
import React, { useCallback, useState } from "react";
|
|
||||||
import { SequenceDiagramViewer } from "./room/GroupCallInspector";
|
|
||||||
import { FieldRow, InputField } from "./input/Input";
|
|
||||||
import { usePageTitle } from "./usePageTitle";
|
|
||||||
|
|
||||||
export function SequenceDiagramViewerPage() {
|
|
||||||
usePageTitle("Inspector");
|
|
||||||
|
|
||||||
const [debugLog, setDebugLog] = useState();
|
|
||||||
const [selectedUserId, setSelectedUserId] = useState();
|
|
||||||
const onChangeDebugLog = useCallback((e) => {
|
|
||||||
if (e.target.files && e.target.files.length > 0) {
|
|
||||||
e.target.files[0].text().then((text) => {
|
|
||||||
setDebugLog(JSON.parse(text));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ marginTop: 20 }}>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
type="file"
|
|
||||||
id="debugLog"
|
|
||||||
name="debugLog"
|
|
||||||
label="Debug Log"
|
|
||||||
onChange={onChangeDebugLog}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
{debugLog && (
|
|
||||||
<SequenceDiagramViewer
|
|
||||||
localUserId={debugLog.localUserId}
|
|
||||||
selectedUserId={selectedUserId}
|
|
||||||
onSelectUserId={setSelectedUserId}
|
|
||||||
remoteUserIds={debugLog.remoteUserIds}
|
|
||||||
events={debugLog.eventsByUserId[selectedUserId]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
67
src/SequenceDiagramViewerPage.tsx
Normal file
67
src/SequenceDiagramViewerPage.tsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SequenceDiagramViewer,
|
||||||
|
SequenceDiagramMatrixEvent,
|
||||||
|
} from "./room/GroupCallInspector";
|
||||||
|
import { FieldRow, InputField } from "./input/Input";
|
||||||
|
import { usePageTitle } from "./usePageTitle";
|
||||||
|
|
||||||
|
interface DebugLog {
|
||||||
|
localUserId: string;
|
||||||
|
eventsByUserId: { [userId: string]: SequenceDiagramMatrixEvent[] };
|
||||||
|
remoteUserIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SequenceDiagramViewerPage() {
|
||||||
|
usePageTitle("Inspector");
|
||||||
|
|
||||||
|
const [debugLog, setDebugLog] = useState<DebugLog>();
|
||||||
|
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: string) => {
|
||||||
|
setDebugLog(JSON.parse(text));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 20 }}>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
type="file"
|
||||||
|
id="debugLog"
|
||||||
|
name="debugLog"
|
||||||
|
label="Debug Log"
|
||||||
|
onChange={onChangeDebugLog}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
{debugLog && (
|
||||||
|
<SequenceDiagramViewer
|
||||||
|
localUserId={debugLog.localUserId}
|
||||||
|
selectedUserId={selectedUserId}
|
||||||
|
onSelectUserId={setSelectedUserId}
|
||||||
|
remoteUserIds={debugLog.remoteUserIds}
|
||||||
|
events={debugLog.eventsByUserId[selectedUserId]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,76 +0,0 @@
|
||||||
import React, { forwardRef, useRef } from "react";
|
|
||||||
import { useTooltipTriggerState } from "@react-stately/tooltip";
|
|
||||||
import { FocusableProvider } from "@react-aria/focus";
|
|
||||||
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
|
|
||||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
|
||||||
import styles from "./Tooltip.module.css";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
|
||||||
|
|
||||||
export const Tooltip = forwardRef(
|
|
||||||
({ position, state, className, ...props }, ref) => {
|
|
||||||
let { tooltipProps } = useTooltip(props, state);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(styles.tooltip, className)}
|
|
||||||
{...mergeProps(props, tooltipProps)}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const TooltipTrigger = forwardRef(({ children, ...rest }, ref) => {
|
|
||||||
const tooltipState = useTooltipTriggerState(rest);
|
|
||||||
const triggerRef = useObjectRef(ref);
|
|
||||||
const overlayRef = useRef();
|
|
||||||
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
|
||||||
rest,
|
|
||||||
tooltipState,
|
|
||||||
triggerRef
|
|
||||||
);
|
|
||||||
|
|
||||||
const { overlayProps } = useOverlayPosition({
|
|
||||||
placement: rest.placement || "top",
|
|
||||||
targetRef: triggerRef,
|
|
||||||
overlayRef,
|
|
||||||
isOpen: tooltipState.isOpen,
|
|
||||||
offset: 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!Array.isArray(children) ||
|
|
||||||
children.length > 2 ||
|
|
||||||
typeof children[1] !== "function"
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"TooltipTrigger must have two props. The first being a button and the second being a render prop."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [tooltipTrigger, tooltip] = children;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FocusableProvider ref={triggerRef} {...triggerProps}>
|
|
||||||
{<tooltipTrigger.type {...mergeProps(tooltipTrigger.props, rest)} />}
|
|
||||||
{tooltipState.isOpen && (
|
|
||||||
<OverlayContainer>
|
|
||||||
<Tooltip
|
|
||||||
state={tooltipState}
|
|
||||||
{...mergeProps(tooltipProps, overlayProps)}
|
|
||||||
ref={overlayRef}
|
|
||||||
>
|
|
||||||
{tooltip()}
|
|
||||||
</Tooltip>
|
|
||||||
</OverlayContainer>
|
|
||||||
)}
|
|
||||||
</FocusableProvider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
TooltipTrigger.defaultProps = {
|
|
||||||
delay: 250,
|
|
||||||
};
|
|
114
src/Tooltip.tsx
Normal file
114
src/Tooltip.tsx
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
ForwardedRef,
|
||||||
|
forwardRef,
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
TooltipTriggerState,
|
||||||
|
useTooltipTriggerState,
|
||||||
|
} from "@react-stately/tooltip";
|
||||||
|
import { FocusableProvider } from "@react-aria/focus";
|
||||||
|
import { useTooltipTrigger, useTooltip } from "@react-aria/tooltip";
|
||||||
|
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
||||||
|
import { Placement } from "@react-types/overlays";
|
||||||
|
|
||||||
|
import styles from "./Tooltip.module.css";
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
className?: string;
|
||||||
|
state: TooltipTriggerState;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
|
||||||
|
(
|
||||||
|
{ state, className, children, ...rest }: TooltipProps,
|
||||||
|
ref: ForwardedRef<HTMLDivElement>
|
||||||
|
) => {
|
||||||
|
const { tooltipProps } = useTooltip(rest, state);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.tooltip, className)}
|
||||||
|
{...mergeProps(rest, tooltipProps)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
interface TooltipTriggerProps {
|
||||||
|
children: ReactElement;
|
||||||
|
placement?: Placement;
|
||||||
|
delay?: number;
|
||||||
|
tooltip: () => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TooltipTrigger = forwardRef<HTMLElement, TooltipTriggerProps>(
|
||||||
|
(
|
||||||
|
{ children, placement, tooltip, ...rest }: TooltipTriggerProps,
|
||||||
|
ref: ForwardedRef<HTMLElement>
|
||||||
|
) => {
|
||||||
|
const tooltipTriggerProps = { delay: 250, ...rest };
|
||||||
|
const tooltipState = useTooltipTriggerState(tooltipTriggerProps);
|
||||||
|
const triggerRef = useObjectRef<HTMLElement>(ref);
|
||||||
|
const overlayRef = useRef();
|
||||||
|
const { triggerProps, tooltipProps } = useTooltipTrigger(
|
||||||
|
tooltipTriggerProps,
|
||||||
|
tooltipState,
|
||||||
|
triggerRef
|
||||||
|
);
|
||||||
|
|
||||||
|
const { overlayProps } = useOverlayPosition({
|
||||||
|
placement: placement || "top",
|
||||||
|
targetRef: triggerRef,
|
||||||
|
overlayRef,
|
||||||
|
isOpen: tooltipState.isOpen,
|
||||||
|
offset: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FocusableProvider ref={triggerRef} {...triggerProps}>
|
||||||
|
<children.type
|
||||||
|
{...mergeProps<typeof children.props | typeof rest>(
|
||||||
|
children.props,
|
||||||
|
rest
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{tooltipState.isOpen && (
|
||||||
|
<OverlayContainer>
|
||||||
|
<Tooltip
|
||||||
|
state={tooltipState}
|
||||||
|
ref={overlayRef}
|
||||||
|
{...mergeProps(tooltipProps, overlayProps)}
|
||||||
|
>
|
||||||
|
{tooltip()}
|
||||||
|
</Tooltip>
|
||||||
|
</OverlayContainer>
|
||||||
|
)}
|
||||||
|
</FocusableProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -1,16 +1,26 @@
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { Item } from "@react-stately/collections";
|
import { Item } from "@react-stately/collections";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import { Button, LinkButton } from "./button";
|
import { Button, LinkButton } from "./button";
|
||||||
import { PopoverMenuTrigger } from "./popover/PopoverMenu";
|
import { PopoverMenuTrigger } from "./popover/PopoverMenu";
|
||||||
import { Menu } from "./Menu";
|
import { Menu } from "./Menu";
|
||||||
import { Tooltip, TooltipTrigger } from "./Tooltip";
|
import { TooltipTrigger } from "./Tooltip";
|
||||||
import { Avatar } from "./Avatar";
|
import { Avatar, Size } from "./Avatar";
|
||||||
import { ReactComponent as UserIcon } from "./icons/User.svg";
|
import { ReactComponent as UserIcon } from "./icons/User.svg";
|
||||||
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
|
import { ReactComponent as LoginIcon } from "./icons/Login.svg";
|
||||||
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
|
import { ReactComponent as LogoutIcon } from "./icons/Logout.svg";
|
||||||
import styles from "./UserMenu.module.css";
|
|
||||||
import { useLocation } from "react-router-dom";
|
|
||||||
import { Body } from "./typography/Typography";
|
import { Body } from "./typography/Typography";
|
||||||
|
import styles from "./UserMenu.module.css";
|
||||||
|
|
||||||
|
interface UserMenuProps {
|
||||||
|
preventNavigation: boolean;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isPasswordlessUser: boolean;
|
||||||
|
displayName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
onAction: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export function UserMenu({
|
export function UserMenu({
|
||||||
preventNavigation,
|
preventNavigation,
|
||||||
|
@ -19,7 +29,7 @@ export function UserMenu({
|
||||||
displayName,
|
displayName,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
onAction,
|
onAction,
|
||||||
}) {
|
}: UserMenuProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
|
@ -62,11 +72,11 @@ export function UserMenu({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverMenuTrigger placement="bottom right">
|
<PopoverMenuTrigger placement="bottom right">
|
||||||
<TooltipTrigger placement="bottom left">
|
<TooltipTrigger tooltip={() => "Profile"} placement="bottom left">
|
||||||
<Button variant="icon" className={styles.userButton}>
|
<Button variant="icon" className={styles.userButton}>
|
||||||
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
|
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
|
||||||
<Avatar
|
<Avatar
|
||||||
size="sm"
|
size={Size.SM}
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
fallback={displayName.slice(0, 1).toUpperCase()}
|
fallback={displayName.slice(0, 1).toUpperCase()}
|
||||||
|
@ -75,12 +85,11 @@ export function UserMenu({
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{() => "Profile"}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
<Menu {...props} label="User menu" onAction={onAction}>
|
<Menu {...props} label="User menu" onAction={onAction}>
|
||||||
{items.map(({ key, icon: Icon, label }) => (
|
{items.map(({ key, icon: Icon, label }) => (
|
||||||
<Item key={key} textValue={label} className={styles.menuItem}>
|
<Item key={key} textValue={label}>
|
||||||
<Icon width={24} height={24} className={styles.menuIcon} />
|
<Icon width={24} height={24} className={styles.menuIcon} />
|
||||||
<Body overflowEllipsis>{label}</Body>
|
<Body overflowEllipsis>{label}</Body>
|
||||||
</Item>
|
</Item>
|
|
@ -1,12 +1,17 @@
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import { useClient } from "./ClientContext";
|
import { useClient } from "./ClientContext";
|
||||||
import { useProfile } from "./profile/useProfile";
|
import { useProfile } from "./profile/useProfile";
|
||||||
import { useModalTriggerState } from "./Modal";
|
import { useModalTriggerState } from "./Modal";
|
||||||
import { ProfileModal } from "./profile/ProfileModal";
|
import { ProfileModal } from "./profile/ProfileModal";
|
||||||
import { UserMenu } from "./UserMenu";
|
import { UserMenu } from "./UserMenu";
|
||||||
|
|
||||||
export function UserMenuContainer({ preventNavigation }) {
|
interface Props {
|
||||||
|
preventNavigation?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 } =
|
||||||
|
@ -15,7 +20,7 @@ export function UserMenuContainer({ preventNavigation }) {
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
|
|
||||||
const onAction = useCallback(
|
const onAction = useCallback(
|
||||||
(value) => {
|
(value: string) => {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "user":
|
case "user":
|
||||||
modalState.open();
|
modalState.open();
|
|
@ -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("/");
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ 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 ArrowDownIcon } from "../icons/ArrowDown.svg";
|
import { ReactComponent as ArrowDownIcon } from "../icons/ArrowDown.svg";
|
||||||
import { TooltipTrigger } from "../Tooltip";
|
import { TooltipTrigger } from "../Tooltip";
|
||||||
import { ReactComponent as OverflowIcon } from "../icons/Overflow.svg";
|
import { VolumeIcon } from "./VolumeIcon";
|
||||||
|
|
||||||
export type ButtonVariant =
|
export type ButtonVariant =
|
||||||
| "default"
|
| "default"
|
||||||
|
@ -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,14 +137,16 @@ export function MicButton({
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
|
tooltip={() => (muted ? "Unmute microphone" : "Mute microphone")}
|
||||||
|
>
|
||||||
<Button variant="toolbar" {...rest} off={muted}>
|
<Button variant="toolbar" {...rest} off={muted}>
|
||||||
{muted ? <MuteMicIcon /> : <MicIcon />}
|
{muted ? <MuteMicIcon /> : <MicIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
{() => (muted ? "Unmute microphone" : "Mute microphone")}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -153,14 +156,16 @@ export function VideoButton({
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
|
tooltip={() => (muted ? "Turn on camera" : "Turn off camera")}
|
||||||
|
>
|
||||||
<Button variant="toolbar" {...rest} off={muted}>
|
<Button variant="toolbar" {...rest} off={muted}>
|
||||||
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
{muted ? <DisableVideoIcon /> : <VideoIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
{() => (muted ? "Turn on camera" : "Turn off camera")}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -172,14 +177,16 @@ 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 (
|
||||||
<TooltipTrigger>
|
<TooltipTrigger
|
||||||
|
tooltip={() => (enabled ? "Stop sharing screen" : "Share screen")}
|
||||||
|
>
|
||||||
<Button variant="toolbarSecondary" {...rest} on={enabled}>
|
<Button variant="toolbarSecondary" {...rest} on={enabled}>
|
||||||
<ScreenshareIcon />
|
<ScreenshareIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{() => (enabled ? "Stop sharing screen" : "Share screen")}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -189,10 +196,11 @@ export function HangupButton({
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger>
|
<TooltipTrigger tooltip={() => "Leave"}>
|
||||||
<Button
|
<Button
|
||||||
variant="toolbar"
|
variant="toolbar"
|
||||||
className={classNames(styles.hangupButton, className)}
|
className={classNames(styles.hangupButton, className)}
|
||||||
|
@ -200,7 +208,6 @@ export function HangupButton({
|
||||||
>
|
>
|
||||||
<HangupIcon />
|
<HangupIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{() => "Leave"}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -210,14 +217,14 @@ export function SettingsButton({
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger>
|
<TooltipTrigger tooltip={() => "Settings"}>
|
||||||
<Button variant="toolbar" {...rest}>
|
<Button variant="toolbar" {...rest}>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{() => "Settings"}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -227,25 +234,31 @@ export function InviteButton({
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
// TODO: add all props for <Button>
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger>
|
<TooltipTrigger tooltip={() => "Invite"}>
|
||||||
<Button variant="toolbar" {...rest}>
|
<Button variant="toolbar" {...rest}>
|
||||||
<AddUserIcon />
|
<AddUserIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{() => "Invite"}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OptionsButton(props: Omit<Props, "variant">) {
|
interface AudioButtonProps extends Omit<Props, "variant"> {
|
||||||
|
/**
|
||||||
|
* A number between 0 and 1
|
||||||
|
*/
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AudioButton({ volume, ...rest }: AudioButtonProps) {
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger>
|
<TooltipTrigger tooltip={() => "Local volume"}>
|
||||||
<Button variant="icon" {...props}>
|
<Button variant="icon" {...rest}>
|
||||||
<OverflowIcon />
|
<VolumeIcon volume={volume} />
|
||||||
</Button>
|
</Button>
|
||||||
{() => "Options"}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,10 @@ 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({
|
||||||
value,
|
value,
|
||||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React 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;
|
||||||
size: ButtonSize;
|
to: H.LocationDescriptor | ((location: H.Location) => H.LocationDescriptor);
|
||||||
children: JSX.Element;
|
size?: ButtonSize;
|
||||||
[index: string]: unknown;
|
variant?: ButtonVariant;
|
||||||
|
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}
|
||||||
|
|
35
src/button/VolumeIcon.tsx
Normal file
35
src/button/VolumeIcon.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { ReactComponent as AudioMuted } from "../icons/AudioMuted.svg";
|
||||||
|
import { ReactComponent as AudioLow } from "../icons/AudioLow.svg";
|
||||||
|
import { ReactComponent as Audio } from "../icons/Audio.svg";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/**
|
||||||
|
* Number between 0 and 1
|
||||||
|
*/
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function VolumeIcon({ volume }: Props) {
|
||||||
|
if (volume <= 0) return <AudioMuted />;
|
||||||
|
if (volume <= 0.5) return <AudioLow />;
|
||||||
|
return <Audio />;
|
||||||
|
}
|
40
src/form/Form.tsx
Normal file
40
src/form/Form.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import classNames from "classnames";
|
||||||
|
import React, { FormEventHandler, forwardRef } from "react";
|
||||||
|
|
||||||
|
import styles from "./Form.module.css";
|
||||||
|
|
||||||
|
interface FormProps {
|
||||||
|
className: string;
|
||||||
|
onSubmit: FormEventHandler<HTMLFormElement>;
|
||||||
|
children: JSX.Element[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Form = forwardRef<HTMLFormElement, FormProps>(
|
||||||
|
({ children, className, onSubmit }, ref) => {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
className={classNames(styles.form, className)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -16,14 +16,22 @@ limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { MatrixClient, RoomMember } from "matrix-js-sdk";
|
||||||
|
|
||||||
import { CopyButton } from "../button";
|
import { CopyButton } from "../button";
|
||||||
import { Facepile } from "../Facepile";
|
import { Facepile } from "../Facepile";
|
||||||
import { Avatar } from "../Avatar";
|
import { Avatar, Size } from "../Avatar";
|
||||||
import styles from "./CallList.module.css";
|
import styles from "./CallList.module.css";
|
||||||
import { getRoomUrl } from "../matrix-utils";
|
import { getRoomUrl } from "../matrix-utils";
|
||||||
import { Body, Caption } from "../typography/Typography";
|
import { Body, Caption } from "../typography/Typography";
|
||||||
|
import { GroupCallRoom } from "./useGroupCallRooms";
|
||||||
|
|
||||||
export function CallList({ rooms, client, disableFacepile }) {
|
interface CallListProps {
|
||||||
|
rooms: GroupCallRoom[];
|
||||||
|
client: MatrixClient;
|
||||||
|
disableFacepile?: boolean;
|
||||||
|
}
|
||||||
|
export function CallList({ rooms, client, disableFacepile }: CallListProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.callList}>
|
<div className={styles.callList}>
|
||||||
|
@ -48,7 +56,14 @@ export function CallList({ rooms, client, disableFacepile }) {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
interface CallTileProps {
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
roomId: string;
|
||||||
|
participants: RoomMember[];
|
||||||
|
client: MatrixClient;
|
||||||
|
disableFacepile?: boolean;
|
||||||
|
}
|
||||||
function CallTile({
|
function CallTile({
|
||||||
name,
|
name,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
|
@ -56,12 +71,12 @@ function CallTile({
|
||||||
participants,
|
participants,
|
||||||
client,
|
client,
|
||||||
disableFacepile,
|
disableFacepile,
|
||||||
}) {
|
}: CallTileProps) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.callTile}>
|
<div className={styles.callTile}>
|
||||||
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
|
<Link to={`/room/${roomId}`} className={styles.callTileLink}>
|
||||||
<Avatar
|
<Avatar
|
||||||
size="lg"
|
size={Size.LG}
|
||||||
bgKey={name}
|
bgKey={name}
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
fallback={name.slice(0, 1).toUpperCase()}
|
fallback={name.slice(0, 1).toUpperCase()}
|
|
@ -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,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||||
import { UnauthenticatedView } from "./UnauthenticatedView";
|
import { UnauthenticatedView } from "./UnauthenticatedView";
|
|
@ -15,18 +15,26 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { PressEvent } from "@react-types/shared";
|
||||||
|
|
||||||
import { Modal, ModalContent } from "../Modal";
|
import { Modal, ModalContent } from "../Modal";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { FieldRow } from "../input/Input";
|
import { FieldRow } from "../input/Input";
|
||||||
import styles from "./JoinExistingCallModal.module.css";
|
import styles from "./JoinExistingCallModal.module.css";
|
||||||
|
|
||||||
export function JoinExistingCallModal({ onJoin, ...rest }) {
|
interface Props {
|
||||||
|
onJoin: (e: PressEvent) => void;
|
||||||
|
onClose: (e: PressEvent) => void;
|
||||||
|
// TODO: add used parameters for <Modal>
|
||||||
|
[index: string]: unknown;
|
||||||
|
}
|
||||||
|
export function JoinExistingCallModal({ onJoin, onClose, ...rest }: Props) {
|
||||||
return (
|
return (
|
||||||
<Modal title="Join existing call?" isDismissable {...rest}>
|
<Modal title="Join existing call?" isDismissable {...rest}>
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<p>This call already exists, would you like to join?</p>
|
<p>This call already exists, would you like to join?</p>
|
||||||
<FieldRow rightAlign className={styles.buttons}>
|
<FieldRow rightAlign className={styles.buttons}>
|
||||||
<Button onPress={rest.onClose}>No</Button>
|
<Button onPress={onClose}>No</Button>
|
||||||
<Button onPress={onJoin}>Yes, join call</Button>
|
<Button onPress={onJoin}>Yes, join call</Button>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
</ModalContent>
|
</ModalContent>
|
|
@ -14,7 +14,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useCallback } from "react";
|
import React, {
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
FormEvent,
|
||||||
|
FormEventHandler,
|
||||||
|
} from "react";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk";
|
||||||
|
|
||||||
import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils";
|
import { createRoom, roomAliasLocalpartFromRoomName } from "../matrix-utils";
|
||||||
import { useGroupCallRooms } from "./useGroupCallRooms";
|
import { useGroupCallRooms } from "./useGroupCallRooms";
|
||||||
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
import { Header, HeaderLogo, LeftNav, RightNav } from "../Header";
|
||||||
|
@ -26,28 +34,35 @@ import { CallList } from "./CallList";
|
||||||
import { UserMenuContainer } from "../UserMenuContainer";
|
import { UserMenuContainer } from "../UserMenuContainer";
|
||||||
import { useModalTriggerState } from "../Modal";
|
import { useModalTriggerState } from "../Modal";
|
||||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
import { Title } from "../typography/Typography";
|
import { Title } from "../typography/Typography";
|
||||||
import { Form } from "../form/Form";
|
import { Form } from "../form/Form";
|
||||||
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
|
import { CallType, CallTypeDropdown } from "./CallTypeDropdown";
|
||||||
|
|
||||||
export function RegisteredView({ client }) {
|
interface Props {
|
||||||
|
client: MatrixClient;
|
||||||
|
isPasswordlessUser: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegisteredView({ client, isPasswordlessUser }: Props) {
|
||||||
const [callType, setCallType] = useState(CallType.Video);
|
const [callType, setCallType] = useState(CallType.Video);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState<Error>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const onSubmit = useCallback(
|
const { modalState, modalProps } = useModalTriggerState();
|
||||||
(e) => {
|
|
||||||
|
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
||||||
|
(e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = new FormData(e.target);
|
const data = new FormData(e.target as HTMLFormElement);
|
||||||
const roomName = data.get("callName");
|
const roomNameData = data.get("callName");
|
||||||
const ptt = callType === CallType.Radio;
|
const roomName = typeof roomNameData === "string" ? roomNameData : "";
|
||||||
|
// const ptt = callType === CallType.Radio;
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const [roomIdOrAlias] = await createRoom(client, roomName, ptt);
|
const [roomIdOrAlias] = await createRoom(client, roomName);
|
||||||
|
|
||||||
if (roomIdOrAlias) {
|
if (roomIdOrAlias) {
|
||||||
history.push(`/room/${roomIdOrAlias}`);
|
history.push(`/room/${roomIdOrAlias}`);
|
||||||
|
@ -64,17 +79,15 @@ export function RegisteredView({ client }) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setError(error);
|
setError(error);
|
||||||
reset();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[client, callType]
|
[client, history, modalState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const recentRooms = useGroupCallRooms(client);
|
const recentRooms = useGroupCallRooms(client);
|
||||||
|
|
||||||
const { modalState, modalProps } = useModalTriggerState();
|
const [existingRoomId, setExistingRoomId] = useState<string>();
|
||||||
const [existingRoomId, setExistingRoomId] = useState();
|
|
||||||
const onJoinExistingRoom = useCallback(() => {
|
const onJoinExistingRoom = useCallback(() => {
|
||||||
history.push(`/${existingRoomId}`);
|
history.push(`/${existingRoomId}`);
|
||||||
}, [history, existingRoomId]);
|
}, [history, existingRoomId]);
|
|
@ -14,11 +14,21 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { GroupCall, MatrixClient, Room, RoomMember } from "matrix-js-sdk";
|
||||||
|
import { GroupCallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/groupCallEventHandler";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
const tsCache = {};
|
export interface GroupCallRoom {
|
||||||
|
roomId: string;
|
||||||
|
roomName: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
room: Room;
|
||||||
|
groupCall: GroupCall;
|
||||||
|
participants: RoomMember[];
|
||||||
|
}
|
||||||
|
const tsCache: { [index: string]: number } = {};
|
||||||
|
|
||||||
function getLastTs(client, r) {
|
function getLastTs(client: MatrixClient, r: Room) {
|
||||||
if (tsCache[r.roomId]) {
|
if (tsCache[r.roomId]) {
|
||||||
return tsCache[r.roomId];
|
return tsCache[r.roomId];
|
||||||
}
|
}
|
||||||
|
@ -59,13 +69,13 @@ function getLastTs(client, r) {
|
||||||
return ts;
|
return ts;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sortRooms(client, rooms) {
|
function sortRooms(client: MatrixClient, rooms: Room[]): Room[] {
|
||||||
return rooms.sort((a, b) => {
|
return rooms.sort((a, b) => {
|
||||||
return getLastTs(client, b) - getLastTs(client, a);
|
return getLastTs(client, b) - getLastTs(client, a);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGroupCallRooms(client) {
|
export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
|
||||||
const [rooms, setRooms] = useState([]);
|
const [rooms, setRooms] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -90,12 +100,15 @@ export function useGroupCallRooms(client) {
|
||||||
|
|
||||||
updateRooms();
|
updateRooms();
|
||||||
|
|
||||||
client.on("GroupCall.incoming", updateRooms);
|
client.on(GroupCallEventHandlerEvent.Incoming, updateRooms);
|
||||||
client.on("GroupCall.participants", updateRooms);
|
client.on(GroupCallEventHandlerEvent.Participants, updateRooms);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
client.removeListener("GroupCall.incoming", updateRooms);
|
client.removeListener(GroupCallEventHandlerEvent.Incoming, updateRooms);
|
||||||
client.removeListener("GroupCall.participants", updateRooms);
|
client.removeListener(
|
||||||
|
GroupCallEventHandlerEvent.Participants,
|
||||||
|
updateRooms
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M7.97991 1.48403L4 4.80062L1 4.80062C0.447715 4.80062 0 5.24834 0 5.80062V10.2006C0 10.7529 0.447714 11.2006 0.999999 11.2006L4 11.2006L7.97991 14.5172C8.30557 14.7886 8.8 14.557 8.8 14.1331V1.86814C8.8 1.44422 8.30557 1.21265 7.97991 1.48403Z" fill="white"/>
|
<path d="M11.9699 2.22605L6 7.20093L1.5 7.20093C0.671573 7.20093 0 7.8725 0 8.70093V15.3009C0 16.1294 0.671571 16.8009 1.5 16.8009L6 16.8009L11.9699 21.7758C12.4584 22.1829 13.2 21.8355 13.2 21.1996V2.80221C13.2 2.16634 12.4584 1.81897 11.9699 2.22605Z" fill="white"/>
|
||||||
<path d="M14.1258 2.79107C13.8998 2.50044 13.4809 2.44808 13.1903 2.67413C12.9 2.89992 12.8475 3.3181 13.0726 3.6087L13.0731 3.60935L13.0738 3.61021L13.0829 3.62231C13.0917 3.63418 13.1059 3.65355 13.1248 3.68011C13.1625 3.73326 13.2187 3.81496 13.2872 3.92256C13.4243 4.13812 13.6097 4.45554 13.7955 4.85371C14.169 5.65407 14.5329 6.75597 14.5329 8.00036C14.5329 9.24475 14.169 10.3466 13.7955 11.147C13.6097 11.5452 13.4243 11.8626 13.2872 12.0782C13.2187 12.1858 13.1625 12.2675 13.1248 12.3206C13.1059 12.3472 13.0917 12.3665 13.0829 12.3784L13.0738 12.3905L13.0731 12.3914L13.0725 12.3921C12.8475 12.6827 12.9 13.1008 13.1903 13.3266C13.4809 13.5526 13.8998 13.5003 14.1258 13.2097L13.629 12.8232C14.1258 13.2096 14.1258 13.2097 14.1258 13.2097L14.1272 13.2079L14.1291 13.2055L14.1346 13.1982L14.1523 13.1748C14.1669 13.1552 14.187 13.1277 14.2119 13.0926C14.2617 13.0225 14.3305 12.9221 14.4121 12.794C14.5749 12.5381 14.7895 12.1698 15.0037 11.7109C15.4302 10.7969 15.8663 9.49883 15.8663 8.00036C15.8663 6.50189 15.4302 5.20379 15.0037 4.28987C14.7895 3.83089 14.5749 3.4626 14.4121 3.20673C14.3305 3.07862 14.2617 2.97818 14.2119 2.90811C14.187 2.87306 14.1669 2.84556 14.1523 2.82596L14.1346 2.80249L14.1291 2.79525L14.1272 2.79278L14.1264 2.79183C14.1264 2.79183 14.1258 2.79107 13.5996 3.20036L14.1258 2.79107Z" fill="white"/>
|
<path d="M21.1888 4.1866C20.8497 3.75065 20.2214 3.67212 19.7855 4.01119C19.35 4.34988 19.2712 4.97715 19.6089 5.41304L19.6097 5.41402L19.6107 5.41531L19.6243 5.43347C19.6376 5.45126 19.6589 5.48033 19.6872 5.52017C19.7438 5.59988 19.828 5.72244 19.9308 5.88385C20.1365 6.20718 20.4145 6.68332 20.6932 7.28057C21.2535 8.48111 21.7994 10.134 21.7994 12.0005C21.7994 13.8671 21.2535 15.52 20.6932 16.7205C20.4145 17.3178 20.1365 17.7939 19.9308 18.1172C19.828 18.2786 19.7438 18.4012 19.6872 18.4809C19.6589 18.5208 19.6376 18.5498 19.6243 18.5676L19.6107 18.5858L19.6097 18.5871L19.6088 18.5882C19.2712 19.0241 19.3501 19.6512 19.7855 19.9899C20.2214 20.329 20.8497 20.2504 21.1888 19.8145L20.4435 19.2348C21.1888 19.8145 21.1888 19.8145 21.1888 19.8145L21.1908 19.8119L21.1936 19.8082L21.2019 19.7974L21.2284 19.7621C21.2503 19.7327 21.2805 19.6915 21.3179 19.6389C21.3925 19.5338 21.4958 19.3832 21.6181 19.191C21.8623 18.8072 22.1843 18.2547 22.5056 17.5663C23.1453 16.1954 23.7994 14.2482 23.7994 12.0005C23.7994 9.75284 23.1453 7.80569 22.5056 6.4348C22.1843 5.74634 21.8623 5.1939 21.6181 4.81009C21.4958 4.61793 21.3925 4.46727 21.3179 4.36217C21.2805 4.30959 21.2503 4.26835 21.2284 4.23893L21.2019 4.20373L21.1936 4.19288L21.1908 4.18917L21.1897 4.18774C21.1897 4.18774 21.1888 4.1866 20.3994 4.80054L21.1888 4.1866Z" fill="white"/>
|
||||||
<path d="M11.7264 5.19121C11.5004 4.90058 11.0815 4.84823 10.7909 5.07427C10.501 5.29973 10.4482 5.71698 10.6722 6.00752L10.6745 6.01057C10.6775 6.01457 10.6831 6.02223 10.691 6.03338C10.7069 6.05572 10.7318 6.09189 10.7628 6.14057C10.8249 6.23827 10.9103 6.38426 10.9961 6.56815C11.1696 6.93993 11.3335 7.44183 11.3335 8.00051C11.3335 8.55918 11.1696 9.06108 10.9961 9.43287C10.9103 9.61675 10.8249 9.76275 10.7628 9.86045C10.7318 9.90912 10.7069 9.94529 10.691 9.96763C10.6831 9.97879 10.6775 9.98645 10.6745 9.99044L10.6722 9.9935C10.4482 10.284 10.501 10.7013 10.7909 10.9267C11.0815 11.1528 11.5004 11.1004 11.7264 10.8098L11.2002 10.4005C11.7264 10.8098 11.7264 10.8098 11.7264 10.8098L11.7276 10.8083L11.7291 10.8064L11.7329 10.8014L11.7439 10.7868C11.7526 10.7751 11.7642 10.7593 11.7781 10.7396C11.806 10.7004 11.8436 10.6455 11.8876 10.5763C11.9755 10.4383 12.0901 10.2414 12.2043 9.99672C12.4308 9.51136 12.6669 8.81326 12.6669 8.00051C12.6669 7.18775 12.4308 6.48965 12.2043 6.0043C12.0901 5.75961 11.9755 5.56275 11.8876 5.42473C11.8436 5.35555 11.806 5.30065 11.7781 5.26138C11.7642 5.24173 11.7526 5.22596 11.7439 5.21422L11.7329 5.19964L11.7291 5.19465L11.7276 5.19274L11.727 5.19193C11.727 5.19193 11.7264 5.19121 11.2002 5.60051L11.7264 5.19121Z" fill="white"/>
|
<path d="M17.5896 7.78682C17.2506 7.35087 16.6223 7.27234 16.1864 7.61141C15.7515 7.94959 15.6723 8.57548 16.0083 9.01128L16.0117 9.01586C16.0162 9.02185 16.0246 9.03334 16.0365 9.05007C16.0603 9.08359 16.0977 9.13784 16.1441 9.21085C16.2374 9.3574 16.3654 9.57639 16.4941 9.85222C16.7544 10.4099 17.0003 11.1627 17.0003 12.0008C17.0003 12.8388 16.7544 13.5916 16.4941 14.1493C16.3654 14.4251 16.2374 14.6441 16.1441 14.7907C16.0977 14.8637 16.0603 14.9179 16.0365 14.9514C16.0246 14.9682 16.0162 14.9797 16.0117 14.9857L16.0083 14.9903C15.6723 15.4261 15.7515 16.0519 16.1864 16.3901C16.6223 16.7292 17.2506 16.6506 17.5896 16.2147L16.8003 15.6008C17.5896 16.2147 17.5896 16.2147 17.5896 16.2147L17.5914 16.2124L17.5936 16.2095L17.5994 16.2021L17.6158 16.1802C17.6289 16.1626 17.6463 16.1389 17.6672 16.1094C17.709 16.0505 17.7654 15.9682 17.8315 15.8644C17.9632 15.6574 18.1352 15.3621 18.3065 14.9951C18.6462 14.267 19.0003 13.2199 19.0003 12.0008C19.0003 10.7816 18.6462 9.73448 18.3065 9.00645C18.1352 8.63942 17.9632 8.34412 17.8315 8.1371C17.7654 8.03333 17.709 7.95097 17.6672 7.89207C17.6463 7.8626 17.6289 7.83893 17.6158 7.82132L17.5994 7.79946L17.5936 7.79198L17.5914 7.78911L17.5905 7.78789C17.5905 7.78789 17.5896 7.78682 16.8003 8.40076L17.5896 7.78682Z" fill="white"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
4
src/icons/AudioLow.svg
Normal file
4
src/icons/AudioLow.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.9699 2.22605L6 7.20093L1.5 7.20093C0.671573 7.20093 0 7.8725 0 8.70093V15.3009C0 16.1294 0.671571 16.8009 1.5 16.8009L6 16.8009L11.9699 21.7758C12.4584 22.1829 13.2 21.8355 13.2 21.1996V2.80221C13.2 2.16634 12.4584 1.81897 11.9699 2.22605Z" fill="white"/>
|
||||||
|
<path d="M17.5896 7.78682C17.2506 7.35087 16.6223 7.27234 16.1864 7.61141C15.7515 7.94959 15.6723 8.57548 16.0083 9.01128L16.0117 9.01586C16.0162 9.02185 16.0246 9.03334 16.0365 9.05007C16.0603 9.08359 16.0977 9.13784 16.1441 9.21085C16.2374 9.3574 16.3654 9.57639 16.4941 9.85222C16.7544 10.4099 17.0003 11.1627 17.0003 12.0008C17.0003 12.8388 16.7544 13.5916 16.4941 14.1493C16.3654 14.4251 16.2374 14.6441 16.1441 14.7907C16.0977 14.8637 16.0603 14.9179 16.0365 14.9514C16.0246 14.9682 16.0162 14.9797 16.0117 14.9857L16.0083 14.9903C15.6723 15.4261 15.7515 16.0519 16.1864 16.3901C16.6223 16.7292 17.2506 16.6506 17.5896 16.2147L16.8003 15.6008C17.5896 16.2147 17.5896 16.2147 17.5896 16.2147L17.5914 16.2124L17.5936 16.2095L17.5994 16.2021L17.6158 16.1802C17.6289 16.1626 17.6463 16.1389 17.6672 16.1094C17.709 16.0505 17.7654 15.9682 17.8315 15.8644C17.9632 15.6574 18.1352 15.3621 18.3065 14.9951C18.6462 14.267 19.0003 13.2199 19.0003 12.0008C19.0003 10.7816 18.6462 9.73448 18.3065 9.00645C18.1352 8.63942 17.9632 8.34412 17.8315 8.1371C17.7654 8.03333 17.709 7.95097 17.6672 7.89207C17.6463 7.8626 17.6289 7.83893 17.6158 7.82132L17.5994 7.79946L17.5936 7.79198L17.5914 7.78911L17.5905 7.78789C17.5905 7.78789 17.5896 7.78682 16.8003 8.40076L17.5896 7.78682Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
3
src/icons/AudioMuted.svg
Normal file
3
src/icons/AudioMuted.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.63174 0.583224C2.15798 0.109466 1.38987 0.109466 0.91611 0.583224C0.442351 1.05698 0.442351 1.8251 0.91611 2.29885L5.3958 6.77855H5.37083L15.3629 16.7706V16.7456L20.7144 22.0972C21.1882 22.5709 21.9563 22.5709 22.4301 22.0972C22.9038 21.6234 22.9038 20.8553 22.4301 20.3816L2.63174 0.583224ZM15.3629 3.23319V9.88521L10.2275 4.74987L13.2404 2.2391C14.0833 1.53675 15.3629 2.13608 15.3629 3.23319ZM4.07191 16.8718H7.7929V16.872L13.2404 21.4116C14.0833 22.114 15.3629 21.5146 15.3629 20.4175V20.2018L2.4839 7.32287C1.87536 7.79641 1.48389 8.53577 1.48389 9.36657V14.2838C1.48389 15.7131 2.64258 16.8718 4.07191 16.8718Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 788 B |
|
@ -38,6 +38,7 @@ limitations under the License.
|
||||||
--quinary-content: #394049;
|
--quinary-content: #394049;
|
||||||
--system: #21262c;
|
--system: #21262c;
|
||||||
--background: #15191e;
|
--background: #15191e;
|
||||||
|
--background-85: rgba(23, 25, 28, 0.85);
|
||||||
--bgColor3: #444; /* This isn't found anywhere in the designs or Compound */
|
--bgColor3: #444; /* This isn't found anywhere in the designs or Compound */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,10 @@ if (import.meta.env.VITE_CUSTOM_THEME) {
|
||||||
"--background",
|
"--background",
|
||||||
import.meta.env.VITE_THEME_BACKGROUND as string
|
import.meta.env.VITE_THEME_BACKGROUND as string
|
||||||
);
|
);
|
||||||
|
style.setProperty(
|
||||||
|
"--background-85",
|
||||||
|
import.meta.env.VITE_THEME_BACKGROUND_85 as string
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
|
|
|
@ -14,14 +14,22 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { forwardRef, useRef } from "react";
|
import React, { forwardRef, HTMLAttributes } from "react";
|
||||||
import { DismissButton, useOverlay } from "@react-aria/overlays";
|
import { DismissButton, useOverlay } from "@react-aria/overlays";
|
||||||
import { FocusScope } from "@react-aria/focus";
|
import { FocusScope } from "@react-aria/focus";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styles from "./Popover.module.css";
|
|
||||||
import { useObjectRef } from "@react-aria/utils";
|
import { useObjectRef } from "@react-aria/utils";
|
||||||
|
|
||||||
export const Popover = forwardRef(
|
import styles from "./Popover.module.css";
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
className?: string;
|
||||||
|
children?: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Popover = forwardRef<HTMLDivElement, Props>(
|
||||||
({ isOpen = true, onClose, className, children, ...rest }, ref) => {
|
({ isOpen = true, onClose, className, children, ...rest }, ref) => {
|
||||||
const popoverRef = useObjectRef(ref);
|
const popoverRef = useObjectRef(ref);
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2022 Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { forwardRef, useRef } from "react";
|
|
||||||
import styles from "./PopoverMenu.module.css";
|
|
||||||
import { useMenuTriggerState } from "@react-stately/menu";
|
|
||||||
import { useMenuTrigger } from "@react-aria/menu";
|
|
||||||
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
|
||||||
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { Popover } from "./Popover";
|
|
||||||
|
|
||||||
export const PopoverMenuTrigger = forwardRef(
|
|
||||||
({ children, placement, className, disableOnState, ...rest }, ref) => {
|
|
||||||
const popoverMenuState = useMenuTriggerState(rest);
|
|
||||||
const buttonRef = useObjectRef(ref);
|
|
||||||
const { menuTriggerProps, menuProps } = useMenuTrigger(
|
|
||||||
{},
|
|
||||||
popoverMenuState,
|
|
||||||
buttonRef
|
|
||||||
);
|
|
||||||
|
|
||||||
const popoverRef = useRef();
|
|
||||||
|
|
||||||
const { overlayProps } = useOverlayPosition({
|
|
||||||
targetRef: buttonRef,
|
|
||||||
overlayRef: popoverRef,
|
|
||||||
placement: placement || "top",
|
|
||||||
offset: 5,
|
|
||||||
isOpen: popoverMenuState.isOpen,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!Array.isArray(children) ||
|
|
||||||
children.length > 2 ||
|
|
||||||
typeof children[1] !== "function"
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"PopoverMenu must have two props. The first being a button and the second being a render prop."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [popoverTrigger, popoverMenu] = children;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(styles.popoverMenuTrigger, className)}>
|
|
||||||
<popoverTrigger.type
|
|
||||||
{...mergeProps(popoverTrigger.props, menuTriggerProps)}
|
|
||||||
on={!disableOnState && popoverMenuState.isOpen}
|
|
||||||
ref={buttonRef}
|
|
||||||
/>
|
|
||||||
{popoverMenuState.isOpen && (
|
|
||||||
<OverlayContainer>
|
|
||||||
<Popover
|
|
||||||
{...overlayProps}
|
|
||||||
isOpen={popoverMenuState.isOpen}
|
|
||||||
onClose={popoverMenuState.close}
|
|
||||||
ref={popoverRef}
|
|
||||||
>
|
|
||||||
{popoverMenu({
|
|
||||||
...menuProps,
|
|
||||||
autoFocus: popoverMenuState.focusStrategy,
|
|
||||||
onClose: popoverMenuState.close,
|
|
||||||
})}
|
|
||||||
</Popover>
|
|
||||||
</OverlayContainer>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
96
src/popover/PopoverMenu.tsx
Normal file
96
src/popover/PopoverMenu.tsx
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { forwardRef, useRef } from "react";
|
||||||
|
import { useMenuTriggerState } from "@react-stately/menu";
|
||||||
|
import { useMenuTrigger } from "@react-aria/menu";
|
||||||
|
import { OverlayContainer, useOverlayPosition } from "@react-aria/overlays";
|
||||||
|
import { mergeProps, useObjectRef } from "@react-aria/utils";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { MenuTriggerProps } from "@react-types/menu";
|
||||||
|
import { Placement } from "@react-types/overlays";
|
||||||
|
|
||||||
|
import styles from "./PopoverMenu.module.css";
|
||||||
|
import { Popover } from "./Popover";
|
||||||
|
|
||||||
|
interface PopoverMenuTriggerProps extends MenuTriggerProps {
|
||||||
|
children: JSX.Element;
|
||||||
|
placement: Placement;
|
||||||
|
className: string;
|
||||||
|
disableOnState: boolean;
|
||||||
|
[index: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PopoverMenuTrigger = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
PopoverMenuTriggerProps
|
||||||
|
>(({ children, placement, className, disableOnState, ...rest }, ref) => {
|
||||||
|
const popoverMenuState = useMenuTriggerState(rest);
|
||||||
|
const buttonRef = useObjectRef(ref);
|
||||||
|
const { menuTriggerProps, menuProps } = useMenuTrigger(
|
||||||
|
{},
|
||||||
|
popoverMenuState,
|
||||||
|
buttonRef
|
||||||
|
);
|
||||||
|
|
||||||
|
const popoverRef = useRef();
|
||||||
|
|
||||||
|
const { overlayProps } = useOverlayPosition({
|
||||||
|
targetRef: buttonRef,
|
||||||
|
overlayRef: popoverRef,
|
||||||
|
placement: placement || "top",
|
||||||
|
offset: 5,
|
||||||
|
isOpen: popoverMenuState.isOpen,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Array.isArray(children) ||
|
||||||
|
children.length > 2 ||
|
||||||
|
typeof children[1] !== "function"
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"PopoverMenu must have two props. The first being a button and the second being a render prop."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [popoverTrigger, popoverMenu] = children;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.popoverMenuTrigger, className)}>
|
||||||
|
<popoverTrigger.type
|
||||||
|
{...mergeProps(popoverTrigger.props, menuTriggerProps)}
|
||||||
|
on={!disableOnState && popoverMenuState.isOpen}
|
||||||
|
ref={buttonRef}
|
||||||
|
/>
|
||||||
|
{popoverMenuState.isOpen && (
|
||||||
|
<OverlayContainer>
|
||||||
|
<Popover
|
||||||
|
{...overlayProps}
|
||||||
|
isOpen={popoverMenuState.isOpen}
|
||||||
|
onClose={popoverMenuState.close}
|
||||||
|
ref={popoverRef}
|
||||||
|
>
|
||||||
|
{popoverMenu({
|
||||||
|
...menuProps,
|
||||||
|
autoFocus: popoverMenuState.focusStrategy,
|
||||||
|
onClose: popoverMenuState.close,
|
||||||
|
})}
|
||||||
|
</Popover>
|
||||||
|
</OverlayContainer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
|
@ -26,7 +26,7 @@ import styles from "./ProfileModal.module.css";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
onClose: () => {};
|
onClose: () => void;
|
||||||
[rest: string]: unknown;
|
[rest: string]: unknown;
|
||||||
}
|
}
|
||||||
export function ProfileModal({ client, ...rest }: Props) {
|
export function ProfileModal({ client, ...rest }: Props) {
|
||||||
|
|
|
@ -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,19 +24,22 @@ 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>
|
<TooltipTrigger tooltip={() => "Layout Type"}>
|
||||||
<Button variant="icon">
|
<Button variant="icon">
|
||||||
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
||||||
</Button>
|
</Button>
|
||||||
{() => "Layout Type"}
|
|
||||||
</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,18 @@ export function SequenceDiagramViewer({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reducer(state, action) {
|
function reducer(
|
||||||
|
state: InspectorContextState,
|
||||||
|
action: {
|
||||||
|
type?: CallEvent | ClientEvent | RoomStateEvent;
|
||||||
|
event?: MatrixEvent;
|
||||||
|
rawEvent?: Record<string, unknown>;
|
||||||
|
callStateEvent?: MatrixEvent;
|
||||||
|
memberStateEvents?: MatrixEvent[];
|
||||||
|
}
|
||||||
|
) {
|
||||||
switch (action.type) {
|
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 +292,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 +317,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.rawEvent;
|
||||||
const eventsByUserId = { ...state.eventsByUserId };
|
const eventsByUserId = { ...state.eventsByUserId };
|
||||||
const fromId = state.localUserId;
|
const fromId = state.localUserId;
|
||||||
const toId = event.userId;
|
const toId = event.userId as string;
|
||||||
|
|
||||||
const remoteUserIds = eventsByUserId[toId]
|
const remoteUserIds = eventsByUserId[toId]
|
||||||
? state.remoteUserIds
|
? state.remoteUserIds
|
||||||
|
@ -287,8 +332,8 @@ function reducer(state, action) {
|
||||||
{
|
{
|
||||||
from: fromId,
|
from: fromId,
|
||||||
to: toId,
|
to: toId,
|
||||||
type: event.eventType,
|
type: event.eventType as string,
|
||||||
content: event.content,
|
content: event.content as CallEventContent,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
ignored: false,
|
ignored: false,
|
||||||
},
|
},
|
||||||
|
@ -301,7 +346,11 @@ function reducer(state, action) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGroupCallState(client, groupCall, pollCallStats) {
|
function useGroupCallState(
|
||||||
|
client: MatrixClient,
|
||||||
|
groupCall: GroupCall,
|
||||||
|
showPollCallStats: boolean
|
||||||
|
) {
|
||||||
const [state, dispatch] = useReducer(reducer, {
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
localUserId: client.getUserId(),
|
localUserId: client.getUserId(),
|
||||||
localSessionId: client.getSessionId(),
|
localSessionId: client.getSessionId(),
|
||||||
|
@ -312,7 +361,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 +372,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: Record<string, unknown>) {
|
||||||
dispatch({ type: "send_voip_event", event });
|
dispatch({ type: CallEvent.SendVoipEvent, rawEvent: 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 +435,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;
|
||||||
|
roomIdOrAlias: string;
|
||||||
|
viaServers: string[];
|
||||||
|
children: (groupCall: GroupCall) => ReactNode;
|
||||||
|
createPtt: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export function GroupCallLoader({
|
export function GroupCallLoader({
|
||||||
client,
|
client,
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
viaServers,
|
viaServers,
|
||||||
createPtt,
|
|
||||||
children,
|
children,
|
||||||
}) {
|
createPtt,
|
||||||
|
}: Props): JSX.Element {
|
||||||
const { loading, error, groupCall } = useLoadGroupCall(
|
const { loading, error, groupCall } = useLoadGroupCall(
|
||||||
client,
|
client,
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
|
@ -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;
|
||||||
|
roomIdOrAlias: string;
|
||||||
|
groupCall: GroupCall;
|
||||||
|
}
|
||||||
export function GroupCallView({
|
export function GroupCallView({
|
||||||
client,
|
client,
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
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;
|
||||||
|
roomIdOrAlias: 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,
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
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
|
||||||
roomIdOrAlias={roomIdOrAlias}
|
roomIdOrAlias={roomIdOrAlias}
|
||||||
client={client}
|
|
||||||
groupCall={groupCall}
|
groupCall={groupCall}
|
||||||
showInvite={true}
|
showInvite={true}
|
||||||
feedbackModalState={feedbackModalState}
|
feedbackModalState={feedbackModalState}
|
|
@ -14,14 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { FC } from "react";
|
||||||
import { Modal, ModalContent } from "../Modal";
|
|
||||||
|
import { Modal, ModalContent, ModalProps } 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({ roomIdOrAlias, ...rest }) {
|
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||||
return (
|
roomIdOrAlias: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => (
|
||||||
<Modal
|
<Modal
|
||||||
title="Invite People"
|
title="Invite People"
|
||||||
isDismissable
|
isDismissable
|
||||||
|
@ -37,4 +41,3 @@ export function InviteModal({ roomIdOrAlias, ...rest }) {
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
|
|
@ -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;
|
||||||
|
roomIdOrAlias: string;
|
||||||
|
isEmbedded: boolean;
|
||||||
|
}
|
||||||
export function LobbyView({
|
export function LobbyView({
|
||||||
client,
|
client,
|
||||||
groupCall,
|
groupCall,
|
||||||
|
@ -43,7 +63,7 @@ export function LobbyView({
|
||||||
toggleMicrophoneMuted,
|
toggleMicrophoneMuted,
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
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 {
|
||||||
|
roomIdOrAlias: string;
|
||||||
|
inCall: boolean;
|
||||||
|
groupCall: GroupCall;
|
||||||
|
showInvite: boolean;
|
||||||
|
feedbackModalState: OverlayTriggerState;
|
||||||
|
feedbackModalProps: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
export function OverflowMenu({
|
export function OverflowMenu({
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
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,19 +86,20 @@ export function OverflowMenu({
|
||||||
feedbackModalState.open();
|
feedbackModalState.open();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
[feedbackModalState, inviteModalState, settingsModalState]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PopoverMenuTrigger disableOnState>
|
<PopoverMenuTrigger disableOnState>
|
||||||
<TooltipTrigger placement="top">
|
<TooltipTrigger tooltip={() => "More"} placement="top">
|
||||||
<Button variant="toolbar">
|
<Button variant="toolbar">
|
||||||
<OverflowIcon />
|
<OverflowIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{() => "More"}
|
|
||||||
</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 />
|
|
@ -38,6 +38,7 @@ import { usePTTSounds } from "../sound/usePttSounds";
|
||||||
import { PTTClips } from "../sound/PTTClips";
|
import { PTTClips } from "../sound/PTTClips";
|
||||||
import { GroupCallInspector } from "./GroupCallInspector";
|
import { GroupCallInspector } from "./GroupCallInspector";
|
||||||
import { OverflowMenu } from "./OverflowMenu";
|
import { OverflowMenu } from "./OverflowMenu";
|
||||||
|
import { Size } from "../Avatar";
|
||||||
|
|
||||||
function getPromptText(
|
function getPromptText(
|
||||||
networkWaiting: boolean,
|
networkWaiting: boolean,
|
||||||
|
@ -112,7 +113,7 @@ export const PTTCallView: React.FC<Props> = ({
|
||||||
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
|
const { modalState: feedbackModalState, modalProps: feedbackModalProps } =
|
||||||
useModalTriggerState();
|
useModalTriggerState();
|
||||||
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
|
const [containerRef, bounds] = useMeasure({ polyfill: ResizeObserver });
|
||||||
const facepileSize = bounds.width < 800 ? "sm" : "md";
|
const facepileSize = bounds.width < 800 ? Size.SM : Size.MD;
|
||||||
const showControls = bounds.height > 500;
|
const showControls = bounds.height > 500;
|
||||||
const pttButtonSize = 232;
|
const pttButtonSize = 232;
|
||||||
|
|
||||||
|
@ -205,7 +206,6 @@ export const PTTCallView: React.FC<Props> = ({
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
inCall
|
inCall
|
||||||
roomIdOrAlias={roomIdOrAlias}
|
roomIdOrAlias={roomIdOrAlias}
|
||||||
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 />;
|
|
@ -14,25 +14,32 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { FC, useEffect } from "react";
|
||||||
import { Modal, ModalContent } from "../Modal";
|
|
||||||
|
import { Modal, ModalContent, ModalProps } 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({
|
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||||
|
rageshakeRequestId: string;
|
||||||
|
roomIdOrAlias: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RageshakeRequestModal: FC<Props> = ({
|
||||||
rageshakeRequestId,
|
rageshakeRequestId,
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
...rest
|
...rest
|
||||||
}) {
|
}) => {
|
||||||
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}>
|
||||||
|
@ -47,7 +54,7 @@ export function RageshakeRequestModal({
|
||||||
submitRageshake({
|
submitRageshake({
|
||||||
sendLogs: true,
|
sendLogs: true,
|
||||||
rageshakeRequestId,
|
rageshakeRequestId,
|
||||||
roomIdOrAlias, // Possibly not a room ID, but oh well
|
roomId: roomIdOrAlias, // Possibly not a room ID, but oh well
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
|
@ -63,4 +70,4 @@ export function RageshakeRequestModal({
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
};
|
|
@ -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);
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useEffect, useState } from "react";
|
import React, { FC, useEffect, useState } from "react";
|
||||||
|
|
||||||
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";
|
||||||
|
@ -24,7 +25,7 @@ import { useRoomParams } from "./useRoomParams";
|
||||||
import { MediaHandlerProvider } from "../settings/useMediaHandler";
|
import { MediaHandlerProvider } from "../settings/useMediaHandler";
|
||||||
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
|
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
|
||||||
|
|
||||||
export function RoomPage() {
|
export const RoomPage: FC = () => {
|
||||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||||
useClient();
|
useClient();
|
||||||
|
|
||||||
|
@ -84,4 +85,4 @@ export function RoomPage() {
|
||||||
</GroupCallLoader>
|
</GroupCallLoader>
|
||||||
</MediaHandlerProvider>
|
</MediaHandlerProvider>
|
||||||
);
|
);
|
||||||
}
|
};
|
|
@ -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;
|
||||||
|
roomIdOrAlias: 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
|
||||||
roomIdOrAlias={roomIdOrAlias}
|
roomIdOrAlias={roomIdOrAlias}
|
||||||
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 [
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
export function useLocationNavigation(enabled = false) {
|
export function useLocationNavigation(enabled = false): void {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -12,7 +12,7 @@ export function useLocationNavigation(enabled = false) {
|
||||||
const url = new URL(tx.pathname, window.location.href);
|
const url = new URL(tx.pathname, window.location.href);
|
||||||
url.search = tx.search;
|
url.search = tx.search;
|
||||||
url.hash = tx.hash;
|
url.hash = tx.hash;
|
||||||
window.location = url.href;
|
window.location.href = url.href;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useFocusVisible } from "@react-aria/interactions";
|
import { useFocusVisible } from "@react-aria/interactions";
|
||||||
|
|
||||||
import styles from "./usePageFocusStyle.module.css";
|
import styles from "./usePageFocusStyle.module.css";
|
||||||
|
|
||||||
export function usePageFocusStyle() {
|
export function usePageFocusStyle(): void {
|
||||||
const { isFocusVisible } = useFocusVisible();
|
const { isFocusVisible } = useFocusVisible();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
|
@ -1,9 +0,0 @@
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
export function usePageTitle(title) {
|
|
||||||
useEffect(() => {
|
|
||||||
const productName =
|
|
||||||
import.meta.env.VITE_PRODUCT_NAME || "Matrix Video Chat";
|
|
||||||
document.title = title ? `${productName} | ${title}` : productName;
|
|
||||||
}, [title]);
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2022 Matrix.org Foundation C.I.C.
|
Copyright 2022 New Vector Ltd
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,14 +14,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from "classnames";
|
import { useEffect } from "react";
|
||||||
import React, { forwardRef } from "react";
|
|
||||||
import styles from "./Form.module.css";
|
|
||||||
|
|
||||||
export const Form = forwardRef(({ children, className, ...rest }, ref) => {
|
export function usePageTitle(title: string): void {
|
||||||
return (
|
useEffect(() => {
|
||||||
<form {...rest} className={classNames(styles.form, className)} ref={ref}>
|
const productName =
|
||||||
{children}
|
import.meta.env.VITE_PRODUCT_NAME || "Matrix Video Chat";
|
||||||
</form>
|
document.title = title ? `${productName} | ${title}` : productName;
|
||||||
);
|
}, [title]);
|
||||||
});
|
}
|
|
@ -20,7 +20,7 @@ import classNames from "classnames";
|
||||||
import styles from "./VideoTile.module.css";
|
import styles from "./VideoTile.module.css";
|
||||||
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
|
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
|
||||||
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
|
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
|
||||||
import { OptionsButton } from "../button/Button";
|
import { AudioButton } from "../button/Button";
|
||||||
|
|
||||||
export const VideoTile = forwardRef(
|
export const VideoTile = forwardRef(
|
||||||
(
|
(
|
||||||
|
@ -38,6 +38,7 @@ export const VideoTile = forwardRef(
|
||||||
mediaRef,
|
mediaRef,
|
||||||
onOptionsPress,
|
onOptionsPress,
|
||||||
showOptions,
|
showOptions,
|
||||||
|
localVolume,
|
||||||
...rest
|
...rest
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
|
@ -53,6 +54,15 @@ export const VideoTile = forwardRef(
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
|
{showOptions && (
|
||||||
|
<div className={classNames(styles.toolbar)}>
|
||||||
|
<AudioButton
|
||||||
|
className={styles.button}
|
||||||
|
volume={localVolume}
|
||||||
|
onPress={onOptionsPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{(videoMuted || noVideo) && (
|
{(videoMuted || noVideo) && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.videoMutedOverlay} />
|
<div className={styles.videoMutedOverlay} />
|
||||||
|
@ -72,11 +82,7 @@ export const VideoTile = forwardRef(
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{showOptions && (
|
|
||||||
<div className={classNames(styles.infoBubble, styles.optionsButton)}>
|
|
||||||
<OptionsButton onPress={onOptionsPress} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<video ref={mediaRef} playsInline disablePictureInPicture />
|
<video ref={mediaRef} playsInline disablePictureInPicture />
|
||||||
</animated.div>
|
</animated.div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -48,8 +48,8 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
color: white;
|
color: var(--primary-content);
|
||||||
background-color: rgba(23, 25, 28, 0.85);
|
background-color: var(--background-85);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -60,14 +60,40 @@
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.optionsButton {
|
.toolbar {
|
||||||
right: 16px;
|
position: absolute;
|
||||||
top: 16px;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 42px;
|
||||||
|
|
||||||
|
color: var(--primary-content);
|
||||||
|
background-color: var(--background-85);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.optionsButton button svg {
|
.videoTile:not(.isLocal):not(:hover) .toolbar {
|
||||||
height: 20px;
|
display: none;
|
||||||
width: 20px;
|
}
|
||||||
|
|
||||||
|
.videoTile:not(.isLocal):hover .presenterLabel {
|
||||||
|
top: calc(42px + 20px); /* toolbar + margin */
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.memberName {
|
.memberName {
|
||||||
|
|
|
@ -82,6 +82,7 @@ export function VideoTileContainer({
|
||||||
avatar={getAvatar && getAvatar(member, width, height)}
|
avatar={getAvatar && getAvatar(member, width, height)}
|
||||||
onOptionsPress={onOptionsPress}
|
onOptionsPress={onOptionsPress}
|
||||||
showOptions={!item.callFeed.isLocal()}
|
showOptions={!item.callFeed.isLocal()}
|
||||||
|
localVolume={localVolume}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{videoTileSettingsModalState.isOpen && (
|
{videoTileSettingsModalState.isOpen && (
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
|
.videoTileSettingsModal {
|
||||||
|
width: 700px;
|
||||||
|
height: 316px;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
position: relative;
|
||||||
margin: 27px 34px;
|
margin: 27px 34px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.localVolumePercentage {
|
.localVolumePercentage {
|
||||||
|
|
|
@ -17,10 +17,10 @@ limitations under the License.
|
||||||
import React, { ChangeEvent, useState } from "react";
|
import React, { ChangeEvent, useState } from "react";
|
||||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
|
|
||||||
import selectInputStyles from "../input/SelectInput.module.css";
|
|
||||||
import { FieldRow } from "../input/Input";
|
import { FieldRow } from "../input/Input";
|
||||||
import { Modal } from "../Modal";
|
import { Modal } from "../Modal";
|
||||||
import styles from "./VideoTileSettingsModal.module.css";
|
import styles from "./VideoTileSettingsModal.module.css";
|
||||||
|
import { VolumeIcon } from "../button/VolumeIcon";
|
||||||
|
|
||||||
interface LocalVolumeProps {
|
interface LocalVolumeProps {
|
||||||
feed: CallFeed;
|
feed: CallFeed;
|
||||||
|
@ -39,11 +39,8 @@ const LocalVolume: React.FC<LocalVolumeProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4 className={selectInputStyles.label}> Local Volume </h4>
|
|
||||||
<FieldRow>
|
<FieldRow>
|
||||||
<span className={styles.localVolumePercentage}>
|
<VolumeIcon volume={localVolume} />
|
||||||
{`${Math.round(localVolume * 100)}%`}
|
|
||||||
</span>
|
|
||||||
<input
|
<input
|
||||||
className={styles.localVolumeSlider}
|
className={styles.localVolumeSlider}
|
||||||
type="range"
|
type="range"
|
||||||
|
@ -65,7 +62,13 @@ interface Props {
|
||||||
|
|
||||||
export const VideoTileSettingsModal = ({ feed, ...rest }: Props) => {
|
export const VideoTileSettingsModal = ({ feed, ...rest }: Props) => {
|
||||||
return (
|
return (
|
||||||
<Modal title="Feed settings" isDismissable mobileFullScreen {...rest}>
|
<Modal
|
||||||
|
className={styles.videoTileSettingsModal}
|
||||||
|
title="Local volume"
|
||||||
|
isDismissable
|
||||||
|
mobileFullScreen
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<LocalVolume feed={feed} />
|
<LocalVolume feed={feed} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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",
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -8397,7 +8397,6 @@ matrix-events-sdk@^0.0.1-beta.7:
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8ba2d257ae24bbed61cd7fe99af081324337161c"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8ba2d257ae24bbed61cd7fe99af081324337161c"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
"@types/sdp-transform" "^2.4.5"
|
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
browser-request "^0.3.3"
|
browser-request "^0.3.3"
|
||||||
bs58 "^4.0.1"
|
bs58 "^4.0.1"
|
||||||
|
@ -8407,13 +8406,12 @@ matrix-events-sdk@^0.0.1-beta.7:
|
||||||
p-retry "^4.5.0"
|
p-retry "^4.5.0"
|
||||||
qs "^6.9.6"
|
qs "^6.9.6"
|
||||||
request "^2.88.2"
|
request "^2.88.2"
|
||||||
sdp-transform "^2.14.1"
|
|
||||||
unhomoglyph "^1.0.6"
|
unhomoglyph "^1.0.6"
|
||||||
|
|
||||||
matrix-widget-api@^0.1.0-beta.18:
|
matrix-widget-api@^1.0.0:
|
||||||
version "0.1.0-beta.18"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.18.tgz#4efd30edec3eeb4211285985464c062fcab59795"
|
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.0.0.tgz#0cde6839cca66ad817ab12aca3490ccc8bac97d1"
|
||||||
integrity sha512-kCpcs6rrB94Mmr2/1gBJ+6auWyZ5UvOMOn5K2VFafz2/NDMzZg9OVWj9KFYnNAuwwBE5/tCztYEj6OQ+hgbwOQ==
|
integrity sha512-cy8p/8EteRPTFIAw7Q9EgPUJc2jD19ZahMR8bMKf2NkILDcjuPMC0UWnsJyB3fSnlGw+VbGepttRpULM31zX8Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/events" "^3.0.0"
|
"@types/events" "^3.0.0"
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
|
|
Loading…
Add table
Reference in a new issue