From 8836669c9f4cb3ba4710968150bf720fad8c3ac1 Mon Sep 17 00:00:00 2001 From: Robert Pintilii Date: Thu, 13 Apr 2023 16:41:16 +0300 Subject: [PATCH] ref(audio-picker) Styles refactor (#13193) Move styles from SCSS to JSS Convert components from class to function --- css/_audio-preview.scss | 100 ------ css/main.scss | 1 - .../components/AbstractRecentList.tsx | 4 +- .../web/audio/AudioSettingsContent.tsx | 330 +++++++++--------- .../web/audio/AudioSettingsPopup.tsx | 13 +- .../components/web/audio/MicrophoneEntry.tsx | 249 ++++++------- .../components/web/audio/SpeakerEntry.tsx | 44 ++- 7 files changed, 327 insertions(+), 414 deletions(-) delete mode 100644 css/_audio-preview.scss diff --git a/css/_audio-preview.scss b/css/_audio-preview.scss deleted file mode 100644 index 782a852c58..0000000000 --- a/css/_audio-preview.scss +++ /dev/null @@ -1,100 +0,0 @@ -.audio-preview { - display: inline-block; - - &-content { - position: relative; - right: auto; - margin: 0; - margin-bottom: 4px; - max-height: calc(100vh - 100px); - overflow: auto; - width: 300px; - - &-ul { - margin:0; - padding:0; - list-style-type: none; - } - } - - &-header:hover { - background-color: initial; - cursor: initial; - } - - &-entry-text { - max-width: 213px; - - &.left-margin { - margin-left: 36px; - } - } - - &-speaker { - position: relative; - - &:hover, &:focus-within, &:focus { - .audio-preview-test-button { - display: inline-block; - } - - .audio-preview-entry-text { - max-width: 178px; - margin-right: 0; - } - } - - .audio-preview-entry-text { - max-width: 238px; - } - } - - &-microphone { - position: relative; - - &--nometer { - .audio-preview-entry-text { - max-width: 238px; - } - } - - &--withmeter { - .audio-preview-entry-text { - max-width: 178px; - } - } - - } - - &-icon { - border-radius: 50%; - display: inline-block; - width: 14px; - - &--exclamation { - margin-left: 6px; - - & svg { - fill: #E54B4B; - } - } - } - - &-test-button { - display: none; - padding: 4px 10px; - position: absolute; - right: 16px; - top: 6px; - } - - &-meter-mic { - position: absolute; - right: 16px; - top: 14px; - } - - &-checkbox-container { - padding: 10px 16px; - } -} diff --git a/css/main.scss b/css/main.scss index 24cb6a6536..8fdbd51cf0 100644 --- a/css/main.scss +++ b/css/main.scss @@ -71,7 +71,6 @@ $flagsImagePath: "../images/"; @import 'chrome-extension-banner'; @import 'settings-button'; @import 'meter'; -@import 'audio-preview'; @import 'premeeting/main'; @import 'modals/invite/invite_more'; @import 'modals/security/security'; diff --git a/react/features/recent-list/components/AbstractRecentList.tsx b/react/features/recent-list/components/AbstractRecentList.tsx index 03cdc354b3..efaeeb1ee8 100644 --- a/react/features/recent-list/components/AbstractRecentList.tsx +++ b/react/features/recent-list/components/AbstractRecentList.tsx @@ -70,11 +70,11 @@ export default class AbstractRecentList

extends AbstractPage

+ style = { styles.emptyListContainer as any }> + style = { styles.emptyListText as any }> { t('welcomepage.recentListEmpty') } diff --git a/react/features/settings/components/web/audio/AudioSettingsContent.tsx b/react/features/settings/components/web/audio/AudioSettingsContent.tsx index 6594893411..bb08c261d9 100644 --- a/react/features/settings/components/web/audio/AudioSettingsContent.tsx +++ b/react/features/settings/components/web/audio/AudioSettingsContent.tsx @@ -1,9 +1,10 @@ -import React, { Component } from 'react'; -import { WithTranslation } from 'react-i18next'; +/* 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 { translate } from '../../../../base/i18n/functions'; import { IconMic, IconVolumeUp } from '../../../../base/icons/svg'; import JitsiMeetJS from '../../../../base/lib-jitsi-meet'; import { equals } from '../../../../base/redux/functions'; @@ -35,7 +36,7 @@ function transformDefaultDeviceLabel(deviceId: string, label: string, t: Functio : label; } -export interface IProps extends WithTranslation { +export interface IProps { /** * The deviceId of the microphone in use. @@ -90,50 +91,63 @@ export interface IProps extends WithTranslation { toggleSuppression: () => void; } -interface IState { +const useStyles = makeStyles()(theme => { + return { + contextMenu: { + position: 'relative', + right: 'auto', + margin: 0, + marginBottom: theme.spacing(1), + maxHeight: 'calc(100vh - 100px)', + overflow: 'auto', + width: '300px' + }, - /** - * An list of objects, each containing the microphone label, audio track, device id - * and track error if the case. - */ - audioTracks: Array<{ deviceId: string; hasError: boolean; jitsiTrack: any; label: string; }>; -} + header: { + '&:hover': { + backgroundColor: 'initial', + cursor: 'initial' + } + }, -/** - * Implements a React {@link Component} which displays a list of all - * the audio input & output devices to choose from. - * - * @augments Component - */ -class AudioSettingsContent extends Component { - _componentWasUnmounted: boolean; - microphoneHeaderId = 'microphone_settings_header'; - speakerHeaderId = 'speaker_settings_header'; + list: { + margin: 0, + padding: 0, + listStyleType: 'none' + }, + checkboxContainer: { + padding: '10px 16px' + } + }; +}); - /** - * Initializes a new {@code AudioSettingsContent} instance. - * - * @param {Object} props - The read-only properties with which the new - * instance is to be initialized. - */ - constructor(props: IProps) { - super(props); - - this._onMicrophoneEntryClick = this._onMicrophoneEntryClick.bind(this); - this._onSpeakerEntryClick = this._onSpeakerEntryClick.bind(this); - - this.state = { - audioTracks: props.microphoneDevices.map(({ deviceId, label }) => { - return { - deviceId, - hasError: false, - jitsiTrack: null, - label - }; - }) +const AudioSettingsContent = ({ + currentMicDeviceId, + currentOutputDeviceId, + measureAudioLevels, + microphoneDevices, + noiseSuppressionEnabled, + outputDevices, + prejoinVisible, + setAudioInputDevice, + setAudioOutputDevice, + toggleSuppression +}: IProps) => { + const _componentWasUnmounted = useRef(false); + 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 microphoneDevicesRef = useRef(microphoneDevices); + const { t } = useTranslation(); /** * Click handler for the microphone entries. @@ -141,9 +155,9 @@ class AudioSettingsContent extends Component { * @param {string} deviceId - The deviceId for the clicked microphone. * @returns {void} */ - _onMicrophoneEntryClick(deviceId: string) { - this.props.setAudioInputDevice(deviceId); - } + const _onMicrophoneEntryClick = useCallback((deviceId: string) => { + setAudioInputDevice(deviceId); + }, [ setAudioInputDevice ]); /** * Click handler for the speaker entries. @@ -151,9 +165,9 @@ class AudioSettingsContent extends Component { * @param {string} deviceId - The deviceId for the clicked speaker. * @returns {void} */ - _onSpeakerEntryClick(deviceId: string) { - this.props.setAudioOutputDevice(deviceId); - } + const _onSpeakerEntryClick = useCallback((deviceId: string) => { + setAudioOutputDevice(deviceId); + }, [ setAudioOutputDevice ]); /** * Renders a single microphone entry. @@ -161,14 +175,13 @@ class AudioSettingsContent extends Component { * @param {Object} data - An object with the deviceId, jitsiTrack & label of the microphone. * @param {number} index - The index of the element, used for creating a key. * @param {length} length - The length of the microphone list. - * @param {Function} t - The translation function. * @returns {React$Node} */ - _renderMicrophoneEntry(data: { deviceId: string; hasError: boolean; jitsiTrack: any; label: string; }, - index: number, length: number, t: Function) { + 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 === this.props.currentMicDeviceId; + const isSelected = deviceId === currentMicDeviceId; return ( { jitsiTrack = { jitsiTrack } key = { `me-${index}` } length = { length } - listHeaderId = { this.microphoneHeaderId } - measureAudioLevels = { this.props.measureAudioLevels } - onClick = { this._onMicrophoneEntryClick }> + listHeaderId = { microphoneHeaderId } + measureAudioLevels = { measureAudioLevels } + onClick = { _onMicrophoneEntryClick }> {label} ); - } + }; /** * Renders a single speaker entry. @@ -193,14 +206,13 @@ class AudioSettingsContent extends Component { * @param {Object} data - An object with the deviceId and label of the speaker. * @param {number} index - The index of the element, used for creating a key. * @param {length} length - The length of the speaker list. - * @param {Function} t - The translation function. * @returns {React$Node} */ - _renderSpeakerEntry(data: { deviceId: string; label: string; }, index: number, length: number, t: Function) { + 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}`; - const isSelected = deviceId === this.props.currentOutputDeviceId; + const isSelected = deviceId === currentOutputDeviceId; return ( { isSelected = { isSelected } key = { key } length = { length } - listHeaderId = { this.speakerHeaderId } - onClick = { this._onSpeakerEntryClick }> + listHeaderId = { speakerHeaderId } + onClick = { _onSpeakerEntryClick }> {label} ); - } + }; + + /** + * Disposes the audio tracks. + * + * @param {Object} tracks - The object holding the audio tracks. + * @returns {void} + */ + const _disposeTracks = (tracks: Array<{ jitsiTrack: any; }>) => { + tracks.forEach(({ jitsiTrack }) => { + jitsiTrack?.dispose(); + }); + }; /** * Creates and updates the audio tracks. * * @returns {void} */ - async _setTracks() { + 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 @@ -230,130 +254,92 @@ class AudioSettingsContent extends Component { return; } - this._disposeTracks(this.state.audioTracks); + _disposeTracks(audioTracks); - const audioTracks = await createLocalAudioTracks(this.props.microphoneDevices, 5000); + const newAudioTracks = await createLocalAudioTracks(microphoneDevices, 5000); - if (this._componentWasUnmounted) { - this._disposeTracks(audioTracks); + if (_componentWasUnmounted.current) { + _disposeTracks(newAudioTracks); } else { - this.setState({ - audioTracks - }); + setAudioTracks(newAudioTracks); } - } + }; - /** - * Disposes the audio tracks. - * - * @param {Object} audioTracks - The object holding the audio tracks. - * @returns {void} - */ - _disposeTracks(audioTracks: Array<{ jitsiTrack: any; }>) { - audioTracks.forEach(({ jitsiTrack }) => { - jitsiTrack?.dispose(); - }); - } + useEffect(() => { + _setTracks(); - /** - * Implements React's {@link Component#componentDidMount}. - * - * @inheritdoc - */ - componentDidMount() { - this._setTracks(); - } + return () => { + _componentWasUnmounted.current = true; + _disposeTracks(audioTracks); + }; + }, []); - /** - * Implements React's {@link Component#componentWillUnmount}. - * - * @inheritdoc - */ - componentWillUnmount() { - this._componentWasUnmounted = true; - this._disposeTracks(this.state.audioTracks); - } - - /** - * Implements React's {@link Component#componentDidUpdate}. - * - * @inheritdoc - */ - componentDidUpdate(prevProps: IProps) { - if (!equals(this.props.microphoneDevices, prevProps.microphoneDevices)) { - this._setTracks(); + useEffect(() => { + if (!equals(microphoneDevices, microphoneDevicesRef.current)) { + _setTracks(); + microphoneDevicesRef.current = microphoneDevices; } - } + }, [ microphoneDevices ]); - - /** - * Implements React's {@link Component#render}. - * - * @inheritdoc - */ - render() { - const { outputDevices, t, noiseSuppressionEnabled, toggleSuppression, prejoinVisible } = this.props; - - return ( -

+ + {outputDevices.length > 0 && ( + accessibilityLabel = { t('settings.speakers') } + className = { classes.header } + icon = { IconVolumeUp } + id = { speakerHeaderId } + text = { t('settings.speakers') } />
    - {this.state.audioTracks.map((data, i) => - this._renderMicrophoneEntry(data, i, this.state.audioTracks.length, t) + {outputDevices.map((data: any, i: number) => + _renderSpeakerEntry(data, i, outputDevices.length) )}
+
) + } + {!prejoinVisible && ( + +
e.stopPropagation() }> + +
- { outputDevices.length > 0 && ( - - -
    - { outputDevices.map((data, i) => - this._renderSpeakerEntry(data, i, outputDevices.length, t) - )} -
-
) - } - {!prejoinVisible && ( - -
e.stopPropagation() }> - -
-
- )} - - ); - } -} + )} + + ); +}; const mapStateToProps = (state: IReduxState) => { return { @@ -370,4 +356,4 @@ const mapDispatchToProps = (dispatch: IStore['dispatch']) => { }; }; -export default translate(connect(mapStateToProps, mapDispatchToProps)(AudioSettingsContent)); +export default connect(mapStateToProps, mapDispatchToProps)(AudioSettingsContent); diff --git a/react/features/settings/components/web/audio/AudioSettingsPopup.tsx b/react/features/settings/components/web/audio/AudioSettingsPopup.tsx index 45a3d7d1d3..bf1fba3c8f 100644 --- a/react/features/settings/components/web/audio/AudioSettingsPopup.tsx +++ b/react/features/settings/components/web/audio/AudioSettingsPopup.tsx @@ -1,5 +1,6 @@ import React, { ReactNode } from 'react'; import { connect } from 'react-redux'; +import { makeStyles } from 'tss-react/mui'; import { IReduxState } from '../../../../app/types'; import { areAudioLevelsEnabled } from '../../../../base/config/functions.web'; @@ -83,6 +84,14 @@ interface IProps { setAudioOutputDevice: Function; } +const useStyles = makeStyles()(() => { + return { + container: { + display: 'inline-block' + } + }; +}); + /** * Popup with audio settings. * @@ -101,8 +110,10 @@ function AudioSettingsPopup({ popupPlacement, measureAudioLevels }: IProps) { + const { classes, cx } = useStyles(); + return ( -
+
{ + return { + container: { + position: 'relative' + }, - /** - * The audio level. - */ - level: number; -} + entryText: { + maxWidth: '238px', -/** - * React {@code Component} representing an entry for the microphone audio settings. - * - * @param {IProps} props - The props of the component. - * @returns { ReactElement} - */ -export default class MicrophoneEntry extends Component { - /** - * Initializes a new {@code MicrophoneEntry} instance. - * - * @param {Object} props - The read-only properties with which the new - * instance is to be initialized. - */ - constructor(props: IProps) { - super(props); + '&.withMeter': { + maxWidth: '178px' + }, - this.state = { - level: -1 - }; - this._onClick = this._onClick.bind(this); - this._onKeyPress = this._onKeyPress.bind(this); - this._updateLevel = this._updateLevel.bind(this); - } + '&.left-margin': { + marginLeft: '36px' + } + }, + + icon: { + borderRadius: '50%', + display: 'inline-block', + width: '14px', + marginLeft: '6px', + + '& svg': { + fill: theme.palette.iconError + } + }, + + meter: { + position: 'absolute', + right: '16px', + top: '14px' + } + }; +}); + +const MicrophoneEntry = ({ + deviceId, + children, + hasError, + index, + isSelected, + length, + jitsiTrack, + listHeaderId, + measureAudioLevels, + onClick: propsClick +}: IProps) => { + const [ level, setLevel ] = useState(-1); + const activeTrackRef = useRef(jitsiTrack); + const { classes, cx } = useStyles(); /** * Click handler for the entry. * * @returns {void} */ - _onClick() { - this.props.onClick(this.props.deviceId); - } + const onClick = useCallback(() => { + propsClick(deviceId); + }, [ propsClick, deviceId ]); /** * Key pressed handler for the entry. @@ -118,12 +139,12 @@ export default class MicrophoneEntry extends Component { * * @returns {void} */ - _onKeyPress(e: React.KeyboardEvent) { + const onKeyPress = useCallback((e: React.KeyboardEvent) => { if (e.key === ' ') { e.preventDefault(); - this.props.onClick(this.props.deviceId); + propsClick(deviceId); } - } + }, [ propsClick, deviceId ]); /** * Updates the level of the meter. @@ -131,122 +152,82 @@ export default class MicrophoneEntry extends Component { * @param {number} num - The audio level provided by the jitsiTrack. * @returns {void} */ - _updateLevel(num: number) { - this.setState({ - level: Math.floor(num / 0.125) - }); - } + const updateLevel = useCallback((num: number) => { + setLevel(Math.floor(num / 0.125)); + }, []); /** * Subscribes to audio level changes coming from the jitsiTrack. * * @returns {void} */ - _startListening() { - const { jitsiTrack, measureAudioLevels } = this.props; - + const startListening = () => { jitsiTrack && measureAudioLevels && jitsiTrack.on( JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, - this._updateLevel); - } + updateLevel); + }; /** * Unsubscribes from changes coming from the jitsiTrack. * - * @param {Object} jitsiTrack - The jitsiTrack to unsubscribe from. + * @param {Object} track - The jitsiTrack to unsubscribe from. * @returns {void} */ - _stopListening(jitsiTrack?: any) { - jitsiTrack?.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateLevel); - this.setState({ - level: -1 - }); - } + const stopListening = (track?: any) => { + track?.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, updateLevel); + setLevel(-1); + }; - /** - * Implements React's {@link Component#componentDidUpdate}. - * - * @inheritdoc - */ - componentDidUpdate(prevProps: IProps) { - if (prevProps.jitsiTrack !== this.props.jitsiTrack) { - this._stopListening(prevProps.jitsiTrack); - this._startListening(); - } - } + useEffect(() => { + startListening(); - /** - * Implements React's {@link Component#componentDidMount}. - * - * @inheritdoc - */ - componentDidMount() { - this._startListening(); - } + return () => { + stopListening(jitsiTrack); + }; + }, []); - /** - * Implements React's {@link Component#componentWillUnmount}. - * - * @inheritdoc - */ - componentWillUnmount() { - this._stopListening(this.props.jitsiTrack); - } + useEffect(() => { + stopListening(activeTrackRef.current); + startListening(); + activeTrackRef.current = jitsiTrack; + }, [ jitsiTrack ]); - /** - * Implements React's {@link Component#render}. - * - * @inheritdoc - */ - render() { - const { - deviceId, - children, - hasError, - index, - isSelected, - length, - jitsiTrack, - listHeaderId, - measureAudioLevels - } = this.props; + const deviceTextId = `choose_microphone${deviceId}`; - const deviceTextId = `choose_microphone${deviceId}`; + const labelledby = `${listHeaderId} ${deviceTextId} `; - const labelledby = `${listHeaderId} ${deviceTextId} `; + return ( +
  • + + {hasError && } + + {Boolean(jitsiTrack) && measureAudioLevels && + } +
  • + ); +}; - const className = `audio-preview-microphone ${measureAudioLevels - ? 'audio-preview-microphone--withmeter' : 'audio-preview-microphone--nometer'}`; - - return ( -
  • - - {hasError && } - - { Boolean(jitsiTrack) && measureAudioLevels && - } -
  • - ); - } -} +export default MicrophoneEntry; diff --git a/react/features/settings/components/web/audio/SpeakerEntry.tsx b/react/features/settings/components/web/audio/SpeakerEntry.tsx index 504690d460..1f7a00b4cf 100644 --- a/react/features/settings/components/web/audio/SpeakerEntry.tsx +++ b/react/features/settings/components/web/audio/SpeakerEntry.tsx @@ -1,5 +1,5 @@ -import clsx from 'clsx'; import React, { useRef } from 'react'; +import { makeStyles } from 'tss-react/mui'; import { IconCheck } from '../../../../base/icons/svg'; import Button from '../../../../base/ui/components/web/Button'; @@ -47,6 +47,41 @@ interface IProps { onClick: Function; } +const useStyles = makeStyles()(() => { + return { + container: { + position: 'relative', + + [[ '&:hover', '&:focus', '&:focus-within' ] as any]: { + '& .entryText': { + maxWidth: '178px', + marginRight: 0 + }, + + '& .testButton': { + display: 'inline-block' + } + } + }, + + entryText: { + maxWidth: '238px', + + '&.left-margin': { + marginLeft: '36px' + } + }, + + testButton: { + display: 'none', + padding: '4px 10px', + position: 'absolute', + right: '16px', + top: '6px' + } + }; +}); + /** * Implements a React {@link Component} which displays an audio * output settings entry. The user can click and play a test sound. @@ -56,6 +91,7 @@ interface IProps { */ const SpeakerEntry = (props: IProps) => { const audioRef = useRef(null); + const { classes, cx } = useStyles(); /** * Click handler for the entry. @@ -110,7 +146,7 @@ const SpeakerEntry = (props: IProps) => { aria-labelledby = { labelledby } aria-posinset = { index } aria-setsize = { length } - className = 'audio-preview-speaker' + className = { classes.container } onClick = { _onClick } onKeyPress = { _onKeyPress } role = 'radio' @@ -121,9 +157,9 @@ const SpeakerEntry = (props: IProps) => { overflowType = { TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER } selected = { isSelected } text = { children } - textClassName = { clsx('audio-preview-entry-text', !isSelected && 'left-margin') }> + textClassName = { cx(classes.entryText, 'entryText', !isSelected && 'left-margin') }>