aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/actions/user.js1
-rw-r--r--src/api/UserApi.js4
-rw-r--r--src/api/server/LocalApi.js4
-rw-r--r--src/api/server/ServerApi.js28
-rw-r--r--src/components/auth/Welcome.js12
-rw-r--r--src/components/services/content/ServiceWebview.js1
-rw-r--r--src/components/settings/account/AccountDashboard.js49
-rw-r--r--src/components/settings/settings/EditSettingsForm.js5
-rw-r--r--src/components/ui/Button.js2
-rw-r--r--src/config.js3
-rw-r--r--src/containers/settings/AccountScreen.js5
-rw-r--r--src/electron/deepLinking.js5
-rw-r--r--src/helpers/async-helpers.js5
-rw-r--r--src/i18n/languages.js1
-rw-r--r--src/i18n/locales/en-US.json4
-rw-r--r--src/index.js26
-rw-r--r--src/lib/Menu.js2
-rw-r--r--src/models/Settings.js6
-rw-r--r--src/stores/AppStore.js23
-rw-r--r--src/stores/SettingsStore.js2
-rw-r--r--src/stores/UserStore.js6
-rw-r--r--src/styles/services.scss1
-rw-r--r--src/styles/settings.scss4
-rw-r--r--src/styles/welcome.scss25
-rw-r--r--src/webview/lib/RecipeWebview.js4
-rw-r--r--src/webview/plugin.js28
-rw-r--r--src/webview/spellchecker.js65
27 files changed, 259 insertions, 62 deletions
diff --git a/src/actions/user.js b/src/actions/user.js
index fe32b8a05..ccf1fa56a 100644
--- a/src/actions/user.js
+++ b/src/actions/user.js
@@ -27,4 +27,5 @@ export default {
27 importLegacyServices: PropTypes.arrayOf(PropTypes.shape({ 27 importLegacyServices: PropTypes.arrayOf(PropTypes.shape({
28 recipe: PropTypes.string.isRequired, 28 recipe: PropTypes.string.isRequired,
29 })).isRequired, 29 })).isRequired,
30 delete: {},
30}; 31};
diff --git a/src/api/UserApi.js b/src/api/UserApi.js
index e8fd75bed..edfb88988 100644
--- a/src/api/UserApi.js
+++ b/src/api/UserApi.js
@@ -46,4 +46,8 @@ export default class UserApi {
46 getLegacyServices() { 46 getLegacyServices() {
47 return this.server.getLegacyServices(); 47 return this.server.getLegacyServices();
48 } 48 }
49
50 delete() {
51 return this.server.deleteAccount();
52 }
49} 53}
diff --git a/src/api/server/LocalApi.js b/src/api/server/LocalApi.js
index eba236f16..79ac6e12f 100644
--- a/src/api/server/LocalApi.js
+++ b/src/api/server/LocalApi.js
@@ -1,5 +1,3 @@
1import SettingsModel from '../../models/Settings';
2
3export default class LocalApi { 1export default class LocalApi {
4 // App 2 // App
5 async updateAppSettings(data) { 3 async updateAppSettings(data) {
@@ -15,7 +13,7 @@ export default class LocalApi {
15 async getAppSettings() { 13 async getAppSettings() {
16 const settingsString = localStorage.getItem('app'); 14 const settingsString = localStorage.getItem('app');
17 try { 15 try {
18 const settings = new SettingsModel(JSON.parse(settingsString) || {}); 16 const settings = JSON.parse(settingsString) || {};
19 console.debug('LocalApi::getAppSettings resolves', settings); 17 console.debug('LocalApi::getAppSettings resolves', settings);
20 18
21 return settings; 19 return settings;
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js
index f25f02eaa..8b3136d27 100644
--- a/src/api/server/ServerApi.js
+++ b/src/api/server/ServerApi.js
@@ -12,6 +12,8 @@ import NewsModel from '../../models/News';
12import UserModel from '../../models/User'; 12import UserModel from '../../models/User';
13import OrderModel from '../../models/Order'; 13import OrderModel from '../../models/Order';
14 14
15import { sleep } from '../../helpers/async-helpers';
16
15import { API } from '../../environment'; 17import { API } from '../../environment';
16 18
17import { 19import {
@@ -125,6 +127,19 @@ export default class ServerApi {
125 return user; 127 return user;
126 } 128 }
127 129
130 async deleteAccount() {
131 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me`, this._prepareAuthRequest({
132 method: 'DELETE',
133 }));
134 if (!request.ok) {
135 throw request;
136 }
137 const data = await request.json();
138
139 console.debug('ServerApi::deleteAccount resolves', data);
140 return data;
141 }
142
128 // Services 143 // Services
129 async getServices() { 144 async getServices() {
130 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/services`, this._prepareAuthRequest({ 145 const request = await window.fetch(`${SERVER_URL}/${API_VERSION}/me/services`, this._prepareAuthRequest({
@@ -290,18 +305,25 @@ export default class ServerApi {
290 305
291 fs.ensureDirSync(recipeTempDirectory); 306 fs.ensureDirSync(recipeTempDirectory);
292 const res = await fetch(packageUrl); 307 const res = await fetch(packageUrl);
308 console.debug('Recipe downloaded', recipeId);
293 const buffer = await res.buffer(); 309 const buffer = await res.buffer();
294 fs.writeFileSync(archivePath, buffer); 310 fs.writeFileSync(archivePath, buffer);
295 311
296 tar.x({ 312 await sleep(10);
313
314 await tar.x({
297 file: archivePath, 315 file: archivePath,
298 cwd: recipeTempDirectory, 316 cwd: recipeTempDirectory,
299 sync: true, 317 preservePaths: true,
318 unlink: true,
319 preserveOwner: false,
320 onwarn: x => console.log('warn', recipeId, x),
300 }); 321 });
301 322
323 await sleep(10);
324
302 const { id } = fs.readJsonSync(path.join(recipeTempDirectory, 'package.json')); 325 const { id } = fs.readJsonSync(path.join(recipeTempDirectory, 'package.json'));
303 const recipeDirectory = path.join(recipesDirectory, id); 326 const recipeDirectory = path.join(recipesDirectory, id);
304
305 fs.copySync(recipeTempDirectory, recipeDirectory); 327 fs.copySync(recipeTempDirectory, recipeDirectory);
306 fs.remove(recipeTempDirectory); 328 fs.remove(recipeTempDirectory);
307 fs.remove(path.join(recipesDirectory, recipeId, 'recipe.tar.gz')); 329 fs.remove(path.join(recipesDirectory, recipeId, 'recipe.tar.gz'));
diff --git a/src/components/auth/Welcome.js b/src/components/auth/Welcome.js
index 06b10ecfe..eb9fbb847 100644
--- a/src/components/auth/Welcome.js
+++ b/src/components/auth/Welcome.js
@@ -55,12 +55,16 @@ export default class Login extends Component {
55 </div> 55 </div>
56 <div className="welcome__featured-services"> 56 <div className="welcome__featured-services">
57 {recipes.map(recipe => ( 57 {recipes.map(recipe => (
58 <img 58 <div
59 key={recipe.id} 59 key={recipe.id}
60 src={recipe.icons.svg}
61 className="welcome__featured-service" 60 className="welcome__featured-service"
62 alt="" 61 >
63 /> 62 <img
63 key={recipe.id}
64 src={recipe.icons.svg}
65 alt=""
66 />
67 </div>
64 ))} 68 ))}
65 </div> 69 </div>
66 </div> 70 </div>
diff --git a/src/components/services/content/ServiceWebview.js b/src/components/services/content/ServiceWebview.js
index faa356d3d..842fde76d 100644
--- a/src/components/services/content/ServiceWebview.js
+++ b/src/components/services/content/ServiceWebview.js
@@ -65,6 +65,7 @@ export default class ServiceWebview extends Component {
65 65
66 const webviewClasses = classnames({ 66 const webviewClasses = classnames({
67 services__webview: true, 67 services__webview: true,
68 'services__webview-wrapper': true,
68 'is-active': service.isActive, 69 'is-active': service.isActive,
69 'services__webview--force-repaint': this.state.forceRepaint, 70 'services__webview--force-repaint': this.state.forceRepaint,
70 }); 71 });
diff --git a/src/components/settings/account/AccountDashboard.js b/src/components/settings/account/AccountDashboard.js
index 75dbdef49..89fa07800 100644
--- a/src/components/settings/account/AccountDashboard.js
+++ b/src/components/settings/account/AccountDashboard.js
@@ -28,6 +28,10 @@ const messages = defineMessages({
28 id: 'settings.account.headlineInvoices', 28 id: 'settings.account.headlineInvoices',
29 defaultMessage: '!!Invoices', 29 defaultMessage: '!!Invoices',
30 }, 30 },
31 headlineDangerZone: {
32 id: 'settings.account.headlineDangerZone',
33 defaultMessage: '!!Danger Zone',
34 },
31 manageSubscriptionButtonLabel: { 35 manageSubscriptionButtonLabel: {
32 id: 'settings.account.manageSubscription.label', 36 id: 'settings.account.manageSubscription.label',
33 defaultMessage: '!!!Manage your subscription', 37 defaultMessage: '!!!Manage your subscription',
@@ -72,6 +76,18 @@ const messages = defineMessages({
72 id: 'settings.account.mining.cancel', 76 id: 'settings.account.mining.cancel',
73 defaultMessage: '!!!Cancel mining', 77 defaultMessage: '!!!Cancel mining',
74 }, 78 },
79 deleteAccount: {
80 id: 'settings.account.deleteAccount',
81 defaultMessage: '!!!Delete account',
82 },
83 deleteInfo: {
84 id: 'settings.account.deleteInfo',
85 defaultMessage: '!!!If you don\'t need your Franz account any longer, you can delete your account and all related data here.',
86 },
87 deleteEmailSent: {
88 id: 'settings.account.deleteEmailSent',
89 defaultMessage: '!!!You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!',
90 },
75}); 91});
76 92
77@observer 93@observer
@@ -90,6 +106,9 @@ export default class AccountDashboard extends Component {
90 openExternalUrl: PropTypes.func.isRequired, 106 openExternalUrl: PropTypes.func.isRequired,
91 onCloseSubscriptionWindow: PropTypes.func.isRequired, 107 onCloseSubscriptionWindow: PropTypes.func.isRequired,
92 stopMiner: PropTypes.func.isRequired, 108 stopMiner: PropTypes.func.isRequired,
109 deleteAccount: PropTypes.func.isRequired,
110 isLoadingDeleteAccount: PropTypes.bool.isRequired,
111 isDeleteAccountSuccessful: PropTypes.bool.isRequired,
93 }; 112 };
94 113
95 static contextTypes = { 114 static contextTypes = {
@@ -111,6 +130,9 @@ export default class AccountDashboard extends Component {
111 retryUserInfoRequest, 130 retryUserInfoRequest,
112 onCloseSubscriptionWindow, 131 onCloseSubscriptionWindow,
113 stopMiner, 132 stopMiner,
133 deleteAccount,
134 isLoadingDeleteAccount,
135 isDeleteAccountSuccessful,
114 } = this.props; 136 } = this.props;
115 const { intl } = this.context; 137 const { intl } = this.context;
116 138
@@ -201,7 +223,7 @@ export default class AccountDashboard extends Component {
201 /> 223 />
202 </div> 224 </div>
203 </div> 225 </div>
204 <div className="account__box account__box--last"> 226 <div className="account__box">
205 <h2>{intl.formatMessage(messages.headlineInvoices)}</h2> 227 <h2>{intl.formatMessage(messages.headlineInvoices)}</h2>
206 <table className="invoices"> 228 <table className="invoices">
207 <tbody> 229 <tbody>
@@ -230,7 +252,7 @@ export default class AccountDashboard extends Component {
230 252
231 {user.isMiner && ( 253 {user.isMiner && (
232 <div className="account franz-form"> 254 <div className="account franz-form">
233 <div className="account__box"> 255 <div className="account__box account__box--last">
234 <h2>{intl.formatMessage(messages.headlineSubscription)}</h2> 256 <h2>{intl.formatMessage(messages.headlineSubscription)}</h2>
235 <div className="account__subscription"> 257 <div className="account__subscription">
236 <div> 258 <div>
@@ -267,7 +289,7 @@ export default class AccountDashboard extends Component {
267 <Loader /> 289 <Loader />
268 ) : ( 290 ) : (
269 <div className="account franz-form"> 291 <div className="account franz-form">
270 <div className="account__box account__box--last"> 292 <div className="account__box">
271 <h2>{intl.formatMessage(messages.headlineUpgrade)}</h2> 293 <h2>{intl.formatMessage(messages.headlineUpgrade)}</h2>
272 <SubscriptionForm 294 <SubscriptionForm
273 onCloseWindow={onCloseSubscriptionWindow} 295 onCloseWindow={onCloseSubscriptionWindow}
@@ -276,8 +298,29 @@ export default class AccountDashboard extends Component {
276 </div> 298 </div>
277 ) 299 )
278 )} 300 )}
301
302 <div className="account franz-form">
303 <div className="account__box">
304 <h2>{intl.formatMessage(messages.headlineDangerZone)}</h2>
305 {!isDeleteAccountSuccessful && (
306 <div className="account__subscription">
307 <p>{intl.formatMessage(messages.deleteInfo)}</p>
308 <Button
309 label={intl.formatMessage(messages.deleteAccount)}
310 buttonType="danger"
311 onClick={() => deleteAccount()}
312 loaded={!isLoadingDeleteAccount}
313 />
314 </div>
315 )}
316 {isDeleteAccountSuccessful && (
317 <p>{intl.formatMessage(messages.deleteEmailSent)}</p>
318 )}
319 </div>
320 </div>
279 </div> 321 </div>
280 )} 322 )}
323
281 </div> 324 </div>
282 <ReactTooltip place="right" type="dark" effect="solid" /> 325 <ReactTooltip place="right" type="dark" effect="solid" />
283 </div> 326 </div>
diff --git a/src/components/settings/settings/EditSettingsForm.js b/src/components/settings/settings/EditSettingsForm.js
index 878e46d6d..ff398aa33 100644
--- a/src/components/settings/settings/EditSettingsForm.js
+++ b/src/components/settings/settings/EditSettingsForm.js
@@ -64,10 +64,6 @@ const messages = defineMessages({
64 id: 'settings.app.currentVersion', 64 id: 'settings.app.currentVersion',
65 defaultMessage: '!!!Current version:', 65 defaultMessage: '!!!Current version:',
66 }, 66 },
67 restartRequired: {
68 id: 'settings.app.restartRequired',
69 defaultMessage: '!!!Changes require restart',
70 },
71}); 67});
72 68
73@observer 69@observer
@@ -158,7 +154,6 @@ export default class EditSettingsForm extends Component {
158 {/* Advanced */} 154 {/* Advanced */}
159 <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2> 155 <h2 id="advanced">{intl.formatMessage(messages.headlineAdvanced)}</h2>
160 <Toggle field={form.$('enableSpellchecking')} /> 156 <Toggle field={form.$('enableSpellchecking')} />
161 <p className="settings__help">{intl.formatMessage(messages.restartRequired)}</p>
162 {/* <Select field={form.$('spellcheckingLanguage')} /> */} 157 {/* <Select field={form.$('spellcheckingLanguage')} /> */}
163 158
164 {/* Updates */} 159 {/* Updates */}
diff --git a/src/components/ui/Button.js b/src/components/ui/Button.js
index 07e94192f..554206cb7 100644
--- a/src/components/ui/Button.js
+++ b/src/components/ui/Button.js
@@ -68,7 +68,7 @@ export default class Button extends Component {
68 loaded={loaded} 68 loaded={loaded}
69 lines={10} 69 lines={10}
70 scale={0.4} 70 scale={0.4}
71 color={buttonType === '' ? '#FFF' : '#373a3c'} 71 color={buttonType !== 'secondary' ? '#FFF' : '#373a3c'}
72 component="span" 72 component="span"
73 /> 73 />
74 {label} 74 {label}
diff --git a/src/config.js b/src/config.js
index e6d8958e6..e66594c59 100644
--- a/src/config.js
+++ b/src/config.js
@@ -14,7 +14,8 @@ export const DEFAULT_APP_SETTINGS = {
14 showMessageBadgeWhenMuted: true, 14 showMessageBadgeWhenMuted: true,
15 enableSpellchecking: true, 15 enableSpellchecking: true,
16 // spellcheckingLanguage: 'auto', 16 // spellcheckingLanguage: 'auto',
17 locale: 'en-US', 17 locale: '',
18 fallbackLocale: 'en-US',
18 beta: false, 19 beta: false,
19 isAppMuted: false, 20 isAppMuted: false,
20}; 21};
diff --git a/src/containers/settings/AccountScreen.js b/src/containers/settings/AccountScreen.js
index a1ac8bda3..008c495d4 100644
--- a/src/containers/settings/AccountScreen.js
+++ b/src/containers/settings/AccountScreen.js
@@ -69,6 +69,7 @@ export default class AccountScreen extends Component {
69 render() { 69 render() {
70 const { user, payment, app } = this.props.stores; 70 const { user, payment, app } = this.props.stores;
71 const { openExternalUrl } = this.props.actions.app; 71 const { openExternalUrl } = this.props.actions.app;
72 const { user: userActions } = this.props.actions;
72 73
73 const isLoadingUserInfo = user.getUserInfoRequest.isExecuting; 74 const isLoadingUserInfo = user.getUserInfoRequest.isExecuting;
74 const isLoadingOrdersInfo = payment.ordersDataRequest.isExecuting; 75 const isLoadingOrdersInfo = payment.ordersDataRequest.isExecuting;
@@ -89,6 +90,9 @@ export default class AccountScreen extends Component {
89 openExternalUrl={url => openExternalUrl({ url })} 90 openExternalUrl={url => openExternalUrl({ url })}
90 onCloseSubscriptionWindow={() => this.onCloseWindow()} 91 onCloseSubscriptionWindow={() => this.onCloseWindow()}
91 stopMiner={() => this.stopMiner()} 92 stopMiner={() => this.stopMiner()}
93 deleteAccount={userActions.delete}
94 isLoadingDeleteAccount={user.deleteAccountRequest.isExecuting}
95 isDeleteAccountSuccessful={user.deleteAccountRequest.wasExecuted && !user.deleteAccountRequest.isError}
92 /> 96 />
93 ); 97 );
94 } 98 }
@@ -109,6 +113,7 @@ AccountScreen.wrappedComponent.propTypes = {
109 }).isRequired, 113 }).isRequired,
110 user: PropTypes.shape({ 114 user: PropTypes.shape({
111 update: PropTypes.func.isRequired, 115 update: PropTypes.func.isRequired,
116 delete: PropTypes.func.isRequired,
112 }).isRequired, 117 }).isRequired,
113 }).isRequired, 118 }).isRequired,
114}; 119};
diff --git a/src/electron/deepLinking.js b/src/electron/deepLinking.js
new file mode 100644
index 000000000..16e68b914
--- /dev/null
+++ b/src/electron/deepLinking.js
@@ -0,0 +1,5 @@
1export default function handleDeepLink(window, rawUrl) {
2 const url = rawUrl.replace('franz://', '');
3
4 window.webContents.send('navigateFromDeepLink', { url });
5}
diff --git a/src/helpers/async-helpers.js b/src/helpers/async-helpers.js
new file mode 100644
index 000000000..2ef01ee09
--- /dev/null
+++ b/src/helpers/async-helpers.js
@@ -0,0 +1,5 @@
1/* eslint-disable import/prefer-default-export */
2
3export function sleep(ms = 0) {
4 return new Promise(r => setTimeout(r, ms));
5}
diff --git a/src/i18n/languages.js b/src/i18n/languages.js
index 677b09405..f32c345af 100644
--- a/src/i18n/languages.js
+++ b/src/i18n/languages.js
@@ -4,6 +4,7 @@ export const APP_LOCALES = {
4 'zh-HANT': 'Chinese (Traditional)', 4 'zh-HANT': 'Chinese (Traditional)',
5 cs: 'Czech', 5 cs: 'Czech',
6 nl: 'Dutch', 6 nl: 'Dutch',
7 es: 'Spanish',
7 fr: 'French', 8 fr: 'French',
8 ka: 'Georgian', 9 ka: 'Georgian',
9 de: 'German', 10 de: 'German',
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index 380871668..48b408e59 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -70,6 +70,7 @@
70 "settings.account.headlineSubscription": "Your subscription", 70 "settings.account.headlineSubscription": "Your subscription",
71 "settings.account.headlineUpgrade": "Upgrade your account & support Franz", 71 "settings.account.headlineUpgrade": "Upgrade your account & support Franz",
72 "settings.account.headlineInvoices": "Invoices", 72 "settings.account.headlineInvoices": "Invoices",
73 "settings.account.headlineDangerZone": "Danger Zone",
73 "settings.account.manageSubscription.label": "Manage your subscription", 74 "settings.account.manageSubscription.label": "Manage your subscription",
74 "settings.account.accountType.basic": "Basic Account", 75 "settings.account.accountType.basic": "Basic Account",
75 "settings.account.accountType.premium": "Premium Supporter Account", 76 "settings.account.accountType.premium": "Premium Supporter Account",
@@ -86,6 +87,9 @@
86 "settings.account.mining.active": "You are right now performing {hashes} calculations per second.", 87 "settings.account.mining.active": "You are right now performing {hashes} calculations per second.",
87 "settings.account.mining.moreInformation": "Get more information", 88 "settings.account.mining.moreInformation": "Get more information",
88 "settings.account.mining.cancel": "Cancel mining", 89 "settings.account.mining.cancel": "Cancel mining",
90 "settings.account.deleteAccount": "Delete account",
91 "settings.account.deleteInfo": "If you don't need your Franz account any longer, you can delete your account and all related data here.",
92 "settings.account.deleteEmailSent": "You have received an email with a link to confirm your account deletion. Your account and data cannot be restored!",
89 "settings.navigation.availableServices": "Available services", 93 "settings.navigation.availableServices": "Available services",
90 "settings.navigation.yourServices": "Your services", 94 "settings.navigation.yourServices": "Your services",
91 "settings.navigation.account": "Account", 95 "settings.navigation.account": "Account",
diff --git a/src/index.js b/src/index.js
index 6a08e5e5a..76b1d8352 100644
--- a/src/index.js
+++ b/src/index.js
@@ -8,6 +8,7 @@ import { isDevMode, isWindows } from './environment';
8import ipcApi from './electron/ipc-api'; 8import ipcApi from './electron/ipc-api';
9import Tray from './lib/Tray'; 9import Tray from './lib/Tray';
10import Settings from './electron/Settings'; 10import Settings from './electron/Settings';
11import handleDeepLink from './electron/deepLinking';
11import { appId } from './package.json'; // eslint-disable-line import/no-unresolved 12import { appId } from './package.json'; // eslint-disable-line import/no-unresolved
12import './electron/exception'; 13import './electron/exception';
13 14
@@ -26,10 +27,19 @@ if (isWindows) {
26} 27}
27 28
28// Force single window 29// Force single window
29const isSecondInstance = app.makeSingleInstance(() => { 30const isSecondInstance = app.makeSingleInstance((argv) => {
30 if (mainWindow) { 31 if (mainWindow) {
31 if (mainWindow.isMinimized()) mainWindow.restore(); 32 if (mainWindow.isMinimized()) mainWindow.restore();
32 mainWindow.focus(); 33 mainWindow.focus();
34
35 if (process.platform === 'win32') {
36 // Keep only command line / deep linked arguments
37 const url = argv.slice(1);
38
39 if (url) {
40 handleDeepLink(mainWindow, url.toString());
41 }
42 }
33 } 43 }
34}); 44});
35 45
@@ -137,6 +147,8 @@ const createWindow = () => {
137 147
138 mainWindow.on('show', () => { 148 mainWindow.on('show', () => {
139 mainWindow.setSkipTaskbar(false); 149 mainWindow.setSkipTaskbar(false);
150
151 handleDeepLink(mainWindow, 'franz://settings/services/add/msteams');
140 }); 152 });
141 153
142 app.mainWindow = mainWindow; 154 app.mainWindow = mainWindow;
@@ -176,3 +188,15 @@ app.on('activate', () => {
176 mainWindow.show(); 188 mainWindow.show();
177 } 189 }
178}); 190});
191
192app.on('will-finish-launching', () => {
193 // Protocol handler for osx
194 app.on('open-url', (event, url) => {
195 event.preventDefault();
196 console.log(`open-url event: ${url}`);
197 handleDeepLink(mainWindow, url);
198 });
199});
200
201// Register App URL
202app.setAsDefaultProtocolClient('franz');
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index 6624ab75e..d9c30466b 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -249,7 +249,7 @@ export default class FranzMenu {
249 } 249 }
250 250
251 @computed get serviceTpl() { 251 @computed get serviceTpl() {
252 const services = this.stores.services.enabled; 252 const services = this.stores.services.allDisplayed;
253 253
254 if (this.stores.user.isLoggedIn) { 254 if (this.stores.user.isLoggedIn) {
255 return services.map((service, i) => ({ 255 return services.map((service, i) => ({
diff --git a/src/models/Settings.js b/src/models/Settings.js
index 35bfe0d05..ca44da258 100644
--- a/src/models/Settings.js
+++ b/src/models/Settings.js
@@ -1,4 +1,4 @@
1import { observable } from 'mobx'; 1import { observable, extendObservable } from 'mobx';
2import { DEFAULT_APP_SETTINGS } from '../config'; 2import { DEFAULT_APP_SETTINGS } from '../config';
3 3
4export default class Settings { 4export default class Settings {
@@ -17,4 +17,8 @@ export default class Settings {
17 constructor(data) { 17 constructor(data) {
18 Object.assign(this, data); 18 Object.assign(this, data);
19 } 19 }
20
21 update(data) {
22 extendObservable(this, data);
23 }
20} 24}
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index 6125a7cff..5a6c12ee1 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -116,6 +116,14 @@ export default class AppStore extends Store {
116 } 116 }
117 }); 117 });
118 118
119 // Handle deep linking (franz://)
120 ipcRenderer.on('navigateFromDeepLink', (event, data) => {
121 const { url } = data;
122 if (!url) return;
123
124 this.stores.router.push(data.url);
125 });
126
119 // Check system idle time every minute 127 // Check system idle time every minute
120 setInterval(() => { 128 setInterval(() => {
121 this.idleTime = idleTimer.getIdleTime(); 129 this.idleTime = idleTimer.getIdleTime();
@@ -138,7 +146,7 @@ export default class AppStore extends Store {
138 this.actions.service.setActivePrev(); 146 this.actions.service.setActivePrev();
139 }); 147 });
140 148
141 // Global Mute 149 // Global Mute
142 key( 150 key(
143 '⌘+shift+m ctrl+shift+m', () => { 151 '⌘+shift+m ctrl+shift+m', () => {
144 this.actions.app.toggleMuteApp(); 152 this.actions.app.toggleMuteApp();
@@ -163,6 +171,11 @@ export default class AppStore extends Store {
163 }); 171 });
164 172
165 this.actions.service.setActive({ serviceId }); 173 this.actions.service.setActive({ serviceId });
174
175 if (!isMac) {
176 const mainWindow = remote.getCurrentWindow();
177 mainWindow.restore();
178 }
166 } 179 }
167 }; 180 };
168 } 181 }
@@ -250,8 +263,10 @@ export default class AppStore extends Store {
250 _setLocale() { 263 _setLocale() {
251 const locale = this.stores.settings.all.locale; 264 const locale = this.stores.settings.all.locale;
252 265
253 if (locale && locale !== this.locale) { 266 if (locale && Object.prototype.hasOwnProperty.call(locales, locale) && locale !== this.locale) {
254 this.locale = locale; 267 this.locale = locale;
268 } else if (!locale) {
269 this.locale = this._getDefaultLocale();
255 } 270 }
256 } 271 }
257 272
@@ -276,6 +291,10 @@ export default class AppStore extends Store {
276 locale = defaultLocale; 291 locale = defaultLocale;
277 } 292 }
278 293
294 if (!locale) {
295 locale = DEFAULT_APP_SETTINGS.fallbackLocale;
296 }
297
279 return locale; 298 return locale;
280 } 299 }
281 300
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js
index 33473f16d..da99a720f 100644
--- a/src/stores/SettingsStore.js
+++ b/src/stores/SettingsStore.js
@@ -26,7 +26,7 @@ export default class SettingsStore extends Store {
26 } 26 }
27 27
28 @computed get all() { 28 @computed get all() {
29 return this.allSettingsRequest.result || new SettingsModel(); 29 return new SettingsModel(this.allSettingsRequest.result);
30 } 30 }
31 31
32 @action async _update({ settings }) { 32 @action async _update({ settings }) {
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index 1cb2ecac3..09000dcdb 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -26,6 +26,7 @@ export default class UserStore extends Store {
26 @observable getUserInfoRequest = new CachedRequest(this.api.user, 'getInfo'); 26 @observable getUserInfoRequest = new CachedRequest(this.api.user, 'getInfo');
27 @observable updateUserInfoRequest = new Request(this.api.user, 'updateInfo'); 27 @observable updateUserInfoRequest = new Request(this.api.user, 'updateInfo');
28 @observable getLegacyServicesRequest = new CachedRequest(this.api.user, 'getLegacyServices'); 28 @observable getLegacyServicesRequest = new CachedRequest(this.api.user, 'getLegacyServices');
29 @observable deleteAccountRequest = new CachedRequest(this.api.user, 'delete');
29 30
30 @observable isImportLegacyServicesExecuting = false; 31 @observable isImportLegacyServicesExecuting = false;
31 @observable isImportLegacyServicesCompleted = false; 32 @observable isImportLegacyServicesCompleted = false;
@@ -57,6 +58,7 @@ export default class UserStore extends Store {
57 this.actions.user.update.listen(this._update.bind(this)); 58 this.actions.user.update.listen(this._update.bind(this));
58 this.actions.user.resetStatus.listen(this._resetStatus.bind(this)); 59 this.actions.user.resetStatus.listen(this._resetStatus.bind(this));
59 this.actions.user.importLegacyServices.listen(this._importLegacyServices.bind(this)); 60 this.actions.user.importLegacyServices.listen(this._importLegacyServices.bind(this));
61 this.actions.user.delete.listen(this._delete.bind(this));
60 62
61 // Reactions 63 // Reactions
62 this.registerReactions([ 64 this.registerReactions([
@@ -212,6 +214,10 @@ export default class UserStore extends Store {
212 this.isImportLegacyServicesCompleted = true; 214 this.isImportLegacyServicesCompleted = true;
213 } 215 }
214 216
217 @action async _delete() {
218 this.deleteAccountRequest.execute();
219 }
220
215 // This is a mobx autorun which forces the user to login if not authenticated 221 // This is a mobx autorun which forces the user to login if not authenticated
216 _requireAuthenticatedUser = () => { 222 _requireAuthenticatedUser = () => {
217 if (this.isTokenExpired) { 223 if (this.isTokenExpired) {
diff --git a/src/styles/services.scss b/src/styles/services.scss
index 282c15121..9f6cfc772 100644
--- a/src/styles/services.scss
+++ b/src/styles/services.scss
@@ -24,6 +24,7 @@
24 display: inline-flex; 24 display: inline-flex;
25 width: 0px; 25 width: 0px;
26 height: 0px; 26 height: 0px;
27 background: $theme-gray-lighter;
27 } 28 }
28 29
29 &.is-active { 30 &.is-active {
diff --git a/src/styles/settings.scss b/src/styles/settings.scss
index 6e93094b4..73cef0813 100644
--- a/src/styles/settings.scss
+++ b/src/styles/settings.scss
@@ -281,6 +281,10 @@
281 margin-left: auto; 281 margin-left: auto;
282 } 282 }
283 283
284 .franz-form__button {
285 white-space: nowrap;
286 }
287
284 div { 288 div {
285 height: auto; 289 height: auto;
286 } 290 }
diff --git a/src/styles/welcome.scss b/src/styles/welcome.scss
index 5365921fb..cfdcc80ad 100644
--- a/src/styles/welcome.scss
+++ b/src/styles/welcome.scss
@@ -58,17 +58,32 @@
58 } 58 }
59 59
60 &__featured-services { 60 &__featured-services {
61 margin-top: 150px;
62 text-align: center; 61 text-align: center;
63 margin-top: 80px; 62 width: 480px;
63 margin: 80px auto 0 auto;
64 display: flex;
65 align-items: center;
66 flex-wrap: wrap;
67 background: #FFF;
68 border-radius: 6px;
69 padding: 20px 20px 5px;
64 } 70 }
65 71
66 &__featured-service { 72 &__featured-service {
67 width: 35px; 73 width: 35px;
68 margin-right: 30px; 74 height: 35px;
75 margin: 0 10px 15px;
76 filter: grayscale(1)
77 opacity(0.5);
78 transition: 0.5s filter, 0.5s opacity;
79
80 &:hover {
81 filter: grayscale(0);
82 opacity: (1);
83 }
69 84
70 &:last-of-type { 85 img {
71 margin-right: 0; 86 width: 35px;
72 } 87 }
73 } 88 }
74 } 89 }
diff --git a/src/webview/lib/RecipeWebview.js b/src/webview/lib/RecipeWebview.js
index 048beea69..be29142af 100644
--- a/src/webview/lib/RecipeWebview.js
+++ b/src/webview/lib/RecipeWebview.js
@@ -40,8 +40,8 @@ class RecipeWebview {
40 && this.countCache.indirect === indirect) return; 40 && this.countCache.indirect === indirect) return;
41 41
42 const count = { 42 const count = {
43 direct, 43 direct: direct > 0 ? direct : 0,
44 indirect, 44 indirect: indirect > 0 ? indirect : 0,
45 }; 45 };
46 46
47 ipcRenderer.sendToHost('messages', count); 47 ipcRenderer.sendToHost('messages', count);
diff --git a/src/webview/plugin.js b/src/webview/plugin.js
index c877132b1..cf38169d3 100644
--- a/src/webview/plugin.js
+++ b/src/webview/plugin.js
@@ -1,14 +1,14 @@
1import { ipcRenderer } from 'electron'; 1import { ipcRenderer } from 'electron';
2import { ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker';
2import path from 'path'; 3import path from 'path';
3 4
5import { isDevMode } from '../environment';
4import RecipeWebview from './lib/RecipeWebview'; 6import RecipeWebview from './lib/RecipeWebview';
5 7
6import Spellchecker from './spellchecker.js'; 8import Spellchecker from './spellchecker.js';
7import './notifications.js'; 9import './notifications.js';
8import './ime.js'; 10import './ime.js';
9 11
10const spellchecker = new Spellchecker();
11
12ipcRenderer.on('initializeRecipe', (e, data) => { 12ipcRenderer.on('initializeRecipe', (e, data) => {
13 const modulePath = path.join(data.recipe.path, 'webview.js'); 13 const modulePath = path.join(data.recipe.path, 'webview.js');
14 // Delete module from cache 14 // Delete module from cache
@@ -21,20 +21,22 @@ ipcRenderer.on('initializeRecipe', (e, data) => {
21 } 21 }
22}); 22});
23 23
24const spellchecker = new Spellchecker();
25spellchecker.initialize();
26
27const contextMenuBuilder = new ContextMenuBuilder(spellchecker.handler, null, isDevMode);
28
29new ContextMenuListener((info) => { // eslint-disable-line
30 contextMenuBuilder.showPopupMenu(info);
31});
32
24ipcRenderer.on('settings-update', (e, data) => { 33ipcRenderer.on('settings-update', (e, data) => {
25 if (data.enableSpellchecking) { 34 console.log('settings-update', data);
26 if (!spellchecker.isEnabled) { 35 spellchecker.toggleSpellchecker(data.enableSpellchecking);
27 spellchecker.enable();
28
29 // TODO: this does not work yet, needs more testing
30 // if (data.spellcheckingLanguage !== 'auto') {
31 // console.log('set spellchecking language to', data.spellcheckingLanguage);
32 // spellchecker.switchLanguage(data.spellcheckingLanguage);
33 // }
34 }
35 }
36}); 36});
37 37
38// initSpellche
39
38document.addEventListener('DOMContentLoaded', () => { 40document.addEventListener('DOMContentLoaded', () => {
39 ipcRenderer.sendToHost('hello'); 41 ipcRenderer.sendToHost('hello');
40}, false); 42}, false);
diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js
index 5beb77e03..a504a4039 100644
--- a/src/webview/spellchecker.js
+++ b/src/webview/spellchecker.js
@@ -1,30 +1,63 @@
1import { SpellCheckHandler, ContextMenuListener, ContextMenuBuilder } from 'electron-spellchecker'; 1import { SpellCheckHandler } from 'electron-spellchecker';
2 2
3import { isMac } from '../environment'; 3import { isMac } from '../environment';
4 4
5export default class Spellchecker { 5export default class Spellchecker {
6 isEnabled = false; 6 isInitialized = false;
7 spellchecker = null; 7 handler = null;
8 initRetries = 0;
9 DOMCheckInterval = null;
10
11 get inputs() {
12 return document.querySelectorAll('input[type="text"], [contenteditable="true"], textarea');
13 }
14
15 initialize() {
16 this.handler = new SpellCheckHandler();
8 17
9 enable() {
10 this.spellchecker = new SpellCheckHandler();
11 if (!isMac) { 18 if (!isMac) {
12 this.spellchecker.attachToInput(); 19 this.attach();
13 this.spellchecker.switchLanguage(navigator.language); 20 } else {
21 this.isInitialized = true;
22 }
23 }
24
25 attach() {
26 let initFailed = false;
27
28 if (this.initRetries > 3) {
29 console.error('Could not initialize spellchecker');
30 return;
14 } 31 }
15 32
16 const contextMenuBuilder = new ContextMenuBuilder(this.spellchecker); 33 try {
34 this.handler.attachToInput();
35 this.handler.switchLanguage(navigator.language);
36 } catch (err) {
37 initFailed = true;
38 this.initRetries = +1;
39 setTimeout(() => { this.attach(); console.warn('Spellchecker init failed, trying again in 5s'); }, 5000);
40 }
17 41
18 new ContextMenuListener((info) => { // eslint-disable-line 42 if (!initFailed) {
19 contextMenuBuilder.showPopupMenu(info); 43 this.isInitialized = true;
44 }
45 }
46
47 toggleSpellchecker(enable = false) {
48 this.inputs.forEach((input) => {
49 input.setAttribute('spellcheck', enable);
20 }); 50 });
51
52 this.intervalHandler(enable);
21 } 53 }
22 54
23 // TODO: this does not work yet, needs more testing 55 intervalHandler(enable) {
24 // switchLanguage(language) { 56 clearInterval(this.DOMCheckInterval);
25 // if (language !== 'auto') { 57
26 // this.spellchecker.switchLanguage(language); 58 if (enable) {
27 // } 59 this.DOMCheckInterval = setInterval(() => this.toggleSpellchecker(enable), 30000);
28 // } 60 }
61 }
29} 62}
30 63