diff --git a/.eslintrc b/.eslintrc
index 8e95ad4fd..948550306 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -13,6 +13,7 @@
"react/jsx-filename-extension": [1, {
"extensions": [".js", ".jsx"]
}],
+ "react/forbid-prop-types": 1,
"no-underscore-dangle": 0,
"max-len": 0,
"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 {
this.local = local;
}
- getAppSettings() {
- return this.local.getAppSettings();
+ getAppSettings(type) {
+ return this.local.getAppSettings(type);
}
- updateAppSettings(data) {
- return this.local.updateAppSettings(data);
+ updateAppSettings(type, data) {
+ return this.local.updateAppSettings(type, data);
}
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;
export default class LocalApi {
// Settings
- getAppSettings() {
+ getAppSettings(type) {
return new Promise((resolve) => {
- ipcRenderer.once('appSettings', (event, data) => {
- debug('LocalApi::getAppSettings resolves', data);
- resolve(data);
+ ipcRenderer.once('appSettings', (event, resp) => {
+ debug('LocalApi::getAppSettings resolves', resp.type, resp.data);
+ resolve(resp);
});
- ipcRenderer.send('getAppSettings');
+ ipcRenderer.send('getAppSettings', type);
});
}
- async updateAppSettings(data) {
- debug('LocalApi::updateAppSettings resolves', data);
- ipcRenderer.send('updateAppSettings', data);
+ async updateAppSettings(type, data) {
+ debug('LocalApi::updateAppSettings resolves', type, data);
+ ipcRenderer.send('updateAppSettings', {
+ type,
+ data,
+ });
}
// 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';
import { TitleBar } from 'electron-react-titlebar';
import InfoBar from '../ui/InfoBar';
-import { component as DelayApp } from '../../features/delayApp';
+import { Component as DelayApp } from '../../features/delayApp';
import globalMessages from '../../i18n/globalMessages';
import { 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 {
{intl.formatMessage(messages.accountTypeEnterprise)}
)}
- {!user.isSSO && (
-
- {intl.formatMessage(messages.accountEditButton)}
-
- )}
+
+ {intl.formatMessage(messages.accountEditButton)}
+
{user.emailValidated}
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
render() {
const { serviceCount } = this.props;
- const { features } = this.props.stores.features;
const { intl } = this.context;
return (
- {features.userCanManageServices && (
-
- {intl.formatMessage(messages.availableServices)}
-
- )}
+
+ {intl.formatMessage(messages.availableServices)}
+
+
+ {isProxyFeatureEnabled && (
+
+
+
+ {intl.formatMessage(messages.headlineProxy)}
+ beta
+
+
+ {form.$('proxy.isEnabled').value && (
+
+ )}
+
+
+ )}
+
{recipe.message && (
@@ -328,7 +367,7 @@ export default @observer class EditServiceForm extends Component {
{/* Delete Button */}
- {action === 'edit' && userCanManageServices && deleteButton}
+ {action === 'edit' && deleteButton}
{/* Save Button */}
{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';
import Button from '../../ui/Button';
import Toggle from '../../ui/Toggle';
import Select from '../../ui/Select';
+import PremiumFeatureContainer from '../../ui/PremiumFeatureContainer';
import { FRANZ_TRANSLATION } from '../../../config';
@@ -95,6 +96,7 @@ export default @observer class EditSettingsForm extends Component {
isClearingAllCache: PropTypes.bool.isRequired,
onClearAllCache: PropTypes.func.isRequired,
cacheSize: PropTypes.string.isRequired,
+ isSpellcheckerPremiumFeature: PropTypes.bool.isRequired,
};
static contextTypes = {
@@ -124,6 +126,7 @@ export default @observer class EditSettingsForm extends Component {
isClearingAllCache,
onClearAllCache,
cacheSize,
+ isSpellcheckerPremiumFeature,
} = this.props;
const { intl } = this.context;
@@ -175,7 +178,14 @@ export default @observer class EditSettingsForm extends Component {
{/* Advanced */}
{intl.formatMessage(messages.headlineAdvanced)}
-
+
+
+
{intl.formatMessage(messages.enableGPUAccelerationInfo)}
{/*
*/}
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({
defaultMessage: '!!!The Franz Premium Supporter Account includes',
},
features: {
- unlimitedServices: {
- id: 'subscription.features.unlimitedServices',
- defaultMessage: '!!!Add unlimited services',
- },
onpremise: {
- id: 'subscription.features.onpremise',
- defaultMessage: '!!!Add on-premise/hosted services like HipChat',
- },
- customServices: {
- id: 'subscription.features.customServices',
- defaultMessage: '!!!Add your custom services',
+ id: 'subscription.features.onpremise.mattermost',
+ defaultMessage: '!!!Add on-premise/hosted services like Mattermost',
},
- encryptedSync: {
- id: 'subscription.features.encryptedSync',
- defaultMessage: '!!!Encrypted session synchronization',
+ noInterruptions: {
+ id: 'subscription.features.noInterruptions',
+ defaultMessage: '!!!No app delays & nagging to upgrade license',
},
vpn: {
id: 'subscription.features.vpn',
defaultMessage: '!!!Proxy & VPN support',
},
+ spellchecker: {
+ id: 'subscription.features.spellchecker',
+ defaultMessage: '!!!Support for Spellchecker',
+ },
ads: {
id: 'subscription.features.ads',
defaultMessage: '!!!No ads, ever!',
@@ -170,16 +166,10 @@ export default @observer class SubscriptionForm extends Component {
- {intl.formatMessage(messages.features.onpremise)}
-
- {intl.formatMessage(messages.features.encryptedSync)}
- {intl.formatMessage(messages.features.comingSoon)}
-
- -
- {intl.formatMessage(messages.features.customServices)}
- {intl.formatMessage(messages.features.comingSoon)}
+ {intl.formatMessage(messages.features.noInterruptions)}
-
- {intl.formatMessage(messages.features.vpn)}
- {intl.formatMessage(messages.features.comingSoon)}
+ {intl.formatMessage(messages.features.spellchecker)}
-
{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 @@
+import React, { Component } from 'react';
+import { inject, observer } from 'mobx-react';
+import PropTypes from 'prop-types';
+import { defineMessages, intlShape } from 'react-intl';
+import injectSheet from 'react-jss';
+
+import { oneOrManyChildElements } from '../../../prop-types';
+
+import UserStore from '../../../stores/UserStore';
+
+import styles from './styles';
+
+const messages = defineMessages({
+ action: {
+ id: 'premiumFeature.button.upgradeAccount',
+ defaultMessage: '!!!Upgrade account',
+ },
+});
+
+export default @inject('stores', 'actions') @injectSheet(styles) @observer class PremiumFeatureContainer extends Component {
+ static propTypes = {
+ classes: PropTypes.object.isRequired,
+ condition: PropTypes.bool,
+ };
+
+ static defaultProps = {
+ condition: true,
+ };
+
+ static contextTypes = {
+ intl: intlShape,
+ };
+
+ render() {
+ const {
+ classes,
+ children,
+ actions,
+ condition,
+ stores,
+ } = this.props;
+
+ const { intl } = this.context;
+
+ return !stores.user.data.isPremium && !!condition ? (
+
+
+
Premium Feature
+
+
+
+ {children}
+
+
+ ) : children;
+ }
+}
+
+PremiumFeatureContainer.wrappedComponent.propTypes = {
+ children: oneOrManyChildElements.isRequired,
+ stores: PropTypes.shape({
+ user: PropTypes.instanceOf(UserStore).isRequired,
+ }).isRequired,
+ actions: PropTypes.shape({
+ ui: PropTypes.shape({
+ openSettings: PropTypes.func.isRequired,
+ }).isRequired,
+ }).isRequired,
+};
+
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 @@
+export default theme => ({
+ container: {
+ background: theme.colorSubscriptionContainerBackground,
+ border: theme.colorSubscriptionContainerBorder,
+ margin: [0, 0, 20, -20],
+ padding: 20,
+ 'border-radius': theme.borderRadius,
+ },
+ titleContainer: {
+ display: 'flex',
+ },
+ title: {
+ 'font-weight': 'bold',
+ color: theme.colorSubscriptionContainerTitle,
+ },
+ actionButton: {
+ background: theme.colorSubscriptionContainerActionButtonBackground,
+ color: theme.colorSubscriptionContainerActionButtonColor,
+ 'margin-left': 'auto',
+ 'border-radius': theme.borderRadiusSmall,
+ padding: [2, 4],
+ 'font-size': 12,
+ },
+ content: {
+ opacity: 0.5,
+ 'margin-top': 20,
+ '& :last-child': {
+ 'margin-bottom': 0,
+ },
+ },
+});
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 {
field: PropTypes.instanceOf(Field).isRequired,
className: PropTypes.string,
showLabel: PropTypes.bool,
+ disabled: PropTypes.bool,
};
static defaultProps = {
className: '',
showLabel: true,
+ disabled: false,
};
onChange(e) {
@@ -27,6 +29,7 @@ export default @observer class Toggle extends Component {
field,
className,
showLabel,
+ disabled,
} = this.props;
if (field.value === '' && field.default !== '') {
@@ -38,6 +41,7 @@ export default @observer class Toggle extends Component {
className={classnames([
'franz-form__field',
'franz-form__toggle-wrapper',
+ 'franz-form__toggle-disabled',
className,
])}
>
@@ -55,7 +59,7 @@ export default @observer class Toggle extends Component {
name={field.name}
value={field.name}
checked={field.value}
- onChange={e => this.onChange(e)}
+ onChange={e => (!disabled ? this.onChange(e) : null)}
/>
{field.error && {field.error}
}
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 = {
beta: false,
isAppMuted: false,
enableGPUAcceleration: true,
+ serviceLimit: 5,
};
export const FRANZ_SERVICE_REQUEST = 'https://bit.ly/franz-service-request';
export const FRANZ_TRANSLATION = 'https://bit.ly/franz-translate';
-export const SETTINGS_PATH = path.join(app.getPath('userData'), 'config', 'settings.json');
+export const FILE_SYSTEM_SETTINGS_TYPES = [
+ 'app',
+ 'proxy',
+];
+
+export 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';
import UserStore from '../../stores/UserStore';
import RecipesStore from '../../stores/RecipesStore';
import ServicesStore from '../../stores/ServicesStore';
-import FeaturesStore from '../../stores/FeaturesStore';
import SettingsStore from '../../stores/SettingsStore';
+import FeaturesStore from '../../stores/FeaturesStore';
import Form from '../../lib/Form';
import { gaPage } from '../../lib/analytics';
@@ -15,6 +15,8 @@ import ServiceError from '../../components/settings/services/ServiceError';
import EditServiceForm from '../../components/settings/services/EditServiceForm';
import { required, url, oneRequired } from '../../helpers/validation-helpers';
+import { config as proxyFeature } from '../../features/serviceProxy';
+
const messages = defineMessages({
name: {
id: 'settings.service.form.name',
@@ -56,6 +58,22 @@ const messages = defineMessages({
id: 'settings.service.form.enableDarkMode',
defaultMessage: '!!!Enable Dark Mode',
},
+ enableProxy: {
+ id: 'settings.service.form.proxy.isEnabled',
+ defaultMessage: '!!!Use Proxy',
+ },
+ proxyHost: {
+ id: 'settings.service.form.proxy.host',
+ defaultMessage: '!!!Proxy Host/IP',
+ },
+ proxyUser: {
+ id: 'settings.service.form.proxy.user',
+ defaultMessage: '!!!User',
+ },
+ proxyPassword: {
+ id: 'settings.service.form.proxy.password',
+ defaultMessage: '!!!Password',
+ },
});
export default @inject('stores', 'actions') @observer class EditServiceScreen extends Component {
@@ -82,7 +100,7 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
}
}
- prepareForm(recipe, service, userCanManageServices) {
+ prepareForm(recipe, service, proxy) {
const { intl } = this.context;
const config = {
fields: {
@@ -128,7 +146,6 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
if (recipe.hasTeamId) {
Object.assign(config.fields, {
team: {
- disabled: !userCanManageServices,
label: intl.formatMessage(messages.team),
placeholder: intl.formatMessage(messages.team),
value: service.team,
@@ -140,7 +157,6 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
if (recipe.hasCustomUrl) {
Object.assign(config.fields, {
customUrl: {
- disabled: !userCanManageServices,
label: intl.formatMessage(messages.customUrl),
placeholder: 'https://',
value: service.customUrl,
@@ -175,6 +191,40 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
});
}
+ if (proxy.isEnabled) {
+ const serviceProxyConfig = this.props.stores.settings.proxy[service.id] || {};
+
+ Object.assign(config.fields, {
+ proxy: {
+ name: 'proxy',
+ label: 'proxy',
+ fields: {
+ isEnabled: {
+ label: intl.formatMessage(messages.enableProxy),
+ value: serviceProxyConfig.isEnabled,
+ default: false,
+ },
+ host: {
+ label: intl.formatMessage(messages.proxyHost),
+ value: serviceProxyConfig.host,
+ default: '',
+ },
+ user: {
+ label: intl.formatMessage(messages.proxyUser),
+ value: serviceProxyConfig.user,
+ default: '',
+ },
+ password: {
+ label: intl.formatMessage(messages.proxyPassword),
+ value: serviceProxyConfig.password,
+ default: '',
+ type: 'password',
+ },
+ },
+ },
+ });
+ }
+
return new Form(config);
}
@@ -192,7 +242,7 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
}
render() {
- const { recipes, services, user, features } = this.props.stores;
+ const { recipes, services, user } = this.props.stores;
const { action } = this.props.router.params;
let recipe;
@@ -227,8 +277,7 @@ export default @inject('stores', 'actions') @observer class EditServiceScreen ex
);
}
- const userCanManageServices = features.features.userCanManageServices;
- const form = this.prepareForm(recipe, service, userCanManageServices);
+ const form = this.prepareForm(recipe, service, proxyFeature);
return (
this.onSubmit(d)}
onDelete={() => this.deleteService()}
+ isProxyFeatureEnabled={proxyFeature.isEnabled}
+ isProxyFeaturePremiumFeature={proxyFeature.isPremium}
/>
);
}
@@ -253,8 +303,8 @@ EditServiceScreen.wrappedComponent.propTypes = {
user: PropTypes.instanceOf(UserStore).isRequired,
recipes: PropTypes.instanceOf(RecipesStore).isRequired,
services: PropTypes.instanceOf(ServicesStore).isRequired,
- features: PropTypes.instanceOf(FeaturesStore).isRequired,
settings: PropTypes.instanceOf(SettingsStore).isRequired,
+ features: PropTypes.instanceOf(FeaturesStore).isRequired,
}).isRequired,
router: PropTypes.shape({
params: PropTypes.shape({
@@ -267,5 +317,8 @@ EditServiceScreen.wrappedComponent.propTypes = {
updateService: PropTypes.func.isRequired,
deleteService: PropTypes.func.isRequired,
}).isRequired,
+ // settings: PropTypes.shape({
+ // update: PropTypes.func.isRequred,
+ // }).isRequired,
}).isRequired,
};
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';
import { APP_LOCALES } from '../../i18n/languages';
import { gaPage } from '../../lib/analytics';
import { DEFAULT_APP_SETTINGS } from '../../config';
+import { config as spellcheckerConfig } from '../../features/spellchecker';
import EditSettingsForm from '../../components/settings/settings/EditSettingsForm';
@@ -161,8 +162,8 @@ export default @inject('stores', 'actions') @observer class EditSettingsScreen e
},
enableSpellchecking: {
label: intl.formatMessage(messages.enableSpellchecking),
- value: settings.all.app.enableSpellchecking,
- default: DEFAULT_APP_SETTINGS.enableSpellchecking,
+ value: !this.props.stores.user.data.isPremium && spellcheckerConfig.isPremiumFeature ? false : settings.all.app.enableSpellchecking,
+ default: !this.props.stores.user.data.isPremium && spellcheckerConfig.isPremiumFeature ? false : DEFAULT_APP_SETTINGS.enableSpellchecking,
},
darkMode: {
label: intl.formatMessage(messages.darkMode),
@@ -218,6 +219,7 @@ export default @inject('stores', 'actions') @observer class EditSettingsScreen e
cacheSize={cacheSize}
isClearingAllCache={isClearingAllCache}
onClearAllCache={clearAllCache}
+ isSpellcheckerPremiumFeature={spellcheckerConfig.isPremiumFeature}
/>
);
}
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
return (
stores.payment.plansRequest.reload()}
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 @@
import { observable, toJS } from 'mobx';
import { pathExistsSync, outputJsonSync, readJsonSync } from 'fs-extra';
+import path from 'path';
-import { SETTINGS_PATH, DEFAULT_APP_SETTINGS } from '../config';
+import { SETTINGS_PATH } from '../config';
const debug = require('debug')('Franz:Settings');
export default class Settings {
- @observable store = DEFAULT_APP_SETTINGS;
+ type = '';
+ @observable store = {};
- constructor() {
- if (!pathExistsSync(SETTINGS_PATH)) {
+ constructor(type, defaultState = {}) {
+ this.type = type;
+ this.store = defaultState;
+ this.defaultState = defaultState;
+
+ if (!pathExistsSync(this.settingsFile)) {
this._writeFile();
} else {
this._hydrate();
@@ -31,16 +37,20 @@ export default class Settings {
}
_merge(settings) {
- return Object.assign(DEFAULT_APP_SETTINGS, this.store, settings);
+ return Object.assign(this.defaultState, this.store, settings);
}
_hydrate() {
- this.store = this._merge(readJsonSync(SETTINGS_PATH));
+ this.store = this._merge(readJsonSync(this.settingsFile));
debug('Hydrate store', toJS(this.store));
}
_writeFile() {
- outputJsonSync(SETTINGS_PATH, this.store);
+ outputJsonSync(this.settingsFile, this.store);
debug('Write settings file', toJS(this.store));
}
+
+ get settingsFile() {
+ return path.join(SETTINGS_PATH, `${this.type === 'app' ? 'settings' : this.type}.json`);
+ }
}
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) {
export default (params) => {
autorun(() => {
- isTrayIconEnabled = params.settings.get('enableSystemTray');
+ isTrayIconEnabled = params.settings.app.get('enableSystemTray');
if (!isTrayIconEnabled) {
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 @@
import { ipcMain } from 'electron';
export default (params) => {
- ipcMain.on('getAppSettings', () => {
- params.mainWindow.webContents.send('appSettings', params.settings.all);
+ ipcMain.on('getAppSettings', (event, type) => {
+ console.log('getAppSettings', type, params.settings[type].all);
+ params.mainWindow.webContents.send('appSettings', {
+ type,
+ data: params.settings[type].all,
+ });
});
ipcMain.on('updateAppSettings', (event, args) => {
- params.settings.set(args);
+ params.settings[args.type].set(args.data);
});
};
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({
},
});
-export default @inject('actions') @observer @injectSheet(styles) class DelayApp extends Component {
+export default @inject('actions') @injectSheet(styles) @observer class DelayApp extends Component {
static propTypes = {
// eslint-disable-next-line
classes: PropTypes.object.isRequired,
@@ -39,8 +39,6 @@ export default @inject('actions') @observer @injectSheet(styles) class DelayApp
}
componentDidMount() {
- // const { reload } = this.props;
-
this.countdownInterval = setInterval(() => {
this.setState({
countdown: this.state.countdown - this.countdownIntervalTimeout,
@@ -53,6 +51,10 @@ export default @inject('actions') @observer @injectSheet(styles) class DelayApp
}, this.countdownIntervalTimeout);
}
+ componentWillUnmount() {
+ clearInterval(this.countdownInterval);
+ }
+
countdownInterval = null;
countdownIntervalTimeout = 1000;
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) {
let shownAfterLaunch = false;
let timeLastDelay = moment();
- config.delayOffset = globalConfig.delayOffset || DEFAULT_DELAY_OFFSET;
- config.delayDuration = globalConfig.wait || DEFAULT_DELAY_DURATION;
+ config.delayOffset = globalConfig.delayOffset !== undefined ? globalConfig.delayOffset : DEFAULT_DELAY_OFFSET;
+ config.delayDuration = globalConfig.wait !== undefined ? globalConfig.wait : DEFAULT_DELAY_DURATION;
autorun(() => {
const diff = moment().diff(timeLastDelay);
@@ -63,5 +63,5 @@ export default function init(stores) {
);
}
-export const component = DelayAppComponent;
+export const Component = DelayAppComponent;
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 @@
-export default (theme) => {
- console.log(theme);
- return ({
- container: {
- background: theme.colorBackground,
- position: 'absolute',
- top: 0,
- width: '100%',
- display: 'flex',
- 'flex-direction': 'column',
- 'align-items': 'center',
- 'justify-content': 'center',
- 'z-index': 150,
- },
- headline: {
- color: theme.colorHeadline,
- margin: [25, 0, 40],
- 'max-width': 500,
- 'text-align': 'center',
- 'line-height': '1.3em',
- },
- button: {
- margin: [40, 0, 20],
- },
- });
-};
+export default theme => ({
+ container: {
+ background: theme.colorBackground,
+ position: 'absolute',
+ top: 0,
+ width: '100%',
+ display: 'flex',
+ 'flex-direction': 'column',
+ 'align-items': 'center',
+ 'justify-content': 'center',
+ 'z-index': 150,
+ },
+ headline: {
+ color: theme.colorHeadline,
+ margin: [25, 0, 40],
+ 'max-width': 500,
+ 'text-align': 'center',
+ 'line-height': '1.3em',
+ },
+ button: {
+ margin: [40, 0, 20],
+ },
+});
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 @@
+import { autorun, reaction, observable } from 'mobx';
+import { remote } from 'electron';
+
+const { session } = remote;
+
+const debug = require('debug')('Franz:feature:serviceProxy');
+
+const DEFAULT_ENABLED = false;
+const DEFAULT_IS_PREMIUM = true;
+
+export const config = observable({
+ isEnabled: DEFAULT_ENABLED,
+ isPremium: DEFAULT_IS_PREMIUM,
+});
+
+export default function init(stores) {
+ reaction(
+ () => stores.features.features.isServiceProxyEnabled,
+ (enabled, r) => {
+ if (enabled) {
+ debug('Initializing `serviceProxy` feature');
+
+ // Dispose the reaction to run this only once
+ r.dispose();
+
+ const { isServiceProxyEnabled, isServiceProxyPremiumFeature } = stores.features.features;
+
+ config.isEnabled = isServiceProxyEnabled !== undefined ? isServiceProxyEnabled : DEFAULT_ENABLED;
+ config.isPremium = isServiceProxyPremiumFeature !== undefined ? isServiceProxyPremiumFeature : DEFAULT_IS_PREMIUM;
+
+ autorun(() => {
+ const services = stores.services.all;
+ const isPremiumUser = stores.user.isPremium;
+
+ if (config.isPremium && !isPremiumUser) return;
+
+ services.forEach((service) => {
+ const s = session.fromPartition(`persist:service-${service.id}`);
+ let proxyHost = 'direct://';
+
+ const serviceProxyConfig = stores.settings.proxy[service.id];
+
+ if (serviceProxyConfig && serviceProxyConfig.isEnabled && serviceProxyConfig.host) {
+ proxyHost = serviceProxyConfig.host;
+ }
+
+ s.setProxy({ proxyRules: proxyHost }, (e) => {
+ debug(`Using proxy "${proxyHost}" for "${service.name}" (${service.id})`, e);
+ });
+ });
+ });
+ }
+ },
+ );
+}
+
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 @@
+import { autorun, reaction } from 'mobx';
+
+const debug = require('debug')('Franz:feature:spellchecker');
+
+const DEFAULT_IS_PREMIUM_FEATURE = true;
+
+export const config = {
+ isPremiumFeature: DEFAULT_IS_PREMIUM_FEATURE,
+};
+
+export default function init(stores) {
+ reaction(
+ () => stores.features.features.isSpellcheckerPremiumFeature,
+ (enabled, r) => {
+ if (enabled) {
+ debug('Initializing `spellchecker` feature');
+
+ // Dispose the reaction to run this only once
+ r.dispose();
+
+ const { isSpellcheckerPremiumFeature } = stores.features.features;
+
+ config.isPremiumFeature = isSpellcheckerPremiumFeature !== undefined ? isSpellcheckerPremiumFeature : DEFAULT_IS_PREMIUM_FEATURE;
+
+ autorun(() => {
+ if (!stores.user.data.isPremium && config.isPremiumFeature) {
+ debug('Override settings.spellcheckerEnabled flag to false');
+
+ Object.assign(stores.settings.all.app, {
+ enableSpellchecker: false,
+ });
+ }
+ });
+ }
+ },
+ );
+}
+
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 @@
+export default (theme) => {
+ console.log(theme);
+ return ({
+ container: {
+ background: theme.colorBackground,
+ position: 'absolute',
+ top: 0,
+ width: '100%',
+ display: 'flex',
+ 'flex-direction': 'column',
+ 'align-items': 'center',
+ 'justify-content': 'center',
+ 'z-index': 150,
+ },
+ headline: {
+ color: theme.colorHeadline,
+ margin: [25, 0, 40],
+ 'max-width': 500,
+ 'text-align': 'center',
+ 'line-height': '1.3em',
+ },
+ button: {
+ margin: [40, 0, 20],
+ },
+ });
+};
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 @@
"subscription.paymentSessionError": "Could not initialize payment form",
"subscription.includedFeatures": "Paid Franz Premium Supporter Account includes",
"subscription.features.onpremise": "Add on-premise/hosted services like HipChat",
- "subscription.features.customServices": "Private services for you and your team",
+ "subscription.features.onpremise.mattermost": "Add on-premise/hosted services like Mattermost",
"subscription.features.encryptedSync": "Encrypted session synchronization",
"subscription.features.vpn": "Proxy & VPN support",
"subscription.features.ads": "No ads, ever!",
+ "subscription.features.spellchecker": "Support for spellchecker",
+ "subscription.features.noInterruptions": "No app delays & nagging to upgrade license",
"subscription.features.comingSoon": "coming soon",
"infobar.servicesUpdated": "Your services have been updated.",
"infobar.updateAvailable": "A new update for Franz is available.",
@@ -131,6 +133,12 @@
"settings.service.form.iconDelete": "Delete",
"settings.service.form.iconUpload": "Drop your image, or click here",
"settings.service.form.enableDarkMode": "Enable Dark Mode",
+ "settings.service.form.proxy.headline": "Proxy Settings",
+ "settings.service.form.proxy.isEnabled": "Use Proxy",
+ "settings.service.form.proxy.host": "Proxy Host/IP",
+ "settings.service.form.proxy.user": "User (optional)",
+ "settings.service.form.proxy.password": "Password (optional)",
+ "settings.service.form.proxy.info": "Proxy settings will not synced with the Franz servers.",
"settings.service.error.headline": "Error",
"settings.service.error.goBack": "Back to services",
"settings.service.error.message": "Could not load service recipe.",
@@ -252,5 +260,6 @@
"validation.minLength": "{field} should be at least {length} characters long",
"feature.delayApp.headline": "Please purchase a Franz Supporter License to skip waiting",
"feature.delayApp.action": "Get a Franz Supporter License",
- "feature.delayApp.text": "Franz will continue in {seconds} seconds."
+ "feature.delayApp.text": "Franz will continue in {seconds} seconds.",
+ "premiumFeature.button.upgradeAccount": "Upgrade account"
}
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 @@
-import { app, BrowserWindow, shell } from 'electron';
+import { app, BrowserWindow, shell, ipcMain } from 'electron';
import fs from 'fs-extra';
import path from 'path';
@@ -12,6 +12,8 @@ import handleDeepLink from './electron/deepLinking';
import { appId } from './package.json'; // eslint-disable-line import/no-unresolved
import './electron/exception';
+import { DEFAULT_APP_SETTINGS } from './config';
+
const debug = require('debug')('Franz:App');
// 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
}
// Initialize Settings
-const settings = new Settings();
+const settings = new Settings('app', DEFAULT_APP_SETTINGS);
+const proxySettings = new Settings('proxy');
// Disable GPU acceleration
if (!settings.get('enableGPUAcceleration')) {
@@ -94,7 +97,14 @@ const createWindow = () => {
const trayIcon = new Tray();
// Initialize ipcApi
- ipcApi({ mainWindow, settings, trayIcon });
+ ipcApi({
+ mainWindow,
+ settings: {
+ app: settings,
+ proxy: proxySettings,
+ },
+ trayIcon,
+ });
// Manage Window State
mainWindowState.manage(mainWindow);
@@ -177,6 +187,24 @@ const createWindow = () => {
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
+// This is the worst possible implementation as the webview.webContents based callback doesn't work 🖕
+app.on('login', (event, webContents, request, authInfo, callback) => {
+ event.preventDefault();
+ debug('browser login event', authInfo);
+ if (authInfo.isProxy && authInfo.scheme === 'basic') {
+ webContents.send('get-service-id');
+
+ ipcMain.on('service-id', (e, id) => {
+ debug('Received service id', id);
+
+ const ps = proxySettings.get(id);
+ callback(ps.user, ps.password);
+ });
+ } else {
+ // TODO: implement basic auth
+ }
+});
+
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// 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 {
this.hasCustomUploadedIcon = data.hasCustomIcon !== undefined ? data.hasCustomIcon : this.hasCustomUploadedIcon;
+ this.proxy = data.proxy !== undefined ? data.proxy : this.proxy;
+
this.recipe = recipe;
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 @@
-import { observable, extendObservable } from 'mobx';
-import { DEFAULT_APP_SETTINGS } from '../config';
-
-export default class Settings {
- @observable app = DEFAULT_APP_SETTINGS
-
- @observable service = {
- activeService: '',
- }
-
- @observable group = {
- collapsed: [],
- disabled: [],
- }
-
- @observable stats = {
- appStarts: 0,
- }
-
- @observable migration = {}
-
- constructor({ app, service, group, stats, migration }) {
- Object.assign(this.app, app);
- Object.assign(this.service, service);
- Object.assign(this.group, group);
- Object.assign(this.stats, stats);
- Object.assign(this.migration, migration);
- }
-
- update(data) {
- extendObservable(this, data);
- }
-}
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';
import CachedRequest from './lib/CachedRequest';
import delayApp from '../features/delayApp';
+import spellchecker from '../features/spellchecker';
+import serviceProxy from '../features/serviceProxy';
export default class FeaturesStore extends Store {
@observable defaultFeaturesRequest = new CachedRequest(this.api.features, 'default');
@@ -36,5 +38,7 @@ export default class FeaturesStore extends Store {
_enableFeatures() {
delayApp(this.stores, this.actions);
+ spellchecker(this.stores, this.actions);
+ serviceProxy(this.stores, this.actions);
}
}
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 {
// Actions
@action async _createService({ recipeId, serviceData, redirect = true }) {
const data = this._cleanUpTeamIdAndCustomUrl(recipeId, serviceData);
+
const response = await this.createServiceRequest.execute(recipeId, data)._promise;
this.allServicesRequest.patch((result) => {
@@ -150,6 +151,13 @@ export default class ServicesStore extends Store {
result.push(response.data);
});
+ this.actions.settings.update({
+ type: 'proxy',
+ data: {
+ [`${response.data.id}`]: data.proxy,
+ },
+ });
+
this.actionStatus = response.status || [];
if (redirect) {
@@ -222,6 +230,13 @@ export default class ServicesStore extends Store {
});
}
+ this.actions.settings.update({
+ type: 'proxy',
+ data: {
+ [`${serviceId}`]: data.proxy,
+ },
+ });
+
if (redirect) {
this.stores.router.push('/settings/services');
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 @@
-import { remote } from 'electron';
+import { remote, ipcRenderer } from 'electron';
import { action, computed, observable } from 'mobx';
import localStorage from 'mobx-localstorage';
import Store from './lib/Store';
-import SettingsModel from '../models/Settings';
import Request from './lib/Request';
import CachedRequest from './lib/CachedRequest';
+import { DEFAULT_APP_SETTINGS, FILE_SYSTEM_SETTINGS_TYPES } from '../config';
+
const { systemPreferences } = remote;
const debug = require('debug')('Franz:SettingsStore');
@@ -14,12 +15,35 @@ export default class SettingsStore extends Store {
@observable appSettingsRequest = new CachedRequest(this.api.local, 'getAppSettings');
@observable updateAppSettingsRequest = new Request(this.api.local, 'updateAppSettings');
+ @observable fileSystemSettingsRequests = [];
+
+ fileSystemSettingsTypes = FILE_SYSTEM_SETTINGS_TYPES;
+ @observable _fileSystemSettingsCache = {
+ app: DEFAULT_APP_SETTINGS,
+ proxy: {},
+ };
+
constructor(...args) {
super(...args);
// Register action handlers
this.actions.settings.update.listen(this._update.bind(this));
this.actions.settings.remove.listen(this._remove.bind(this));
+
+ this.fileSystemSettingsTypes.forEach((type) => {
+ this.fileSystemSettingsRequests[type] = new CachedRequest(this.api.local, 'getAppSettings');
+ });
+
+ ipcRenderer.on('appSettings', (event, resp) => {
+ debug('Get appSettings resolves', resp, resp.type, resp.data);
+
+ this._fileSystemSettingsCache[resp.type] = resp.data;
+ });
+
+ this.fileSystemSettingsTypes.forEach((type) => {
+ console.log(type);
+ ipcRenderer.send('getAppSettings', type);
+ });
}
async setup() {
@@ -28,29 +52,53 @@ export default class SettingsStore extends Store {
await this._migrate();
}
+ @computed get app() {
+ return this._fileSystemSettingsCache.app || DEFAULT_APP_SETTINGS;
+ }
+
+ @computed get proxy() {
+ return this._fileSystemSettingsCache.proxy || {};
+ }
+
+ @computed get service() {
+ return localStorage.getItem('service') || {
+ activeService: '',
+ };
+ }
+
+ @computed get stats() {
+ return localStorage.getItem('stats') || {
+ activeService: '',
+ };
+ }
+
+ @computed get migration() {
+ return localStorage.getItem('migration') || {};
+ }
+
@computed get all() {
- return new SettingsModel({
- app: this.appSettingsRequest.execute().result || {},
- service: localStorage.getItem('service') || {},
- group: localStorage.getItem('group') || {},
- stats: localStorage.getItem('stats') || {},
- migration: localStorage.getItem('migration') || {},
- });
+ return {
+ app: this.app,
+ proxy: this.proxy,
+ service: this.service,
+ stats: this.stats,
+ migration: this.migration,
+ };
}
@action async _update({ type, data }) {
const appSettings = this.all;
- if (type !== 'app') {
+ if (!this.fileSystemSettingsTypes.includes(type)) {
debug('Update settings', type, data, this.all);
localStorage.setItem(type, Object.assign(appSettings[type], data));
} else {
debug('Update settings on file system', type, data);
- this.updateAppSettingsRequest.execute(data);
-
- this.appSettingsRequest.patch((result) => {
- if (!result) return;
- Object.assign(result, data);
+ ipcRenderer.send('updateAppSettings', {
+ type,
+ data,
});
+
+ Object.assign(this._fileSystemSettingsCache[type], data);
}
}
@@ -128,4 +176,8 @@ export default class SettingsStore extends Store {
debug('Set up dark mode');
}
}
+
+ _getFileBasedSettings(type) {
+ ipcRenderer.send('getAppSettings', type);
+ }
}
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 @@
margin: 25px 0 15px;
&:first-of-type { margin-top: 0; }
+
+ .badge {
+ font-weight: normal;
+ margin-left: 10px;
+ }
}
}
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 @@
import * as legacyStyles from '../default/legacy';
export const colorBackground = legacyStyles.darkThemeGrayDarkest;
+export const colorBackgroundSubscriptionContainer = legacyStyles.themeBrandInfo;
export 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 @@
import * as legacyStyles from './legacy';
-/* legacy config, injected into sass */
-export const themeBrandPrimary = '#3498db';
-export const themeBrandSuccess = '#5cb85c';
-export const themeBrandInfo = '#5bc0de';
-export const themeBrandWarning = '#FF9F00';
-export const themeBrandDanger = '#d9534f';
+export const brandPrimary = '#3498db';
+export const brandSuccess = '#5cb85c';
+export const brandInfo = '#5bc0de';
+export const brandWarning = '#FF9F00';
+export const brandDanger = '#d9534f';
-export const colorBackground = legacyStyles.themeGrayLighter;
+export const borderRadius = legacyStyles.themeBorderRadius;
+export const borderRadiusSmall = legacyStyles.themeBorderRadiusSmall;
+export const colorBackground = legacyStyles.themeGrayLighter;
export const colorHeadline = legacyStyles.themeGrayDark;
+
+// Subscription Container Component
+export const colorSubscriptionContainerBackground = 'none';
+export const colorSubscriptionContainerBorder = [1, 'solid', brandPrimary];
+export const colorSubscriptionContainerTitle = brandPrimary;
+export const colorSubscriptionContainerActionButtonBackground = brandPrimary;
+export const colorSubscriptionContainerActionButtonColor = '#FFF';
+
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) => {
}
});
-// initSpellchecker
+// Needed for current implementation of electrons 'login' event
+ipcRenderer.on('get-service-id', (event) => {
+ debug('Asking for service id', event);
+
+ event.sender.send('service-id', serviceData.id);
+});
+
document.addEventListener('DOMContentLoaded', () => {
ipcRenderer.sendToHost('hello');