Finish settings modal
This commit is contained in:
parent
4e2d1c5dcd
commit
94f42019df
28 changed files with 975 additions and 356 deletions
|
@ -11,9 +11,12 @@
|
|||
"@react-aria/focus": "^3.5.0",
|
||||
"@react-aria/menu": "^3.3.0",
|
||||
"@react-aria/overlays": "^3.7.3",
|
||||
"@react-aria/select": "^3.6.0",
|
||||
"@react-aria/tabs": "^3.1.0",
|
||||
"@react-aria/utils": "^3.10.0",
|
||||
"@react-stately/collections": "^3.3.4",
|
||||
"@react-stately/overlays": "^3.1.3",
|
||||
"@react-stately/select": "^3.1.3",
|
||||
"@react-stately/tree": "^3.2.0",
|
||||
"@sentry/react": "^6.13.3",
|
||||
"@sentry/tracing": "^6.13.3",
|
||||
|
|
|
@ -30,6 +30,7 @@ import { RegisterPage } from "./RegisterPage";
|
|||
import { LoginPage } from "./LoginPage";
|
||||
import { Center } from "./Layout";
|
||||
import { GuestAuthPage } from "./GuestAuthPage";
|
||||
import { OverlayProvider } from "@react-aria/overlays";
|
||||
|
||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||
|
||||
|
@ -49,7 +50,7 @@ export default function App() {
|
|||
|
||||
return (
|
||||
<Router>
|
||||
<>
|
||||
<OverlayProvider>
|
||||
{loading ? (
|
||||
<Center>
|
||||
<p>Loading...</p>
|
||||
|
@ -74,7 +75,7 @@ export default function App() {
|
|||
</SentryRoute>
|
||||
</Switch>
|
||||
)}
|
||||
</>
|
||||
</OverlayProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,36 +1,36 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { ButtonTooltip, HeaderButton } from "./RoomButton";
|
||||
import { Popover, PopoverMenu, PopoverMenuItem } from "./PopoverMenu";
|
||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
||||
import { ReactComponent as SpotlightIcon } from "./icons/Spotlight.svg";
|
||||
import { ReactComponent as FreedomIcon } from "./icons/Freedom.svg";
|
||||
import { ReactComponent as CheckIcon } from "./icons/Check.svg";
|
||||
import styles from "./GridLayoutMenu.module.css";
|
||||
import { Menu } from "./Menu";
|
||||
import { Item } from "@react-stately/collections";
|
||||
|
||||
export function GridLayoutMenu({ layout, setLayout }) {
|
||||
const onAction = useCallback((value) => setLayout(value));
|
||||
|
||||
return (
|
||||
<PopoverMenu onAction={onAction} placement="bottom right">
|
||||
<PopoverMenuTrigger placement="bottom right">
|
||||
<HeaderButton>
|
||||
<ButtonTooltip>Layout Type</ButtonTooltip>
|
||||
{layout === "spotlight" ? <SpotlightIcon /> : <FreedomIcon />}
|
||||
</HeaderButton>
|
||||
{(props) => (
|
||||
<Popover {...props} label="Grid layout menu">
|
||||
<PopoverMenuItem key="freedom" textValue="Freedom">
|
||||
<Menu {...props} label="Grid layout menu" onAction={setLayout}>
|
||||
<Item key="freedom" textValue="Freedom">
|
||||
<FreedomIcon />
|
||||
<span>Freedom</span>
|
||||
{layout === "freedom" && <CheckIcon className={styles.checkIcon} />}
|
||||
</PopoverMenuItem>
|
||||
<PopoverMenuItem key="spotlight" textValue="Spotlight">
|
||||
</Item>
|
||||
<Item key="spotlight" textValue="Spotlight">
|
||||
<SpotlightIcon />
|
||||
<span>Spotlight</span>
|
||||
{layout === "spotlight" && (
|
||||
<CheckIcon className={styles.checkIcon} />
|
||||
)}
|
||||
</PopoverMenuItem>
|
||||
</Popover>
|
||||
</Item>
|
||||
</Menu>
|
||||
)}
|
||||
</PopoverMenu>
|
||||
</PopoverMenuTrigger>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,14 @@
|
|||
import React from "react";
|
||||
import { Overlay } from "./Overlay";
|
||||
import { Modal, ModalContent } from "./Modal";
|
||||
import { CopyButton } from "./CopyButton";
|
||||
import { HeaderButton, ButtonTooltip } from "./RoomButton";
|
||||
import { ReactComponent as AddUserIcon } from "./icons/AddUser.svg";
|
||||
|
||||
export function InviteModalButton({ roomUrl }) {
|
||||
export function InviteModal({ roomUrl, ...rest }) {
|
||||
return (
|
||||
<Overlay>
|
||||
<HeaderButton>
|
||||
<ButtonTooltip>Add User</ButtonTooltip>
|
||||
<AddUserIcon width={20} height={20} />
|
||||
</HeaderButton>
|
||||
{(modalProps) => (
|
||||
<Modal title="Invite People" isDismissable {...modalProps}>
|
||||
<Modal title="Invite People" isDismissable {...rest}>
|
||||
<ModalContent>
|
||||
<p>Copy and share this meeting link</p>
|
||||
<CopyButton value={roomUrl} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)}
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
|
50
src/ListBox.jsx
Normal file
50
src/ListBox.jsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
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>
|
||||
);
|
||||
}
|
34
src/ListBox.module.css
Normal file
34
src/ListBox.module.css
Normal file
|
@ -0,0 +1,34 @@
|
|||
.listBox {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: transparent;
|
||||
color: var(--textColor1);
|
||||
padding: 8px 16px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
color: #0dbd8b;
|
||||
}
|
||||
|
||||
.option.focused {
|
||||
background-color: var(--bgColor3);
|
||||
}
|
||||
|
||||
.option.disabled {
|
||||
color: var(--textColor2);
|
||||
background-color: var(--bgColor3);
|
||||
}
|
60
src/Menu.jsx
Normal file
60
src/Menu.jsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
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 { mergeProps } from "@react-aria/utils";
|
||||
import { useFocus } from "@react-aria/interactions";
|
||||
import classNames from "classnames";
|
||||
|
||||
export function Menu({ className, onAction, ...rest }) {
|
||||
const state = useTreeState({ ...rest, selectionMode: "none" });
|
||||
const menuRef = useRef();
|
||||
const { menuProps } = useMenu(rest, state, menuRef);
|
||||
|
||||
return (
|
||||
<ul
|
||||
{...mergeProps(menuProps, rest)}
|
||||
ref={menuRef}
|
||||
className={classNames(styles.menu, className)}
|
||||
>
|
||||
{[...state.collection].map((item) => (
|
||||
<MenuItem
|
||||
key={item.key}
|
||||
item={item}
|
||||
state={state}
|
||||
onAction={onAction}
|
||||
onClose={rest.onClose}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function MenuItem({ item, state, onAction, onClose }) {
|
||||
const ref = useRef();
|
||||
const { menuItemProps } = useMenuItem(
|
||||
{
|
||||
key: item.key,
|
||||
isDisabled: item.isDisabled,
|
||||
onAction,
|
||||
onClose,
|
||||
},
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
const [isFocused, setFocused] = useState(false);
|
||||
const { focusProps } = useFocus({ onFocusChange: setFocused });
|
||||
|
||||
return (
|
||||
<li
|
||||
{...mergeProps(menuItemProps, focusProps)}
|
||||
ref={ref}
|
||||
className={classNames(styles.menuItem, {
|
||||
[styles.focused]: isFocused,
|
||||
})}
|
||||
>
|
||||
{item.rendered}
|
||||
</li>
|
||||
);
|
||||
}
|
40
src/Menu.module.css
Normal file
40
src/Menu.module.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
.menu {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
cursor: pointer;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
color: var(--textColor1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menuItem > * {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.menuItem > :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.menuItem.focused,
|
||||
.menuItem:hover {
|
||||
background-color: var(--bgColor3);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.menuItem.focused:first-child,
|
||||
.menuItem:hover:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.menuItem.focused:last-child,
|
||||
.menuItem:hover:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useRef } from "react";
|
||||
import React, { useRef, useMemo } from "react";
|
||||
import {
|
||||
useOverlay,
|
||||
usePreventScroll,
|
||||
useModal,
|
||||
OverlayContainer,
|
||||
} from "@react-aria/overlays";
|
||||
import { useOverlayTriggerState } from "@react-stately/overlays";
|
||||
import { useDialog } from "@react-aria/dialog";
|
||||
import { FocusScope } from "@react-aria/focus";
|
||||
import { useButton } from "@react-aria/button";
|
||||
|
@ -13,7 +14,7 @@ import styles from "./Modal.module.css";
|
|||
import classNames from "classnames";
|
||||
|
||||
export function Modal(props) {
|
||||
const { title, children } = props;
|
||||
const { title, children, className } = props;
|
||||
const modalRef = useRef();
|
||||
const { overlayProps, underlayProps } = useOverlay(props, modalRef);
|
||||
usePreventScroll();
|
||||
|
@ -33,7 +34,7 @@ export function Modal(props) {
|
|||
{...dialogProps}
|
||||
{...modalProps}
|
||||
ref={modalRef}
|
||||
className={styles.modal}
|
||||
className={classNames(styles.modal, className)}
|
||||
>
|
||||
<div className={styles.modalHeader}>
|
||||
<h3 {...titleProps}>{title}</h3>
|
||||
|
@ -60,3 +61,68 @@ export function ModalContent({ children, className, ...rest }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useModalTriggerState() {
|
||||
const modalState = useOverlayTriggerState({});
|
||||
const modalProps = useMemo(
|
||||
() => ({ isOpen: modalState.isOpen, onClose: modalState.close }),
|
||||
[modalState]
|
||||
);
|
||||
return { modalState, modalProps };
|
||||
}
|
||||
|
||||
export function useToggleModalButton(modalState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.toggle(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useOpenModalButton(modalState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.open(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useCloseModalButton(modalState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => modalState.close(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function ModalTrigger({ children }) {
|
||||
const { modalState, modalProps } = useModalState();
|
||||
const buttonRef = useRef();
|
||||
const { buttonProps } = useToggleModalButton(modalState, buttonRef);
|
||||
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children.length > 2 ||
|
||||
typeof children[1] !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"ModalTrigger must have two props. The first being a button and the second being a render prop."
|
||||
);
|
||||
}
|
||||
|
||||
const [modalTrigger, modal] = children;
|
||||
|
||||
return (
|
||||
<>
|
||||
<modalTrigger.type
|
||||
{...modalTrigger.props}
|
||||
{...buttonProps}
|
||||
ref={buttonRef}
|
||||
/>
|
||||
{modalState.isOpen && modal(modalProps)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,31 +1,70 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { ButtonTooltip, RoomButton } from "./RoomButton";
|
||||
import { Popover, PopoverMenu, PopoverMenuItem } from "./PopoverMenu";
|
||||
import { Menu } from "./Menu";
|
||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { ReactComponent as SettingsIcon } from "./icons/Settings.svg";
|
||||
import { ReactComponent as AddUserIcon } from "./icons/AddUser.svg";
|
||||
import { ReactComponent as OverflowIcon } from "./icons/Overflow.svg";
|
||||
import { useModalTriggerState } from "./Modal";
|
||||
import { SettingsModal } from "./SettingsModal";
|
||||
import { InviteModal } from "./InviteModal";
|
||||
|
||||
export function OverflowMenu({ roomUrl }) {
|
||||
const onAction = useCallback((e) => console.log(e));
|
||||
export function OverflowMenu({
|
||||
roomUrl,
|
||||
setShowInspector,
|
||||
showInspector,
|
||||
client,
|
||||
}) {
|
||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||
useModalTriggerState();
|
||||
const { modalState: settingsModalState, modalProps: settingsModalProps } =
|
||||
useModalTriggerState();
|
||||
|
||||
// TODO: On closing modal, focus should be restored to the trigger button
|
||||
// https://github.com/adobe/react-spectrum/issues/2444
|
||||
const onAction = useCallback((key) => {
|
||||
switch (key) {
|
||||
case "invite":
|
||||
inviteModalState.open();
|
||||
break;
|
||||
case "settings":
|
||||
settingsModalState.open();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<PopoverMenu onAction={onAction}>
|
||||
<>
|
||||
<PopoverMenuTrigger>
|
||||
<RoomButton>
|
||||
<ButtonTooltip>More</ButtonTooltip>
|
||||
<OverflowIcon />
|
||||
</RoomButton>
|
||||
{(props) => (
|
||||
<Popover {...props} label="More menu">
|
||||
<PopoverMenuItem key="invite" textValue="Invite people">
|
||||
<Menu {...props} label="More menu" onAction={onAction}>
|
||||
<Item key="invite" textValue="Invite people">
|
||||
<AddUserIcon />
|
||||
<span>Invite people</span>
|
||||
</PopoverMenuItem>
|
||||
<PopoverMenuItem key="settings" textValue="Settings">
|
||||
</Item>
|
||||
<Item key="settings" textValue="Settings">
|
||||
<SettingsIcon />
|
||||
<span>Settings</span>
|
||||
</PopoverMenuItem>
|
||||
</Popover>
|
||||
</Item>
|
||||
</Menu>
|
||||
)}
|
||||
</PopoverMenu>
|
||||
</PopoverMenuTrigger>
|
||||
{settingsModalState.isOpen && (
|
||||
<SettingsModal
|
||||
{...settingsModalProps}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
client={client}
|
||||
/>
|
||||
)}
|
||||
{inviteModalState.isOpen && (
|
||||
<InviteModal roomUrl={roomUrl} {...inviteModalProps} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import React, { useRef } from "react";
|
||||
import { useOverlayTriggerState } from "@react-stately/overlays";
|
||||
import { useButton } from "@react-aria/button";
|
||||
|
||||
export function useToggleOverlayButton(overlayState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => overlayState.toggle(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useOpenOverlayButton(overlayState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => overlayState.open(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function useCloseOverlayButton(overlayState, ref) {
|
||||
return useButton(
|
||||
{
|
||||
onPress: () => overlayState.close(),
|
||||
},
|
||||
ref
|
||||
);
|
||||
}
|
||||
|
||||
export function Overlay({ children }) {
|
||||
const overlayState = useOverlayTriggerState({});
|
||||
const buttonRef = useRef();
|
||||
const { buttonProps } = useToggleOverlayButton(overlayState, buttonRef);
|
||||
|
||||
if (
|
||||
!Array.isArray(children) ||
|
||||
children.length > 2 ||
|
||||
typeof children[1] !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"Overlay trigger must have two props. The first being a button and the second being a render prop."
|
||||
);
|
||||
}
|
||||
|
||||
const [overlayTrigger, overlay] = children;
|
||||
|
||||
return (
|
||||
<>
|
||||
<overlayTrigger.type
|
||||
{...overlayTrigger.props}
|
||||
{...buttonProps}
|
||||
ref={buttonRef}
|
||||
/>
|
||||
{overlayState.isOpen &&
|
||||
overlay({ isOpen: overlayState.isOpen, onClose: overlayState.close })}
|
||||
</>
|
||||
);
|
||||
}
|
39
src/Popover.jsx
Normal file
39
src/Popover.jsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import {
|
||||
DismissButton,
|
||||
useOverlay,
|
||||
OverlayContainer,
|
||||
} from "@react-aria/overlays";
|
||||
import { FocusScope } from "@react-aria/focus";
|
||||
import classNames from "classnames";
|
||||
import styles from "./Popover.module.css";
|
||||
|
||||
export const Popover = forwardRef(
|
||||
({ isOpen = true, onClose, className, children, ...rest }, ref) => {
|
||||
const { overlayProps } = useOverlay(
|
||||
{
|
||||
isOpen,
|
||||
onClose,
|
||||
shouldCloseOnBlur: true,
|
||||
isDismissable: true,
|
||||
},
|
||||
ref
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<FocusScope restoreFocus>
|
||||
<div
|
||||
{...overlayProps}
|
||||
{...rest}
|
||||
className={classNames(styles.popover, className)}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
<DismissButton onDismiss={onClose} />
|
||||
</div>
|
||||
</FocusScope>
|
||||
</OverlayContainer>
|
||||
);
|
||||
}
|
||||
);
|
8
src/Popover.module.css
Normal file
8
src/Popover.module.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
.popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 194px;
|
||||
background: var(--bgColor2);
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 8px;
|
||||
}
|
|
@ -1,22 +1,17 @@
|
|||
import React, { useRef, useState, forwardRef } from "react";
|
||||
import React, { useRef } from "react";
|
||||
import styles from "./PopoverMenu.module.css";
|
||||
import { useMenuTriggerState } from "@react-stately/menu";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { useMenu, useMenuItem, useMenuTrigger } from "@react-aria/menu";
|
||||
import { useTreeState } from "@react-stately/tree";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { mergeProps } from "@react-aria/utils";
|
||||
import { FocusScope } from "@react-aria/focus";
|
||||
import { useFocus } from "@react-aria/interactions";
|
||||
import {
|
||||
useOverlay,
|
||||
DismissButton,
|
||||
useOverlayPosition,
|
||||
OverlayContainer,
|
||||
} from "@react-aria/overlays";
|
||||
import { useMenuTrigger } from "@react-aria/menu";
|
||||
import { useOverlayPosition } from "@react-aria/overlays";
|
||||
import classNames from "classnames";
|
||||
import { Popover } from "./Popover";
|
||||
|
||||
export function PopoverMenu({ children, placement, ...rest }) {
|
||||
export function PopoverMenuTrigger({
|
||||
children,
|
||||
placement,
|
||||
className,
|
||||
...rest
|
||||
}) {
|
||||
const popoverMenuState = useMenuTriggerState(rest);
|
||||
const buttonRef = useRef();
|
||||
const { menuTriggerProps, menuProps } = useMenuTrigger(
|
||||
|
@ -27,7 +22,7 @@ export function PopoverMenu({ children, placement, ...rest }) {
|
|||
|
||||
const popoverRef = useRef();
|
||||
|
||||
const { overlayProps: positionProps } = useOverlayPosition({
|
||||
const { overlayProps } = useOverlayPosition({
|
||||
targetRef: buttonRef,
|
||||
overlayRef: popoverRef,
|
||||
placement: placement || "top",
|
||||
|
@ -45,102 +40,30 @@ export function PopoverMenu({ children, placement, ...rest }) {
|
|||
);
|
||||
}
|
||||
|
||||
const [popoverTrigger, popover] = children;
|
||||
const [popoverTrigger, popoverMenu] = children;
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", display: "inline-block" }}>
|
||||
<div className={classNames(styles.popoverMenuTrigger, className)}>
|
||||
<popoverTrigger.type
|
||||
{...popoverTrigger.props}
|
||||
{...menuTriggerProps}
|
||||
on={popoverMenuState.isOpen}
|
||||
ref={buttonRef}
|
||||
/>
|
||||
{popoverMenuState.isOpen &&
|
||||
popover({
|
||||
isOpen: popoverMenuState.isOpen,
|
||||
onClose: popoverMenuState.close,
|
||||
{popoverMenuState.isOpen && (
|
||||
<Popover
|
||||
{...overlayProps}
|
||||
isOpen={popoverMenuState.isOpen}
|
||||
onClose={popoverMenuState.close}
|
||||
ref={popoverRef}
|
||||
>
|
||||
{popoverMenu({
|
||||
...menuProps,
|
||||
autoFocus: popoverMenuState.focusStrategy,
|
||||
domProps: menuProps,
|
||||
ref: popoverRef,
|
||||
positionProps,
|
||||
...rest,
|
||||
onClose: popoverMenuState.close,
|
||||
})}
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Popover = forwardRef((props, ref) => {
|
||||
const state = useTreeState({ ...props, selectionMode: "none" });
|
||||
const menuRef = useRef();
|
||||
const { menuProps } = useMenu(props, state, menuRef);
|
||||
const { overlayProps } = useOverlay(
|
||||
{
|
||||
onClose: props.onClose,
|
||||
shouldCloseOnBlur: true,
|
||||
isOpen: true,
|
||||
isDismissable: true,
|
||||
},
|
||||
ref
|
||||
);
|
||||
|
||||
return (
|
||||
<OverlayContainer>
|
||||
<FocusScope restoreFocus>
|
||||
<div
|
||||
className={styles.popover}
|
||||
{...mergeProps(overlayProps, props.positionProps)}
|
||||
ref={ref}
|
||||
>
|
||||
<DismissButton onDismiss={props.onClose} />
|
||||
<ul
|
||||
{...mergeProps(menuProps, props.domProps)}
|
||||
ref={menuRef}
|
||||
className={styles.popoverMenu}
|
||||
>
|
||||
{[...state.collection].map((item) => (
|
||||
<PopoverMenuItemContainer
|
||||
key={item.key}
|
||||
item={item}
|
||||
state={state}
|
||||
onAction={props.onAction}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<DismissButton onDismiss={props.onClose} />
|
||||
</div>
|
||||
</FocusScope>
|
||||
</OverlayContainer>
|
||||
);
|
||||
});
|
||||
|
||||
function PopoverMenuItemContainer({ item, state, onAction, onClose }) {
|
||||
const ref = useRef();
|
||||
const { menuItemProps } = useMenuItem(
|
||||
{
|
||||
key: item.key,
|
||||
isDisabled: item.isDisabled,
|
||||
onAction,
|
||||
onClose,
|
||||
},
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
const [isFocused, setFocused] = useState(false);
|
||||
const { focusProps } = useFocus({ onFocusChange: setFocused });
|
||||
|
||||
return (
|
||||
<li
|
||||
{...mergeProps(menuItemProps, focusProps)}
|
||||
ref={ref}
|
||||
className={classNames(styles.popoverMenuItem, {
|
||||
[styles.focused]: isFocused,
|
||||
})}
|
||||
>
|
||||
{item.rendered}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export const PopoverMenuItem = Item;
|
||||
|
|
|
@ -1,49 +1,4 @@
|
|||
.popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 194px;
|
||||
background: var(--bgColor2);
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.popoverMenu {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.popoverMenuItem {
|
||||
cursor: pointer;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
color: var(--textColor1);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.popoverMenuItem > * {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.popoverMenuItem > :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.popoverMenuItem.focused,
|
||||
.popoverMenuItem:hover {
|
||||
background-color: var(--bgColor3);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.popoverMenuItem.focused:first-child,
|
||||
.popoverMenuItem:hover:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.popoverMenuItem.focused:last-child,
|
||||
.popoverMenuItem:hover:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
.popoverMenuTrigger {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
90
src/Room.jsx
90
src/Room.jsx
|
@ -21,7 +21,6 @@ import {
|
|||
HangupButton,
|
||||
MicButton,
|
||||
VideoButton,
|
||||
LayoutToggleButton,
|
||||
ScreenshareButton,
|
||||
DropdownButton,
|
||||
} from "./RoomButton";
|
||||
|
@ -47,10 +46,10 @@ import { fetchGroupCall } from "./ConferenceCallManagerHooks";
|
|||
import { ErrorModal } from "./ErrorModal";
|
||||
import { GroupCallInspector } from "./GroupCallInspector";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { InviteModalButton } from "./InviteModal";
|
||||
import { OverflowMenu } from "./OverflowMenu";
|
||||
import { GridLayoutMenu } from "./GridLayoutMenu";
|
||||
import { UserMenu } from "./UserMenu";
|
||||
import { useMediaHandler } from "./useMediaHandler";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in navigator.mediaDevices;
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
|
@ -351,77 +350,6 @@ function RoomSetupView({
|
|||
);
|
||||
}
|
||||
|
||||
function useMediaHandler(client) {
|
||||
const [{ audioInput, videoInput, audioInputs, videoInputs }, setState] =
|
||||
useState(() => {
|
||||
const mediaHandler = client.getMediaHandler();
|
||||
|
||||
return {
|
||||
audioInput: mediaHandler.audioInput,
|
||||
videoInput: mediaHandler.videoInput,
|
||||
audioInputs: [],
|
||||
videoInputs: [],
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const mediaHandler = client.getMediaHandler();
|
||||
|
||||
function updateDevices() {
|
||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||
const audioInputs = devices.filter(
|
||||
(device) => device.kind === "audioinput"
|
||||
);
|
||||
const videoInputs = devices.filter(
|
||||
(device) => device.kind === "videoinput"
|
||||
);
|
||||
|
||||
setState((prevState) => ({
|
||||
audioInput: mediaHandler.audioInput,
|
||||
videoInput: mediaHandler.videoInput,
|
||||
audioInputs,
|
||||
videoInputs,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
updateDevices();
|
||||
|
||||
mediaHandler.on("local_streams_changed", updateDevices);
|
||||
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
|
||||
|
||||
return () => {
|
||||
mediaHandler.removeListener("local_streams_changed", updateDevices);
|
||||
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setAudioInput = useCallback(
|
||||
(deviceId) => {
|
||||
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
|
||||
client.getMediaHandler().setAudioInput(deviceId);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
const setVideoInput = useCallback(
|
||||
(deviceId) => {
|
||||
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
|
||||
client.getMediaHandler().setVideoInput(deviceId);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
return {
|
||||
audioInput,
|
||||
audioInputs,
|
||||
setAudioInput,
|
||||
videoInput,
|
||||
videoInputs,
|
||||
setVideoInput,
|
||||
};
|
||||
}
|
||||
|
||||
function InRoomView({
|
||||
onLogout,
|
||||
client,
|
||||
|
@ -443,15 +371,6 @@ function InRoomView({
|
|||
|
||||
const [layout, setLayout] = useVideoGridLayout();
|
||||
|
||||
const {
|
||||
audioInput,
|
||||
audioInputs,
|
||||
setAudioInput,
|
||||
videoInput,
|
||||
videoInputs,
|
||||
setVideoInput,
|
||||
} = useMediaHandler(client);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const participants = [];
|
||||
|
||||
|
@ -539,7 +458,12 @@ function InRoomView({
|
|||
onPress={toggleScreensharing}
|
||||
/>
|
||||
)}
|
||||
<OverflowMenu roomUrl={window.location.href} />
|
||||
<OverflowMenu
|
||||
roomUrl={window.location.href}
|
||||
setShowInspector={setShowInspector}
|
||||
showInspector={showInspector}
|
||||
client={client}
|
||||
/>
|
||||
<HangupButton onPress={onLeave} />
|
||||
</div>
|
||||
<GroupCallInspector
|
||||
|
|
71
src/SelectInput.jsx
Normal file
71
src/SelectInput.jsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import React, { useRef } from "react";
|
||||
import { HiddenSelect, useSelect } from "@react-aria/select";
|
||||
import { useButton } from "@react-aria/button";
|
||||
import { useSelectState } from "@react-stately/select";
|
||||
import { Popover } from "./Popover";
|
||||
import { ListBox } from "./ListBox";
|
||||
import { useOverlayPosition } from "@react-aria/overlays";
|
||||
import styles from "./SelectInput.module.css";
|
||||
import classNames from "classnames";
|
||||
import { ReactComponent as ArrowDownIcon } from "./icons/ArrowDown.svg";
|
||||
|
||||
export function SelectInput(props) {
|
||||
const state = useSelectState(props);
|
||||
|
||||
const ref = useRef();
|
||||
const { labelProps, triggerProps, valueProps, menuProps } = useSelect(
|
||||
props,
|
||||
state,
|
||||
ref
|
||||
);
|
||||
|
||||
const { buttonProps } = useButton(triggerProps, ref);
|
||||
|
||||
const popoverRef = useRef();
|
||||
|
||||
const { overlayProps } = useOverlayPosition({
|
||||
targetRef: ref,
|
||||
overlayRef: popoverRef,
|
||||
placement: "bottom left",
|
||||
offset: 5,
|
||||
isOpen: state.isOpen,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.selectInput, props.className)}>
|
||||
<h4 {...labelProps} className={styles.label}>
|
||||
{props.label}
|
||||
</h4>
|
||||
<HiddenSelect
|
||||
state={state}
|
||||
triggerRef={ref}
|
||||
label={props.label}
|
||||
name={props.name}
|
||||
/>
|
||||
<button {...buttonProps} ref={ref} className={styles.selectTrigger}>
|
||||
<span {...valueProps} className={styles.selectedItem}>
|
||||
{state.selectedItem
|
||||
? state.selectedItem.rendered
|
||||
: "Select an option"}
|
||||
</span>
|
||||
<ArrowDownIcon />
|
||||
</button>
|
||||
{state.isOpen && (
|
||||
<Popover
|
||||
ref={popoverRef}
|
||||
isOpen={state.isOpen}
|
||||
onClose={state.close}
|
||||
className={styles.popover}
|
||||
{...overlayProps}
|
||||
>
|
||||
<ListBox
|
||||
{...menuProps}
|
||||
className
|
||||
state={state}
|
||||
optionClassName={styles.option}
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
45
src/SelectInput.module.css
Normal file
45
src/SelectInput.module.css
Normal file
|
@ -0,0 +1,45 @@
|
|||
.selectInput {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.selectTrigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
background-color: var(--bgColor1);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
font-size: 15px;
|
||||
color: var(--textColor1);
|
||||
height: 40px;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.selectedItem {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.popover {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.option:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.option:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
94
src/SettingsModal.jsx
Normal file
94
src/SettingsModal.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
import React from "react";
|
||||
import { Modal } from "./Modal";
|
||||
import styles from "./SettingsModal.module.css";
|
||||
import { TabContainer, TabItem } from "./Tabs";
|
||||
import { ReactComponent as AudioIcon } from "./icons/Audio.svg";
|
||||
import { ReactComponent as VideoIcon } from "./icons/Video.svg";
|
||||
import { ReactComponent as DeveloperIcon } from "./icons/Developer.svg";
|
||||
import { SelectInput } from "./SelectInput";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { useMediaHandler } from "./useMediaHandler";
|
||||
import { FieldRow, InputField } from "./Input";
|
||||
|
||||
export function SettingsModal({
|
||||
client,
|
||||
setShowInspector,
|
||||
showInspector,
|
||||
...rest
|
||||
}) {
|
||||
const {
|
||||
audioInput,
|
||||
audioInputs,
|
||||
setAudioInput,
|
||||
videoInput,
|
||||
videoInputs,
|
||||
setVideoInput,
|
||||
} = useMediaHandler(client);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Settings"
|
||||
isDismissable
|
||||
className={styles.settingsModal}
|
||||
{...rest}
|
||||
>
|
||||
<TabContainer className={styles.tabContainer}>
|
||||
<TabItem
|
||||
title={
|
||||
<>
|
||||
<AudioIcon width={16} height={16} />
|
||||
<span>Audio</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SelectInput
|
||||
label="Microphone"
|
||||
selectedKey={audioInput}
|
||||
onSelectionChange={setAudioInput}
|
||||
>
|
||||
{audioInputs.map(({ deviceId, label }) => (
|
||||
<Item key={deviceId}>{label}</Item>
|
||||
))}
|
||||
</SelectInput>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
title={
|
||||
<>
|
||||
<VideoIcon width={16} height={16} />
|
||||
<span>Video</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SelectInput
|
||||
label="Webcam"
|
||||
selectedKey={videoInput}
|
||||
onSelectionChange={setVideoInput}
|
||||
>
|
||||
{videoInputs.map(({ deviceId, label }) => (
|
||||
<Item key={deviceId}>{label}</Item>
|
||||
))}
|
||||
</SelectInput>
|
||||
</TabItem>
|
||||
<TabItem
|
||||
title={
|
||||
<>
|
||||
<DeveloperIcon width={16} height={16} />
|
||||
<span>Developer</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="showInspector"
|
||||
name="inspector"
|
||||
label="Show Call Inspector"
|
||||
type="checkbox"
|
||||
checked={showInspector}
|
||||
onChange={(e) => setShowInspector(e.target.checked)}
|
||||
/>
|
||||
</FieldRow>
|
||||
</TabItem>
|
||||
</TabContainer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
8
src/SettingsModal.module.css
Normal file
8
src/SettingsModal.module.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
.settingsModal {
|
||||
width: 774px;
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
margin: 27px 16px;
|
||||
}
|
53
src/Tabs.jsx
Normal file
53
src/Tabs.jsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import React, { useRef } from "react";
|
||||
import { useTabList, useTab, useTabPanel } from "@react-aria/tabs";
|
||||
import { Item } from "@react-stately/collections";
|
||||
import { useTabListState } from "@react-stately/tabs";
|
||||
import styles from "./Tabs.module.css";
|
||||
import classNames from "classnames";
|
||||
|
||||
export function TabContainer(props) {
|
||||
const state = useTabListState(props);
|
||||
const ref = useRef();
|
||||
const { tabListProps } = useTabList(props, state, ref);
|
||||
return (
|
||||
<div className={classNames(styles.tabContainer, props.className)}>
|
||||
<ul {...tabListProps} ref={ref} className={styles.tabList}>
|
||||
{[...state.collection].map((item) => (
|
||||
<Tab key={item.key} item={item} state={state} />
|
||||
))}
|
||||
</ul>
|
||||
<TabPanel key={state.selectedItem?.key} state={state} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Tab({ item, state }) {
|
||||
const { key, rendered } = item;
|
||||
const ref = useRef();
|
||||
const { tabProps } = useTab({ key }, state, ref);
|
||||
|
||||
return (
|
||||
<li
|
||||
{...tabProps}
|
||||
ref={ref}
|
||||
className={classNames(styles.tab, {
|
||||
[styles.selected]: state.selectedKey === key,
|
||||
[styles.disabled]: state.disabledKeys.has(key),
|
||||
})}
|
||||
>
|
||||
{rendered}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function TabPanel({ state, ...props }) {
|
||||
const ref = useRef();
|
||||
const { tabPanelProps } = useTabPanel(props, state, ref);
|
||||
return (
|
||||
<div {...tabPanelProps} ref={ref} className={styles.tabPanel}>
|
||||
{state.selectedItem?.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const TabItem = Item;
|
59
src/Tabs.module.css
Normal file
59
src/Tabs.module.css
Normal file
|
@ -0,0 +1,59 @@
|
|||
.tabContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tabList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 190px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab > * {
|
||||
color: var(--textColor2);
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.tab svg * {
|
||||
fill: var(--textColor2);
|
||||
}
|
||||
|
||||
.tab > :last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.tab.selected {
|
||||
background-color: #0dbd8b;
|
||||
}
|
||||
|
||||
.tab.selected * {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tab.selected svg * {
|
||||
fill: #ffffff;
|
||||
}
|
||||
|
||||
.tab.disabled {
|
||||
}
|
||||
|
||||
.tabPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 0 40px;
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useCallback, useMemo } from "react";
|
||||
import { ButtonTooltip, HeaderButton } from "./RoomButton";
|
||||
import { Popover, PopoverMenu, PopoverMenuItem } from "./PopoverMenu";
|
||||
import { PopoverMenuTrigger } from "./PopoverMenu";
|
||||
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 { Item } from "@react-stately/collections";
|
||||
import { Menu } from "./Menu";
|
||||
|
||||
export function UserMenu({ userName, signedIn, onLogin, onLogout }) {
|
||||
const onAction = useCallback((value) => {
|
||||
|
@ -46,21 +48,21 @@ export function UserMenu({ userName, signedIn, onLogin, onLogout }) {
|
|||
}, [signedIn, userName]);
|
||||
|
||||
return (
|
||||
<PopoverMenu onAction={onAction} placement="bottom right">
|
||||
<PopoverMenuTrigger onAction={onAction} placement="bottom right">
|
||||
<HeaderButton className={styles.userButton}>
|
||||
<ButtonTooltip>Profile</ButtonTooltip>
|
||||
<UserIcon />
|
||||
</HeaderButton>
|
||||
{(props) => (
|
||||
<Popover {...props} label="User menu">
|
||||
<Menu {...props} label="User menu">
|
||||
{items.map(({ key, icon: Icon, label }) => (
|
||||
<PopoverMenuItem key={key} textValue={label}>
|
||||
<Item key={key} textValue={label}>
|
||||
<Icon />
|
||||
<span>{label}</span>
|
||||
</PopoverMenuItem>
|
||||
</Item>
|
||||
))}
|
||||
</Popover>
|
||||
</Menu>
|
||||
)}
|
||||
</PopoverMenu>
|
||||
</PopoverMenuTrigger>
|
||||
);
|
||||
}
|
||||
|
|
10
src/icons/ArrowDown.svg
Normal file
10
src/icons/ArrowDown.svg
Normal file
|
@ -0,0 +1,10 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_965_9448)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.70711 5.36321C2.09763 4.97268 2.73182 4.97166 3.1236 5.36091L8.08934 10.2946L13.0391 5.34488C13.4296 4.95435 14.0638 4.95333 14.4556 5.34258C14.8474 5.73184 14.8484 6.36398 14.4579 6.75451L8.80101 12.4114C8.41049 12.8019 7.7763 12.8029 7.38452 12.4137L1.70939 6.77513C1.3176 6.38587 1.31658 5.75373 1.70711 5.36321Z" fill="#8E99A4"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_965_9448">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 633 B |
5
src/icons/Audio.svg
Normal file
5
src/icons/Audio.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" 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="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="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"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
3
src/icons/Developer.svg
Normal file
3
src/icons/Developer.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 3C2.23858 3 0 5.23858 0 8C0 10.7614 2.23858 13 5 13H11C13.7614 13 16 10.7614 16 8C16 5.23858 13.7614 3 11 3H5ZM8 8C8 9.65685 6.65685 11 5 11C3.34315 11 2 9.65685 2 8C2 6.34315 3.34315 5 5 5C6.65685 5 8 6.34315 8 8Z" fill="#A9B2BC"/>
|
||||
</svg>
|
After Width: | Height: | Size: 388 B |
74
src/useMediaHandler.js
Normal file
74
src/useMediaHandler.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
let audioOutput;
|
||||
|
||||
export function useMediaHandler(client) {
|
||||
const [{ audioInput, videoInput, audioInputs, videoInputs }, setState] =
|
||||
useState(() => {
|
||||
const mediaHandler = client.getMediaHandler();
|
||||
|
||||
return {
|
||||
audioInput: mediaHandler.audioInput,
|
||||
videoInput: mediaHandler.videoInput,
|
||||
audioInputs: [],
|
||||
videoInputs: [],
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const mediaHandler = client.getMediaHandler();
|
||||
|
||||
function updateDevices() {
|
||||
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
||||
const audioInputs = devices.filter(
|
||||
(device) => device.kind === "audioinput"
|
||||
);
|
||||
const videoInputs = devices.filter(
|
||||
(device) => device.kind === "videoinput"
|
||||
);
|
||||
|
||||
setState((prevState) => ({
|
||||
audioInput: mediaHandler.audioInput,
|
||||
videoInput: mediaHandler.videoInput,
|
||||
audioInputs,
|
||||
videoInputs,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
updateDevices();
|
||||
|
||||
mediaHandler.on("local_streams_changed", updateDevices);
|
||||
navigator.mediaDevices.addEventListener("devicechange", updateDevices);
|
||||
|
||||
return () => {
|
||||
mediaHandler.removeListener("local_streams_changed", updateDevices);
|
||||
navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const setAudioInput = useCallback(
|
||||
(deviceId) => {
|
||||
setState((prevState) => ({ ...prevState, audioInput: deviceId }));
|
||||
client.getMediaHandler().setAudioInput(deviceId);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
const setVideoInput = useCallback(
|
||||
(deviceId) => {
|
||||
setState((prevState) => ({ ...prevState, videoInput: deviceId }));
|
||||
client.getMediaHandler().setVideoInput(deviceId);
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
return {
|
||||
audioInput,
|
||||
audioInputs,
|
||||
setAudioInput,
|
||||
videoInput,
|
||||
videoInputs,
|
||||
setVideoInput,
|
||||
};
|
||||
}
|
124
yarn.lock
124
yarn.lock
|
@ -337,6 +337,32 @@
|
|||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-aria/label@^3.2.1":
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/label/-/label-3.2.1.tgz#e6562259e6b17e3856c4c3e0060903cf705d094b"
|
||||
integrity sha512-QZ5/dpJKRjB1JtFZfOVd5GUiCpA2yMgmNA6ky6jT5XNAo7H14QqGRFUGDTLAQYGd+Bc3s+NayOT3NKUYur/3Xw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-types/label" "^3.5.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-aria/listbox@^3.4.0":
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/listbox/-/listbox-3.4.0.tgz#384f72f544540b53cb56f5734949aff6f3be723e"
|
||||
integrity sha512-Tc6JAPHrNKbjFMOCI50YHFBltSxBc84CaLIQdVo4c9KYiwgoAy1ULeSnRyp4ru3qpnffJZEUCNWD+864+MZVEQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/focus" "^3.4.1"
|
||||
"@react-aria/interactions" "^3.7.0"
|
||||
"@react-aria/label" "^3.2.1"
|
||||
"@react-aria/selection" "^3.7.0"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-stately/collections" "^3.3.3"
|
||||
"@react-stately/list" "^3.4.0"
|
||||
"@react-types/listbox" "^3.2.1"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-aria/menu@^3.3.0":
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/menu/-/menu-3.3.0.tgz#09364a306b3b0dec7f3cf532bfa184a1f4e26da7"
|
||||
|
@ -369,6 +395,25 @@
|
|||
"@react-types/overlays" "^3.5.1"
|
||||
dom-helpers "^3.3.1"
|
||||
|
||||
"@react-aria/select@^3.6.0":
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/select/-/select-3.6.0.tgz#d5750614de36af7b26c6d2285bf996647818f8cc"
|
||||
integrity sha512-jHLyeiy1iR1qaoFdJpQa2V7RL4Nb9JfVDNHNbgp8I5peoU+2oIN34NbqHTnsKlOBfhBRih2PnLogT2Iw4FZ3+Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/i18n" "^3.3.3"
|
||||
"@react-aria/interactions" "^3.7.0"
|
||||
"@react-aria/label" "^3.2.1"
|
||||
"@react-aria/listbox" "^3.4.0"
|
||||
"@react-aria/menu" "^3.3.0"
|
||||
"@react-aria/selection" "^3.7.0"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-aria/visually-hidden" "^3.2.3"
|
||||
"@react-stately/select" "^3.1.3"
|
||||
"@react-types/button" "^3.4.1"
|
||||
"@react-types/select" "^3.5.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-aria/selection@^3.7.0":
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/selection/-/selection-3.7.0.tgz#77dc483dca82303f20b8567bfac24a2e57bade6d"
|
||||
|
@ -390,6 +435,22 @@
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
|
||||
"@react-aria/tabs@^3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/tabs/-/tabs-3.1.0.tgz#e64b17a592610195466026d7866bde0b32784e69"
|
||||
integrity sha512-3mU8UclpGVI7muLVTXlJVgHn7RJ+eyBWxPzlgFPBR35acdBrhIt1hBv7PGTAhCg6Zj75CQr07BM/Kdi1WWOP5g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-aria/focus" "^3.5.0"
|
||||
"@react-aria/i18n" "^3.3.3"
|
||||
"@react-aria/interactions" "^3.7.0"
|
||||
"@react-aria/selection" "^3.7.0"
|
||||
"@react-aria/utils" "^3.10.0"
|
||||
"@react-stately/list" "^3.4.0"
|
||||
"@react-stately/tabs" "^3.0.1"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
"@react-types/tabs" "^3.0.1"
|
||||
|
||||
"@react-aria/utils@^3.10.0", "@react-aria/utils@^3.8.2", "@react-aria/utils@^3.9.0":
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-aria/utils/-/utils-3.10.0.tgz#2f6f0b0ccede17241fca1cbd76978e1bf8f5a2b0"
|
||||
|
@ -464,6 +525,17 @@
|
|||
"@babel/runtime" "^7.6.2"
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-stately/list@^3.3.0", "@react-stately/list@^3.4.0":
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/list/-/list-3.4.0.tgz#34ef0645f126efac14dbdcf80ca767dea7a87c3d"
|
||||
integrity sha512-nqVuECSySZU79lF53+YMCl+N1krCoulYNSIohSsply8MN54gJsNHKOKWnhIx05Cdw6hz1rHmdSJ+/sRHhVvBSw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-stately/collections" "^3.3.3"
|
||||
"@react-stately/selection" "^3.8.0"
|
||||
"@react-stately/utils" "^3.3.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-stately/menu@^3.2.3":
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/menu/-/menu-3.2.3.tgz#eb58e3cfc941d49637bac04aa474935f08bc7215"
|
||||
|
@ -484,6 +556,20 @@
|
|||
"@react-stately/utils" "^3.2.2"
|
||||
"@react-types/overlays" "^3.5.1"
|
||||
|
||||
"@react-stately/select@^3.1.3":
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/select/-/select-3.1.3.tgz#539340e9ccdf8d0b331d289f18f1bb5c81ab3655"
|
||||
integrity sha512-r0M2gcyyfo7vDDZGsOb64XQlVHtNQl+3mId3gYA46sHEu81C8Lhy4YSPZjItppnCLigBlm88hISl/i0e+XBx8g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-stately/collections" "^3.3.3"
|
||||
"@react-stately/list" "^3.3.0"
|
||||
"@react-stately/menu" "^3.2.3"
|
||||
"@react-stately/selection" "^3.7.0"
|
||||
"@react-stately/utils" "^3.2.2"
|
||||
"@react-types/select" "^3.3.1"
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-stately/selection@^3.7.0", "@react-stately/selection@^3.8.0":
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/selection/-/selection-3.8.0.tgz#c5292d9b0e67cb48b1bfabd050da57949950e431"
|
||||
|
@ -494,6 +580,16 @@
|
|||
"@react-stately/utils" "^3.3.0"
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-stately/tabs@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/tabs/-/tabs-3.0.1.tgz#7df000f8c5c14b3bf973348c491e12bcfcafe49d"
|
||||
integrity sha512-XhF/5mt8eme3mu0+4nC7Du+e5OWSu0W8SeKfbH9JmTWTCayZpPtui68nRStJK6OkgHs28gA+j55RSsTqT/N1Fg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.6.2"
|
||||
"@react-stately/list" "^3.3.0"
|
||||
"@react-stately/utils" "^3.2.2"
|
||||
"@react-types/tabs" "^3.0.1"
|
||||
|
||||
"@react-stately/toggle@^3.2.3":
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@react-stately/toggle/-/toggle-3.2.3.tgz#a4de6edc16982990492c6c557e5194f46dacc809"
|
||||
|
@ -544,6 +640,20 @@
|
|||
"@react-types/overlays" "^3.5.1"
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-types/label@^3.5.0":
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/label/-/label-3.5.0.tgz#c7093871f42c62e1b5523f61a0856a2f58d4cf2a"
|
||||
integrity sha512-a9lpQUyV4XwsZv0gV1jPjPWicSSa+DRliuXLTwORirxNLF0kMk89DLYf0a9CZhiEniJYqoqR3laJDvLAFW1x/Q==
|
||||
dependencies:
|
||||
"@react-types/shared" "^3.9.0"
|
||||
|
||||
"@react-types/listbox@^3.2.1":
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/listbox/-/listbox-3.2.1.tgz#881bbc9690343f35fe08a99526a90618f53328bf"
|
||||
integrity sha512-uBYx5BgL8gyH62UhSXAFyFDwAD4ALcK5gjOk+p/vWsFm0vvmtutALkb3yYjDQvwdI89pSZDjN4j7QChCmlNcmQ==
|
||||
dependencies:
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-types/menu@^3.3.0":
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/menu/-/menu-3.4.1.tgz#42f58ce3b79b844441627c5cd3126705b3b00063"
|
||||
|
@ -559,11 +669,25 @@
|
|||
dependencies:
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@react-types/select@^3.3.1", "@react-types/select@^3.5.0":
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/select/-/select-3.5.0.tgz#50ae1e7b14925189ea45c8322a776fd40473d31e"
|
||||
integrity sha512-XdLS/kvvlOZbVP/wn8tX5iAL0kpND3ZSea8KXG3EkwIw8sn1xcd8tYx7TkdF89IdNbg2pmzn9stOv9RUbC8MoQ==
|
||||
dependencies:
|
||||
"@react-types/shared" "^3.10.0"
|
||||
|
||||
"@react-types/shared@^3.10.0", "@react-types/shared@^3.8.0", "@react-types/shared@^3.9.0":
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.10.0.tgz#bdafed2ebcd31149c178312252dda0babde316d0"
|
||||
integrity sha512-B1gTRpE5qkSpfGxw8BHeOwvBPP3gnfKnzPHV0FJQHtgJ46oJS64WyloDAp1D9cLVsFHaI6s/HviXL51kVce2ww==
|
||||
|
||||
"@react-types/tabs@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-types/tabs/-/tabs-3.0.1.tgz#a32931e95303d4442e51d4c687d4e154654014f6"
|
||||
integrity sha512-GvPVU9GAqImHFhU+Do+pdGK/vZA4kqA699Gly1V95DUmtdG3GSwTnwlvM/Sy80/F9fKZDGokZnQmBFo8MFZyIw==
|
||||
dependencies:
|
||||
"@react-types/shared" "^3.8.0"
|
||||
|
||||
"@sentry/browser@6.13.3":
|
||||
version "6.13.3"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.13.3.tgz#d4511791b1e484ad48785eba3bce291fdf115c1e"
|
||||
|
|
Loading…
Reference in a new issue