init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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;

View 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;

View 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);

View 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);

View 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;

View File

@@ -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;

View 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;

View 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;

View 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;

View File

@@ -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;

View 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;

View 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;

View File

@@ -0,0 +1 @@
export const AVATAR_SIZE = 64;

View 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]
}
};