mirror of https://github.com/jitsi/jitsi-meet
[RN] Fix PR feedbacks, write persistency docspull/2357/head jitsi-meet_2737
parent
871ef9ff0e
commit
bfcd34358b
@ -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 default translate(connect(_mapStateToProps)(AppSettings)); |
@ -0,0 +1,138 @@ |
||||
/* @flow */ |
||||
|
||||
import React, { Component } from 'react'; |
||||
import { |
||||
Text, |
||||
View } from 'react-native'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import styles, { ANDROID_UNDERLINE_COLOR } from './styles'; |
||||
|
||||
import { translate } from '../../base/i18n'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link FormRow} |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
*/ |
||||
children: Object, |
||||
|
||||
/** |
||||
* Prop to decide if a row separator is to be rendered. |
||||
*/ |
||||
fieldSeparator: boolean, |
||||
|
||||
/** |
||||
* The i18n key of the text label of the form field. |
||||
*/ |
||||
i18nLabel: string, |
||||
|
||||
/** |
||||
* Invoked to obtain translated strings. |
||||
*/ |
||||
t: Function |
||||
} |
||||
|
||||
/** |
||||
* 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<Props> { |
||||
|
||||
/** |
||||
* Initializes a new {@code FormRow} instance. |
||||
* |
||||
* @param {Object} props - Component properties. |
||||
*/ |
||||
constructor(props) { |
||||
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} |
||||
*/ |
||||
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 } > |
||||
<Text style = { styles.text } > |
||||
{ t(this.props.i18nLabel) } |
||||
</Text> |
||||
</View> |
||||
<View style = { styles.fieldValueContainer } > |
||||
{ newChild } |
||||
</View> |
||||
</View> |
||||
); |
||||
} |
||||
|
||||
_getDefaultFieldProps: (field: Component<*, *>) => Object; |
||||
|
||||
/** |
||||
* 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). |
||||
* |
||||
* @private |
||||
* @param {Object} field - The field (child) component. |
||||
* @returns {Object} |
||||
*/ |
||||
_getDefaultFieldProps(field: Object) { |
||||
if (field && field.type) { |
||||
switch (field.type.displayName) { |
||||
case 'TextInput': |
||||
return { |
||||
style: styles.textInputField, |
||||
underlineColorAndroid: ANDROID_UNDERLINE_COLOR |
||||
}; |
||||
} |
||||
} |
||||
|
||||
return {}; |
||||
} |
||||
|
||||
_getRowStyle: () => Array<Object>; |
||||
|
||||
/** |
||||
* Assembles the row style array based on the row's props. |
||||
* |
||||
* @private |
||||
* @returns {Array<Object>} |
||||
*/ |
||||
_getRowStyle() { |
||||
const rowStyle = [ |
||||
styles.fieldContainer |
||||
]; |
||||
|
||||
if (this.props.fieldSeparator) { |
||||
rowStyle.push(styles.fieldSeparator); |
||||
} |
||||
|
||||
return rowStyle; |
||||
} |
||||
} |
||||
|
||||
export default translate(connect()(FormRow)); |
@ -0,0 +1 @@ |
||||
export { default as AppSettings } from './AppSettings'; |
@ -0,0 +1,98 @@ |
||||
import { |
||||
BoxModel, |
||||
ColorPalette, |
||||
createStyleSheet |
||||
} from '../../base/styles'; |
||||
|
||||
const LABEL_TAB = 300; |
||||
|
||||
export const ANDROID_UNDERLINE_COLOR = 'transparent'; |
||||
|
||||
/** |
||||
* The styles of the React {@code Components} of the feature welcome including |
||||
* {@code WelcomePage} and {@code BlankPage}. |
||||
*/ |
||||
export default createStyleSheet({ |
||||
|
||||
/** |
||||
* Standardized style for a field container {@code View}. |
||||
*/ |
||||
fieldContainer: { |
||||
flexDirection: 'row', |
||||
alignItems: 'center', |
||||
minHeight: 65 |
||||
}, |
||||
|
||||
/** |
||||
* Standard container for a {@code View} containing a field label. |
||||
*/ |
||||
fieldLabelContainer: { |
||||
flexDirection: 'row', |
||||
alignItems: 'center', |
||||
width: LABEL_TAB |
||||
}, |
||||
|
||||
/** |
||||
* Field container style for all but last row {@code View}. |
||||
*/ |
||||
fieldSeparator: { |
||||
borderBottomWidth: 1 |
||||
}, |
||||
|
||||
/** |
||||
* Style for the {@code View} containing each |
||||
* field values (the actual field). |
||||
*/ |
||||
fieldValueContainer: { |
||||
flex: 1, |
||||
justifyContent: 'flex-end', |
||||
flexDirection: 'row', |
||||
alignItems: 'center' |
||||
}, |
||||
|
||||
/** |
||||
* Page header {@code View}. |
||||
*/ |
||||
headerContainer: { |
||||
backgroundColor: ColorPalette.blue, |
||||
flexDirection: 'row', |
||||
alignItems: 'center', |
||||
padding: 2 * BoxModel.margin |
||||
}, |
||||
|
||||
/** |
||||
* The title {@code Text} of the header. |
||||
*/ |
||||
headerTitle: { |
||||
color: ColorPalette.white, |
||||
fontSize: 25 |
||||
}, |
||||
|
||||
/** |
||||
* The top level container {@code View}. |
||||
*/ |
||||
settingsContainer: { |
||||
backgroundColor: ColorPalette.white, |
||||
flex: 1, |
||||
flexDirection: 'column', |
||||
margin: 0, |
||||
padding: 2 * BoxModel.padding |
||||
}, |
||||
|
||||
/** |
||||
* Global {@code Text} color for the page. |
||||
*/ |
||||
text: { |
||||
color: ColorPalette.black, |
||||
fontSize: 20 |
||||
}, |
||||
|
||||
/** |
||||
* Standard text input field style. |
||||
*/ |
||||
textInputField: { |
||||
fontSize: 20, |
||||
flex: 1, |
||||
textAlign: 'right' |
||||
} |
||||
}); |
@ -0,0 +1,5 @@ |
||||
export * from './actions'; |
||||
export * from './components'; |
||||
export * from './functions'; |
||||
|
||||
import './reducer'; |
@ -0,0 +1,31 @@ |
||||
// @flow
|
||||
|
||||
import { |
||||
HIDE_APP_SETTINGS, |
||||
SHOW_APP_SETTINGS |
||||
} from './actionTypes'; |
||||
|
||||
import { ReducerRegistry } from '../base/redux'; |
||||
|
||||
const DEFAULT_STATE = { |
||||
visible: false |
||||
}; |
||||
|
||||
ReducerRegistry.register( |
||||
'features/app-settings', (state = DEFAULT_STATE, action) => { |
||||
switch (action.type) { |
||||
case HIDE_APP_SETTINGS: |
||||
return { |
||||
...state, |
||||
visible: false |
||||
}; |
||||
|
||||
case SHOW_APP_SETTINGS: |
||||
return { |
||||
...state, |
||||
visible: true |
||||
}; |
||||
} |
||||
|
||||
return state; |
||||
}); |
@ -0,0 +1,15 @@ |
||||
/** |
||||
* Create an action for when the local profile is updated. |
||||
* |
||||
* { |
||||
* type: PROFILE_UPDATED, |
||||
* profile: { |
||||
* displayName: string, |
||||
* defaultURL: URL, |
||||
* email: string, |
||||
* startWithAudioMuted: boolean, |
||||
* startWithVideoMuted: boolean |
||||
* } |
||||
* } |
||||
*/ |
||||
export const PROFILE_UPDATED = Symbol('PROFILE_UPDATED'); |
@ -0,0 +1,23 @@ |
||||
import { PROFILE_UPDATED } from './actionTypes'; |
||||
|
||||
/** |
||||
* Create an action for when the local profile is updated. |
||||
* |
||||
* @param {Object} profile - The new profile data. |
||||
* @returns {{ |
||||
* type: UPDATE_PROFILE, |
||||
* profile: { |
||||
* displayName: string, |
||||
* defaultURL: URL, |
||||
* email: string, |
||||
* startWithAudioMuted: boolean, |
||||
* startWithVideoMuted: boolean |
||||
* } |
||||
* }} |
||||
*/ |
||||
export function updateProfile(profile) { |
||||
return { |
||||
type: PROFILE_UPDATED, |
||||
profile |
||||
}; |
||||
} |
@ -0,0 +1,15 @@ |
||||
/* @flow */ |
||||
|
||||
/** |
||||
* Retreives the current profile settings from redux store. The profile |
||||
* is persisted to localStorage so it's a good candidate to store settings |
||||
* in it. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @returns {Object} |
||||
*/ |
||||
export function getProfile(state: Object) { |
||||
const profileStateSlice = state['features/base/profile']; |
||||
|
||||
return profileStateSlice ? profileStateSlice.profile || {} : {}; |
||||
} |
@ -0,0 +1,5 @@ |
||||
export * from './actions'; |
||||
export * from './functions'; |
||||
|
||||
import './middleware'; |
||||
import './reducer'; |
@ -0,0 +1,43 @@ |
||||
/* @flow */ |
||||
import { PROFILE_UPDATED } from './actionTypes'; |
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry'; |
||||
|
||||
import { participantUpdated } from '../participants'; |
||||
import { getProfile } from '../profile'; |
||||
import { toState } from '../redux'; |
||||
|
||||
/** |
||||
* A MiddleWare to update the local participant when the profile |
||||
* is updated. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @returns {Function} |
||||
*/ |
||||
MiddlewareRegistry.register(store => next => action => { |
||||
const result = next(action); |
||||
|
||||
switch (action.type) { |
||||
case PROFILE_UPDATED: |
||||
_updateLocalParticipant(store); |
||||
} |
||||
|
||||
return result; |
||||
}); |
||||
|
||||
/** |
||||
* Updates the local participant according to profile changes. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @returns {void} |
||||
*/ |
||||
function _updateLocalParticipant(store) { |
||||
const profile = getProfile(toState(store)); |
||||
|
||||
const newLocalParticipant = { |
||||
email: profile.email, |
||||
local: true, |
||||
name: profile.displayName |
||||
}; |
||||
|
||||
store.dispatch(participantUpdated(newLocalParticipant)); |
||||
} |
@ -0,0 +1,25 @@ |
||||
// @flow
|
||||
|
||||
import { |
||||
PROFILE_UPDATED |
||||
} from './actionTypes'; |
||||
|
||||
import { ReducerRegistry } from '../redux'; |
||||
|
||||
const DEFAULT_STATE = { |
||||
profile: {} |
||||
}; |
||||
|
||||
const STORE_NAME = 'features/base/profile'; |
||||
|
||||
ReducerRegistry.register( |
||||
STORE_NAME, (state = DEFAULT_STATE, action) => { |
||||
switch (action.type) { |
||||
case PROFILE_UPDATED: |
||||
return { |
||||
profile: action.profile |
||||
}; |
||||
} |
||||
|
||||
return state; |
||||
}); |
@ -1,3 +1,5 @@ |
||||
export * from './functions'; |
||||
export { default as MiddlewareRegistry } from './MiddlewareRegistry'; |
||||
export { default as ReducerRegistry } from './ReducerRegistry'; |
||||
|
||||
import './middleware'; |
||||
|
@ -0,0 +1,36 @@ |
||||
/* @flow */ |
||||
import _ from 'lodash'; |
||||
|
||||
import { persistState } from './functions'; |
||||
import MiddlewareRegistry from './MiddlewareRegistry'; |
||||
|
||||
import { toState } from '../redux'; |
||||
|
||||
/** |
||||
* The delay that passes between the last state change and the state to be |
||||
* persisted in the storage. |
||||
*/ |
||||
const PERSIST_DELAY = 2000; |
||||
|
||||
/** |
||||
* A throttled function to avoid repetitive state persisting. |
||||
*/ |
||||
const throttledFunc = _.throttle(state => { |
||||
persistState(state); |
||||
}, PERSIST_DELAY); |
||||
|
||||
/** |
||||
* A master MiddleWare to selectively persist state. Please use the |
||||
* {@link persisterconfig.json} to set which subtrees of the Redux state |
||||
* should be persisted. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @returns {Function} |
||||
*/ |
||||
MiddlewareRegistry.register(store => next => action => { |
||||
const result = next(action); |
||||
|
||||
throttledFunc(toState(store)); |
||||
|
||||
return result; |
||||
}); |
@ -0,0 +1,5 @@ |
||||
{ |
||||
"features/base/profile": { |
||||
"profile": true |
||||
} |
||||
} |
@ -0,0 +1,36 @@ |
||||
Jitsi Meet - redux state persistency |
||||
==================================== |
||||
Jitsi Meet has a persistency layer that persist a subtree (or specific subtrees) into window.localStorage (on web) or |
||||
AsyncStorage (on mobile). |
||||
|
||||
Usage |
||||
===== |
||||
If a subtree of the redux store should be persisted (e.g. ``'features/base/participants'``), then persistency for that |
||||
subtree should be enabled in the config file by creating a key in |
||||
|
||||
``` |
||||
react/features/base/redux/persisterconfig.json |
||||
``` |
||||
and defining all the fields of the subtree that has to be persisted, e.g.: |
||||
```json |
||||
{ |
||||
"features/base/participants": { |
||||
"avatarID": true, |
||||
"avatarURL": true, |
||||
"name": true |
||||
}, |
||||
"another/subtree": { |
||||
"someField": true |
||||
} |
||||
} |
||||
``` |
||||
When it's done, Jitsi Meet will persist these subtrees/fields and rehidrate them on startup. |
||||
|
||||
Throttling |
||||
========== |
||||
To avoid too frequent write operations in the storage, we utilise throttling in the persistency layer, meaning that the storage |
||||
gets persisted only once in every 2 seconds, even if multiple redux state changes occur during this period. This throttling timeout |
||||
can be configured in |
||||
``` |
||||
react/features/base/redux/middleware.js#PERSIST_DELAY |
||||
``` |
Loading…
Reference in new issue