aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/layout/AppLayout.js2
-rw-r--r--src/components/services/content/WebviewCrashHandler.js7
-rw-r--r--src/components/subscription/SubscriptionPopup.js3
-rw-r--r--src/components/ui/Modal/index.js26
-rw-r--r--src/components/ui/Modal/styles.js1
-rw-r--r--src/config.js7
-rw-r--r--src/features/basicAuth/Component.js1
-rw-r--r--src/features/basicAuth/index.js3
-rw-r--r--src/features/delayApp/index.js6
-rw-r--r--src/features/shareFranz/Component.js170
-rw-r--r--src/features/shareFranz/index.js52
-rw-r--r--src/i18n/locales/en-US.json9
-rw-r--r--src/lib/analytics.js4
-rw-r--r--src/stores/AppStore.js7
-rw-r--r--src/stores/FeaturesStore.js2
-rw-r--r--src/stores/RecipePreviewsStore.js3
-rw-r--r--src/stores/RequestStore.js3
-rw-r--r--src/stores/ServicesStore.js5
18 files changed, 282 insertions, 29 deletions
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index bce792e56..593149e72 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -7,6 +7,7 @@ import { TitleBar } from 'electron-react-titlebar';
7import InfoBar from '../ui/InfoBar'; 7import InfoBar from '../ui/InfoBar';
8import { Component as DelayApp } from '../../features/delayApp'; 8import { Component as DelayApp } from '../../features/delayApp';
9import { Component as BasicAuth } from '../../features/basicAuth'; 9import { Component as BasicAuth } from '../../features/basicAuth';
10import { Component as ShareFranz } from '../../features/shareFranz';
10import ErrorBoundary from '../util/ErrorBoundary'; 11import ErrorBoundary from '../util/ErrorBoundary';
11 12
12// import globalMessages from '../../i18n/globalMessages'; 13// import globalMessages from '../../i18n/globalMessages';
@@ -164,6 +165,7 @@ export default @observer class AppLayout extends Component {
164 )} 165 )}
165 {isDelayAppScreenVisible && (<DelayApp />)} 166 {isDelayAppScreenVisible && (<DelayApp />)}
166 <BasicAuth /> 167 <BasicAuth />
168 <ShareFranz />
167 {services} 169 {services}
168 </div> 170 </div>
169 </div> 171 </div>
diff --git a/src/components/services/content/WebviewCrashHandler.js b/src/components/services/content/WebviewCrashHandler.js
index 42bc3c877..7a69dba87 100644
--- a/src/components/services/content/WebviewCrashHandler.js
+++ b/src/components/services/content/WebviewCrashHandler.js
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
2import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import ms from 'ms';
5 6
6import Button from '../../ui/Button'; 7import Button from '../../ui/Button';
7 8
@@ -35,12 +36,12 @@ export default @observer class WebviewCrashHandler extends Component {
35 }; 36 };
36 37
37 state = { 38 state = {
38 countdown: 10000, 39 countdown: ms('10s'),
39 } 40 }
40 41
41 countdownInterval = null; 42 countdownInterval = null;
42 43
43 countdownIntervalTimeout = 1000; 44 countdownIntervalTimeout = ms('1s');
44 45
45 46
46 componentDidMount() { 47 componentDidMount() {
@@ -75,7 +76,7 @@ export default @observer class WebviewCrashHandler extends Component {
75 <p className="footnote"> 76 <p className="footnote">
76 {intl.formatMessage(messages.autoReload, { 77 {intl.formatMessage(messages.autoReload, {
77 name, 78 name,
78 seconds: this.state.countdown / 1000, 79 seconds: this.state.countdown / ms('1s'),
79 })} 80 })}
80 </p> 81 </p>
81 </div> 82 </div>
diff --git a/src/components/subscription/SubscriptionPopup.js b/src/components/subscription/SubscriptionPopup.js
index b5d7c4b2d..0f6f0260f 100644
--- a/src/components/subscription/SubscriptionPopup.js
+++ b/src/components/subscription/SubscriptionPopup.js
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
3import { observer } from 'mobx-react'; 3import { observer } from 'mobx-react';
4import { defineMessages, intlShape } from 'react-intl'; 4import { defineMessages, intlShape } from 'react-intl';
5import Webview from 'react-electron-web-view'; 5import Webview from 'react-electron-web-view';
6import ms from 'ms';
6 7
7import Button from '../ui/Button'; 8import Button from '../ui/Button';
8 9
@@ -42,7 +43,7 @@ export default @observer class SubscriptionPopup extends Component {
42 43
43 setTimeout(() => { 44 setTimeout(() => {
44 this.props.closeWindow(); 45 this.props.closeWindow();
45 }, 4000); 46 }, ms('4s'));
46 } 47 }
47 48
48 render() { 49 render() {
diff --git a/src/components/ui/Modal/index.js b/src/components/ui/Modal/index.js
index d84e4c713..8e6ec5a0e 100644
--- a/src/components/ui/Modal/index.js
+++ b/src/components/ui/Modal/index.js
@@ -5,6 +5,9 @@ import classnames from 'classnames';
5import injectCSS from 'react-jss'; 5import injectCSS from 'react-jss';
6 6
7import styles from './styles'; 7import styles from './styles';
8import { Icon } from '../../../../packages/ui/lib';
9
10// ReactModal.setAppElement('#root');
8 11
9export default @injectCSS(styles) class Modal extends Component { 12export default @injectCSS(styles) class Modal extends Component {
10 static propTypes = { 13 static propTypes = {
@@ -14,11 +17,15 @@ export default @injectCSS(styles) class Modal extends Component {
14 isOpen: PropTypes.bool.isRequired, 17 isOpen: PropTypes.bool.isRequired,
15 portal: PropTypes.string, 18 portal: PropTypes.string,
16 close: PropTypes.func.isRequired, 19 close: PropTypes.func.isRequired,
20 shouldCloseOnOverlayClick: PropTypes.bool,
21 showClose: PropTypes.bool,
17 } 22 }
18 23
19 static defaultProps = { 24 static defaultProps = {
20 className: null, 25 className: null,
21 portal: 'modal-portal', 26 portal: 'modal-portal',
27 shouldCloseOnOverlayClick: false,
28 showClose: true,
22 } 29 }
23 30
24 render() { 31 render() {
@@ -29,6 +36,8 @@ export default @injectCSS(styles) class Modal extends Component {
29 isOpen, 36 isOpen,
30 portal, 37 portal,
31 close, 38 close,
39 shouldCloseOnOverlayClick,
40 showClose,
32 } = this.props; 41 } = this.props;
33 42
34 return ( 43 return (
@@ -42,14 +51,17 @@ export default @injectCSS(styles) class Modal extends Component {
42 overlayClassName={classes.overlay} 51 overlayClassName={classes.overlay}
43 portal={portal} 52 portal={portal}
44 onRequestClose={close} 53 onRequestClose={close}
54 shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
45 > 55 >
46 {/* <button 56 {showClose && close && (
47 type="button" 57 <button
48 className={classnames({ 58 type="button"
49 [`${classes.close}`]: true, 59 className={classes.close}
50 'mdi mdi-close': true, 60 onClick={close}
51 })} 61 >
52 /> */} 62 <Icon icon="mdiClose" size={1.5} />
63 </button>
64 )}
53 <div className={classes.content}> 65 <div className={classes.content}>
54 {children} 66 {children}
55 </div> 67 </div>
diff --git a/src/components/ui/Modal/styles.js b/src/components/ui/Modal/styles.js
index 56fecbf55..49b970c97 100644
--- a/src/components/ui/Modal/styles.js
+++ b/src/components/ui/Modal/styles.js
@@ -28,5 +28,6 @@ export default theme => ({
28 position: 'absolute', 28 position: 'absolute',
29 top: 0, 29 top: 0,
30 right: 0, 30 right: 0,
31 padding: 20,
31 }, 32 },
32}); 33});
diff --git a/src/config.js b/src/config.js
index b11952520..30a5a5cc0 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,13 +1,14 @@
1import electron from 'electron'; 1import electron from 'electron';
2import path from 'path'; 2import path from 'path';
3import isDevMode from 'electron-is-dev'; 3import isDevMode from 'electron-is-dev';
4import ms from 'ms';
4 5
5import { asarPath } from './helpers/asar-helpers'; 6import { asarPath } from './helpers/asar-helpers';
6 7
7const app = process.type === 'renderer' ? electron.remote.app : electron.app; 8const app = process.type === 'renderer' ? electron.remote.app : electron.app;
8const systemPreferences = process.type === 'renderer' ? electron.remote.systemPreferences : electron.systemPreferences; 9const systemPreferences = process.type === 'renderer' ? electron.remote.systemPreferences : electron.systemPreferences;
9 10
10export const CHECK_INTERVAL = 1000 * 3600; // How often should we perform checks 11export const CHECK_INTERVAL = ms('1h'); // How often should we perform checks
11export const LOCAL_API = 'http://localhost:3000'; 12export const LOCAL_API = 'http://localhost:3000';
12export const DEV_API = 'https://dev.franzinfra.com'; 13export const DEV_API = 'https://dev.franzinfra.com';
13export const LIVE_API = 'https://api.franzinfra.com'; 14export const LIVE_API = 'https://api.franzinfra.com';
@@ -35,8 +36,8 @@ export const DEFAULT_FEATURES_CONFIG = {
35 isSpellcheckerPremiumFeature: false, 36 isSpellcheckerPremiumFeature: false,
36 needToWaitToProceed: false, 37 needToWaitToProceed: false,
37 needToWaitToProceedConfig: { 38 needToWaitToProceedConfig: {
38 delayOffset: 3600000, 39 delayOffset: ms('1h'),
39 wait: 10000, 40 wait: ms('10s'),
40 }, 41 },
41 isServiceProxyEnabled: false, 42 isServiceProxyEnabled: false,
42 isServiceProxyPremiumFeature: true, 43 isServiceProxyPremiumFeature: true,
diff --git a/src/features/basicAuth/Component.js b/src/features/basicAuth/Component.js
index 13395fb40..a8252acb7 100644
--- a/src/features/basicAuth/Component.js
+++ b/src/features/basicAuth/Component.js
@@ -62,6 +62,7 @@ export default @injectSheet(styles) @observer class BasicAuthModal extends Compo
62 isOpen={isModalVisible} 62 isOpen={isModalVisible}
63 className={classes.modal} 63 className={classes.modal}
64 close={this.cancel.bind(this)} 64 close={this.cancel.bind(this)}
65 showClose={false}
65 > 66 >
66 <h1>Sign in</h1> 67 <h1>Sign in</h1>
67 <p> 68 <p>
diff --git a/src/features/basicAuth/index.js b/src/features/basicAuth/index.js
index 00ad65ce6..89607824b 100644
--- a/src/features/basicAuth/index.js
+++ b/src/features/basicAuth/index.js
@@ -6,7 +6,7 @@ import BasicAuthComponent from './Component';
6const debug = require('debug')('Franz:feature:basicAuth'); 6const debug = require('debug')('Franz:feature:basicAuth');
7 7
8const defaultState = { 8const defaultState = {
9 isModalVisible: false, 9 isModalVisible: true,
10 service: null, 10 service: null,
11 authInfo: null, 11 authInfo: null,
12}; 12};
@@ -15,7 +15,6 @@ export const state = observable(defaultState);
15 15
16export function resetState() { 16export function resetState() {
17 Object.assign(state, defaultState); 17 Object.assign(state, defaultState);
18 console.log('reset state', state);
19} 18}
20 19
21export default function initialize() { 20export default function initialize() {
diff --git a/src/features/delayApp/index.js b/src/features/delayApp/index.js
index 48aac34b6..abc8274cf 100644
--- a/src/features/delayApp/index.js
+++ b/src/features/delayApp/index.js
@@ -28,8 +28,12 @@ export default function init(stores) {
28 let shownAfterLaunch = false; 28 let shownAfterLaunch = false;
29 let timeLastDelay = moment(); 29 let timeLastDelay = moment();
30 30
31 window.franz.features.delayApp = {
32 state,
33 };
34
31 reaction( 35 reaction(
32 () => stores.features.features.needToWaitToProceed && !stores.user.data.isPremium, 36 () => stores.user.isLoggedIn && stores.features.features.needToWaitToProceed && !stores.user.data.isPremium,
33 (isEnabled) => { 37 (isEnabled) => {
34 if (isEnabled) { 38 if (isEnabled) {
35 debug('Enabling `delayApp` feature'); 39 debug('Enabling `delayApp` feature');
diff --git a/src/features/shareFranz/Component.js b/src/features/shareFranz/Component.js
new file mode 100644
index 000000000..d463664df
--- /dev/null
+++ b/src/features/shareFranz/Component.js
@@ -0,0 +1,170 @@
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { observer, inject } from 'mobx-react';
4import injectSheet from 'react-jss';
5import { defineMessages, intlShape } from 'react-intl';
6import { Button } from '@meetfranz/forms';
7import { H1, Icon } from '@meetfranz/ui';
8
9import Modal from '../../components/ui/Modal';
10import { state } from '.';
11import { gaEvent } from '../../lib/analytics';
12import ServicesStore from '../../stores/ServicesStore';
13
14const messages = defineMessages({
15 headline: {
16 id: 'feature.shareFranz.headline',
17 defaultMessage: '!!!Franz is better together!',
18 },
19 text: {
20 id: 'feature.shareFranz.text',
21 defaultMessage: '!!!Tell your friends and colleagues how awesome Franz is and help us to spread the word.',
22 },
23 actions: {
24 email: {
25 id: 'feature.shareFranz.action.email',
26 defaultMessage: '!!!Share as email',
27 },
28 facebook: {
29 id: 'feature.shareFranz.action.facebook',
30 defaultMessage: '!!!Share on Facebook',
31 },
32 twitter: {
33 id: 'feature.shareFranz.action.twitter',
34 defaultMessage: '!!!Share on Twitter',
35 },
36 },
37 shareText: {
38 email: {
39 id: 'feature.shareFranz.shareText.email',
40 defaultMessage: '!!! I\'ve added {count} services to Franz! Get the free app for WhatsApp, Messenger, Slack, Skype and co at www.meetfranz.com',
41 },
42 twitter: {
43 id: 'feature.shareFranz.shareText.twitter',
44 defaultMessage: '!!! I\'ve added {count} services to Franz! Get the free app for WhatsApp, Messenger, Slack, Skype and co at www.meetfranz.com /cc @MeetFranz',
45 },
46 },
47});
48
49const styles = theme => ({
50 modal: {
51 width: '80%',
52 maxWidth: 600,
53 background: theme.styleTypes.primary.accent,
54 textAlign: 'center',
55 color: theme.styleTypes.primary.contrast,
56 },
57 heartContainer: {
58 display: 'flex',
59 justifyContent: 'center',
60 borderRadius: '100%',
61 background: theme.brandDanger,
62 padding: 20,
63 width: 100,
64 height: 100,
65 margin: [-70, 'auto', 30],
66 },
67 heart: {
68 fill: theme.styleTypes.primary.contrast,
69 },
70 headline: {
71 textAlign: 'center',
72 fontSize: 40,
73 marginBottom: 20,
74 },
75 actions: {
76 display: 'flex',
77 justifyContent: 'space-between',
78 marginTop: 30,
79 },
80 cta: {
81 background: theme.styleTypes.primary.contrast,
82 color: theme.styleTypes.primary.accent,
83
84 '& svg': {
85 fill: theme.styleTypes.primary.accent,
86 },
87 },
88});
89
90export default @injectSheet(styles) @inject('stores') @observer class ShareFranzModal extends Component {
91 static propTypes = {
92 classes: PropTypes.object.isRequired,
93 }
94
95 static contextTypes = {
96 intl: intlShape,
97 };
98
99 close() {
100 state.isModalVisible = false;
101 }
102
103 render() {
104 const { isModalVisible } = state;
105
106 const {
107 classes,
108 stores,
109 } = this.props;
110
111 const serviceCount = stores.services.all.length;
112
113 const { intl } = this.context;
114
115 return (
116 <Modal
117 isOpen={isModalVisible}
118 className={classes.modal}
119 shouldCloseOnOverlayClick
120 close={this.close.bind(this)}
121 >
122 <div className={classes.heartContainer}>
123 <Icon icon="mdiHeart" className={classes.heart} size={4} />
124 </div>
125 <H1 className={classes.headline}>
126 {intl.formatMessage(messages.headline)}
127 </H1>
128 <p>{intl.formatMessage(messages.text)}</p>
129 <div className={classes.actions}>
130 <Button
131 label={intl.formatMessage(messages.actions.email)}
132 className={classes.cta}
133 icon="mdiEmail"
134 href={`mailto:?subject=Meet the cool app Franz&body=${intl.formatMessage(messages.shareText.email, { count: serviceCount })}}`}
135 target="_blank"
136 onClick={() => {
137 gaEvent('Share Franz', 'share', 'Share via email');
138 }}
139 />
140 <Button
141 label={intl.formatMessage(messages.actions.facebook)}
142 className={classes.cta}
143 icon="mdiFacebookBox"
144 href="https://www.facebook.com/sharer/sharer.php?u=https://www.meetfranz.com?utm_source=facebook&utm_medium=referral&utm_campaign=share-button"
145 target="_blank"
146 onClick={() => {
147 gaEvent('Share Franz', 'share', 'Share via Facebook');
148 }}
149 />
150 <Button
151 label={intl.formatMessage(messages.actions.twitter)}
152 className={classes.cta}
153 icon="mdiTwitter"
154 href={`http://twitter.com/intent/tweet?status=${intl.formatMessage(messages.shareText.twitter, { count: serviceCount })}`}
155 target="_blank"
156 onClick={() => {
157 gaEvent('Share Franz', 'share', 'Share via Twitter');
158 }}
159 />
160 </div>
161 </Modal>
162 );
163 }
164}
165
166ShareFranzModal.wrappedComponent.propTypes = {
167 stores: PropTypes.shape({
168 services: PropTypes.instanceOf(ServicesStore).isRequired,
169 }).isRequired,
170};
diff --git a/src/features/shareFranz/index.js b/src/features/shareFranz/index.js
new file mode 100644
index 000000000..3a8ec95d3
--- /dev/null
+++ b/src/features/shareFranz/index.js
@@ -0,0 +1,52 @@
1import { observable, reaction } from 'mobx';
2import ms from 'ms';
3
4import { state as delayAppState } from '../delayApp';
5import { gaEvent, gaPage } from '../../lib/analytics';
6
7export { default as Component } from './Component';
8
9const debug = require('debug')('Franz:feature:shareFranz');
10
11const defaultState = {
12 isModalVisible: false,
13 lastShown: null,
14};
15
16export const state = observable(defaultState);
17
18export default function initialize(stores) {
19 debug('Initialize shareFranz feature');
20
21 window.franz.features.shareFranz = {
22 state,
23 };
24
25 function showModal() {
26 debug('Showing share window');
27
28 state.isModalVisible = true;
29
30 gaEvent('Share Franz', 'show');
31 gaPage('/share-modal');
32 }
33
34 reaction(
35 () => stores.user.isLoggedIn,
36 () => {
37 setTimeout(() => {
38 if (stores.settings.stats.appStarts % 30 === 0) {
39 if (delayAppState.isDelayAppScreenVisible) {
40 debug('Delaying share modal by 5 minutes');
41 setTimeout(() => showModal(), ms('5m'));
42 } else {
43 showModal();
44 }
45 }
46 }, ms('2s'));
47 },
48 {
49 fireImmediately: true,
50 },
51 );
52}
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index cd5c417e3..988ac46f2 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -286,5 +286,12 @@
286 "feature.delayApp.text": "Franz will continue in {seconds} seconds.", 286 "feature.delayApp.text": "Franz will continue in {seconds} seconds.",
287 "premiumFeature.button.upgradeAccount": "Upgrade account", 287 "premiumFeature.button.upgradeAccount": "Upgrade account",
288 "app.errorHandler.headline": "Something went wrong", 288 "app.errorHandler.headline": "Something went wrong",
289 "app.errorHandler.action": "Reload" 289 "app.errorHandler.action": "Reload",
290 "feature.shareFranz.headline": "Franz is better together!",
291 "feature.shareFranz.text": "Tell your friends and colleagues how awesome Franz is and help us to spread the word.",
292 "feature.shareFranz.action.email": "Send as email",
293 "feature.shareFranz.action.facebook": "Share on Facebook",
294 "feature.shareFranz.action.twitter": "Share on Twitter",
295 "feature.shareFranz.shareText.email": "I've added {count} services to Franz! Get the free app for WhatsApp, Messenger, Slack, Skype and co at www.meetfranz.com",
296 "feature.shareFranz.shareText.twitter": "I've added {count} services to Franz! Get the free app for WhatsApp, Messenger, Slack, Skype and co at www.meetfranz.com /cc @MeetFranz"
290} 297}
diff --git a/src/lib/analytics.js b/src/lib/analytics.js
index 7044e5bb7..0519192d1 100644
--- a/src/lib/analytics.js
+++ b/src/lib/analytics.js
@@ -37,7 +37,3 @@ export function gaEvent(category, action, label) {
37 37
38 debug('GA track event', category, action); 38 debug('GA track event', category, action);
39} 39}
40
41setTimeout(() => {
42 ga('send', 'Ping');
43}, 1000 * 60 * 10); // Ping GA every 10 Minutes
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index d90f32744..168aa7e48 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -7,6 +7,7 @@ import key from 'keymaster';
7import { getDoNotDisturb } from '@meetfranz/electron-notification-state'; 7import { getDoNotDisturb } from '@meetfranz/electron-notification-state';
8import AutoLaunch from 'auto-launch'; 8import AutoLaunch from 'auto-launch';
9import prettyBytes from 'pretty-bytes'; 9import prettyBytes from 'pretty-bytes';
10import ms from 'ms';
10 11
11import Store from './lib/Store'; 12import Store from './lib/Store';
12import Request from './lib/Request'; 13import Request from './lib/Request';
@@ -112,12 +113,12 @@ export default class AppStore extends Store {
112 // Check if system is muted 113 // Check if system is muted
113 // There are no events to subscribe so we need to poll everey 5s 114 // There are no events to subscribe so we need to poll everey 5s
114 this._systemDND(); 115 this._systemDND();
115 setInterval(() => this._systemDND(), 5000); 116 setInterval(() => this._systemDND(), ms('5s'));
116 117
117 // Check for updates once every 4 hours 118 // Check for updates once every 4 hours
118 setInterval(() => this._checkForUpdates(), CHECK_INTERVAL); 119 setInterval(() => this._checkForUpdates(), CHECK_INTERVAL);
119 // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues) 120 // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues)
120 setTimeout(() => this._checkForUpdates(), 30000); 121 setTimeout(() => this._checkForUpdates(), ms('30s'));
121 ipcRenderer.on('autoUpdate', (event, data) => { 122 ipcRenderer.on('autoUpdate', (event, data) => {
122 if (data.available) { 123 if (data.available) {
123 this.updateStatus = this.updateStatusTypes.AVAILABLE; 124 this.updateStatus = this.updateStatusTypes.AVAILABLE;
@@ -316,7 +317,7 @@ export default class AppStore extends Store {
316 } else { 317 } else {
317 const deltaTime = moment().diff(this.timeOfflineStart); 318 const deltaTime = moment().diff(this.timeOfflineStart);
318 319
319 if (deltaTime > 30 * 60 * 1000) { 320 if (deltaTime > ms('30m')) {
320 this.actions.service.reloadAll(); 321 this.actions.service.reloadAll();
321 } 322 }
322 } 323 }
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js
index 05a620f0b..b7130904b 100644
--- a/src/stores/FeaturesStore.js
+++ b/src/stores/FeaturesStore.js
@@ -8,6 +8,7 @@ import spellchecker from '../features/spellchecker';
8import serviceProxy from '../features/serviceProxy'; 8import serviceProxy from '../features/serviceProxy';
9import basicAuth from '../features/basicAuth'; 9import basicAuth from '../features/basicAuth';
10import workspaces from '../features/workspaces'; 10import workspaces from '../features/workspaces';
11import shareFranz from '../features/shareFranz';
11 12
12import { DEFAULT_FEATURES_CONFIG } from '../config'; 13import { DEFAULT_FEATURES_CONFIG } from '../config';
13 14
@@ -58,5 +59,6 @@ export default class FeaturesStore extends Store {
58 serviceProxy(this.stores, this.actions); 59 serviceProxy(this.stores, this.actions);
59 basicAuth(this.stores, this.actions); 60 basicAuth(this.stores, this.actions);
60 workspaces(this.stores, this.actions); 61 workspaces(this.stores, this.actions);
62 shareFranz(this.stores, this.actions);
61 } 63 }
62} 64}
diff --git a/src/stores/RecipePreviewsStore.js b/src/stores/RecipePreviewsStore.js
index 10b2928e3..382820d58 100644
--- a/src/stores/RecipePreviewsStore.js
+++ b/src/stores/RecipePreviewsStore.js
@@ -1,5 +1,6 @@
1import { action, computed, observable } from 'mobx'; 1import { action, computed, observable } from 'mobx';
2import { debounce } from 'lodash'; 2import { debounce } from 'lodash';
3import ms from 'ms';
3 4
4import Store from './lib/Store'; 5import Store from './lib/Store';
5import CachedRequest from './lib/CachedRequest'; 6import CachedRequest from './lib/CachedRequest';
@@ -48,5 +49,5 @@ export default class RecipePreviewsStore extends Store {
48 // Helper 49 // Helper
49 _analyticsSearch = debounce((needle) => { 50 _analyticsSearch = debounce((needle) => {
50 gaEvent('Recipe', 'search', needle); 51 gaEvent('Recipe', 'search', needle);
51 }, 3000); 52 }, ms('3s'));
52} 53}
diff --git a/src/stores/RequestStore.js b/src/stores/RequestStore.js
index 2629e0a38..9254e3223 100644
--- a/src/stores/RequestStore.js
+++ b/src/stores/RequestStore.js
@@ -1,4 +1,5 @@
1import { action, computed, observable } from 'mobx'; 1import { action, computed, observable } from 'mobx';
2import ms from 'ms';
2 3
3import Store from './lib/Store'; 4import Store from './lib/Store';
4 5
@@ -13,7 +14,7 @@ export default class RequestStore extends Store {
13 14
14 retries = 0; 15 retries = 0;
15 16
16 retryDelay = 2000; 17 retryDelay = ms('2s');
17 18
18 constructor(...args) { 19 constructor(...args) {
19 super(...args); 20 super(...args);
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index efd57a09d..c63bef196 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -5,6 +5,7 @@ import {
5 observable, 5 observable,
6} from 'mobx'; 6} from 'mobx';
7import { debounce, remove } from 'lodash'; 7import { debounce, remove } from 'lodash';
8import ms from 'ms';
8 9
9import Store from './lib/Store'; 10import Store from './lib/Store';
10import Request from './lib/Request'; 11import Request from './lib/Request';
@@ -679,7 +680,7 @@ export default class ServicesStore extends Store {
679 _initRecipePolling(serviceId) { 680 _initRecipePolling(serviceId) {
680 const service = this.one(serviceId); 681 const service = this.one(serviceId);
681 682
682 const delay = 2000; 683 const delay = ms('2s');
683 684
684 if (service) { 685 if (service) {
685 if (service.timer !== null) { 686 if (service.timer !== null) {
@@ -700,7 +701,7 @@ export default class ServicesStore extends Store {
700 701
701 _reorderAnalytics = debounce(() => { 702 _reorderAnalytics = debounce(() => {
702 gaEvent('Service', 'order'); 703 gaEvent('Service', 'order');
703 }, 5000); 704 }, ms('5s'));
704 705
705 _wrapIndex(index, delta, size) { 706 _wrapIndex(index, delta, size) {
706 return (((index + delta) % size) + size) % size; 707 return (((index + delta) % size) + size) % size;