aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/features/conference/components/Conference.js143
-rw-r--r--app/features/navbar/actionTypes.js18
-rw-r--r--app/features/navbar/actions.js34
-rw-r--r--app/features/navbar/components/Navbar.js38
-rw-r--r--app/features/navbar/components/index.js1
-rw-r--r--app/features/navbar/index.js5
-rw-r--r--app/features/navbar/reducer.js39
-rw-r--r--app/features/navbar/styled/DrawerContainer.js8
-rw-r--r--app/features/navbar/styled/index.js1
-rw-r--r--app/features/redux/index.js1
-rw-r--r--app/features/redux/middleware.js2
-rw-r--r--app/features/redux/persistor.js7
-rw-r--r--app/features/redux/reducers.js6
-rw-r--r--app/features/redux/store.js14
-rw-r--r--app/features/settings/actionTypes.js30
-rw-r--r--app/features/settings/actions.js51
-rw-r--r--app/features/settings/components/SettingsAction.js60
-rw-r--r--app/features/settings/components/SettingsDrawer.js193
-rw-r--r--app/features/settings/components/index.js2
-rw-r--r--app/features/settings/index.js7
-rw-r--r--app/features/settings/middleware.js24
-rw-r--r--app/features/settings/reducer.js53
-rw-r--r--app/features/settings/styled/AvatarContainer.js9
-rw-r--r--app/features/settings/styled/ProfileContainer.js8
-rw-r--r--app/features/settings/styled/index.js2
-rw-r--r--app/features/utils/functions.js33
-rw-r--r--app/index.js17
27 files changed, 794 insertions, 12 deletions
diff --git a/app/features/conference/components/Conference.js b/app/features/conference/components/Conference.js
index 8683820..f308ef0 100644
--- a/app/features/conference/components/Conference.js
+++ b/app/features/conference/components/Conference.js
@@ -14,21 +14,38 @@ import {
} from 'jitsi-meet-electron-utils';
import config from '../../config';
+import { setEmail, setName } from '../../settings';
import { Wrapper } from '../styled';
type Props = {
/**
+ * Redux dispatch.
+ */
+ dispatch: Dispatch<*>;
+
+ /**
* React Router match object.
* This contains parameters passed through <Route /> component.
*/
match: Object;
/**
- * Redux dispatch.
+ * Avatar URL.
*/
- dispatch: Dispatch<*>;
+ _avatarURL: string;
+
+ /**
+ * Email of user.
+ */
+ _email: string;
+
+ /**
+ * Name of user.
+ */
+ _name: string;
+
};
/**
@@ -77,6 +94,26 @@ class Conference extends Component<Props, *> {
}
/**
+ * Keep profile settings in sync with Conference.
+ *
+ * @param {Props} prevProps - Component's prop values before update.
+ * @returns {void}
+ */
+ componentDidUpdate(prevProps) {
+ const { props } = this;
+
+ if (props._avatarURL !== prevProps._avatarURL) {
+ this._setAvatarURL(props._avatarURL);
+ }
+ if (props._email !== prevProps._email) {
+ this._setEmail(props._email);
+ }
+ if (props._name !== prevProps._name) {
+ this._setName(props._name);
+ }
+ }
+
+ /**
* Remove conference on unmounting.
*
* @returns {void}
@@ -131,7 +168,107 @@ class Conference extends Component<Props, *> {
setupWiFiStats(iframe);
this._api.on('readyToClose', () => this._navigateToHome());
+
+ this._api.on('videoConferenceJoined',
+ (conferenceInfo: Object) =>
+ this._onVideoConferenceJoined(conferenceInfo));
+ }
+
+ /**
+ * Updates redux state's user name from conference.
+ *
+ * @param {Object} params - Returned object from event.
+ * @param {string} id - Local Participant ID.
+ * @returns {void}
+ */
+ _onDisplayNameChange(params: Object, id: string) {
+ if (params.id === id) {
+ this.props.dispatch(setName(params.displayname));
+ }
}
+
+ /**
+ * Updates redux state's email from conference.
+ *
+ * @param {Object} params - Returned object from event.
+ * @param {string} id - Local Participant ID.
+ * @returns {void}
+ */
+ _onEmailChange(params: Object, id: string) {
+ if (params.id === id) {
+ this.props.dispatch(setEmail(params.email));
+ }
+ }
+
+ /**
+ * Saves conference info on joining it.
+ *
+ * @param {Object} conferenceInfo - Contains information about the current
+ * conference.
+ * @returns {void}
+ */
+ _onVideoConferenceJoined(conferenceInfo: Object) {
+
+ this._setAvatarURL(this.props._avatarURL);
+ this._setEmail(this.props._email);
+ this._setName(this.props._name);
+
+ const { id } = conferenceInfo;
+
+ this._api.on('displayNameChange',
+ (params: Object) => this._onDisplayNameChange(params, id));
+ this._api.on('emailChange',
+ (params: Object) => this._onEmailChange(params, id));
+ }
+
+ /**
+ * Set Avatar URL from settings to conference.
+ *
+ * @param {string} avatarURL - Avatar URL.
+ * @returns {void}
+ */
+ _setAvatarURL(avatarURL: string) {
+ this._api.executeCommand('avatarUrl', avatarURL);
+ }
+
+ /**
+ * Set email from settings to conference.
+ *
+ * @param {string} email - Email of user.
+ * @returns {void}
+ */
+ _setEmail(email: string) {
+ this._api.executeCommand('email', email);
+ }
+
+ /**
+ * Set name from settings to conference.
+ *
+ * @param {string} name - Name of user.
+ * @returns {void}
+ */
+ _setName(name: string) {
+ this._api.executeCommand('displayName', name);
+ }
+
+}
+
+/**
+ * Maps (parts of) the redux state to the React props.
+ *
+ * @param {Object} state - The redux state.
+ * @returns {{
+ * _avatarURL: string,
+ * _email: string,
+ * _name: string
+ * }}
+ */
+function _mapStateToProps(state: Object) {
+ return {
+ _avatarURL: state.settings.avatarURL,
+ _email: state.settings.email,
+ _name: state.settings.name
+ };
}
-export default connect()(Conference);
+export default connect(_mapStateToProps)(Conference);
diff --git a/app/features/navbar/actionTypes.js b/app/features/navbar/actionTypes.js
new file mode 100644
index 0000000..0c5efae
--- /dev/null
+++ b/app/features/navbar/actionTypes.js
@@ -0,0 +1,18 @@
+/**
+ * The type of (redux) action that opens specified Drawer.
+ *
+ * {
+ * type: OPEN_DRAWER,
+ * drawerComponent: React.ComponentType<*>
+ * }
+ */
+export const OPEN_DRAWER = Symbol('OPEN_DRAWER');
+
+/**
+ * The type of (redux) action that closes all Drawer.
+ *
+ * {
+ * type: CLOSE_DRAWER
+ * }
+ */
+export const CLOSE_DRAWER = Symbol('CLOSE_DRAWER');
diff --git a/app/features/navbar/actions.js b/app/features/navbar/actions.js
new file mode 100644
index 0000000..af301b1
--- /dev/null
+++ b/app/features/navbar/actions.js
@@ -0,0 +1,34 @@
+// @flow
+
+import type { ComponentType } from 'react';
+
+import { CLOSE_DRAWER, OPEN_DRAWER } from './actionTypes';
+
+/**
+ * Closes the drawers.
+ *
+ * @returns {{
+ * type: CLOSE_DRAWER,
+ * }}
+ */
+export function closeDrawer() {
+ return {
+ type: CLOSE_DRAWER
+ };
+}
+
+/**
+ * Opens the specified drawer.
+ *
+ * @param {string} drawerComponent - Component of the drawer.
+ * @returns {{
+ * type: OPEN_DRAWER,
+ * drawerComponent: ComponentType<*>
+ * }}
+ */
+export function openDrawer(drawerComponent: ComponentType<*>) {
+ return {
+ type: OPEN_DRAWER,
+ drawerComponent
+ };
+}
diff --git a/app/features/navbar/components/Navbar.js b/app/features/navbar/components/Navbar.js
index f767436..c193211 100644
--- a/app/features/navbar/components/Navbar.js
+++ b/app/features/navbar/components/Navbar.js
@@ -4,7 +4,9 @@
import Navigation, { AkGlobalItem } from '@atlaskit/navigation';
import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { SettingsAction, SettingsDrawer } from '../../settings';
import { isElectronMac } from '../../utils';
import HelpAction from './HelpAction';
@@ -24,6 +26,19 @@ class Navbar extends Component<*> {
}
/**
+ * Get the array of Primary actions of Global Navigation.
+ *
+ * @returns {ReactElement[]}
+ */
+ _getPrimaryActions() {
+ return [
+ <AkGlobalItem key = { 0 }>
+ <SettingsAction />
+ </AkGlobalItem>
+ ];
+ }
+
+ /**
* Get the array of Secondary actions of Global Navigation.
*
* @returns {ReactElement[]}
@@ -44,6 +59,12 @@ class Navbar extends Component<*> {
render() {
return (
<Navigation
+ drawers = { [
+ <SettingsDrawer
+ isOpen = { this.props._isSettingsDrawerOpen }
+ key = { 0 } />
+ ] }
+ globalPrimaryActions = { this._getPrimaryActions() }
globalPrimaryIcon = { this._getPrimaryIcon() }
globalSecondaryActions = { this._getSecondaryActions() }
isElectronMac = { isElectronMac() }
@@ -53,4 +74,19 @@ class Navbar extends Component<*> {
}
}
-export default Navbar;
+/**
+ * Maps (parts of) the redux state to the React props.
+ *
+ * @param {Object} state - The redux state.
+ * @returns {{
+ * _isSettingsDrawerOpen: boolean
+ * }}
+ */
+function _mapStateToProps(state: Object) {
+ return {
+ _isSettingsDrawerOpen: state.navbar.openDrawer === SettingsDrawer
+ };
+}
+
+
+export default connect(_mapStateToProps)(Navbar);
diff --git a/app/features/navbar/components/index.js b/app/features/navbar/components/index.js
index ee1d016..0e08bca 100644
--- a/app/features/navbar/components/index.js
+++ b/app/features/navbar/components/index.js
@@ -1 +1,2 @@
+export { default as Logo } from './Logo';
export { default as Navbar } from './Navbar';
diff --git a/app/features/navbar/index.js b/app/features/navbar/index.js
index 07635cb..da41a84 100644
--- a/app/features/navbar/index.js
+++ b/app/features/navbar/index.js
@@ -1 +1,6 @@
+export * from './actions';
+export * from './actionTypes';
export * from './components';
+export * from './styled';
+
+export { default as reducer } from './reducer';
diff --git a/app/features/navbar/reducer.js b/app/features/navbar/reducer.js
new file mode 100644
index 0000000..ec0e498
--- /dev/null
+++ b/app/features/navbar/reducer.js
@@ -0,0 +1,39 @@
+// @flow
+
+import type { ComponentType } from 'react';
+
+import { CLOSE_DRAWER, OPEN_DRAWER } from './actionTypes';
+
+type State = {
+ openDrawer: typeof undefined | ComponentType<*>
+};
+
+const DEFAULT_STATE = {
+ openDrawer: undefined
+};
+
+/**
+ * Reduces redux actions for features/settings.
+ *
+ * @param {State} state - Current reduced redux state.
+ * @param {Object} action - Action which was dispatched.
+ * @returns {State} - Updated reduced redux state.
+ */
+export default (state: State = DEFAULT_STATE, action: Object) => {
+ switch (action.type) {
+ case CLOSE_DRAWER:
+ return {
+ ...state,
+ openDrawer: undefined
+ };
+
+ case OPEN_DRAWER:
+ return {
+ ...state,
+ openDrawer: action.drawerComponent
+ };
+
+ default:
+ return state;
+ }
+};
diff --git a/app/features/navbar/styled/DrawerContainer.js b/app/features/navbar/styled/DrawerContainer.js
new file mode 100644
index 0000000..8f4a2dc
--- /dev/null
+++ b/app/features/navbar/styled/DrawerContainer.js
@@ -0,0 +1,8 @@
+// @flow
+
+import styled from 'styled-components';
+
+export default styled.div`
+ margin-right: 68px;
+ padding: 0 8px;
+`;
diff --git a/app/features/navbar/styled/index.js b/app/features/navbar/styled/index.js
new file mode 100644
index 0000000..065175a
--- /dev/null
+++ b/app/features/navbar/styled/index.js
@@ -0,0 +1 @@
+export { default as DrawerContainer } from './DrawerContainer';
diff --git a/app/features/redux/index.js b/app/features/redux/index.js
index 487ab35..f76681b 100644
--- a/app/features/redux/index.js
+++ b/app/features/redux/index.js
@@ -1 +1,2 @@
+export { default as persistor } from './persistor';
export { default as store } from './store';
diff --git a/app/features/redux/middleware.js b/app/features/redux/middleware.js
index 8b1a2cd..9938da5 100644
--- a/app/features/redux/middleware.js
+++ b/app/features/redux/middleware.js
@@ -4,8 +4,10 @@ import { applyMiddleware } from 'redux';
import { createLogger } from 'redux-logger';
import { middleware as routerMiddleware } from '../router';
+import { middleware as settingsMiddleware } from '../settings';
export default applyMiddleware(
routerMiddleware,
+ settingsMiddleware,
createLogger()
);
diff --git a/app/features/redux/persistor.js b/app/features/redux/persistor.js
new file mode 100644
index 0000000..dfc12ef
--- /dev/null
+++ b/app/features/redux/persistor.js
@@ -0,0 +1,7 @@
+// @flow
+
+import { persistStore } from 'redux-persist';
+
+import store from './store';
+
+export default persistStore(store);
diff --git a/app/features/redux/reducers.js b/app/features/redux/reducers.js
index 256873c..667b922 100644
--- a/app/features/redux/reducers.js
+++ b/app/features/redux/reducers.js
@@ -2,8 +2,12 @@
import { combineReducers } from 'redux';
+import { reducer as navbarReducer } from '../navbar';
import { reducer as routerReducer } from '../router';
+import { reducer as settingsReducer } from '../settings';
export default combineReducers({
- router: routerReducer
+ navbar: navbarReducer,
+ router: routerReducer,
+ settings: settingsReducer
});
diff --git a/app/features/redux/store.js b/app/features/redux/store.js
index 2e4dc00..992e802 100644
--- a/app/features/redux/store.js
+++ b/app/features/redux/store.js
@@ -1,8 +1,20 @@
// @flow
import { createStore } from 'redux';
+import { persistReducer } from 'redux-persist';
+import createElectronStorage from 'redux-persist-electron-storage';
import middleware from './middleware';
import reducers from './reducers';
-export default createStore(reducers, middleware);
+const persistConfig = {
+ key: 'root',
+ storage: createElectronStorage(),
+ whitelist: [
+ 'settings'
+ ]
+};
+
+const persistedReducer = persistReducer(persistConfig, reducers);
+
+export default createStore(persistedReducer, middleware);
diff --git a/app/features/settings/actionTypes.js b/app/features/settings/actionTypes.js
new file mode 100644
index 0000000..977ef4d
--- /dev/null
+++ b/app/features/settings/actionTypes.js
@@ -0,0 +1,30 @@
+/**
+ * The type of (redux) action that sets the Avatar URL.
+ *
+ * {
+ * type: SET_AVATAR_URL,
+ * avatarURL: string
+ * }
+ */
+export const SET_AVATAR_URL = Symbol('SET_AVATAR_URL');
+
+/**
+ * The type of (redux) action that sets the email of the user.
+ *
+ * {
+ * type: SET_EMAIL,
+ * email: string
+ * }
+ */
+export const SET_EMAIL = Symbol('SET_EMAIL');
+
+/**
+ * The type of (redux) action that sets the name of the user.
+ *
+ * {
+ * type: SET_NAME,
+ * name: string
+ * }
+ */
+export const SET_NAME = Symbol('SET_NAME');
+
diff --git a/app/features/settings/actions.js b/app/features/settings/actions.js
new file mode 100644
index 0000000..dd5a383
--- /dev/null
+++ b/app/features/settings/actions.js
@@ -0,0 +1,51 @@
+// @flow
+
+import { SET_AVATAR_URL, SET_EMAIL, SET_NAME } from './actionTypes';
+
+/**
+ * Set Avatar URL.
+ *
+ * @param {string} avatarURL - Avatar URL.
+ * @returns {{
+ * type: SET_AVATAR_URL,
+ * avatarURL: string
+ * }}
+ */
+export function setAvatarURL(avatarURL: string) {
+ return {
+ type: SET_AVATAR_URL,
+ avatarURL
+ };
+}
+
+/**
+ * Set the email of the user.
+ *
+ * @param {string} email - Email of the user.
+ * @returns {{
+ * type: SET_EMAIL,
+ * email: string
+ * }}
+ */
+export function setEmail(email: string) {
+ return {
+ type: SET_EMAIL,
+ email
+ };
+}
+
+/**
+ * Set the name of the user.
+ *
+ * @param {string} name - Name of the user.
+ * @returns {{
+ * type: SET_NAME,
+ * name: string
+ * }}
+ */
+export function setName(name: string) {
+ return {
+ type: SET_NAME,
+ name
+ };
+}
diff --git a/app/features/settings/components/SettingsAction.js b/app/features/settings/components/SettingsAction.js
new file mode 100644
index 0000000..080e3c6
--- /dev/null
+++ b/app/features/settings/components/SettingsAction.js
@@ -0,0 +1,60 @@
+// @flow
+
+import SettingsIcon from '@atlaskit/icon/glyph/settings';
+
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import type { Dispatch } from 'redux';
+
+import { openDrawer } from '../../navbar';
+
+import SettingsDrawer from './SettingsDrawer';
+
+type Props = {
+
+ /**
+ * Redux dispatch.
+ */
+ dispatch: Dispatch<*>;
+};
+
+/**
+ * Setttings Action for Navigation Bar.
+ */
+class SettingsAction extends Component<Props, *> {
+ /**
+ * Initializes a new {@code SettingsAction} instance.
+ *
+ * @inheritdoc
+ */
+ constructor() {
+ super();
+
+ this._onIconClick = this._onIconClick.bind(this);
+ }
+
+ /**
+ * Render function of component.
+ *
+ * @returns {ReactElement}
+ */
+ render() {
+ return (
+ <SettingsIcon
+ onClick = { this._onIconClick } />
+ );
+ }
+
+ _onIconClick: (*) => void;
+
+ /**
+ * Open Settings drawer when SettingsAction is clicked.
+ *
+ * @returns {void}
+ */
+ _onIconClick() {
+ this.props.dispatch(openDrawer(SettingsDrawer));
+ }
+}
+
+export default connect()(SettingsAction);
diff --git a/app/features/settings/components/SettingsDrawer.js b/app/features/settings/components/SettingsDrawer.js
new file mode 100644
index 0000000..7869cd2
--- /dev/null
+++ b/app/features/settings/components/SettingsDrawer.js
@@ -0,0 +1,193 @@
+// @flow
+
+import Avatar from '@atlaskit/avatar';
+import FieldText from '@atlaskit/field-text';
+import ArrowLeft from '@atlaskit/icon/glyph/arrow-left';
+import { AkCustomDrawer } from '@atlaskit/navigation';
+
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import type { Dispatch } from 'redux';
+
+import { closeDrawer, DrawerContainer, Logo } from '../../navbar';
+import { AvatarContainer, ProfileContainer } from '../styled';
+import { setEmail, setName } from '../actions';
+
+type Props = {
+
+ /**
+ * Redux dispatch.
+ */
+ dispatch: Dispatch<*>;
+
+ /**
+ * Is the drawer open or not.
+ */
+ isOpen: boolean;
+
+ /**
+ * Avatar URL.
+ */
+ _avatarURL: string;
+
+ /**
+ * Email of the user.
+ */
+ _email: string;
+
+ /**
+ * Name of the user.
+ */
+ _name: string;
+};
+
+/**
+ * Drawer that open when SettingsAction is clicked.
+ */
+class SettingsDrawer extends Component<Props, *> {
+ /**
+ * Initializes a new {@code SettingsDrawer} instance.
+ *
+ * @inheritdoc
+ */
+ constructor(props) {
+ super(props);
+
+ this._onBackButton = this._onBackButton.bind(this);
+ this._onEmailBlur = this._onEmailBlur.bind(this);
+ this._onEmailFormSubmit = this._onEmailFormSubmit.bind(this);
+ this._onNameBlur = this._onNameBlur.bind(this);
+ this._onNameFormSubmit = this._onNameFormSubmit.bind(this);
+ }
+
+ /**
+ * Render function of component.
+ *
+ * @returns {ReactElement}
+ */
+ render() {
+ return (
+ <AkCustomDrawer
+ backIcon = { <ArrowLeft label = 'Back' /> }
+ isOpen = { this.props.isOpen }
+ onBackButton = { this._onBackButton }
+ primaryIcon = { <Logo /> } >
+ <DrawerContainer>
+ <ProfileContainer>
+ <AvatarContainer>
+ <Avatar
+ size = 'xlarge'
+ src = { this.props._avatarURL } />
+ </AvatarContainer>
+ <form onSubmit = { this._onNameFormSubmit }>
+ <FieldText
+ label = 'Name'
+ onBlur = { this._onNameBlur }
+ shouldFitContainer = { true }
+ type = 'text'
+ value = { this.props._name } />
+ </form>
+ <form onSubmit = { this._onEmailFormSubmit }>
+ <FieldText
+ label = 'Email'
+ onBlur = { this._onEmailBlur }
+ shouldFitContainer = { true }
+ type = 'text'
+ value = { this.props._email } />
+ </form>
+ </ProfileContainer>
+ </DrawerContainer>
+ </AkCustomDrawer>
+ );
+ }
+
+
+ _onBackButton: (*) => void;
+
+ /**
+ * Closes the drawer when back button is clicked.
+ *
+ * @returns {void}
+ */
+ _onBackButton() {
+ this.props.dispatch(closeDrawer());
+ }
+
+ _onEmailBlur: (*) => void;
+
+ /**
+ * Updates Avatar URL in (redux) state when email is updated.
+ *
+ * @param {SyntheticInputEvent<HTMLInputElement>} event - Event by which
+ * this function is called.
+ * @returns {void}
+ */
+ _onEmailBlur(event: SyntheticInputEvent<HTMLInputElement>) {
+ this.props.dispatch(setEmail(event.currentTarget.value));
+ }
+
+ _onEmailFormSubmit: (*) => void;
+
+ /**
+ * Prevents submission of the form and updates email.
+ *
+ * @param {SyntheticEvent<HTMLFormElement>} event - Event by which
+ * this function is called.
+ * @returns {void}
+ */
+ _onEmailFormSubmit(event: SyntheticEvent<HTMLFormElement>) {
+ event.preventDefault();
+
+ // $FlowFixMe
+ this.props.dispatch(setEmail(event.currentTarget.elements[0].value));
+ }
+
+ _onNameBlur: (*) => void;
+
+ /**
+ * Updates Avatar URL in (redux) state when name is updated.
+ *
+ * @param {SyntheticInputEvent<HTMLInputElement>} event - Event by which
+ * this function is called.
+ * @returns {void}
+ */
+ _onNameBlur(event: SyntheticInputEvent<HTMLInputElement>) {
+ this.props.dispatch(setName(event.currentTarget.value));
+ }
+
+ _onNameFormSubmit: (*) => void;
+
+ /**
+ * Prevents submission of the form and updates name.
+ *
+ * @param {SyntheticEvent<HTMLFormElement>} event - Event by which
+ * this function is called.
+ * @returns {void}
+ */
+ _onNameFormSubmit(event: SyntheticEvent<HTMLFormElement>) {
+ event.preventDefault();
+
+ // $FlowFixMe
+ this.props.dispatch(setName(event.currentTarget.elements[0].value));
+ }
+}
+
+/**
+ * Maps (parts of) the redux state to the React props.
+ *
+ * @param {Object} state - The redux state.
+ * @returns {{
+ * _avatarURL: string,
+ * _email: string,
+ * _name: string
+ * }}
+ */
+function _mapStateToProps(state: Object) {
+ return {
+ _avatarURL: state.settings.avatarURL,
+ _email: state.settings.email,
+ _name: state.settings.name
+ };
+}
+
+export default connect(_mapStateToProps)(SettingsDrawer);
diff --git a/app/features/settings/components/index.js b/app/features/settings/components/index.js
new file mode 100644
index 0000000..63dd49d
--- /dev/null
+++ b/app/features/settings/components/index.js
@@ -0,0 +1,2 @@
+export { default as SettingsAction } from './SettingsAction';
+export { default as SettingsDrawer } from './SettingsDrawer';
diff --git a/app/features/settings/index.js b/app/features/settings/index.js
new file mode 100644
index 0000000..17b8002
--- /dev/null
+++ b/app/features/settings/index.js
@@ -0,0 +1,7 @@
+export * from './actions';
+export * from './actionTypes';
+export * from './components';
+export * from './styled';
+
+export { default as middleware } from './middleware';
+export { default as reducer } from './reducer';
diff --git a/app/features/settings/middleware.js b/app/features/settings/middleware.js
new file mode 100644
index 0000000..0d6e489
--- /dev/null
+++ b/app/features/settings/middleware.js
@@ -0,0 +1,24 @@
+// @flow
+
+import { getAvatarURL } from '../utils';
+import { SET_EMAIL, SET_NAME } from './actionTypes';
+import { setAvatarURL } from './actions';
+
+export default (store: Object) => (next: Function) => (action: Object) => {
+ const result = next(action);
+ const state = store.getState();
+
+ switch (action.type) {
+ case SET_EMAIL:
+ case SET_NAME: {
+ const avatarURL = getAvatarURL({
+ email: state.settings.email,
+ id: state.settings.name
+ });
+
+ store.dispatch(setAvatarURL(avatarURL));
+ }
+ }
+
+ return result;
+};
diff --git a/app/features/settings/reducer.js b/app/features/settings/reducer.js
new file mode 100644
index 0000000..02d42d5
--- /dev/null
+++ b/app/features/settings/reducer.js
@@ -0,0 +1,53 @@
+// @flow
+
+import os from 'os';
+
+import { getAvatarURL } from '../utils';
+
+import { SET_AVATAR_URL, SET_EMAIL, SET_NAME } from './actionTypes';
+
+type State = {
+ avatarURL: string,
+ email: string,
+ name: string
+};
+
+const username = os.userInfo().username;
+
+const DEFAULT_STATE = {
+ avatarURL: getAvatarURL({ id: username }),
+ email: '',
+ name: username
+};
+
+/**
+ * Reduces redux actions for features/settings.
+ *
+ * @param {State} state - Current reduced redux state.
+ * @param {Object} action - Action which was dispatched.
+ * @returns {State} - Updated reduced redux state.
+ */
+export default (state: State = DEFAULT_STATE, action: Object) => {
+ switch (action.type) {
+ case SET_AVATAR_URL:
+ return {
+ ...state,
+ avatarURL: action.avatarURL
+ };
+
+ case SET_EMAIL:
+ return {
+ ...state,
+ email: action.email
+ };
+
+ case SET_NAME:
+ return {
+ ...state,
+ name: action.name
+ };
+
+ default:
+ return state;
+ }
+};
diff --git a/app/features/settings/styled/AvatarContainer.js b/app/features/settings/styled/AvatarContainer.js
new file mode 100644
index 0000000..65e617e
--- /dev/null
+++ b/app/features/settings/styled/AvatarContainer.js
@@ -0,0 +1,9 @@
+// @flow
+
+import styled from 'styled-components';
+
+export default styled.div`
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+`;
diff --git a/app/features/settings/styled/ProfileContainer.js b/app/features/settings/styled/ProfileContainer.js
new file mode 100644
index 0000000..8daae3a
--- /dev/null
+++ b/app/features/settings/styled/ProfileContainer.js
@@ -0,0 +1,8 @@
+// @flow
+
+import styled from 'styled-components';
+
+export default styled.div`
+ margin: 0 auto;
+ width: 70%;
+`;
diff --git a/app/features/settings/styled/index.js b/app/features/settings/styled/index.js
new file mode 100644
index 0000000..ed00704
--- /dev/null
+++ b/app/features/settings/styled/index.js
@@ -0,0 +1,2 @@
+export { default as AvatarContainer } from './AvatarContainer';
+export { default as ProfileContainer } from './ProfileContainer';
diff --git a/app/features/utils/functions.js b/app/features/utils/functions.js
index a86d05f..5f31ddd 100644
--- a/app/features/utils/functions.js
+++ b/app/features/utils/functions.js
@@ -3,6 +3,7 @@
// @flow
import { shell } from 'electron';
+import md5 from 'js-md5';
/**
* Opens the provided link in default broswer.
@@ -22,3 +23,35 @@ export function openExternalLink(link: string) {
export function isElectronMac() {
return process.platform === 'darwin';
}
+
+/**
+ * Returns the Avatar URL to be used.
+ *
+ * @param {string} key - Unique key to generate Avatar URL.
+ * @returns {string}
+ */
+export function getAvatarURL({ email, id }: {
+ email: string,
+ id: string
+}) {
+ let key = email || id;
+ let urlPrefix;
+ let urlSuffix;
+
+ // If the ID looks like an e-mail address, we'll use Gravatar because it
+ // supports e-mail addresses.
+ if (key && key.indexOf('@') > 0) {
+
+ // URL prefix and suffix of gravatar service.
+ urlPrefix = 'https://www.gravatar.com/avatar/';
+ urlSuffix = '?d=wavatar&size=200';
+ } else {
+ key = id;
+
+ // Otherwise, use a default (meeples, of course).
+ urlPrefix = 'https://abotars.jitsi.net/meeple/';
+ urlSuffix = '';
+ }
+
+ return urlPrefix + md5.hex(key.trim().toLowerCase()) + urlSuffix;
+}
diff --git a/app/index.js b/app/index.js
index 96fe943..215fca2 100644
--- a/app/index.js
+++ b/app/index.js
@@ -1,16 +1,17 @@
// @flow
-import React, { Component } from 'react';
-import { render } from 'react-dom';
-import { Provider } from 'react-redux';
-
/**
* AtlasKit components will deflect from appearance if css-reset is not present.
*/
import '@atlaskit/css-reset';
+import React, { Component } from 'react';
+import { render } from 'react-dom';
+import { Provider } from 'react-redux';
+import { PersistGate } from 'redux-persist/integration/react';
+
import { App } from './features/app';
-import { store } from './features/redux';
+import { persistor, store } from './features/redux';
/**
* Component encapsulating App component with redux store using provider.
@@ -24,7 +25,11 @@ class Root extends Component<*> {
render() {
return (
<Provider store = { store }>
- <App />
+ <PersistGate
+ loading = { null }
+ persistor = { persistor }>
+ <App />
+ </PersistGate>
</Provider>
);
}