aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Stefan Malzner <stefan@adlk.io>2018-11-28 20:51:17 +0100
committerLibravatar Stefan Malzner <stefan@adlk.io>2018-11-28 20:51:17 +0100
commit8b648553a115ed753f78bb81af2752eadaf47754 (patch)
treeb56cfd517a836dffa6b738896df125940f9936b8
parentfix(App): App menu was not initialized on app launch. Resolving copy & paste ... (diff)
parentFix linting issues (diff)
downloadferdium-app-8b648553a115ed753f78bb81af2752eadaf47754.tar.gz
ferdium-app-8b648553a115ed753f78bb81af2752eadaf47754.tar.zst
ferdium-app-8b648553a115ed753f78bb81af2752eadaf47754.zip
Merge branch 'update/monetization' into develop
-rw-r--r--.eslintrc1
-rw-r--r--src/api/LocalApi.js8
-rw-r--r--src/api/server/LocalApi.js19
-rw-r--r--src/components/layout/AppLayout.js2
-rw-r--r--src/components/settings/account/AccountDashboard.js8
-rw-r--r--src/components/settings/navigation/SettingsNavigation.js25
-rw-r--r--src/components/settings/services/EditServiceForm.js45
-rw-r--r--src/components/settings/settings/EditSettingsForm.js12
-rw-r--r--src/components/subscription/SubscriptionForm.js32
-rw-r--r--src/components/ui/PremiumFeatureContainer/index.js76
-rw-r--r--src/components/ui/PremiumFeatureContainer/styles.js31
-rw-r--r--src/components/ui/Toggle.js6
-rw-r--r--src/config.js8
-rw-r--r--src/containers/settings/EditServiceScreen.js71
-rw-r--r--src/containers/settings/EditSettingsScreen.js6
-rw-r--r--src/containers/subscription/SubscriptionFormScreen.js1
-rw-r--r--src/electron/Settings.js24
-rw-r--r--src/electron/ipc-api/appIndicator.js2
-rw-r--r--src/electron/ipc-api/settings.js10
-rw-r--r--src/features/delayApp/Component.js8
-rw-r--r--src/features/delayApp/index.js6
-rw-r--r--src/features/delayApp/styles.js49
-rw-r--r--src/features/serviceProxy/index.js56
-rw-r--r--src/features/spellchecker/index.js38
-rw-r--r--src/features/spellchecker/styles.js26
-rw-r--r--src/i18n/locales/en-US.json13
-rw-r--r--src/index.js34
-rw-r--r--src/models/Service.js2
-rw-r--r--src/models/Settings.js33
-rw-r--r--src/stores/FeaturesStore.js4
-rw-r--r--src/stores/ServicesStore.js15
-rw-r--r--src/stores/SettingsStore.js82
-rw-r--r--src/styles/settings.scss5
-rw-r--r--src/theme/dark/index.js1
-rw-r--r--src/theme/default/index.js23
-rw-r--r--src/webview/plugin.js8
36 files changed, 611 insertions, 179 deletions
diff --git a/.eslintrc b/.eslintrc
index 8e95ad4fd..948550306 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -13,6 +13,7 @@
13 "react/jsx-filename-extension": [1, { 13 "react/jsx-filename-extension": [1, {
14 "extensions": [".js", ".jsx"] 14 "extensions": [".js", ".jsx"]
15 }], 15 }],
16 "react/forbid-prop-types": 1,
16 "no-underscore-dangle": 0, 17 "no-underscore-dangle": 0,
17 "max-len": 0, 18 "max-len": 0,
18 "class-methods-use-this": 0, 19 "class-methods-use-this": 0,
diff --git a/src/api/LocalApi.js b/src/api/LocalApi.js
index 741917104..e2a46874a 100644
--- a/src/api/LocalApi.js
+++ b/src/api/LocalApi.js
@@ -4,12 +4,12 @@ export default class LocalApi {
4 this.local = local; 4 this.local = local;
5 } 5 }
6 6
7 getAppSettings() { 7 getAppSettings(type) {
8 return this.local.getAppSettings(); 8 return this.local.getAppSettings(type);
9 } 9 }
10 10
11 updateAppSettings(data) { 11 updateAppSettings(type, data) {
12 return this.local.updateAppSettings(data); 12 return this.local.updateAppSettings(type, data);
13 } 13 }
14 14
15 getAppCacheSize() { 15 getAppCacheSize() {
diff --git a/src/api/server/LocalApi.js b/src/api/server/LocalApi.js
index 4814bba66..ab1604a27 100644
--- a/src/api/server/LocalApi.js
+++ b/src/api/server/LocalApi.js
@@ -9,20 +9,23 @@ const { session } = remote;
9 9
10export default class LocalApi { 10export default class LocalApi {
11 // Settings 11 // Settings
12 getAppSettings() { 12 getAppSettings(type) {
13 return new Promise((resolve) => { 13 return new Promise((resolve) => {
14 ipcRenderer.once('appSettings', (event, data) => { 14 ipcRenderer.once('appSettings', (event, resp) => {
15 debug('LocalApi::getAppSettings resolves', data); 15 debug('LocalApi::getAppSettings resolves', resp.type, resp.data);
16 resolve(data); 16 resolve(resp);
17 }); 17 });
18 18
19 ipcRenderer.send('getAppSettings'); 19 ipcRenderer.send('getAppSettings', type);
20 }); 20 });
21 } 21 }
22 22
23 async updateAppSettings(data) { 23 async updateAppSettings(type, data) {
24 debug('LocalApi::updateAppSettings resolves', data); 24 debug('LocalApi::updateAppSettings resolves', type, data);
25 ipcRenderer.send('updateAppSettings', data); 25 ipcRenderer.send('updateAppSettings', {
26 type,
27 data,
28 });
26 } 29 }
27 30
28 // Services 31 // Services
diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js
index 4b20fc480..3ababe54a 100644
--- a/src/components/layout/AppLayout.js
+++ b/src/components/layout/AppLayout.js
@@ -5,7 +5,7 @@ import { defineMessages, intlShape } from 'react-intl';
5import { TitleBar } from 'electron-react-titlebar'; 5import { TitleBar } from 'electron-react-titlebar';
6 6
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 globalMessages from '../../i18n/globalMessages'; 9import globalMessages from '../../i18n/globalMessages';
10 10
11import { isWindows } from '../../environment'; 11import { isWindows } from '../../environment';
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index ede519fd6..06c7074dd 100644
--- a/src/components/settings/account/AccountDashboard.js
+++ b/src/components/settings/account/AccountDashboard.js
@@ -180,11 +180,9 @@ export default @observer class AccountDashboard extends Component {
180 <span className="badge badge--success">{intl.formatMessage(messages.accountTypeEnterprise)}</span> 180 <span className="badge badge--success">{intl.formatMessage(messages.accountTypeEnterprise)}</span>
181 )} 181 )}
182 </div> 182 </div>
183 {!user.isSSO && ( 183 <Link to="/settings/user/edit" className="button">
184 <Link to="/settings/user/edit" className="button"> 184 {intl.formatMessage(messages.accountEditButton)}
185 {intl.formatMessage(messages.accountEditButton)} 185 </Link>
186 </Link>
187 )}
188 {user.emailValidated} 186 {user.emailValidated}
189 </div> 187 </div>
190 </div> 188 </div>
diff --git a/src/components/settings/navigation/SettingsNavigation.js b/src/components/settings/navigation/SettingsNavigation.js
index d8b410aaf..b86d94ac7 100644
--- a/src/components/settings/navigation/SettingsNavigation.js
+++ b/src/components/settings/navigation/SettingsNavigation.js
@@ -43,20 +43,17 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
43 43
44 render() { 44 render() {
45 const { serviceCount } = this.props; 45 const { serviceCount } = this.props;
46 const { features } = this.props.stores.features;
47 const { intl } = this.context; 46 const { intl } = this.context;
48 47
49 return ( 48 return (
50 <div className="settings-navigation"> 49 <div className="settings-navigation">
51 {features.userCanManageServices && ( 50 <Link
52 <Link 51 to="/settings/recipes"
53 to="/settings/recipes" 52 className="settings-navigation__link"
54 className="settings-navigation__link" 53 activeClassName="is-active"
55 activeClassName="is-active" 54 >
56 > 55 {intl.formatMessage(messages.availableServices)}
57 {intl.formatMessage(messages.availableServices)} 56 </Link>
58 </Link>
59 )}
60 <Link 57 <Link
61 to="/settings/services" 58 to="/settings/services"
62 className="settings-navigation__link" 59 className="settings-navigation__link"
@@ -97,11 +94,3 @@ export default @inject('stores') @observer class SettingsNavigation extends Comp
97 } 94 }
98} 95}
99 96
100SettingsNavigation.wrappedComponent.propTypes = {
101 stores: PropTypes.shape({
102 features: PropTypes.shape({
103 features: PropTypes.object.isRequired,
104 }).isRequired,
105 }).isRequired,
106};
107
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index 777a95fcf..47772efae 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -15,6 +15,8 @@ import Toggle from '../../ui/Toggle';
15import Button from '../../ui/Button'; 15import Button from '../../ui/Button';
16import ImageUpload from '../../ui/ImageUpload'; 16import ImageUpload from '../../ui/ImageUpload';
17 17
18import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
19
18const messages = defineMessages({ 20const messages = defineMessages({
19 saveService: { 21 saveService: {
20 id: 'settings.service.form.saveButton', 22 id: 'settings.service.form.saveButton',
@@ -92,6 +94,14 @@ const messages = defineMessages({
92 id: 'settings.service.form.iconUpload', 94 id: 'settings.service.form.iconUpload',
93 defaultMessage: '!!!Drop your image, or click here', 95 defaultMessage: '!!!Drop your image, or click here',
94 }, 96 },
97 headlineProxy: {
98 id: 'settings.service.form.proxy.headline',
99 defaultMessage: '!!!Proxy Settings',
100 },
101 proxyInfo: {
102 id: 'settings.service.form.proxy.info',
103 defaultMessage: '!!!Proxy settings will not be synchronized with the Franz servers.',
104 },
95}); 105});
96 106
97export default @observer class EditServiceForm extends Component { 107export default @observer class EditServiceForm extends Component {
@@ -106,13 +116,14 @@ export default @observer class EditServiceForm extends Component {
106 return null; 116 return null;
107 }, 117 },
108 user: PropTypes.instanceOf(User).isRequired, 118 user: PropTypes.instanceOf(User).isRequired,
109 userCanManageServices: PropTypes.bool.isRequired,
110 action: PropTypes.string.isRequired, 119 action: PropTypes.string.isRequired,
111 form: PropTypes.instanceOf(Form).isRequired, 120 form: PropTypes.instanceOf(Form).isRequired,
112 onSubmit: PropTypes.func.isRequired, 121 onSubmit: PropTypes.func.isRequired,
113 onDelete: PropTypes.func.isRequired, 122 onDelete: PropTypes.func.isRequired,
114 isSaving: PropTypes.bool.isRequired, 123 isSaving: PropTypes.bool.isRequired,
115 isDeleting: PropTypes.bool.isRequired, 124 isDeleting: PropTypes.bool.isRequired,
125 isProxyFeatureEnabled: PropTypes.bool.isRequired,
126 isProxyFeaturePremiumFeature: PropTypes.bool.isRequired,
116 }; 127 };
117 128
118 static defaultProps = { 129 static defaultProps = {
@@ -169,11 +180,12 @@ export default @observer class EditServiceForm extends Component {
169 service, 180 service,
170 action, 181 action,
171 user, 182 user,
172 userCanManageServices,
173 form, 183 form,
174 isSaving, 184 isSaving,
175 isDeleting, 185 isDeleting,
176 onDelete, 186 onDelete,
187 isProxyFeatureEnabled,
188 isProxyFeaturePremiumFeature,
177 } = this.props; 189 } = this.props;
178 const { intl } = this.context; 190 const { intl } = this.context;
179 191
@@ -318,6 +330,33 @@ export default @observer class EditServiceForm extends Component {
318 /> 330 />
319 </div> 331 </div>
320 </div> 332 </div>
333
334 {isProxyFeatureEnabled && (
335 <PremiumFeatureContainer condition={isProxyFeaturePremiumFeature}>
336 <div className="settings__settings-group">
337 <h3>
338 {intl.formatMessage(messages.headlineProxy)}
339 <span className="badge badge--success">beta</span>
340 </h3>
341 <Toggle field={form.$('proxy.isEnabled')} />
342 {form.$('proxy.isEnabled').value && (
343 <div>
344 <Input field={form.$('proxy.host')} />
345 <Input field={form.$('proxy.user')} />
346 <Input
347 field={form.$('proxy.password')}
348 showPasswordToggle
349 />
350 <p>
351 <span className="mdi mdi-information" />
352 {intl.formatMessage(messages.proxyInfo)}
353 </p>
354 </div>
355 )}
356 </div>
357 </PremiumFeatureContainer>
358 )}
359
321 {recipe.message && ( 360 {recipe.message && (
322 <p className="settings__message"> 361 <p className="settings__message">
323 <span className="mdi mdi-information" /> 362 <span className="mdi mdi-information" />
@@ -328,7 +367,7 @@ export default @observer class EditServiceForm extends Component {
328 </div> 367 </div>
329 <div className="settings__controls"> 368 <div className="settings__controls">
330 {/* Delete Button */} 369 {/* Delete Button */}
331 {action === 'edit' && userCanManageServices && deleteButton} 370 {action === 'edit' && deleteButton}
332 371
333 {/* Save Button */} 372 {/* Save Button */}
334 {isSaving || isValidatingCustomUrl ? ( 373 {isSaving || isValidatingCustomUrl ? (
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index b5c048ebd..280449ead 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -8,6 +8,7 @@ import Form from '../../../lib/Form';
8import Button from '../../ui/Button'; 8import Button from '../../ui/Button';
9import Toggle from '../../ui/Toggle'; 9import Toggle from '../../ui/Toggle';
10import Select from '../../ui/Select'; 10import Select from '../../ui/Select';
11import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
11 12
12import { FRANZ_TRANSLATION } from '../../../config'; 13import { FRANZ_TRANSLATION } from '../../../config';
13 14
@@ -95,6 +96,7 @@ export default @observer class EditSettingsForm extends Component {
95 isClearingAllCache: PropTypes.bool.isRequired, 96 isClearingAllCache: PropTypes.bool.isRequired,
96 onClearAllCache: PropTypes.func.isRequired, 97 onClearAllCache: PropTypes.func.isRequired,
97 cacheSize: PropTypes.string.isRequired, 98 cacheSize: PropTypes.string.isRequired,
99 isSpellcheckerPremiumFeature: PropTypes.bool.isRequired,
98 }; 100 };
99 101
100 static contextTypes = { 102 static contextTypes = {
@@ -124,6 +126,7 @@ export default @observer class EditSettingsForm extends Component {
124 isClearingAllCache, 126 isClearingAllCache,
125 onClearAllCache, 127 onClearAllCache,
126 cacheSize, 128 cacheSize,
129 isSpellcheckerPremiumFeature,
127 } = this.props; 130 } = this.props;
128 const { intl } = this.context; 131 const { intl } = this.context;
129 132
@@ -175,7 +178,14 @@ export default @observer class EditSettingsForm extends Component {
175 178
176 {/* Advanced */} 179 {/* Advanced */}
177 <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2> 180 <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2>
178 <Toggle field={form.$('enableSpellchecking')} /> 181 <PremiumFeatureContainer
182 condition={isSpellcheckerPremiumFeature}
183 >
184 <Toggle
185 field={form.$('enableSpellchecking')}
186 disabled
187 />
188 </PremiumFeatureContainer>
179 <Toggle field={form.$('enableGPUAcceleration')} /> 189 <Toggle field={form.$('enableGPUAcceleration')} />
180 <p className="settings__help">{intl.formatMessage(messages.enableGPUAccelerationInfo)}</p> 190 <p className="settings__help">{intl.formatMessage(messages.enableGPUAccelerationInfo)}</p>
181 {/* <Select field={form.$('spellcheckingLanguage')} /> */} 191 {/* <Select field={form.$('spellcheckingLanguage')} /> */}
diff --git a/src/components/subscription/SubscriptionForm.js b/src/components/subscription/SubscriptionForm.js
index 5992e4204..12965e307 100644
--- a/src/components/subscription/SubscriptionForm.js
+++ b/src/components/subscription/SubscriptionForm.js
@@ -36,26 +36,22 @@ const messages = defineMessages({
36 defaultMessage: '!!!The Franz Premium Supporter Account includes', 36 defaultMessage: '!!!The Franz Premium Supporter Account includes',
37 }, 37 },
38 features: { 38 features: {
39 unlimitedServices: {
40 id: 'subscription.features.unlimitedServices',
41 defaultMessage: '!!!Add unlimited services',
42 },
43 onpremise: { 39 onpremise: {
44 id: 'subscription.features.onpremise', 40 id: 'subscription.features.onpremise.mattermost',
45 defaultMessage: '!!!Add on-premise/hosted services like HipChat', 41 defaultMessage: '!!!Add on-premise/hosted services like Mattermost',
46 },
47 customServices: {
48 id: 'subscription.features.customServices',
49 defaultMessage: '!!!Add your custom services',
50 }, 42 },
51 encryptedSync: { 43 noInterruptions: {
52 id: 'subscription.features.encryptedSync', 44 id: 'subscription.features.noInterruptions',
53 defaultMessage: '!!!Encrypted session synchronization', 45 defaultMessage: '!!!No app delays & nagging to upgrade license',
54 }, 46 },
55 vpn: { 47 vpn: {
56 id: 'subscription.features.vpn', 48 id: 'subscription.features.vpn',
57 defaultMessage: '!!!Proxy & VPN support', 49 defaultMessage: '!!!Proxy & VPN support',
58 }, 50 },
51 spellchecker: {
52 id: 'subscription.features.spellchecker',
53 defaultMessage: '!!!Support for Spellchecker',
54 },
59 ads: { 55 ads: {
60 id: 'subscription.features.ads', 56 id: 'subscription.features.ads',
61 defaultMessage: '!!!No ads, ever!', 57 defaultMessage: '!!!No ads, ever!',
@@ -170,16 +166,10 @@ export default @observer class SubscriptionForm extends Component {
170 <ul className="subscription__premium-features"> 166 <ul className="subscription__premium-features">
171 <li>{intl.formatMessage(messages.features.onpremise)}</li> 167 <li>{intl.formatMessage(messages.features.onpremise)}</li>
172 <li> 168 <li>
173 {intl.formatMessage(messages.features.encryptedSync)} 169 {intl.formatMessage(messages.features.noInterruptions)}
174 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span>
175 </li>
176 <li>
177 {intl.formatMessage(messages.features.customServices)}
178 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span>
179 </li> 170 </li>
180 <li> 171 <li>
181 {intl.formatMessage(messages.features.vpn)} 172 {intl.formatMessage(messages.features.spellchecker)}
182 <span className="badge">{intl.formatMessage(messages.features.comingSoon)}</span>
183 </li> 173 </li>
184 <li> 174 <li>
185 {intl.formatMessage(messages.features.ads)} 175 {intl.formatMessage(messages.features.ads)}
diff --git a/src/components/ui/PremiumFeatureContainer/index.js b/src/components/ui/PremiumFeatureContainer/index.js
new file mode 100644
index 000000000..73984be94
--- /dev/null
+++ b/src/components/ui/PremiumFeatureContainer/index.js
@@ -0,0 +1,76 @@
1import React, { Component } from 'react';
2import { inject, observer } from 'mobx-react';
3import PropTypes from 'prop-types';
4import { defineMessages, intlShape } from 'react-intl';
5import injectSheet from 'react-jss';
6
7import { oneOrManyChildElements } from '../../../prop-types';
8
9import UserStore from '../../../stores/UserStore';
10
11import styles from './styles';
12
13const messages = defineMessages({
14 action: {
15 id: 'premiumFeature.button.upgradeAccount',
16 defaultMessage: '!!!Upgrade account',
17 },
18});
19
20export default @inject('stores', 'actions') @injectSheet(styles) @observer class PremiumFeatureContainer extends Component {
21 static propTypes = {
22 classes: PropTypes.object.isRequired,
23 condition: PropTypes.bool,
24 };
25
26 static defaultProps = {
27 condition: true,
28 };
29
30 static contextTypes = {
31 intl: intlShape,
32 };
33
34 render() {
35 const {
36 classes,
37 children,
38 actions,
39 condition,
40 stores,
41 } = this.props;
42
43 const { intl } = this.context;
44
45 return !stores.user.data.isPremium && !!condition ? (
46 <div className={classes.container}>
47 <div className={classes.titleContainer}>
48 <p className={classes.title}>Premium Feature</p>
49 <button
50 className={classes.actionButton}
51 type="button"
52 onClick={() => actions.ui.openSettings({ path: 'user' })}
53 >
54 {intl.formatMessage(messages.action)}
55 </button>
56 </div>
57 <div className={classes.content}>
58 {children}
59 </div>
60 </div>
61 ) : children;
62 }
63}
64
65PremiumFeatureContainer.wrappedComponent.propTypes = {
66 children: oneOrManyChildElements.isRequired,
67 stores: PropTypes.shape({
68 user: PropTypes.instanceOf(UserStore).isRequired,
69 }).isRequired,
70 actions: PropTypes.shape({
71 ui: PropTypes.shape({
72 openSettings: PropTypes.func.isRequired,
73 }).isRequired,
74 }).isRequired,
75};
76
diff --git a/src/components/ui/PremiumFeatureContainer/styles.js b/src/components/ui/PremiumFeatureContainer/styles.js
new file mode 100644
index 000000000..16c40d0ec
--- /dev/null
+++ b/src/components/ui/PremiumFeatureContainer/styles.js
@@ -0,0 +1,31 @@
1export default theme => ({
2 container: {
3 background: theme.colorSubscriptionContainerBackground,
4 border: theme.colorSubscriptionContainerBorder,
5 margin: [0, 0, 20, -20],
6 padding: 20,
7 'border-radius': theme.borderRadius,
8 },
9 titleContainer: {
10 display: 'flex',
11 },
12 title: {
13 'font-weight': 'bold',
14 color: theme.colorSubscriptionContainerTitle,
15 },
16 actionButton: {
17 background: theme.colorSubscriptionContainerActionButtonBackground,
18 color: theme.colorSubscriptionContainerActionButtonColor,
19 'margin-left': 'auto',
20 'border-radius': theme.borderRadiusSmall,
21 padding: [2, 4],
22 'font-size': 12,
23 },
24 content: {
25 opacity: 0.5,
26 'margin-top': 20,
27 '& :last-child': {
28 'margin-bottom': 0,
29 },
30 },
31});
diff --git a/src/components/ui/Toggle.js b/src/components/ui/Toggle.js
index f7c2ec955..78fb77cbe 100644
--- a/src/components/ui/Toggle.js
+++ b/src/components/ui/Toggle.js
@@ -9,11 +9,13 @@ export default @observer class Toggle extends Component {
9 field: PropTypes.instanceOf(Field).isRequired, 9 field: PropTypes.instanceOf(Field).isRequired,
10 className: PropTypes.string, 10 className: PropTypes.string,
11 showLabel: PropTypes.bool, 11 showLabel: PropTypes.bool,
12 disabled: PropTypes.bool,
12 }; 13 };
13 14
14 static defaultProps = { 15 static defaultProps = {
15 className: '', 16 className: '',
16 showLabel: true, 17 showLabel: true,
18 disabled: false,
17 }; 19 };
18 20
19 onChange(e) { 21 onChange(e) {
@@ -27,6 +29,7 @@ export default @observer class Toggle extends Component {
27 field, 29 field,
28 className, 30 className,
29 showLabel, 31 showLabel,
32 disabled,
30 } = this.props; 33 } = this.props;
31 34
32 if (field.value === '' && field.default !== '') { 35 if (field.value === '' && field.default !== '') {
@@ -38,6 +41,7 @@ export default @observer class Toggle extends Component {
38 className={classnames([ 41 className={classnames([
39 'franz-form__field', 42 'franz-form__field',
40 'franz-form__toggle-wrapper', 43 'franz-form__toggle-wrapper',
44 'franz-form__toggle-disabled',
41 className, 45 className,
42 ])} 46 ])}
43 > 47 >
@@ -55,7 +59,7 @@ export default @observer class Toggle extends Component {
55 name={field.name} 59 name={field.name}
56 value={field.name} 60 value={field.name}
57 checked={field.value} 61 checked={field.value}
58 onChange={e => this.onChange(e)} 62 onChange={e => (!disabled ? this.onChange(e) : null)}
59 /> 63 />
60 </label> 64 </label>
61 {field.error && <div className={field.error}>{field.error}</div>} 65 {field.error && <div className={field.error}>{field.error}</div>}
diff --git a/src/config.js b/src/config.js
index ce946f00a..b5702a202 100644
--- a/src/config.js
+++ b/src/config.js
@@ -23,9 +23,15 @@ export const DEFAULT_APP_SETTINGS = {
23 beta: false, 23 beta: false,
24 isAppMuted: false, 24 isAppMuted: false,
25 enableGPUAcceleration: true, 25 enableGPUAcceleration: true,
26 serviceLimit: 5,
26}; 27};
27 28
28export const FRANZ_SERVICE_REQUEST = 'https://bit.ly/franz-service-request'; 29export const FRANZ_SERVICE_REQUEST = 'https://bit.ly/franz-service-request';
29export const FRANZ_TRANSLATION = 'https://bit.ly/franz-translate'; 30export const FRANZ_TRANSLATION = 'https://bit.ly/franz-translate';
30 31
31export const SETTINGS_PATH = path.join(app.getPath('userData'), 'config', 'settings.json'); 32export const FILE_SYSTEM_SETTINGS_TYPES = [
33 'app',
34 'proxy',
35];
36
37export const SETTINGS_PATH = path.join(app.getPath('userData'), 'config');
diff --git a/src/containers/settings/EditServiceScreen.js b/src/containers/settings/EditServiceScreen.js
index 17d727642..639e8b070 100644
--- a/src/containers/settings/EditServiceScreen.js
+++ b/src/containers/settings/EditServiceScreen.js
@@ -6,8 +6,8 @@ import { defineMessages, intlShape } from 'react-intl';
6import UserStore from '../../stores/UserStore'; 6import UserStore from '../../stores/UserStore';
7import RecipesStore from '../../stores/RecipesStore'; 7import RecipesStore from '../../stores/RecipesStore';
8import ServicesStore from '../../stores/ServicesStore'; 8import ServicesStore from '../../stores/ServicesStore';
9import FeaturesStore from '../../stores/FeaturesStore';
10import SettingsStore from '../../stores/SettingsStore'; 9import SettingsStore from '../../stores/SettingsStore';
10import FeaturesStore from '../../stores/FeaturesStore';
11import Form from '../../lib/Form'; 11import Form from '../../lib/Form';
12import { gaPage } from '../../lib/analytics'; 12import { gaPage } from '../../lib/analytics';
13 13
@@ -15,6 +15,8 @@ import ServiceError from '../../components/settings/services/ServiceError';
15import EditServiceForm from '../../components/settings/services/EditServiceForm'; 15import EditServiceForm from '../../components/settings/services/EditServiceForm';
16import { required, url, oneRequired } from '../../helpers/validation-helpers'; 16import { required, url, oneRequired } from '../../helpers/validation-helpers';
17 17
18import { config as proxyFeature } from '../../features/serviceProxy';
19
18const messages = defineMessages({ 20const messages = defineMessages({
19 name: { 21 name: {
20 id: 'settings.service.form.name', 22 id: 'settings.service.form.name',
@@ -56,6 +58,22 @@ const messages = defineMessages({
56 id: 'settings.service.form.enableDarkMode', 58 id: 'settings.service.form.enableDarkMode',
57 defaultMessage: '!!!Enable Dark Mode', 59 defaultMessage: '!!!Enable Dark Mode',
58 }, 60 },
61 enableProxy: {
62 id: 'settings.service.form.proxy.isEnabled',
63 defaultMessage: '!!!Use Proxy',
64 },
65 proxyHost: {
66 id: 'settings.service.form.proxy.host',
67 defaultMessage: '!!!Proxy Host/IP',
68 },
69 proxyUser: {
70 id: 'settings.service.form.proxy.user',
71 defaultMessage: '!!!User',
72 },
73 proxyPassword: {
74 id: 'settings.service.form.proxy.password',
75 defaultMessage: '!!!Password',
76 },
59}); 77});
60 78
61export default @inject('stores', 'actions') @observer class EditServiceScreen extends Component { 79export default @inject('stores', 'actions') @observer class EditServiceScreen extends Component {
@@ -82,7 +100,7 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
82 } 100 }
83 } 101 }
84 102
85 prepareForm(recipe, service, userCanManageServices) { 103 prepareForm(recipe, service, proxy) {
86 const { intl } = this.context; 104 const { intl } = this.context;
87 const config = { 105 const config = {
88 fields: { 106 fields: {
@@ -128,7 +146,6 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
128 if (recipe.hasTeamId) { 146 if (recipe.hasTeamId) {
129 Object.assign(config.fields, { 147 Object.assign(config.fields, {
130 team: { 148 team: {
131 disabled: !userCanManageServices,
132 label: intl.formatMessage(messages.team), 149 label: intl.formatMessage(messages.team),
133 placeholder: intl.formatMessage(messages.team), 150 placeholder: intl.formatMessage(messages.team),
134 value: service.team, 151 value: service.team,
@@ -140,7 +157,6 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
140 if (recipe.hasCustomUrl) { 157 if (recipe.hasCustomUrl) {
141 Object.assign(config.fields, { 158 Object.assign(config.fields, {
142 customUrl: { 159 customUrl: {
143 disabled: !userCanManageServices,
144 label: intl.formatMessage(messages.customUrl), 160 label: intl.formatMessage(messages.customUrl),
145 placeholder: 'https://', 161 placeholder: 'https://',
146 value: service.customUrl, 162 value: service.customUrl,
@@ -175,6 +191,40 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
175 }); 191 });
176 } 192 }
177 193
194 if (proxy.isEnabled) {
195 const serviceProxyConfig = this.props.stores.settings.proxy[service.id] || {};
196
197 Object.assign(config.fields, {
198 proxy: {
199 name: 'proxy',
200 label: 'proxy',
201 fields: {
202 isEnabled: {
203 label: intl.formatMessage(messages.enableProxy),
204 value: serviceProxyConfig.isEnabled,
205 default: false,
206 },
207 host: {
208 label: intl.formatMessage(messages.proxyHost),
209 value: serviceProxyConfig.host,
210 default: '',
211 },
212 user: {
213 label: intl.formatMessage(messages.proxyUser),
214 value: serviceProxyConfig.user,
215 default: '',
216 },
217 password: {
218 label: intl.formatMessage(messages.proxyPassword),
219 value: serviceProxyConfig.password,
220 default: '',
221 type: 'password',
222 },
223 },
224 },
225 });
226 }
227
178 return new Form(config); 228 return new Form(config);
179 } 229 }
180 230
@@ -192,7 +242,7 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
192 } 242 }
193 243
194 render() { 244 render() {
195 const { recipes, services, user, features } = this.props.stores; 245 const { recipes, services, user } = this.props.stores;
196 const { action } = this.props.router.params; 246 const { action } = this.props.router.params;
197 247
198 let recipe; 248 let recipe;
@@ -227,8 +277,7 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
227 ); 277 );
228 } 278 }
229 279
230 const userCanManageServices = features.features.userCanManageServices; 280 const form = this.prepareForm(recipe, service, proxyFeature);
231 const form = this.prepareForm(recipe, service, userCanManageServices);
232 281
233 return ( 282 return (
234 <EditServiceForm 283 <EditServiceForm
@@ -236,13 +285,14 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
236 recipe={recipe} 285 recipe={recipe}
237 service={service} 286 service={service}
238 user={user.data} 287 user={user.data}
239 userCanManageServices={userCanManageServices}
240 form={form} 288 form={form}
241 status={services.actionStatus} 289 status={services.actionStatus}
242 isSaving={services.updateServiceRequest.isExecuting || services.createServiceRequest.isExecuting} 290 isSaving={services.updateServiceRequest.isExecuting || services.createServiceRequest.isExecuting}
243 isDeleting={services.deleteServiceRequest.isExecuting} 291 isDeleting={services.deleteServiceRequest.isExecuting}
244 onSubmit={d => this.onSubmit(d)} 292 onSubmit={d => this.onSubmit(d)}
245 onDelete={() => this.deleteService()} 293 onDelete={() => this.deleteService()}
294 isProxyFeatureEnabled={proxyFeature.isEnabled}
295 isProxyFeaturePremiumFeature={proxyFeature.isPremium}
246 /> 296 />
247 ); 297 );
248 } 298 }
@@ -253,8 +303,8 @@ EditServiceScreen.wrappedComponent.propTypes = {
253 user: PropTypes.instanceOf(UserStore).isRequired, 303 user: PropTypes.instanceOf(UserStore).isRequired,
254 recipes: PropTypes.instanceOf(RecipesStore).isRequired, 304 recipes: PropTypes.instanceOf(RecipesStore).isRequired,
255 services: PropTypes.instanceOf(ServicesStore).isRequired, 305 services: PropTypes.instanceOf(ServicesStore).isRequired,
256 features: PropTypes.instanceOf(FeaturesStore).isRequired,
257 settings: PropTypes.instanceOf(SettingsStore).isRequired, 306 settings: PropTypes.instanceOf(SettingsStore).isRequired,
307 features: PropTypes.instanceOf(FeaturesStore).isRequired,
258 }).isRequired, 308 }).isRequired,
259 router: PropTypes.shape({ 309 router: PropTypes.shape({
260 params: PropTypes.shape({ 310 params: PropTypes.shape({
@@ -267,5 +317,8 @@ EditServiceScreen.wrappedComponent.propTypes = {
267 updateService: PropTypes.func.isRequired, 317 updateService: PropTypes.func.isRequired,
268 deleteService: PropTypes.func.isRequired, 318 deleteService: PropTypes.func.isRequired,
269 }).isRequired, 319 }).isRequired,
320 // settings: PropTypes.shape({
321 // update: PropTypes.func.isRequred,
322 // }).isRequired,
270 }).isRequired, 323 }).isRequired,
271}; 324};
diff --git a/src/containers/settings/EditSettingsScreen.js b/src/containers/settings/EditSettingsScreen.js
index df6442eb8..7da009c8b 100644
--- a/src/containers/settings/EditSettingsScreen.js
+++ b/src/containers/settings/EditSettingsScreen.js
@@ -10,6 +10,7 @@ import Form from '../../lib/Form';
10import { APP_LOCALES } from '../../i18n/languages'; 10import { APP_LOCALES } from '../../i18n/languages';
11import { gaPage } from '../../lib/analytics'; 11import { gaPage } from '../../lib/analytics';
12import { DEFAULT_APP_SETTINGS } from '../../config'; 12import { DEFAULT_APP_SETTINGS } from '../../config';
13import { config as spellcheckerConfig } from '../../features/spellchecker';
13 14
14 15
15import EditSettingsForm from '../../components/settings/settings/EditSettingsForm'; 16import EditSettingsForm from '../../components/settings/settings/EditSettingsForm';
@@ -161,8 +162,8 @@ export default @inject('stores', 'actions') @observer class EditSettingsScreen e
161 }, 162 },
162 enableSpellchecking: { 163 enableSpellchecking: {
163 label: intl.formatMessage(messages.enableSpellchecking), 164 label: intl.formatMessage(messages.enableSpellchecking),
164 value: settings.all.app.enableSpellchecking, 165 value: !this.props.stores.user.data.isPremium && spellcheckerConfig.isPremiumFeature ? false : settings.all.app.enableSpellchecking,
165 default: DEFAULT_APP_SETTINGS.enableSpellchecking, 166 default: !this.props.stores.user.data.isPremium && spellcheckerConfig.isPremiumFeature ? false : DEFAULT_APP_SETTINGS.enableSpellchecking,
166 }, 167 },
167 darkMode: { 168 darkMode: {
168 label: intl.formatMessage(messages.darkMode), 169 label: intl.formatMessage(messages.darkMode),
@@ -218,6 +219,7 @@ export default @inject('stores', 'actions') @observer class EditSettingsScreen e
218 cacheSize={cacheSize} 219 cacheSize={cacheSize}
219 isClearingAllCache={isClearingAllCache} 220 isClearingAllCache={isClearingAllCache}
220 onClearAllCache={clearAllCache} 221 onClearAllCache={clearAllCache}
222 isSpellcheckerPremiumFeature={spellcheckerConfig.isPremiumFeature}
221 /> 223 />
222 ); 224 );
223 } 225 }
diff --git a/src/containers/subscription/SubscriptionFormScreen.js b/src/containers/subscription/SubscriptionFormScreen.js
index 9f7571bda..50ed19bef 100644
--- a/src/containers/subscription/SubscriptionFormScreen.js
+++ b/src/containers/subscription/SubscriptionFormScreen.js
@@ -79,7 +79,6 @@ export default @inject('stores', 'actions') @observer class SubscriptionFormScre
79 return ( 79 return (
80 <SubscriptionForm 80 <SubscriptionForm
81 plan={stores.payment.plan} 81 plan={stores.payment.plan}
82 // form={this.prepareForm(stores.payment.plan)}
83 isLoading={stores.payment.plansRequest.isExecuting} 82 isLoading={stores.payment.plansRequest.isExecuting}
84 retryPlanRequest={() => stores.payment.plansRequest.reload()} 83 retryPlanRequest={() => stores.payment.plansRequest.reload()}
85 isCreatingHostedPage={stores.payment.createHostedPageRequest.isExecuting} 84 isCreatingHostedPage={stores.payment.createHostedPageRequest.isExecuting}
diff --git a/src/electron/Settings.js b/src/electron/Settings.js
index 7b04406a2..6ac3b9177 100644
--- a/src/electron/Settings.js
+++ b/src/electron/Settings.js
@@ -1,15 +1,21 @@
1import { observable, toJS } from 'mobx'; 1import { observable, toJS } from 'mobx';
2import { pathExistsSync, outputJsonSync, readJsonSync } from 'fs-extra'; 2import { pathExistsSync, outputJsonSync, readJsonSync } from 'fs-extra';
3import path from 'path';
3 4
4import { SETTINGS_PATH, DEFAULT_APP_SETTINGS } from '../config'; 5import { SETTINGS_PATH } from '../config';
5 6
6const debug = require('debug')('Franz:Settings'); 7const debug = require('debug')('Franz:Settings');
7 8
8export default class Settings { 9export default class Settings {
9 @observable store = DEFAULT_APP_SETTINGS; 10 type = '';
11 @observable store = {};
10 12
11 constructor() { 13 constructor(type, defaultState = {}) {
12 if (!pathExistsSync(SETTINGS_PATH)) { 14 this.type = type;
15 this.store = defaultState;
16 this.defaultState = defaultState;
17
18 if (!pathExistsSync(this.settingsFile)) {
13 this._writeFile(); 19 this._writeFile();
14 } else { 20 } else {
15 this._hydrate(); 21 this._hydrate();
@@ -31,16 +37,20 @@ export default class Settings {
31 } 37 }
32 38
33 _merge(settings) { 39 _merge(settings) {
34 return Object.assign(DEFAULT_APP_SETTINGS, this.store, settings); 40 return Object.assign(this.defaultState, this.store, settings);
35 } 41 }
36 42
37 _hydrate() { 43 _hydrate() {
38 this.store = this._merge(readJsonSync(SETTINGS_PATH)); 44 this.store = this._merge(readJsonSync(this.settingsFile));
39 debug('Hydrate store', toJS(this.store)); 45 debug('Hydrate store', toJS(this.store));
40 } 46 }
41 47
42 _writeFile() { 48 _writeFile() {
43 outputJsonSync(SETTINGS_PATH, this.store); 49 outputJsonSync(this.settingsFile, this.store);
44 debug('Write settings file', toJS(this.store)); 50 debug('Write settings file', toJS(this.store));
45 } 51 }
52
53 get settingsFile() {
54 return path.join(SETTINGS_PATH, `${this.type === 'app' ? 'settings' : this.type}.json`);
55 }
46} 56}
diff --git a/src/electron/ipc-api/appIndicator.js b/src/electron/ipc-api/appIndicator.js
index d31819068..e568bf35d 100644
--- a/src/electron/ipc-api/appIndicator.js
+++ b/src/electron/ipc-api/appIndicator.js
@@ -15,7 +15,7 @@ function getAsset(type, asset) {
15 15
16export default (params) => { 16export default (params) => {
17 autorun(() => { 17 autorun(() => {
18 isTrayIconEnabled = params.settings.get('enableSystemTray'); 18 isTrayIconEnabled = params.settings.app.get('enableSystemTray');
19 19
20 if (!isTrayIconEnabled) { 20 if (!isTrayIconEnabled) {
21 params.trayIcon.hide(); 21 params.trayIcon.hide();
diff --git a/src/electron/ipc-api/settings.js b/src/electron/ipc-api/settings.js
index 3eab68a91..ce006bb92 100644
--- a/src/electron/ipc-api/settings.js
+++ b/src/electron/ipc-api/settings.js
@@ -1,11 +1,15 @@
1import { ipcMain } from 'electron'; 1import { ipcMain } from 'electron';
2 2
3export default (params) => { 3export default (params) => {
4 ipcMain.on('getAppSettings', () => { 4 ipcMain.on('getAppSettings', (event, type) => {
5 params.mainWindow.webContents.send('appSettings', params.settings.all); 5 console.log('getAppSettings', type, params.settings[type].all);
6 params.mainWindow.webContents.send('appSettings', {
7 type,
8 data: params.settings[type].all,
9 });
6 }); 10 });
7 11
8 ipcMain.on('updateAppSettings', (event, args) => { 12 ipcMain.on('updateAppSettings', (event, args) => {
9 params.settings.set(args); 13 params.settings[args.type].set(args.data);
10 }); 14 });
11}; 15};
diff --git a/src/features/delayApp/Component.js b/src/features/delayApp/Component.js
index 2bfa1162e..403340c7b 100644
--- a/src/features/delayApp/Component.js
+++ b/src/features/delayApp/Component.js
@@ -24,7 +24,7 @@ const messages = defineMessages({
24 }, 24 },
25}); 25});
26 26
27export default @inject('actions') @observer @injectSheet(styles) class DelayApp extends Component { 27export default @inject('actions') @injectSheet(styles) @observer class DelayApp extends Component {
28 static propTypes = { 28 static propTypes = {
29 // eslint-disable-next-line 29 // eslint-disable-next-line
30 classes: PropTypes.object.isRequired, 30 classes: PropTypes.object.isRequired,
@@ -39,8 +39,6 @@ export default @inject('actions') @observer @injectSheet(styles) class DelayApp
39 } 39 }
40 40
41 componentDidMount() { 41 componentDidMount() {
42 // const { reload } = this.props;
43
44 this.countdownInterval = setInterval(() => { 42 this.countdownInterval = setInterval(() => {
45 this.setState({ 43 this.setState({
46 countdown: this.state.countdown - this.countdownIntervalTimeout, 44 countdown: this.state.countdown - this.countdownIntervalTimeout,
@@ -53,6 +51,10 @@ export default @inject('actions') @observer @injectSheet(styles) class DelayApp
53 }, this.countdownIntervalTimeout); 51 }, this.countdownIntervalTimeout);
54 } 52 }
55 53
54 componentWillUnmount() {
55 clearInterval(this.countdownInterval);
56 }
57
56 countdownInterval = null; 58 countdownInterval = null;
57 countdownIntervalTimeout = 1000; 59 countdownIntervalTimeout = 1000;
58 60
diff --git a/src/features/delayApp/index.js b/src/features/delayApp/index.js
index a3cce03ee..334433df8 100644
--- a/src/features/delayApp/index.js
+++ b/src/features/delayApp/index.js
@@ -38,8 +38,8 @@ export default function init(stores) {
38 let shownAfterLaunch = false; 38 let shownAfterLaunch = false;
39 let timeLastDelay = moment(); 39 let timeLastDelay = moment();
40 40
41 config.delayOffset = globalConfig.delayOffset || DEFAULT_DELAY_OFFSET; 41 config.delayOffset = globalConfig.delayOffset !== undefined ? globalConfig.delayOffset : DEFAULT_DELAY_OFFSET;
42 config.delayDuration = globalConfig.wait || DEFAULT_DELAY_DURATION; 42 config.delayDuration = globalConfig.wait !== undefined ? globalConfig.wait : DEFAULT_DELAY_DURATION;
43 43
44 autorun(() => { 44 autorun(() => {
45 const diff = moment().diff(timeLastDelay); 45 const diff = moment().diff(timeLastDelay);
@@ -63,5 +63,5 @@ export default function init(stores) {
63 ); 63 );
64} 64}
65 65
66export const component = DelayAppComponent; 66export const Component = DelayAppComponent;
67 67
diff --git a/src/features/delayApp/styles.js b/src/features/delayApp/styles.js
index 097368d9a..5c214cfdf 100644
--- a/src/features/delayApp/styles.js
+++ b/src/features/delayApp/styles.js
@@ -1,26 +1,23 @@
1export default (theme) => { 1export default theme => ({
2 console.log(theme); 2 container: {
3 return ({ 3 background: theme.colorBackground,
4 container: { 4 position: 'absolute',
5 background: theme.colorBackground, 5 top: 0,
6 position: 'absolute', 6 width: '100%',
7 top: 0, 7 display: 'flex',
8 width: '100%', 8 'flex-direction': 'column',
9 display: 'flex', 9 'align-items': 'center',
10 'flex-direction': 'column', 10 'justify-content': 'center',
11 'align-items': 'center', 11 'z-index': 150,
12 'justify-content': 'center', 12 },
13 'z-index': 150, 13 headline: {
14 }, 14 color: theme.colorHeadline,
15 headline: { 15 margin: [25, 0, 40],
16 color: theme.colorHeadline, 16 'max-width': 500,
17 margin: [25, 0, 40], 17 'text-align': 'center',
18 'max-width': 500, 18 'line-height': '1.3em',
19 'text-align': 'center', 19 },
20 'line-height': '1.3em', 20 button: {
21 }, 21 margin: [40, 0, 20],
22 button: { 22 },
23 margin: [40, 0, 20], 23});
24 },
25 });
26};
diff --git a/src/features/serviceProxy/index.js b/src/features/serviceProxy/index.js
new file mode 100644
index 000000000..edb1c9367
--- /dev/null
+++ b/src/features/serviceProxy/index.js
@@ -0,0 +1,56 @@
1import { autorun, reaction, observable } from 'mobx';
2import { remote } from 'electron';
3
4const { session } = remote;
5
6const debug = require('debug')('Franz:feature:serviceProxy');
7
8const DEFAULT_ENABLED = false;
9const DEFAULT_IS_PREMIUM = true;
10
11export const config = observable({
12 isEnabled: DEFAULT_ENABLED,
13 isPremium: DEFAULT_IS_PREMIUM,
14});
15
16export default function init(stores) {
17 reaction(
18 () => stores.features.features.isServiceProxyEnabled,
19 (enabled, r) => {
20 if (enabled) {
21 debug('Initializing `serviceProxy` feature');
22
23 // Dispose the reaction to run this only once
24 r.dispose();
25
26 const { isServiceProxyEnabled, isServiceProxyPremiumFeature } = stores.features.features;
27
28 config.isEnabled = isServiceProxyEnabled !== undefined ? isServiceProxyEnabled : DEFAULT_ENABLED;
29 config.isPremium = isServiceProxyPremiumFeature !== undefined ? isServiceProxyPremiumFeature : DEFAULT_IS_PREMIUM;
30
31 autorun(() => {
32 const services = stores.services.all;
33 const isPremiumUser = stores.user.isPremium;
34
35 if (config.isPremium && !isPremiumUser) return;
36
37 services.forEach((service) => {
38 const s = session.fromPartition(`persist:service-${service.id}`);
39 let proxyHost = 'direct://';
40
41 const serviceProxyConfig = stores.settings.proxy[service.id];
42
43 if (serviceProxyConfig && serviceProxyConfig.isEnabled && serviceProxyConfig.host) {
44 proxyHost = serviceProxyConfig.host;
45 }
46
47 s.setProxy({ proxyRules: proxyHost }, (e) => {
48 debug(`Using proxy "${proxyHost}" for "${service.name}" (${service.id})`, e);
49 });
50 });
51 });
52 }
53 },
54 );
55}
56
diff --git a/src/features/spellchecker/index.js b/src/features/spellchecker/index.js
new file mode 100644
index 000000000..8b3fb7e00
--- /dev/null
+++ b/src/features/spellchecker/index.js
@@ -0,0 +1,38 @@
1import { autorun, reaction } from 'mobx';
2
3const debug = require('debug')('Franz:feature:spellchecker');
4
5const DEFAULT_IS_PREMIUM_FEATURE = true;
6
7export const config = {
8 isPremiumFeature: DEFAULT_IS_PREMIUM_FEATURE,
9};
10
11export default function init(stores) {
12 reaction(
13 () => stores.features.features.isSpellcheckerPremiumFeature,
14 (enabled, r) => {
15 if (enabled) {
16 debug('Initializing `spellchecker` feature');
17
18 // Dispose the reaction to run this only once
19 r.dispose();
20
21 const { isSpellcheckerPremiumFeature } = stores.features.features;
22
23 config.isPremiumFeature = isSpellcheckerPremiumFeature !== undefined ? isSpellcheckerPremiumFeature : DEFAULT_IS_PREMIUM_FEATURE;
24
25 autorun(() => {
26 if (!stores.user.data.isPremium && config.isPremiumFeature) {
27 debug('Override settings.spellcheckerEnabled flag to false');
28
29 Object.assign(stores.settings.all.app, {
30 enableSpellchecker: false,
31 });
32 }
33 });
34 }
35 },
36 );
37}
38
diff --git a/src/features/spellchecker/styles.js b/src/features/spellchecker/styles.js
new file mode 100644
index 000000000..097368d9a
--- /dev/null
+++ b/src/features/spellchecker/styles.js
@@ -0,0 +1,26 @@
1export default (theme) => {
2 console.log(theme);
3 return ({
4 container: {
5 background: theme.colorBackground,
6 position: 'absolute',
7 top: 0,
8 width: '100%',
9 display: 'flex',
10 'flex-direction': 'column',
11 'align-items': 'center',
12 'justify-content': 'center',
13 'z-index': 150,
14 },
15 headline: {
16 color: theme.colorHeadline,
17 margin: [25, 0, 40],
18 'max-width': 500,
19 'text-align': 'center',
20 'line-height': '1.3em',
21 },
22 button: {
23 margin: [40, 0, 20],
24 },
25 });
26};
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 24e29df18..8d82f98a4 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -50,10 +50,12 @@
50 "subscription.paymentSessionError": "Could not initialize payment form", 50 "subscription.paymentSessionError": "Could not initialize payment form",
51 "subscription.includedFeatures": "Paid Franz Premium Supporter Account includes", 51 "subscription.includedFeatures": "Paid Franz Premium Supporter Account includes",
52 "subscription.features.onpremise": "Add on-premise/hosted services like HipChat", 52 "subscription.features.onpremise": "Add on-premise/hosted services like HipChat",
53 "subscription.features.customServices": "Private services for you and your team", 53 "subscription.features.onpremise.mattermost": "Add on-premise/hosted services like Mattermost",
54 "subscription.features.encryptedSync": "Encrypted session synchronization", 54 "subscription.features.encryptedSync": "Encrypted session synchronization",
55 "subscription.features.vpn": "Proxy & VPN support", 55 "subscription.features.vpn": "Proxy & VPN support",
56 "subscription.features.ads": "No ads, ever!", 56 "subscription.features.ads": "No ads, ever!",
57 "subscription.features.spellchecker": "Support for spellchecker",
58 "subscription.features.noInterruptions": "No app delays & nagging to upgrade license",
57 "subscription.features.comingSoon": "coming soon", 59 "subscription.features.comingSoon": "coming soon",
58 "infobar.servicesUpdated": "Your services have been updated.", 60 "infobar.servicesUpdated": "Your services have been updated.",
59 "infobar.updateAvailable": "A new update for Franz is available.", 61 "infobar.updateAvailable": "A new update for Franz is available.",
@@ -131,6 +133,12 @@
131 "settings.service.form.iconDelete": "Delete", 133 "settings.service.form.iconDelete": "Delete",
132 "settings.service.form.iconUpload": "Drop your image, or click here", 134 "settings.service.form.iconUpload": "Drop your image, or click here",
133 "settings.service.form.enableDarkMode": "Enable Dark Mode", 135 "settings.service.form.enableDarkMode": "Enable Dark Mode",
136 "settings.service.form.proxy.headline": "Proxy Settings",
137 "settings.service.form.proxy.isEnabled": "Use Proxy",
138 "settings.service.form.proxy.host": "Proxy Host/IP",
139 "settings.service.form.proxy.user": "User (optional)",
140 "settings.service.form.proxy.password": "Password (optional)",
141 "settings.service.form.proxy.info": "Proxy settings will not synced with the Franz servers.",
134 "settings.service.error.headline": "Error", 142 "settings.service.error.headline": "Error",
135 "settings.service.error.goBack": "Back to services", 143 "settings.service.error.goBack": "Back to services",
136 "settings.service.error.message": "Could not load service recipe.", 144 "settings.service.error.message": "Could not load service recipe.",
@@ -252,5 +260,6 @@
252 "validation.minLength": "{field} should be at least {length} characters long", 260 "validation.minLength": "{field} should be at least {length} characters long",
253 "feature.delayApp.headline": "Please purchase a Franz Supporter License to skip waiting", 261 "feature.delayApp.headline": "Please purchase a Franz Supporter License to skip waiting",
254 "feature.delayApp.action": "Get a Franz Supporter License", 262 "feature.delayApp.action": "Get a Franz Supporter License",
255 "feature.delayApp.text": "Franz will continue in {seconds} seconds." 263 "feature.delayApp.text": "Franz will continue in {seconds} seconds.",
264 "premiumFeature.button.upgradeAccount": "Upgrade account"
256} 265}
diff --git a/src/index.js b/src/index.js
index 7d906ad71..994531dbf 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,4 @@
1import { app, BrowserWindow, shell } from 'electron'; 1import { app, BrowserWindow, shell, ipcMain } from 'electron';
2import fs from 'fs-extra'; 2import fs from 'fs-extra';
3import path from 'path'; 3import path from 'path';
4 4
@@ -12,6 +12,8 @@ import handleDeepLink from './electron/deepLinking';
12import { appId } from './package.json'; // eslint-disable-line import/no-unresolved 12import { appId } from './package.json'; // eslint-disable-line import/no-unresolved
13import './electron/exception'; 13import './electron/exception';
14 14
15import { DEFAULT_APP_SETTINGS } from './config';
16
15const debug = require('debug')('Franz:App'); 17const debug = require('debug')('Franz:App');
16 18
17// Keep a global reference of the window object, if you don't, the window will 19// Keep a global reference of the window object, if you don't, the window will
@@ -62,7 +64,8 @@ if (isLinux && ['Pantheon', 'Unity:Unity7'].indexOf(process.env.XDG_CURRENT_DESK
62} 64}
63 65
64// Initialize Settings 66// Initialize Settings
65const settings = new Settings(); 67const settings = new Settings('app', DEFAULT_APP_SETTINGS);
68const proxySettings = new Settings('proxy');
66 69
67// Disable GPU acceleration 70// Disable GPU acceleration
68if (!settings.get('enableGPUAcceleration')) { 71if (!settings.get('enableGPUAcceleration')) {
@@ -94,7 +97,14 @@ const createWindow = () => {
94 const trayIcon = new Tray(); 97 const trayIcon = new Tray();
95 98
96 // Initialize ipcApi 99 // Initialize ipcApi
97 ipcApi({ mainWindow, settings, trayIcon }); 100 ipcApi({
101 mainWindow,
102 settings: {
103 app: settings,
104 proxy: proxySettings,
105 },
106 trayIcon,
107 });
98 108
99 // Manage Window State 109 // Manage Window State
100 mainWindowState.manage(mainWindow); 110 mainWindowState.manage(mainWindow);
@@ -177,6 +187,24 @@ const createWindow = () => {
177// Some APIs can only be used after this event occurs. 187// Some APIs can only be used after this event occurs.
178app.on('ready', createWindow); 188app.on('ready', createWindow);
179 189
190// This is the worst possible implementation as the webview.webContents based callback doesn't work 🖕
191app.on('login', (event, webContents, request, authInfo, callback) => {
192 event.preventDefault();
193 debug('browser login event', authInfo);
194 if (authInfo.isProxy && authInfo.scheme === 'basic') {
195 webContents.send('get-service-id');
196
197 ipcMain.on('service-id', (e, id) => {
198 debug('Received service id', id);
199
200 const ps = proxySettings.get(id);
201 callback(ps.user, ps.password);
202 });
203 } else {
204 // TODO: implement basic auth
205 }
206});
207
180// Quit when all windows are closed. 208// Quit when all windows are closed.
181app.on('window-all-closed', () => { 209app.on('window-all-closed', () => {
182 // On OS X it is common for applications and their menu bar 210 // On OS X it is common for applications and their menu bar
diff --git a/src/models/Service.js b/src/models/Service.js
index 1bab8bd68..41180dd76 100644
--- a/src/models/Service.js
+++ b/src/models/Service.js
@@ -69,6 +69,8 @@ export default class Service {
69 69
70 this.hasCustomUploadedIcon = data.hasCustomIcon !== undefined ? data.hasCustomIcon : this.hasCustomUploadedIcon; 70 this.hasCustomUploadedIcon = data.hasCustomIcon !== undefined ? data.hasCustomIcon : this.hasCustomUploadedIcon;
71 71
72 this.proxy = data.proxy !== undefined ? data.proxy : this.proxy;
73
72 this.recipe = recipe; 74 this.recipe = recipe;
73 75
74 autorun(() => { 76 autorun(() => {
diff --git a/src/models/Settings.js b/src/models/Settings.js
deleted file mode 100644
index 0e4c59057..000000000
--- a/src/models/Settings.js
+++ /dev/null
@@ -1,33 +0,0 @@
1import { observable, extendObservable } from 'mobx';
2import { DEFAULT_APP_SETTINGS } from '../config';
3
4export default class Settings {
5 @observable app = DEFAULT_APP_SETTINGS
6
7 @observable service = {
8 activeService: '',
9 }
10
11 @observable group = {
12 collapsed: [],
13 disabled: [],
14 }
15
16 @observable stats = {
17 appStarts: 0,
18 }
19
20 @observable migration = {}
21
22 constructor({ app, service, group, stats, migration }) {
23 Object.assign(this.app, app);
24 Object.assign(this.service, service);
25 Object.assign(this.group, group);
26 Object.assign(this.stats, stats);
27 Object.assign(this.migration, migration);
28 }
29
30 update(data) {
31 extendObservable(this, data);
32 }
33}
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js
index 9740d078f..59abeb218 100644
--- a/src/stores/FeaturesStore.js
+++ b/src/stores/FeaturesStore.js
@@ -4,6 +4,8 @@ import Store from './lib/Store';
4import CachedRequest from './lib/CachedRequest'; 4import CachedRequest from './lib/CachedRequest';
5 5
6import delayApp from '../features/delayApp'; 6import delayApp from '../features/delayApp';
7import spellchecker from '../features/spellchecker';
8import serviceProxy from '../features/serviceProxy';
7 9
8export default class FeaturesStore extends Store { 10export default class FeaturesStore extends Store {
9 @observable defaultFeaturesRequest = new CachedRequest(this.api.features, 'default'); 11 @observable defaultFeaturesRequest = new CachedRequest(this.api.features, 'default');
@@ -36,5 +38,7 @@ export default class FeaturesStore extends Store {
36 38
37 _enableFeatures() { 39 _enableFeatures() {
38 delayApp(this.stores, this.actions); 40 delayApp(this.stores, this.actions);
41 spellchecker(this.stores, this.actions);
42 serviceProxy(this.stores, this.actions);
39 } 43 }
40} 44}
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index cdb2db142..e22b343e7 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -143,6 +143,7 @@ export default class ServicesStore extends Store {
143 // Actions 143 // Actions
144 @action async _createService({ recipeId, serviceData, redirect = true }) { 144 @action async _createService({ recipeId, serviceData, redirect = true }) {
145 const data = this._cleanUpTeamIdAndCustomUrl(recipeId, serviceData); 145 const data = this._cleanUpTeamIdAndCustomUrl(recipeId, serviceData);
146
146 const response = await this.createServiceRequest.execute(recipeId, data)._promise; 147 const response = await this.createServiceRequest.execute(recipeId, data)._promise;
147 148
148 this.allServicesRequest.patch((result) => { 149 this.allServicesRequest.patch((result) => {
@@ -150,6 +151,13 @@ export default class ServicesStore extends Store {
150 result.push(response.data); 151 result.push(response.data);
151 }); 152 });
152 153
154 this.actions.settings.update({
155 type: 'proxy',
156 data: {
157 [`${response.data.id}`]: data.proxy,
158 },
159 });
160
153 this.actionStatus = response.status || []; 161 this.actionStatus = response.status || [];
154 162
155 if (redirect) { 163 if (redirect) {
@@ -222,6 +230,13 @@ export default class ServicesStore extends Store {
222 }); 230 });
223 } 231 }
224 232
233 this.actions.settings.update({
234 type: 'proxy',
235 data: {
236 [`${serviceId}`]: data.proxy,
237 },
238 });
239
225 if (redirect) { 240 if (redirect) {
226 this.stores.router.push('/settings/services'); 241 this.stores.router.push('/settings/services');
227 gaEvent('Service', 'update', service.recipe.id); 242 gaEvent('Service', 'update', service.recipe.id);
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js
index f1b067115..b62ac15e0 100644
--- a/src/stores/SettingsStore.js
+++ b/src/stores/SettingsStore.js
@@ -1,12 +1,13 @@
1import { remote } from 'electron'; 1import { remote, ipcRenderer } from 'electron';
2import { action, computed, observable } from 'mobx'; 2import { action, computed, observable } from 'mobx';
3import localStorage from 'mobx-localstorage'; 3import localStorage from 'mobx-localstorage';
4 4
5import Store from './lib/Store'; 5import Store from './lib/Store';
6import SettingsModel from '../models/Settings';
7import Request from './lib/Request'; 6import Request from './lib/Request';
8import CachedRequest from './lib/CachedRequest'; 7import CachedRequest from './lib/CachedRequest';
9 8
9import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES } from '../config';
10
10const { systemPreferences } = remote; 11const { systemPreferences } = remote;
11const debug = require('debug')('Franz:SettingsStore'); 12const debug = require('debug')('Franz:SettingsStore');
12 13
@@ -14,12 +15,35 @@ export default class SettingsStore extends Store {
14 @observable appSettingsRequest = new CachedRequest(this.api.local, 'getAppSettings'); 15 @observable appSettingsRequest = new CachedRequest(this.api.local, 'getAppSettings');
15 @observable updateAppSettingsRequest = new Request(this.api.local, 'updateAppSettings'); 16 @observable updateAppSettingsRequest = new Request(this.api.local, 'updateAppSettings');
16 17
18 @observable fileSystemSettingsRequests = [];
19
20 fileSystemSettingsTypes = FILE_SYSTEM_SETTINGS_TYPES;
21 @observable _fileSystemSettingsCache = {
22 app: DEFAULT_APP_SETTINGS,
23 proxy: {},
24 };
25
17 constructor(...args) { 26 constructor(...args) {
18 super(...args); 27 super(...args);
19 28
20 // Register action handlers 29 // Register action handlers
21 this.actions.settings.update.listen(this._update.bind(this)); 30 this.actions.settings.update.listen(this._update.bind(this));
22 this.actions.settings.remove.listen(this._remove.bind(this)); 31 this.actions.settings.remove.listen(this._remove.bind(this));
32
33 this.fileSystemSettingsTypes.forEach((type) => {
34 this.fileSystemSettingsRequests[type] = new CachedRequest(this.api.local, 'getAppSettings');
35 });
36
37 ipcRenderer.on('appSettings', (event, resp) => {
38 debug('Get appSettings resolves', resp, resp.type, resp.data);
39
40 this._fileSystemSettingsCache[resp.type] = resp.data;
41 });
42
43 this.fileSystemSettingsTypes.forEach((type) => {
44 console.log(type);
45 ipcRenderer.send('getAppSettings', type);
46 });
23 } 47 }
24 48
25 async setup() { 49 async setup() {
@@ -28,29 +52,53 @@ export default class SettingsStore extends Store {
28 await this._migrate(); 52 await this._migrate();
29 } 53 }
30 54
55 @computed get app() {
56 return this._fileSystemSettingsCache.app || DEFAULT_APP_SETTINGS;
57 }
58
59 @computed get proxy() {
60 return this._fileSystemSettingsCache.proxy || {};
61 }
62
63 @computed get service() {
64 return localStorage.getItem('service') || {
65 activeService: '',
66 };
67 }
68
69 @computed get stats() {
70 return localStorage.getItem('stats') || {
71 activeService: '',
72 };
73 }
74
75 @computed get migration() {
76 return localStorage.getItem('migration') || {};
77 }
78
31 @computed get all() { 79 @computed get all() {
32 return new SettingsModel({ 80 return {
33 app: this.appSettingsRequest.execute().result || {}, 81 app: this.app,
34 service: localStorage.getItem('service') || {}, 82 proxy: this.proxy,
35 group: localStorage.getItem('group') || {}, 83 service: this.service,
36 stats: localStorage.getItem('stats') || {}, 84 stats: this.stats,
37 migration: localStorage.getItem('migration') || {}, 85 migration: this.migration,
38 }); 86 };
39 } 87 }
40 88
41 @action async _update({ type, data }) { 89 @action async _update({ type, data }) {
42 const appSettings = this.all; 90 const appSettings = this.all;
43 if (type !== 'app') { 91 if (!this.fileSystemSettingsTypes.includes(type)) {
44 debug('Update settings', type, data, this.all); 92 debug('Update settings', type, data, this.all);
45 localStorage.setItem(type, Object.assign(appSettings[type], data)); 93 localStorage.setItem(type, Object.assign(appSettings[type], data));
46 } else { 94 } else {
47 debug('Update settings on file system', type, data); 95 debug('Update settings on file system', type, data);
48 this.updateAppSettingsRequest.execute(data); 96 ipcRenderer.send('updateAppSettings', {
49 97 type,
50 this.appSettingsRequest.patch((result) => { 98 data,
51 if (!result) return;
52 Object.assign(result, data);
53 }); 99 });
100
101 Object.assign(this._fileSystemSettingsCache[type], data);
54 } 102 }
55 } 103 }
56 104
@@ -128,4 +176,8 @@ export default class SettingsStore extends Store {
128 debug('Set up dark mode'); 176 debug('Set up dark mode');
129 } 177 }
130 } 178 }
179
180 _getFileBasedSettings(type) {
181 ipcRenderer.send('getAppSettings', type);
182 }
131} 183}
diff --git a/src/styles/settings.scss b/src/styles/settings.scss
index 5e7e35fd8..f94ca114d 100644
--- a/src/styles/settings.scss
+++ b/src/styles/settings.scss
@@ -249,6 +249,11 @@
249 margin: 25px 0 15px; 249 margin: 25px 0 15px;
250 250
251 &:first-of-type { margin-top: 0; } 251 &:first-of-type { margin-top: 0; }
252
253 .badge {
254 font-weight: normal;
255 margin-left: 10px;
256 }
252 } 257 }
253 } 258 }
254 259
diff --git a/src/theme/dark/index.js b/src/theme/dark/index.js
index e0e017c7c..496a51119 100644
--- a/src/theme/dark/index.js
+++ b/src/theme/dark/index.js
@@ -1,5 +1,6 @@
1import * as legacyStyles from '../default/legacy'; 1import * as legacyStyles from '../default/legacy';
2 2
3export const colorBackground = legacyStyles.darkThemeGrayDarkest; 3export const colorBackground = legacyStyles.darkThemeGrayDarkest;
4export const colorBackgroundSubscriptionContainer = legacyStyles.themeBrandInfo;
4 5
5export const colorHeadline = legacyStyles.darkThemeTextColor; 6export const colorHeadline = legacyStyles.darkThemeTextColor;
diff --git a/src/theme/default/index.js b/src/theme/default/index.js
index f8b6e898d..8766fb609 100644
--- a/src/theme/default/index.js
+++ b/src/theme/default/index.js
@@ -1,12 +1,21 @@
1import * as legacyStyles from './legacy'; 1import * as legacyStyles from './legacy';
2 2
3/* legacy config, injected into sass */ 3export const brandPrimary = '#3498db';
4export const themeBrandPrimary = '#3498db'; 4export const brandSuccess = '#5cb85c';
5export const themeBrandSuccess = '#5cb85c'; 5export const brandInfo = '#5bc0de';
6export const themeBrandInfo = '#5bc0de'; 6export const brandWarning = '#FF9F00';
7export const themeBrandWarning = '#FF9F00'; 7export const brandDanger = '#d9534f';
8export const themeBrandDanger = '#d9534f';
9 8
10export const colorBackground = legacyStyles.themeGrayLighter; 9export const borderRadius = legacyStyles.themeBorderRadius;
10export const borderRadiusSmall = legacyStyles.themeBorderRadiusSmall;
11 11
12export const colorBackground = legacyStyles.themeGrayLighter;
12export const colorHeadline = legacyStyles.themeGrayDark; 13export const colorHeadline = legacyStyles.themeGrayDark;
14
15// Subscription Container Component
16export const colorSubscriptionContainerBackground = 'none';
17export const colorSubscriptionContainerBorder = [1, 'solid', brandPrimary];
18export const colorSubscriptionContainerTitle = brandPrimary;
19export const colorSubscriptionContainerActionButtonBackground = brandPrimary;
20export const colorSubscriptionContainerActionButtonColor = '#FFF';
21
diff --git a/src/webview/plugin.js b/src/webview/plugin.js
index e6fdc4efd..427ec75ad 100644
--- a/src/webview/plugin.js
+++ b/src/webview/plugin.js
@@ -64,7 +64,13 @@ ipcRenderer.on('service-settings-update', (e, data) => {
64 } 64 }
65}); 65});
66 66
67// initSpellchecker 67// Needed for current implementation of electrons 'login' event
68ipcRenderer.on('get-service-id', (event) => {
69 debug('Asking for service id', event);
70
71 event.sender.send('service-id', serviceData.id);
72});
73
68 74
69document.addEventListener('DOMContentLoaded', () => { 75document.addEventListener('DOMContentLoaded', () => {
70 ipcRenderer.sendToHost('hello'); 76 ipcRenderer.sendToHost('hello');