[RN] Add app-settings feature
[RN] Fix PR feedbacks, write persistency docs
This commit is contained in:
parent
871ef9ff0e
commit
bfcd34358b
|
@ -535,5 +535,13 @@
|
|||
"invite": "Invite in __app__",
|
||||
"title": "Call access info",
|
||||
"tooltip": "Get access info about the meeting"
|
||||
},
|
||||
"profileModal": {
|
||||
"displayName": "Display name",
|
||||
"email": "Email",
|
||||
"header": "Settings",
|
||||
"serverURL": "Server URL",
|
||||
"startWithAudioMuted": "Start with audio muted",
|
||||
"startWithVideoMuted": "Start with video muted"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* The type of (redux) action which signals the request
|
||||
* to hide the app settings modal.
|
||||
*
|
||||
* {
|
||||
* type: HIDE_APP_SETTINGS
|
||||
* }
|
||||
*/
|
||||
export const HIDE_APP_SETTINGS = Symbol('HIDE_APP_SETTINGS');
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals the request
|
||||
* to show the app settings modal where available.
|
||||
*
|
||||
* {
|
||||
* type: SHOW_APP_SETTINGS
|
||||
* }
|
||||
*/
|
||||
export const SHOW_APP_SETTINGS = Symbol('SHOW_APP_SETTINGS');
|
|
@ -0,0 +1,32 @@
|
|||
/* @flow */
|
||||
|
||||
import {
|
||||
HIDE_APP_SETTINGS,
|
||||
SHOW_APP_SETTINGS
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Redux-signals the request to open the app settings modal.
|
||||
*
|
||||
* @returns {{
|
||||
* type: SHOW_APP_SETTINGS
|
||||
* }}
|
||||
*/
|
||||
export function showAppSettings() {
|
||||
return {
|
||||
type: SHOW_APP_SETTINGS
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Redux-signals the request to hide the app settings modal.
|
||||
*
|
||||
* @returns {{
|
||||
* type: HIDE_APP_SETTINGS
|
||||
* }}
|
||||
*/
|
||||
export function hideAppSettings() {
|
||||
return {
|
||||
type: HIDE_APP_SETTINGS
|
||||
};
|
||||
}
|
|
@ -0,0 +1,311 @@
|
|||
// @flow
|
||||
|
||||
import { Component } from 'react';
|
||||
|
||||
import { hideAppSettings } from '../actions';
|
||||
import { getProfile, updateProfile } from '../../base/profile';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AbstractAppSettings}
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The current profile object.
|
||||
*/
|
||||
_profile: Object,
|
||||
|
||||
/**
|
||||
* The visibility prop of the settings modal.
|
||||
*/
|
||||
_visible: boolean,
|
||||
|
||||
/**
|
||||
* Redux store dispatch function.
|
||||
*/
|
||||
dispatch: Dispatch<*>
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link AbstractAppSettings}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The display name field value on the settings screen.
|
||||
*/
|
||||
displayName: string,
|
||||
|
||||
/**
|
||||
* The email field value on the settings screen.
|
||||
*/
|
||||
email: string,
|
||||
|
||||
/**
|
||||
* The server url field value on the settings screen.
|
||||
*/
|
||||
serverURL: string,
|
||||
|
||||
/**
|
||||
* The start audio muted switch value on the settings screen.
|
||||
*/
|
||||
startWithAudioMuted: boolean,
|
||||
|
||||
/**
|
||||
* The start video muted switch value on the settings screen.
|
||||
*/
|
||||
startWithVideoMuted: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Base (abstract) class for container component rendering
|
||||
* the app settings page.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export class AbstractAppSettings extends Component<Props, State> {
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AbstractAppSettings} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the component.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onChangeDisplayName = this._onChangeDisplayName.bind(this);
|
||||
this._onChangeEmail = this._onChangeEmail.bind(this);
|
||||
this._onChangeServerName = this._onChangeServerName.bind(this);
|
||||
this._onRequestClose = this._onRequestClose.bind(this);
|
||||
this._onSaveDisplayName = this._onSaveDisplayName.bind(this);
|
||||
this._onSaveEmail = this._onSaveEmail.bind(this);
|
||||
this._onSaveServerName = this._onSaveServerName.bind(this);
|
||||
this._onStartAudioMutedChange
|
||||
= this._onStartAudioMutedChange.bind(this);
|
||||
this._onStartVideoMutedChange
|
||||
= this._onStartVideoMutedChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes React's {@link Component#componentWillReceiveProps()} to make
|
||||
* sure we have the state Initialized on component mount.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillMount() {
|
||||
this._updateStateFromProps(this.props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillReceiveProps()}. Invoked
|
||||
* before this mounted component receives new props.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @param {Props} nextProps - New props component will receive.
|
||||
*/
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
this._updateStateFromProps(nextProps);
|
||||
}
|
||||
|
||||
_onChangeDisplayName: (string) => void;
|
||||
|
||||
/**
|
||||
* Handles the display name field value change.
|
||||
*
|
||||
* @protected
|
||||
* @param {string} text - The value typed in the name field.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChangeDisplayName(text) {
|
||||
this.setState({
|
||||
displayName: text
|
||||
});
|
||||
}
|
||||
|
||||
_onChangeEmail: (string) => void;
|
||||
|
||||
/**
|
||||
* Handles the email field value change.
|
||||
*
|
||||
* @protected
|
||||
* @param {string} text - The value typed in the email field.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChangeEmail(text) {
|
||||
this.setState({
|
||||
email: text
|
||||
});
|
||||
}
|
||||
|
||||
_onChangeServerName: (string) => void;
|
||||
|
||||
/**
|
||||
* Handles the server name field value change.
|
||||
*
|
||||
* @protected
|
||||
* @param {string} text - The server URL typed in the server field.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChangeServerName(text) {
|
||||
this.setState({
|
||||
serverURL: text
|
||||
});
|
||||
}
|
||||
|
||||
_onRequestClose: () => void;
|
||||
|
||||
/**
|
||||
* Handles the hardware back button.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRequestClose() {
|
||||
this.props.dispatch(hideAppSettings());
|
||||
}
|
||||
|
||||
_onSaveDisplayName: () => void;
|
||||
|
||||
/**
|
||||
* Handles the display name field onEndEditing.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSaveDisplayName() {
|
||||
this._updateProfile({
|
||||
displayName: this.state.displayName
|
||||
});
|
||||
}
|
||||
|
||||
_onSaveEmail: () => void;
|
||||
|
||||
/**
|
||||
* Handles the email field onEndEditing.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSaveEmail() {
|
||||
this._updateProfile({
|
||||
email: this.state.email
|
||||
});
|
||||
}
|
||||
|
||||
_onSaveServerName: () => void;
|
||||
|
||||
/**
|
||||
* Handles the server name field onEndEditing.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSaveServerName() {
|
||||
let serverURL;
|
||||
|
||||
if (this.state.serverURL.endsWith('/')) {
|
||||
serverURL = this.state.serverURL.substr(
|
||||
0, this.state.serverURL.length - 1
|
||||
);
|
||||
} else {
|
||||
serverURL = this.state.serverURL;
|
||||
}
|
||||
|
||||
this._updateProfile({
|
||||
defaultURL: serverURL
|
||||
});
|
||||
this.setState({
|
||||
serverURL
|
||||
});
|
||||
}
|
||||
|
||||
_onStartAudioMutedChange: (boolean) => void;
|
||||
|
||||
/**
|
||||
* Handles the start audio muted change event.
|
||||
*
|
||||
* @protected
|
||||
* @param {boolean} newValue - The new value for the
|
||||
* start audio muted option.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onStartAudioMutedChange(newValue) {
|
||||
this.setState({
|
||||
startWithAudioMuted: newValue
|
||||
});
|
||||
|
||||
this._updateProfile({
|
||||
startWithAudioMuted: newValue
|
||||
});
|
||||
}
|
||||
|
||||
_onStartVideoMutedChange: (boolean) => void;
|
||||
|
||||
/**
|
||||
* Handles the start video muted change event.
|
||||
*
|
||||
* @protected
|
||||
* @param {boolean} newValue - The new value for the
|
||||
* start video muted option.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onStartVideoMutedChange(newValue) {
|
||||
this.setState({
|
||||
startWithVideoMuted: newValue
|
||||
});
|
||||
|
||||
this._updateProfile({
|
||||
startWithVideoMuted: newValue
|
||||
});
|
||||
}
|
||||
|
||||
_updateProfile: (Object) => void;
|
||||
|
||||
/**
|
||||
* Updates the persisted profile on any change.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} updateObject - The partial update object for the profile.
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateProfile(updateObject: Object) {
|
||||
this.props.dispatch(updateProfile({
|
||||
...this.props._profile,
|
||||
...updateObject
|
||||
}));
|
||||
}
|
||||
|
||||
_updateStateFromProps: (Object) => void;
|
||||
|
||||
/**
|
||||
* Updates the component state when (new) props are received.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} props - The component's props.
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateStateFromProps(props) {
|
||||
this.setState({
|
||||
displayName: props._profile.displayName,
|
||||
email: props._profile.email,
|
||||
serverURL: props._profile.defaultURL,
|
||||
startWithAudioMuted: props._profile.startWithAudioMuted,
|
||||
startWithVideoMuted: props._profile.startWithVideoMuted
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the React {@code Component} props of
|
||||
* {@code AbstractAppSettings}.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @protected
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
return {
|
||||
_profile: getProfile(state),
|
||||
_visible: state['features/app-settings'].visible
|
||||
};
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
_mapStateToProps,
|
||||
AbstractAppSettings
|
||||
} from './AbstractAppSettings';
|
||||
import FormRow from './FormRow';
|
||||
import styles from './styles';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
/**
|
||||
* The native container rendering the app settings page.
|
||||
*
|
||||
* @extends AbstractAppSettings
|
||||
*/
|
||||
class AppSettings extends AbstractAppSettings {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}, renders the settings page.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
animationType = 'slide'
|
||||
onRequestClose = { this._onRequestClose }
|
||||
presentationStyle = 'fullScreen'
|
||||
style = { styles.modal }
|
||||
visible = { this.props._visible }>
|
||||
<View style = { styles.headerContainer } >
|
||||
<Text style = { [ styles.text, styles.headerTitle ] } >
|
||||
{ t('profileModal.header') }
|
||||
</Text>
|
||||
</View>
|
||||
<View style = { styles.settingsContainer } >
|
||||
<FormRow
|
||||
fieldSeparator = { true }
|
||||
i18nLabel = 'profileModal.serverURL' >
|
||||
<TextInput
|
||||
autoCapitalize = 'none'
|
||||
onChangeText = { this._onChangeServerName }
|
||||
onEndEditing = { this._onSaveServerName }
|
||||
placeholder = 'https://jitsi.example.com'
|
||||
value = { this.state.serverURL } />
|
||||
</FormRow>
|
||||
<FormRow
|
||||
fieldSeparator = { true }
|
||||
i18nLabel = 'profileModal.displayName' >
|
||||
<TextInput
|
||||
onChangeText = { this._onChangeDisplayName }
|
||||
onEndEditing = { this._onSaveDisplayName }
|
||||
placeholder = 'John Doe'
|
||||
value = { this.state.displayName } />
|
||||
</FormRow>
|
||||
<FormRow
|
||||
fieldSeparator = { true }
|
||||
i18nLabel = 'profileModal.email' >
|
||||
<TextInput
|
||||
onChangeText = { this._onChangeEmail }
|
||||
onEndEditing = { this._onSaveEmail }
|
||||
placeholder = 'email@example.com'
|
||||
value = { this.state.email } />
|
||||
</FormRow>
|
||||
<FormRow
|
||||
fieldSeparator = { true }
|
||||
i18nLabel = 'profileModal.startWithAudioMuted' >
|
||||
<Switch
|
||||
onValueChange = {
|
||||
this._onStartAudioMutedChange
|
||||
}
|
||||
value = { this.state.startWithAudioMuted } />
|
||||
</FormRow>
|
||||
<FormRow
|
||||
i18nLabel = 'profileModal.startWithVideoMuted' >
|
||||
<Switch
|
||||
onValueChange = {
|
||||
this._onStartVideoMutedChange
|
||||
}
|
||||
value = { this.state.startWithVideoMuted } />
|
||||
</FormRow>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export |