This commit is contained in:
9
react/features/settings/actionTypes.ts
Normal file
9
react/features/settings/actionTypes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* The type of (redux) action which sets the visibility of the audio settings popup.
|
||||
*/
|
||||
export const SET_AUDIO_SETTINGS_VISIBILITY = 'SET_AUDIO_SETTINGS_VISIBILITY';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the visibility of the video settings popup.
|
||||
*/
|
||||
export const SET_VIDEO_SETTINGS_VISIBILITY = 'SET_VIDEO_SETTINGS_VISIBILITY';
|
||||
38
react/features/settings/actions.native.ts
Normal file
38
react/features/settings/actions.native.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Linking } from 'react-native';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { isTokenAuthEnabled } from '../authentication/functions';
|
||||
import { hangup } from '../base/connection/actions.native';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
|
||||
import LogoutDialog from './components/native/LogoutDialog';
|
||||
|
||||
|
||||
/**
|
||||
* Opens {@code LogoutDialog}.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function openLogoutDialog() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
const config = state['features/base/config'];
|
||||
const logoutUrl = config.tokenLogoutUrl;
|
||||
|
||||
dispatch(openDialog(LogoutDialog, {
|
||||
onLogout() {
|
||||
if (isTokenAuthEnabled(config)) {
|
||||
if (logoutUrl) {
|
||||
Linking.openURL(logoutUrl);
|
||||
}
|
||||
|
||||
dispatch(hangup(true));
|
||||
} else {
|
||||
conference?.room.xmpp.moderator.logout(() => dispatch(hangup(true)));
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
}
|
||||
342
react/features/settings/actions.web.ts
Normal file
342
react/features/settings/actions.web.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { setTokenAuthUrlSuccess } from '../authentication/actions.web';
|
||||
import { isTokenAuthEnabled } from '../authentication/functions';
|
||||
import {
|
||||
setFollowMe,
|
||||
setFollowMeRecorder,
|
||||
setStartMutedPolicy,
|
||||
setStartReactionsMuted
|
||||
} from '../base/conference/actions';
|
||||
import { getConferenceState } from '../base/conference/functions';
|
||||
import { hangup } from '../base/connection/actions.web';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import i18next from '../base/i18n/i18next';
|
||||
import { browser } from '../base/lib-jitsi-meet';
|
||||
import { getNormalizedDisplayName } from '../base/participants/functions';
|
||||
import { updateSettings } from '../base/settings/actions';
|
||||
import { getLocalVideoTrack } from '../base/tracks/functions.web';
|
||||
import { appendURLHashParam } from '../base/util/uri';
|
||||
import { disableKeyboardShortcuts, enableKeyboardShortcuts } from '../keyboard-shortcuts/actions';
|
||||
import { toggleBackgroundEffect } from '../virtual-background/actions';
|
||||
import virtualBackgroundLogger from '../virtual-background/logger';
|
||||
|
||||
import {
|
||||
SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
SET_VIDEO_SETTINGS_VISIBILITY
|
||||
} from './actionTypes';
|
||||
import LogoutDialog from './components/web/LogoutDialog';
|
||||
import SettingsDialog from './components/web/SettingsDialog';
|
||||
import {
|
||||
getModeratorTabProps,
|
||||
getMoreTabProps,
|
||||
getNotificationsTabProps,
|
||||
getProfileTabProps,
|
||||
getShortcutsTabProps
|
||||
} from './functions.web';
|
||||
|
||||
|
||||
/**
|
||||
* Opens {@code LogoutDialog}.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function openLogoutDialog() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
|
||||
const config = state['features/base/config'];
|
||||
const logoutUrl = config.tokenLogoutUrl;
|
||||
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { jwt } = state['features/base/jwt'];
|
||||
|
||||
dispatch(openDialog(LogoutDialog, {
|
||||
onLogout() {
|
||||
if (isTokenAuthEnabled(config) && config.tokenAuthUrlAutoRedirect && jwt) {
|
||||
|
||||
// user is logging out remove auto redirect indication
|
||||
dispatch(setTokenAuthUrlSuccess(false));
|
||||
}
|
||||
|
||||
if (logoutUrl && browser.isElectron()) {
|
||||
const url = appendURLHashParam(logoutUrl, 'electron', 'true');
|
||||
|
||||
window.open(url, '_blank');
|
||||
dispatch(hangup(true));
|
||||
} else {
|
||||
if (logoutUrl) {
|
||||
window.location.href = logoutUrl;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
conference?.room.xmpp.moderator.logout(() => dispatch(hangup(true)));
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@code SettingsDialog}.
|
||||
*
|
||||
* @param {string} defaultTab - The tab in {@code SettingsDialog} that should be
|
||||
* displayed initially.
|
||||
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
|
||||
* welcome page or not.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function openSettingsDialog(defaultTab?: string, isDisplayedOnWelcomePage?: boolean) {
|
||||
return openDialog(SettingsDialog, {
|
||||
defaultTab,
|
||||
isDisplayedOnWelcomePage
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the audio settings.
|
||||
*
|
||||
* @param {boolean} value - The new value.
|
||||
* @returns {Function}
|
||||
*/
|
||||
function setAudioSettingsVisibility(value: boolean) {
|
||||
return {
|
||||
type: SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visibility of the video settings.
|
||||
*
|
||||
* @param {boolean} value - The new value.
|
||||
* @returns {Function}
|
||||
*/
|
||||
function setVideoSettingsVisibility(value: boolean) {
|
||||
return {
|
||||
type: SET_VIDEO_SETTINGS_VISIBILITY,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings from the "More" tab of the settings dialog.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitMoreTab(newState: any) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const currentState = getMoreTabProps(state);
|
||||
|
||||
if (newState.maxStageParticipants !== currentState.maxStageParticipants) {
|
||||
dispatch(updateSettings({ maxStageParticipants: Number(newState.maxStageParticipants) }));
|
||||
}
|
||||
|
||||
if (newState.hideSelfView !== currentState.hideSelfView) {
|
||||
dispatch(updateSettings({ disableSelfView: newState.hideSelfView }));
|
||||
}
|
||||
|
||||
if (newState.currentLanguage !== currentState.currentLanguage) {
|
||||
i18next.changeLanguage(newState.currentLanguage);
|
||||
|
||||
const { conference } = getConferenceState(state);
|
||||
|
||||
conference?.setTranscriptionLanguage(newState.currentLanguage);
|
||||
}
|
||||
|
||||
if (newState.showSubtitlesOnStage !== currentState.showSubtitlesOnStage) {
|
||||
dispatch(updateSettings({ showSubtitlesOnStage: newState.showSubtitlesOnStage }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings from the "Moderator" tab of the settings dialog.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitModeratorTab(newState: any) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getModeratorTabProps(getState());
|
||||
|
||||
if (newState.followMeEnabled !== currentState.followMeEnabled) {
|
||||
dispatch(setFollowMe(newState.followMeEnabled));
|
||||
}
|
||||
|
||||
if (newState.followMeRecorderEnabled !== currentState.followMeRecorderEnabled) {
|
||||
dispatch(setFollowMeRecorder(newState.followMeRecorderEnabled));
|
||||
}
|
||||
|
||||
if (newState.startReactionsMuted !== currentState.startReactionsMuted) {
|
||||
batch(() => {
|
||||
// updating settings we want to update and backend (notify the rest of the participants)
|
||||
dispatch(setStartReactionsMuted(newState.startReactionsMuted, true));
|
||||
dispatch(updateSettings({ soundsReactions: !newState.startReactionsMuted }));
|
||||
});
|
||||
}
|
||||
|
||||
if (newState.startAudioMuted !== currentState.startAudioMuted
|
||||
|| newState.startVideoMuted !== currentState.startVideoMuted) {
|
||||
dispatch(setStartMutedPolicy(
|
||||
newState.startAudioMuted, newState.startVideoMuted));
|
||||
}
|
||||
|
||||
if (newState.chatWithPermissionsEnabled !== currentState.chatWithPermissionsEnabled) {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
|
||||
const currentPermissions = conference?.getMetadataHandler().getMetadata().permissions || {};
|
||||
|
||||
conference?.getMetadataHandler().setMetadata('permissions', {
|
||||
...currentPermissions,
|
||||
groupChatRestricted: newState.chatWithPermissionsEnabled
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings from the "Profile" tab of the settings dialog.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitProfileTab(newState: any) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getProfileTabProps(getState());
|
||||
|
||||
if (newState.displayName !== currentState.displayName) {
|
||||
dispatch(updateSettings({ displayName: getNormalizedDisplayName(newState.displayName) }));
|
||||
}
|
||||
|
||||
if (newState.email !== currentState.email) {
|
||||
APP.conference.changeLocalEmail(newState.email);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings from the "Sounds" tab of the settings dialog.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitNotificationsTab(newState: any) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getNotificationsTabProps(getState());
|
||||
const shouldNotUpdateReactionSounds = getModeratorTabProps(getState()).startReactionsMuted;
|
||||
const shouldUpdate = (newState.soundsIncomingMessage !== currentState.soundsIncomingMessage)
|
||||
|| (newState.soundsParticipantJoined !== currentState.soundsParticipantJoined)
|
||||
|| (newState.soundsParticipantKnocking !== currentState.soundsParticipantKnocking)
|
||||
|| (newState.soundsParticipantLeft !== currentState.soundsParticipantLeft)
|
||||
|| (newState.soundsTalkWhileMuted !== currentState.soundsTalkWhileMuted)
|
||||
|| (newState.soundsReactions !== currentState.soundsReactions);
|
||||
|
||||
if (shouldUpdate) {
|
||||
const settingsToUpdate = {
|
||||
soundsIncomingMessage: newState.soundsIncomingMessage,
|
||||
soundsParticipantJoined: newState.soundsParticipantJoined,
|
||||
soundsParticipantKnocking: newState.soundsParticipantKnocking,
|
||||
soundsParticipantLeft: newState.soundsParticipantLeft,
|
||||
soundsTalkWhileMuted: newState.soundsTalkWhileMuted,
|
||||
soundsReactions: newState.soundsReactions
|
||||
};
|
||||
|
||||
if (shouldNotUpdateReactionSounds) {
|
||||
delete settingsToUpdate.soundsReactions;
|
||||
}
|
||||
dispatch(updateSettings(settingsToUpdate));
|
||||
}
|
||||
|
||||
const enabledNotifications = newState.enabledNotifications;
|
||||
|
||||
if (enabledNotifications !== currentState.enabledNotifications) {
|
||||
dispatch(updateSettings({
|
||||
userSelectedNotifications: {
|
||||
...getState()['features/base/settings'].userSelectedNotifications,
|
||||
...enabledNotifications
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the audio settings.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function toggleAudioSettings() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const value = getState()['features/settings'].audioSettingsVisible;
|
||||
|
||||
dispatch(setAudioSettingsVisibility(!value));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility of the video settings.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function toggleVideoSettings() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const value = getState()['features/settings'].videoSettingsVisible;
|
||||
|
||||
dispatch(setVideoSettingsVisibility(!value));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings from the "Shortcuts" tab of the settings dialog.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitShortcutsTab(newState: any) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getShortcutsTabProps(getState());
|
||||
|
||||
if (newState.keyboardShortcutsEnabled !== currentState.keyboardShortcutsEnabled) {
|
||||
if (newState.keyboardShortcutsEnabled) {
|
||||
dispatch(enableKeyboardShortcuts());
|
||||
} else {
|
||||
dispatch(disableKeyboardShortcuts());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings from the "Virtual Background" tab of the settings dialog.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @param {boolean} isCancel - Whether the change represents a cancel.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitVirtualBackgroundTab(newState: any, isCancel = false) {
|
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const track = getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack;
|
||||
const { localFlipX } = state['features/base/settings'];
|
||||
|
||||
if (newState.options?.selectedThumbnail) {
|
||||
await dispatch(toggleBackgroundEffect(newState.options, track));
|
||||
|
||||
if (!isCancel) {
|
||||
// Set x scale to default value.
|
||||
dispatch(updateSettings({
|
||||
localFlipX
|
||||
}));
|
||||
|
||||
virtualBackgroundLogger.info(`Virtual background type: '${
|
||||
typeof newState.options.backgroundType === 'undefined'
|
||||
? 'none' : newState.options.backgroundType}' applied!`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
114
react/features/settings/components/native/AdvancedSection.tsx
Normal file
114
react/features/settings/components/native/AdvancedSection.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, NativeModules, Platform, Text } from 'react-native';
|
||||
import { Divider } from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import Switch from '../../../base/ui/components/native/Switch';
|
||||
|
||||
import FormRow from './FormRow';
|
||||
import FormSection from './FormSection';
|
||||
import styles from './styles';
|
||||
|
||||
const { AppInfo } = NativeModules;
|
||||
|
||||
const AdvancedSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
disableCrashReporting,
|
||||
disableCallIntegration,
|
||||
disableP2P
|
||||
} = useSelector((state: IReduxState) => state['features/base/settings']);
|
||||
|
||||
const onSwitchToggled = useCallback((name: string) => (enabled?: boolean) => {
|
||||
|
||||
if (name === 'disableCrashReporting' && enabled === true) {
|
||||
Alert.alert(
|
||||
t('settingsView.alertTitle'),
|
||||
t('settingsView.disableCrashReportingWarning'),
|
||||
[
|
||||
{
|
||||
onPress: () => dispatch(updateSettings({ disableCrashReporting: true })),
|
||||
text: t('settingsView.alertOk')
|
||||
},
|
||||
{
|
||||
text: t('settingsView.alertCancel')
|
||||
}
|
||||
]
|
||||
);
|
||||
} else {
|
||||
dispatch(updateSettings({ [name]: enabled }));
|
||||
}
|
||||
}, [ dispatch, updateSettings ]);
|
||||
|
||||
const switches = useMemo(() => {
|
||||
const partialSwitches = [
|
||||
{
|
||||
label: 'settingsView.disableCallIntegration',
|
||||
state: disableCallIntegration,
|
||||
name: 'disableCallIntegration'
|
||||
},
|
||||
{
|
||||
label: 'settingsView.disableP2P',
|
||||
state: disableP2P,
|
||||
name: 'disableP2P'
|
||||
},
|
||||
{
|
||||
label: 'settingsView.disableCrashReporting',
|
||||
state: disableCrashReporting,
|
||||
name: 'disableCrashReporting'
|
||||
}
|
||||
];
|
||||
|
||||
if (Platform.OS !== 'android') {
|
||||
partialSwitches.shift();
|
||||
}
|
||||
|
||||
if (!AppInfo.GOOGLE_SERVICES_ENABLED) {
|
||||
partialSwitches.pop();
|
||||
}
|
||||
|
||||
return partialSwitches;
|
||||
}, [ disableCallIntegration, disableP2P, disableCrashReporting ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormSection
|
||||
label = 'settingsView.advanced'>
|
||||
{
|
||||
switches.map(({ label, state, name }) => (
|
||||
<FormRow
|
||||
key = { label }
|
||||
label = { label }>
|
||||
<Switch
|
||||
checked = { Boolean(state) }
|
||||
onChange = { onSwitchToggled(name) } />
|
||||
</FormRow>
|
||||
))
|
||||
}
|
||||
</FormSection>
|
||||
{/* @ts-ignore */}
|
||||
<Divider style = { styles.fieldSeparator } />
|
||||
<FormSection
|
||||
label = 'settingsView.buildInfoSection'>
|
||||
<FormRow
|
||||
label = 'settingsView.version'>
|
||||
<Text style = { styles.text }>
|
||||
{`${AppInfo.version} build ${AppInfo.buildNumber}`}
|
||||
</Text>
|
||||
</FormRow>
|
||||
<FormRow
|
||||
label = 'settingsView.sdkVersion'>
|
||||
<Text style = { styles.text }>
|
||||
{AppInfo.sdkVersion}
|
||||
</Text>
|
||||
</FormRow>
|
||||
</FormSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedSection;
|
||||
101
react/features/settings/components/native/ConferenceSection.tsx
Normal file
101
react/features/settings/components/native/ConferenceSection.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { getDefaultURL } from '../../../app/functions.native';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
import Switch from '../../../base/ui/components/native/Switch';
|
||||
import { isServerURLChangeEnabled, normalizeUserInputURL } from '../../functions.native';
|
||||
|
||||
import FormRow from './FormRow';
|
||||
import FormSection from './FormSection';
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
const ConferenceSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
serverURL,
|
||||
startCarMode,
|
||||
startWithAudioMuted,
|
||||
startWithVideoMuted
|
||||
} = useSelector((state: IReduxState) => state['features/base/settings']);
|
||||
|
||||
const defaultServerURL = useSelector((state: IReduxState) => getDefaultURL(state));
|
||||
const [ newServerURL, setNewServerURL ] = useState(serverURL ?? '');
|
||||
|
||||
const serverURLChangeEnabled = useSelector((state: IReduxState) => isServerURLChangeEnabled(state));
|
||||
|
||||
const switches = useMemo(() => [
|
||||
{
|
||||
label: 'settingsView.startCarModeInLowBandwidthMode',
|
||||
state: startCarMode,
|
||||
name: 'startCarMode'
|
||||
},
|
||||
{
|
||||
label: 'settingsView.startWithAudioMuted',
|
||||
state: startWithAudioMuted,
|
||||
name: 'startWithAudioMuted'
|
||||
},
|
||||
{
|
||||
label: 'settingsView.startWithVideoMuted',
|
||||
state: startWithVideoMuted,
|
||||
name: 'startWithVideoMuted'
|
||||
}
|
||||
], [ startCarMode, startWithAudioMuted, startWithVideoMuted ]);
|
||||
|
||||
const onChangeServerURL = useCallback(value => {
|
||||
|
||||
setNewServerURL(value);
|
||||
dispatch(updateSettings({
|
||||
serverURL: value
|
||||
}));
|
||||
}, [ dispatch, newServerURL ]);
|
||||
|
||||
const processServerURL = useCallback(() => {
|
||||
const normalizedURL = normalizeUserInputURL(newServerURL);
|
||||
|
||||
onChangeServerURL(normalizedURL);
|
||||
}, [ newServerURL ]);
|
||||
|
||||
useEffect(() => () => processServerURL(), []);
|
||||
|
||||
const onSwitchToggled = useCallback((name: string) => (enabled?: boolean) => {
|
||||
|
||||
// @ts-ignore
|
||||
dispatch(updateSettings({ [name]: enabled }));
|
||||
}, [ dispatch ]);
|
||||
|
||||
return (
|
||||
<FormSection
|
||||
label = 'settingsView.conferenceSection'>
|
||||
<Input
|
||||
autoCapitalize = 'none'
|
||||
customStyles = {{ container: styles.customContainer }}
|
||||
editable = { serverURLChangeEnabled }
|
||||
keyboardType = { 'url' }
|
||||
label = { t('settingsView.serverURL') }
|
||||
onBlur = { processServerURL }
|
||||
onChange = { onChangeServerURL }
|
||||
placeholder = { defaultServerURL }
|
||||
textContentType = { 'URL' } // iOS only
|
||||
value = { newServerURL } />
|
||||
{
|
||||
switches.map(({ label, state, name }) => (
|
||||
<FormRow
|
||||
key = { label }
|
||||
label = { label }>
|
||||
<Switch
|
||||
checked = { Boolean(state) }
|
||||
onChange = { onSwitchToggled(name) } />
|
||||
</FormRow>
|
||||
))
|
||||
}
|
||||
</FormSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConferenceSection;
|
||||
146
react/features/settings/components/native/FormRow.tsx
Normal file
146
react/features/settings/components/native/FormRow.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { Text, View, ViewStyle } from 'react-native';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
|
||||
import styles, { ANDROID_UNDERLINE_COLOR, PLACEHOLDER_COLOR } from './styles';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link FormRow}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Component's children.
|
||||
*/
|
||||
children: React.ReactElement;
|
||||
|
||||
/**
|
||||
* Prop to decide if a row separator is to be rendered.
|
||||
*/
|
||||
fieldSeparator?: boolean;
|
||||
|
||||
/**
|
||||
* The i18n key of the text label of the form field.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* One of 'row' (default) or 'column'.
|
||||
*/
|
||||
layout?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@code Component} which renders a standardized row on a
|
||||
* form. The component should have exactly one child component.
|
||||
*/
|
||||
class FormRow extends Component<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code FormRow} instance.
|
||||
*
|
||||
* @param {Object} props - Component properties.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
React.Children.only(this.props.children);
|
||||
|
||||
this._getDefaultFieldProps = this._getDefaultFieldProps.bind(this);
|
||||
this._getRowStyle = this._getRowStyle.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @override
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { t } = this.props;
|
||||
|
||||
// Some field types need additional props to look good and standardized
|
||||
// on a form.
|
||||
const newChild
|
||||
= React.cloneElement(
|
||||
this.props.children,
|
||||
this._getDefaultFieldProps(this.props.children));
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { this._getRowStyle() } >
|
||||
<View style = { styles.fieldLabelContainer as ViewStyle } >
|
||||
<Text
|
||||
style = { [
|
||||
styles.text,
|
||||
styles.fieldLabelText
|
||||
] } >
|
||||
{ t(this.props.label) }
|
||||
</Text>
|
||||
</View>
|
||||
<View style = { styles.fieldValueContainer as ViewStyle } >
|
||||
{ newChild }
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles the default props to the field child component of this form
|
||||
* row.
|
||||
*
|
||||
* Currently tested/supported field types:
|
||||
* - TextInput
|
||||
* - Switch (needs no addition props ATM).
|
||||
*
|
||||
* @param {Object} field - The field (child) component.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getDefaultFieldProps(field?: React.ReactElement) {
|
||||
if (field?.type) { // @ts-ignore
|
||||
switch (field.type.displayName) {
|
||||
case 'TextInput':
|
||||
return {
|
||||
placeholderTextColor: PLACEHOLDER_COLOR,
|
||||
style: [
|
||||
styles.textInputField,
|
||||
this.props.layout === 'column' ? styles.textInputFieldColumn : undefined
|
||||
],
|
||||
underlineColorAndroid: ANDROID_UNDERLINE_COLOR
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles the row style array based on the row's props.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
_getRowStyle() {
|
||||
const { fieldSeparator, layout } = this.props;
|
||||
const rowStyle: ViewStyle[] = [
|
||||
styles.fieldContainer as ViewStyle
|
||||
];
|
||||
|
||||
if (fieldSeparator) {
|
||||
rowStyle.push(styles.fieldSeparator);
|
||||
}
|
||||
|
||||
if (layout === 'column') {
|
||||
rowStyle.push(
|
||||
styles.fieldContainerColumn as ViewStyle
|
||||
);
|
||||
}
|
||||
|
||||
return rowStyle;
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(FormRow);
|
||||
42
react/features/settings/components/native/FormSection.tsx
Normal file
42
react/features/settings/components/native/FormSection.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { Text, View } from 'react-native';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link FormSection}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The children to be displayed within this Link.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
|
||||
/**
|
||||
* The i18n key of the text label of the section.
|
||||
*/
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Section accordion on settings form.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
function FormSection({ children, label, t }: IProps) {
|
||||
return (
|
||||
<View>
|
||||
{label && <Text style = { styles.formSectionTitleText }>
|
||||
{ t(label) }
|
||||
</Text>}
|
||||
{ children }
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(FormSection);
|
||||
61
react/features/settings/components/native/GeneralSection.tsx
Normal file
61
react/features/settings/components/native/GeneralSection.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Text, TouchableHighlight, View, ViewStyle } from 'react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import i18next, { DEFAULT_LANGUAGE } from '../../../base/i18n/i18next';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowRight } from '../../../base/icons/svg';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import Switch from '../../../base/ui/components/native/Switch';
|
||||
import { navigate } from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
|
||||
import FormRow from './FormRow';
|
||||
import FormSection from './FormSection';
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
const GeneralSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
disableSelfView,
|
||||
} = useSelector((state: IReduxState) => state['features/base/settings']);
|
||||
|
||||
const { language = DEFAULT_LANGUAGE } = i18next;
|
||||
|
||||
const onSelfViewToggled = useCallback((enabled?: boolean) =>
|
||||
dispatch(updateSettings({ disableSelfView: enabled }))
|
||||
, [ dispatch, updateSettings ]);
|
||||
|
||||
const navigateToLanguageSelect = useCallback(() => {
|
||||
navigate(screen.settings.language);
|
||||
}, [ navigate, screen ]);
|
||||
|
||||
return (
|
||||
<FormSection>
|
||||
<FormRow label = 'videothumbnail.hideSelfView'>
|
||||
<Switch
|
||||
checked = { Boolean(disableSelfView) }
|
||||
onChange = { onSelfViewToggled } />
|
||||
</FormRow>
|
||||
<FormRow label = 'settings.language'>
|
||||
<View style = { styles.languageButtonContainer as ViewStyle }>
|
||||
<TouchableHighlight onPress = { navigateToLanguageSelect }>
|
||||
<View style = { styles.languageButton as ViewStyle }>
|
||||
<Text
|
||||
style = { styles.languageText }>{t(`languages:${language}`)}</Text>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { IconArrowRight } />
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
</FormRow>
|
||||
</FormSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralSection;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useCallback, useLayoutEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ScrollView, Text, TouchableHighlight, View, ViewStyle } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import i18next, { DEFAULT_LANGUAGE, LANGUAGES } from '../../../base/i18n/i18next';
|
||||
import { IconArrowLeft } from '../../../base/icons/svg';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import BaseThemeNative from '../../../base/ui/components/BaseTheme.native';
|
||||
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
|
||||
import { goBack, navigate } from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const LanguageSelectView = ({ isInWelcomePage }: { isInWelcomePage?: boolean; }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation();
|
||||
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
|
||||
const { language: currentLanguage = DEFAULT_LANGUAGE } = i18next;
|
||||
|
||||
const setLanguage = useCallback(language => () => {
|
||||
i18next.changeLanguage(language);
|
||||
conference?.setTranscriptionLanguage(language);
|
||||
navigate(screen.settings.main);
|
||||
}, [ conference, i18next ]);
|
||||
|
||||
const headerLeft = () => (
|
||||
<HeaderNavigationButton
|
||||
color = { BaseThemeNative.palette.link01 }
|
||||
onPress = { goBack }
|
||||
src = { IconArrowLeft }
|
||||
style = { styles.backBtn }
|
||||
twoActions = { true } />
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft
|
||||
});
|
||||
}, [ navigation ]);
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
|
||||
// @ts-ignore
|
||||
safeAreaInsets = { [ !isInWelcomePage && 'bottom', 'left', 'right' ].filter(Boolean) }
|
||||
style = { styles.settingsViewContainer }>
|
||||
<ScrollView
|
||||
bounces = { isInWelcomePage }
|
||||
contentContainerStyle = { styles.profileView as ViewStyle }>
|
||||
{
|
||||
LANGUAGES.map(language => (
|
||||
<TouchableHighlight
|
||||
disabled = { currentLanguage === language }
|
||||
key = { language }
|
||||
onPress = { setLanguage(language) }>
|
||||
<View
|
||||
style = { styles.languageOption as ViewStyle }>
|
||||
<Text
|
||||
style = { [
|
||||
styles.text,
|
||||
styles.fieldLabelText,
|
||||
currentLanguage === language && styles.selectedLanguage ] }>
|
||||
{ t(`languages:${language}`) }
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
))
|
||||
}
|
||||
</ScrollView>
|
||||
</JitsiScreen>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSelectView;
|
||||
57
react/features/settings/components/native/LinksSection.tsx
Normal file
57
react/features/settings/components/native/LinksSection.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Linking, View, ViewStyle } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getLegalUrls } from '../../../base/config/functions.native';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
|
||||
import FormSection from './FormSection';
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
const LinksSection = () => {
|
||||
const {
|
||||
privacy,
|
||||
helpCentre,
|
||||
terms
|
||||
} = useSelector((state: IReduxState) => getLegalUrls(state));
|
||||
|
||||
const links = useMemo(() => [
|
||||
{
|
||||
label: 'settingsView.help',
|
||||
link: helpCentre
|
||||
},
|
||||
{
|
||||
label: 'settingsView.terms',
|
||||
link: terms
|
||||
},
|
||||
{
|
||||
label: 'settingsView.privacy',
|
||||
link: privacy
|
||||
}
|
||||
], [ privacy, helpCentre, terms ]);
|
||||
|
||||
const onLinkPress = useCallback(link => () => Linking.openURL(link), [ Linking ]);
|
||||
|
||||
return (
|
||||
<FormSection>
|
||||
<View style = { styles.linksSection as ViewStyle }>
|
||||
{
|
||||
links.map(({ label, link }) => (
|
||||
<Button
|
||||
accessibilityLabel = { label }
|
||||
key = { label }
|
||||
labelKey = { label }
|
||||
onClick = { onLinkPress(link) }
|
||||
style = { styles.linksButton }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
))
|
||||
}
|
||||
</View>
|
||||
</FormSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinksSection;
|
||||
21
react/features/settings/components/native/LogoutDialog.tsx
Normal file
21
react/features/settings/components/native/LogoutDialog.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import ConfirmDialog
|
||||
from '../../../base/dialog/components/native/ConfirmDialog';
|
||||
|
||||
interface ILogoutDialogProps extends WithTranslation {
|
||||
onLogout: Function;
|
||||
}
|
||||
|
||||
|
||||
const LogoutDialog: React.FC<ILogoutDialogProps> = ({ onLogout }: ILogoutDialogProps) => (
|
||||
<ConfirmDialog
|
||||
cancelLabel = 'dialog.Cancel'
|
||||
confirmLabel = 'dialog.Yes'
|
||||
descriptionKey = 'dialog.logoutQuestion'
|
||||
onSubmit = { onLogout } />
|
||||
);
|
||||
|
||||
export default LogoutDialog;
|
||||
|
||||
145
react/features/settings/components/native/ModeratorSection.tsx
Normal file
145
react/features/settings/components/native/ModeratorSection.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import {
|
||||
setFollowMe,
|
||||
setFollowMeRecorder,
|
||||
setStartMutedPolicy,
|
||||
setStartReactionsMuted
|
||||
} from '../../../base/conference/actions';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import Switch from '../../../base/ui/components/native/Switch';
|
||||
import { getModeratorTabProps } from '../../functions.native';
|
||||
|
||||
import FormRow from './FormRow';
|
||||
import FormSection from './FormSection';
|
||||
|
||||
const ModeratorSection = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
audioModerationEnabled,
|
||||
chatWithPermissionsEnabled,
|
||||
followMeActive,
|
||||
followMeEnabled,
|
||||
followMeRecorderActive,
|
||||
followMeRecorderEnabled,
|
||||
startAudioMuted,
|
||||
startVideoMuted,
|
||||
startReactionsMuted,
|
||||
videoModerationEnabled
|
||||
} = useSelector((state: IReduxState) => getModeratorTabProps(state));
|
||||
|
||||
const { disableReactionsModeration } = useSelector((state: IReduxState) => state['features/base/config']);
|
||||
|
||||
const onStartAudioMutedToggled = useCallback((enabled?: boolean) => {
|
||||
dispatch(setStartMutedPolicy(
|
||||
Boolean(enabled), Boolean(startVideoMuted)));
|
||||
}, [ startVideoMuted, dispatch, setStartMutedPolicy ]);
|
||||
|
||||
const onStartVideoMutedToggled = useCallback((enabled?: boolean) => {
|
||||
dispatch(setStartMutedPolicy(
|
||||
Boolean(startAudioMuted), Boolean(enabled)));
|
||||
}, [ startAudioMuted, dispatch, setStartMutedPolicy ]);
|
||||
|
||||
const onFollowMeToggled = useCallback((enabled?: boolean) => {
|
||||
dispatch(setFollowMe(Boolean(enabled)));
|
||||
}, [ dispatch, setFollowMe ]);
|
||||
|
||||
const onFollowMeRecorderToggled = useCallback((enabled?: boolean) => {
|
||||
dispatch(setFollowMeRecorder(Boolean(enabled)));
|
||||
}, [ dispatch, setFollowMeRecorder ]);
|
||||
|
||||
const onStartReactionsMutedToggled = useCallback((enabled?: boolean) => {
|
||||
dispatch(setStartReactionsMuted(Boolean(enabled), true));
|
||||
dispatch(updateSettings({ soundsReactions: enabled }));
|
||||
}, [ dispatch, updateSettings, setStartReactionsMuted ]);
|
||||
|
||||
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
|
||||
const onChatWithPermissionsToggled = useCallback((enabled?: boolean) => {
|
||||
const currentPermissions = conference?.getMetadataHandler().getMetadata().permissions || {};
|
||||
|
||||
conference?.getMetadataHandler().setMetadata('permissions', {
|
||||
...currentPermissions,
|
||||
groupChatRestricted: enabled
|
||||
});
|
||||
}, [ dispatch, conference ]);
|
||||
|
||||
const followMeRecorderChecked = followMeRecorderEnabled && !followMeRecorderActive;
|
||||
|
||||
const moderationSettings = useMemo(() => {
|
||||
const moderation = [
|
||||
{
|
||||
disabled: audioModerationEnabled,
|
||||
label: 'settings.startAudioMuted',
|
||||
state: startAudioMuted,
|
||||
onChange: onStartAudioMutedToggled
|
||||
},
|
||||
{
|
||||
disabled: videoModerationEnabled,
|
||||
label: 'settings.startVideoMuted',
|
||||
state: startVideoMuted,
|
||||
onChange: onStartVideoMutedToggled
|
||||
},
|
||||
{
|
||||
disabled: followMeActive || followMeRecorderActive,
|
||||
label: 'settings.followMe',
|
||||
state: followMeEnabled && !followMeActive && !followMeRecorderChecked,
|
||||
onChange: onFollowMeToggled
|
||||
},
|
||||
{
|
||||
disabled: followMeRecorderActive || followMeActive,
|
||||
label: 'settings.followMeRecorder',
|
||||
state: followMeRecorderChecked,
|
||||
onChange: onFollowMeRecorderToggled
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: 'settings.startReactionsMuted',
|
||||
state: startReactionsMuted,
|
||||
onChange: onStartReactionsMutedToggled
|
||||
},
|
||||
{
|
||||
label: 'settings.chatWithPermissions',
|
||||
state: chatWithPermissionsEnabled,
|
||||
onChange: onChatWithPermissionsToggled
|
||||
},
|
||||
];
|
||||
|
||||
if (disableReactionsModeration) {
|
||||
moderation.pop();
|
||||
}
|
||||
|
||||
return moderation;
|
||||
}, [ startAudioMuted,
|
||||
startVideoMuted,
|
||||
followMeEnabled,
|
||||
followMeRecorderEnabled,
|
||||
disableReactionsModeration,
|
||||
onStartAudioMutedToggled,
|
||||
onStartVideoMutedToggled,
|
||||
onFollowMeToggled,
|
||||
onFollowMeRecorderToggled,
|
||||
onStartReactionsMutedToggled,
|
||||
startReactionsMuted ]);
|
||||
|
||||
return (
|
||||
<FormSection
|
||||
label = 'settings.playSounds'>
|
||||
{
|
||||
moderationSettings.map(({ label, state, onChange, disabled }) => (
|
||||
<FormRow
|
||||
key = { label }
|
||||
label = { label }>
|
||||
<Switch
|
||||
checked = { Boolean(state) }
|
||||
disabled = { disabled }
|
||||
onChange = { onChange } />
|
||||
</FormRow>
|
||||
))
|
||||
}
|
||||
</FormSection>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModeratorSection;
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Divider } from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import Switch from '../../../base/ui/components/native/Switch';
|
||||
import { getNotificationsTabProps } from '../../functions.any';
|
||||
|
||||
import FormRow from './FormRow';
|
||||
import FormSection from './FormSection';
|
||||
import styles from './styles';
|
||||
|
||||
const NotificationsSection = () => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
soundsIncomingMessage,
|
||||
soundsParticipantJoined,
|
||||
soundsParticipantKnocking,
|
||||
soundsParticipantLeft,
|
||||
soundsReactions,
|
||||
soundsTalkWhileMuted,
|
||||
enableReactions,
|
||||
enabledNotifications,
|
||||
disabledSounds,
|
||||
moderatorMutedSoundsReactions
|
||||
} = useSelector((state: IReduxState) => getNotificationsTabProps(state));
|
||||
|
||||
const sounds = useMemo(() => {
|
||||
const partialSounds = [
|
||||
{
|
||||
label: 'settings.reactions',
|
||||
state: soundsReactions,
|
||||
name: 'soundsReactions',
|
||||
disabled: Boolean(moderatorMutedSoundsReactions
|
||||
|| disabledSounds.includes('REACTION_SOUND'))
|
||||
},
|
||||
{
|
||||
label: 'settings.incomingMessage',
|
||||
state: soundsIncomingMessage,
|
||||
name: 'soundsIncomingMessage'
|
||||
},
|
||||
{
|
||||
label: 'settings.participantJoined',
|
||||
state: soundsParticipantJoined,
|
||||
name: 'soundsParticipantJoined'
|
||||
},
|
||||
{
|
||||
label: 'settings.participantLeft',
|
||||
state: soundsParticipantLeft,
|
||||
name: 'soundsParticipantLeft'
|
||||
},
|
||||
{
|
||||
label: 'settings.talkWhileMuted',
|
||||
state: soundsTalkWhileMuted,
|
||||
name: 'soundsTalkWhileMuted'
|
||||
},
|
||||
{
|
||||
label: 'settings.participantKnocking',
|
||||
state: soundsParticipantKnocking,
|
||||
name: 'soundsParticipantKnocking'
|
||||
}
|
||||
];
|
||||
|
||||
if (!enableReactions) {
|
||||
partialSounds.shift();
|
||||
}
|
||||
|
||||
return partialSounds;
|
||||
|
||||
}, [ soundsReactions,
|
||||
soundsIncomingMessage,
|
||||
soundsParticipantJoined,
|
||||
soundsParticipantLeft,
|
||||
soundsTalkWhileMuted,
|
||||
soundsParticipantKnocking,
|
||||
enableReactions ]);
|
||||
|
||||
const onSoundToggled = useCallback((name: string) => (enabled?: boolean) => {
|
||||
dispatch(updateSettings({ [name]: enabled }));
|
||||
}, [ dispatch, updateSettings ]);
|
||||
|
||||
|
||||
const onNotificationToggled = useCallback((name: string) => (enabled?: boolean) => {
|
||||
dispatch(updateSettings({
|
||||
userSelectedNotifications: {
|
||||
...enabledNotifications,
|
||||
[name]: Boolean(enabled)
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}, [ dispatch, updateSettings, enabledNotifications ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormSection
|
||||
label = 'settings.playSounds'>
|
||||
{
|
||||
sounds.map(({ label, state, name, disabled }) => (
|
||||
<FormRow
|
||||
key = { label }
|
||||
label = { label }>
|
||||
<Switch
|
||||
checked = { Boolean(state) }
|
||||
disabled = { disabled }
|
||||
onChange = { onSoundToggled(name) } />
|
||||
</FormRow>
|
||||
))
|
||||
}
|
||||
</FormSection>
|
||||
{
|
||||
Object.keys(enabledNotifications).length > 0 && (
|
||||
<>
|
||||
{/* @ts-ignore */}
|
||||
<Divider style = { styles.fieldSeparator } />
|
||||
<FormSection
|
||||
label = 'notify.displayNotifications'>
|
||||
{
|
||||
Object.keys(enabledNotifications).map(name => (
|
||||
<FormRow
|
||||
key = { name }
|
||||
label = { name }>
|
||||
<Switch
|
||||
checked = { Boolean(enabledNotifications[name]) }
|
||||
onChange = { onNotificationToggled(name) } />
|
||||
</FormRow>)
|
||||
)
|
||||
}
|
||||
</FormSection>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsSection;
|
||||
165
react/features/settings/components/native/ProfileView.tsx
Normal file
165
react/features/settings/components/native/ProfileView.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useCallback, useLayoutEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ScrollView, Text, View, ViewStyle } from 'react-native';
|
||||
import { Edge } from 'react-native-safe-area-context';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { login, logout } from '../../../authentication/actions.native';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { IconArrowLeft } from '../../../base/icons/svg';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import BaseThemeNative from '../../../base/ui/components/BaseTheme.native';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import HeaderNavigationButton
|
||||
from '../../../mobile/navigation/components/HeaderNavigationButton';
|
||||
import {
|
||||
goBack,
|
||||
navigate
|
||||
} from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
|
||||
import FormSection from './FormSection';
|
||||
import { AVATAR_SIZE } from './constants';
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
const ProfileView = ({ isInWelcomePage }: {
|
||||
isInWelcomePage?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { displayName: reduxDisplayName, email: reduxEmail } = useSelector(
|
||||
(state: IReduxState) => state['features/base/settings']
|
||||
);
|
||||
const participant = useSelector((state: IReduxState) => getLocalParticipant(state));
|
||||
const { locationURL } = useSelector((state: IReduxState) => state['features/base/connection']);
|
||||
|
||||
const [ displayName, setDisplayName ] = useState(reduxDisplayName);
|
||||
const [ email, setEmail ] = useState(reduxEmail);
|
||||
|
||||
const { authLogin: isAutenticated } = useSelector((state: IReduxState) => state['features/base/conference']);
|
||||
|
||||
const onDisplayNameChanged = useCallback(newDisplayName => {
|
||||
setDisplayName(newDisplayName);
|
||||
}, [ setDisplayName ]);
|
||||
|
||||
const onEmailChanged = useCallback(newEmail => {
|
||||
setEmail(newEmail);
|
||||
}, [ setEmail ]);
|
||||
|
||||
const onApplySettings = useCallback(() => {
|
||||
dispatch(updateSettings({
|
||||
displayName,
|
||||
email
|
||||
}));
|
||||
|
||||
navigate(screen.settings.main);
|
||||
},
|
||||
[ dispatch, updateSettings, email, displayName ]);
|
||||
|
||||
const onLogin = useCallback(() => {
|
||||
dispatch(login());
|
||||
}, [ dispatch ]);
|
||||
|
||||
const onLogout = useCallback(() => {
|
||||
dispatch(logout());
|
||||
}, [ dispatch ]);
|
||||
|
||||
const headerLeft = () => (
|
||||
<HeaderNavigationButton
|
||||
color = { BaseThemeNative.palette.link01 }
|
||||
onPress = { goBack }
|
||||
src = { IconArrowLeft }
|
||||
style = { styles.backBtn }
|
||||
twoActions = { true } />
|
||||
);
|
||||
|
||||
const headerRight = () => {
|
||||
if (isAutenticated) {
|
||||
return (
|
||||
<HeaderNavigationButton
|
||||
label = { t('toolbar.logout') }
|
||||
onPress = { onLogout }
|
||||
style = { styles.logBtn }
|
||||
twoActions = { true } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderNavigationButton
|
||||
label = { t('toolbar.login') }
|
||||
onPress = { onLogin }
|
||||
style = { styles.logBtn }
|
||||
twoActions = { true } />
|
||||
);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft,
|
||||
headerRight: !isInWelcomePage
|
||||
&& !locationURL?.hostname?.includes('8x8.vc')
|
||||
&& headerRight
|
||||
});
|
||||
}, [ navigation ]);
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
hasBottomTextInput = { true }
|
||||
|
||||
// @ts-ignore
|
||||
safeAreaInsets = { [ !isInWelcomePage && 'bottom', 'left', 'right' ].filter(Boolean) as Edge[] }
|
||||
style = { styles.settingsViewContainer }>
|
||||
<ScrollView
|
||||
bounces = { isInWelcomePage }
|
||||
contentContainerStyle = { styles.profileView as ViewStyle }>
|
||||
<View>
|
||||
<View style = { styles.avatarContainer as ViewStyle }>
|
||||
<Avatar
|
||||
participantId = { participant?.id }
|
||||
size = { AVATAR_SIZE } />
|
||||
</View>
|
||||
<FormSection>
|
||||
<Input
|
||||
customStyles = {{ container: styles.customContainer }}
|
||||
label = { t('settingsView.displayName') }
|
||||
onChange = { onDisplayNameChanged }
|
||||
placeholder = { t('settingsView.displayNamePlaceholderText') }
|
||||
textContentType = { 'name' } // iOS only
|
||||
value = { displayName ?? '' } />
|
||||
<Input
|
||||
autoCapitalize = 'none'
|
||||
customStyles = {{ container: styles.customContainer }}
|
||||
keyboardType = { 'email-address' }
|
||||
label = { t('settingsView.email') }
|
||||
onChange = { onEmailChanged }
|
||||
placeholder = { t('settingsView.emailPlaceholderText') }
|
||||
textContentType = { 'emailAddress' } // iOS only
|
||||
value = { email ?? '' } />
|
||||
<Text style = { styles.gavatarMessageContainer }>
|
||||
{ t('settingsView.gavatarMessage') }
|
||||
</Text>
|
||||
</FormSection>
|
||||
</View>
|
||||
<Button
|
||||
accessibilityLabel = { t('settingsView.apply') }
|
||||
labelKey = { 'settingsView.apply' }
|
||||
onClick = { onApplySettings }
|
||||
style = { styles.applyProfileSettingsButton }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
</ScrollView>
|
||||
</JitsiScreen>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileView;
|
||||
101
react/features/settings/components/native/SettingsView.tsx
Normal file
101
react/features/settings/components/native/SettingsView.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
ScrollView,
|
||||
Text,
|
||||
TextStyle,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
ViewStyle
|
||||
} from 'react-native';
|
||||
import { Divider } from 'react-native-paper';
|
||||
import { Edge } from 'react-native-safe-area-context';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowRight } from '../../../base/icons/svg';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { navigate } from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import { shouldShowModeratorSettings } from '../../functions.native';
|
||||
|
||||
import AdvancedSection from './AdvancedSection';
|
||||
import ConferenceSection from './ConferenceSection';
|
||||
import GeneralSection from './GeneralSection';
|
||||
import LinksSection from './LinksSection';
|
||||
import ModeratorSection from './ModeratorSection';
|
||||
import NotificationsSection from './NotificationsSection';
|
||||
import { AVATAR_SIZE } from './constants';
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
interface IProps {
|
||||
|
||||
isInWelcomePage?: boolean | undefined;
|
||||
}
|
||||
|
||||
const SettingsView = ({ isInWelcomePage }: IProps) => {
|
||||
const { displayName } = useSelector((state: IReduxState) => state['features/base/settings']);
|
||||
const localParticipant = useSelector((state: IReduxState) => getLocalParticipant(state));
|
||||
const showModeratorSettings = useSelector((state: IReduxState) => shouldShowModeratorSettings(state));
|
||||
const { visible } = useSelector((state: IReduxState) => state['features/settings']);
|
||||
|
||||
const addBottomInset = !isInWelcomePage;
|
||||
const localParticipantId = localParticipant?.id;
|
||||
const scrollBounces = Boolean(isInWelcomePage);
|
||||
|
||||
if (visible !== undefined && !visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
safeAreaInsets = { [ addBottomInset && 'bottom', 'left', 'right' ].filter(Boolean) as Edge[] }
|
||||
style = { styles.settingsViewContainer }>
|
||||
<ScrollView bounces = { scrollBounces }>
|
||||
<View style = { styles.profileContainerWrapper as ViewStyle }>
|
||||
<TouchableHighlight
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
onPress = { () => navigate(screen.settings.profile) }>
|
||||
<View
|
||||
style = { styles.profileContainer as ViewStyle }>
|
||||
<Avatar
|
||||
participantId = { localParticipantId }
|
||||
size = { AVATAR_SIZE } />
|
||||
<Text style = { styles.displayName as TextStyle }>
|
||||
{ displayName }
|
||||
</Text>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { IconArrowRight }
|
||||
style = { styles.profileViewArrow } />
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
<GeneralSection />
|
||||
{ isInWelcomePage && <>
|
||||
<Divider style = { styles.fieldSeparator as ViewStyle } />
|
||||
<ConferenceSection />
|
||||
</> }
|
||||
<Divider style = { styles.fieldSeparator as ViewStyle } />
|
||||
<NotificationsSection />
|
||||
|
||||
{ showModeratorSettings
|
||||
&& <>
|
||||
<Divider style = { styles.fieldSeparator as ViewStyle } />
|
||||
<ModeratorSection />
|
||||
</> }
|
||||
<Divider style = { styles.fieldSeparator as ViewStyle } />
|
||||
<AdvancedSection />
|
||||
<Divider style = { styles.fieldSeparator as ViewStyle } />
|
||||
<LinksSection />
|
||||
</ScrollView>
|
||||
</JitsiScreen>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsView;
|
||||
1
react/features/settings/components/native/constants.ts
Normal file
1
react/features/settings/components/native/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const AVATAR_SIZE = 64;
|
||||
244
react/features/settings/components/native/styles.ts
Normal file
244
react/features/settings/components/native/styles.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
export const ANDROID_UNDERLINE_COLOR = 'transparent';
|
||||
export const PLACEHOLDER_COLOR = BaseTheme.palette.focus01;
|
||||
|
||||
/**
|
||||
* The styles of the native components of the feature {@code settings}.
|
||||
*/
|
||||
export default {
|
||||
|
||||
profileContainerWrapper: {
|
||||
margin: BaseTheme.spacing[4]
|
||||
},
|
||||
|
||||
profileContainer: {
|
||||
backgroundColor: BaseTheme.palette.ui02,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
padding: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
profileView: {
|
||||
flexGrow: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
|
||||
applyProfileSettingsButton: {
|
||||
marginHorizontal: BaseTheme.spacing[4],
|
||||
marginVertical: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
padding: BaseTheme.spacing[3],
|
||||
margin: BaseTheme.spacing[4]
|
||||
},
|
||||
|
||||
gavatarMessageContainer: {
|
||||
marginHorizontal: BaseTheme.spacing[4],
|
||||
color: BaseTheme.palette.text02,
|
||||
marginTop: -BaseTheme.spacing[2],
|
||||
...BaseTheme.typography.bodyShortRegular
|
||||
},
|
||||
|
||||
displayName: {
|
||||
...BaseTheme.typography.bodyLongRegularLarge,
|
||||
color: BaseTheme.palette.text01,
|
||||
marginLeft: BaseTheme.spacing[3],
|
||||
position: 'relative'
|
||||
},
|
||||
|
||||
profileViewArrow: {
|
||||
position: 'absolute',
|
||||
right: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for screen container.
|
||||
*/
|
||||
settingsViewContainer: {
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* Standardized style for a field container {@code View}.
|
||||
*/
|
||||
fieldContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
minHeight: BaseTheme.spacing[8],
|
||||
paddingHorizontal: BaseTheme.spacing[2],
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
|
||||
/**
|
||||
* * Appended style for column layout fields.
|
||||
*/
|
||||
fieldContainerColumn: {
|
||||
alignItems: 'flex-start',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
/**
|
||||
* Standard container for a {@code View} containing a field label.
|
||||
*/
|
||||
fieldLabelContainer: {
|
||||
alignItems: 'center',
|
||||
flexShrink: 1,
|
||||
flexDirection: 'row',
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
paddingRight: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
/**
|
||||
* Text of the field labels on the form.
|
||||
*/
|
||||
fieldLabelText: {
|
||||
...BaseTheme.typography.bodyShortRegularLarge
|
||||
},
|
||||
|
||||
/**
|
||||
* Field container style for all but last row {@code View}.
|
||||
*/
|
||||
fieldSeparator: {
|
||||
marginHorizontal: BaseTheme.spacing[4],
|
||||
borderBottomWidth: 1,
|
||||
borderColor: BaseTheme.palette.ui05,
|
||||
marginVertical: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the {@code View} containing each
|
||||
* field values (the actual field).
|
||||
*/
|
||||
fieldValueContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
flexShrink: 1,
|
||||
justifyContent: 'flex-end',
|
||||
paddingRight: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the form section separator titles.
|
||||
*/
|
||||
|
||||
formSectionTitleContent: {
|
||||
backgroundColor: BaseTheme.palette.ui02,
|
||||
paddingVertical: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
formSectionTitleText: {
|
||||
...BaseTheme.typography.bodyShortBold,
|
||||
color: BaseTheme.palette.text02,
|
||||
marginHorizontal: BaseTheme.spacing[4],
|
||||
marginVertical: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
/**
|
||||
* Global {@code Text} color for the components.
|
||||
*/
|
||||
text: {
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
/**
|
||||
* Text input container style.
|
||||
*/
|
||||
customContainer: {
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
marginHorizontal: BaseTheme.spacing[4],
|
||||
marginTop: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
languageButtonContainer: {
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
languageButton: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
height: BaseTheme.spacing[7],
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
languageOption: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
height: BaseTheme.spacing[6],
|
||||
marginHorizontal: BaseTheme.spacing[4],
|
||||
borderBottomWidth: 1,
|
||||
borderColor: BaseTheme.palette.ui05
|
||||
},
|
||||
|
||||
selectedLanguage: {
|
||||
color: BaseTheme.palette.text03
|
||||
},
|
||||
|
||||
languageText: {
|
||||
...BaseTheme.typography.bodyShortRegularLarge,
|
||||
color: BaseTheme.palette.text01,
|
||||
marginHorizontal: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
/**
|
||||
* Standard text input field style.
|
||||
*/
|
||||
textInputField: {
|
||||
color: BaseTheme.palette.field01,
|
||||
flex: 1,
|
||||
...BaseTheme.typography.bodyShortRegularLarge,
|
||||
textAlign: 'right'
|
||||
},
|
||||
|
||||
/**
|
||||
* Appended style for column layout fields.
|
||||
*/
|
||||
textInputFieldColumn: {
|
||||
backgroundColor: 'rgb(245, 245, 245)',
|
||||
borderRadius: 8,
|
||||
marginVertical: 5,
|
||||
paddingVertical: 3,
|
||||
textAlign: 'left'
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for screen container.
|
||||
*/
|
||||
screenContainer: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
linksSection: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
linksButton: {
|
||||
width: '33%',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
...BaseTheme.typography.bodyShortBoldLarge
|
||||
},
|
||||
|
||||
logBtn: {
|
||||
marginRight: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
backBtn: {
|
||||
marginLeft: BaseTheme.spacing[3]
|
||||
}
|
||||
};
|
||||
307
react/features/settings/components/web/CalendarTab.tsx
Normal file
307
react/features/settings/components/web/CalendarTab.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import Spinner from '../../../base/ui/components/web/Spinner';
|
||||
import { bootstrapCalendarIntegration, clearCalendarIntegration, signIn } from '../../../calendar-sync/actions';
|
||||
import MicrosoftSignInButton from '../../../calendar-sync/components/MicrosoftSignInButton';
|
||||
import { CALENDAR_TYPE } from '../../../calendar-sync/constants';
|
||||
import { isCalendarEnabled } from '../../../calendar-sync/functions';
|
||||
import GoogleSignInButton from '../../../google-api/components/GoogleSignInButton';
|
||||
import logger from '../../logger';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link CalendarTab}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The name given to this Jitsi Application.
|
||||
*/
|
||||
_appName: string;
|
||||
|
||||
/**
|
||||
* Whether or not to display a button to sign in to Google.
|
||||
*/
|
||||
_enableGoogleIntegration: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to display a button to sign in to Microsoft.
|
||||
*/
|
||||
_enableMicrosoftIntegration: boolean;
|
||||
|
||||
/**
|
||||
* The current calendar integration in use, if any.
|
||||
*/
|
||||
_isConnectedToCalendar: boolean;
|
||||
|
||||
/**
|
||||
* The email address associated with the calendar integration in use.
|
||||
*/
|
||||
_profileEmail?: string;
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* Invoked to change the configured calendar integration.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link CalendarTab}.
|
||||
*/
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* Whether or not any third party APIs are being loaded.
|
||||
*/
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center' as const,
|
||||
minHeight: '100px',
|
||||
color: theme.palette.text01,
|
||||
...theme.typography.bodyShortRegular
|
||||
},
|
||||
|
||||
button: {
|
||||
marginTop: theme.spacing(4)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying calendar integration.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class CalendarTab extends Component<IProps, IState> {
|
||||
/**
|
||||
* Initializes a new {@code CalendarTab} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loading: true
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onClickDisconnect = this._onClickDisconnect.bind(this);
|
||||
this._onClickGoogle = this._onClickGoogle.bind(this);
|
||||
this._onClickMicrosoft = this._onClickMicrosoft.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads third party APIs as needed and bootstraps the initial calendar
|
||||
* state if not already set.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidMount() {
|
||||
this.props.dispatch(bootstrapCalendarIntegration())
|
||||
.catch((err: any) => logger.error('CalendarTab bootstrap failed', err))
|
||||
.then(() => this.setState({ loading: false }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let view;
|
||||
|
||||
if (this.state.loading) {
|
||||
view = this._renderLoadingState();
|
||||
} else if (this.props._isConnectedToCalendar) {
|
||||
view = this._renderSignOutState();
|
||||
} else {
|
||||
view = this._renderSignInState();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
{ view }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the action to start the sign in flow for a given calendar
|
||||
* integration type.
|
||||
*
|
||||
* @param {string} type - The calendar type to try integrating with.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_attemptSignIn(type: string) {
|
||||
this.props.dispatch(signIn(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to sign out of the currently connected third party
|
||||
* used for calendar integration.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClickDisconnect() {
|
||||
// We clear the integration state instead of actually signing out. This
|
||||
// is for two primary reasons. Microsoft does not support a sign out and
|
||||
// instead relies on clearing of local auth data. Google signout can
|
||||
// also sign the user out of YouTube. So for now we've decided not to
|
||||
// do an actual sign out.
|
||||
this.props.dispatch(clearCalendarIntegration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the sign in flow for Google calendar integration.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClickGoogle() {
|
||||
this._attemptSignIn(CALENDAR_TYPE.GOOGLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the sign in flow for Microsoft calendar integration.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClickMicrosoft() {
|
||||
this._attemptSignIn(CALENDAR_TYPE.MICROSOFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a React Element to indicate third party APIs are being loaded.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderLoadingState() {
|
||||
return (
|
||||
<Spinner />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a React Element to sign into a third party for calendar
|
||||
* integration.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSignInState() {
|
||||
const {
|
||||
_appName,
|
||||
_enableGoogleIntegration,
|
||||
_enableMicrosoftIntegration,
|
||||
t
|
||||
} = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{ t('settings.calendar.about',
|
||||
{ appName: _appName || '' }) }
|
||||
</p>
|
||||
{ _enableGoogleIntegration
|
||||
&& <div className = { classes.button }>
|
||||
<GoogleSignInButton
|
||||
onClick = { this._onClickGoogle }
|
||||
text = { t('liveStreaming.signIn') } />
|
||||
</div> }
|
||||
{ _enableMicrosoftIntegration
|
||||
&& <div className = { classes.button }>
|
||||
<MicrosoftSignInButton
|
||||
onClick = { this._onClickMicrosoft }
|
||||
text = { t('settings.calendar.microsoftSignIn') } />
|
||||
</div> }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a React Element to sign out of the currently connected third
|
||||
* party used for calendar integration.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSignOutState() {
|
||||
const { _profileEmail, t } = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ t('settings.calendar.signedIn',
|
||||
{ email: _profileEmail }) }
|
||||
<Button
|
||||
className = { classes.button }
|
||||
id = 'calendar_logout'
|
||||
label = { t('settings.calendar.disconnect') }
|
||||
onClick = { this._onClickDisconnect } />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code CalendarTab} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _appName: string,
|
||||
* _enableGoogleIntegration: boolean,
|
||||
* _enableMicrosoftIntegration: boolean,
|
||||
* _isConnectedToCalendar: boolean,
|
||||
* _profileEmail: string
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const calendarState = state['features/calendar-sync'] || {};
|
||||
const {
|
||||
googleApiApplicationClientID,
|
||||
microsoftApiApplicationClientID
|
||||
} = state['features/base/config'];
|
||||
const calendarEnabled = isCalendarEnabled(state);
|
||||
|
||||
return {
|
||||
_appName: interfaceConfig.APP_NAME,
|
||||
_enableGoogleIntegration: Boolean(
|
||||
calendarEnabled && googleApiApplicationClientID),
|
||||
_enableMicrosoftIntegration: Boolean(
|
||||
calendarEnabled && microsoftApiApplicationClientID),
|
||||
_isConnectedToCalendar: calendarState.integrationReady,
|
||||
_profileEmail: calendarState.profileEmail
|
||||
};
|
||||
}
|
||||
|
||||
export default withStyles(translate(connect(_mapStateToProps)(CalendarTab)), styles);
|
||||
38
react/features/settings/components/web/LogoutDialog.tsx
Normal file
38
react/features/settings/components/web/LogoutDialog.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
|
||||
/**
|
||||
* The type of {@link LogoutDialog}'s React {@code Component} props.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Logout handler.
|
||||
*/
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Logout dialog.
|
||||
*
|
||||
* @param {Object} props - The props of the component.
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
function LogoutDialog({ onLogout, t }: IProps) {
|
||||
return (
|
||||
<Dialog
|
||||
ok = {{ translationKey: 'dialog.Yes' }}
|
||||
onSubmit = { onLogout }
|
||||
titleKey = { t('dialog.logoutTitle') }>
|
||||
<div>
|
||||
{ t('dialog.logoutQuestion') }
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(connect()(LogoutDialog));
|
||||
281
react/features/settings/components/web/ModeratorTab.tsx
Normal file
281
react/features/settings/components/web/ModeratorTab.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps } from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ModeratorTab}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
/**
|
||||
* Whether the user has selected the audio moderation feature to be enabled.
|
||||
*/
|
||||
audioModerationEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether the user has selected the chat with permissions feature to be enabled.
|
||||
*/
|
||||
chatWithPermissionsEnabled: boolean;
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* Whether to hide chat with permissions.
|
||||
*/
|
||||
disableChatWithPermissions: boolean;
|
||||
|
||||
/**
|
||||
* If set hides the reactions moderation setting.
|
||||
*/
|
||||
disableReactionsModeration: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not follow me is currently active (enabled by some other participant).
|
||||
*/
|
||||
followMeActive: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the user has selected the Follow Me feature to be enabled.
|
||||
*/
|
||||
followMeEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether follow me for recorder is currently active (enabled by some other participant).
|
||||
*/
|
||||
followMeRecorderActive: boolean;
|
||||
|
||||
/**
|
||||
* Whether the user has selected the Follow Me Recorder feature to be enabled.
|
||||
*/
|
||||
followMeRecorderEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the user has selected the Start Audio Muted feature to be
|
||||
* enabled.
|
||||
*/
|
||||
startAudioMuted: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the user has selected the Start Reactions Muted feature to be
|
||||
* enabled.
|
||||
*/
|
||||
startReactionsMuted: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the user has selected the Start Video Muted feature to be
|
||||
* enabled.
|
||||
*/
|
||||
startVideoMuted: boolean;
|
||||
|
||||
/**
|
||||
* Whether the user has selected the video moderation feature to be enabled.
|
||||
*/
|
||||
videoModerationEnabled: boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const
|
||||
},
|
||||
|
||||
title: {
|
||||
...theme.typography.heading6,
|
||||
color: `${theme.palette.text01} !important`,
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
checkbox: {
|
||||
marginBottom: theme.spacing(3)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying language and moderator settings.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class ModeratorTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code ModeratorTab} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onStartAudioMutedChanged = this._onStartAudioMutedChanged.bind(this);
|
||||
this._onStartVideoMutedChanged = this._onStartVideoMutedChanged.bind(this);
|
||||
this._onStartReactionsMutedChanged = this._onStartReactionsMutedChanged.bind(this);
|
||||
this._onFollowMeEnabledChanged = this._onFollowMeEnabledChanged.bind(this);
|
||||
this._onFollowMeRecorderEnabledChanged = this._onFollowMeRecorderEnabledChanged.bind(this);
|
||||
this._onChatWithPermissionsChanged = this._onChatWithPermissionsChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if conferences should start
|
||||
* with audio muted.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onStartAudioMutedChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ startAudioMuted: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if conferences should start
|
||||
* with video disabled.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onStartVideoMutedChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ startVideoMuted: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if conferences should start
|
||||
* with reactions muted.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onStartReactionsMutedChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ startReactionsMuted: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if follow-me mode
|
||||
* should be activated.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFollowMeEnabledChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({
|
||||
followMeEnabled: checked,
|
||||
followMeRecorderEnabled: checked ? false : undefined
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if follow-me for recorder mode should be activated.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFollowMeRecorderEnabledChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({
|
||||
followMeEnabled: checked ? false : undefined,
|
||||
followMeRecorderEnabled: checked
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if chat with permissions should be activated.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChatWithPermissionsChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ chatWithPermissionsEnabled: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
audioModerationEnabled,
|
||||
chatWithPermissionsEnabled,
|
||||
disableChatWithPermissions,
|
||||
disableReactionsModeration,
|
||||
followMeActive,
|
||||
followMeEnabled,
|
||||
followMeRecorderActive,
|
||||
followMeRecorderEnabled,
|
||||
startAudioMuted,
|
||||
startVideoMuted,
|
||||
startReactionsMuted,
|
||||
t,
|
||||
videoModerationEnabled
|
||||
} = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
const followMeRecorderChecked = followMeRecorderEnabled && !followMeRecorderActive;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { `moderator-tab ${classes.container}` }
|
||||
key = 'moderator'>
|
||||
<h2 className = { classes.title }>
|
||||
{t('settings.moderatorOptions')}
|
||||
</h2>
|
||||
{ !audioModerationEnabled && <Checkbox
|
||||
checked = { startAudioMuted }
|
||||
className = { classes.checkbox }
|
||||
label = { t('settings.startAudioMuted') }
|
||||
name = 'start-audio-muted'
|
||||
onChange = { this._onStartAudioMutedChanged } /> }
|
||||
{ !videoModerationEnabled && <Checkbox
|
||||
checked = { startVideoMuted }
|
||||
className = { classes.checkbox }
|
||||
label = { t('settings.startVideoMuted') }
|
||||
name = 'start-video-muted'
|
||||
onChange = { this._onStartVideoMutedChanged } /> }
|
||||
<Checkbox
|
||||
checked = { followMeEnabled && !followMeActive && !followMeRecorderChecked }
|
||||
className = { classes.checkbox }
|
||||
disabled = { followMeActive || followMeRecorderActive }
|
||||
label = { t('settings.followMe') }
|
||||
name = 'follow-me'
|
||||
onChange = { this._onFollowMeEnabledChanged } />
|
||||
<Checkbox
|
||||
checked = { followMeRecorderChecked }
|
||||
className = { classes.checkbox }
|
||||
disabled = { followMeRecorderActive || followMeActive }
|
||||
label = { t('settings.followMeRecorder') }
|
||||
name = 'follow-me-recorder'
|
||||
onChange = { this._onFollowMeRecorderEnabledChanged } />
|
||||
{ !disableReactionsModeration
|
||||
&& <Checkbox
|
||||
checked = { startReactionsMuted }
|
||||
className = { classes.checkbox }
|
||||
label = { t('settings.startReactionsMuted') }
|
||||
name = 'start-reactions-muted'
|
||||
onChange = { this._onStartReactionsMutedChanged } /> }
|
||||
{ !disableChatWithPermissions
|
||||
&& <Checkbox
|
||||
checked = { chatWithPermissionsEnabled }
|
||||
className = { classes.checkbox }
|
||||
label = { t('settings.chatWithPermissions') }
|
||||
name = 'chat-with-permissions'
|
||||
onChange = { this._onChatWithPermissionsChanged } /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(translate(ModeratorTab), styles);
|
||||
283
react/features/settings/components/web/MoreTab.tsx
Normal file
283
react/features/settings/components/web/MoreTab.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps
|
||||
} from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
import Select from '../../../base/ui/components/web/Select';
|
||||
import { MAX_ACTIVE_PARTICIPANTS } from '../../../filmstrip/constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link MoreTab}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* Indicates if closed captions are enabled.
|
||||
*/
|
||||
areClosedCaptionsEnabled: boolean;
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* The currently selected language to display in the language select
|
||||
* dropdown.
|
||||
*/
|
||||
currentLanguage: string;
|
||||
|
||||
/**
|
||||
* Whether to show hide self view setting.
|
||||
*/
|
||||
disableHideSelfView: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not follow me is currently active (enabled by some other participant).
|
||||
*/
|
||||
followMeActive: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to hide self-view screen.
|
||||
*/
|
||||
hideSelfView: boolean;
|
||||
|
||||
/**
|
||||
* Whether we are in visitors mode.
|
||||
*/
|
||||
iAmVisitor: boolean;
|
||||
|
||||
/**
|
||||
* All available languages to display in the language select dropdown.
|
||||
*/
|
||||
languages: Array<string>;
|
||||
|
||||
/**
|
||||
* The number of max participants to display on stage.
|
||||
*/
|
||||
maxStageParticipants: number;
|
||||
|
||||
/**
|
||||
* Whether or not to display the language select dropdown.
|
||||
*/
|
||||
showLanguageSettings: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to display moderator-only settings.
|
||||
*/
|
||||
showModeratorSettings: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to show subtitles on stage.
|
||||
*/
|
||||
showSubtitlesOnStage: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the stage filmstrip is enabled.
|
||||
*/
|
||||
stageFilmstripEnabled: boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
padding: '0 2px'
|
||||
},
|
||||
|
||||
divider: {
|
||||
margin: `${theme.spacing(4)} 0`,
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
border: 0,
|
||||
backgroundColor: theme.palette.ui03
|
||||
},
|
||||
|
||||
checkbox: {
|
||||
margin: `${theme.spacing(3)} 0`
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying language and moderator settings.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class MoreTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code MoreTab} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._renderMaxStageParticipantsSelect = this._renderMaxStageParticipantsSelect.bind(this);
|
||||
this._onMaxStageParticipantsSelect = this._onMaxStageParticipantsSelect.bind(this);
|
||||
this._onHideSelfViewChanged = this._onHideSelfViewChanged.bind(this);
|
||||
this._onShowSubtitlesOnStageChanged = this._onShowSubtitlesOnStageChanged.bind(this);
|
||||
this._onLanguageItemSelect = this._onLanguageItemSelect.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
areClosedCaptionsEnabled,
|
||||
disableHideSelfView,
|
||||
iAmVisitor,
|
||||
hideSelfView,
|
||||
showLanguageSettings,
|
||||
showSubtitlesOnStage,
|
||||
t
|
||||
} = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { clsx('more-tab', classes.container) }
|
||||
key = 'more'>
|
||||
{this._renderMaxStageParticipantsSelect()}
|
||||
{!disableHideSelfView && !iAmVisitor && (
|
||||
<Checkbox
|
||||
checked = { hideSelfView }
|
||||
className = { classes.checkbox }
|
||||
label = { t('videothumbnail.hideSelfView') }
|
||||
name = 'hide-self-view'
|
||||
onChange = { this._onHideSelfViewChanged } />
|
||||
)}
|
||||
{areClosedCaptionsEnabled && <Checkbox
|
||||
checked = { showSubtitlesOnStage }
|
||||
className = { classes.checkbox }
|
||||
label = { t('settings.showSubtitlesOnStage') }
|
||||
name = 'show-subtitles-button'
|
||||
onChange = { this._onShowSubtitlesOnStageChanged } /> }
|
||||
{showLanguageSettings && this._renderLanguageSelect()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select a max number of stage participants from the select dropdown.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMaxStageParticipantsSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const maxParticipants = Number(e.target.value);
|
||||
|
||||
super._onChange({ maxStageParticipants: maxParticipants });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if hide self view should be enabled.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onHideSelfViewChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ hideSelfView: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if show subtitles button should be enabled.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onShowSubtitlesOnStageChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ showSubtitlesOnStage: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select a language from select dropdown.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLanguageItemSelect(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const language = e.target.value;
|
||||
|
||||
super._onChange({ currentLanguage: language });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for the max stage participants dropdown.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderMaxStageParticipantsSelect() {
|
||||
const { maxStageParticipants, t, stageFilmstripEnabled } = this.props;
|
||||
|
||||
if (!stageFilmstripEnabled) {
|
||||
return null;
|
||||
}
|
||||
const maxParticipantsItems = Array(MAX_ACTIVE_PARTICIPANTS).fill(0)
|
||||
.map((no, index) => {
|
||||
return {
|
||||
value: index + 1,
|
||||
label: `${index + 1}`
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
id = 'more-maxStageParticipants-select'
|
||||
label = { t('settings.maxStageParticipants') }
|
||||
onChange = { this._onMaxStageParticipantsSelect }
|
||||
options = { maxParticipantsItems }
|
||||
value = { maxStageParticipants } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the menu item for changing displayed language.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderLanguageSelect() {
|
||||
const {
|
||||
currentLanguage,
|
||||
languages,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const languageItems
|
||||
= languages.map((language: string) => {
|
||||
return {
|
||||
value: language,
|
||||
label: t(`languages:${language}`)
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Select
|
||||
id = 'more-language-select'
|
||||
label = { t('settings.language') }
|
||||
onChange = { this._onLanguageItemSelect }
|
||||
options = { languageItems }
|
||||
value = { currentLanguage } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(translate(MoreTab), styles);
|
||||
273
react/features/settings/components/web/NotificationsTab.tsx
Normal file
273
react/features/settings/components/web/NotificationsTab.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps } from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link NotificationsTab}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* Array of disabled sounds ids.
|
||||
*/
|
||||
disabledSounds: string[];
|
||||
|
||||
/**
|
||||
* Whether or not the reactions feature is enabled.
|
||||
*/
|
||||
enableReactions: Boolean;
|
||||
|
||||
/**
|
||||
* The types of enabled notifications that can be configured and their specific visibility.
|
||||
*/
|
||||
enabledNotifications: Object;
|
||||
|
||||
/**
|
||||
* Whether or not moderator muted the sounds.
|
||||
*/
|
||||
moderatorMutedSoundsReactions: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to display notifications settings.
|
||||
*/
|
||||
showNotificationsSettings: boolean;
|
||||
|
||||
/**
|
||||
* Whether sound settings should be displayed or not.
|
||||
*/
|
||||
showSoundsSettings: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the incoming message should play.
|
||||
*/
|
||||
soundsIncomingMessage: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the participant joined should play.
|
||||
*/
|
||||
soundsParticipantJoined: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the participant entering the lobby should play.
|
||||
*/
|
||||
soundsParticipantKnocking: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the participant left should play.
|
||||
*/
|
||||
soundsParticipantLeft: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for reactions should play.
|
||||
*/
|
||||
soundsReactions: Boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the sound for the talk while muted notification should play.
|
||||
*/
|
||||
soundsTalkWhileMuted: Boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
|
||||
'@media (max-width: 607px)': {
|
||||
flexDirection: 'column' as const
|
||||
}
|
||||
},
|
||||
|
||||
column: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
flex: 1,
|
||||
|
||||
'&:first-child:not(:last-child)': {
|
||||
marginRight: theme.spacing(3),
|
||||
|
||||
'@media (max-width: 607px)': {
|
||||
marginRight: 0,
|
||||
marginBottom: theme.spacing(3)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
title: {
|
||||
...theme.typography.heading6,
|
||||
color: `${theme.palette.text01} !important`,
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
checkbox: {
|
||||
marginBottom: theme.spacing(3)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying the local user's sound settings.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class NotificationsTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code SoundsTab} instance.
|
||||
*
|
||||
* @param {IProps} props - The React {@code Component} props to initialize
|
||||
* the new {@code SoundsTab} instance with.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onEnabledNotificationsChanged = this._onEnabledNotificationsChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes a sound setting state.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override _onChange({ target }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ [target.name]: target.checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if the given type of
|
||||
* notifications should be shown.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
* @param {string} type - The type of the notification.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onEnabledNotificationsChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>, type: any) {
|
||||
super._onChange({
|
||||
enabledNotifications: {
|
||||
...this.props.enabledNotifications,
|
||||
[type]: checked
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
disabledSounds,
|
||||
enabledNotifications,
|
||||
showNotificationsSettings,
|
||||
showSoundsSettings,
|
||||
soundsIncomingMessage,
|
||||
soundsParticipantJoined,
|
||||
soundsParticipantKnocking,
|
||||
soundsParticipantLeft,
|
||||
soundsTalkWhileMuted,
|
||||
soundsReactions,
|
||||
enableReactions,
|
||||
moderatorMutedSoundsReactions,
|
||||
t
|
||||
} = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
return (
|
||||
<form
|
||||
className = { classes.container }
|
||||
key = 'sounds'>
|
||||
{showSoundsSettings && (
|
||||
<fieldset className = { classes.column }>
|
||||
<legend className = { classes.title }>
|
||||
{t('settings.playSounds')}
|
||||
</legend>
|
||||
{enableReactions && <Checkbox
|
||||
checked = { soundsReactions && !disabledSounds.includes('REACTION_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { Boolean(moderatorMutedSoundsReactions
|
||||
|| disabledSounds.includes('REACTION_SOUND')) }
|
||||
label = { t('settings.reactions') }
|
||||
name = 'soundsReactions'
|
||||
onChange = { this._onChange } />
|
||||
}
|
||||
<Checkbox
|
||||
checked = { soundsIncomingMessage && !disabledSounds.includes('INCOMING_MSG_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { disabledSounds.includes('INCOMING_MSG_SOUND') }
|
||||
label = { t('settings.incomingMessage') }
|
||||
name = 'soundsIncomingMessage'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsParticipantJoined
|
||||
&& !disabledSounds.includes('PARTICIPANT_JOINED_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { disabledSounds.includes('PARTICIPANT_JOINED_SOUND') }
|
||||
label = { t('settings.participantJoined') }
|
||||
name = 'soundsParticipantJoined'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsParticipantLeft && !disabledSounds.includes('PARTICIPANT_LEFT_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { disabledSounds.includes('PARTICIPANT_LEFT_SOUND') }
|
||||
label = { t('settings.participantLeft') }
|
||||
name = 'soundsParticipantLeft'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsTalkWhileMuted && !disabledSounds.includes('TALK_WHILE_MUTED_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { disabledSounds.includes('TALK_WHILE_MUTED_SOUND') }
|
||||
label = { t('settings.talkWhileMuted') }
|
||||
name = 'soundsTalkWhileMuted'
|
||||
onChange = { this._onChange } />
|
||||
<Checkbox
|
||||
checked = { soundsParticipantKnocking
|
||||
&& !disabledSounds.includes('KNOCKING_PARTICIPANT_SOUND') }
|
||||
className = { classes.checkbox }
|
||||
disabled = { disabledSounds.includes('KNOCKING_PARTICIPANT_SOUND') }
|
||||
label = { t('settings.participantKnocking') }
|
||||
name = 'soundsParticipantKnocking'
|
||||
onChange = { this._onChange } />
|
||||
</fieldset>
|
||||
)}
|
||||
{showNotificationsSettings && (
|
||||
<fieldset className = { classes.column }>
|
||||
<legend className = { classes.title }>
|
||||
{t('notify.displayNotifications')}
|
||||
</legend>
|
||||
{
|
||||
Object.keys(enabledNotifications).map(key => (
|
||||
<Checkbox
|
||||
checked = { Boolean(enabledNotifications[key as
|
||||
keyof typeof enabledNotifications]) }
|
||||
className = { classes.checkbox }
|
||||
key = { key }
|
||||
label = { t(key) }
|
||||
name = { `show-${key}` }
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
onChange = { e => this._onEnabledNotificationsChanged(e, key) } />
|
||||
))
|
||||
}
|
||||
</fieldset>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(translate(NotificationsTab), styles);
|
||||
251
react/features/settings/components/web/ProfileTab.tsx
Normal file
251
react/features/settings/components/web/ProfileTab.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import { createProfilePanelButtonEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IStore } from '../../../app/types';
|
||||
import { login, logout } from '../../../authentication/actions.web';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps } from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import Input from '../../../base/ui/components/web/Input';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ProfileTab}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* Whether server-side authentication is available.
|
||||
*/
|
||||
authEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The name of the currently (server-side) authenticated user.
|
||||
*/
|
||||
authLogin: string;
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* Invoked to change the configured calendar integration.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* The display name to display for the local participant.
|
||||
*/
|
||||
displayName: string;
|
||||
|
||||
/**
|
||||
* The email to display for the local participant.
|
||||
*/
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* Whether to hide the email input in the profile settings.
|
||||
*/
|
||||
hideEmailInSettings?: boolean;
|
||||
|
||||
/**
|
||||
* The id of the local participant.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* If the display name is read only.
|
||||
*/
|
||||
readOnlyName: boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
width: '100%',
|
||||
padding: '0 2px'
|
||||
},
|
||||
|
||||
avatarContainer: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
marginBottom: theme.spacing(4)
|
||||
},
|
||||
|
||||
bottomMargin: {
|
||||
marginBottom: theme.spacing(4)
|
||||
},
|
||||
|
||||
label: {
|
||||
color: `${theme.palette.text01} !important`,
|
||||
...theme.typography.bodyShortRegular,
|
||||
marginBottom: theme.spacing(2)
|
||||
},
|
||||
|
||||
name: {
|
||||
marginBottom: theme.spacing(1)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying the local user's profile.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class ProfileTab extends AbstractDialogTab<IProps, any> {
|
||||
static defaultProps = {
|
||||
displayName: '',
|
||||
email: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code ConnectedSettingsDialog} instance.
|
||||
*
|
||||
* @param {IProps} props - The React {@code Component} props to initialize
|
||||
* the new {@code ConnectedSettingsDialog} instance with.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onAuthToggle = this._onAuthToggle.bind(this);
|
||||
this._onDisplayNameChange = this._onDisplayNameChange.bind(this);
|
||||
this._onEmailChange = this._onEmailChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes display name of the user.
|
||||
*
|
||||
* @param {string} value - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDisplayNameChange(value: string) {
|
||||
super._onChange({ displayName: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes email of the user.
|
||||
*
|
||||
* @param {string} value - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onEmailChange(value: string) {
|
||||
super._onChange({ email: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
authEnabled,
|
||||
displayName,
|
||||
email,
|
||||
hideEmailInSettings,
|
||||
id,
|
||||
readOnlyName,
|
||||
t
|
||||
} = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
return (
|
||||
<div className = { classes.container } >
|
||||
<div className = { classes.avatarContainer }>
|
||||
<Avatar
|
||||
participantId = { id }
|
||||
size = { 60 } />
|
||||
</div>
|
||||
<Input
|
||||
className = { classes.bottomMargin }
|
||||
disabled = { readOnlyName }
|
||||
id = 'setDisplayName'
|
||||
label = { t('profile.setDisplayNameLabel') }
|
||||
name = 'name'
|
||||
onChange = { this._onDisplayNameChange }
|
||||
placeholder = { t('settings.name') }
|
||||
type = 'text'
|
||||
value = { displayName } />
|
||||
{!hideEmailInSettings && <div className = 'profile-edit-field'>
|
||||
<Input
|
||||
className = { classes.bottomMargin }
|
||||
id = 'setEmail'
|
||||
label = { t('profile.setEmailLabel') }
|
||||
name = 'email'
|
||||
onChange = { this._onEmailChange }
|
||||
placeholder = { t('profile.setEmailInput') }
|
||||
type = 'text'
|
||||
value = { email } />
|
||||
</div>}
|
||||
{ authEnabled && this._renderAuth() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the dialog for logging in or out of a server and closes this
|
||||
* dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAuthToggle() {
|
||||
if (this.props.authLogin) {
|
||||
sendAnalytics(createProfilePanelButtonEvent('logout.button'));
|
||||
|
||||
this.props.dispatch(logout());
|
||||
} else {
|
||||
sendAnalytics(createProfilePanelButtonEvent('login.button'));
|
||||
|
||||
this.props.dispatch(login());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a React Element for interacting with server-side authentication.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderAuth() {
|
||||
const {
|
||||
authLogin,
|
||||
t
|
||||
} = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className = { classes.label }>
|
||||
{ t('toolbar.authenticate') }
|
||||
</h2>
|
||||
{ authLogin
|
||||
&& <div className = { classes.name }>
|
||||
{ t('settings.loggedIn', { name: authLogin }) }
|
||||
</div> }
|
||||
<Button
|
||||
accessibilityLabel = { authLogin ? t('toolbar.logout') : t('toolbar.login') }
|
||||
id = 'login_button'
|
||||
label = { authLogin ? t('toolbar.logout') : t('toolbar.login') }
|
||||
onClick = { this._onAuthToggle } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(translate(connect()(ProfileTab)), styles);
|
||||
50
react/features/settings/components/web/SettingsButton.ts
Normal file
50
react/features/settings/components/web/SettingsButton.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconGear } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { openSettingsDialog } from '../../actions';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link SettingsButton}.
|
||||
*/
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* The default tab at which the settings dialog will be opened.
|
||||
*/
|
||||
defaultTab: string;
|
||||
|
||||
/**
|
||||
* Indicates whether the device selection dialog is displayed on the
|
||||
* welcome page or not.
|
||||
*/
|
||||
isDisplayedOnWelcomePage: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract implementation of a button for accessing settings.
|
||||
*/
|
||||
class SettingsButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.Settings';
|
||||
override icon = IconGear;
|
||||
override label = 'toolbar.Settings';
|
||||
override tooltip = 'toolbar.Settings';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens the appropriate dialog.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch, isDisplayedOnWelcomePage = false } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('settings'));
|
||||
dispatch(openSettingsDialog(undefined, isDisplayedOnWelcomePage));
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(SettingsButton));
|
||||
330
react/features/settings/components/web/SettingsDialog.tsx
Normal file
330
react/features/settings/components/web/SettingsDialog.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import {
|
||||
IconBell,
|
||||
IconCalendar,
|
||||
IconGear,
|
||||
IconImage,
|
||||
IconModerator,
|
||||
IconShortcuts,
|
||||
IconUser,
|
||||
IconVideo,
|
||||
IconVolumeUp
|
||||
} from '../../../base/icons/svg';
|
||||
import DialogWithTabs, { IDialogTab } from '../../../base/ui/components/web/DialogWithTabs';
|
||||
import { isCalendarEnabled } from '../../../calendar-sync/functions.web';
|
||||
import { submitAudioDeviceSelectionTab, submitVideoDeviceSelectionTab } from '../../../device-selection/actions.web';
|
||||
import AudioDevicesSelection from '../../../device-selection/components/AudioDevicesSelection';
|
||||
import VideoDeviceSelection from '../../../device-selection/components/VideoDeviceSelection';
|
||||
import {
|
||||
getAudioDeviceSelectionDialogProps,
|
||||
getVideoDeviceSelectionDialogProps
|
||||
} from '../../../device-selection/functions.web';
|
||||
import { checkBlurSupport, checkVirtualBackgroundEnabled } from '../../../virtual-background/functions';
|
||||
import { iAmVisitor } from '../../../visitors/functions';
|
||||
import {
|
||||
submitModeratorTab,
|
||||
submitMoreTab,
|
||||
submitNotificationsTab,
|
||||
submitProfileTab,
|
||||
submitShortcutsTab,
|
||||
submitVirtualBackgroundTab
|
||||
} from '../../actions';
|
||||
import { SETTINGS_TABS } from '../../constants';
|
||||
import {
|
||||
getModeratorTabProps,
|
||||
getMoreTabProps,
|
||||
getNotificationsMap,
|
||||
getNotificationsTabProps,
|
||||
getProfileTabProps,
|
||||
getShortcutsTabProps,
|
||||
getVirtualBackgroundTabProps
|
||||
} from '../../functions';
|
||||
|
||||
import CalendarTab from './CalendarTab';
|
||||
import ModeratorTab from './ModeratorTab';
|
||||
import MoreTab from './MoreTab';
|
||||
import NotificationsTab from './NotificationsTab';
|
||||
import ProfileTab from './ProfileTab';
|
||||
import ShortcutsTab from './ShortcutsTab';
|
||||
import VirtualBackgroundTab from './VirtualBackgroundTab';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link ConnectedSettingsDialog}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Information about the tabs to be rendered.
|
||||
*/
|
||||
_tabs: IDialogTab<any>[];
|
||||
|
||||
/**
|
||||
* Which settings tab should be initially displayed. If not defined then
|
||||
* the first tab will be displayed.
|
||||
*/
|
||||
defaultTab: string;
|
||||
|
||||
/**
|
||||
* Invoked to save changed settings.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Indicates whether the device selection dialog is displayed on the
|
||||
* welcome page or not.
|
||||
*/
|
||||
isDisplayedOnWelcomePage: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(() => {
|
||||
return {
|
||||
settingsDialog: {
|
||||
display: 'flex',
|
||||
width: '100%'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const SettingsDialog = ({ _tabs, defaultTab, dispatch }: IProps) => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const correctDefaultTab = _tabs.find(tab => tab.name === defaultTab)?.name;
|
||||
const tabs = _tabs.map(tab => {
|
||||
return {
|
||||
...tab,
|
||||
className: `settings-pane ${classes.settingsDialog}`,
|
||||
submit: (...args: any) => tab.submit
|
||||
&& dispatch(tab.submit(...args))
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<DialogWithTabs
|
||||
className = 'settings-dialog'
|
||||
defaultTab = { correctDefaultTab }
|
||||
tabs = { tabs }
|
||||
titleKey = 'settings.title' />
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code ConnectedSettingsDialog} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The props passed to the component.
|
||||
* @private
|
||||
* @returns {{
|
||||
* tabs: Array<Object>
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { isDisplayedOnWelcomePage } = ownProps;
|
||||
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
|
||||
|
||||
// The settings sections to display.
|
||||
const showDeviceSettings = configuredTabs.includes('devices');
|
||||
const moreTabProps = getMoreTabProps(state);
|
||||
const moderatorTabProps = getModeratorTabProps(state);
|
||||
const { showModeratorSettings } = moderatorTabProps;
|
||||
const showMoreTab = configuredTabs.includes('more');
|
||||
const showProfileSettings
|
||||
= configuredTabs.includes('profile') && !state['features/base/config'].disableProfile;
|
||||
const showCalendarSettings
|
||||
= configuredTabs.includes('calendar') && isCalendarEnabled(state);
|
||||
const showSoundsSettings = configuredTabs.includes('sounds');
|
||||
const enabledNotifications = getNotificationsMap(state);
|
||||
const showNotificationsSettings = Object.keys(enabledNotifications).length > 0;
|
||||
const virtualBackgroundSupported = checkBlurSupport();
|
||||
const enableVirtualBackground = checkVirtualBackgroundEnabled(state);
|
||||
const tabs: IDialogTab<any>[] = [];
|
||||
const _iAmVisitor = iAmVisitor(state);
|
||||
|
||||
if (showDeviceSettings) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.AUDIO,
|
||||
component: AudioDevicesSelection,
|
||||
labelKey: 'settings.audio',
|
||||
props: getAudioDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getAudioDeviceSelectionDialogProps>) => {
|
||||
// Ensure the device selection tab gets updated when new devices
|
||||
// are found by taking the new props and only preserving the
|
||||
// current user selected devices. If this were not done, the
|
||||
// tab would keep using a copy of the initial props it received,
|
||||
// leaving the device list to become stale.
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
noiseSuppressionEnabled: tabState.noiseSuppressionEnabled,
|
||||
selectedAudioInputId: tabState.selectedAudioInputId,
|
||||
selectedAudioOutputId: tabState.selectedAudioOutputId
|
||||
};
|
||||
},
|
||||
submit: (newState: any) => submitAudioDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
|
||||
icon: IconVolumeUp
|
||||
});
|
||||
!_iAmVisitor && tabs.push({
|
||||
name: SETTINGS_TABS.VIDEO,
|
||||
component: VideoDeviceSelection,
|
||||
labelKey: 'settings.video',
|
||||
props: getVideoDeviceSelectionDialogProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getVideoDeviceSelectionDialogProps>) => {
|
||||
// Ensure the device selection tab gets updated when new devices
|
||||
// are found by taking the new props and only preserving the
|
||||
// current user selected devices. If this were not done, the
|
||||
// tab would keep using a copy of the initial props it received,
|
||||
// leaving the device list to become stale.
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
currentFramerate: tabState?.currentFramerate,
|
||||
localFlipX: tabState.localFlipX,
|
||||
selectedVideoInputId: tabState.selectedVideoInputId
|
||||
};
|
||||
},
|
||||
submit: (newState: any) => submitVideoDeviceSelectionTab(newState, isDisplayedOnWelcomePage),
|
||||
icon: IconVideo
|
||||
});
|
||||
}
|
||||
|
||||
if (virtualBackgroundSupported && !_iAmVisitor && enableVirtualBackground) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.VIRTUAL_BACKGROUND,
|
||||
component: VirtualBackgroundTab,
|
||||
labelKey: 'virtualBackground.title',
|
||||
props: getVirtualBackgroundTabProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getVirtualBackgroundTabProps>,
|
||||
tabStates: any) => {
|
||||
const videoTabState = tabStates[tabs.findIndex(tab => tab.name === SETTINGS_TABS.VIDEO)];
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
selectedVideoInputId: videoTabState?.selectedVideoInputId || newProps.selectedVideoInputId,
|
||||
options: tabState.options
|
||||
};
|
||||
},
|
||||
submit: (newState: any) => submitVirtualBackgroundTab(newState),
|
||||
cancel: () => {
|
||||
const { options } = getVirtualBackgroundTabProps(state, isDisplayedOnWelcomePage);
|
||||
|
||||
return submitVirtualBackgroundTab({ options }, true);
|
||||
},
|
||||
icon: IconImage
|
||||
});
|
||||
}
|
||||
|
||||
if ((showSoundsSettings || showNotificationsSettings) && !_iAmVisitor) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.NOTIFICATIONS,
|
||||
component: NotificationsTab,
|
||||
labelKey: 'settings.notifications',
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getNotificationsTabProps>) => {
|
||||
return {
|
||||
...newProps,
|
||||
enabledNotifications: tabState?.enabledNotifications || {},
|
||||
soundsIncomingMessage: tabState?.soundsIncomingMessage,
|
||||
soundsParticipantJoined: tabState?.soundsParticipantJoined,
|
||||
soundsParticipantKnocking: tabState?.soundsParticipantKnocking,
|
||||
soundsParticipantLeft: tabState?.soundsParticipantLeft,
|
||||
soundsReactions: tabState?.soundsReactions,
|
||||
soundsTalkWhileMuted: tabState?.soundsTalkWhileMuted
|
||||
};
|
||||
},
|
||||
props: getNotificationsTabProps(state, showSoundsSettings),
|
||||
submit: submitNotificationsTab,
|
||||
icon: IconBell
|
||||
});
|
||||
}
|
||||
|
||||
if (showModeratorSettings && !_iAmVisitor) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.MODERATOR,
|
||||
component: ModeratorTab,
|
||||
labelKey: 'settings.moderator',
|
||||
props: moderatorTabProps,
|
||||
propsUpdateFunction: (tabState: any, newProps: typeof moderatorTabProps) => {
|
||||
// Updates tab props, keeping users selection
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
chatWithPermissionsEnabled: tabState?.chatWithPermissionsEnabled,
|
||||
followMeEnabled: tabState?.followMeEnabled,
|
||||
followMeRecorderEnabled: tabState?.followMeRecorderEnabled,
|
||||
startAudioMuted: tabState?.startAudioMuted,
|
||||
startVideoMuted: tabState?.startVideoMuted,
|
||||
startReactionsMuted: tabState?.startReactionsMuted
|
||||
};
|
||||
},
|
||||
submit: submitModeratorTab,
|
||||
icon: IconModerator
|
||||
});
|
||||
}
|
||||
|
||||
if (showProfileSettings) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.PROFILE,
|
||||
component: ProfileTab,
|
||||
labelKey: 'profile.title',
|
||||
props: getProfileTabProps(state),
|
||||
submit: submitProfileTab,
|
||||
icon: IconUser
|
||||
});
|
||||
}
|
||||
|
||||
if (showCalendarSettings && !_iAmVisitor) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.CALENDAR,
|
||||
component: CalendarTab,
|
||||
labelKey: 'settings.calendar.title',
|
||||
icon: IconCalendar
|
||||
});
|
||||
}
|
||||
|
||||
!_iAmVisitor && tabs.push({
|
||||
name: SETTINGS_TABS.SHORTCUTS,
|
||||
component: ShortcutsTab,
|
||||
labelKey: 'settings.shortcuts',
|
||||
props: getShortcutsTabProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getShortcutsTabProps>) => {
|
||||
// Updates tab props, keeping users selection
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
keyboardShortcutsEnabled: tabState?.keyboardShortcutsEnabled
|
||||
};
|
||||
},
|
||||
submit: submitShortcutsTab,
|
||||
icon: IconShortcuts
|
||||
});
|
||||
|
||||
if (showMoreTab && !_iAmVisitor) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.MORE,
|
||||
component: MoreTab,
|
||||
labelKey: 'settings.more',
|
||||
props: moreTabProps,
|
||||
propsUpdateFunction: (tabState: any, newProps: typeof moreTabProps) => {
|
||||
// Updates tab props, keeping users selection
|
||||
|
||||
return {
|
||||
...newProps,
|
||||
currentLanguage: tabState?.currentLanguage,
|
||||
hideSelfView: tabState?.hideSelfView,
|
||||
showSubtitlesOnStage: tabState?.showSubtitlesOnStage,
|
||||
maxStageParticipants: tabState?.maxStageParticipants
|
||||
};
|
||||
},
|
||||
submit: submitMoreTab,
|
||||
icon: IconGear
|
||||
});
|
||||
}
|
||||
|
||||
return { _tabs: tabs };
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(SettingsDialog);
|
||||
178
react/features/settings/components/web/ShortcutsTab.tsx
Normal file
178
react/features/settings/components/web/ShortcutsTab.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps } from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Checkbox from '../../../base/ui/components/web/Checkbox';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ShortcutsTab}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* Whether to display the shortcuts or not.
|
||||
*/
|
||||
displayShortcuts: boolean;
|
||||
|
||||
/**
|
||||
* Whether the keyboard shortcuts are enabled or not.
|
||||
*/
|
||||
keyboardShortcutsEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The keyboard shortcuts descriptions.
|
||||
*/
|
||||
keyboardShortcutsHelpDescriptions: Map<string, string>;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
width: '100%',
|
||||
paddingBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
checkbox: {
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
listContainer: {
|
||||
listStyleType: 'none',
|
||||
padding: 0,
|
||||
margin: 0
|
||||
},
|
||||
|
||||
listItem: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: `${theme.spacing(1)} 0`,
|
||||
...theme.typography.bodyShortRegular,
|
||||
color: theme.palette.text01
|
||||
},
|
||||
|
||||
listItemKey: {
|
||||
backgroundColor: theme.palette.ui04,
|
||||
...theme.typography.labelBold,
|
||||
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
|
||||
borderRadius: `${Number(theme.shape.borderRadius) / 2}px`
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying the local user's profile.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class ShortcutsTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code MoreTab} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onKeyboardShortcutEnableChanged = this._onKeyboardShortcutEnableChanged.bind(this);
|
||||
this._renderShortcutsListItem = this._renderShortcutsListItem.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if global keyboard shortcuts
|
||||
* should be enabled.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyboardShortcutEnableChanged({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) {
|
||||
super._onChange({ keyboardShortcutsEnabled: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a keyboard shortcut with key and description.
|
||||
*
|
||||
* @param {string} keyboardKey - The keyboard key for the shortcut.
|
||||
* @param {string} translationKey - The translation key for the shortcut description.
|
||||
* @returns {JSX}
|
||||
*/
|
||||
_renderShortcutsListItem(keyboardKey: string, translationKey: string) {
|
||||
const { t } = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
let modifierKey = 'Alt';
|
||||
|
||||
if (window.navigator?.platform) {
|
||||
if (window.navigator.platform.indexOf('Mac') !== -1) {
|
||||
modifierKey = '⌥';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className = { classes.listItem }
|
||||
key = { keyboardKey }>
|
||||
<span
|
||||
aria-label = { t(translationKey) }>
|
||||
{t(translationKey)}
|
||||
</span>
|
||||
<span className = { classes.listItemKey }>
|
||||
{keyboardKey.startsWith(':')
|
||||
? `${modifierKey} + ${keyboardKey.slice(1)}`
|
||||
: keyboardKey}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
displayShortcuts,
|
||||
keyboardShortcutsHelpDescriptions,
|
||||
keyboardShortcutsEnabled,
|
||||
t
|
||||
} = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
const shortcutDescriptions: Map<string, string> = displayShortcuts
|
||||
? keyboardShortcutsHelpDescriptions
|
||||
: new Map();
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<Checkbox
|
||||
checked = { keyboardShortcutsEnabled }
|
||||
className = { classes.checkbox }
|
||||
label = { t('prejoin.keyboardShortcuts') }
|
||||
name = 'enable-keyboard-shortcuts'
|
||||
onChange = { this._onKeyboardShortcutEnableChanged } />
|
||||
{displayShortcuts && (
|
||||
<ul className = { classes.listContainer }>
|
||||
{Array.from(shortcutDescriptions)
|
||||
.map(description => this._renderShortcutsListItem(...description))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(translate(ShortcutsTab), styles);
|
||||
102
react/features/settings/components/web/VirtualBackgroundTab.tsx
Normal file
102
react/features/settings/components/web/VirtualBackgroundTab.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps
|
||||
} from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import VirtualBackgrounds from '../../../virtual-background/components/VirtualBackgrounds';
|
||||
import { IVirtualBackground } from '../../../virtual-background/reducer';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VirtualBackgroundTab}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* Virtual background options.
|
||||
*/
|
||||
options: IVirtualBackground;
|
||||
|
||||
/**
|
||||
* The id of the selected video device.
|
||||
*/
|
||||
selectedVideoInputId: string;
|
||||
}
|
||||
|
||||
const styles = () => {
|
||||
return {
|
||||
container: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying language and moderator settings.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class VirtualBackgroundTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code VirtualBackgroundTab} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onOptionsChanged = this._onOptionsChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if follow-me mode
|
||||
* should be activated.
|
||||
*
|
||||
* @param {Object} options - The new background options.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOptionsChanged(options: any) {
|
||||
super._onChange({ options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
options,
|
||||
selectedVideoInputId
|
||||
} = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { classes.container }
|
||||
id = 'virtual-background-dialog'
|
||||
key = 'virtual-background'>
|
||||
<VirtualBackgrounds
|
||||
onOptionsChange = { this._onOptionsChanged }
|
||||
options = { options }
|
||||
selectedThumbnail = { options.selectedThumbnail ?? '' }
|
||||
selectedVideoInputId = { selectedVideoInputId } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(translate(VirtualBackgroundTab), styles);
|
||||
@@ -0,0 +1,357 @@
|
||||
/* 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';
|
||||
|
||||
const browser = JitsiMeetJS.util.browser;
|
||||
|
||||
/**
|
||||
* Translates the default device label into a more user friendly one.
|
||||
*
|
||||
* @param {string} deviceId - The device Id.
|
||||
* @param {string} label - The device label.
|
||||
* @param {Function} t - The translation function.
|
||||
* @returns {string}
|
||||
*/
|
||||
function transformDefaultDeviceLabel(deviceId: string, label: string, t: Function) {
|
||||
return deviceId === 'default'
|
||||
? t('settings.sameAsSystem', { label: label.replace('Default - ', '') })
|
||||
: label;
|
||||
}
|
||||
|
||||
export interface IProps {
|
||||
|
||||
/**
|
||||
* The deviceId of the microphone in use.
|
||||
*/
|
||||
currentMicDeviceId: string;
|
||||
|
||||
/**
|
||||
* The deviceId of the output device in use.
|
||||
*/
|
||||
currentOutputDeviceId?: string;
|
||||
|
||||
/**
|
||||
* 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; }>;
|
||||
|
||||
/**
|
||||
* Whether noise suppression is enabled or not.
|
||||
*/
|
||||
noiseSuppressionEnabled: boolean;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
prejoinVisible: boolean;
|
||||
|
||||
/**
|
||||
* Used to set a new microphone as the current one.
|
||||
*/
|
||||
setAudioInputDevice: Function;
|
||||
|
||||
/**
|
||||
* Used to set a new output device as the current one.
|
||||
*/
|
||||
setAudioOutputDevice: Function;
|
||||
|
||||
/**
|
||||
* Function to toggle noise suppression.
|
||||
*/
|
||||
toggleSuppression: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
contextMenu: {
|
||||
position: 'relative',
|
||||
right: 'auto',
|
||||
margin: 0,
|
||||
marginBottom: theme.spacing(1),
|
||||
maxHeight: 'calc(100dvh - 100px)',
|
||||
overflow: 'auto',
|
||||
width: '300px'
|
||||
},
|
||||
|
||||
header: {
|
||||
'&:hover': {
|
||||
backgroundColor: 'initial',
|
||||
cursor: 'initial'
|
||||
}
|
||||
},
|
||||
|
||||
list: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
listStyleType: 'none'
|
||||
},
|
||||
|
||||
checkboxContainer: {
|
||||
padding: '10px 16px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
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.
|
||||
*
|
||||
* @param {string} deviceId - The deviceId for the clicked microphone.
|
||||
* @returns {void}
|
||||
*/
|
||||
const _onMicrophoneEntryClick = useCallback((deviceId: string) => {
|
||||
setAudioInputDevice(deviceId);
|
||||
}, [ setAudioInputDevice ]);
|
||||
|
||||
/**
|
||||
* Click handler for the speaker entries.
|
||||
*
|
||||
* @param {string} deviceId - The deviceId for the clicked speaker.
|
||||
* @returns {void}
|
||||
*/
|
||||
const _onSpeakerEntryClick = useCallback((deviceId: string) => {
|
||||
setAudioOutputDevice(deviceId);
|
||||
}, [ setAudioOutputDevice ]);
|
||||
|
||||
/**
|
||||
* Renders a single microphone entry.
|
||||
*
|
||||
* @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.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
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 (
|
||||
<MicrophoneEntry
|
||||
deviceId = { deviceId }
|
||||
hasError = { hasError }
|
||||
index = { index }
|
||||
isSelected = { isSelected }
|
||||
jitsiTrack = { jitsiTrack }
|
||||
key = { `me-${index}` }
|
||||
length = { length }
|
||||
measureAudioLevels = { measureAudioLevels }
|
||||
onClick = { _onMicrophoneEntryClick }>
|
||||
{label}
|
||||
</MicrophoneEntry>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a single speaker entry.
|
||||
*
|
||||
* @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.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
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 === currentOutputDeviceId;
|
||||
|
||||
return (
|
||||
<SpeakerEntry
|
||||
deviceId = { deviceId }
|
||||
index = { index }
|
||||
isSelected = { isSelected }
|
||||
key = { key }
|
||||
length = { length }
|
||||
onClick = { _onSpeakerEntryClick }>
|
||||
{label}
|
||||
</SpeakerEntry>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
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.
|
||||
return;
|
||||
}
|
||||
|
||||
_disposeTracks(audioTracks);
|
||||
|
||||
const newAudioTracks = await createLocalAudioTracks(microphoneDevices, 5000);
|
||||
|
||||
if (_componentWasUnmounted.current) {
|
||||
_disposeTracks(newAudioTracks);
|
||||
} else {
|
||||
setAudioTracks(newAudioTracks);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
_setTracks();
|
||||
|
||||
return () => {
|
||||
_componentWasUnmounted.current = true;
|
||||
_disposeTracks(audioTracks);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!equals(microphoneDevices, microphoneDevicesRef.current)) {
|
||||
_setTracks();
|
||||
microphoneDevicesRef.current = microphoneDevices;
|
||||
}
|
||||
}, [ microphoneDevices ]);
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
activateFocusTrap = { true }
|
||||
aria-labelledby = 'audio-settings-button'
|
||||
className = { classes.contextMenu }
|
||||
hidden = { false }
|
||||
id = 'audio-settings-dialog'
|
||||
role = 'menu'
|
||||
tabIndex = { -1 }>
|
||||
<ContextMenuItemGroup
|
||||
aria-labelledby = { microphoneHeaderId }
|
||||
role = 'group'>
|
||||
<ContextMenuItem
|
||||
className = { classes.header }
|
||||
icon = { IconMic }
|
||||
id = { microphoneHeaderId }
|
||||
text = { t('settings.microphones') } />
|
||||
<ul
|
||||
className = { classes.list }
|
||||
role = 'presentation'>
|
||||
{audioTracks.map((data, i) =>
|
||||
_renderMicrophoneEntry(data, i, audioTracks.length)
|
||||
)}
|
||||
</ul>
|
||||
</ContextMenuItemGroup>
|
||||
{outputDevices.length > 0 && (
|
||||
<ContextMenuItemGroup
|
||||
aria-labelledby = { speakerHeaderId }
|
||||
role = 'group'>
|
||||
<ContextMenuItem
|
||||
className = { classes.header }
|
||||
icon = { IconVolumeUp }
|
||||
id = { speakerHeaderId }
|
||||
text = { t('settings.speakers') } />
|
||||
<ul
|
||||
className = { classes.list }
|
||||
role = 'presentation'>
|
||||
{outputDevices.map((data: any, i: number) =>
|
||||
_renderSpeakerEntry(data, i, outputDevices.length)
|
||||
)}
|
||||
</ul>
|
||||
</ContextMenuItemGroup>)
|
||||
}
|
||||
{!prejoinVisible && (
|
||||
<ContextMenuItemGroup>
|
||||
<div
|
||||
className = { classes.checkboxContainer }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick = { e => e.stopPropagation() }>
|
||||
<Checkbox
|
||||
checked = { noiseSuppressionEnabled }
|
||||
label = { t('toolbar.noiseSuppression') }
|
||||
onChange = { toggleSuppression } />
|
||||
</div>
|
||||
</ContextMenuItemGroup>
|
||||
)}
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: IReduxState) => {
|
||||
return {
|
||||
noiseSuppressionEnabled: isNoiseSuppressionEnabled(state),
|
||||
prejoinVisible: isPrejoinPageVisible(state)
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
|
||||
return {
|
||||
toggleSuppression() {
|
||||
dispatch(toggleNoiseSuppression());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AudioSettingsContent);
|
||||
@@ -0,0 +1,164 @@
|
||||
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';
|
||||
import {
|
||||
setAudioInputDeviceAndUpdateSettings,
|
||||
setAudioOutputDevice as setAudioOutputDeviceAction
|
||||
} from '../../../../base/devices/actions.web';
|
||||
import {
|
||||
getAudioInputDeviceData,
|
||||
getAudioOutputDeviceData
|
||||
} from '../../../../base/devices/functions.web';
|
||||
import Popover from '../../../../base/popover/components/Popover.web';
|
||||
import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants';
|
||||
import {
|
||||
getCurrentMicDeviceId,
|
||||
getCurrentOutputDeviceId
|
||||
} from '../../../../base/settings/functions.web';
|
||||
import { toggleAudioSettings } from '../../../actions';
|
||||
import { getAudioSettingsVisibility } from '../../../functions.web';
|
||||
|
||||
import AudioSettingsContent from './AudioSettingsContent';
|
||||
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Component's children (the audio button).
|
||||
*/
|
||||
children: ReactNode;
|
||||
|
||||
/**
|
||||
* The deviceId of the microphone in use.
|
||||
*/
|
||||
currentMicDeviceId: string;
|
||||
|
||||
/**
|
||||
* The deviceId of the output device in use.
|
||||
*/
|
||||
currentOutputDeviceId?: string;
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the popup.
|
||||
*/
|
||||
isOpen: boolean;
|
||||
|
||||
/**
|
||||
* 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; }>;
|
||||
|
||||
/**
|
||||
* Callback executed when the popup closes.
|
||||
*/
|
||||
onClose: Function;
|
||||
|
||||
/**
|
||||
* A list of objects containing the labels and deviceIds
|
||||
* of all the output devices.
|
||||
*/
|
||||
outputDevices: Array<{ deviceId: string; label: string; }>;
|
||||
|
||||
/**
|
||||
* The popup placement enum value.
|
||||
*/
|
||||
popupPlacement: string;
|
||||
|
||||
/**
|
||||
* Used to set a new microphone as the current one.
|
||||
*/
|
||||
setAudioInputDevice: Function;
|
||||
|
||||
/**
|
||||
* Used to set a new output device as the current one.
|
||||
*/
|
||||
setAudioOutputDevice: Function;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(() => {
|
||||
return {
|
||||
container: {
|
||||
display: 'inline-block'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Popup with audio settings.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function AudioSettingsPopup({
|
||||
children,
|
||||
currentMicDeviceId,
|
||||
currentOutputDeviceId,
|
||||
isOpen,
|
||||
microphoneDevices,
|
||||
setAudioInputDevice,
|
||||
setAudioOutputDevice,
|
||||
onClose,
|
||||
outputDevices,
|
||||
popupPlacement,
|
||||
measureAudioLevels
|
||||
}: IProps) {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { cx(classes.container, 'audio-preview') }>
|
||||
<Popover
|
||||
allowClick = { true }
|
||||
content = { <AudioSettingsContent
|
||||
currentMicDeviceId = { currentMicDeviceId }
|
||||
currentOutputDeviceId = { currentOutputDeviceId }
|
||||
measureAudioLevels = { measureAudioLevels }
|
||||
microphoneDevices = { microphoneDevices }
|
||||
outputDevices = { outputDevices }
|
||||
setAudioInputDevice = { setAudioInputDevice }
|
||||
setAudioOutputDevice = { setAudioOutputDevice } /> }
|
||||
headingId = 'audio-settings-button'
|
||||
onPopoverClose = { onClose }
|
||||
position = { popupPlacement }
|
||||
trigger = 'click'
|
||||
visible = { isOpen }>
|
||||
{children}
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { videoSpaceWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
return {
|
||||
popupPlacement: videoSpaceWidth <= Number(SMALL_MOBILE_WIDTH) ? 'auto' : 'top-end',
|
||||
currentMicDeviceId: getCurrentMicDeviceId(state),
|
||||
currentOutputDeviceId: getCurrentOutputDeviceId(state),
|
||||
isOpen: Boolean(getAudioSettingsVisibility(state)),
|
||||
microphoneDevices: getAudioInputDeviceData(state) ?? [],
|
||||
outputDevices: getAudioOutputDeviceData(state) ?? [],
|
||||
measureAudioLevels: areAudioLevelsEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onClose: toggleAudioSettings,
|
||||
setAudioInputDevice: setAudioInputDeviceAndUpdateSettings,
|
||||
setAudioOutputDevice: setAudioOutputDeviceAction
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AudioSettingsPopup);
|
||||
45
react/features/settings/components/web/audio/Meter.tsx
Normal file
45
react/features/settings/components/web/audio/Meter.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import Icon from '../../../../base/icons/components/Icon';
|
||||
import { IconMeter } from '../../../../base/icons/svg';
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Own class name for the component.
|
||||
*/
|
||||
className: string;
|
||||
|
||||
/**
|
||||
* Flag indicating whether the component is greyed out/disabled.
|
||||
*/
|
||||
isDisabled?: boolean;
|
||||
|
||||
/**
|
||||
* The level of the meter.
|
||||
* Should be between 0 and 7 as per the used SVG.
|
||||
*/
|
||||
level: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* React {@code Component} representing an audio level meter.
|
||||
*
|
||||
* @returns { ReactElement}
|
||||
*/
|
||||
export default function({ className, isDisabled, level }: IProps) {
|
||||
let ownClassName;
|
||||
|
||||
if (level > -1) {
|
||||
ownClassName = `metr metr-l-${level}`;
|
||||
} else {
|
||||
ownClassName = `metr ${isDisabled ? 'metr--disabled' : ''}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Icon
|
||||
className = { `${ownClassName} ${className}` }
|
||||
size = { 12 }
|
||||
src = { IconMeter } />
|
||||
);
|
||||
}
|
||||
223
react/features/settings/components/web/audio/MicrophoneEntry.tsx
Normal file
223
react/features/settings/components/web/audio/MicrophoneEntry.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../../base/icons/components/Icon';
|
||||
import { IconCheck, IconExclamationSolid } from '../../../../base/icons/svg';
|
||||
import JitsiMeetJS from '../../../../base/lib-jitsi-meet/_';
|
||||
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
|
||||
import { TEXT_OVERFLOW_TYPES } from '../../../../base/ui/constants.any';
|
||||
|
||||
import Meter from './Meter';
|
||||
|
||||
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The text for this component.
|
||||
*/
|
||||
children: string;
|
||||
|
||||
/**
|
||||
* The deviceId of the microphone.
|
||||
*/
|
||||
deviceId: string;
|
||||
|
||||
/**
|
||||
* Flag indicating if there is a problem with the device.
|
||||
*/
|
||||
hasError?: boolean;
|
||||
|
||||
/**
|
||||
* Index of the device item used to generate this entry.
|
||||
* Indexes are 0 based.
|
||||
*/
|
||||
index: number;
|
||||
|
||||
/**
|
||||
* Flag indicating the selection state.
|
||||
*/
|
||||
isSelected: boolean;
|
||||
|
||||
/**
|
||||
* The audio track for the current entry.
|
||||
*/
|
||||
jitsiTrack: any;
|
||||
|
||||
/**
|
||||
* The id for the label, that contains the item text.
|
||||
*/
|
||||
labelId?: string;
|
||||
|
||||
/**
|
||||
* The length of the microphone list.
|
||||
*/
|
||||
length: number;
|
||||
|
||||
|
||||
/**
|
||||
* Used to decide whether to listen to audio level changes.
|
||||
*/
|
||||
measureAudioLevels: boolean;
|
||||
|
||||
/**
|
||||
* Click handler for component.
|
||||
*/
|
||||
onClick: Function;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
position: 'relative'
|
||||
},
|
||||
|
||||
entryText: {
|
||||
maxWidth: '238px',
|
||||
|
||||
'&.withMeter': {
|
||||
maxWidth: '178px'
|
||||
},
|
||||
|
||||
'&.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,
|
||||
measureAudioLevels,
|
||||
onClick: propsClick
|
||||
}: IProps) => {
|
||||
const [ level, setLevel ] = useState(-1);
|
||||
const activeTrackRef = useRef(jitsiTrack);
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
/**
|
||||
* Click handler for the entry.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const onClick = useCallback(() => {
|
||||
propsClick(deviceId);
|
||||
}, [ propsClick, deviceId ]);
|
||||
|
||||
/**
|
||||
* Key pressed handler for the entry.
|
||||
*
|
||||
* @param {Object} e - The event.
|
||||
* @private
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const onKeyPress = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
propsClick(deviceId);
|
||||
}
|
||||
}, [ propsClick, deviceId ]);
|
||||
|
||||
/**
|
||||
* Updates the level of the meter.
|
||||
*
|
||||
* @param {number} num - The audio level provided by the jitsiTrack.
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateLevel = useCallback((num: number) => {
|
||||
setLevel(Math.floor(num / 0.125));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Subscribes to audio level changes coming from the jitsiTrack.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const startListening = () => {
|
||||
jitsiTrack && measureAudioLevels && jitsiTrack.on(
|
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||
updateLevel);
|
||||
};
|
||||
|
||||
/**
|
||||
* Unsubscribes from changes coming from the jitsiTrack.
|
||||
*
|
||||
* @param {Object} track - The jitsiTrack to unsubscribe from.
|
||||
* @returns {void}
|
||||
*/
|
||||
const stopListening = (track?: any) => {
|
||||
track?.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, updateLevel);
|
||||
setLevel(-1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
startListening();
|
||||
|
||||
return () => {
|
||||
stopListening(jitsiTrack);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
stopListening(activeTrackRef.current);
|
||||
startListening();
|
||||
activeTrackRef.current = jitsiTrack;
|
||||
}, [ jitsiTrack ]);
|
||||
|
||||
return (
|
||||
<li
|
||||
aria-checked = { isSelected }
|
||||
aria-posinset = { index + 1 } // Add one to offset the 0 based index.
|
||||
aria-setsize = { length }
|
||||
className = { classes.container }
|
||||
onClick = { onClick }
|
||||
onKeyPress = { onKeyPress }
|
||||
role = 'menuitemradio'
|
||||
tabIndex = { 0 }>
|
||||
<ContextMenuItem
|
||||
icon = { isSelected ? IconCheck : undefined }
|
||||
overflowType = { TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER }
|
||||
selected = { isSelected }
|
||||
text = { children }
|
||||
textClassName = { cx(classes.entryText,
|
||||
measureAudioLevels && 'withMeter',
|
||||
!isSelected && 'left-margin') }>
|
||||
{hasError && <Icon
|
||||
className = { classes.icon }
|
||||
size = { 16 }
|
||||
src = { IconExclamationSolid } />}
|
||||
</ContextMenuItem>
|
||||
{Boolean(jitsiTrack) && measureAudioLevels && <Meter
|
||||
className = { classes.meter }
|
||||
isDisabled = { hasError }
|
||||
level = { level } />
|
||||
}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default MicrophoneEntry;
|
||||
176
react/features/settings/components/web/audio/SpeakerEntry.tsx
Normal file
176
react/features/settings/components/web/audio/SpeakerEntry.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IconCheck } from '../../../../base/icons/svg';
|
||||
import Button from '../../../../base/ui/components/web/Button';
|
||||
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
|
||||
import { BUTTON_TYPES, TEXT_OVERFLOW_TYPES } from '../../../../base/ui/constants.any';
|
||||
import logger from '../../../logger';
|
||||
|
||||
const TEST_SOUND_PATH = 'sounds/ring.mp3';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link SpeakerEntry}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The text label for the entry.
|
||||
*/
|
||||
children: string;
|
||||
|
||||
/**
|
||||
* The deviceId of the speaker.
|
||||
*/
|
||||
deviceId: string;
|
||||
|
||||
/**
|
||||
* Index of the device item used to generate this entry.
|
||||
* Indexes are 0 based.
|
||||
*/
|
||||
index: number;
|
||||
|
||||
/**
|
||||
* Flag controlling the selection state of the entry.
|
||||
*/
|
||||
isSelected: boolean;
|
||||
|
||||
/**
|
||||
* Flag controlling the selection state of the entry.
|
||||
*/
|
||||
length: number;
|
||||
|
||||
/**
|
||||
* Click handler for the component.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param {IProps} props - Component props.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const SpeakerEntry = (props: IProps) => {
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
const { classes, cx } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
* Click handler for the entry.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onClick() {
|
||||
props.onClick(props.deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Key pressed handler for the entry.
|
||||
*
|
||||
* @param {Object} e - The event.
|
||||
* @private
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onKeyPress(e: React.KeyboardEvent) {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
props.onClick(props.deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Click handler for Test button.
|
||||
* Sets the current audio output id and plays a sound.
|
||||
*
|
||||
* @param {Object} e - The synthetic event.
|
||||
* @returns {void}
|
||||
*/
|
||||
async function _onTestButtonClick(e: React.KeyboardEvent | React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
|
||||
try {
|
||||
await audioRef.current?.setSinkId(props.deviceId);
|
||||
audioRef.current?.play();
|
||||
} catch (err) {
|
||||
logger.log('Could not set sink id', err);
|
||||
}
|
||||
}
|
||||
|
||||
const { children, isSelected, index, length } = props;
|
||||
const testLabel = t('deviceSelection.testAudio');
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
return (
|
||||
<li
|
||||
aria-checked = { isSelected }
|
||||
aria-label = { children }
|
||||
aria-posinset = { index + 1 } // Add one to offset the 0 based index.
|
||||
aria-setsize = { length }
|
||||
className = { classes.container }
|
||||
onClick = { _onClick }
|
||||
onKeyPress = { _onKeyPress }
|
||||
role = 'menuitemradio'
|
||||
tabIndex = { 0 }>
|
||||
<ContextMenuItem
|
||||
icon = { isSelected ? IconCheck : undefined }
|
||||
overflowType = { TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER }
|
||||
selected = { isSelected }
|
||||
text = { children }
|
||||
textClassName = { cx(classes.entryText, 'entryText', !isSelected && 'left-margin') } />
|
||||
<audio
|
||||
preload = 'auto'
|
||||
ref = { audioRef }
|
||||
src = { TEST_SOUND_PATH } />
|
||||
<Button
|
||||
accessibilityLabel = { `${testLabel} ${children}` }
|
||||
className = { cx(classes.testButton, 'testButton') }
|
||||
label = { testLabel }
|
||||
onClick = { _onTestButtonClick }
|
||||
onKeyPress = { _onTestButtonClick }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default SpeakerEntry;
|
||||
@@ -0,0 +1,358 @@
|
||||
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 { IconImage } from '../../../../base/icons/svg';
|
||||
import { Video } from '../../../../base/media/components/index';
|
||||
import { equals } from '../../../../base/redux/functions';
|
||||
import { updateSettings } from '../../../../base/settings/actions';
|
||||
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 { checkBlurSupport, checkVirtualBackgroundEnabled } from '../../../../virtual-background/functions';
|
||||
import { openSettingsDialog } from '../../../actions';
|
||||
import { SETTINGS_TABS } from '../../../constants';
|
||||
import { createLocalVideoTracks } from '../../../functions.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VideoSettingsContent}.
|
||||
*/
|
||||
export interface IProps {
|
||||
|
||||
/**
|
||||
* Callback to change the flip state.
|
||||
*/
|
||||
changeFlip: (flip: boolean) => void;
|
||||
|
||||
/**
|
||||
* The deviceId of the camera device currently being used.
|
||||
*/
|
||||
currentCameraDeviceId: string;
|
||||
|
||||
/**
|
||||
* Whether the local video flip is disabled.
|
||||
*/
|
||||
disableLocalVideoFlip: boolean | undefined;
|
||||
|
||||
/**
|
||||
* Whether or not the local video is flipped.
|
||||
*/
|
||||
localFlipX: boolean;
|
||||
|
||||
/**
|
||||
* Open virtual background dialog.
|
||||
*/
|
||||
selectBackground: () => void;
|
||||
|
||||
/**
|
||||
* Callback invoked to change current camera.
|
||||
*/
|
||||
setVideoInputDevice: Function;
|
||||
|
||||
/**
|
||||
* Callback invoked to toggle the settings popup visibility.
|
||||
*/
|
||||
toggleVideoSettings: Function;
|
||||
|
||||
/**
|
||||
* All the camera device ids currently connected.
|
||||
*/
|
||||
videoDeviceIds: string[];
|
||||
|
||||
/**
|
||||
* Whether or not the virtual background is visible.
|
||||
*/
|
||||
visibleVirtualBackground: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
maxHeight: 'calc(100dvh - 100px)',
|
||||
overflow: 'auto',
|
||||
margin: 0,
|
||||
marginBottom: theme.spacing(1),
|
||||
position: 'relative',
|
||||
right: 'auto'
|
||||
},
|
||||
|
||||
previewEntry: {
|
||||
cursor: 'pointer',
|
||||
height: '138px',
|
||||
width: '244px',
|
||||
position: 'relative',
|
||||
margin: '0 7px',
|
||||
marginBottom: theme.spacing(1),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'hidden',
|
||||
|
||||
'&:last-child': {
|
||||
marginBottom: 0
|
||||
}
|
||||
},
|
||||
|
||||
selectedEntry: {
|
||||
border: `2px solid ${theme.palette.action01Hover}`
|
||||
},
|
||||
|
||||
previewVideo: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
objectFit: 'cover'
|
||||
},
|
||||
|
||||
error: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
position: 'absolute'
|
||||
},
|
||||
|
||||
labelContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
maxWidth: '100%',
|
||||
zIndex: 2,
|
||||
padding: theme.spacing(2)
|
||||
},
|
||||
|
||||
label: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
borderRadius: '4px',
|
||||
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
|
||||
color: theme.palette.text01,
|
||||
...theme.typography.labelBold,
|
||||
width: 'fit-content',
|
||||
maxwidth: `calc(100% - ${theme.spacing(2)} - ${theme.spacing(2)})`,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
|
||||
checkboxContainer: {
|
||||
padding: '10px 14px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const stopPropagation = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const VideoSettingsContent = ({
|
||||
changeFlip,
|
||||
currentCameraDeviceId,
|
||||
disableLocalVideoFlip,
|
||||
localFlipX,
|
||||
selectBackground,
|
||||
setVideoInputDevice,
|
||||
toggleVideoSettings,
|
||||
videoDeviceIds,
|
||||
visibleVirtualBackground
|
||||
}: IProps) => {
|
||||
const _componentWasUnmounted = useRef(false);
|
||||
const [ trackData, setTrackData ] = useState(new Array(videoDeviceIds.length).fill({
|
||||
jitsiTrack: null
|
||||
}));
|
||||
const { t } = useTranslation();
|
||||
const videoDevicesRef = useRef(videoDeviceIds);
|
||||
const trackDataRef = useRef(trackData);
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
/**
|
||||
* Toggles local video flip state.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const _onToggleFlip = useCallback(() => {
|
||||
changeFlip(!localFlipX);
|
||||
}, [ localFlipX, changeFlip ]);
|
||||
|
||||
/**
|
||||
* Destroys all the tracks from trackData object.
|
||||
*
|
||||
* @param {Object[]} tracks - An array of tracks that are to be disposed.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const _disposeTracks = (tracks: { jitsiTrack: any; }[]) => {
|
||||
tracks.forEach(({ jitsiTrack }) => {
|
||||
jitsiTrack?.dispose();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates and updates the track data.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const _setTracks = async () => {
|
||||
_disposeTracks(trackData);
|
||||
|
||||
const newTrackData = await createLocalVideoTracks(videoDeviceIds, 5000);
|
||||
|
||||
// In case the component gets unmounted before the tracks are created
|
||||
// avoid a leak by not setting the state
|
||||
if (_componentWasUnmounted.current) {
|
||||
_disposeTracks(newTrackData);
|
||||
} else {
|
||||
setTrackData(newTrackData);
|
||||
trackDataRef.current = newTrackData;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the click handler used when selecting the video preview.
|
||||
*
|
||||
* @param {string} deviceId - The id of the camera device.
|
||||
* @returns {Function}
|
||||
*/
|
||||
const _onEntryClick = (deviceId: string) => () => {
|
||||
setVideoInputDevice(deviceId);
|
||||
toggleVideoSettings();
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a preview entry.
|
||||
*
|
||||
* @param {Object} data - The track data.
|
||||
* @param {number} index - The index of the entry.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const _renderPreviewEntry = (data: { deviceId: string; error?: string; jitsiTrack: any | null; },
|
||||
index: number) => {
|
||||
const { error, jitsiTrack, deviceId } = data;
|
||||
const isSelected = deviceId === currentCameraDeviceId;
|
||||
const key = `vp-${index}`;
|
||||
const tabIndex = '0';
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
className = { classes.previewEntry }
|
||||
key = { key }
|
||||
tabIndex = { -1 } >
|
||||
<div className = { classes.error }>{t(error)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const previewProps: any = {
|
||||
className: classes.previewEntry,
|
||||
key,
|
||||
tabIndex
|
||||
};
|
||||
const label = jitsiTrack?.getTrackLabel();
|
||||
|
||||
if (isSelected) {
|
||||
previewProps['aria-checked'] = true;
|
||||
previewProps.className = cx(classes.previewEntry, classes.selectedEntry);
|
||||
} else {
|
||||
previewProps['aria-checked'] = false;
|
||||
previewProps.onClick = _onEntryClick(deviceId);
|
||||
previewProps.onKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
previewProps.onClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{ ...previewProps }
|
||||
role = 'menuitemradio'>
|
||||
<div className = { classes.labelContainer }>
|
||||
{label && <div className = { classes.label }>
|
||||
<span>{label}</span>
|
||||
</div>}
|
||||
</div>
|
||||
<Video
|
||||
className = { cx(classes.previewVideo, 'flipVideoX') }
|
||||
id = { `video_settings_preview-${index}` }
|
||||
playsinline = { true }
|
||||
videoTrack = {{ jitsiTrack }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
_setTracks();
|
||||
|
||||
return () => {
|
||||
_componentWasUnmounted.current = true;
|
||||
_disposeTracks(trackDataRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!equals(videoDeviceIds, videoDevicesRef.current)) {
|
||||
_setTracks();
|
||||
videoDevicesRef.current = videoDeviceIds;
|
||||
}
|
||||
}, [ videoDeviceIds ]);
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
activateFocusTrap = { true }
|
||||
className = { classes.container }
|
||||
hidden = { false }
|
||||
id = 'video-settings-dialog'>
|
||||
<ContextMenuItemGroup role = 'group'>
|
||||
{trackData.map((data, i) => _renderPreviewEntry(data, i))}
|
||||
</ContextMenuItemGroup>
|
||||
<ContextMenuItemGroup role = 'group'>
|
||||
{ visibleVirtualBackground && <ContextMenuItem
|
||||
accessibilityLabel = { t('virtualBackground.title') }
|
||||
icon = { IconImage }
|
||||
onClick = { selectBackground }
|
||||
role = 'menuitem'
|
||||
text = { t('virtualBackground.title') } /> }
|
||||
{!disableLocalVideoFlip && (
|
||||
<div
|
||||
className = { classes.checkboxContainer }
|
||||
onClick = { stopPropagation }
|
||||
role = 'menuitem'>
|
||||
<Checkbox
|
||||
checked = { localFlipX }
|
||||
label = { t('videothumbnail.mirrorVideo') }
|
||||
onChange = { _onToggleFlip } />
|
||||
</div>
|
||||
)}
|
||||
</ContextMenuItemGroup>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: IReduxState) => {
|
||||
const { disableLocalVideoFlip } = state['features/base/config'];
|
||||
const { localFlipX } = state['features/base/settings'];
|
||||
|
||||
return {
|
||||
disableLocalVideoFlip,
|
||||
localFlipX: Boolean(localFlipX),
|
||||
visibleVirtualBackground: checkBlurSupport()
|
||||
&& checkVirtualBackgroundEnabled(state)
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
|
||||
return {
|
||||
selectBackground: () => dispatch(openSettingsDialog(SETTINGS_TABS.VIRTUAL_BACKGROUND)),
|
||||
changeFlip: (flip: boolean) => {
|
||||
dispatch(updateSettings({
|
||||
localFlipX: flip
|
||||
}));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VideoSettingsContent);
|
||||
@@ -0,0 +1,128 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import {
|
||||
setVideoInputDeviceAndUpdateSettings
|
||||
} from '../../../../base/devices/actions.web';
|
||||
import {
|
||||
getVideoDeviceIds
|
||||
} from '../../../../base/devices/functions.web';
|
||||
import Popover from '../../../../base/popover/components/Popover.web';
|
||||
import { SMALL_MOBILE_WIDTH } from '../../../../base/responsive-ui/constants';
|
||||
import { getCurrentCameraDeviceId } from '../../../../base/settings/functions.web';
|
||||
import { toggleVideoSettings } from '../../../actions';
|
||||
import { getVideoSettingsVisibility } from '../../../functions.web';
|
||||
|
||||
import VideoSettingsContent from './VideoSettingsContent';
|
||||
|
||||
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Component children (the Video button).
|
||||
*/
|
||||
children: ReactNode;
|
||||
|
||||
/**
|
||||
* The deviceId of the camera device currently being used.
|
||||
*/
|
||||
currentCameraDeviceId: string;
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the popup.
|
||||
*/
|
||||
isOpen: boolean;
|
||||
|
||||
/**
|
||||
* Callback executed when the popup closes.
|
||||
*/
|
||||
onClose: Function;
|
||||
|
||||
/**
|
||||
* The popup placement enum value.
|
||||
*/
|
||||
popupPlacement: string;
|
||||
|
||||
/**
|
||||
* Callback invoked to change current camera.
|
||||
*/
|
||||
setVideoInputDevice: Function;
|
||||
|
||||
/**
|
||||
* All the camera device ids currently connected.
|
||||
*/
|
||||
videoDeviceIds: string[];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(() => {
|
||||
return {
|
||||
container: {
|
||||
background: 'none',
|
||||
display: 'inline-block'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Popup with a preview of all the video devices.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function VideoSettingsPopup({
|
||||
currentCameraDeviceId,
|
||||
children,
|
||||
isOpen,
|
||||
onClose,
|
||||
popupPlacement,
|
||||
setVideoInputDevice,
|
||||
videoDeviceIds
|
||||
}: IProps) {
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
return (
|
||||
<div className = { cx('video-preview', classes.container) }>
|
||||
<Popover
|
||||
allowClick = { true }
|
||||
content = { <VideoSettingsContent
|
||||
currentCameraDeviceId = { currentCameraDeviceId }
|
||||
setVideoInputDevice = { setVideoInputDevice }
|
||||
toggleVideoSettings = { onClose }
|
||||
videoDeviceIds = { videoDeviceIds } /> }
|
||||
headingId = 'video-settings-button'
|
||||
onPopoverClose = { onClose }
|
||||
position = { popupPlacement }
|
||||
role = 'menu'
|
||||
trigger = 'click'
|
||||
visible = { isOpen }>
|
||||
{ children }
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated {@code VideoSettingsPopup}'s
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { videoSpaceWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
return {
|
||||
currentCameraDeviceId: getCurrentCameraDeviceId(state),
|
||||
isOpen: Boolean(getVideoSettingsVisibility(state)),
|
||||
popupPlacement: videoSpaceWidth <= Number(SMALL_MOBILE_WIDTH) ? 'auto' : 'top-end',
|
||||
videoDeviceIds: getVideoDeviceIds(state) ?? []
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onClose: toggleVideoSettings,
|
||||
setVideoInputDevice: setVideoInputDeviceAndUpdateSettings
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VideoSettingsPopup);
|
||||
21
react/features/settings/constants.ts
Normal file
21
react/features/settings/constants.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export const SETTINGS_TABS = {
|
||||
AUDIO: 'audio_tab',
|
||||
CALENDAR: 'calendar_tab',
|
||||
MORE: 'more_tab',
|
||||
MODERATOR: 'moderator-tab',
|
||||
NOTIFICATIONS: 'notifications_tab',
|
||||
PROFILE: 'profile_tab',
|
||||
SHORTCUTS: 'shortcuts_tab',
|
||||
VIDEO: 'video_tab',
|
||||
VIRTUAL_BACKGROUND: 'virtual-background_tab'
|
||||
};
|
||||
|
||||
/**
|
||||
* Default frame rate to be used for capturing screenshare.
|
||||
*/
|
||||
export const SS_DEFAULT_FRAME_RATE = 5;
|
||||
|
||||
/**
|
||||
* Supported framerates to be used for capturing screenshare.
|
||||
*/
|
||||
export const SS_SUPPORTED_FRAMERATES = [ 5, 15, 30 ];
|
||||
283
react/features/settings/functions.any.ts
Normal file
283
react/features/settings/functions.any.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { MEDIA_TYPE } from '../av-moderation/constants';
|
||||
import { isEnabledFromState } from '../av-moderation/functions';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { isNameReadOnly } from '../base/config/functions.any';
|
||||
import { SERVER_URL_CHANGE_ENABLED } from '../base/flags/constants';
|
||||
import { getFeatureFlag } from '../base/flags/functions';
|
||||
import i18next, { DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n/i18next';
|
||||
import { getLocalParticipant } from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { getHideSelfView } from '../base/settings/functions.any';
|
||||
import { parseStandardURIString } from '../base/util/uri';
|
||||
import { isStageFilmstripEnabled } from '../filmstrip/functions';
|
||||
import { isFollowMeActive, isFollowMeRecorderActive } from '../follow-me/functions';
|
||||
import { isReactionsEnabled } from '../reactions/functions.any';
|
||||
import { areClosedCaptionsEnabled } from '../subtitles/functions.any';
|
||||
import { iAmVisitor } from '../visitors/functions';
|
||||
|
||||
import { shouldShowModeratorSettings } from './functions';
|
||||
|
||||
/**
|
||||
* Returns true if user is allowed to change Server URL.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @returns {boolean} True to indicate that user can change Server URL, false otherwise.
|
||||
*/
|
||||
export function isServerURLChangeEnabled(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
|
||||
return getFeatureFlag(state, SERVER_URL_CHANGE_ENABLED, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a URL entered by the user.
|
||||
* FIXME: Consider adding this to base/util/uri.
|
||||
*
|
||||
* @param {string} url - The URL to validate.
|
||||
* @returns {string|null} - The normalized URL, or null if the URL is invalid.
|
||||
*/
|
||||
export function normalizeUserInputURL(url: string) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
||||
if (url) {
|
||||
url = url.replace(/\s/g, '').toLowerCase();
|
||||
|
||||
const urlRegExp = new RegExp('^(\\w+://)?(.+)$');
|
||||
const urlComponents = urlRegExp.exec(url);
|
||||
|
||||
if (urlComponents && !urlComponents[1]?.startsWith('http')) {
|
||||
url = `https://${urlComponents[2]}`;
|
||||
}
|
||||
|
||||
const parsedURI = parseStandardURIString(url);
|
||||
|
||||
if (!parsedURI.host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsedURI.toString();
|
||||
}
|
||||
|
||||
return url;
|
||||
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the notification types and their user selected configuration.
|
||||
*
|
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @returns {Object} - The section of notifications to be configured.
|
||||
*/
|
||||
export function getNotificationsMap(stateful: IStateful): { [key: string]: boolean; } {
|
||||
const state = toState(stateful);
|
||||
const { notifications } = state['features/base/config'];
|
||||
const { userSelectedNotifications } = state['features/base/settings'];
|
||||
|
||||
if (!userSelectedNotifications) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.keys(userSelectedNotifications)
|
||||
.filter(key => !notifications || notifications.includes(key))
|
||||
.reduce((notificationsMap, key) => {
|
||||
return {
|
||||
...notificationsMap,
|
||||
[key]: userSelectedNotifications[key]
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
function normalizeCurrentLanguage(language: string) {
|
||||
if (!language) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [ country, lang ] = language.split('-');
|
||||
const jitsiNormalized = `${country}${lang ?? ''}`;
|
||||
|
||||
if (LANGUAGES.includes(jitsiNormalized)) {
|
||||
return jitsiNormalized;
|
||||
}
|
||||
|
||||
if (LANGUAGES.includes(country)) {
|
||||
return country;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the "More" tab from settings dialog from Redux
|
||||
* state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @returns {Object} - The properties for the "More" tab from settings dialog.
|
||||
*/
|
||||
export function getMoreTabProps(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const stageFilmstripEnabled = isStageFilmstripEnabled(state);
|
||||
const language = normalizeCurrentLanguage(i18next.language) || DEFAULT_LANGUAGE;
|
||||
const configuredTabs: string[] = interfaceConfig.SETTINGS_SECTIONS || [];
|
||||
|
||||
// when self view is controlled by the config we hide the settings
|
||||
const { disableSelfView, disableSelfViewSettings } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
areClosedCaptionsEnabled: areClosedCaptionsEnabled(state),
|
||||
currentLanguage: language,
|
||||
disableHideSelfView: disableSelfViewSettings || disableSelfView,
|
||||
hideSelfView: getHideSelfView(state),
|
||||
iAmVisitor: iAmVisitor(state),
|
||||
languages: LANGUAGES,
|
||||
maxStageParticipants: state['features/base/settings'].maxStageParticipants,
|
||||
showLanguageSettings: configuredTabs.includes('language'),
|
||||
showSubtitlesOnStage: state['features/base/settings'].showSubtitlesOnStage,
|
||||
stageFilmstripEnabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the "More" tab from settings dialog from Redux
|
||||
* state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @returns {Object} - The properties for the "More" tab from settings dialog.
|
||||
*/
|
||||
export function getModeratorTabProps(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
conference,
|
||||
followMeEnabled,
|
||||
followMeRecorderEnabled,
|
||||
startAudioMutedPolicy,
|
||||
startVideoMutedPolicy,
|
||||
startReactionsMuted
|
||||
} = state['features/base/conference'];
|
||||
const { groupChatWithPermissions } = state['features/chat'];
|
||||
const { disableReactionsModeration } = state['features/base/config'];
|
||||
const followMeActive = isFollowMeActive(state);
|
||||
const followMeRecorderActive = isFollowMeRecorderActive(state);
|
||||
const showModeratorSettings = shouldShowModeratorSettings(state);
|
||||
const conferenceMetadata = conference?.getMetadataHandler()?.getMetadata();
|
||||
const disableChatWithPermissions = !conferenceMetadata?.allownersEnabled;
|
||||
const isAudioModerationEnabled = isEnabledFromState(MEDIA_TYPE.AUDIO, state);
|
||||
const isVideoModerationEnabled = isEnabledFromState(MEDIA_TYPE.VIDEO, state);
|
||||
|
||||
// The settings sections to display.
|
||||
return {
|
||||
audioModerationEnabled: isAudioModerationEnabled,
|
||||
videoModerationEnabled: isVideoModerationEnabled,
|
||||
chatWithPermissionsEnabled: Boolean(groupChatWithPermissions),
|
||||
showModeratorSettings: Boolean(conference && showModeratorSettings),
|
||||
disableChatWithPermissions: Boolean(disableChatWithPermissions),
|
||||
disableReactionsModeration: Boolean(disableReactionsModeration),
|
||||
followMeActive: Boolean(conference && followMeActive),
|
||||
followMeEnabled: Boolean(conference && followMeEnabled),
|
||||
followMeRecorderActive: Boolean(conference && followMeRecorderActive),
|
||||
followMeRecorderEnabled: Boolean(conference && followMeRecorderEnabled),
|
||||
startReactionsMuted: Boolean(conference && startReactionsMuted),
|
||||
startAudioMuted: Boolean(conference && startAudioMutedPolicy),
|
||||
startVideoMuted: Boolean(conference && startVideoMutedPolicy)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the "Profile" tab from settings dialog from Redux
|
||||
* state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @returns {Object} - The properties for the "Profile" tab from settings
|
||||
* dialog.
|
||||
*/
|
||||
export function getProfileTabProps(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
authEnabled,
|
||||
authLogin,
|
||||
conference
|
||||
} = state['features/base/conference'];
|
||||
const config = state['features/base/config'];
|
||||
let { hideEmailInSettings } = config;
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
if (config.gravatar?.disabled
|
||||
|| (localParticipant?.avatarURL && localParticipant?.avatarURL.length > 0)) {
|
||||
hideEmailInSettings = true;
|
||||
}
|
||||
|
||||
return {
|
||||
authEnabled: Boolean(conference && authEnabled),
|
||||
authLogin,
|
||||
displayName: localParticipant?.name,
|
||||
email: localParticipant?.email,
|
||||
hideEmailInSettings,
|
||||
id: localParticipant?.id,
|
||||
readOnlyName: isNameReadOnly(state)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the "Sounds" tab from settings dialog from Redux
|
||||
* state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @param {boolean} showSoundsSettings - Whether to show the sound settings or not.
|
||||
* @returns {Object} - The properties for the "Sounds" tab from settings
|
||||
* dialog.
|
||||
*/
|
||||
export function getNotificationsTabProps(stateful: IStateful, showSoundsSettings?: boolean) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
soundsIncomingMessage,
|
||||
soundsParticipantJoined,
|
||||
soundsParticipantKnocking,
|
||||
soundsParticipantLeft,
|
||||
soundsTalkWhileMuted,
|
||||
soundsReactions
|
||||
} = state['features/base/settings'];
|
||||
|
||||
const enableReactions = isReactionsEnabled(state);
|
||||
const moderatorMutedSoundsReactions = state['features/base/conference'].startReactionsMuted ?? false;
|
||||
const enabledNotifications = getNotificationsMap(stateful);
|
||||
|
||||
return {
|
||||
disabledSounds: state['features/base/config'].disabledSounds || [],
|
||||
enabledNotifications,
|
||||
showNotificationsSettings: Object.keys(enabledNotifications).length > 0,
|
||||
soundsIncomingMessage,
|
||||
soundsParticipantJoined,
|
||||
soundsParticipantKnocking,
|
||||
soundsParticipantLeft,
|
||||
soundsTalkWhileMuted,
|
||||
soundsReactions,
|
||||
enableReactions,
|
||||
moderatorMutedSoundsReactions,
|
||||
showSoundsSettings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visibility state of the audio settings.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getAudioSettingsVisibility(state: IReduxState) {
|
||||
return state['features/settings'].audioSettingsVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visibility state of the video settings.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getVideoSettingsVisibility(state: IReduxState) {
|
||||
return state['features/settings'].videoSettingsVisible;
|
||||
}
|
||||
38
react/features/settings/functions.native.ts
Normal file
38
react/features/settings/functions.native.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { getParticipantsPaneConfig } from '../participants-pane/functions';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Used on web.
|
||||
*
|
||||
* @param {(Function|Object)} _stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @param {boolean} _isDisplayedOnWelcomePage - Indicates whether the shortcuts dialog is displayed on the
|
||||
* welcome page or not.
|
||||
* @returns {Object} - The properties for the "Shortcuts" tab from settings
|
||||
* dialog.
|
||||
*/
|
||||
export function getShortcutsTabProps(_stateful: any, _isDisplayedOnWelcomePage?: boolean) {
|
||||
// needed to fix lint error.
|
||||
return {
|
||||
keyboardShortcutsEnabled: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if moderator tab in settings should be visible/accessible.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @returns {boolean} True to indicate that moderator tab should be visible, false otherwise.
|
||||
*/
|
||||
export function shouldShowModeratorSettings(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { hideModeratorSettingsTab } = getParticipantsPaneConfig(state);
|
||||
const hasModeratorRights = isLocalParticipantModerator(state);
|
||||
|
||||
return hasModeratorRights && !hideModeratorSettingsTab;
|
||||
}
|
||||
147
react/features/settings/functions.web.ts
Normal file
147
react/features/settings/functions.web.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { createLocalTrack } from '../base/lib-jitsi-meet/functions';
|
||||
import { isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { getUserSelectedCameraDeviceId } from '../base/settings/functions.web';
|
||||
import { areKeyboardShortcutsEnabled, getKeyboardShortcutsHelpDescriptions } from '../keyboard-shortcuts/functions';
|
||||
import { getParticipantsPaneConfig } from '../participants-pane/functions';
|
||||
import { isPrejoinPageVisible } from '../prejoin/functions';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Returns a promise which resolves with a list of objects containing
|
||||
* all the video jitsiTracks and appropriate errors for the given device ids.
|
||||
*
|
||||
* @param {string[]} ids - The list of the camera ids for which to create tracks.
|
||||
* @param {number} [timeout] - A timeout for the createLocalTrack function call.
|
||||
*
|
||||
* @returns {Promise<Object[]>}
|
||||
*/
|
||||
export function createLocalVideoTracks(ids: string[], timeout?: number) {
|
||||
return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId, timeout)
|
||||
.then((jitsiTrack: any) => {
|
||||
return {
|
||||
jitsiTrack,
|
||||
deviceId
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
return {
|
||||
jitsiTrack: null,
|
||||
deviceId,
|
||||
error: 'deviceSelection.previewUnavailable'
|
||||
};
|
||||
})));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a promise which resolves with a list of objects containing
|
||||
* the audio track and the corresponding audio device information.
|
||||
*
|
||||
* @param {Object[]} devices - A list of microphone devices.
|
||||
* @param {number} [timeout] - A timeout for the createLocalTrack function call.
|
||||
* @returns {Promise<{
|
||||
* deviceId: string,
|
||||
* hasError: boolean,
|
||||
* jitsiTrack: Object,
|
||||
* label: string
|
||||
* }[]>}
|
||||
*/
|
||||
export function createLocalAudioTracks(devices: Array<{ deviceId: string; label: string; }>, timeout?: number) {
|
||||
return Promise.all(
|
||||
devices.map(async ({ deviceId, label }) => {
|
||||
let jitsiTrack = null;
|
||||
let hasError = false;
|
||||
|
||||
try {
|
||||
jitsiTrack = await createLocalTrack('audio', deviceId, timeout);
|
||||
} catch (err) {
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
return {
|
||||
deviceId,
|
||||
hasError,
|
||||
jitsiTrack,
|
||||
label
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the "Shortcuts" tab from settings dialog from Redux
|
||||
* state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the shortcuts dialog is displayed on the
|
||||
* welcome page or not.
|
||||
* @returns {Object} - The properties for the "Shortcuts" tab from settings
|
||||
* dialog.
|
||||
*/
|
||||
export function getShortcutsTabProps(stateful: IStateful, isDisplayedOnWelcomePage?: boolean) {
|
||||
const state = toState(stateful);
|
||||
|
||||
return {
|
||||
displayShortcuts: !isDisplayedOnWelcomePage && !isPrejoinPageVisible(state),
|
||||
keyboardShortcutsEnabled: areKeyboardShortcutsEnabled(state),
|
||||
keyboardShortcutsHelpDescriptions: getKeyboardShortcutsHelpDescriptions(state)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the "Virtual Background" tab from settings dialog from Redux
|
||||
* state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
|
||||
* welcome page or not.
|
||||
* @returns {Object} - The properties for the "Shortcuts" tab from settings
|
||||
* dialog.
|
||||
*/
|
||||
export function getVirtualBackgroundTabProps(stateful: IStateful, isDisplayedOnWelcomePage?: boolean) {
|
||||
const state = toState(stateful);
|
||||
const settings = state['features/base/settings'];
|
||||
const userSelectedCamera = getUserSelectedCameraDeviceId(state);
|
||||
let selectedVideoInputId = settings.cameraDeviceId;
|
||||
|
||||
if (isDisplayedOnWelcomePage) {
|
||||
selectedVideoInputId = userSelectedCamera;
|
||||
}
|
||||
|
||||
return {
|
||||
options: state['features/virtual-background'],
|
||||
selectedVideoInputId
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for web. Indicates if the setting section is enabled.
|
||||
*
|
||||
* @param {string} settingName - The name of the setting section as defined in
|
||||
* interface_config.js and SettingsMenu.js.
|
||||
* @returns {boolean} True to indicate that the given setting section
|
||||
* is enabled, false otherwise.
|
||||
*/
|
||||
export function isSettingEnabled(settingName: string) {
|
||||
return interfaceConfig.SETTINGS_SECTIONS.includes(settingName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if moderator tab in settings should be visible/accessible.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @returns {boolean} True to indicate that moderator tab should be visible, false otherwise.
|
||||
*/
|
||||
export function shouldShowModeratorSettings(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { hideModeratorSettingsTab } = getParticipantsPaneConfig(state);
|
||||
const hasModeratorRights = Boolean(isSettingEnabled('moderator') && isLocalParticipantModerator(state));
|
||||
|
||||
return hasModeratorRights && !hideModeratorSettingsTab;
|
||||
}
|
||||
3
react/features/settings/logger.ts
Normal file
3
react/features/settings/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/settings');
|
||||
33
react/features/settings/middleware.web.ts
Normal file
33
react/features/settings/middleware.web.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { SETTINGS_UPDATED } from '../base/settings/actionTypes';
|
||||
import { getHideSelfView } from '../base/settings/functions.web';
|
||||
import { showNotification } from '../notifications/actions';
|
||||
import { DISABLE_SELF_VIEW_NOTIFICATION_ID, NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
|
||||
import { openSettingsDialog } from './actions';
|
||||
import { SETTINGS_TABS } from './constants';
|
||||
|
||||
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const oldValue = getHideSelfView(getState());
|
||||
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case SETTINGS_UPDATED: {
|
||||
const newValue = action.settings.disableSelfView;
|
||||
|
||||
if (newValue !== oldValue && newValue) {
|
||||
dispatch(showNotification({
|
||||
uid: DISABLE_SELF_VIEW_NOTIFICATION_ID,
|
||||
titleKey: 'notify.selfViewTitle',
|
||||
customActionNameKey: [ 'settings.title' ],
|
||||
customActionHandler: [ () =>
|
||||
dispatch(openSettingsDialog(SETTINGS_TABS.MORE))
|
||||
]
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
28
react/features/settings/reducer.ts
Normal file
28
react/features/settings/reducer.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
SET_VIDEO_SETTINGS_VISIBILITY
|
||||
} from './actionTypes';
|
||||
|
||||
export interface ISettingsState {
|
||||
audioSettingsVisible?: boolean;
|
||||
videoSettingsVisible?: boolean;
|
||||
}
|
||||
|
||||
ReducerRegistry.register('features/settings', (state: ISettingsState = {}, action) => {
|
||||
switch (action.type) {
|
||||
case SET_AUDIO_SETTINGS_VISIBILITY:
|
||||
return {
|
||||
...state,
|
||||
audioSettingsVisible: action.value
|
||||
};
|
||||
case SET_VIDEO_SETTINGS_VISIBILITY:
|
||||
return {
|
||||
...state,
|
||||
videoSettingsVisible: action.value
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
Reference in New Issue
Block a user