diff --git a/lang/main-es.json b/lang/main-es.json index 172418910a32..0936d1587675 100644 --- a/lang/main-es.json +++ b/lang/main-es.json @@ -715,6 +715,11 @@ "copyLink": "Copiar enlace", "meetingFull": "La reunión está llena", "participants": "participantes" + }, + "settings": { + "video": { + "videoInput": "Entrada de vídeo" + } } }, "notify": { @@ -1417,7 +1422,7 @@ "grantModerator": "Convertir en moderador", "hideSelfView": "Esconder vista propia", "kick": "Expulsar", - "mirrorVideo": "Espejar mi video", + "mirrorVideo": "Espejar mi vídeo", "moderator": "Moderador", "mute": "Se silenció el participante", "muted": "Silenciado", diff --git a/lang/main.json b/lang/main.json index daef743d64ad..5a6ad53d365d 100644 --- a/lang/main.json +++ b/lang/main.json @@ -768,6 +768,11 @@ "copyLink": "Copy Link", "meetingFull": "Meeting is full", "participants": "participants" + }, + "settings": { + "video": { + "videoInput": "Video input" + } } }, "notify": { diff --git a/package-lock.json b/package-lock.json index d1a1c71e5918..b23c8a319648 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@internxt/css-config": "^1.0.2", "@internxt/lib": "^1.2.1", "@internxt/sdk": "^1.4.77", - "@internxt/ui": "^0.0.21", + "@internxt/ui": "^0.0.22", "@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.17/jitsi-excalidraw-0.0.17.tgz", "@jitsi/js-utils": "2.2.1", "@jitsi/logger": "2.0.2", @@ -3450,9 +3450,10 @@ } }, "node_modules/@internxt/ui": { - "version": "0.0.21", - "resolved": "https://npm.pkg.github.com/download/@internxt/ui/0.0.21/d20ae174ae6201833a21792270d17f1cd827c1a9", - "integrity": "sha512-Mv0rVGi6EHSUUenynMxzpuEsU7d3DMMq+L1kNQ8W3NT1fYIQmVKhey7JVOCfy/JUbkS8dlrXRA5tX/MbvF3E1w==", + "version": "0.0.22", + "resolved": "https://npm.pkg.github.com/download/@internxt/ui/0.0.22/ef8af50986f75f58ef053bb8b3819754297b061d", + "integrity": "sha512-PkjO9pBl3U0vuTdEB+KI6YjnV0rPJoacW86vKpo7wrDRH264bT8cbPOBUcj3cBR2XaBqzNbDWyJCzcoy7jlgSw==", + "license": "MIT", "dependencies": { "@internxt/css-config": "^1.0.2", "@phosphor-icons/react": "^2.1.7", diff --git a/package.json b/package.json index e15d2b4b8c42..48689977ec2a 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@internxt/css-config": "^1.0.2", "@internxt/lib": "^1.2.1", "@internxt/sdk": "^1.4.77", - "@internxt/ui": "^0.0.21", + "@internxt/ui": "^0.0.22", "@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.17/jitsi-excalidraw-0.0.17.tgz", "@jitsi/js-utils": "2.2.1", "@jitsi/logger": "2.0.2", diff --git a/react/features/base/meet/general/components/MediaControls.tsx b/react/features/base/meet/general/components/MediaControls.tsx index 7a67c7be548d..782d9b644468 100644 --- a/react/features/base/meet/general/components/MediaControls.tsx +++ b/react/features/base/meet/general/components/MediaControls.tsx @@ -1,6 +1,6 @@ import { CircleButton } from "@internxt/ui"; import { ExclamationMark, Microphone, MicrophoneSlash, VideoCamera, VideoCameraSlash } from "@phosphor-icons/react"; -import React from "react"; +import React, { useState } from "react"; import MeetAudioSettingsPopUp from "../containers/MeetAudioSettingsPopup"; import CustomVideoSettingsPopUp from "../containers/MeetVideoSettingsPopUp"; @@ -31,6 +31,8 @@ const MediaControls: React.FC = ({ }) => { const audioIndicatorProps = !hasAudioPermissions ? indicatorProps : undefined; const videoIndicatorProps = !hasVideoPermissions ? indicatorProps : undefined; + const [isOpenVideo, setIsOpenVideo] = useState(false); + const [isOpenAudio, setIsOpenAudio] = useState(false); return (
@@ -41,6 +43,15 @@ const MediaControls: React.FC = ({ onClick={onVideoClick} onClickToggleButton={onVideoOptionsClick} dropdown={} + isOpen={isOpenVideo} + handleClose={() => { + setIsOpenVideo(false); + setIsOpenAudio(false); + }} + handleOpen={() => { + setIsOpenVideo(true); + setIsOpenAudio(false); + }} > {hasVideoPermissions && !isVideoMuted ? ( @@ -55,6 +66,15 @@ const MediaControls: React.FC = ({ onClick={onAudioClick} onClickToggleButton={onAudioOptionsClick} dropdown={} + isOpen={isOpenAudio} + handleClose={() => { + setIsOpenAudio(false); + setIsOpenVideo(false); + }} + handleOpen={() => { + setIsOpenAudio(true); + setIsOpenVideo(false); + }} > {isAudioMuted || !hasAudioPermissions ? ( diff --git a/react/features/base/meet/general/containers/MeetAudioSettingsPopup.tsx b/react/features/base/meet/general/containers/MeetAudioSettingsPopup.tsx index 5e3fa3bb2105..006d4a2536b5 100644 --- a/react/features/base/meet/general/containers/MeetAudioSettingsPopup.tsx +++ b/react/features/base/meet/general/containers/MeetAudioSettingsPopup.tsx @@ -26,8 +26,6 @@ interface MeetAudioSettingsPopUpProps { audioTrack?: any; } - - const MeetAudioSettingsPopUp = ({ currentMicDeviceId, currentOutputDeviceId, @@ -37,10 +35,8 @@ const MeetAudioSettingsPopUp = ({ setAudioOutputDevice, measureAudioLevels, }: MeetAudioSettingsPopUpProps) => { - - return ( -
+
{ return ( -
+
- parseFloat(styles.getPropertyValue(name)); +const getFloatStyleProperty = (styles: CSSStyleDeclaration, name: string) => parseFloat(styles.getPropertyValue(name)); /** -* Gets the outer height of an element, including margins. -* -* @param {Element} element - Target element. -* @returns {number} Computed height. -*/ + * Gets the outer height of an element, including margins. + * + * @param {Element} element - Target element. + * @returns {number} Computed height. + */ const getComputedOuterHeight = (element: HTMLElement) => { const computedStyle = getComputedStyle(element); - return element.offsetHeight - + getFloatStyleProperty(computedStyle, 'margin-top') - + getFloatStyleProperty(computedStyle, 'margin-bottom'); + return ( + element.offsetHeight + + getFloatStyleProperty(computedStyle, "margin-top") + + getFloatStyleProperty(computedStyle, "margin-bottom") + ); }; interface IProps { - /** * ARIA attributes. */ @@ -131,41 +129,41 @@ interface IProps { const MAX_HEIGHT = 400; -const useStyles = makeStyles()(theme => { +const useStyles = makeStyles()((theme) => { return { contextMenu: { backgroundColor: theme.palette.ui01, - border: `1px solid ${theme.palette.ui04}`, + //border: `1px solid ${theme.palette.ui04}`, borderRadius: `${Number(theme.shape.borderRadius)}px`, - boxShadow: '0px 1px 2px rgba(41, 41, 41, 0.25)', + boxShadow: "0px 1px 2px rgba(41, 41, 41, 0.25)", color: theme.palette.text01, ...withPixelLineHeight(theme.typography.bodyShortRegular), - marginTop: '48px', - position: 'absolute', + marginTop: "48px", + position: "absolute", right: `${participantsPaneTheme.panePadding}px`, top: 0, zIndex: 2, maxHeight: `${MAX_HEIGHT}px`, - overflowY: 'auto', - padding: `${theme.spacing(2)} 0` + overflowY: "auto", + padding: `${theme.spacing(2)} 0`, }, contextMenuHidden: { - pointerEvents: 'none', - visibility: 'hidden' + pointerEvents: "none", + visibility: "hidden", }, drawer: { - paddingTop: '16px', + paddingTop: "16px", - '& > div': { + "& > div": { ...withPixelLineHeight(theme.typography.bodyShortRegularLarge), - '& svg': { - fill: theme.palette.icon01 - } - } - } + "& svg": { + fill: theme.palette.icon01, + }, + }, + }, }; }); @@ -189,7 +187,7 @@ const ContextMenu = ({ tabIndex, ...aria }: IProps) => { - const [ isHidden, setIsHidden ] = useState(true); + const [isHidden, setIsHidden] = useState(true); const containerRef = useRef(null); const { classes: styles, cx } = useStyles(); const _overflowDrawer = useSelector(showOverflowDrawer); @@ -198,216 +196,207 @@ const ContextMenu = ({ if (_overflowDrawer) { return; } - if (entity && offsetTarget - && containerRef.current - && offsetTarget?.offsetParent - && offsetTarget.offsetParent instanceof HTMLElement + if ( + entity && + offsetTarget && + containerRef.current && + offsetTarget?.offsetParent && + offsetTarget.offsetParent instanceof HTMLElement ) { const { current: container } = containerRef; // make sure the max height is not set - container.style.maxHeight = 'none'; - const { offsetTop, offsetParent: { offsetHeight, scrollTop } } = offsetTarget; + container.style.maxHeight = "none"; + const { + offsetTop, + offsetParent: { offsetHeight, scrollTop }, + } = offsetTarget; let outerHeight = getComputedOuterHeight(container); let height = Math.min(MAX_HEIGHT, outerHeight); if (offsetTop + height > offsetHeight + scrollTop && height > offsetTop) { // top offset and + padding + border - container.style.maxHeight = `${offsetTop - ((spacing[2] * 2) + 2)}px`; + container.style.maxHeight = `${offsetTop - (spacing[2] * 2 + 2)}px`; } // get the height after style changes outerHeight = getComputedOuterHeight(container); height = Math.min(MAX_HEIGHT, outerHeight); - container.style.top = offsetTop + height > offsetHeight + scrollTop - ? `${offsetTop - outerHeight}` - : `${offsetTop}`; + container.style.top = + offsetTop + height > offsetHeight + scrollTop ? `${offsetTop - outerHeight}` : `${offsetTop}`; setIsHidden(false); } else { hidden === undefined && setIsHidden(true); } - }, [ entity, offsetTarget, _overflowDrawer ]); + }, [entity, offsetTarget, _overflowDrawer]); useEffect(() => { if (hidden !== undefined) { setIsHidden(hidden); } - }, [ hidden ]); + }, [hidden]); - const handleKeyDown = useCallback((event: KeyboardEvent) => { - const { current: listRef } = containerRef; - const currentFocusElement = document.activeElement; + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + const { current: listRef } = containerRef; + const currentFocusElement = document.activeElement; - const moveFocus = ( + const moveFocus = ( list: Element | null, currentFocus: Element | null, - traversalFunction: ( - list: Element | null, - currentFocus: Element | null - ) => Element | null - ) => { - let wrappedOnce = false; - let nextFocus = traversalFunction(list, currentFocus); - - /* eslint-disable no-unmodified-loop-condition */ - while (list && nextFocus) { - // Prevent infinite loop. - if (nextFocus === list.firstChild) { - if (wrappedOnce) { - return; + traversalFunction: (list: Element | null, currentFocus: Element | null) => Element | null + ) => { + let wrappedOnce = false; + let nextFocus = traversalFunction(list, currentFocus); + + /* eslint-disable no-unmodified-loop-condition */ + while (list && nextFocus) { + // Prevent infinite loop. + if (nextFocus === list.firstChild) { + if (wrappedOnce) { + return; + } + wrappedOnce = true; } - wrappedOnce = true; - } - // Same logic as useAutocomplete.js - const nextFocusDisabled - /* eslint-disable no-extra-parens */ - = (nextFocus as HTMLInputElement).disabled - || nextFocus.getAttribute('aria-disabled') === 'true'; + // Same logic as useAutocomplete.js + const nextFocusDisabled = + /* eslint-disable no-extra-parens */ + (nextFocus as HTMLInputElement).disabled || nextFocus.getAttribute("aria-disabled") === "true"; - if (!nextFocus.hasAttribute('tabindex') || nextFocusDisabled) { - // Move to the next element. - nextFocus = traversalFunction(list, nextFocus); - } else { - /* eslint-disable no-extra-parens */ - (nextFocus as HTMLElement).focus(); + if (!nextFocus.hasAttribute("tabindex") || nextFocusDisabled) { + // Move to the next element. + nextFocus = traversalFunction(list, nextFocus); + } else { + /* eslint-disable no-extra-parens */ + (nextFocus as HTMLElement).focus(); - return; + return; + } } - } - }; + }; + + const previousItem = (list: Element | null, item: Element | null): Element | null => { + /** + * To find the last child of the list. + * + * @param {Element | null} element - Element. + * @returns {Element | null} + */ + function lastChild(element: Element | null): Element | null { + while (element?.lastElementChild) { + /* eslint-disable no-param-reassign */ + element = element.lastElementChild; + } - const previousItem = ( - list: Element | null, - item: Element | null - ): Element | null => { - /** - * To find the last child of the list. - * - * @param {Element | null} element - Element. - * @returns {Element | null} - */ - function lastChild(element: Element | null): Element | null { - while (element?.lastElementChild) { - /* eslint-disable no-param-reassign */ - element = element.lastElementChild; + return element; } - return element; - } - - if (!list) { - return null; - } - if (list === item) { - return list.lastElementChild; - } - if (item?.previousElementSibling) { - return lastChild(item.previousElementSibling); - } - if (item && item?.parentElement !== list) { - return item.parentElement; - } + if (!list) { + return null; + } + if (list === item) { + return list.lastElementChild; + } + if (item?.previousElementSibling) { + return lastChild(item.previousElementSibling); + } + if (item && item?.parentElement !== list) { + return item.parentElement; + } - return lastChild(list.lastElementChild); - }; + return lastChild(list.lastElementChild); + }; - const nextItem = ( - list: Element | null, - item: Element | null - ): Element | null => { - if (!list) { - return null; - } + const nextItem = (list: Element | null, item: Element | null): Element | null => { + if (!list) { + return null; + } - if (list === item) { - return list.firstElementChild; - } - if (item?.firstElementChild) { - return item.firstElementChild; - } - if (item?.nextElementSibling) { - return item.nextElementSibling; - } - while (item && item.parentElement !== list) { - /* eslint-disable no-param-reassign */ - item = item.parentElement; + if (list === item) { + return list.firstElementChild; + } + if (item?.firstElementChild) { + return item.firstElementChild; + } if (item?.nextElementSibling) { return item.nextElementSibling; } - } - - return list?.firstElementChild; - }; - - if (event.key === 'Escape') { - // Close the menu - setIsHidden(true); - - } else if (event.key === 'ArrowUp') { - // Move focus to the previous menu item - event.preventDefault(); - moveFocus(listRef, currentFocusElement, previousItem); + while (item && item.parentElement !== list) { + /* eslint-disable no-param-reassign */ + item = item.parentElement; + if (item?.nextElementSibling) { + return item.nextElementSibling; + } + } - } else if (event.key === 'ArrowDown') { - // Move focus to the next menu item - event.preventDefault(); - moveFocus(listRef, currentFocusElement, nextItem); - } - }, [ containerRef ]); + return list?.firstElementChild; + }; + + if (event.key === "Escape") { + // Close the menu + setIsHidden(true); + } else if (event.key === "ArrowUp") { + // Move focus to the previous menu item + event.preventDefault(); + moveFocus(listRef, currentFocusElement, previousItem); + } else if (event.key === "ArrowDown") { + // Move focus to the next menu item + event.preventDefault(); + moveFocus(listRef, currentFocusElement, nextItem); + } + }, + [containerRef] + ); const removeFocus = useCallback(() => { onDrawerClose?.(); - }, [ onMouseLeave ]); + }, [onMouseLeave]); if (_overflowDrawer && inDrawer) { - return (
- {children} -
); + return ( +
+ {children} +
+ ); } - return _overflowDrawer - ? - -
+ return _overflowDrawer ? ( + + +
{children}
- : + enabled={activateFocusTrap && !isHidden} + onClickOutside={removeFocus} + onEscapeKey={removeFocus} + >
+ {...aria} + aria-label={accessibilityLabel} + className={cx(styles.contextMenu, isHidden && styles.contextMenuHidden, className)} + id={id} + onClick={onClick} + onKeyDown={onKeyDown ?? handleKeyDown} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + ref={containerRef} + role={role} + tabIndex={tabIndex} + > {children}
-
; + + ); }; export default ContextMenu; diff --git a/react/features/base/ui/components/web/ContextMenuItem.tsx b/react/features/base/ui/components/web/ContextMenuItem.tsx index e41352f95b3b..cbdf8f3ef0e1 100644 --- a/react/features/base/ui/components/web/ContextMenuItem.tsx +++ b/react/features/base/ui/components/web/ContextMenuItem.tsx @@ -1,16 +1,15 @@ -import React, { ReactNode, useCallback } from 'react'; -import { useSelector } from 'react-redux'; -import { makeStyles } from 'tss-react/mui'; +import React, { ReactNode, useCallback } from "react"; +import { useSelector } from "react-redux"; +import { makeStyles } from "tss-react/mui"; -import { showOverflowDrawer } from '../../../../toolbox/functions.web'; -import Icon from '../../../icons/components/Icon'; -import { withPixelLineHeight } from '../../../styles/functions.web'; -import { TEXT_OVERFLOW_TYPES } from '../../constants.any'; +import { showOverflowDrawer } from "../../../../toolbox/functions.web"; +import Icon from "../../../icons/components/Icon"; +import { withPixelLineHeight } from "../../../styles/functions.web"; +import { TEXT_OVERFLOW_TYPES } from "../../constants.any"; -import TextWithOverflow from './TextWithOverflow'; +import TextWithOverflow from "./TextWithOverflow"; export interface IProps { - /** * Label used for accessibility. */ @@ -85,7 +84,7 @@ export interface IProps { * If no onClick handler is provided, we assume the context menu item is * not interactive and no role will be set. */ - role?: 'tab' | 'button' | 'menuitem'; + role?: "tab" | "button" | "menuitem"; /** * Whether the item is marked as selected. @@ -108,79 +107,108 @@ export interface IProps { textClassName?: string; } -const useStyles = makeStyles()(theme => { +const useStyles = makeStyles()((theme) => { return { contextMenuItem: { - alignItems: 'center', - cursor: 'pointer', - display: 'flex', - minHeight: '40px', - padding: '10px 16px', - boxSizing: 'border-box', - - '& > *:not(:last-child)': { - marginRight: theme.spacing(3) + alignItems: "center", + cursor: "pointer", + display: "flex", + minHeight: "40px", + padding: "10px 16px", + boxSizing: "border-box", + + "& > *:not(:last-child)": { + marginRight: theme.spacing(3), }, - '&:hover': { - backgroundColor: theme.palette.ui02 + "&:hover": { + //backgroundColor: theme.palette.ui02, + borderRadius: theme.shape.borderRadius, + backgroundColor: "rgba(255, 255, 255, 0.15)", }, - '&:active': { - backgroundColor: theme.palette.ui03 + "&:active": { + backgroundColor: theme.palette.ui03, }, - '&.focus-visible': { - boxShadow: `inset 0 0 0 2px ${theme.palette.action01Hover}` - } + "&.focus-visible": { + boxShadow: `inset 0 0 0 2px ${theme.palette.action01Hover}`, + }, }, selected: { - borderLeft: `3px solid ${theme.palette.action01Hover}`, - paddingLeft: '13px', - backgroundColor: theme.palette.ui02 + //borderLeft: `3px solid ${theme.palette.action01Hover}`, + //paddingLeft: "13px", + //backgroundColor: theme.palette.ui02, + borderRadius: theme.shape.borderRadius, + backgroundColor: "white", + paddingLeft: "16px", + "&:hover": { + color: "white", + "& *": { color: "white" }, + "& svg": { fill: "white" }, + "& button *": { color: "black" }, + }, }, contextMenuItemDisabled: { - pointerEvents: 'none' + pointerEvents: "none", }, contextMenuItemIconDisabled: { - '& svg': { - fill: `${theme.palette.text03} !important` - } + "& svg": { + fill: `${theme.palette.text03} !important`, + }, }, contextMenuItemLabelDisabled: { color: theme.palette.text03, - '&:hover': { - background: 'none' + "&:hover": { + background: "none", }, - '& svg': { - fill: theme.palette.text03 - } + "& svg": { + fill: theme.palette.text03, + }, }, contextMenuItemDrawer: { - padding: '13px 16px' + padding: "13px 16px", }, contextMenuItemIcon: { - '& svg': { - fill: theme.palette.icon01 - } + "& svg": { + fill: theme.palette.icon01, + }, + }, + + contextMenuItemIconSelected: { + "& svg": { + fill: "black", + }, }, text: { ...withPixelLineHeight(theme.typography.bodyShortRegular), - color: theme.palette.text01 + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + color: theme.palette.text01, + width: "100%", + }, + textSelected: { + ...withPixelLineHeight(theme.typography.bodyShortRegular), + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + color: "black", + width: "100%", }, drawerText: { - ...withPixelLineHeight(theme.typography.bodyShortRegularLarge) - } + ...withPixelLineHeight(theme.typography.bodyShortRegularLarge), + }, }; }); @@ -198,70 +226,86 @@ const ContextMenuItem = ({ onKeyDown, onKeyPress, overflowType, - role = 'button', + role = "button", selected, testId, text, - textClassName }: IProps) => { + textClassName, +}: IProps) => { const { classes: styles, cx } = useStyles(); const _overflowDrawer: boolean = useSelector(showOverflowDrawer); const style = backgroundColor ? { backgroundColor } : {}; - const onKeyPressHandler = useCallback(e => { - // only trigger the fallback behavior (onClick) if we dont have any explicit keyboard event handler - if (onClick && !onKeyPress && !onKeyDown && (e.key === 'Enter' || e.key === ' ')) { - e.preventDefault(); - onClick(e); - } - - if (onKeyPress) { - onKeyPress(e); - } - }, [ onClick, onKeyPress, onKeyDown ]); + const onKeyPressHandler = useCallback( + (e) => { + // only trigger the fallback behavior (onClick) if we dont have any explicit keyboard event handler + if (onClick && !onKeyPress && !onKeyDown && (e.key === "Enter" || e.key === " ")) { + e.preventDefault(); + onClick(e); + } + + if (onKeyPress) { + onKeyPress(e); + } + }, + [onClick, onKeyPress, onKeyDown] + ); let tabIndex: undefined | 0 | -1; - if (role === 'tab') { + if (role === "tab") { tabIndex = selected ? 0 : -1; } - if (role === 'button' && !disabled) { + if (role === "button" && !disabled) { tabIndex = 0; } return (
- {customIcon ? customIcon - : icon && } + aria-controls={controls} + aria-disabled={disabled} + aria-label={accessibilityLabel} + aria-selected={role === "tab" ? selected : undefined} + className={cx( + styles.contextMenuItem, + _overflowDrawer && styles.contextMenuItemDrawer, + disabled && styles.contextMenuItemDisabled, + selected && styles.selected, + className + )} + data-testid={testId} + id={id} + key={text} + onClick={disabled ? undefined : onClick} + onKeyDown={disabled ? undefined : onKeyDown} + onKeyPress={disabled ? undefined : onKeyPressHandler} + role={onClick ? role : undefined} + style={style} + tabIndex={onClick ? tabIndex : undefined} + > + {customIcon + ? customIcon + : icon && ( + + )} {text && ( + className={cx( + selected ? styles.textSelected : styles.text, + _overflowDrawer && styles.drawerText, + disabled && styles.contextMenuItemLabelDisabled, + textClassName + )} + overflowType={overflowType} + > {text} )} diff --git a/react/features/base/ui/components/web/ContextMenuItemGroup.tsx b/react/features/base/ui/components/web/ContextMenuItemGroup.tsx index 6e54ee6d77ee..5e86fa11cfe5 100644 --- a/react/features/base/ui/components/web/ContextMenuItemGroup.tsx +++ b/react/features/base/ui/components/web/ContextMenuItemGroup.tsx @@ -1,11 +1,9 @@ -import React, { ReactNode } from 'react'; -import { makeStyles } from 'tss-react/mui'; - -import ContextMenuItem, { IProps as ItemProps } from './ContextMenuItem'; +import React, { ReactNode } from "react"; +import { makeStyles } from "tss-react/mui"; +import ContextMenuItem, { IProps as ItemProps } from "./ContextMenuItem"; interface IProps { - /** * List of actions in this group. */ @@ -17,41 +15,38 @@ interface IProps { children?: ReactNode; } -const useStyles = makeStyles()(theme => { +const useStyles = makeStyles()((theme) => { return { contextMenuItemGroup: { - '&:not(:empty)': { - padding: `${theme.spacing(2)} 0` + width: "95%", + margin: "0 auto", + "&:not(:empty)": { + padding: `${theme.spacing(2)} 0`, }, - '& + &:not(:empty)': { - borderTop: `1px solid ${theme.palette.ui03}` + "& + &:not(:empty)": { + borderTop: `1px solid ${theme.palette.ui03}`, }, - '&:first-of-type': { - paddingTop: 0 + "&:first-of-type": { + paddingTop: 0, }, - '&:last-of-type': { - paddingBottom: 0 - } - } + "&:last-of-type": { + paddingBottom: 0, + }, + }, }; }); -const ContextMenuItemGroup = ({ - actions, - children -}: IProps) => { +const ContextMenuItemGroup = ({ actions, children }: IProps) => { const { classes: styles } = useStyles(); return ( -
+
{children} - {actions?.map(actionProps => ( - + {actions?.map((actionProps) => ( + ))}
); diff --git a/react/features/settings/components/web/audio/AudioSettingsContent.tsx b/react/features/settings/components/web/audio/AudioSettingsContent.tsx index e29f260a0e23..baea1fd25dbe 100644 --- a/react/features/settings/components/web/audio/AudioSettingsContent.tsx +++ b/react/features/settings/components/web/audio/AudioSettingsContent.tsx @@ -1,24 +1,24 @@ /* eslint-disable react/no-multi-comp */ -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { connect } from 'react-redux'; -import { makeStyles } from 'tss-react/mui'; - -import { IReduxState, IStore } from '../../../../app/types'; -import { IconMic, IconVolumeUp } from '../../../../base/icons/svg'; -import JitsiMeetJS from '../../../../base/lib-jitsi-meet'; -import { equals } from '../../../../base/redux/functions'; -import Checkbox from '../../../../base/ui/components/web/Checkbox'; -import ContextMenu from '../../../../base/ui/components/web/ContextMenu'; -import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem'; -import ContextMenuItemGroup from '../../../../base/ui/components/web/ContextMenuItemGroup'; -import { toggleNoiseSuppression } from '../../../../noise-suppression/actions'; -import { isNoiseSuppressionEnabled } from '../../../../noise-suppression/functions'; -import { isPrejoinPageVisible } from '../../../../prejoin/functions'; -import { createLocalAudioTracks } from '../../../functions.web'; - -import MicrophoneEntry from './MicrophoneEntry'; -import SpeakerEntry from './SpeakerEntry'; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { makeStyles } from "tss-react/mui"; + +import { IReduxState, IStore } from "../../../../app/types"; +import { IconMic, IconVolumeUp } from "../../../../base/icons/svg"; +import JitsiMeetJS from "../../../../base/lib-jitsi-meet"; +import { equals } from "../../../../base/redux/functions"; +import Checkbox from "../../../../base/ui/components/web/Checkbox"; +import ContextMenu from "../../../../base/ui/components/web/ContextMenu"; +import ContextMenuItem from "../../../../base/ui/components/web/ContextMenuItem"; +import ContextMenuItemGroup from "../../../../base/ui/components/web/ContextMenuItemGroup"; +import { toggleNoiseSuppression } from "../../../../noise-suppression/actions"; +import { isNoiseSuppressionEnabled } from "../../../../noise-suppression/functions"; +import { isPrejoinPageVisible } from "../../../../prejoin/functions"; +import { createLocalAudioTracks } from "../../../functions.web"; + +import MicrophoneEntry from "./MicrophoneEntry"; +import SpeakerEntry from "./SpeakerEntry"; const browser = JitsiMeetJS.util.browser; @@ -31,33 +31,30 @@ const browser = JitsiMeetJS.util.browser; * @returns {string} */ function transformDefaultDeviceLabel(deviceId: string, label: string, t: Function) { - return deviceId === 'default' - ? t('settings.sameAsSystem', { label: label.replace('Default - ', '') }) - : label; + return deviceId === "default" ? t("settings.sameAsSystem", { label: label.replace("Default - ", "") }) : label; } export interface IProps { - /** - * The deviceId of the microphone in use. - */ + * The deviceId of the microphone in use. + */ currentMicDeviceId: string; /** - * The deviceId of the output device in use. - */ + * The deviceId of the output device in use. + */ currentOutputDeviceId?: string; /** - * Used to decide whether to measure audio levels for microphone devices. - */ + * Used to decide whether to measure audio levels for microphone devices. + */ measureAudioLevels: boolean; /** - * A list with objects containing the labels and deviceIds - * of all the input devices. - */ - microphoneDevices: Array<{ deviceId: string; label: string; }>; + * A list with objects containing the labels and deviceIds + * of all the input devices. + */ + microphoneDevices: Array<{ deviceId: string; label: string }>; /** * Whether noise suppression is enabled or not. @@ -65,10 +62,10 @@ export interface IProps { noiseSuppressionEnabled: boolean; /** - * A list of objects containing the labels and deviceIds - * of all the output devices. - */ - outputDevices: Array<{ deviceId: string; label: string; }>; + * A list of objects containing the labels and deviceIds + * of all the output devices. + */ + outputDevices: Array<{ deviceId: string; label: string }>; /** * Whether the prejoin page is visible or not. @@ -76,13 +73,13 @@ export interface IProps { prejoinVisible: boolean; /** - * Used to set a new microphone as the current one. - */ + * Used to set a new microphone as the current one. + */ setAudioInputDevice: Function; /** - * Used to set a new output device as the current one. - */ + * Used to set a new output device as the current one. + */ setAudioOutputDevice: Function; /** @@ -91,34 +88,34 @@ export interface IProps { toggleSuppression: () => void; } -const useStyles = makeStyles()(theme => { +const useStyles = makeStyles()((theme) => { return { contextMenu: { - position: 'relative', - right: 'auto', + position: "relative", + right: "auto", margin: 0, marginBottom: theme.spacing(1), - maxHeight: 'calc(100dvh - 100px)', - overflow: 'auto', - width: '300px' + maxHeight: "calc(100dvh - 100px)", + overflow: "auto", + width: "300px", }, header: { - '&:hover': { - backgroundColor: 'initial', - cursor: 'initial' - } + "&:hover": { + backgroundColor: "initial", + cursor: "initial", + }, }, list: { margin: 0, padding: 0, - listStyleType: 'none' + listStyleType: "none", }, checkboxContainer: { - padding: '10px 16px' - } + padding: "10px 16px", + }, }; }); @@ -132,20 +129,22 @@ const AudioSettingsContent = ({ prejoinVisible, setAudioInputDevice, setAudioOutputDevice, - toggleSuppression + toggleSuppression, }: IProps) => { const _componentWasUnmounted = useRef(false); - const microphoneHeaderId = 'microphone_settings_header'; - const speakerHeaderId = 'speaker_settings_header'; + const microphoneHeaderId = "microphone_settings_header"; + const speakerHeaderId = "speaker_settings_header"; const { classes } = useStyles(); - const [ audioTracks, setAudioTracks ] = useState(microphoneDevices.map(({ deviceId, label }) => { - return { - deviceId, - hasError: false, - jitsiTrack: null, - label - }; - })); + const [audioTracks, setAudioTracks] = useState( + microphoneDevices.map(({ deviceId, label }) => { + return { + deviceId, + hasError: false, + jitsiTrack: null, + label, + }; + }) + ); const microphoneDevicesRef = useRef(microphoneDevices); const { t } = useTranslation(); @@ -155,9 +154,12 @@ const AudioSettingsContent = ({ * @param {string} deviceId - The deviceId for the clicked microphone. * @returns {void} */ - const _onMicrophoneEntryClick = useCallback((deviceId: string) => { - setAudioInputDevice(deviceId); - }, [ setAudioInputDevice ]); + const _onMicrophoneEntryClick = useCallback( + (deviceId: string) => { + setAudioInputDevice(deviceId); + }, + [setAudioInputDevice] + ); /** * Click handler for the speaker entries. @@ -165,9 +167,12 @@ const AudioSettingsContent = ({ * @param {string} deviceId - The deviceId for the clicked speaker. * @returns {void} */ - const _onSpeakerEntryClick = useCallback((deviceId: string) => { - setAudioOutputDevice(deviceId); - }, [ setAudioOutputDevice ]); + const _onSpeakerEntryClick = useCallback( + (deviceId: string) => { + setAudioOutputDevice(deviceId); + }, + [setAudioOutputDevice] + ); /** * Renders a single microphone entry. @@ -177,23 +182,27 @@ const AudioSettingsContent = ({ * @param {length} length - The length of the microphone list. * @returns {React$Node} */ - const _renderMicrophoneEntry = (data: { deviceId: string; hasError: boolean; jitsiTrack: any; label: string; }, - index: number, length: number) => { + const _renderMicrophoneEntry = ( + data: { deviceId: string; hasError: boolean; jitsiTrack: any; label: string }, + index: number, + length: number + ) => { const { deviceId, jitsiTrack, hasError } = data; const label = transformDefaultDeviceLabel(deviceId, data.label, t); const isSelected = deviceId === currentMicDeviceId; return ( + deviceId={deviceId} + hasError={hasError} + index={index} + isSelected={isSelected} + jitsiTrack={jitsiTrack} + key={`me-${index}`} + length={length} + measureAudioLevels={measureAudioLevels} + onClick={_onMicrophoneEntryClick} + > {label} ); @@ -207,7 +216,7 @@ const AudioSettingsContent = ({ * @param {length} length - The length of the speaker list. * @returns {React$Node} */ - const _renderSpeakerEntry = (data: { deviceId: string; label: string; }, index: number, length: number) => { + const _renderSpeakerEntry = (data: { deviceId: string; label: string }, index: number, length: number) => { const { deviceId } = data; const label = transformDefaultDeviceLabel(deviceId, data.label, t); const key = `se-${index}`; @@ -215,12 +224,13 @@ const AudioSettingsContent = ({ return ( + deviceId={deviceId} + index={index} + isSelected={isSelected} + key={key} + length={length} + onClick={_onSpeakerEntryClick} + > {label} ); @@ -232,7 +242,7 @@ const AudioSettingsContent = ({ * @param {Object} tracks - The object holding the audio tracks. * @returns {void} */ - const _disposeTracks = (tracks: Array<{ jitsiTrack: any; }>) => { + const _disposeTracks = (tracks: Array<{ jitsiTrack: any }>) => { tracks.forEach(({ jitsiTrack }) => { jitsiTrack?.dispose(); }); @@ -245,7 +255,6 @@ const AudioSettingsContent = ({ */ const _setTracks = async () => { if (browser.isWebKitBased()) { - // It appears that at the time of this writing, creating audio tracks blocks the browser's main thread for // long time on safari. Wasn't able to confirm which part of track creation does the blocking exactly, but // not creating the tracks seems to help and makes the UI much more responsive. @@ -277,61 +286,58 @@ const AudioSettingsContent = ({ _setTracks(); microphoneDevicesRef.current = microphoneDevices; } - }, [ microphoneDevices ]); + }, [microphoneDevices]); return (