Merge pull request #489 from vector-im/SimonBrandner/task/ts-src

This commit is contained in:
Šimon Brandner 2022-08-01 18:10:41 +02:00 committed by GitHub
commit 2e38558a9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 561 additions and 254 deletions

View file

@ -18,6 +18,7 @@ import React from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import * as Sentry from "@sentry/react";
import { OverlayProvider } from "@react-aria/overlays";
import { HomePage } from "./home/HomePage";
import { LoginPage } from "./auth/LoginPage";
import { RegisterPage } from "./auth/RegisterPage";
@ -31,7 +32,11 @@ import { CrashView } from "./FullScreenView";
const SentryRoute = Sentry.withSentryRouting(Route);
export default function App({ history }) {
interface AppProps {
history: History;
}
export default function App({ history }: AppProps) {
usePageFocusStyle();
const errorPage = <CrashView />;

View file

@ -48,11 +48,11 @@ const resolveAvatarSrc = (client: MatrixClient, src: string, size: number) =>
interface Props extends React.HTMLAttributes<HTMLDivElement> {
bgKey?: string;
src: string;
fallback: string;
src?: string;
size?: Size | number;
className: string;
className?: string;
style?: CSSProperties;
fallback: string;
}
export const Avatar: React.FC<Props> = ({

View file

@ -1,22 +1,48 @@
import React from "react";
import styles from "./Facepile.module.css";
import classNames from "classnames";
import { Avatar, sizes } from "./Avatar";
/*
Copyright 2022 New Vector Ltd
const overlapMap = {
xs: 2,
sm: 4,
md: 8,
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, { 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({
className,
client,
participants,
max,
size,
max = 3,
size = Size.XS,
...rest
}) {
}: Props) {
const _size = sizes.get(size);
const _overlap = overlapMap[size];
@ -56,8 +82,3 @@ export function Facepile({
</div>
);
}
Facepile.defaultProps = {
max: 3,
size: "xs",
};

View file

@ -1,13 +1,19 @@
import React, { useCallback, useEffect } from "react";
import React, { ReactNode, useCallback, useEffect } from "react";
import { useLocation } from "react-router-dom";
import styles from "./FullScreenView.module.css";
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
import classNames from "classnames";
import { Header, HeaderLogo, LeftNav, RightNav } from "./Header";
import { LinkButton, Button } from "./button";
import { useSubmitRageshake } from "./settings/submit-rageshake";
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 (
<div className={classNames(styles.page, className)}>
<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();
useEffect(() => {
@ -31,7 +41,7 @@ export function ErrorView({ error }) {
}, [error]);
const onReload = useCallback(() => {
window.location = "/";
window.location.href = "/";
}, []);
return (
@ -72,7 +82,7 @@ export function CrashView() {
}, [submitRageshake]);
const onReload = useCallback(() => {
window.location = "/";
window.location.href = "/";
}, []);
let logsComponent;

View file

@ -1,18 +1,26 @@
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 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 { Subtitle } from "./typography/Typography";
import { Avatar } from "./Avatar";
import { IncompatibleVersionModal } from "./IncompatibleVersionModal";
import { AriaButtonProps } from "@react-types/button";
import { Room } from "matrix-js-sdk";
import styles from "./Header.module.css";
import { useModalTriggerState } from "./Modal";
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 (
<header className={classNames(styles.header, className)} {...rest}>
{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 (
<div
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 (
<div
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 (
<Link className={classNames(styles.headerLogo, className)} to="/">
<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 (
<>
<div className={styles.roomAvatar}>
<Avatar
size="md"
size={Size.MD}
src={avatarUrl}
bgKey={roomName}
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({
roomName,
avatarUrl,
isEmbedded,
...rest
}) {
}: RoomSetupHeaderInfoProps) {
const ref = useRef();
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 onDetailsClick = useCallback(() => {

View file

@ -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
View 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;

View file

@ -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
View 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>
);
}

View file

@ -1,15 +1,28 @@
import React, { useRef, useState } from "react";
import styles from "./Menu.module.css";
import { useMenu, useMenuItem } from "@react-aria/menu";
import { useTreeState } from "@react-stately/tree";
import { AriaMenuOptions, useMenu, useMenuItem } from "@react-aria/menu";
import { TreeState, useTreeState } from "@react-stately/tree";
import { mergeProps } from "@react-aria/utils";
import { useFocus } from "@react-aria/interactions";
import classNames from "classnames";
import { Node } from "@react-types/shared";
export function Menu({ className, onAction, ...rest }) {
const state = useTreeState({ ...rest, selectionMode: "none" });
import styles from "./Menu.module.css";
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 { menuProps } = useMenu(rest, state, menuRef);
const { menuProps } = useMenu<T>(rest, state, menuRef);
return (
<ul
@ -23,19 +36,25 @@ export function Menu({ className, onAction, ...rest }) {
item={item}
state={state}
onAction={onAction}
onClose={rest.onClose}
onClose={onClose}
/>
))}
</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 { menuItemProps } = useMenuItem(
{
key: item.key,
isDisabled: item.isDisabled,
onAction,
onClose,
},

View file

@ -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 {
useOverlay,
usePreventScroll,
useModal,
OverlayContainer,
OverlayProps,
} from "@react-aria/overlays";
import { useOverlayTriggerState } from "@react-stately/overlays";
import {
OverlayTriggerState,
useOverlayTriggerState,
} from "@react-stately/overlays";
import { useDialog } from "@react-aria/dialog";
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 styles from "./Modal.module.css";
import classNames from "classnames";
export function Modal(props) {
const { title, children, className, mobileFullScreen } = props;
interface ModalProps extends OverlayProps, AriaDialogProps {
title: string;
children: ReactNode;
className?: string;
mobileFullScreen?: boolean;
onClose?: () => void;
}
export function Modal({
title,
children,
className,
mobileFullScreen,
onClose,
...rest
}: ModalProps) {
const modalRef = useRef();
const { overlayProps, underlayProps } = useOverlay(props, modalRef);
const { overlayProps, underlayProps } = useOverlay(
{ ...rest, onClose },
modalRef
);
usePreventScroll();
const { modalProps } = useModal();
const { dialogProps, titleProps } = useDialog(props, modalRef);
const { dialogProps, titleProps } = useDialog(rest, modalRef);
const closeButtonRef = useRef();
const { buttonProps: closeButtonProps } = useButton({
onPress: () => props.onClose(),
});
const { buttonProps: closeButtonProps } = useButton(
{
onPress: () => onClose(),
},
closeButtonRef
);
return (
<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 (
<div className={classNames(styles.content, className)} {...rest}>
{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 modalProps = useMemo(
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
@ -75,7 +131,10 @@ export function useModalTriggerState() {
return { modalState, modalProps };
}
export function useToggleModalButton(modalState, ref) {
export function useToggleModalButton(
modalState: OverlayTriggerState,
ref: React.RefObject<HTMLButtonElement>
): ButtonAria<React.ButtonHTMLAttributes<HTMLButtonElement>> {
return useButton(
{
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(
{
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(
{
onPress: () => modalState.close(),
@ -102,8 +167,12 @@ export function useCloseModalButton(modalState, ref) {
);
}
export function ModalTrigger({ children }) {
const { modalState, modalProps } = useModalState();
interface ModalTriggerProps {
children: ReactNode;
}
export function ModalTrigger({ children }: ModalTriggerProps) {
const { modalState, modalProps } = useModalTriggerState();
const buttonRef = useRef();
const { buttonProps } = useToggleModalButton(modalState, buttonRef);

View file

@ -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 { SequenceDiagramViewer } from "./room/GroupCallInspector";
import { FieldRow, InputField } from "./input/Input";
import { usePageTitle } from "./usePageTitle";
interface DebugLog {
localUserId: string;
eventsByUserId: Record<string, {}>;
remoteUserIds: string[];
}
export function SequenceDiagramViewerPage() {
usePageTitle("Inspector");
const [debugLog, setDebugLog] = useState();
const [selectedUserId, setSelectedUserId] = useState();
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) => {

View file

@ -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
View 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>
);
}
);

View file

@ -1,16 +1,26 @@
import React, { useMemo } from "react";
import { Item } from "@react-stately/collections";
import { useLocation } from "react-router-dom";
import { Button, LinkButton } from "./button";
import { PopoverMenuTrigger } from "./popover/PopoverMenu";
import { Menu } from "./Menu";
import { Tooltip, TooltipTrigger } from "./Tooltip";
import { Avatar } from "./Avatar";
import { TooltipTrigger } from "./Tooltip";
import { Avatar, Size } from "./Avatar";
import { ReactComponent as UserIcon } from "./icons/User.svg";
import { ReactComponent as LoginIcon } from "./icons/Login.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 styles from "./UserMenu.module.css";
interface UserMenuProps {
preventNavigation: boolean;
isAuthenticated: boolean;
isPasswordlessUser: boolean;
displayName: string;
avatarUrl: string;
onAction: (value: string) => void;
}
export function UserMenu({
preventNavigation,
@ -19,7 +29,7 @@ export function UserMenu({
displayName,
avatarUrl,
onAction,
}) {
}: UserMenuProps) {
const location = useLocation();
const items = useMemo(() => {
@ -62,11 +72,11 @@ export function UserMenu({
return (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger placement="bottom left">
<TooltipTrigger tooltip={() => "Profile"} placement="bottom left">
<Button variant="icon" className={styles.userButton}>
{isAuthenticated && (!isPasswordlessUser || avatarUrl) ? (
<Avatar
size="sm"
size={Size.SM}
className={styles.avatar}
src={avatarUrl}
fallback={displayName.slice(0, 1).toUpperCase()}
@ -75,12 +85,11 @@ export function UserMenu({
<UserIcon />
)}
</Button>
{() => "Profile"}
</TooltipTrigger>
{(props) => (
<Menu {...props} label="User menu" onAction={onAction}>
{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} />
<Body overflowEllipsis>{label}</Body>
</Item>

View file

@ -1,12 +1,17 @@
import React, { useCallback } from "react";
import { useHistory, useLocation } from "react-router-dom";
import { useClient } from "./ClientContext";
import { useProfile } from "./profile/useProfile";
import { useModalTriggerState } from "./Modal";
import { ProfileModal } from "./profile/ProfileModal";
import { UserMenu } from "./UserMenu";
export function UserMenuContainer({ preventNavigation }) {
interface Props {
preventNavigation: boolean;
}
export function UserMenuContainer({ preventNavigation }: Props) {
const location = useLocation();
const history = useHistory();
const { isAuthenticated, isPasswordlessUser, logout, userName, client } =
@ -15,7 +20,7 @@ export function UserMenuContainer({ preventNavigation }) {
const { modalState, modalProps } = useModalTriggerState();
const onAction = useCallback(
(value) => {
(value: string) => {
switch (value) {
case "user":
modalState.open();

View file

@ -139,11 +139,12 @@ export function MicButton({
[index: string]: unknown;
}) {
return (
<TooltipTrigger>
<TooltipTrigger
tooltip={() => (muted ? "Unmute microphone" : "Mute microphone")}
>
<Button variant="toolbar" {...rest} off={muted}>
{muted ? <MuteMicIcon /> : <MicIcon />}
</Button>
{() => (muted ? "Unmute microphone" : "Mute microphone")}
</TooltipTrigger>
);
}
@ -156,11 +157,12 @@ export function VideoButton({
[index: string]: unknown;
}) {
return (
<TooltipTrigger>
<TooltipTrigger
tooltip={() => (muted ? "Turn on camera" : "Turn off camera")}
>
<Button variant="toolbar" {...rest} off={muted}>
{muted ? <DisableVideoIcon /> : <VideoIcon />}
</Button>
{() => (muted ? "Turn on camera" : "Turn off camera")}
</TooltipTrigger>
);
}
@ -175,11 +177,12 @@ export function ScreenshareButton({
[index: string]: unknown;
}) {
return (
<TooltipTrigger>
<TooltipTrigger
tooltip={() => (enabled ? "Stop sharing screen" : "Share screen")}
>
<Button variant="toolbarSecondary" {...rest} on={enabled}>
<ScreenshareIcon />
</Button>
{() => (enabled ? "Stop sharing screen" : "Share screen")}
</TooltipTrigger>
);
}
@ -192,7 +195,7 @@ export function HangupButton({
[index: string]: unknown;
}) {
return (
<TooltipTrigger>
<TooltipTrigger tooltip={() => "Leave"}>
<Button
variant="toolbar"
className={classNames(styles.hangupButton, className)}
@ -200,7 +203,6 @@ export function HangupButton({
>
<HangupIcon />
</Button>
{() => "Leave"}
</TooltipTrigger>
);
}
@ -213,11 +215,10 @@ export function SettingsButton({
[index: string]: unknown;
}) {
return (
<TooltipTrigger>
<TooltipTrigger tooltip={() => "Settings"}>
<Button variant="toolbar" {...rest}>
<SettingsIcon />
</Button>
{() => "Settings"}
</TooltipTrigger>
);
}
@ -230,22 +231,20 @@ export function InviteButton({
[index: string]: unknown;
}) {
return (
<TooltipTrigger>
<TooltipTrigger tooltip={() => "Invite"}>
<Button variant="toolbar" {...rest}>
<AddUserIcon />
</Button>
{() => "Invite"}
</TooltipTrigger>
);
}
export function OptionsButton(props: Omit<Props, "variant">) {
return (
<TooltipTrigger>
<TooltipTrigger tooltip={() => "Options"}>
<Button variant="icon" {...props}>
<OverflowIcon />
</Button>
{() => "Options"}
</TooltipTrigger>
);
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, { ReactNode } from "react";
import { Link } from "react-router-dom";
import classNames from "classnames";
@ -25,10 +25,10 @@ import {
ButtonSize,
} from "./Button";
interface Props {
className: string;
variant: ButtonVariant;
size: ButtonSize;
children: JSX.Element;
className?: string;
variant?: ButtonVariant;
size?: ButtonSize;
children: ReactNode;
[index: string]: unknown;
}

View file

@ -26,7 +26,7 @@ import styles from "./ProfileModal.module.css";
interface Props {
client: MatrixClient;
onClose: () => {};
onClose: () => void;
[rest: string]: unknown;
}
export function ProfileModal({ client, ...rest }: Props) {

View file

@ -28,11 +28,10 @@ import { Tooltip, TooltipTrigger } from "../Tooltip";
export function GridLayoutMenu({ layout, setLayout }) {
return (
<PopoverMenuTrigger placement="bottom right">
<TooltipTrigger>
<TooltipTrigger tooltip={() => "Layout Type"}>
<Button variant="icon">
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
</Button>
{() => "Layout Type"}
</TooltipTrigger>
{(props) => (
<Menu {...props} label="Grid layout menu" onAction={setLayout}>

View file

@ -61,11 +61,10 @@ export function OverflowMenu({
return (
<>
<PopoverMenuTrigger disableOnState>
<TooltipTrigger placement="top">
<TooltipTrigger tooltip={() => "More"} placement="top">
<Button variant="toolbar">
<OverflowIcon />
</Button>
{() => "More"}
</TooltipTrigger>
{(props) => (
<Menu {...props} label="More menu" onAction={onAction}>

View file

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

View file

@ -27,10 +27,10 @@ import { useModalTriggerState } from "../Modal";
interface RageShakeSubmitOptions {
description: string;
roomId: string;
label: string;
roomId?: string;
label?: string;
sendLogs: boolean;
rageshakeRequestId: string;
rageshakeRequestId?: string;
}
export function useSubmitRageshake(): {

View file

@ -1,7 +1,7 @@
import { useEffect } from "react";
import { useHistory } from "react-router-dom";
export function useLocationNavigation(enabled = false) {
export function useLocationNavigation(enabled = false): void {
const history = useHistory();
useEffect(() => {
@ -12,7 +12,7 @@ export function useLocationNavigation(enabled = false) {
const url = new URL(tx.pathname, window.location.href);
url.search = tx.search;
url.hash = tx.hash;
window.location = url.href;
window.location.href = url.href;
});
}

View file

@ -1,8 +1,9 @@
import { useEffect } from "react";
import { useFocusVisible } from "@react-aria/interactions";
import styles from "./usePageFocusStyle.module.css";
export function usePageFocusStyle() {
export function usePageFocusStyle(): void {
const { isFocusVisible } = useFocusVisible();
useEffect(() => {

View file

@ -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
View 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]);
}