diff options
Diffstat (limited to 'app')
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> ); } |