diff --git a/package.json b/package.json index 4bdcbbb..0e3d1f3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.jsx b/src/App.jsx index 52150b8..026d24c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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 ( - <> + {loading ? (

Loading...

@@ -74,7 +75,7 @@ export default function App() { )} - + ); } diff --git a/src/GridLayoutMenu.jsx b/src/GridLayoutMenu.jsx index 57eee1f..a5d5665 100644 --- a/src/GridLayoutMenu.jsx +++ b/src/GridLayoutMenu.jsx @@ -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 ( - + Layout Type {layout === "spotlight" ? : } {(props) => ( - - + + Freedom {layout === "freedom" && } - - + + Spotlight {layout === "spotlight" && ( )} - - + + )} - + ); } diff --git a/src/InviteModal.jsx b/src/InviteModal.jsx index 7986896..18c35ac 100644 --- a/src/InviteModal.jsx +++ b/src/InviteModal.jsx @@ -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 ( - - - Add User - - - {(modalProps) => ( - - -

Copy and share this meeting link

- -
-
- )} -
+ + +

Copy and share this meeting link

+ +
+
); } diff --git a/src/ListBox.jsx b/src/ListBox.jsx new file mode 100644 index 0000000..478b6f0 --- /dev/null +++ b/src/ListBox.jsx @@ -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 ( +
    + {[...state.collection].map((item) => ( +
+ ); +} + +function Option({ item, state, className }) { + const ref = useRef(); + const { optionProps, isSelected, isFocused, isDisabled } = useOption( + { key: item.key }, + state, + ref + ); + + return ( +
  • + {item.rendered} +
  • + ); +} diff --git a/src/ListBox.module.css b/src/ListBox.module.css new file mode 100644 index 0000000..060ef09 --- /dev/null +++ b/src/ListBox.module.css @@ -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); +} diff --git a/src/Menu.jsx b/src/Menu.jsx new file mode 100644 index 0000000..260db87 --- /dev/null +++ b/src/Menu.jsx @@ -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 ( +
      + {[...state.collection].map((item) => ( + + ))} +
    + ); +} + +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 ( +
  • + {item.rendered} +
  • + ); +} diff --git a/src/Menu.module.css b/src/Menu.module.css new file mode 100644 index 0000000..091dec1 --- /dev/null +++ b/src/Menu.module.css @@ -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; +} diff --git a/src/Modal.jsx b/src/Modal.jsx index 25fdb2f..4423975 100644 --- a/src/Modal.jsx +++ b/src/Modal.jsx @@ -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)} >

    {title}

    @@ -60,3 +61,68 @@ export function ModalContent({ children, className, ...rest }) {
    ); } + +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 ( + <> + + {modalState.isOpen && modal(modalProps)} + + ); +} diff --git a/src/OverflowMenu.jsx b/src/OverflowMenu.jsx index 28418aa..16b65e9 100644 --- a/src/OverflowMenu.jsx +++ b/src/OverflowMenu.jsx @@ -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 ( - - - More - - - {(props) => ( - - - - Invite people - - - - Settings - - + <> + + + More + + + {(props) => ( + + + + Invite people + + + + Settings + + + )} + + {settingsModalState.isOpen && ( + )} - + {inviteModalState.isOpen && ( + + )} + ); } diff --git a/src/Overlay.jsx b/src/Overlay.jsx deleted file mode 100644 index 499c4f7..0000000 --- a/src/Overlay.jsx +++ /dev/null @@ -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 ( - <> - - {overlayState.isOpen && - overlay({ isOpen: overlayState.isOpen, onClose: overlayState.close })} - - ); -} diff --git a/src/Popover.jsx b/src/Popover.jsx new file mode 100644 index 0000000..314d134 --- /dev/null +++ b/src/Popover.jsx @@ -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 ( + + +
    + {children} + +
    +
    +
    + ); + } +); diff --git a/src/Popover.module.css b/src/Popover.module.css new file mode 100644 index 0000000..c84777d --- /dev/null +++ b/src/Popover.module.css @@ -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; +} diff --git a/src/PopoverMenu.jsx b/src/PopoverMenu.jsx index 5c1e6cb..e758038 100644 --- a/src/PopoverMenu.jsx +++ b/src/PopoverMenu.jsx @@ -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 ( -
    +
    - {popoverMenuState.isOpen && - popover({ - isOpen: popoverMenuState.isOpen, - onClose: popoverMenuState.close, - autoFocus: popoverMenuState.focusStrategy, - domProps: menuProps, - ref: popoverRef, - positionProps, - ...rest, - })} + {popoverMenuState.isOpen && ( + + {popoverMenu({ + ...menuProps, + autoFocus: popoverMenuState.focusStrategy, + onClose: popoverMenuState.close, + })} + + )}
    ); } - -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 ( - - -
    - -
      - {[...state.collection].map((item) => ( - - ))} -
    - -
    -
    -
    - ); -}); - -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 ( -
  • - {item.rendered} -
  • - ); -} - -export const PopoverMenuItem = Item; diff --git a/src/PopoverMenu.module.css b/src/PopoverMenu.module.css index 241f7b8..f0efdc9 100644 --- a/src/PopoverMenu.module.css +++ b/src/PopoverMenu.module.css @@ -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; } diff --git a/src/Room.jsx b/src/Room.jsx index c39b367..c427312 100644 --- a/src/Room.jsx +++ b/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} /> )} - +
    +

    + {props.label} +

    + + + {state.isOpen && ( + + + + )} + + ); +} diff --git a/src/SelectInput.module.css b/src/SelectInput.module.css new file mode 100644 index 0000000..4f048d6 --- /dev/null +++ b/src/SelectInput.module.css @@ -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; +} diff --git a/src/SettingsModal.jsx b/src/SettingsModal.jsx new file mode 100644 index 0000000..7525697 --- /dev/null +++ b/src/SettingsModal.jsx @@ -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 ( + + + + + Audio + + } + > + + {audioInputs.map(({ deviceId, label }) => ( + {label} + ))} + + + + + Video + + } + > + + {videoInputs.map(({ deviceId, label }) => ( + {label} + ))} + + + + + Developer + + } + > + + setShowInspector(e.target.checked)} + /> + + + + + ); +} diff --git a/src/SettingsModal.module.css b/src/SettingsModal.module.css new file mode 100644 index 0000000..86cd1c0 --- /dev/null +++ b/src/SettingsModal.module.css @@ -0,0 +1,8 @@ +.settingsModal { + width: 774px; + height: 480px; +} + +.tabContainer { + margin: 27px 16px; +} diff --git a/src/Tabs.jsx b/src/Tabs.jsx new file mode 100644 index 0000000..e7c511a --- /dev/null +++ b/src/Tabs.jsx @@ -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 ( +
    +
      + {[...state.collection].map((item) => ( + + ))} +
    + +
    + ); +} + +function Tab({ item, state }) { + const { key, rendered } = item; + const ref = useRef(); + const { tabProps } = useTab({ key }, state, ref); + + return ( +
  • + {rendered} +
  • + ); +} + +function TabPanel({ state, ...props }) { + const ref = useRef(); + const { tabPanelProps } = useTabPanel(props, state, ref); + return ( +
    + {state.selectedItem?.props.children} +
    + ); +} + +export const TabItem = Item; diff --git a/src/Tabs.module.css b/src/Tabs.module.css new file mode 100644 index 0000000..289c44f --- /dev/null +++ b/src/Tabs.module.css @@ -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; +} diff --git a/src/UserMenu.jsx b/src/UserMenu.jsx index e774091..b5a5851 100644 --- a/src/UserMenu.jsx +++ b/src/UserMenu.jsx @@ -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 ( - + Profile {(props) => ( - + {items.map(({ key, icon: Icon, label }) => ( - + {label} - + ))} - + )} - + ); } diff --git a/src/icons/ArrowDown.svg b/src/icons/ArrowDown.svg new file mode 100644 index 0000000..37713a2 --- /dev/null +++ b/src/icons/ArrowDown.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/icons/Audio.svg b/src/icons/Audio.svg new file mode 100644 index 0000000..f541ebc --- /dev/null +++ b/src/icons/Audio.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/Developer.svg b/src/icons/Developer.svg new file mode 100644 index 0000000..7d1a095 --- /dev/null +++ b/src/icons/Developer.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/useMediaHandler.js b/src/useMediaHandler.js new file mode 100644 index 0000000..ad42f30 --- /dev/null +++ b/src/useMediaHandler.js @@ -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, + }; +} diff --git a/yarn.lock b/yarn.lock index 19e03c9..323d666 100644 --- a/yarn.lock +++ b/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"