Merge pull request #489 from vector-im/SimonBrandner/task/ts-src
This commit is contained in:
commit
2e38558a9d
27 changed files with 561 additions and 254 deletions
|
@ -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> = ({
|
||||||
|
|
|
@ -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,28 @@
|
||||||
import React, { useRef, useState } from "react";
|
import React, { 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;
|
||||||
|
onAction: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Menu<T extends object>({
|
||||||
|
className,
|
||||||
|
onAction,
|
||||||
|
onClose,
|
||||||
|
...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 +36,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: () => 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,
|
||||||
},
|
},
|
|
@ -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) {
|
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,13 +1,36 @@
|
||||||
|
/*
|
||||||
|
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 React, { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { SequenceDiagramViewer } from "./room/GroupCallInspector";
|
import { SequenceDiagramViewer } from "./room/GroupCallInspector";
|
||||||
import { FieldRow, InputField } from "./input/Input";
|
import { FieldRow, InputField } from "./input/Input";
|
||||||
import { usePageTitle } from "./usePageTitle";
|
import { usePageTitle } from "./usePageTitle";
|
||||||
|
|
||||||
|
interface DebugLog {
|
||||||
|
localUserId: string;
|
||||||
|
eventsByUserId: Record<string, {}>;
|
||||||
|
remoteUserIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export function SequenceDiagramViewerPage() {
|
export function SequenceDiagramViewerPage() {
|
||||||
usePageTitle("Inspector");
|
usePageTitle("Inspector");
|
||||||
|
|
||||||
const [debugLog, setDebugLog] = useState();
|
const [debugLog, setDebugLog] = useState<DebugLog>();
|
||||||
const [selectedUserId, setSelectedUserId] = useState();
|
const [selectedUserId, setSelectedUserId] = useState<string>();
|
||||||
const onChangeDebugLog = useCallback((e) => {
|
const onChangeDebugLog = useCallback((e) => {
|
||||||
if (e.target.files && e.target.files.length > 0) {
|
if (e.target.files && e.target.files.length > 0) {
|
||||||
e.target.files[0].text().then((text) => {
|
e.target.files[0].text().then((text) => {
|
|
@ -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 }: 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();
|
|
@ -139,11 +139,12 @@ export function MicButton({
|
||||||
[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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -156,11 +157,12 @@ export function VideoButton({
|
||||||
[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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -175,11 +177,12 @@ export function ScreenshareButton({
|
||||||
[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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -192,7 +195,7 @@ export function HangupButton({
|
||||||
[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 +203,6 @@ export function HangupButton({
|
||||||
>
|
>
|
||||||
<HangupIcon />
|
<HangupIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{() => "Leave"}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -213,11 +215,10 @@ export function SettingsButton({
|
||||||
[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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -230,22 +231,20 @@ export function InviteButton({
|
||||||
[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">) {
|
export function OptionsButton(props: Omit<Props, "variant">) {
|
||||||
return (
|
return (
|
||||||
<TooltipTrigger>
|
<TooltipTrigger tooltip={() => "Options"}>
|
||||||
<Button variant="icon" {...props}>
|
<Button variant="icon" {...props}>
|
||||||
<OverflowIcon />
|
<OverflowIcon />
|
||||||
</Button>
|
</Button>
|
||||||
{() => "Options"}
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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 { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
@ -25,10 +25,10 @@ import {
|
||||||
ButtonSize,
|
ButtonSize,
|
||||||
} from "./Button";
|
} from "./Button";
|
||||||
interface Props {
|
interface Props {
|
||||||
className: string;
|
className?: string;
|
||||||
variant: ButtonVariant;
|
variant?: ButtonVariant;
|
||||||
size: ButtonSize;
|
size?: ButtonSize;
|
||||||
children: JSX.Element;
|
children: ReactNode;
|
||||||
[index: string]: unknown;
|
[index: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -28,11 +28,10 @@ import { Tooltip, TooltipTrigger } from "../Tooltip";
|
||||||
export function GridLayoutMenu({ layout, setLayout }) {
|
export function GridLayoutMenu({ layout, setLayout }) {
|
||||||
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) => (
|
||||||
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
||||||
|
|
|
@ -61,11 +61,10 @@ export function OverflowMenu({
|
||||||
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) => (
|
||||||
<Menu {...props} label="More menu" onAction={onAction}>
|
<Menu {...props} label="More menu" onAction={onAction}>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -27,10 +27,10 @@ import { useModalTriggerState } from "../Modal";
|
||||||
|
|
||||||
interface RageShakeSubmitOptions {
|
interface RageShakeSubmitOptions {
|
||||||
description: string;
|
description: string;
|
||||||
roomId: string;
|
roomId?: string;
|
||||||
label: string;
|
label?: string;
|
||||||
sendLogs: boolean;
|
sendLogs: boolean;
|
||||||
rageshakeRequestId: string;
|
rageshakeRequestId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSubmitRageshake(): {
|
export function useSubmitRageshake(): {
|
||||||
|
|
|
@ -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]);
|
|
||||||
}
|
|
25
src/usePageTitle.ts
Normal file
25
src/usePageTitle.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
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 { useEffect } from "react";
|
||||||
|
|
||||||
|
export function usePageTitle(title: string): void {
|
||||||
|
useEffect(() => {
|
||||||
|
const productName =
|
||||||
|
import.meta.env.VITE_PRODUCT_NAME || "Matrix Video Chat";
|
||||||
|
document.title = title ? `${productName} | ${title}` : productName;
|
||||||
|
}, [title]);
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue