[RN] Add app-settings feature

[RN] Fix PR feedbacks, write persistency docs
This commit is contained in:
zbettenbuk 2017-12-14 18:02:32 +01:00 committed by Paweł Domas
parent 871ef9ff0e
commit bfcd34358b
29 changed files with 1288 additions and 76 deletions

View File

@ -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"
}
}

View File

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

View File

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

View File

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

View File

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