aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar Markus Hatvan <markus_hatvan@aon.at>2021-09-14 19:58:52 +0200
committerLibravatar GitHub <noreply@github.com>2021-09-14 19:58:52 +0200
commit95df3522a15631abc51a4295cae0ea401a8d4e1e (patch)
treee5eb0f368c947683f01458e912f21756fb0d99cb /src
parentdocs: add sad270 as a contributor for bug, userTesting [skip ci] (#1941) (diff)
downloadferdium-app-95df3522a15631abc51a4295cae0ea401a8d4e1e.tar.gz
ferdium-app-95df3522a15631abc51a4295cae0ea401a8d4e1e.tar.zst
ferdium-app-95df3522a15631abc51a4295cae0ea401a8d4e1e.zip
feat: add eslint-plugin-unicorn (#1936)
Diffstat (limited to 'src')
-rw-r--r--src/actions/lib/actions.ts3
-rw-r--r--src/api/apiBase.ts17
-rw-r--r--src/api/server/ServerApi.js80
-rw-r--r--src/api/utils/auth.js15
-rw-r--r--src/app.js2
-rw-r--r--src/components/auth/Invite.js10
-rw-r--r--src/components/settings/SettingsLayout.js1
-rw-r--r--src/components/settings/services/EditServiceForm.js4
-rw-r--r--src/components/settings/services/ServicesDashboard.js2
-rw-r--r--src/components/ui/ImageUpload.js4
-rw-r--r--src/components/ui/InfoBar.js3
-rw-r--r--src/components/ui/Infobox.js3
-rw-r--r--src/components/ui/Modal/index.js2
-rw-r--r--src/components/ui/Select.js2
-rw-r--r--src/components/ui/effects/Appear.js10
-rw-r--r--src/config.ts2
-rw-r--r--src/containers/settings/AccountScreen.js14
-rw-r--r--src/containers/settings/SettingsWindow.js4
-rw-r--r--src/electron/ipc-api/appIndicator.ts4
-rw-r--r--src/electron/ipc-api/autoUpdate.ts4
-rw-r--r--src/electron/ipc-api/cld.ts4
-rw-r--r--src/electron/ipc-api/dnd.ts4
-rw-r--r--src/electron/ipc-api/download.ts10
-rw-r--r--src/features/appearance/index.js26
-rw-r--r--src/features/communityRecipes/store.js8
-rw-r--r--src/features/quickSwitch/Component.js6
-rw-r--r--src/features/serviceProxy/index.js4
-rwxr-xr-xsrc/features/settingsWS/store.js20
-rw-r--r--src/features/todos/preload.js12
-rw-r--r--src/features/utils/FeatureStore.js8
-rw-r--r--src/features/webControls/containers/WebControlsScreen.js28
-rw-r--r--src/features/workspaces/components/EditWorkspaceForm.js2
-rw-r--r--src/features/workspaces/components/WorkspaceDrawerItem.js2
-rw-r--r--src/features/workspaces/models/Workspace.js2
-rw-r--r--src/features/workspaces/store.js8
-rw-r--r--src/helpers/i18n-helpers.ts12
-rw-r--r--src/helpers/password-helpers.ts14
-rw-r--r--src/helpers/recipe-helpers.ts14
-rw-r--r--src/helpers/schedule-helpers.ts8
-rw-r--r--src/helpers/url-helpers.ts2
-rw-r--r--src/helpers/userAgent-helpers.ts2
-rw-r--r--src/helpers/validation-helpers.ts19
-rw-r--r--src/i18n/apply-branding.js15
-rw-r--r--src/i18n/translations.js10
-rw-r--r--src/index.js6
-rw-r--r--src/internal-server/app/Controllers/Http/RecipeController.js51
-rw-r--r--src/internal-server/app/Controllers/Http/ServiceController.js16
-rw-r--r--src/internal-server/app/Controllers/Http/UserController.js176
-rw-r--r--src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js2
-rw-r--r--src/internal-server/app/Models/Recipe.js3
-rw-r--r--src/internal-server/app/Models/Service.js3
-rw-r--r--src/internal-server/app/Models/Token.js3
-rw-r--r--src/internal-server/app/Models/User.js3
-rw-r--r--src/internal-server/app/Models/Workspace.js3
-rw-r--r--src/internal-server/config/shield.js3
-rw-r--r--src/internal-server/public/js/transfer.js20
-rw-r--r--src/internal-server/start/kernel.js10
-rw-r--r--src/internal-server/start/migrate.js24
-rw-r--r--src/internal-server/test.js2
-rw-r--r--src/lib/Menu.js38
-rw-r--r--src/lib/TouchBar.js8
-rw-r--r--src/models/News.ts6
-rw-r--r--src/models/Recipe.ts6
-rw-r--r--src/models/RecipePreview.ts6
-rw-r--r--src/models/Service.js142
-rw-r--r--src/models/User.ts4
-rw-r--r--src/models/UserAgent.js19
-rw-r--r--src/prop-types.ts1
-rw-r--r--src/stores/AppStore.js32
-rw-r--r--src/stores/FeaturesStore.js16
-rw-r--r--src/stores/GlobalErrorStore.js10
-rw-r--r--src/stores/RecipesStore.js51
-rw-r--r--src/stores/RequestStore.js18
-rw-r--r--src/stores/ServicesStore.js291
-rw-r--r--src/stores/SettingsStore.js60
-rw-r--r--src/stores/UserStore.js78
-rw-r--r--src/stores/index.ts4
-rw-r--r--src/stores/lib/CachedRequest.js96
-rw-r--r--src/stores/lib/Request.js2
-rw-r--r--src/stores/lib/Store.js6
-rw-r--r--src/webview/badge.ts2
-rw-r--r--src/webview/contextMenuBuilder.js248
-rw-r--r--src/webview/darkmode.ts6
-rw-r--r--src/webview/lib/RecipeWebview.js25
-rw-r--r--src/webview/lib/Userscript.js10
-rw-r--r--src/webview/recipe.js41
-rw-r--r--src/webview/sessionHandler.ts17
-rw-r--r--src/webview/spellchecker.ts2
88 files changed, 1095 insertions, 901 deletions
diff --git a/src/actions/lib/actions.ts b/src/actions/lib/actions.ts
index ed42eabc0..412a0d895 100644
--- a/src/actions/lib/actions.ts
+++ b/src/actions/lib/actions.ts
@@ -1,5 +1,6 @@
1export const createActionsFromDefinitions = (actionDefinitions, validate) => { 1export const createActionsFromDefinitions = (actionDefinitions, validate) => {
2 const actions = {}; 2 const actions = {};
3 // eslint-disable-next-line unicorn/no-array-for-each
3 Object.keys(actionDefinitions).forEach(actionName => { 4 Object.keys(actionDefinitions).forEach(actionName => {
4 const action = (params = {}) => { 5 const action = (params = {}) => {
5 const schema = actionDefinitions[actionName]; 6 const schema = actionDefinitions[actionName];
@@ -14,6 +15,7 @@ export const createActionsFromDefinitions = (actionDefinitions, validate) => {
14 listeners.splice(listeners.indexOf(listener), 1); 15 listeners.splice(listeners.indexOf(listener), 1);
15 }; 16 };
16 action.notify = params => 17 action.notify = params =>
18 // eslint-disable-next-line unicorn/no-array-for-each
17 action.listeners.forEach(listener => listener(params)); 19 action.listeners.forEach(listener => listener(params));
18 }); 20 });
19 return actions; 21 return actions;
@@ -21,6 +23,7 @@ export const createActionsFromDefinitions = (actionDefinitions, validate) => {
21 23
22export default (definitions, validate) => { 24export default (definitions, validate) => {
23 const newActions = {}; 25 const newActions = {};
26 // eslint-disable-next-line unicorn/no-array-for-each
24 Object.keys(definitions).forEach(scopeName => { 27 Object.keys(definitions).forEach(scopeName => {
25 newActions[scopeName] = createActionsFromDefinitions( 28 newActions[scopeName] = createActionsFromDefinitions(
26 definitions[scopeName], 29 definitions[scopeName],
diff --git a/src/api/apiBase.ts b/src/api/apiBase.ts
index dc10fad91..510ccb619 100644
--- a/src/api/apiBase.ts
+++ b/src/api/apiBase.ts
@@ -12,8 +12,6 @@ import {
12 12
13// Note: This cannot be used from the internal-server since we are not running within the context of a browser window 13// Note: This cannot be used from the internal-server since we are not running within the context of a browser window
14const apiBase = (withVersion = true) => { 14const apiBase = (withVersion = true) => {
15 let url: string;
16
17 if ( 15 if (
18 !(window as any).ferdi || 16 !(window as any).ferdi ||
19 !(window as any).ferdi.stores.settings || 17 !(window as any).ferdi.stores.settings ||
@@ -23,15 +21,12 @@ const apiBase = (withVersion = true) => {
23 // Stores have not yet been loaded - return SERVER_NOT_LOADED to force a retry when stores are loaded 21 // Stores have not yet been loaded - return SERVER_NOT_LOADED to force a retry when stores are loaded
24 return SERVER_NOT_LOADED; 22 return SERVER_NOT_LOADED;
25 } 23 }
26 if ((window as any).ferdi.stores.settings.all.app.server === LOCAL_SERVER) { 24 const url =
27 // Use URL for local server 25 (window as any).ferdi.stores.settings.all.app.server === LOCAL_SERVER
28 url = `http://${LOCAL_HOSTNAME}:${ 26 ? `http://${LOCAL_HOSTNAME}:${
29 (window as any).ferdi.stores.requests.localServerPort 27 (window as any).ferdi.stores.requests.localServerPort
30 }`; 28 }`
31 } else { 29 : (window as any).ferdi.stores.settings.all.app.server;
32 // Load URL from store
33 url = (window as any).ferdi.stores.settings.all.app.server;
34 }
35 30
36 return withVersion ? `${url}/${API_VERSION}` : url; 31 return withVersion ? `${url}/${API_VERSION}` : url;
37}; 32};
diff --git a/src/api/server/ServerApi.js b/src/api/server/ServerApi.js
index b5042525a..fb0495b19 100644
--- a/src/api/server/ServerApi.js
+++ b/src/api/server/ServerApi.js
@@ -1,6 +1,16 @@
1/* eslint-disable global-require */
1import { join } from 'path'; 2import { join } from 'path';
2import tar from 'tar'; 3import tar from 'tar';
3import { readdirSync, statSync, writeFileSync, copySync, ensureDirSync, pathExistsSync, readJsonSync, removeSync } from 'fs-extra'; 4import {
5 readdirSync,
6 statSync,
7 writeFileSync,
8 copySync,
9 ensureDirSync,
10 pathExistsSync,
11 readJsonSync,
12 removeSync,
13} from 'fs-extra';
4import { require as remoteRequire } from '@electron/remote'; 14import { require as remoteRequire } from '@electron/remote';
5 15
6import ServiceModel from '../../models/Service'; 16import ServiceModel from '../../models/Service';
@@ -12,7 +22,14 @@ import UserModel from '../../models/User';
12import { sleep } from '../../helpers/async-helpers'; 22import { sleep } from '../../helpers/async-helpers';
13 23
14import { SERVER_NOT_LOADED } from '../../config'; 24import { SERVER_NOT_LOADED } from '../../config';
15import { osArch, osPlatform, asarRecipesPath, userDataRecipesPath, userDataPath, ferdiVersion } from '../../environment'; 25import {
26 osArch,
27 osPlatform,
28 asarRecipesPath,
29 userDataRecipesPath,
30 userDataPath,
31 ferdiVersion,
32} from '../../environment';
16import apiBase from '../apiBase'; 33import apiBase from '../apiBase';
17import { prepareAuthRequest, sendAuthRequest } from '../utils/auth'; 34import { prepareAuthRequest, sendAuthRequest } from '../utils/auth';
18 35
@@ -310,22 +327,22 @@ export default class ServerApi {
310 // Recipes 327 // Recipes
311 async getInstalledRecipes() { 328 async getInstalledRecipes() {
312 const recipesDirectory = getRecipeDirectory(); 329 const recipesDirectory = getRecipeDirectory();
313 const paths = readdirSync(recipesDirectory) 330 const paths = readdirSync(recipesDirectory).filter(
314 .filter( 331 file =>
315 file => 332 statSync(join(recipesDirectory, file)).isDirectory() &&
316 statSync(join(recipesDirectory, file)).isDirectory() && 333 file !== 'temp' &&
317 file !== 'temp' && 334 file !== 'dev',
318 file !== 'dev', 335 );
319 );
320 336
321 this.recipes = paths 337 this.recipes = paths
322 .map(id => { 338 .map(id => {
323 // eslint-disable-next-line 339 // eslint-disable-next-line import/no-dynamic-require
324 const Recipe = require(id)(RecipeModel); 340 const Recipe = require(id)(RecipeModel);
325 return new Recipe(loadRecipeConfig(id)); 341 return new Recipe(loadRecipeConfig(id));
326 }) 342 })
327 .filter(recipe => recipe.id); 343 .filter(recipe => recipe.id);
328 344
345 // eslint-disable-next-line unicorn/prefer-spread
329 this.recipes = this.recipes.concat(this._getDevRecipes()); 346 this.recipes = this.recipes.concat(this._getDevRecipes());
330 347
331 debug('StubServerApi::getInstalledRecipes resolves', this.recipes); 348 debug('StubServerApi::getInstalledRecipes resolves', this.recipes);
@@ -425,8 +442,8 @@ export default class ServerApi {
425 removeSync(join(recipesDirectory, recipeId, 'recipe.tar.gz')); 442 removeSync(join(recipesDirectory, recipeId, 'recipe.tar.gz'));
426 443
427 return id; 444 return id;
428 } catch (err) { 445 } catch (error) {
429 console.error(err); 446 console.error(error);
430 447
431 return false; 448 return false;
432 } 449 }
@@ -434,7 +451,9 @@ export default class ServerApi {
434 451
435 // News 452 // News
436 async getLatestNews() { 453 async getLatestNews() {
437 const url = `${apiBase(true)}/news?platform=${osPlatform}&arch=${osArch}&version=${ferdiVersion}`; 454 const url = `${apiBase(
455 true,
456 )}/news?platform=${osPlatform}&arch=${osArch}&version=${ferdiVersion}`;
438 const request = await sendAuthRequest(url); 457 const request = await sendAuthRequest(url);
439 if (!request.ok) throw request; 458 if (!request.ok) throw request;
440 const data = await request.json(); 459 const data = await request.json();
@@ -494,7 +513,7 @@ export default class ServerApi {
494 debug('ServerApi::getLegacyServices resolves', services); 513 debug('ServerApi::getLegacyServices resolves', services);
495 return services; 514 return services;
496 } 515 }
497 } catch (err) { 516 } catch {
498 console.error('ServerApi::getLegacyServices no config found'); 517 console.error('ServerApi::getLegacyServices no config found');
499 } 518 }
500 519
@@ -523,8 +542,8 @@ export default class ServerApi {
523 } 542 }
524 543
525 return new ServiceModel(service, recipe); 544 return new ServiceModel(service, recipe);
526 } catch (e) { 545 } catch (error) {
527 debug(e); 546 debug(error);
528 return null; 547 return null;
529 } 548 }
530 } 549 }
@@ -559,7 +578,7 @@ export default class ServerApi {
559 578
560 return recipe; 579 return recipe;
561 }), 580 }),
562 ).catch(err => console.error("Can't load recipe", err)); 581 ).catch(error => console.error("Can't load recipe", error));
563 } 582 }
564 583
565 _mapRecipePreviewModel(recipes) { 584 _mapRecipePreviewModel(recipes) {
@@ -567,8 +586,8 @@ export default class ServerApi {
567 .map(recipe => { 586 .map(recipe => {
568 try { 587 try {
569 return new RecipePreviewModel(recipe); 588 return new RecipePreviewModel(recipe);
570 } catch (e) { 589 } catch (error) {
571 console.error(e); 590 console.error(error);
572 return null; 591 return null;
573 } 592 }
574 }) 593 })
@@ -580,8 +599,8 @@ export default class ServerApi {
580 .map(newsItem => { 599 .map(newsItem => {
581 try { 600 try {
582 return new NewsModel(newsItem); 601 return new NewsModel(newsItem);
583 } catch (e) { 602 } catch (error) {
584 console.error(e); 603 console.error(error);
585 return null; 604 return null;
586 } 605 }
587 }) 606 })
@@ -591,22 +610,21 @@ export default class ServerApi {
591 _getDevRecipes() { 610 _getDevRecipes() {
592 const recipesDirectory = getDevRecipeDirectory(); 611 const recipesDirectory = getDevRecipeDirectory();
593 try { 612 try {
594 const paths = readdirSync(recipesDirectory) 613 const paths = readdirSync(recipesDirectory).filter(
595 .filter( 614 file =>
596 file => 615 statSync(join(recipesDirectory, file)).isDirectory() &&
597 statSync(join(recipesDirectory, file)).isDirectory() && 616 file !== 'temp',
598 file !== 'temp', 617 );
599 );
600 618
601 const recipes = paths 619 const recipes = paths
602 .map(id => { 620 .map(id => {
603 let Recipe; 621 let Recipe;
604 try { 622 try {
605 // eslint-disable-next-line 623 // eslint-disable-next-line import/no-dynamic-require
606 Recipe = require(id)(RecipeModel); 624 Recipe = require(id)(RecipeModel);
607 return new Recipe(loadRecipeConfig(id)); 625 return new Recipe(loadRecipeConfig(id));
608 } catch (err) { 626 } catch (error) {
609 console.error(err); 627 console.error(error);
610 } 628 }
611 629
612 return false; 630 return false;
@@ -624,7 +642,7 @@ export default class ServerApi {
624 }); 642 });
625 643
626 return recipes; 644 return recipes;
627 } catch (err) { 645 } catch {
628 debug('Could not load dev recipes'); 646 debug('Could not load dev recipes');
629 return false; 647 return false;
630 } 648 }
diff --git a/src/api/utils/auth.js b/src/api/utils/auth.js
index e493b2962..527c68840 100644
--- a/src/api/utils/auth.js
+++ b/src/api/utils/auth.js
@@ -1,7 +1,11 @@
1import localStorage from 'mobx-localstorage'; 1import localStorage from 'mobx-localstorage';
2import { ferdiLocale, ferdiVersion } from '../../environment'; 2import { ferdiLocale, ferdiVersion } from '../../environment';
3 3
4export const prepareAuthRequest = (options = { method: 'GET' }, auth = true) => { 4export const prepareAuthRequest = (
5 // eslint-disable-next-line unicorn/no-object-as-default-parameter
6 options = { method: 'GET' },
7 auth = true,
8) => {
5 const request = Object.assign(options, { 9 const request = Object.assign(options, {
6 mode: 'cors', 10 mode: 'cors',
7 headers: { 11 headers: {
@@ -16,12 +20,13 @@ export const prepareAuthRequest = (options = { method: 'GET' }, auth = true) =>
16 }); 20 });
17 21
18 if (auth) { 22 if (auth) {
19 request.headers.Authorization = `Bearer ${localStorage.getItem('authToken')}`; 23 request.headers.Authorization = `Bearer ${localStorage.getItem(
24 'authToken',
25 )}`;
20 } 26 }
21 27
22 return request; 28 return request;
23}; 29};
24 30
25export const sendAuthRequest = (url, options, auth) => ( 31export const sendAuthRequest = (url, options, auth) =>
26 window.fetch(url, prepareAuthRequest(options, auth)) 32 window.fetch(url, prepareAuthRequest(options, auth));
27);
diff --git a/src/app.js b/src/app.js
index e0d2dbc5a..8a1f99320 100644
--- a/src/app.js
+++ b/src/app.js
@@ -44,7 +44,7 @@ window.addEventListener('load', () => {
44 </I18N> 44 </I18N>
45 </Provider> 45 </Provider>
46 ); 46 );
47 render(preparedApp, document.getElementById('root')); 47 render(preparedApp, document.querySelector('#root'));
48 }, 48 },
49 }; 49 };
50 window.ferdi.render(); 50 window.ferdi.render();
diff --git a/src/components/auth/Invite.js b/src/components/auth/Invite.js
index 519691ede..df8980314 100644
--- a/src/components/auth/Invite.js
+++ b/src/components/auth/Invite.js
@@ -65,7 +65,7 @@ class Invite extends Component {
65 { 65 {
66 fields: { 66 fields: {
67 invite: [ 67 invite: [
68 ...Array(3).fill({ 68 ...Array.from({ length: 3 }).fill({
69 fields: { 69 fields: {
70 name: { 70 name: {
71 label: this.props.intl.formatMessage(messages.nameLabel), 71 label: this.props.intl.formatMessage(messages.nameLabel),
@@ -95,19 +95,19 @@ class Invite extends Component {
95 this.props.intl, 95 this.props.intl,
96 ); 96 );
97 97
98 document.querySelector('input:first-child').focus(); 98 document.querySelector('input:first-child')?.focus();
99 } 99 }
100 100
101 submit(e) { 101 submit(e) {
102 e.preventDefault(); 102 e.preventDefault();
103 103
104 this.form.submit({ 104 this.form?.submit({
105 onSuccess: form => { 105 onSuccess: form => {
106 this.props.onSubmit({ invites: form.values().invite }); 106 this.props.onSubmit({ invites: form.values().invite });
107 107
108 this.form.clear(); 108 this.form?.clear();
109 // this.form.$('invite.0.name').focus(); // path accepted but does not focus ;( 109 // this.form.$('invite.0.name').focus(); // path accepted but does not focus ;(
110 document.querySelector('input:first-child').focus(); 110 document.querySelector('input:first-child')?.focus();
111 this.setState({ showSuccessInfo: true }); 111 this.setState({ showSuccessInfo: true });
112 }, 112 },
113 onError: () => {}, 113 onError: () => {},
diff --git a/src/components/settings/SettingsLayout.js b/src/components/settings/SettingsLayout.js
index 0574b3765..71250bd4d 100644
--- a/src/components/settings/SettingsLayout.js
+++ b/src/components/settings/SettingsLayout.js
@@ -29,6 +29,7 @@ class SettingsLayout extends Component {
29 componentWillUnmount() { 29 componentWillUnmount() {
30 document.removeEventListener( 30 document.removeEventListener(
31 'keydown', 31 'keydown',
32 // eslint-disable-next-line unicorn/no-invalid-remove-event-listener
32 this.handleKeyDown.bind(this), 33 this.handleKeyDown.bind(this),
33 false, 34 false,
34 ); 35 );
diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js
index 9a9abeab4..7df6d5c78 100644
--- a/src/components/settings/services/EditServiceForm.js
+++ b/src/components/settings/services/EditServiceForm.js
@@ -183,8 +183,8 @@ class EditServiceForm extends Component {
183 removeTrailingSlash: false, 183 removeTrailingSlash: false,
184 }); 184 });
185 isValid = await recipe.validateUrl(values.customUrl); 185 isValid = await recipe.validateUrl(values.customUrl);
186 } catch (err) { 186 } catch (error) {
187 console.warn('ValidateURL', err); 187 console.warn('ValidateURL', error);
188 isValid = false; 188 isValid = false;
189 } 189 }
190 } 190 }
diff --git a/src/components/settings/services/ServicesDashboard.js b/src/components/settings/services/ServicesDashboard.js
index 847f2ea06..9272b05c9 100644
--- a/src/components/settings/services/ServicesDashboard.js
+++ b/src/components/settings/services/ServicesDashboard.js
@@ -91,7 +91,7 @@ class ServicesDashboard extends Component {
91 <h1>{intl.formatMessage(messages.headline)}</h1> 91 <h1>{intl.formatMessage(messages.headline)}</h1>
92 </div> 92 </div>
93 <div className="settings__body"> 93 <div className="settings__body">
94 {(services.length !== 0 || searchNeedle) && !isLoading && ( 94 {(services.length > 0 || searchNeedle) && !isLoading && (
95 <SearchInput 95 <SearchInput
96 placeholder={intl.formatMessage(messages.searchService)} 96 placeholder={intl.formatMessage(messages.searchService)}
97 onChange={needle => filterServices({ needle })} 97 onChange={needle => filterServices({ needle })}
diff --git a/src/components/ui/ImageUpload.js b/src/components/ui/ImageUpload.js
index 8ea31ca40..49aff389b 100644
--- a/src/components/ui/ImageUpload.js
+++ b/src/components/ui/ImageUpload.js
@@ -30,14 +30,14 @@ class ImageUpload extends Component {
30 onDrop(acceptedFiles) { 30 onDrop(acceptedFiles) {
31 const { field } = this.props; 31 const { field } = this.props;
32 32
33 acceptedFiles.forEach(file => { 33 for (const file of acceptedFiles) {
34 const imgPath = isWindows ? file.path.replace(/\\/g, '/') : file.path; 34 const imgPath = isWindows ? file.path.replace(/\\/g, '/') : file.path;
35 this.setState({ 35 this.setState({
36 path: imgPath, 36 path: imgPath,
37 }); 37 });
38 38
39 this.props.field.onDrop(file); 39 this.props.field.onDrop(file);
40 }); 40 }
41 41
42 field.set(''); 42 field.set('');
43 } 43 }
diff --git a/src/components/ui/InfoBar.js b/src/components/ui/InfoBar.js
index f5cbad48b..dc6be10da 100644
--- a/src/components/ui/InfoBar.js
+++ b/src/components/ui/InfoBar.js
@@ -5,7 +5,6 @@ import classnames from 'classnames';
5import Loader from 'react-loader'; 5import Loader from 'react-loader';
6import { defineMessages, injectIntl } from 'react-intl'; 6import { defineMessages, injectIntl } from 'react-intl';
7 7
8// import { oneOrManyChildElements } from '../../prop-types';
9import Appear from './effects/Appear'; 8import Appear from './effects/Appear';
10 9
11const messages = defineMessages({ 10const messages = defineMessages({
@@ -18,7 +17,7 @@ const messages = defineMessages({
18@observer 17@observer
19class InfoBar extends Component { 18class InfoBar extends Component {
20 static propTypes = { 19 static propTypes = {
21 // eslint-disable-next-line 20 // eslint-disable-next-line react/forbid-prop-types
22 children: PropTypes.any.isRequired, 21 children: PropTypes.any.isRequired,
23 onClick: PropTypes.func, 22 onClick: PropTypes.func,
24 type: PropTypes.string, 23 type: PropTypes.string,
diff --git a/src/components/ui/Infobox.js b/src/components/ui/Infobox.js
index 13ae2303b..9e34bf110 100644
--- a/src/components/ui/Infobox.js
+++ b/src/components/ui/Infobox.js
@@ -15,7 +15,8 @@ const messages = defineMessages({
15@observer 15@observer
16class Infobox extends Component { 16class Infobox extends Component {
17 static propTypes = { 17 static propTypes = {
18 children: PropTypes.any.isRequired, // eslint-disable-line 18 // eslint-disable-next-line react/forbid-prop-types
19 children: PropTypes.any.isRequired,
19 icon: PropTypes.string, 20 icon: PropTypes.string,
20 type: PropTypes.string, 21 type: PropTypes.string,
21 ctaOnClick: PropTypes.func, 22 ctaOnClick: PropTypes.func,
diff --git a/src/components/ui/Modal/index.js b/src/components/ui/Modal/index.js
index 9e6830b0c..3c7c66c59 100644
--- a/src/components/ui/Modal/index.js
+++ b/src/components/ui/Modal/index.js
@@ -54,7 +54,7 @@ class Modal extends Component {
54 portal={portal} 54 portal={portal}
55 onRequestClose={close} 55 onRequestClose={close}
56 shouldCloseOnOverlayClick={shouldCloseOnOverlayClick} 56 shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
57 appElement={document.getElementById('root')} 57 appElement={document.querySelector('#root')}
58 > 58 >
59 {showClose && close && ( 59 {showClose && close && (
60 <button type="button" className={classes.close} onClick={close}> 60 <button type="button" className={classes.close} onClick={close}>
diff --git a/src/components/ui/Select.js b/src/components/ui/Select.js
index 15b4c28e7..5ac7ddd6d 100644
--- a/src/components/ui/Select.js
+++ b/src/components/ui/Select.js
@@ -49,7 +49,7 @@ class Select extends Component {
49 let selected = field.value; 49 let selected = field.value;
50 50
51 if (multiple) { 51 if (multiple) {
52 if (typeof field.value === 'string' && field.value.substr(0, 1) === '[') { 52 if (typeof field.value === 'string' && field.value.slice(0, 1) === '[') {
53 // Value is JSON encoded 53 // Value is JSON encoded
54 selected = JSON.parse(field.value); 54 selected = JSON.parse(field.value);
55 } else if (typeof field.value === 'object') { 55 } else if (typeof field.value === 'object') {
diff --git a/src/components/ui/effects/Appear.js b/src/components/ui/effects/Appear.js
index 1255fce2e..183181f8f 100644
--- a/src/components/ui/effects/Appear.js
+++ b/src/components/ui/effects/Appear.js
@@ -1,11 +1,11 @@
1/* eslint-disable react/no-did-mount-set-state */
2import React, { Component } from 'react'; 1import React, { Component } from 'react';
3import PropTypes from 'prop-types'; 2import PropTypes from 'prop-types';
4import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; 3import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
5 4
6export default class Appear extends Component { 5export default class Appear extends Component {
7 static propTypes = { 6 static propTypes = {
8 children: PropTypes.any.isRequired, // eslint-disable-line 7 // eslint-disable-next-line react/forbid-prop-types
8 children: PropTypes.any.isRequired,
9 transitionName: PropTypes.string, 9 transitionName: PropTypes.string,
10 className: PropTypes.string, 10 className: PropTypes.string,
11 }; 11 };
@@ -24,11 +24,7 @@ export default class Appear extends Component {
24 } 24 }
25 25
26 render() { 26 render() {
27 const { 27 const { children, transitionName, className } = this.props;
28 children,
29 transitionName,
30 className,
31 } = this.props;
32 28
33 if (!this.state.mounted) { 29 if (!this.state.mounted) {
34 return null; 30 return null;
diff --git a/src/config.ts b/src/config.ts
index 7bb2525a5..6ad58c7a5 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -5,7 +5,7 @@ import ms from 'ms';
5export const CHECK_INTERVAL = ms('1h'); // How often should we perform checks 5export const CHECK_INTERVAL = ms('1h'); // How often should we perform checks
6 6
7export const LOCAL_HOSTNAME = 'localhost'; 7export const LOCAL_HOSTNAME = 'localhost';
8export const LOCAL_PORT = 45569; 8export const LOCAL_PORT = 45_569;
9export const LOCAL_API = 'http://localhost:3000'; 9export const LOCAL_API = 'http://localhost:3000';
10export const DEV_FRANZ_API = 'https://dev.franzinfra.com'; 10export const DEV_FRANZ_API = 'https://dev.franzinfra.com';
11 11
diff --git a/src/containers/settings/AccountScreen.js b/src/containers/settings/AccountScreen.js
index cc3929656..0f9457fd0 100644
--- a/src/containers/settings/AccountScreen.js
+++ b/src/containers/settings/AccountScreen.js
@@ -32,14 +32,12 @@ class AccountScreen extends Component {
32 32
33 const api = stores.settings.all.app.server; 33 const api = stores.settings.all.app.server;
34 34
35 let url; 35 const url =
36 if (api === LIVE_FRANZ_API) { 36 api === LIVE_FRANZ_API
37 url = stores.user.getAuthURL( 37 ? stores.user.getAuthURL(
38 `${WEBSITE}${route}?utm_source=app&utm_medium=account_dashboard`, 38 `${WEBSITE}${route}?utm_source=app&utm_medium=account_dashboard`,
39 ); 39 )
40 } else { 40 : `${api}${route}`;
41 url = `${api}${route}`;
42 }
43 41
44 actions.app.openExternalUrl({ url }); 42 actions.app.openExternalUrl({ url });
45 } 43 }
diff --git a/src/containers/settings/SettingsWindow.js b/src/containers/settings/SettingsWindow.js
index 58e73f2f3..e03c4c1d2 100644
--- a/src/containers/settings/SettingsWindow.js
+++ b/src/containers/settings/SettingsWindow.js
@@ -19,11 +19,11 @@ class SettingsContainer extends Component {
19 el = document.createElement('div'); 19 el = document.createElement('div');
20 20
21 componentDidMount() { 21 componentDidMount() {
22 this.portalRoot.appendChild(this.el); 22 this.portalRoot.append(this.el);
23 } 23 }
24 24
25 componentWillUnmount() { 25 componentWillUnmount() {
26 this.portalRoot.removeChild(this.el); 26 this.el.remove();
27 } 27 }
28 28
29 render() { 29 render() {
diff --git a/src/electron/ipc-api/appIndicator.ts b/src/electron/ipc-api/appIndicator.ts
index 5b5f2bac7..a51ed8161 100644
--- a/src/electron/ipc-api/appIndicator.ts
+++ b/src/electron/ipc-api/appIndicator.ts
@@ -34,8 +34,7 @@ export default params => {
34 34
35 ipcMain.on('updateAppIndicator', (_event, args) => { 35 ipcMain.on('updateAppIndicator', (_event, args) => {
36 // Flash TaskBar for windows, bounce Dock on Mac 36 // Flash TaskBar for windows, bounce Dock on Mac
37 if (!(app as any).mainWindow.isFocused()) { 37 if (!(app as any).mainWindow.isFocused() && params.settings.app.get('notifyTaskBarOnMessage')) {
38 if (params.settings.app.get('notifyTaskBarOnMessage')) {
39 if (isWindows) { 38 if (isWindows) {
40 (app as any).mainWindow.flashFrame(true); 39 (app as any).mainWindow.flashFrame(true);
41 (app as any).mainWindow.once('focus', () => 40 (app as any).mainWindow.once('focus', () =>
@@ -45,7 +44,6 @@ export default params => {
45 app.dock.bounce('informational'); 44 app.dock.bounce('informational');
46 } 45 }
47 } 46 }
48 }
49 47
50 // Update badge 48 // Update badge
51 if (isMac && typeof args.indicator === 'string') { 49 if (isMac && typeof args.indicator === 'string') {
diff --git a/src/electron/ipc-api/autoUpdate.ts b/src/electron/ipc-api/autoUpdate.ts
index 70890539d..31c614ab7 100644
--- a/src/electron/ipc-api/autoUpdate.ts
+++ b/src/electron/ipc-api/autoUpdate.ts
@@ -44,8 +44,8 @@ export default (params: { mainWindow: BrowserWindow; settings: any }) => {
44 app.quit(); 44 app.quit();
45 }, 20); 45 }, 20);
46 } 46 }
47 } catch (e) { 47 } catch (error) {
48 console.error(e); 48 console.error(error);
49 event.sender.send('autoUpdate', { error: true }); 49 event.sender.send('autoUpdate', { error: true });
50 } 50 }
51 } 51 }
diff --git a/src/electron/ipc-api/cld.ts b/src/electron/ipc-api/cld.ts
index b907f3730..4221f9b22 100644
--- a/src/electron/ipc-api/cld.ts
+++ b/src/electron/ipc-api/cld.ts
@@ -16,8 +16,8 @@ export default async () => {
16 16
17 return result.languages[0].code; 17 return result.languages[0].code;
18 } 18 }
19 } catch (e) { 19 } catch (error) {
20 console.error(e); 20 console.error(error);
21 } 21 }
22 }); 22 });
23}; 23};
diff --git a/src/electron/ipc-api/dnd.ts b/src/electron/ipc-api/dnd.ts
index 6fb8999a3..afaef9a66 100644
--- a/src/electron/ipc-api/dnd.ts
+++ b/src/electron/ipc-api/dnd.ts
@@ -15,8 +15,8 @@ export default async () => {
15 const isDND = getDoNotDisturb(); 15 const isDND = getDoNotDisturb();
16 debug('Fetching DND state, set to', isDND); 16 debug('Fetching DND state, set to', isDND);
17 return isDND; 17 return isDND;
18 } catch (e) { 18 } catch (error) {
19 console.error(e); 19 console.error(error);
20 return false; 20 return false;
21 } 21 }
22 }); 22 });
diff --git a/src/electron/ipc-api/download.ts b/src/electron/ipc-api/download.ts
index 822658f26..af15b157e 100644
--- a/src/electron/ipc-api/download.ts
+++ b/src/electron/ipc-api/download.ts
@@ -7,7 +7,7 @@ import { PathLike } from 'fs';
7const debug = require('debug')('Ferdi:ipcApi:download'); 7const debug = require('debug')('Ferdi:ipcApi:download');
8 8
9function decodeBase64Image(dataString: string) { 9function decodeBase64Image(dataString: string) {
10 const matches = dataString.match(/^data:([A-Za-z-+/]+);base64,(.+)$/); 10 const matches = dataString.match(/^data:([+/A-Za-z-]+);base64,(.+)$/);
11 11
12 if (matches?.length !== 3) { 12 if (matches?.length !== 3) {
13 return new Error('Invalid input string'); 13 return new Error('Invalid input string');
@@ -47,12 +47,12 @@ export default (params: { mainWindow: BrowserWindow }) => {
47 ); 47 );
48 48
49 debug('File blob saved to', saveDialog.filePath); 49 debug('File blob saved to', saveDialog.filePath);
50 } catch (err) { 50 } catch (error) {
51 console.log(err); 51 console.log(error);
52 } 52 }
53 } 53 }
54 } catch (e) { 54 } catch (error) {
55 console.error(e); 55 console.error(error);
56 } 56 }
57 }, 57 },
58 ); 58 );
diff --git a/src/features/appearance/index.js b/src/features/appearance/index.js
index d1db68ac6..0c935be32 100644
--- a/src/features/appearance/index.js
+++ b/src/features/appearance/index.js
@@ -14,7 +14,7 @@ function createStyleElement() {
14} 14}
15 15
16function setAppearance(style) { 16function setAppearance(style) {
17 const styleElement = document.getElementById(STYLE_ELEMENT_ID); 17 const styleElement = document.querySelector(`#${STYLE_ELEMENT_ID}`);
18 18
19 if (styleElement) { 19 if (styleElement) {
20 styleElement.innerHTML = style; 20 styleElement.innerHTML = style;
@@ -30,18 +30,18 @@ function darkenAbsolute(originalColor, absoluteChange) {
30function generateAccentStyle(accentColorStr) { 30function generateAccentStyle(accentColorStr) {
31 let style = ''; 31 let style = '';
32 32
33 Object.keys(themeInfo).forEach(property => { 33 for (const property of Object.keys(themeInfo)) {
34 style += ` 34 style += `
35 ${themeInfo[property]} { 35 ${themeInfo[property]} {
36 ${property}: ${accentColorStr}; 36 ${property}: ${accentColorStr};
37 } 37 }
38 `; 38 `;
39 }); 39 }
40 40
41 let accentColor = color(DEFAULT_APP_SETTINGS.accentColor); 41 let accentColor = color(DEFAULT_APP_SETTINGS.accentColor);
42 try { 42 try {
43 accentColor = color(accentColorStr); 43 accentColor = color(accentColorStr);
44 } catch (e) { 44 } catch {
45 // Ignore invalid accent color. 45 // Ignore invalid accent color.
46 } 46 }
47 const darkerColorStr = darkenAbsolute(accentColor, 5).hex(); 47 const darkerColorStr = darkenAbsolute(accentColor, 5).hex();
@@ -133,14 +133,14 @@ function generateShowDragAreaStyle(accentColor) {
133} 133}
134 134
135function generateVerticalStyle(widthStr, alwaysShowWorkspaces) { 135function generateVerticalStyle(widthStr, alwaysShowWorkspaces) {
136 if (!document.getElementById('vertical-style')) { 136 if (!document.querySelector('#vertical-style')) {
137 const link = document.createElement('link'); 137 const link = document.createElement('link');
138 link.id = 'vertical-style'; 138 link.id = 'vertical-style';
139 link.rel = 'stylesheet'; 139 link.rel = 'stylesheet';
140 link.type = 'text/css'; 140 link.type = 'text/css';
141 link.href = './styles/vertical.css'; 141 link.href = './styles/vertical.css';
142 142
143 document.head.appendChild(link); 143 document.head.append(link);
144 } 144 }
145 const width = Number(widthStr); 145 const width = Number(widthStr);
146 const sidebarWidth = width - 4; 146 const sidebarWidth = width - 4;
@@ -150,12 +150,12 @@ function generateVerticalStyle(widthStr, alwaysShowWorkspaces) {
150 .sidebar { 150 .sidebar {
151 height: ${sidebarWidth + verticalStyleOffset + 1}px !important; 151 height: ${sidebarWidth + verticalStyleOffset + 1}px !important;
152 ${ 152 ${
153 alwaysShowWorkspaces 153 alwaysShowWorkspaces
154 ? ` 154 ? `
155 width: calc(100% - 300px) !important; 155 width: calc(100% - 300px) !important;
156 ` 156 `
157 : '' 157 : ''
158} 158 }
159 } 159 }
160 160
161 .sidebar .sidebar__button { 161 .sidebar .sidebar__button {
@@ -220,10 +220,10 @@ function generateStyle(settings) {
220 } 220 }
221 if (useVerticalStyle) { 221 if (useVerticalStyle) {
222 style += generateVerticalStyle(serviceRibbonWidth, alwaysShowWorkspaces); 222 style += generateVerticalStyle(serviceRibbonWidth, alwaysShowWorkspaces);
223 } else if (document.getElementById('vertical-style')) { 223 } else if (document.querySelector('#vertical-style')) {
224 const link = document.getElementById('vertical-style'); 224 const link = document.querySelector('#vertical-style');
225 if (link) { 225 if (link) {
226 document.head.removeChild(link); 226 link.remove();
227 } 227 }
228 } 228 }
229 if (alwaysShowWorkspaces) { 229 if (alwaysShowWorkspaces) {
diff --git a/src/features/communityRecipes/store.js b/src/features/communityRecipes/store.js
index a3614dd11..05e18e2f7 100644
--- a/src/features/communityRecipes/store.js
+++ b/src/features/communityRecipes/store.js
@@ -18,11 +18,13 @@ export class CommunityRecipesStore extends FeatureStore {
18 @computed get communityRecipes() { 18 @computed get communityRecipes() {
19 if (!this.stores) return []; 19 if (!this.stores) return [];
20 20
21 return this.stores.recipePreviews.dev.map((r) => { 21 return this.stores.recipePreviews.dev.map(recipePreview => {
22 // TODO: Need to figure out if this is even necessary/used 22 // TODO: Need to figure out if this is even necessary/used
23 r.isDevRecipe = !!r.author.find((a) => a.email === this.stores.user.data.email); 23 recipePreview.isDevRecipe = !!recipePreview.author.some(
24 author => author.email === this.stores.user.data.email,
25 );
24 26
25 return r; 27 return recipePreview;
26 }); 28 });
27 } 29 }
28} 30}
diff --git a/src/features/quickSwitch/Component.js b/src/features/quickSwitch/Component.js
index df2bf968d..f21db0ebd 100644
--- a/src/features/quickSwitch/Component.js
+++ b/src/features/quickSwitch/Component.js
@@ -140,7 +140,7 @@ class QuickSwitchModal extends Component {
140 let services = []; 140 let services = [];
141 if ( 141 if (
142 this.state.search && 142 this.state.search &&
143 compact(invoke(this.state.search, 'match', /^[a-z0-9]/i)).length > 0 143 compact(invoke(this.state.search, 'match', /^[\da-z]/i)).length > 0
144 ) { 144 ) {
145 // Apply simple search algorythm to list of all services 145 // Apply simple search algorythm to list of all services
146 services = this.props.stores.services.allDisplayed; 146 services = this.props.stores.services.allDisplayed;
@@ -261,7 +261,7 @@ class QuickSwitchModal extends Component {
261 // Wrapped inside timeout to let the modal render first 261 // Wrapped inside timeout to let the modal render first
262 setTimeout(() => { 262 setTimeout(() => {
263 if (this.inputRef.current) { 263 if (this.inputRef.current) {
264 this.inputRef.current.getElementsByTagName('input')[0].focus(); 264 this.inputRef.current.querySelectorAll('input')[0].focus();
265 } 265 }
266 }, 10); 266 }, 10);
267 267
@@ -273,7 +273,7 @@ class QuickSwitchModal extends Component {
273 // search query change when modal not visible 273 // search query change when modal not visible
274 setTimeout(() => { 274 setTimeout(() => {
275 if (this.inputRef.current) { 275 if (this.inputRef.current) {
276 this.inputRef.current.getElementsByTagName('input')[0].blur(); 276 this.inputRef.current.querySelectorAll('input')[0].blur();
277 } 277 }
278 }, 100); 278 }, 100);
279 279
diff --git a/src/features/serviceProxy/index.js b/src/features/serviceProxy/index.js
index eb7116651..b9320cda9 100644
--- a/src/features/serviceProxy/index.js
+++ b/src/features/serviceProxy/index.js
@@ -18,7 +18,7 @@ export default function init(stores) {
18 18
19 debug('Service Proxy autorun'); 19 debug('Service Proxy autorun');
20 20
21 services.forEach((service) => { 21 for (const service of services) {
22 const s = session.fromPartition(`persist:service-${service.id}`); 22 const s = session.fromPartition(`persist:service-${service.id}`);
23 23
24 if (config.isEnabled) { 24 if (config.isEnabled) {
@@ -33,6 +33,6 @@ export default function init(stores) {
33 }); 33 });
34 } 34 }
35 } 35 }
36 }); 36 }
37 }); 37 });
38} 38}
diff --git a/src/features/settingsWS/store.js b/src/features/settingsWS/store.js
index 9100f33d1..e36ccee72 100755
--- a/src/features/settingsWS/store.js
+++ b/src/features/settingsWS/store.js
@@ -25,11 +25,13 @@ export class SettingsWSStore extends FeatureStore {
25 this.stores = stores; 25 this.stores = stores;
26 this.actions = actions; 26 this.actions = actions;
27 27
28 this._registerReactions(createReactions([ 28 this._registerReactions(
29 this._initialize.bind(this), 29 createReactions([
30 this._reconnect.bind(this), 30 this._initialize.bind(this),
31 this._close.bind(this), 31 this._reconnect.bind(this),
32 ])); 32 this._close.bind(this),
33 ]),
34 );
33 } 35 }
34 36
35 connect() { 37 connect() {
@@ -51,12 +53,12 @@ export class SettingsWSStore extends FeatureStore {
51 this.heartbeat(); 53 this.heartbeat();
52 }); 54 });
53 55
54 this.ws.on('message', (data) => { 56 this.ws.on('message', data => {
55 const resp = JSON.parse(data); 57 const resp = JSON.parse(data);
56 debug('Received message', resp); 58 debug('Received message', resp);
57 59
58 if (resp.id) { 60 if (resp.id) {
59 this.stores.user.getUserInfoRequest.patch((result) => { 61 this.stores.user.getUserInfoRequest.patch(result => {
60 if (!result) return; 62 if (!result) return;
61 63
62 debug('Patching user object with new values'); 64 debug('Patching user object with new values');
@@ -66,8 +68,8 @@ export class SettingsWSStore extends FeatureStore {
66 }); 68 });
67 69
68 this.ws.on('ping', this.heartbeat.bind(this)); 70 this.ws.on('ping', this.heartbeat.bind(this));
69 } catch (err) { 71 } catch (error) {
70 console.err(err); 72 console.error(error);
71 } 73 }
72 } 74 }
73 75
diff --git a/src/features/todos/preload.js b/src/features/todos/preload.js
index 9bd76a704..3b86ddbc5 100644
--- a/src/features/todos/preload.js
+++ b/src/features/todos/preload.js
@@ -7,7 +7,9 @@ debug('Preloading Todos Webview');
7 7
8let hostMessageListener = ({ action }) => { 8let hostMessageListener = ({ action }) => {
9 switch (action) { 9 switch (action) {
10 case 'todos:initialize-as-service': ipcRenderer.sendToHost('hello'); break; 10 case 'todos:initialize-as-service':
11 ipcRenderer.sendToHost('hello');
12 break;
11 default: 13 default:
12 } 14 }
13}; 15};
@@ -15,7 +17,9 @@ let hostMessageListener = ({ action }) => {
15window.ferdi = { 17window.ferdi = {
16 onInitialize(ipcHostMessageListener) { 18 onInitialize(ipcHostMessageListener) {
17 hostMessageListener = ipcHostMessageListener; 19 hostMessageListener = ipcHostMessageListener;
18 ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, { action: 'todos:initialized' }); 20 ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, {
21 action: 'todos:initialized',
22 });
19 }, 23 },
20 sendToHost(message) { 24 sendToHost(message) {
21 ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, message); 25 ipcRenderer.sendToHost(IPC.TODOS_CLIENT_CHANNEL, message);
@@ -30,7 +34,7 @@ ipcRenderer.on(IPC.TODOS_HOST_CHANNEL, (event, message) => {
30if (window.location.href === 'https://app.franztodos.com/login/') { 34if (window.location.href === 'https://app.franztodos.com/login/') {
31 // Insert info element informing about Franz accounts 35 // Insert info element informing about Franz accounts
32 const infoElement = document.createElement('p'); 36 const infoElement = document.createElement('p');
33 infoElement.innerText = `You are using Franz's official Todo Service. 37 infoElement.textContent = `You are using Franz's official Todo Service.
34This service will only work with accounts registered with Franz - no Ferdi accounts will work here! 38This service will only work with accounts registered with Franz - no Ferdi accounts will work here!
35If you do not have a Franz account you can change the Todo service by going into Ferdi's settings and changing the "Todo server". 39If you do not have a Franz account you can change the Todo service by going into Ferdi's settings and changing the "Todo server".
36You can choose any service as this Todo server, e.g. Todoist or Apple Notes.`; 40You can choose any service as this Todo server, e.g. Todoist or Apple Notes.`;
@@ -42,7 +46,7 @@ You can choose any service as this Todo server, e.g. Todoist or Apple Notes.`;
42 const textElement = document.querySelector('p'); 46 const textElement = document.querySelector('p');
43 if (textElement) { 47 if (textElement) {
44 clearInterval(waitForReact); 48 clearInterval(waitForReact);
45 textElement.parentElement.insertBefore(infoElement, textElement); 49 textElement.parentElement?.insertBefore(infoElement, textElement);
46 } else { 50 } else {
47 numChecks += 1; 51 numChecks += 1;
48 52
diff --git a/src/features/utils/FeatureStore.js b/src/features/utils/FeatureStore.js
index 4d4e217a9..afe726294 100644
--- a/src/features/utils/FeatureStore.js
+++ b/src/features/utils/FeatureStore.js
@@ -16,11 +16,11 @@ export class FeatureStore {
16 } 16 }
17 17
18 _startActions(actions = this._actions) { 18 _startActions(actions = this._actions) {
19 actions.forEach((a) => a.start()); 19 for (const a of actions) a.start();
20 } 20 }
21 21
22 _stopActions(actions = this._actions) { 22 _stopActions(actions = this._actions) {
23 actions.forEach((a) => a.stop()); 23 for (const a of actions) a.stop();
24 } 24 }
25 25
26 // REACTIONS 26 // REACTIONS
@@ -31,10 +31,10 @@ export class FeatureStore {
31 } 31 }
32 32
33 _startReactions(reactions = this._reactions) { 33 _startReactions(reactions = this._reactions) {
34 reactions.forEach((r) => r.start()); 34 for (const r of reactions) r.start();
35 } 35 }
36 36
37 _stopReactions(reactions = this._reactions) { 37 _stopReactions(reactions = this._reactions) {
38 reactions.forEach((r) => r.stop()); 38 for (const r of reactions) r.stop();
39 } 39 }
40} 40}
diff --git a/src/features/webControls/containers/WebControlsScreen.js b/src/features/webControls/containers/WebControlsScreen.js
index e1e1b9991..0273bb13e 100644
--- a/src/features/webControls/containers/WebControlsScreen.js
+++ b/src/features/webControls/containers/WebControlsScreen.js
@@ -16,7 +16,8 @@ const URL_EVENTS = [
16 'did-navigate-in-page', 16 'did-navigate-in-page',
17]; 17];
18 18
19@inject('stores', 'actions') @observer 19@inject('stores', 'actions')
20@observer
20class WebControlsScreen extends Component { 21class WebControlsScreen extends Component {
21 @observable url = ''; 22 @observable url = '';
22 23
@@ -36,15 +37,15 @@ class WebControlsScreen extends Component {
36 this.webview = service.webview; 37 this.webview = service.webview;
37 this.url = this.webview.getURL(); 38 this.url = this.webview.getURL();
38 39
39 URL_EVENTS.forEach((event) => { 40 for (const event of URL_EVENTS) {
40 this.webview.addEventListener(event, (e) => { 41 this.webview.addEventListener(event, e => {
41 if (!e.isMainFrame) return; 42 if (!e.isMainFrame) return;
42 43
43 this.url = e.url; 44 this.url = e.url;
44 this.canGoBack = this.webview.canGoBack(); 45 this.canGoBack = this.webview.canGoBack();
45 this.canGoForward = this.webview.canGoForward(); 46 this.canGoForward = this.webview.canGoForward();
46 }); 47 });
47 }); 48 }
48 } 49 }
49 }); 50 });
50 } 51 }
@@ -83,13 +84,16 @@ class WebControlsScreen extends Component {
83 84
84 try { 85 try {
85 url = new URL(url).toString(); 86 url = new URL(url).toString();
86 } catch (err) { 87 } catch {
87 // eslint-disable-next-line no-useless-escape 88 url =
88 if (url.match(/^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9\-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$/)) { 89 // eslint-disable-next-line no-useless-escape
89 url = `http://${url}`; 90 /^((?!-))(xn--)?[\da-z][\d_a-z-]{0,61}[\da-z]{0,1}\.(xn--)?([\da-z\-]{1,61}|[\da-z-]{1,30}\.[a-z]{2,})$/.test(
90 } else { 91 url,
91 url = SEARCH_ENGINE_URLS[this.settings.app.searchEngine]({ searchTerm: url }); 92 )
92 } 93 ? `http://${url}`
94 : SEARCH_ENGINE_URLS[this.settings.app.searchEngine]({
95 searchTerm: url,
96 });
93 } 97 }
94 98
95 this.webview.loadURL(url); 99 this.webview.loadURL(url);
@@ -114,7 +118,7 @@ class WebControlsScreen extends Component {
114 goBack={() => this.goBack()} 118 goBack={() => this.goBack()}
115 canGoForward={this.canGoForward} 119 canGoForward={this.canGoForward}
116 goForward={() => this.goForward()} 120 goForward={() => this.goForward()}
117 navigate={(url) => this.navigate(url)} 121 navigate={url => this.navigate(url)}
118 url={this.url} 122 url={this.url}
119 /> 123 />
120 ); 124 );
diff --git a/src/features/workspaces/components/EditWorkspaceForm.js b/src/features/workspaces/components/EditWorkspaceForm.js
index cae95e9ed..f562733dd 100644
--- a/src/features/workspaces/components/EditWorkspaceForm.js
+++ b/src/features/workspaces/components/EditWorkspaceForm.js
@@ -108,7 +108,7 @@ class EditWorkspaceForm extends Component {
108 default: false, 108 default: false,
109 }, 109 },
110 services: { 110 services: {
111 value: workspace.services.slice(), 111 value: [...workspace.services],
112 }, 112 },
113 }, 113 },
114 }); 114 });
diff --git a/src/features/workspaces/components/WorkspaceDrawerItem.js b/src/features/workspaces/components/WorkspaceDrawerItem.js
index 82e1b81a4..7df2b60be 100644
--- a/src/features/workspaces/components/WorkspaceDrawerItem.js
+++ b/src/features/workspaces/components/WorkspaceDrawerItem.js
@@ -143,7 +143,7 @@ class WorkspaceDrawerItem extends Component {
143 isActive ? classes.activeServices : null, 143 isActive ? classes.activeServices : null,
144 ])} 144 ])}
145 > 145 >
146 {services.length 146 {services.length > 0
147 ? services.join(', ') 147 ? services.join(', ')
148 : intl.formatMessage(messages.noServicesAddedYet)} 148 : intl.formatMessage(messages.noServicesAddedYet)}
149 </span> 149 </span>
diff --git a/src/features/workspaces/models/Workspace.js b/src/features/workspaces/models/Workspace.js
index d9488e991..14add9437 100644
--- a/src/features/workspaces/models/Workspace.js
+++ b/src/features/workspaces/models/Workspace.js
@@ -15,7 +15,7 @@ export default class Workspace {
15 15
16 constructor(data) { 16 constructor(data) {
17 if (!data.id) { 17 if (!data.id) {
18 throw Error('Workspace requires Id'); 18 throw new Error('Workspace requires Id');
19 } 19 }
20 20
21 this.id = data.id; 21 this.id = data.id;
diff --git a/src/features/workspaces/store.js b/src/features/workspaces/store.js
index ec9d7ee7f..73e882990 100644
--- a/src/features/workspaces/store.js
+++ b/src/features/workspaces/store.js
@@ -302,8 +302,8 @@ export default class WorkspacesStore extends FeatureStore {
302 const { allServicesRequest } = services; 302 const { allServicesRequest } = services;
303 const servicesHaveBeenLoaded = allServicesRequest.wasExecuted && !allServicesRequest.isError; 303 const servicesHaveBeenLoaded = allServicesRequest.wasExecuted && !allServicesRequest.isError;
304 // Loop through all workspaces and remove invalid service ids (locally) 304 // Loop through all workspaces and remove invalid service ids (locally)
305 this.workspaces.forEach((workspace) => { 305 for (const workspace of this.workspaces) {
306 workspace.services.forEach((serviceId) => { 306 for (const serviceId of workspace.services) {
307 if ( 307 if (
308 servicesHaveBeenLoaded 308 servicesHaveBeenLoaded
309 && !services.one(serviceId) 309 && !services.one(serviceId)
@@ -311,7 +311,7 @@ export default class WorkspacesStore extends FeatureStore {
311 ) { 311 ) {
312 workspace.services.remove(serviceId); 312 workspace.services.remove(serviceId);
313 } 313 }
314 }); 314 }
315 }); 315 }
316 }; 316 };
317} 317}
diff --git a/src/helpers/i18n-helpers.ts b/src/helpers/i18n-helpers.ts
index c1f18f446..ec7dc8e98 100644
--- a/src/helpers/i18n-helpers.ts
+++ b/src/helpers/i18n-helpers.ts
@@ -4,13 +4,11 @@ export function getLocale({
4 let localeStr = locale; 4 let localeStr = locale;
5 if (locales[locale] === undefined) { 5 if (locales[locale] === undefined) {
6 let localeFuzzy: string | undefined; 6 let localeFuzzy: string | undefined;
7 Object.keys(locales).forEach((localStr) => { 7 for (const localStr of Object.keys(locales)) {
8 if (locales && Object.hasOwnProperty.call(locales, localStr)) { 8 if (locales && Object.hasOwnProperty.call(locales, localStr) && locale.slice(0, 2) === localStr.slice(0, 2)) {
9 if (locale.substring(0, 2) === localStr.substring(0, 2)) {
10 localeFuzzy = localStr; 9 localeFuzzy = localStr;
11 } 10 }
12 } 11 }
13 });
14 12
15 if (localeFuzzy !== undefined) { 13 if (localeFuzzy !== undefined) {
16 localeStr = localeFuzzy; 14 localeStr = localeFuzzy;
@@ -61,12 +59,12 @@ export function getSelectOptions({
61 if (sort) { 59 if (sort) {
62 keys = keys.sort(Intl.Collator().compare); 60 keys = keys.sort(Intl.Collator().compare);
63 } 61 }
64 keys.forEach((key) => { 62 for (const key of keys) {
65 options.push({ 63 options.push({
66 value: key, 64 value: key,
67 label: locales[key], 65 label: locales[key],
68 }); 66 });
69 }); 67 }
70 68
71 return options; 69 return options;
72} 70}
diff --git a/src/helpers/password-helpers.ts b/src/helpers/password-helpers.ts
index 89c75c752..e5d9a4a25 100644
--- a/src/helpers/password-helpers.ts
+++ b/src/helpers/password-helpers.ts
@@ -12,9 +12,9 @@ export function scorePassword(password: string) {
12 12
13 // award every unique letter until 5 repetitions 13 // award every unique letter until 5 repetitions
14 const letters = {}; 14 const letters = {};
15 for (let i = 0; i < password.length; i += 1) { 15 for (const letter of password) {
16 letters[password[i]] = (letters[password[i]] || 0) + 1; 16 letters[letter] = (letters[letter] || 0) + 1;
17 score += 5.0 / letters[password[i]]; 17 score += 5 / letters[letter];
18 } 18 }
19 19
20 // bonus points for mixing it up 20 // bonus points for mixing it up
@@ -26,11 +26,11 @@ export function scorePassword(password: string) {
26 }; 26 };
27 27
28 let variationCount = 0; 28 let variationCount = 0;
29 Object.keys(variations).forEach((key) => { 29 for (const key of Object.keys(variations)) {
30 variationCount += (variations[key] === true) ? 1 : 0; 30 variationCount += variations[key] === true ? 1 : 0;
31 }); 31 }
32 32
33 score += (variationCount - 1) * 10; 33 score += (variationCount - 1) * 10;
34 34
35 return parseInt(score.toString(), 10); 35 return Number.parseInt(score.toString(), 10);
36} 36}
diff --git a/src/helpers/recipe-helpers.ts b/src/helpers/recipe-helpers.ts
index 965429210..65ef04088 100644
--- a/src/helpers/recipe-helpers.ts
+++ b/src/helpers/recipe-helpers.ts
@@ -1,3 +1,4 @@
1/* eslint-disable global-require */
1import { parse } from 'path'; 2import { parse } from 'path';
2import { userDataRecipesPath } from '../environment'; 3import { userDataRecipesPath } from '../environment';
3 4
@@ -15,20 +16,17 @@ export function loadRecipeConfig(recipeId: string) {
15 // Delete module from cache 16 // Delete module from cache
16 delete require.cache[require.resolve(configPath)]; 17 delete require.cache[require.resolve(configPath)];
17 18
18 // eslint-disable-next-line 19 // eslint-disable-next-line import/no-dynamic-require
19 let config = require(configPath); 20 const config = require(configPath);
20 21
21 const moduleConfigPath = require.resolve(configPath); 22 const moduleConfigPath = require.resolve(configPath);
22 config.path = parse(moduleConfigPath).dir; 23 config.path = parse(moduleConfigPath).dir;
23 24
24 return config; 25 return config;
25 } catch (e) { 26 } catch (error) {
26 console.error(e); 27 console.error(error);
27 return null; 28 return null;
28 } 29 }
29} 30}
30 31
31module.paths.unshift( 32module.paths.unshift(getDevRecipeDirectory(), getRecipeDirectory());
32 getDevRecipeDirectory(),
33 getRecipeDirectory(),
34);
diff --git a/src/helpers/schedule-helpers.ts b/src/helpers/schedule-helpers.ts
index 754fd5556..55b7c1e6f 100644
--- a/src/helpers/schedule-helpers.ts
+++ b/src/helpers/schedule-helpers.ts
@@ -5,15 +5,15 @@ export function isInTimeframe(start: string, end: string) {
5 startHourStr, 5 startHourStr,
6 startMinuteStr, 6 startMinuteStr,
7 ] = start.split(':'); 7 ] = start.split(':');
8 const startHour = parseInt(startHourStr, 10); 8 const startHour = Number.parseInt(startHourStr, 10);
9 const startMinute = parseInt(startMinuteStr, 10); 9 const startMinute = Number.parseInt(startMinuteStr, 10);
10 10
11 const [ 11 const [
12 endHourStr, 12 endHourStr,
13 endMinuteStr, 13 endMinuteStr,
14 ] = end.split(':'); 14 ] = end.split(':');
15 const endHour = parseInt(endHourStr, 10); 15 const endHour = Number.parseInt(endHourStr, 10);
16 const endMinute = parseInt(endMinuteStr, 10); 16 const endMinute = Number.parseInt(endMinuteStr, 10);
17 17
18 const currentHour = new Date().getHours(); 18 const currentHour = new Date().getHours();
19 const currentMinute = new Date().getMinutes(); 19 const currentMinute = new Date().getMinutes();
diff --git a/src/helpers/url-helpers.ts b/src/helpers/url-helpers.ts
index 3657ae693..1e87ecabb 100644
--- a/src/helpers/url-helpers.ts
+++ b/src/helpers/url-helpers.ts
@@ -12,7 +12,7 @@ export function isValidExternalURL(url: string | URL) {
12 let parsedUrl: URL; 12 let parsedUrl: URL;
13 try { 13 try {
14 parsedUrl = new URL(url.toString()); 14 parsedUrl = new URL(url.toString());
15 } catch (_) { 15 } catch {
16 return false; 16 return false;
17 } 17 }
18 18
diff --git a/src/helpers/userAgent-helpers.ts b/src/helpers/userAgent-helpers.ts
index 73c8bfd03..091a76400 100644
--- a/src/helpers/userAgent-helpers.ts
+++ b/src/helpers/userAgent-helpers.ts
@@ -8,7 +8,7 @@ import {
8function macOS() { 8function macOS() {
9 const version = macosVersion() || ''; 9 const version = macosVersion() || '';
10 let cpuName = os.cpus()[0].model.split(' ')[0]; 10 let cpuName = os.cpus()[0].model.split(' ')[0];
11 if (cpuName && cpuName.match(/\(/)) { 11 if (cpuName && /\(/.test(cpuName)) {
12 cpuName = cpuName.split('(')[0]; 12 cpuName = cpuName.split('(')[0];
13 } 13 }
14 return `Macintosh; ${cpuName} Mac OS X ${version.replace(/\./g, '_')}`; 14 return `Macintosh; ${cpuName} Mac OS X ${version.replace(/\./g, '_')}`;
diff --git a/src/helpers/validation-helpers.ts b/src/helpers/validation-helpers.ts
index 3a9622309..23c297443 100644
--- a/src/helpers/validation-helpers.ts
+++ b/src/helpers/validation-helpers.ts
@@ -49,16 +49,15 @@ export function url({ field }) {
49 const value = field.value.trim(); 49 const value = field.value.trim();
50 let isValid = false; 50 let isValid = false;
51 51
52 if (value !== '') { 52 isValid =
53 // eslint-disable-next-line 53 value !== ''
54 isValid = Boolean( 54 ? Boolean(
55 value.match( 55 // eslint-disable-next-line unicorn/better-regex
56 /(^|[\s.:;?\-\]<(])(https?:\/\/[-\w;/?:@&=+$|_.!~*|'()[\]%#,☺]+[\w/#](\(\))?)(?=$|[\s',|().:;?\-[\]>)])/i, 56 /(^|[\s.:;?\-\]<(])(https?:\/\/[-\w;/?:@&=+$|_.!~*|'()[\]%#,☺]+[\w/#](\(\))?)(?=$|[\s',|().:;?\-[\]>)])/i.test(
57 ), 57 value,
58 ); 58 ),
59 } else { 59 )
60 isValid = true; 60 : true;
61 }
62 61
63 return [ 62 return [
64 isValid, 63 isValid,
diff --git a/src/i18n/apply-branding.js b/src/i18n/apply-branding.js
index 7aeabc4af..8ec573919 100644
--- a/src/i18n/apply-branding.js
+++ b/src/i18n/apply-branding.js
@@ -7,7 +7,7 @@ const path = require('path');
7console.log('Applying Ferdi branding to translations...'); 7console.log('Applying Ferdi branding to translations...');
8 8
9// Keys to ignore when applying branding 9// Keys to ignore when applying branding
10const ignore = [ 10const ignore = new Set([
11 'login.customServerSuggestion', 11 'login.customServerSuggestion',
12 'login.customServerQuestion', 12 'login.customServerQuestion',
13 'settings.app.todoServerInfo', 13 'settings.app.todoServerInfo',
@@ -18,10 +18,10 @@ const ignore = [
18 'settings.team.copy', 18 'settings.team.copy',
19 'settings.team.manageAction', 19 'settings.team.manageAction',
20 'settings.app.serverMoneyInfo', 20 'settings.app.serverMoneyInfo',
21]; 21]);
22 22
23// Files to ignore when applying branding 23// Files to ignore when applying branding
24const ignoreFiles = ['.DS_Store', '.', '..']; 24const ignoreFiles = new Set(['.DS_Store', '.', '..']);
25 25
26// What to replace 26// What to replace
27const replace = { 27const replace = {
@@ -38,14 +38,15 @@ const replaceFind = Object.keys(replace);
38const replaceReplaceWith = Object.values(replace); 38const replaceReplaceWith = Object.values(replace);
39 39
40const replaceStr = (str, find, replaceWith) => { 40const replaceStr = (str, find, replaceWith) => {
41 for (let i = 0; i < find.length; i += 1) { 41 for (const [i, element] of find.entries()) {
42 str = str.replace(new RegExp(find[i], 'gi'), replaceWith[i]); 42 str = str.replace(new RegExp(element, 'gi'), replaceWith[i]);
43 } 43 }
44 return str; 44 return str;
45}; 45};
46 46
47// eslint-disable-next-line unicorn/no-array-for-each
47files.forEach(async file => { 48files.forEach(async file => {
48 if (ignoreFiles.includes(file)) return; 49 if (ignoreFiles.has(file)) return;
49 50
50 // Read locale data 51 // Read locale data
51 const filePath = path.join(locales, file); 52 const filePath = path.join(locales, file);
@@ -53,7 +54,7 @@ files.forEach(async file => {
53 54
54 // Replace branding 55 // Replace branding
55 for (const key in locale) { 56 for (const key in locale) {
56 if (!ignore.includes(key)) { 57 if (!ignore.has(key)) {
57 locale[key] = replaceStr(locale[key], replaceFind, replaceReplaceWith); 58 locale[key] = replaceStr(locale[key], replaceFind, replaceReplaceWith);
58 } 59 }
59 } 60 }
diff --git a/src/i18n/translations.js b/src/i18n/translations.js
index 161a172ba..9a7dc7453 100644
--- a/src/i18n/translations.js
+++ b/src/i18n/translations.js
@@ -1,13 +1,15 @@
1/* eslint-disable global-require */
1import { APP_LOCALES } from './languages'; 2import { APP_LOCALES } from './languages';
2 3
3const translations = []; 4const translations = [];
4Object.keys(APP_LOCALES).forEach((key) => { 5for (const key of Object.keys(APP_LOCALES)) {
5 try { 6 try {
6 const translation = require(`./locales/${key}.json`); // eslint-disable-line 7 // eslint-disable-next-line import/no-dynamic-require
8 const translation = require(`./locales/${key}.json`);
7 translations[key] = translation; 9 translations[key] = translation;
8 } catch (err) { 10 } catch {
9 console.warn(`Can't find translations for ${key}`); 11 console.warn(`Can't find translations for ${key}`);
10 } 12 }
11}); 13}
12 14
13module.exports = translations; 15module.exports = translations;
diff --git a/src/index.js b/src/index.js
index 08d9a3a45..758b11dc9 100644
--- a/src/index.js
+++ b/src/index.js
@@ -147,7 +147,7 @@ if (!gotTheLock) {
147// https://github.com/electron/electron/issues/9046 147// https://github.com/electron/electron/issues/9046
148if ( 148if (
149 isLinux && 149 isLinux &&
150 ['Pantheon', 'Unity:Unity7'].indexOf(process.env.XDG_CURRENT_DESKTOP) !== -1 150 ['Pantheon', 'Unity:Unity7'].includes(process.env.XDG_CURRENT_DESKTOP)
151) { 151) {
152 process.env.XDG_CURRENT_DESKTOP = 'Unity'; 152 process.env.XDG_CURRENT_DESKTOP = 'Unity';
153} 153}
@@ -475,7 +475,7 @@ ipcMain.on(
475 debug( 475 debug(
476 `Received modifyRequestHeaders ${modifiedRequestHeaders} for serviceId ${serviceId}`, 476 `Received modifyRequestHeaders ${modifiedRequestHeaders} for serviceId ${serviceId}`,
477 ); 477 );
478 modifiedRequestHeaders.forEach(headerFilterSet => { 478 for (const headerFilterSet of modifiedRequestHeaders) {
479 const { headers, requestFilters } = headerFilterSet; 479 const { headers, requestFilters } = headerFilterSet;
480 session 480 session
481 .fromPartition(`persist:service-${serviceId}`) 481 .fromPartition(`persist:service-${serviceId}`)
@@ -488,7 +488,7 @@ ipcMain.on(
488 } 488 }
489 callback({ requestHeaders: details.requestHeaders }); 489 callback({ requestHeaders: details.requestHeaders });
490 }); 490 });
491 }); 491 }
492 }, 492 },
493); 493);
494 494
diff --git a/src/internal-server/app/Controllers/Http/RecipeController.js b/src/internal-server/app/Controllers/Http/RecipeController.js
index 1a7595a9d..2c7baf2a4 100644
--- a/src/internal-server/app/Controllers/Http/RecipeController.js
+++ b/src/internal-server/app/Controllers/Http/RecipeController.js
@@ -1,8 +1,6 @@
1const Recipe = use('App/Models/Recipe'); 1const Recipe = use('App/Models/Recipe');
2const Drive = use('Drive'); 2const Drive = use('Drive');
3const { 3const { validateAll } = use('Validator');
4 validateAll,
5} = use('Validator');
6const Env = use('Env'); 4const Env = use('Env');
7 5
8const fetch = require('node-fetch'); 6const fetch = require('node-fetch');
@@ -14,9 +12,7 @@ const RECIPES_URL = `${LIVE_FERDI_API}/${API_VERSION}/recipes`;
14 12
15class RecipeController { 13class RecipeController {
16 // List official and custom recipes 14 // List official and custom recipes
17 async list({ 15 async list({ response }) {
18 response,
19 }) {
20 const officialRecipes = JSON.parse(await (await fetch(RECIPES_URL)).text()); 16 const officialRecipes = JSON.parse(await (await fetch(RECIPES_URL)).text());
21 const customRecipesArray = (await Recipe.all()).rows; 17 const customRecipesArray = (await Recipe.all()).rows;
22 const customRecipes = customRecipesArray.map(recipe => ({ 18 const customRecipes = customRecipesArray.map(recipe => ({
@@ -25,19 +21,13 @@ class RecipeController {
25 ...JSON.parse(recipe.data), 21 ...JSON.parse(recipe.data),
26 })); 22 }));
27 23
28 const recipes = [ 24 const recipes = [...officialRecipes, ...customRecipes];
29 ...officialRecipes,
30 ...customRecipes,
31 ];
32 25
33 return response.send(recipes); 26 return response.send(recipes);
34 } 27 }
35 28
36 // Search official and custom recipes 29 // Search official and custom recipes
37 async search({ 30 async search({ request, response }) {
38 request,
39 response,
40 }) {
41 // Validate user input 31 // Validate user input
42 const validation = await validateAll(request.all(), { 32 const validation = await validateAll(request.all(), {
43 needle: 'required', 33 needle: 'required',
@@ -64,13 +54,23 @@ class RecipeController {
64 })); 54 }));
65 } else { 55 } else {
66 let remoteResults = []; 56 let remoteResults = [];
67 if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq 57 // eslint-disable-next-line eqeqeq
68 remoteResults = JSON.parse(await (await fetch(`${RECIPES_URL}/search?needle=${encodeURIComponent(needle)}`)).text()); 58 if (Env.get('CONNECT_WITH_FRANZ') == 'true') {
59 // eslint-disable-line eqeqeq
60 remoteResults = JSON.parse(
61 await (
62 await fetch(
63 `${RECIPES_URL}/search?needle=${encodeURIComponent(needle)}`,
64 )
65 ).text(),
66 );
69 } 67 }
70 68
71 debug('remoteResults:', remoteResults); 69 debug('remoteResults:', remoteResults);
72 70
73 const localResultsArray = (await Recipe.query().where('name', 'LIKE', `%${needle}%`).fetch()).toJSON(); 71 const localResultsArray = (
72 await Recipe.query().where('name', 'LIKE', `%${needle}%`).fetch()
73 ).toJSON();
74 const localResults = localResultsArray.map(recipe => ({ 74 const localResults = localResultsArray.map(recipe => ({
75 id: recipe.recipeId, 75 id: recipe.recipeId,
76 name: recipe.name, 76 name: recipe.name,
@@ -79,20 +79,14 @@ class RecipeController {
79 79
80 debug('localResults:', localResults); 80 debug('localResults:', localResults);
81 81
82 results = [ 82 results = [...localResults, ...(remoteResults || [])];
83 ...localResults,
84 ...remoteResults || [],
85 ];
86 } 83 }
87 84
88 return response.send(results); 85 return response.send(results);
89 } 86 }
90 87
91 // Download a recipe 88 // Download a recipe
92 async download({ 89 async download({ response, params }) {
93 response,
94 params,
95 }) {
96 // Validate user input 90 // Validate user input
97 const validation = await validateAll(params, { 91 const validation = await validateAll(params, {
98 recipe: 'required|accepted', 92 recipe: 'required|accepted',
@@ -108,14 +102,17 @@ class RecipeController {
108 const service = params.recipe; 102 const service = params.recipe;
109 103
110 // Check for invalid characters 104 // Check for invalid characters
111 if (/\.{1,}/.test(service) || /\/{1,}/.test(service)) { 105 if (/\.+/.test(service) || /\/+/.test(service)) {
112 return response.send('Invalid recipe name'); 106 return response.send('Invalid recipe name');
113 } 107 }
114 108
115 // Check if recipe exists in recipes folder 109 // Check if recipe exists in recipes folder
116 if (await Drive.exists(`${service}.tar.gz`)) { 110 if (await Drive.exists(`${service}.tar.gz`)) {
117 return response.send(await Drive.get(`${service}.tar.gz`)); 111 return response.send(await Drive.get(`${service}.tar.gz`));
118 } if (Env.get('CONNECT_WITH_FRANZ') == 'true') { // eslint-disable-line eqeqeq 112 }
113 // eslint-disable-next-line eqeqeq
114 if (Env.get('CONNECT_WITH_FRANZ') == 'true') {
115 // eslint-disable-line eqeqeq
119 return response.redirect(`${RECIPES_URL}/download/${service}`); 116 return response.redirect(`${RECIPES_URL}/download/${service}`);
120 } 117 }
121 return response.status(400).send({ 118 return response.status(400).send({
diff --git a/src/internal-server/app/Controllers/Http/ServiceController.js b/src/internal-server/app/Controllers/Http/ServiceController.js
index f2af9d411..ae463617d 100644
--- a/src/internal-server/app/Controllers/Http/ServiceController.js
+++ b/src/internal-server/app/Controllers/Http/ServiceController.js
@@ -135,13 +135,11 @@ class ServiceController {
135 135
136 const newSettings = { 136 const newSettings = {
137 ...settings, 137 ...settings,
138 ...{ 138 iconId,
139 iconId, 139 customIconVersion:
140 customIconVersion: 140 settings && settings.customIconVersion
141 settings && settings.customIconVersion 141 ? settings.customIconVersion + 1
142 ? settings.customIconVersion + 1 142 : 1,
143 : 1,
144 },
145 }; 143 };
146 144
147 // Update data in database 145 // Update data in database
@@ -157,9 +155,7 @@ class ServiceController {
157 id, 155 id,
158 name: service.name, 156 name: service.name,
159 ...newSettings, 157 ...newSettings,
160 iconUrl: `http://${hostname}:${port}/${API_VERSION}/icon/${ 158 iconUrl: `http://${hostname}:${port}/${API_VERSION}/icon/${newSettings.iconId}`,
161 newSettings.iconId
162 }`,
163 userId: 1, 159 userId: 1,
164 }, 160 },
165 status: ['updated'], 161 status: ['updated'],
diff --git a/src/internal-server/app/Controllers/Http/UserController.js b/src/internal-server/app/Controllers/Http/UserController.js
index 994dcc0dc..7b71aac14 100644
--- a/src/internal-server/app/Controllers/Http/UserController.js
+++ b/src/internal-server/app/Controllers/Http/UserController.js
@@ -1,34 +1,37 @@
1const User = use('App/Models/User'); 1const User = use('App/Models/User');
2const Service = use('App/Models/Service'); 2const Service = use('App/Models/Service');
3const Workspace = use('App/Models/Workspace'); 3const Workspace = use('App/Models/Workspace');
4const { 4const { validateAll } = use('Validator');
5 validateAll,
6} = use('Validator');
7 5
8const btoa = require('btoa'); 6const btoa = require('btoa');
9const fetch = require('node-fetch'); 7const fetch = require('node-fetch');
10const uuid = require('uuid/v4'); 8const uuid = require('uuid/v4');
11const crypto = require('crypto'); 9const crypto = require('crypto');
12const { DEFAULT_APP_SETTINGS, API_VERSION } = require('../../../../environment'); 10const {
11 DEFAULT_APP_SETTINGS,
12 API_VERSION,
13} = require('../../../../environment');
13const { default: userAgent } = require('../../../../helpers/userAgent-helpers'); 14const { default: userAgent } = require('../../../../helpers/userAgent-helpers');
14 15
15const apiRequest = (url, route, method, auth) => new Promise((resolve, reject) => { 16const apiRequest = (url, route, method, auth) =>
16 try { 17 new Promise((resolve, reject) => {
17 fetch(`${url}/${API_VERSION}/${route}`, { 18 try {
18 method, 19 fetch(`${url}/${API_VERSION}/${route}`, {
19 headers: { 20 method,
20 Authorization: `Bearer ${auth}`, 21 headers: {
21 'User-Agent': userAgent(), 22 Authorization: `Bearer ${auth}`,
22 }, 23 'User-Agent': userAgent(),
23 }) 24 },
24 .then(data => data.json()) 25 })
25 .then(json => resolve(json)); 26 .then(data => data.json())
26 } catch (e) { 27 .then(json => resolve(json));
27 reject(); 28 } catch {
28 } 29 reject();
29}); 30 }
31 });
30 32
31const LOGIN_SUCCESS_TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M'; 33const LOGIN_SUCCESS_TOKEN =
34 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJGZXJkaSBJbnRlcm5hbCBTZXJ2ZXIiLCJpYXQiOjE1NzEwNDAyMTUsImV4cCI6MjUzMzk1NDE3ODQ0LCJhdWQiOiJnZXRmZXJkaS5jb20iLCJzdWIiOiJmZXJkaUBsb2NhbGhvc3QiLCJ1c2VySWQiOiIxIn0.9_TWFGp6HROv8Yg82Rt6i1-95jqWym40a-HmgrdMC6M';
32 35
33const DEFAULT_USER_DATA = { 36const DEFAULT_USER_DATA = {
34 accountType: 'individual', 37 accountType: 'individual',
@@ -45,10 +48,7 @@ const DEFAULT_USER_DATA = {
45 48
46class UserController { 49class UserController {
47 // Register a new user 50 // Register a new user
48 async signup({ 51 async signup({ request, response }) {
49 request,
50 response,
51 }) {
52 // Validate user input 52 // Validate user input
53 const validation = await validateAll(request.all(), { 53 const validation = await validateAll(request.all(), {
54 firstname: 'required', 54 firstname: 'required',
@@ -70,10 +70,7 @@ class UserController {
70 } 70 }
71 71
72 // Login using an existing user 72 // Login using an existing user
73 async login({ 73 async login({ request, response }) {
74 request,
75 response,
76 }) {
77 if (!request.header('Authorization')) { 74 if (!request.header('Authorization')) {
78 return response.status(401).send({ 75 return response.status(401).send({
79 message: 'Please provide authorization', 76 message: 'Please provide authorization',
@@ -88,23 +85,21 @@ class UserController {
88 } 85 }
89 86
90 // Return information about the current user 87 // Return information about the current user
91 async me({ 88 async me({ response }) {
92 response,
93 }) {
94 const user = await User.find(1); 89 const user = await User.find(1);
95 90
96 const settings = typeof user.settings === 'string' ? JSON.parse(user.settings) : user.settings; 91 const settings =
92 typeof user.settings === 'string'
93 ? JSON.parse(user.settings)
94 : user.settings;
97 95
98 return response.send({ 96 return response.send({
99 ...DEFAULT_USER_DATA, 97 ...DEFAULT_USER_DATA,
100 ...settings || {}, 98 ...settings,
101 }); 99 });
102 } 100 }
103 101
104 async updateMe({ 102 async updateMe({ request, response }) {
105 request,
106 response,
107 }) {
108 const user = await User.find(1); 103 const user = await User.find(1);
109 104
110 let settings = user.settings || {}; 105 let settings = user.settings || {};
@@ -125,16 +120,11 @@ class UserController {
125 ...DEFAULT_USER_DATA, 120 ...DEFAULT_USER_DATA,
126 ...newSettings, 121 ...newSettings,
127 }, 122 },
128 status: [ 123 status: ['data-updated'],
129 'data-updated',
130 ],
131 }); 124 });
132 } 125 }
133 126
134 async import({ 127 async import({ request, response }) {
135 request,
136 response,
137 }) {
138 // Validate user input 128 // Validate user input
139 const validation = await validateAll(request.all(), { 129 const validation = await validateAll(request.all(), {
140 email: 'required|email', 130 email: 'required|email',
@@ -142,7 +132,8 @@ class UserController {
142 server: 'required', 132 server: 'required',
143 }); 133 });
144 if (validation.fails()) { 134 if (validation.fails()) {
145 let errorMessage = 'There was an error while trying to import your account:\n'; 135 let errorMessage =
136 'There was an error while trying to import your account:\n';
146 for (const message of validation.messages()) { 137 for (const message of validation.messages()) {
147 if (message.validation === 'required') { 138 if (message.validation === 'required') {
148 errorMessage += `- Please make sure to supply your ${message.field}\n`; 139 errorMessage += `- Please make sure to supply your ${message.field}\n`;
@@ -155,13 +146,12 @@ class UserController {
155 return response.status(401).send(errorMessage); 146 return response.status(401).send(errorMessage);
156 } 147 }
157 148
158 const { 149 const { email, password, server } = request.all();
159 email,
160 password,
161 server,
162 } = request.all();
163 150
164 const hashedPassword = crypto.createHash('sha256').update(password).digest('base64'); 151 const hashedPassword = crypto
152 .createHash('sha256')
153 .update(password)
154 .digest('base64');
165 155
166 // Try to get an authentication token 156 // Try to get an authentication token
167 let token; 157 let token;
@@ -178,16 +168,17 @@ class UserController {
178 const content = await rawResponse.json(); 168 const content = await rawResponse.json();
179 169
180 if (!content.message || content.message !== 'Successfully logged in') { 170 if (!content.message || content.message !== 'Successfully logged in') {
181 const errorMessage = 'Could not login into Franz with your supplied credentials. Please check and try again'; 171 const errorMessage =
172 'Could not login into Franz with your supplied credentials. Please check and try again';
182 return response.status(401).send(errorMessage); 173 return response.status(401).send(errorMessage);
183 } 174 }
184 175
185 // eslint-disable-next-line prefer-destructuring 176 // eslint-disable-next-line prefer-destructuring
186 token = content.token; 177 token = content.token;
187 } catch (e) { 178 } catch (error) {
188 return response.status(401).send({ 179 return response.status(401).send({
189 message: 'Cannot login to Franz', 180 message: 'Cannot login to Franz',
190 error: e, 181 error,
191 }); 182 });
192 } 183 }
193 184
@@ -195,12 +186,13 @@ class UserController {
195 let userInf = false; 186 let userInf = false;
196 try { 187 try {
197 userInf = await apiRequest(server, 'me', 'GET', token); 188 userInf = await apiRequest(server, 'me', 'GET', token);
198 } catch (e) { 189 } catch (error) {
199 const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${e}`; 190 const errorMessage = `Could not get your user info from Franz. Please check your credentials or try again later.\nError: ${error}`;
200 return response.status(401).send(errorMessage); 191 return response.status(401).send(errorMessage);
201 } 192 }
202 if (!userInf) { 193 if (!userInf) {
203 const errorMessage = 'Could not get your user info from Franz. Please check your credentials or try again later'; 194 const errorMessage =
195 'Could not get your user info from Franz. Please check your credentials or try again later';
204 return response.status(401).send(errorMessage); 196 return response.status(401).send(errorMessage);
205 } 197 }
206 198
@@ -213,8 +205,8 @@ class UserController {
213 for (const service of services) { 205 for (const service of services) {
214 await this._createAndCacheService(service, serviceIdTranslation); // eslint-disable-line no-await-in-loop 206 await this._createAndCacheService(service, serviceIdTranslation); // eslint-disable-line no-await-in-loop
215 } 207 }
216 } catch (e) { 208 } catch (error) {
217 const errorMessage = `Could not import your services into our system.\nError: ${e}`; 209 const errorMessage = `Could not import your services into our system.\nError: ${error}`;
218 return response.status(401).send(errorMessage); 210 return response.status(401).send(errorMessage);
219 } 211 }
220 212
@@ -225,12 +217,14 @@ class UserController {
225 for (const workspace of workspaces) { 217 for (const workspace of workspaces) {
226 await this._createWorkspace(workspace, serviceIdTranslation); // eslint-disable-line no-await-in-loop 218 await this._createWorkspace(workspace, serviceIdTranslation); // eslint-disable-line no-await-in-loop
227 } 219 }
228 } catch (e) { 220 } catch (error) {
229 const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`; 221 const errorMessage = `Could not import your workspaces into our system.\nError: ${error}`;
230 return response.status(401).send(errorMessage); 222 return response.status(401).send(errorMessage);
231 } 223 }
232 224
233 return response.send('Your account has been imported. You can now use your Franz account in Ferdi.'); 225 return response.send(
226 'Your account has been imported. You can now use your Franz account in Ferdi.',
227 );
234 } 228 }
235 229
236 // Account import/export 230 // Account import/export
@@ -255,10 +249,7 @@ class UserController {
255 .send(exportData); 249 .send(exportData);
256 } 250 }
257 251
258 async importFerdi({ 252 async importFerdi({ request, response }) {
259 request,
260 response,
261 }) {
262 const validation = await validateAll(request.all(), { 253 const validation = await validateAll(request.all(), {
263 file: 'required', 254 file: 'required',
264 }); 255 });
@@ -269,8 +260,10 @@ class UserController {
269 let file; 260 let file;
270 try { 261 try {
271 file = JSON.parse(request.input('file')); 262 file = JSON.parse(request.input('file'));
272 } catch (e) { 263 } catch {
273 return response.send('Could not import: Invalid file, could not read file'); 264 return response.send(
265 'Could not import: Invalid file, could not read file',
266 );
274 } 267 }
275 268
276 if (!file || !file.services || !file.workspaces) { 269 if (!file || !file.services || !file.workspaces) {
@@ -284,8 +277,8 @@ class UserController {
284 for (const service of file.services) { 277 for (const service of file.services) {
285 await this._createAndCacheService(service, serviceIdTranslation); // eslint-disable-line no-await-in-loop 278 await this._createAndCacheService(service, serviceIdTranslation); // eslint-disable-line no-await-in-loop
286 } 279 }
287 } catch (e) { 280 } catch (error) {
288 const errorMessage = `Could not import your services into our system.\nError: ${e}`; 281 const errorMessage = `Could not import your services into our system.\nError: ${error}`;
289 return response.send(errorMessage); 282 return response.send(errorMessage);
290 } 283 }
291 284
@@ -294,8 +287,8 @@ class UserController {
294 for (const workspace of file.workspaces) { 287 for (const workspace of file.workspaces) {
295 await this._createWorkspace(workspace, serviceIdTranslation); // eslint-disable-line no-await-in-loop 288 await this._createWorkspace(workspace, serviceIdTranslation); // eslint-disable-line no-await-in-loop
296 } 289 }
297 } catch (e) { 290 } catch (error) {
298 const errorMessage = `Could not import your workspaces into our system.\nError: ${e}`; 291 const errorMessage = `Could not import your workspaces into our system.\nError: ${error}`;
299 return response.status(401).send(errorMessage); 292 return response.status(401).send(errorMessage);
300 } 293 }
301 294
@@ -306,15 +299,29 @@ class UserController {
306 let newWorkspaceId; 299 let newWorkspaceId;
307 do { 300 do {
308 newWorkspaceId = uuid(); 301 newWorkspaceId = uuid();
309 } while ((await Workspace.query().where('workspaceId', newWorkspaceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop 302 } while (
310 303 (await Workspace.query().where('workspaceId', newWorkspaceId).fetch())
311 if (workspace.services && typeof (workspace.services) === 'string' && workspace.services.length > 0) { 304 .rows.length > 0
305 ); // eslint-disable-line no-await-in-loop
306
307 if (
308 workspace.services &&
309 typeof workspace.services === 'string' &&
310 workspace.services.length > 0
311 ) {
312 workspace.services = JSON.parse(workspace.services); 312 workspace.services = JSON.parse(workspace.services);
313 } 313 }
314 const services = (workspace.services && typeof (workspace.services) === 'object') ? 314 const services =
315 workspace.services.map(oldServiceId => serviceIdTranslation[oldServiceId]) : 315 workspace.services && typeof workspace.services === 'object'
316 []; 316 ? workspace.services.map(
317 if (workspace.data && typeof (workspace.data) === 'string' && workspace.data.length > 0) { 317 oldServiceId => serviceIdTranslation[oldServiceId],
318 )
319 : [];
320 if (
321 workspace.data &&
322 typeof workspace.data === 'string' &&
323 workspace.data.length > 0
324 ) {
318 workspace.data = JSON.parse(workspace.data); 325 workspace.data = JSON.parse(workspace.data);
319 } 326 }
320 327
@@ -332,12 +339,19 @@ class UserController {
332 let newServiceId; 339 let newServiceId;
333 do { 340 do {
334 newServiceId = uuid(); 341 newServiceId = uuid();
335 } while ((await Service.query().where('serviceId', newServiceId).fetch()).rows.length > 0); // eslint-disable-line no-await-in-loop 342 } while (
343 (await Service.query().where('serviceId', newServiceId).fetch()).rows
344 .length > 0
345 ); // eslint-disable-line no-await-in-loop
336 346
337 // store the old serviceId as the key for future lookup 347 // store the old serviceId as the key for future lookup
338 serviceIdTranslation[service.serviceId] = newServiceId; 348 serviceIdTranslation[service.serviceId] = newServiceId;
339 349
340 if (service.settings && typeof (service.settings) === 'string' && service.settings.length > 0) { 350 if (
351 service.settings &&
352 typeof service.settings === 'string' &&
353 service.settings.length > 0
354 ) {
341 service.settings = JSON.parse(service.settings); 355 service.settings = JSON.parse(service.settings);
342 } 356 }
343 357
diff --git a/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js b/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js
index 87f1f6c25..9591cdc41 100644
--- a/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js
+++ b/src/internal-server/app/Middleware/ConvertEmptyStringsToNull.js
@@ -1,6 +1,6 @@
1class ConvertEmptyStringsToNull { 1class ConvertEmptyStringsToNull {
2 async handle({ request }, next) { 2 async handle({ request }, next) {
3 if (Object.keys(request.body).length) { 3 if (Object.keys(request.body).length > 0) {
4 request.body = Object.assign( 4 request.body = Object.assign(
5 ...Object.keys(request.body).map(key => ({ 5 ...Object.keys(request.body).map(key => ({
6 [key]: request.body[key] !== '' ? request.body[key] : null, 6 [key]: request.body[key] !== '' ? request.body[key] : null,
diff --git a/src/internal-server/app/Models/Recipe.js b/src/internal-server/app/Models/Recipe.js
index bd9741114..f9370e206 100644
--- a/src/internal-server/app/Models/Recipe.js
+++ b/src/internal-server/app/Models/Recipe.js
@@ -1,7 +1,6 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model'); 2const Model = use('Model');
3 3
4class Recipe extends Model { 4class Recipe extends Model {}
5}
6 5
7module.exports = Recipe; 6module.exports = Recipe;
diff --git a/src/internal-server/app/Models/Service.js b/src/internal-server/app/Models/Service.js
index a2e5c981e..95321686c 100644
--- a/src/internal-server/app/Models/Service.js
+++ b/src/internal-server/app/Models/Service.js
@@ -1,7 +1,6 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model'); 2const Model = use('Model');
3 3
4class Service extends Model { 4class Service extends Model {}
5}
6 5
7module.exports = Service; 6module.exports = Service;
diff --git a/src/internal-server/app/Models/Token.js b/src/internal-server/app/Models/Token.js
index 83e989117..1388b94ad 100644
--- a/src/internal-server/app/Models/Token.js
+++ b/src/internal-server/app/Models/Token.js
@@ -1,7 +1,6 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model'); 2const Model = use('Model');
3 3
4class Token extends Model { 4class Token extends Model {}
5}
6 5
7module.exports = Token; 6module.exports = Token;
diff --git a/src/internal-server/app/Models/User.js b/src/internal-server/app/Models/User.js
index 907710d8d..f17f04c3e 100644
--- a/src/internal-server/app/Models/User.js
+++ b/src/internal-server/app/Models/User.js
@@ -2,7 +2,6 @@
2/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 2/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
3const Model = use('Model'); 3const Model = use('Model');
4 4
5class User extends Model { 5class User extends Model {}
6}
7 6
8module.exports = User; 7module.exports = User;
diff --git a/src/internal-server/app/Models/Workspace.js b/src/internal-server/app/Models/Workspace.js
index dcf39ac75..c47c02e37 100644
--- a/src/internal-server/app/Models/Workspace.js
+++ b/src/internal-server/app/Models/Workspace.js
@@ -1,7 +1,6 @@
1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 1/** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */
2const Model = use('Model'); 2const Model = use('Model');
3 3
4class Workspace extends Model { 4class Workspace extends Model {}
5}
6 5
7module.exports = Workspace; 6module.exports = Workspace;
diff --git a/src/internal-server/config/shield.js b/src/internal-server/config/shield.js
index 76f430e91..4ff22c3f9 100644
--- a/src/internal-server/config/shield.js
+++ b/src/internal-server/config/shield.js
@@ -25,8 +25,7 @@ module.exports = {
25 | } 25 | }
26 | 26 |
27 */ 27 */
28 directives: { 28 directives: {},
29 },
30 /* 29 /*
31 |-------------------------------------------------------------------------- 30 |--------------------------------------------------------------------------
32 | Report only 31 | Report only
diff --git a/src/internal-server/public/js/transfer.js b/src/internal-server/public/js/transfer.js
index 8382bba02..36fdbd61a 100644
--- a/src/internal-server/public/js/transfer.js
+++ b/src/internal-server/public/js/transfer.js
@@ -1,13 +1,17 @@
1const submitBtn = document.getElementById('submit'); 1const submitBtn = document.querySelector('#submit');
2const fileInput = document.getElementById('file'); 2const fileInput = document.querySelector('#file');
3const fileOutput = document.getElementById('fileoutput'); 3const fileOutput = document.querySelector('#fileoutput');
4 4
5fileInput.addEventListener('change', () => { 5fileInput?.addEventListener('change', () => {
6 const reader = new FileReader(); 6 const reader = new FileReader();
7 reader.onload = () => { 7 reader.addEventListener('load', () => {
8 const text = reader.result; 8 const text = reader.result;
9 fileOutput.value = text; 9 if (fileOutput) {
10 submitBtn.disabled = false; 10 fileOutput.value = text;
11 }; 11 }
12 if (submitBtn) {
13 submitBtn.disabled = false;
14 }
15 });
12 reader.readAsText(fileInput.files[0]); 16 reader.readAsText(fileInput.files[0]);
13}); 17});
diff --git a/src/internal-server/start/kernel.js b/src/internal-server/start/kernel.js
index 7b540f829..f72e445f2 100644
--- a/src/internal-server/start/kernel.js
+++ b/src/internal-server/start/kernel.js
@@ -32,8 +32,7 @@ const globalMiddleware = [
32| Route.get().middleware('auth') 32| Route.get().middleware('auth')
33| 33|
34*/ 34*/
35const namedMiddleware = { 35const namedMiddleware = {};
36};
37 36
38/* 37/*
39|-------------------------------------------------------------------------- 38|--------------------------------------------------------------------------
@@ -45,11 +44,8 @@ const namedMiddleware = {
45| control over request lifecycle. 44| control over request lifecycle.
46| 45|
47*/ 46*/
48const serverMiddleware = [ 47const serverMiddleware = ['Adonis/Middleware/Static'];
49 'Adonis/Middleware/Static',
50];
51 48
52Server 49Server.registerGlobal(globalMiddleware)
53 .registerGlobal(globalMiddleware)
54 .registerNamed(namedMiddleware) 50 .registerNamed(namedMiddleware)
55 .use(serverMiddleware); 51 .use(serverMiddleware);
diff --git a/src/internal-server/start/migrate.js b/src/internal-server/start/migrate.js
index c27e07bc5..0f25240cc 100644
--- a/src/internal-server/start/migrate.js
+++ b/src/internal-server/start/migrate.js
@@ -6,29 +6,39 @@ const { ferdiVersion } = require('../../environment');
6const Database = use('Database'); 6const Database = use('Database');
7const User = use('App/Models/User'); 7const User = use('App/Models/User');
8 8
9const migrateLog = (text) => { 9const migrateLog = text => {
10 console.log('\x1b[36m%s\x1b[0m', 'Ferdi Migration:', '\x1b[0m', text); 10 console.log('\u001B[36m%s\u001B[0m', 'Ferdi Migration:', '\u001B[0m', text);
11}; 11};
12 12
13module.exports = async () => { 13module.exports = async () => {
14 migrateLog('🧙‍ Running database migration wizard'); 14 migrateLog('🧙‍ Running database migration wizard');
15 15
16 // Make sure user table exists 16 // Make sure user table exists
17 await Database.raw('CREATE TABLE IF NOT EXISTS `users` (`id` integer not null primary key autoincrement, `settings` text, `created_at` datetime, `updated_at` datetime);'); 17 await Database.raw(
18 'CREATE TABLE IF NOT EXISTS `users` (`id` integer not null primary key autoincrement, `settings` text, `created_at` datetime, `updated_at` datetime);',
19 );
18 20
19 const user = await User.find(1); 21 const user = await User.find(1);
20 let settings; 22 let settings;
21 if (!user) { 23 if (!user) {
22 migrateLog('🎩 Migrating from old Ferdi version as user doesn\'t exist'); 24 migrateLog("🎩 Migrating from old Ferdi version as user doesn't exist");
23 25
24 // Create new user 26 // Create new user
25 await Database.raw('INSERT INTO "users" ("id") VALUES (\'1\');'); 27 await Database.raw('INSERT INTO "users" ("id") VALUES (\'1\');');
26 } else { 28 } else {
27 settings = typeof user.settings === 'string' ? JSON.parse(user.settings) : user.settings; 29 settings =
30 typeof user.settings === 'string'
31 ? JSON.parse(user.settings)
32 : user.settings;
28 } 33 }
29 34
30 if (!settings || !settings.db_version || settings.db_version !== ferdiVersion) { 35 if (
31 const srcVersion = settings && settings.db_version ? settings.db_version : '5.4.0-beta.2'; 36 !settings ||
37 !settings.db_version ||
38 settings.db_version !== ferdiVersion
39 ) {
40 const srcVersion =
41 settings && settings.db_version ? settings.db_version : '5.4.0-beta.2';
32 migrateLog(`🔮 Migrating table from ${srcVersion} to ${ferdiVersion}`); 42 migrateLog(`🔮 Migrating table from ${srcVersion} to ${ferdiVersion}`);
33 43
34 // Migrate database to current Ferdi version 44 // Migrate database to current Ferdi version
diff --git a/src/internal-server/test.js b/src/internal-server/test.js
index 8d4807d06..ef85743f3 100644
--- a/src/internal-server/test.js
+++ b/src/internal-server/test.js
@@ -6,4 +6,4 @@ const dummyUserFolder = path.join(__dirname, 'user_data');
6 6
7fs.ensureDirSync(dummyUserFolder); 7fs.ensureDirSync(dummyUserFolder);
8 8
9server(dummyUserFolder, 45568); 9server(dummyUserFolder, 45_568);
diff --git a/src/lib/Menu.js b/src/lib/Menu.js
index 8e2d8bdca..563db087b 100644
--- a/src/lib/Menu.js
+++ b/src/lib/Menu.js
@@ -952,7 +952,7 @@ class FranzMenu {
952 }, 952 },
953 ); 953 );
954 954
955 services.allDisplayed.forEach((service, i) => 955 for (const [i, service] of services.allDisplayed.entries()) {
956 menu.push({ 956 menu.push({
957 label: this._getServiceName(service), 957 label: this._getServiceName(service),
958 accelerator: i < 9 ? `${cmdOrCtrlShortcutKey()}+${i + 1}` : null, 958 accelerator: i < 9 ? `${cmdOrCtrlShortcutKey()}+${i + 1}` : null,
@@ -965,8 +965,8 @@ class FranzMenu {
965 app.mainWindow.restore(); 965 app.mainWindow.restore();
966 } 966 }
967 }, 967 },
968 }), 968 });
969 ); 969 }
970 970
971 if ( 971 if (
972 services.active && 972 services.active &&
@@ -1018,23 +1018,23 @@ class FranzMenu {
1018 }); 1018 });
1019 } 1019 }
1020 1020
1021 menu.push({ 1021 menu.push(
1022 type: 'separator', 1022 {
1023 }); 1023 type: 'separator',
1024
1025 // Default workspace
1026 menu.push({
1027 label: intl.formatMessage(menuItems.defaultWorkspace),
1028 accelerator: `${cmdOrCtrlShortcutKey()}+${altKey()}+0`,
1029 type: 'radio',
1030 checked: !activeWorkspace,
1031 click: () => {
1032 workspaceActions.deactivate();
1033 }, 1024 },
1034 }); 1025 {
1026 label: intl.formatMessage(menuItems.defaultWorkspace),
1027 accelerator: `${cmdOrCtrlShortcutKey()}+${altKey()}+0`,
1028 type: 'radio',
1029 checked: !activeWorkspace,
1030 click: () => {
1031 workspaceActions.deactivate();
1032 },
1033 },
1034 );
1035 1035
1036 // Workspace items 1036 // Workspace items
1037 workspaces.forEach((workspace, i) => 1037 for (const [i, workspace] of workspaces.entries()) {
1038 menu.push({ 1038 menu.push({
1039 label: workspace.name, 1039 label: workspace.name,
1040 accelerator: 1040 accelerator:
@@ -1044,8 +1044,8 @@ class FranzMenu {
1044 click: () => { 1044 click: () => {
1045 workspaceActions.activate({ workspace }); 1045 workspaceActions.activate({ workspace });
1046 }, 1046 },
1047 }), 1047 });
1048 ); 1048 }
1049 1049
1050 return menu; 1050 return menu;
1051 } 1051 }
diff --git a/src/lib/TouchBar.js b/src/lib/TouchBar.js
index 3397afdb2..c80931200 100644
--- a/src/lib/TouchBar.js
+++ b/src/lib/TouchBar.js
@@ -15,8 +15,8 @@ export default class FranzTouchBar {
15 if (isMac && semver.gt(osRelease, '16.6.0')) { 15 if (isMac && semver.gt(osRelease, '16.6.0')) {
16 this.build = autorun(this._build.bind(this)); 16 this.build = autorun(this._build.bind(this));
17 } 17 }
18 } catch (err) { 18 } catch (error) {
19 console.error(err); 19 console.error(error);
20 } 20 }
21 } 21 }
22 22
@@ -27,7 +27,7 @@ export default class FranzTouchBar {
27 const { TouchBarButton, TouchBarSpacer } = TouchBar; 27 const { TouchBarButton, TouchBarSpacer } = TouchBar;
28 28
29 const buttons = []; 29 const buttons = [];
30 this.stores.services.allDisplayed.forEach(((service) => { 30 for (const service of this.stores.services.allDisplayed) {
31 buttons.push(new TouchBarButton({ 31 buttons.push(new TouchBarButton({
32 label: `${service.name}${service.unreadDirectMessageCount > 0 32 label: `${service.name}${service.unreadDirectMessageCount > 0
33 ? ' 🔴' : ''} ${service.unreadDirectMessageCount === 0 33 ? ' 🔴' : ''} ${service.unreadDirectMessageCount === 0
@@ -38,7 +38,7 @@ export default class FranzTouchBar {
38 this.actions.service.setActive({ serviceId: service.id }); 38 this.actions.service.setActive({ serviceId: service.id });
39 }, 39 },
40 }), new TouchBarSpacer({ size: 'small' })); 40 }), new TouchBarSpacer({ size: 'small' }));
41 })); 41 }
42 42
43 const touchBar = new TouchBar({ items: buttons }); 43 const touchBar = new TouchBar({ items: buttons });
44 currentWindow.setTouchBar(touchBar); 44 currentWindow.setTouchBar(touchBar);
diff --git a/src/models/News.ts b/src/models/News.ts
index a6ff86dda..4fc21f590 100644
--- a/src/models/News.ts
+++ b/src/models/News.ts
@@ -1,5 +1,3 @@
1// @flow
2
3import { ifUndefinedString, ifUndefinedBoolean } from '../jsUtils'; 1import { ifUndefinedString, ifUndefinedBoolean } from '../jsUtils';
4 2
5interface INews { 3interface INews {
@@ -20,11 +18,11 @@ export default class News {
20 18
21 constructor(data: INews) { 19 constructor(data: INews) {
22 if (!data) { 20 if (!data) {
23 throw Error('News config not valid'); 21 throw new Error('News config not valid');
24 } 22 }
25 23
26 if (!data.id) { 24 if (!data.id) {
27 throw Error('News requires Id'); 25 throw new Error('News requires Id');
28 } 26 }
29 27
30 this.id = data.id; 28 this.id = data.id;
diff --git a/src/models/Recipe.ts b/src/models/Recipe.ts
index 0a93fbc5a..6022fb520 100644
--- a/src/models/Recipe.ts
+++ b/src/models/Recipe.ts
@@ -68,16 +68,16 @@ export default class Recipe {
68 // TODO: Need to reconcile which of these are optional/mandatory 68 // TODO: Need to reconcile which of these are optional/mandatory
69 constructor(data: IRecipe) { 69 constructor(data: IRecipe) {
70 if (!data) { 70 if (!data) {
71 throw Error('Recipe config not valid'); 71 throw new Error('Recipe config not valid');
72 } 72 }
73 73
74 if (!data.id) { 74 if (!data.id) {
75 // Ferdi 4 recipes do not have an Id 75 // Ferdi 4 recipes do not have an Id
76 throw Error(`Recipe '${data.name}' requires Id`); 76 throw new Error(`Recipe '${data.name}' requires Id`);
77 } 77 }
78 78
79 if (!semver.valid(data.version)) { 79 if (!semver.valid(data.version)) {
80 throw Error(`Version ${data.version} of recipe '${data.name}' is not a valid semver version`); 80 throw new Error(`Version ${data.version} of recipe '${data.name}' is not a valid semver version`);
81 } 81 }
82 82
83 this.id = data.id || this.id; 83 this.id = data.id || this.id;
diff --git a/src/models/RecipePreview.ts b/src/models/RecipePreview.ts
index 4d2cc8450..fb8cb3e3e 100644
--- a/src/models/RecipePreview.ts
+++ b/src/models/RecipePreview.ts
@@ -1,5 +1,3 @@
1// @flow
2
3interface IRecipePreview { 1interface IRecipePreview {
4 id: string; 2 id: string;
5 name: string; 3 name: string;
@@ -21,11 +19,11 @@ export default class RecipePreview {
21 19
22 constructor(data: IRecipePreview) { 20 constructor(data: IRecipePreview) {
23 if (!data) { 21 if (!data) {
24 throw Error('RecipePreview config not valid'); 22 throw new Error('RecipePreview config not valid');
25 } 23 }
26 24
27 if (!data.id) { 25 if (!data.id) {
28 throw Error(`RecipePreview '${data.name}' requires Id`); 26 throw new Error(`RecipePreview '${data.name}' requires Id`);
29 } 27 }
30 28
31 Object.assign(this, data); 29 Object.assign(this, data);
diff --git a/src/models/Service.js b/src/models/Service.js
index 4ee054b2b..cc001f98d 100644
--- a/src/models/Service.js
+++ b/src/models/Service.js
@@ -8,7 +8,11 @@ import { todosStore } from '../features/todos';
8import { isValidExternalURL } from '../helpers/url-helpers'; 8import { isValidExternalURL } from '../helpers/url-helpers';
9import UserAgent from './UserAgent'; 9import UserAgent from './UserAgent';
10import { DEFAULT_SERVICE_ORDER } from '../config'; 10import { DEFAULT_SERVICE_ORDER } from '../config';
11import { ifUndefinedString, ifUndefinedBoolean, ifUndefinedNumber } from '../jsUtils'; 11import {
12 ifUndefinedString,
13 ifUndefinedBoolean,
14 ifUndefinedNumber,
15} from '../jsUtils';
12 16
13const debug = require('debug')('Ferdi:Service'); 17const debug = require('debug')('Ferdi:Service');
14 18
@@ -95,11 +99,11 @@ export default class Service {
95 99
96 constructor(data, recipe) { 100 constructor(data, recipe) {
97 if (!data) { 101 if (!data) {
98 throw Error('Service config not valid'); 102 throw new Error('Service config not valid');
99 } 103 }
100 104
101 if (!recipe) { 105 if (!recipe) {
102 throw Error('Service recipe not valid'); 106 throw new Error('Service recipe not valid');
103 } 107 }
104 108
105 this.recipe = recipe; 109 this.recipe = recipe;
@@ -115,22 +119,51 @@ export default class Service {
115 119
116 this.order = ifUndefinedNumber(data.order, this.order); 120 this.order = ifUndefinedNumber(data.order, this.order);
117 this.isEnabled = ifUndefinedBoolean(data.isEnabled, this.isEnabled); 121 this.isEnabled = ifUndefinedBoolean(data.isEnabled, this.isEnabled);
118 this.isNotificationEnabled = ifUndefinedBoolean(data.isNotificationEnabled, this.isNotificationEnabled); 122 this.isNotificationEnabled = ifUndefinedBoolean(
119 this.isBadgeEnabled = ifUndefinedBoolean(data.isBadgeEnabled, this.isBadgeEnabled); 123 data.isNotificationEnabled,
120 this.isIndirectMessageBadgeEnabled = ifUndefinedBoolean(data.isIndirectMessageBadgeEnabled, this.isIndirectMessageBadgeEnabled); 124 this.isNotificationEnabled,
125 );
126 this.isBadgeEnabled = ifUndefinedBoolean(
127 data.isBadgeEnabled,
128 this.isBadgeEnabled,
129 );
130 this.isIndirectMessageBadgeEnabled = ifUndefinedBoolean(
131 data.isIndirectMessageBadgeEnabled,
132 this.isIndirectMessageBadgeEnabled,
133 );
121 this.isMuted = ifUndefinedBoolean(data.isMuted, this.isMuted); 134 this.isMuted = ifUndefinedBoolean(data.isMuted, this.isMuted);
122 this.isDarkModeEnabled = ifUndefinedBoolean(data.isDarkModeEnabled, this.isDarkModeEnabled); 135 this.isDarkModeEnabled = ifUndefinedBoolean(
123 this.darkReaderSettings = ifUndefinedString(data.darkReaderSettings, this.darkReaderSettings); 136 data.isDarkModeEnabled,
124 this.hasCustomUploadedIcon = ifUndefinedBoolean(data.hasCustomIcon, this.hasCustomUploadedIcon); 137 this.isDarkModeEnabled,
138 );
139 this.darkReaderSettings = ifUndefinedString(
140 data.darkReaderSettings,
141 this.darkReaderSettings,
142 );
143 this.hasCustomUploadedIcon = ifUndefinedBoolean(
144 data.hasCustomIcon,
145 this.hasCustomUploadedIcon,
146 );
125 this.proxy = ifUndefinedString(data.proxy, this.proxy); 147 this.proxy = ifUndefinedString(data.proxy, this.proxy);
126 this.spellcheckerLanguage = ifUndefinedString(data.spellcheckerLanguage, this.spellcheckerLanguage); 148 this.spellcheckerLanguage = ifUndefinedString(
127 this.userAgentPref = ifUndefinedString(data.userAgentPref, this.userAgentPref); 149 data.spellcheckerLanguage,
128 this.isHibernationEnabled = ifUndefinedBoolean(data.isHibernationEnabled, this.isHibernationEnabled); 150 this.spellcheckerLanguage,
151 );
152 this.userAgentPref = ifUndefinedString(
153 data.userAgentPref,
154 this.userAgentPref,
155 );
156 this.isHibernationEnabled = ifUndefinedBoolean(
157 data.isHibernationEnabled,
158 this.isHibernationEnabled,
159 );
129 160
130 // Check if "Hibernate on Startup" is enabled and hibernate all services except active one 161 // Check if "Hibernate on Startup" is enabled and hibernate all services except active one
131 const { hibernateOnStartup } = window.ferdi.stores.settings.app; 162 const { hibernateOnStartup } = window.ferdi.stores.settings.app;
132 // The service store is probably not loaded yet so we need to use localStorage data to get active service 163 // The service store is probably not loaded yet so we need to use localStorage data to get active service
133 const isActive = window.localStorage.service && JSON.parse(window.localStorage.service).activeService === this.id; 164 const isActive =
165 window.localStorage.service &&
166 JSON.parse(window.localStorage.service).activeService === this.id;
134 if (hibernateOnStartup && !isActive) { 167 if (hibernateOnStartup && !isActive) {
135 this.isHibernationRequested = true; 168 this.isHibernationRequested = true;
136 } 169 }
@@ -189,9 +222,14 @@ export default class Service {
189 if (this.recipe.hasCustomUrl && this.customUrl) { 222 if (this.recipe.hasCustomUrl && this.customUrl) {
190 let url; 223 let url;
191 try { 224 try {
192 url = normalizeUrl(this.customUrl, { stripWWW: false, removeTrailingSlash: false }); 225 url = normalizeUrl(this.customUrl, {
193 } catch (err) { 226 stripWWW: false,
194 console.error(`Service (${this.recipe.name}): '${this.customUrl}' is not a valid Url.`); 227 removeTrailingSlash: false,
228 });
229 } catch {
230 console.error(
231 `Service (${this.recipe.name}): '${this.customUrl}' is not a valid Url.`,
232 );
195 } 233 }
196 234
197 if (typeof this.recipe.buildUrl === 'function') { 235 if (typeof this.recipe.buildUrl === 'function') {
@@ -241,7 +279,9 @@ export default class Service {
241 } 279 }
242 280
243 initializeWebViewEvents({ handleIPCMessage, openWindow, stores }) { 281 initializeWebViewEvents({ handleIPCMessage, openWindow, stores }) {
244 const webviewWebContents = webContents.fromId(this.webview.getWebContentsId()); 282 const webviewWebContents = webContents.fromId(
283 this.webview.getWebContentsId(),
284 );
245 285
246 this.userAgentModel.setWebviewReference(this.webview); 286 this.userAgentModel.setWebviewReference(this.webview);
247 287
@@ -270,9 +310,15 @@ export default class Service {
270 debug(this.name, 'knownCertificateHosts is not defined in the recipe'); 310 debug(this.name, 'knownCertificateHosts is not defined in the recipe');
271 } 311 }
272 312
273 this.webview.addEventListener('ipc-message', async (e) => { 313 this.webview.addEventListener('ipc-message', async e => {
274 if (e.channel === 'inject-js-unsafe') { 314 if (e.channel === 'inject-js-unsafe') {
275 await Promise.all(e.args.map((script) => this.webview.executeJavaScript(`"use strict"; (() => { ${script} })();`))); 315 await Promise.all(
316 e.args.map(script =>
317 this.webview.executeJavaScript(
318 `"use strict"; (() => { ${script} })();`,
319 ),
320 ),
321 );
276 } else { 322 } else {
277 handleIPCMessage({ 323 handleIPCMessage({
278 serviceId: this.id, 324 serviceId: this.id,
@@ -282,27 +328,33 @@ export default class Service {
282 } 328 }
283 }); 329 });
284 330
285 this.webview.addEventListener('new-window', (event, url, frameName, options) => { 331 this.webview.addEventListener(
286 debug('new-window', event, url, frameName, options); 332 'new-window',
287 if (!isValidExternalURL(event.url)) { 333 (event, url, frameName, options) => {
288 return; 334 debug('new-window', event, url, frameName, options);
289 } 335 if (!isValidExternalURL(event.url)) {
290 if (event.disposition === 'foreground-tab' || event.disposition === 'background-tab') { 336 return;
291 openWindow({ 337 }
292 event, 338 if (
293 url, 339 event.disposition === 'foreground-tab' ||
294 frameName, 340 event.disposition === 'background-tab'
295 options, 341 ) {
296 }); 342 openWindow({
297 } else { 343 event,
298 ipcRenderer.send('open-browser-window', { 344 url,
299 url: event.url, 345 frameName,
300 serviceId: this.id, 346 options,
301 }); 347 });
302 } 348 } else {
303 }); 349 ipcRenderer.send('open-browser-window', {
350 url: event.url,
351 serviceId: this.id,
352 });
353 }
354 },
355 );
304 356
305 this.webview.addEventListener('did-start-loading', (event) => { 357 this.webview.addEventListener('did-start-loading', event => {
306 debug('Did start load', this.name, event); 358 debug('Did start load', this.name, event);
307 359
308 this.hasCrashed = false; 360 this.hasCrashed = false;
@@ -321,9 +373,13 @@ export default class Service {
321 this.webview.addEventListener('did-frame-finish-load', didLoad.bind(this)); 373 this.webview.addEventListener('did-frame-finish-load', didLoad.bind(this));
322 this.webview.addEventListener('did-navigate', didLoad.bind(this)); 374 this.webview.addEventListener('did-navigate', didLoad.bind(this));
323 375
324 this.webview.addEventListener('did-fail-load', (event) => { 376 this.webview.addEventListener('did-fail-load', event => {
325 debug('Service failed to load', this.name, event); 377 debug('Service failed to load', this.name, event);
326 if (event.isMainFrame && event.errorCode !== -21 && event.errorCode !== -3) { 378 if (
379 event.isMainFrame &&
380 event.errorCode !== -21 &&
381 event.errorCode !== -3
382 ) {
327 this.isError = true; 383 this.isError = true;
328 this.errorMessage = event.errorDescription; 384 this.errorMessage = event.errorDescription;
329 this.isLoading = false; 385 this.isLoading = false;
@@ -365,12 +421,12 @@ export default class Service {
365 421
366 initializeWebViewListener() { 422 initializeWebViewListener() {
367 if (this.webview && this.recipe.events) { 423 if (this.webview && this.recipe.events) {
368 Object.keys(this.recipe.events).forEach((eventName) => { 424 for (const eventName of Object.keys(this.recipe.events)) {
369 const eventHandler = this.recipe[this.recipe.events[eventName]]; 425 const eventHandler = this.recipe[this.recipe.events[eventName]];
370 if (typeof eventHandler === 'function') { 426 if (typeof eventHandler === 'function') {
371 this.webview.addEventListener(eventName, eventHandler); 427 this.webview.addEventListener(eventName, eventHandler);
372 } 428 }
373 }); 429 }
374 } 430 }
375 } 431 }
376 432
diff --git a/src/models/User.ts b/src/models/User.ts
index 54a6838df..a04d46d3c 100644
--- a/src/models/User.ts
+++ b/src/models/User.ts
@@ -43,11 +43,11 @@ export default class User {
43 43
44 constructor(data: IUser) { 44 constructor(data: IUser) {
45 if (!data) { 45 if (!data) {
46 throw Error('User config not valid'); 46 throw new Error('User config not valid');
47 } 47 }
48 48
49 if (!data.id) { 49 if (!data.id) {
50 throw Error('User requires Id'); 50 throw new Error('User requires Id');
51 } 51 }
52 52
53 this.id = data.id; 53 this.id = data.id;
diff --git a/src/models/UserAgent.js b/src/models/UserAgent.js
index 930ae19ef..8ec274aa5 100644
--- a/src/models/UserAgent.js
+++ b/src/models/UserAgent.js
@@ -1,9 +1,4 @@
1import { 1import { action, computed, observe, observable } from 'mobx';
2 action,
3 computed,
4 observe,
5 observable,
6} from 'mobx';
7 2
8import defaultUserAgent from '../helpers/userAgent-helpers'; 3import defaultUserAgent from '../helpers/userAgent-helpers';
9 4
@@ -27,7 +22,7 @@ export default class UserAgent {
27 this.getUserAgent = overrideUserAgent; 22 this.getUserAgent = overrideUserAgent;
28 } 23 }
29 24
30 observe(this, 'webview', (change) => { 25 observe(this, 'webview', change => {
31 const { oldValue, newValue } = change; 26 const { oldValue, newValue } = change;
32 if (oldValue !== null) { 27 if (oldValue !== null) {
33 this._removeWebviewEvents(oldValue); 28 this._removeWebviewEvents(oldValue);
@@ -64,11 +59,13 @@ export default class UserAgent {
64 59
65 @computed get userAgentWithoutChromeVersion() { 60 @computed get userAgentWithoutChromeVersion() {
66 const withChrome = this.userAgentWithChromeVersion; 61 const withChrome = this.userAgentWithChromeVersion;
67 return withChrome.replace(/Chrome\/[0-9.]+/, 'Chrome'); 62 return withChrome.replace(/Chrome\/[\d.]+/, 'Chrome');
68 } 63 }
69 64
70 @computed get userAgent() { 65 @computed get userAgent() {
71 return this.chromelessUserAgent ? this.userAgentWithoutChromeVersion : this.userAgentWithChromeVersion; 66 return this.chromelessUserAgent
67 ? this.userAgentWithoutChromeVersion
68 : this.userAgentWithChromeVersion;
72 } 69 }
73 70
74 @action setWebviewReference(webview) { 71 @action setWebviewReference(webview) {
@@ -95,10 +92,10 @@ export default class UserAgent {
95 _addWebviewEvents(webview) { 92 _addWebviewEvents(webview) {
96 debug('Adding event handlers'); 93 debug('Adding event handlers');
97 94
98 this._willNavigateListener = (event) => this._handleNavigate(event.url, true); 95 this._willNavigateListener = event => this._handleNavigate(event.url, true);
99 webview.addEventListener('will-navigate', this._willNavigateListener); 96 webview.addEventListener('will-navigate', this._willNavigateListener);
100 97
101 this._didNavigateListener = (event) => this._handleNavigate(event.url); 98 this._didNavigateListener = event => this._handleNavigate(event.url);
102 webview.addEventListener('did-navigate', this._didNavigateListener); 99 webview.addEventListener('did-navigate', this._didNavigateListener);
103 } 100 }
104 101
diff --git a/src/prop-types.ts b/src/prop-types.ts
index 459b9a7b9..07607f105 100644
--- a/src/prop-types.ts
+++ b/src/prop-types.ts
@@ -1,6 +1,5 @@
1import PropTypes from 'prop-types'; 1import PropTypes from 'prop-types';
2 2
3// eslint-disable-next-line
4export const oneOrManyChildElements = PropTypes.oneOfType([ 3export const oneOrManyChildElements = PropTypes.oneOfType([
5 PropTypes.arrayOf(PropTypes.element), 4 PropTypes.arrayOf(PropTypes.element),
6 PropTypes.element, 5 PropTypes.element,
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index 469e7519e..85f74a91e 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -251,16 +251,14 @@ export default class AppStore extends Store {
251 // macOS catalina notifications hack 251 // macOS catalina notifications hack
252 // notifications got stuck after upgrade but forcing a notification 252 // notifications got stuck after upgrade but forcing a notification
253 // via `new Notification` triggered the permission request 253 // via `new Notification` triggered the permission request
254 if (isMac) { 254 if (isMac && !localStorage.getItem(CATALINA_NOTIFICATION_HACK_KEY)) {
255 if (!localStorage.getItem(CATALINA_NOTIFICATION_HACK_KEY)) { 255 debug('Triggering macOS Catalina notification permission trigger');
256 debug('Triggering macOS Catalina notification permission trigger'); 256 // eslint-disable-next-line no-new
257 // eslint-disable-next-line no-new 257 new window.Notification('Welcome to Ferdi 5', {
258 new window.Notification('Welcome to Ferdi 5', { 258 body: 'Have a wonderful day & happy messaging.',
259 body: 'Have a wonderful day & happy messaging.', 259 });
260 });
261 260
262 localStorage.setItem(CATALINA_NOTIFICATION_HACK_KEY, true); 261 localStorage.setItem(CATALINA_NOTIFICATION_HACK_KEY, true);
263 }
264 } 262 }
265 } 263 }
266 264
@@ -325,7 +323,7 @@ export default class AppStore extends Store {
325 323
326 debug('New notification', title, options); 324 debug('New notification', title, options);
327 325
328 notification.onclick = () => { 326 notification.addEventListener('click', () => {
329 if (serviceId) { 327 if (serviceId) {
330 this.actions.service.sendIPCMessage({ 328 this.actions.service.sendIPCMessage({
331 channel: `notification-onclick:${notificationId}`, 329 channel: `notification-onclick:${notificationId}`,
@@ -346,7 +344,7 @@ export default class AppStore extends Store {
346 344
347 debug('Notification click handler'); 345 debug('Notification click handler');
348 } 346 }
349 }; 347 });
350 } 348 }
351 349
352 @action _setBadge({ unreadDirectMessageCount, unreadIndirectMessageCount }) { 350 @action _setBadge({ unreadDirectMessageCount, unreadIndirectMessageCount }) {
@@ -360,7 +358,7 @@ export default class AppStore extends Store {
360 ) { 358 ) {
361 indicator = 0; 359 indicator = 0;
362 } else { 360 } else {
363 indicator = parseInt(indicator, 10); 361 indicator = Number.parseInt(indicator, 10);
364 } 362 }
365 363
366 ipcRenderer.send('updateAppIndicator', { 364 ipcRenderer.send('updateAppIndicator', {
@@ -379,8 +377,8 @@ export default class AppStore extends Store {
379 debug('disabling launch on startup'); 377 debug('disabling launch on startup');
380 autoLauncher.disable(); 378 autoLauncher.disable();
381 } 379 }
382 } catch (err) { 380 } catch (error) {
383 console.warn(err); 381 console.warn(error);
384 } 382 }
385 } 383 }
386 384
@@ -438,7 +436,7 @@ export default class AppStore extends Store {
438 const allServiceIds = await getServiceIdsFromPartitions(); 436 const allServiceIds = await getServiceIdsFromPartitions();
439 const allOrphanedServiceIds = allServiceIds.filter( 437 const allOrphanedServiceIds = allServiceIds.filter(
440 id => 438 id =>
441 !this.stores.services.all.find( 439 !this.stores.services.all.some(
442 s => id.replace('service-', '') === s.id, 440 s => id.replace('service-', '') === s.id,
443 ), 441 ),
444 ); 442 );
@@ -447,8 +445,8 @@ export default class AppStore extends Store {
447 await Promise.all( 445 await Promise.all(
448 allOrphanedServiceIds.map(id => removeServicePartitionDirectory(id)), 446 allOrphanedServiceIds.map(id => removeServicePartitionDirectory(id)),
449 ); 447 );
450 } catch (ex) { 448 } catch (error) {
451 console.log('Error while deleting service partition directory - ', ex); 449 console.log('Error while deleting service partition directory -', error);
452 } 450 }
453 await Promise.all( 451 await Promise.all(
454 this.stores.services.all.map(s => 452 this.stores.services.all.map(s =>
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js
index 1d50dd714..8e0134d7f 100644
--- a/src/stores/FeaturesStore.js
+++ b/src/stores/FeaturesStore.js
@@ -51,7 +51,9 @@ export default class FeaturesStore extends Store {
51 let requestResult = {}; 51 let requestResult = {};
52 try { 52 try {
53 requestResult = this.featuresRequest.execute().result; 53 requestResult = this.featuresRequest.execute().result;
54 } catch (e) {} // eslint-disable-line no-empty 54 } catch (error) {
55 console.error(error);
56 }
55 Object.assign(features, requestResult); 57 Object.assign(features, requestResult);
56 } 58 }
57 runInAction('FeaturesStore::_updateFeatures', () => { 59 runInAction('FeaturesStore::_updateFeatures', () => {
@@ -69,15 +71,15 @@ export default class FeaturesStore extends Store {
69 } 71 }
70 72
71 _setupFeatures() { 73 _setupFeatures() {
72 serviceProxy(this.stores, this.actions); 74 serviceProxy(this.stores);
73 basicAuth(this.stores, this.actions); 75 basicAuth();
74 workspaces(this.stores, this.actions); 76 workspaces(this.stores, this.actions);
75 quickSwitch(this.stores, this.actions); 77 quickSwitch();
76 nightlyBuilds(this.stores, this.actions); 78 nightlyBuilds();
77 publishDebugInfo(this.stores, this.actions); 79 publishDebugInfo();
78 settingsWS(this.stores, this.actions); 80 settingsWS(this.stores, this.actions);
79 communityRecipes(this.stores, this.actions); 81 communityRecipes(this.stores, this.actions);
80 todos(this.stores, this.actions); 82 todos(this.stores, this.actions);
81 appearance(this.stores, this.actions); 83 appearance(this.stores);
82 } 84 }
83} 85}
diff --git a/src/stores/GlobalErrorStore.js b/src/stores/GlobalErrorStore.js
index aacaa247f..7cbfdc608 100644
--- a/src/stores/GlobalErrorStore.js
+++ b/src/stores/GlobalErrorStore.js
@@ -12,9 +12,9 @@ export default class GlobalErrorStore extends Store {
12 constructor(...args) { 12 constructor(...args) {
13 super(...args); 13 super(...args);
14 14
15 window.onerror = (...errorArgs) => { 15 window.addEventListener('error', (...errorArgs) => {
16 this._handleConsoleError.call(this, ['error', ...errorArgs]); 16 this._handleConsoleError.call(this, ['error', ...errorArgs]);
17 }; 17 });
18 18
19 const origConsoleError = console.error; 19 const origConsoleError = console.error;
20 window.console.error = (...errorArgs) => { 20 window.console.error = (...errorArgs) => {
@@ -38,7 +38,7 @@ export default class GlobalErrorStore extends Store {
38 } 38 }
39 39
40 _handleConsoleError(type, error, url, line) { 40 _handleConsoleError(type, error, url, line) {
41 if (typeof type === 'object' && type.length && type.length >= 1) { 41 if (typeof type === 'object' && type.length > 0) {
42 this.messages.push({ 42 this.messages.push({
43 type: type[0], 43 type: type[0],
44 info: type, 44 info: type,
@@ -53,14 +53,14 @@ export default class GlobalErrorStore extends Store {
53 } 53 }
54 } 54 }
55 55
56 _handleRequests = action(async (request) => { 56 _handleRequests = action(async request => {
57 if (request.isError) { 57 if (request.isError) {
58 this.error = request.error; 58 this.error = request.error;
59 59
60 if (request.error.json) { 60 if (request.error.json) {
61 try { 61 try {
62 this.response = await request.error.json(); 62 this.response = await request.error.json();
63 } catch (error) { 63 } catch {
64 this.response = {}; 64 this.response = {};
65 } 65 }
66 if (this.error.status === 401) { 66 if (this.error.status === 401) {
diff --git a/src/stores/RecipesStore.js b/src/stores/RecipesStore.js
index d2acebb75..bfbdc57a8 100644
--- a/src/stores/RecipesStore.js
+++ b/src/stores/RecipesStore.js
@@ -25,9 +25,7 @@ export default class RecipesStore extends Store {
25 this.actions.recipe.update.listen(this._update.bind(this)); 25 this.actions.recipe.update.listen(this._update.bind(this));
26 26
27 // Reactions 27 // Reactions
28 this.registerReactions([ 28 this.registerReactions([this._checkIfRecipeIsInstalled.bind(this)]);
29 this._checkIfRecipeIsInstalled.bind(this),
30 ]);
31 } 29 }
32 30
33 setup() { 31 setup() {
@@ -39,7 +37,10 @@ export default class RecipesStore extends Store {
39 } 37 }
40 38
41 @computed get active() { 39 @computed get active() {
42 const match = matchRoute('/settings/services/add/:id', this.stores.router.location.pathname); 40 const match = matchRoute(
41 '/settings/services/add/:id',
42 this.stores.router.location.pathname,
43 );
43 if (match) { 44 if (match) {
44 const activeRecipe = this.one(match.id); 45 const activeRecipe = this.one(match.id);
45 if (activeRecipe) { 46 if (activeRecipe) {
@@ -53,11 +54,11 @@ export default class RecipesStore extends Store {
53 } 54 }
54 55
55 @computed get recipeIdForServices() { 56 @computed get recipeIdForServices() {
56 return this.stores.services.all.map((s) => s.recipe.id); 57 return this.stores.services.all.map(s => s.recipe.id);
57 } 58 }
58 59
59 one(id) { 60 one(id) {
60 return this.all.find((recipe) => recipe.id === id); 61 return this.all.find(recipe => recipe.id === id);
61 } 62 }
62 63
63 isInstalled(id) { 64 isInstalled(id) {
@@ -77,41 +78,43 @@ export default class RecipesStore extends Store {
77 const recipes = {}; 78 const recipes = {};
78 79
79 // Hackfix, reference this.all to fetch services 80 // Hackfix, reference this.all to fetch services
80 debug(`Check Recipe updates for ${this.all.map((recipe) => recipe.id)}`); 81 debug(`Check Recipe updates for ${this.all.map(recipe => recipe.id)}`);
81 82
82 recipeIds.forEach((r) => { 83 for (const r of recipeIds) {
83 const recipe = this.one(r); 84 const recipe = this.one(r);
84 recipes[r] = recipe.version; 85 recipes[r] = recipe.version;
85 }); 86 }
86 87
87 if (Object.keys(recipes).length === 0) return; 88 if (Object.keys(recipes).length === 0) return;
88 89
89 const remoteUpdates = await this.getRecipeUpdatesRequest.execute(recipes)._promise; 90 const remoteUpdates = await this.getRecipeUpdatesRequest.execute(recipes)
91 ._promise;
90 92
91 // Check for local updates 93 // Check for local updates
92 const allJsonFile = asarRecipesPath('all.json'); 94 const allJsonFile = asarRecipesPath('all.json');
93 const allJson = readJSONSync(allJsonFile); 95 const allJson = readJSONSync(allJsonFile);
94 const localUpdates = []; 96 const localUpdates = [];
95 97
96 Object.keys(recipes).forEach((recipe) => { 98 for (const recipe of Object.keys(recipes)) {
97 const version = recipes[recipe]; 99 const version = recipes[recipe];
98 100
99 // Find recipe in local recipe repository 101 // Find recipe in local recipe repository
100 const localRecipe = allJson.find((r) => r.id === recipe); 102 const localRecipe = allJson.find(r => r.id === recipe);
101 103
102 if (localRecipe && semver.lt(version, localRecipe.version)) { 104 if (localRecipe && semver.lt(version, localRecipe.version)) {
103 localUpdates.push(recipe); 105 localUpdates.push(recipe);
104 } 106 }
105 }); 107 }
106 108
107 const updates = [ 109 const updates = [...remoteUpdates, ...localUpdates];
108 ...remoteUpdates, 110 debug(
109 ...localUpdates, 111 'Got update information (local, remote):',
110 ]; 112 localUpdates,
111 debug('Got update information (local, remote):', localUpdates, remoteUpdates); 113 remoteUpdates,
114 );
112 115
113 const length = updates.length - 1; 116 const length = updates.length - 1;
114 const syncUpdate = async (i) => { 117 const syncUpdate = async i => {
115 const update = updates[i]; 118 const update = updates[i];
116 119
117 this.actions.recipe.install({ recipeId: update }); 120 this.actions.recipe.install({ recipeId: update });
@@ -134,7 +137,9 @@ export default class RecipesStore extends Store {
134 async _checkIfRecipeIsInstalled() { 137 async _checkIfRecipeIsInstalled() {
135 const { router } = this.stores; 138 const { router } = this.stores;
136 139
137 const match = router.location && matchRoute('/settings/services/add/:id', router.location.pathname); 140 const match =
141 router.location &&
142 matchRoute('/settings/services/add/:id', router.location.pathname);
138 if (match) { 143 if (match) {
139 const recipeId = match.id; 144 const recipeId = match.id;
140 145
@@ -142,9 +147,11 @@ export default class RecipesStore extends Store {
142 router.push('/settings/recipes'); 147 router.push('/settings/recipes');
143 debug(`Recipe ${recipeId} is not installed, trying to install it`); 148 debug(`Recipe ${recipeId} is not installed, trying to install it`);
144 149
145 const recipe = await this.installRecipeRequest.execute(recipeId)._promise; 150 const recipe = await this.installRecipeRequest.execute(recipeId)
151 ._promise;
146 if (recipe) { 152 if (recipe) {
147 await this.allRecipesRequest.invalidate({ immediately: true })._promise; 153 await this.allRecipesRequest.invalidate({ immediately: true })
154 ._promise;
148 router.push(`/settings/services/add/${recipeId}`); 155 router.push(`/settings/services/add/${recipeId}`);
149 } else { 156 } else {
150 router.push('/settings/recipes'); 157 router.push('/settings/recipes');
diff --git a/src/stores/RequestStore.js b/src/stores/RequestStore.js
index a92f4c685..6d2f2ef91 100644
--- a/src/stores/RequestStore.js
+++ b/src/stores/RequestStore.js
@@ -13,7 +13,7 @@ export default class RequestStore extends Store {
13 13
14 @observable showRequiredRequestsError = false; 14 @observable showRequiredRequestsError = false;
15 15
16 @observable localServerPort = 45569; 16 @observable localServerPort = 45_569;
17 17
18 retries = 0; 18 retries = 0;
19 19
@@ -22,11 +22,11 @@ export default class RequestStore extends Store {
22 constructor(...args) { 22 constructor(...args) {
23 super(...args); 23 super(...args);
24 24
25 this.actions.requests.retryRequiredRequests.listen(this._retryRequiredRequests.bind(this)); 25 this.actions.requests.retryRequiredRequests.listen(
26 this._retryRequiredRequests.bind(this),
27 );
26 28
27 this.registerReactions([ 29 this.registerReactions([this._autoRetry.bind(this)]);
28 this._autoRetry.bind(this),
29 ]);
30 } 30 }
31 31
32 setup() { 32 setup() {
@@ -41,13 +41,11 @@ export default class RequestStore extends Store {
41 } 41 }
42 42
43 @computed get areRequiredRequestsSuccessful() { 43 @computed get areRequiredRequestsSuccessful() {
44 return !this.userInfoRequest.isError 44 return !this.userInfoRequest.isError && !this.servicesRequest.isError;
45 && !this.servicesRequest.isError;
46 } 45 }
47 46
48 @computed get areRequiredRequestsLoading() { 47 @computed get areRequiredRequestsLoading() {
49 return this.userInfoRequest.isExecuting 48 return this.userInfoRequest.isExecuting || this.servicesRequest.isExecuting;
50 || this.servicesRequest.isExecuting;
51 } 49 }
52 50
53 @action _retryRequiredRequests() { 51 @action _retryRequiredRequests() {
@@ -67,7 +65,7 @@ export default class RequestStore extends Store {
67 } 65 }
68 66
69 this._autoRetry(); 67 this._autoRetry();
70 debug(`Retry required requests delayed in ${(delay) / 1000}s`); 68 debug(`Retry required requests delayed in ${delay / 1000}s`);
71 }, delay); 69 }, delay);
72 } 70 }
73 } 71 }
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index 75bc71388..67fd4103f 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -10,7 +10,10 @@ import Request from './lib/Request';
10import CachedRequest from './lib/CachedRequest'; 10import CachedRequest from './lib/CachedRequest';
11import { matchRoute } from '../helpers/routing-helpers'; 11import { matchRoute } from '../helpers/routing-helpers';
12import { isInTimeframe } from '../helpers/schedule-helpers'; 12import { isInTimeframe } from '../helpers/schedule-helpers';
13import { getRecipeDirectory, getDevRecipeDirectory } from '../helpers/recipe-helpers'; 13import {
14 getRecipeDirectory,
15 getDevRecipeDirectory,
16} from '../helpers/recipe-helpers';
14import { workspaceStore } from '../features/workspaces'; 17import { workspaceStore } from '../features/workspaces';
15import { KEEP_WS_LOADED_USID } from '../config'; 18import { KEEP_WS_LOADED_USID } from '../config';
16import { SPELLCHECKER_LOCALES } from '../i18n/languages'; 19import { SPELLCHECKER_LOCALES } from '../i18n/languages';
@@ -125,63 +128,49 @@ export default class ServicesStore extends Store {
125 setup() { 128 setup() {
126 // Single key reactions for the sake of your CPU 129 // Single key reactions for the sake of your CPU
127 reaction( 130 reaction(
128 () => ( 131 () => this.stores.settings.app.enableSpellchecking,
129 this.stores.settings.app.enableSpellchecking
130 ),
131 () => { 132 () => {
132 this._shareSettingsWithServiceProcess(); 133 this._shareSettingsWithServiceProcess();
133 }, 134 },
134 ); 135 );
135 136
136 reaction( 137 reaction(
137 () => ( 138 () => this.stores.settings.app.spellcheckerLanguage,
138 this.stores.settings.app.spellcheckerLanguage
139 ),
140 () => { 139 () => {
141 this._shareSettingsWithServiceProcess(); 140 this._shareSettingsWithServiceProcess();
142 }, 141 },
143 ); 142 );
144 143
145 reaction( 144 reaction(
146 () => ( 145 () => this.stores.settings.app.darkMode,
147 this.stores.settings.app.darkMode
148 ),
149 () => { 146 () => {
150 this._shareSettingsWithServiceProcess(); 147 this._shareSettingsWithServiceProcess();
151 }, 148 },
152 ); 149 );
153 150
154 reaction( 151 reaction(
155 () => ( 152 () => this.stores.settings.app.adaptableDarkMode,
156 this.stores.settings.app.adaptableDarkMode
157 ),
158 () => { 153 () => {
159 this._shareSettingsWithServiceProcess(); 154 this._shareSettingsWithServiceProcess();
160 }, 155 },
161 ); 156 );
162 157
163 reaction( 158 reaction(
164 () => ( 159 () => this.stores.settings.app.universalDarkMode,
165 this.stores.settings.app.universalDarkMode
166 ),
167 () => { 160 () => {
168 this._shareSettingsWithServiceProcess(); 161 this._shareSettingsWithServiceProcess();
169 }, 162 },
170 ); 163 );
171 164
172 reaction( 165 reaction(
173 () => ( 166 () => this.stores.settings.app.searchEngine,
174 this.stores.settings.app.searchEngine
175 ),
176 () => { 167 () => {
177 this._shareSettingsWithServiceProcess(); 168 this._shareSettingsWithServiceProcess();
178 }, 169 },
179 ); 170 );
180 171
181 reaction( 172 reaction(
182 () => ( 173 () => this.stores.settings.app.clipboardNotifications,
183 this.stores.settings.app.clipboardNotifications
184 ),
185 () => { 174 () => {
186 this._shareSettingsWithServiceProcess(); 175 this._shareSettingsWithServiceProcess();
187 }, 176 },
@@ -215,12 +204,12 @@ export default class ServicesStore extends Store {
215 * Run various maintenance tasks on services 204 * Run various maintenance tasks on services
216 */ 205 */
217 _serviceMaintenance() { 206 _serviceMaintenance() {
218 this.enabled.forEach(service => { 207 for (const service of this.enabled) {
219 // Defines which services should be hibernated or woken up 208 // Defines which services should be hibernated or woken up
220 if (!service.isActive) { 209 if (!service.isActive) {
221 if ( 210 if (
222 !service.lastHibernated && 211 !service.lastHibernated &&
223 (Date.now() - service.lastUsed) > 212 Date.now() - service.lastUsed >
224 ms(`${this.stores.settings.all.app.hibernationStrategy}s`) 213 ms(`${this.stores.settings.all.app.hibernationStrategy}s`)
225 ) { 214 ) {
226 // If service is stale, hibernate it. 215 // If service is stale, hibernate it.
@@ -230,8 +219,8 @@ export default class ServicesStore extends Store {
230 if ( 219 if (
231 service.lastHibernated && 220 service.lastHibernated &&
232 Number(this.stores.settings.all.app.wakeUpStrategy) > 0 && 221 Number(this.stores.settings.all.app.wakeUpStrategy) > 0 &&
233 (Date.now() - service.lastHibernated) > 222 Date.now() - service.lastHibernated >
234 ms(`${this.stores.settings.all.app.wakeUpStrategy}s`) 223 ms(`${this.stores.settings.all.app.wakeUpStrategy}s`)
235 ) { 224 ) {
236 // If service is in hibernation and the wakeup time has elapsed, wake it. 225 // If service is in hibernation and the wakeup time has elapsed, wake it.
237 this._awake({ serviceId: service.id }); 226 this._awake({ serviceId: service.id });
@@ -240,7 +229,7 @@ export default class ServicesStore extends Store {
240 229
241 if ( 230 if (
242 service.lastPoll && 231 service.lastPoll &&
243 (service.lastPoll - service.lastPollAnswer) > ms('1m') 232 service.lastPoll - service.lastPollAnswer > ms('1m')
244 ) { 233 ) {
245 // If service did not reply for more than 1m try to reload. 234 // If service did not reply for more than 1m try to reload.
246 if (!service.isActive) { 235 if (!service.isActive) {
@@ -261,7 +250,7 @@ export default class ServicesStore extends Store {
261 service.lostRecipeConnection = false; 250 service.lostRecipeConnection = false;
262 service.lostRecipeReloadAttempt = 0; 251 service.lostRecipeReloadAttempt = 0;
263 } 252 }
264 }); 253 }
265 } 254 }
266 255
267 // Computed props 256 // Computed props
@@ -270,8 +259,7 @@ export default class ServicesStore extends Store {
270 const services = this.allServicesRequest.execute().result; 259 const services = this.allServicesRequest.execute().result;
271 if (services) { 260 if (services) {
272 return observable( 261 return observable(
273 services 262 [...services]
274 .slice()
275 .slice() 263 .slice()
276 .sort((a, b) => a.order - b.order) 264 .sort((a, b) => a.order - b.order)
277 .map((s, index) => { 265 .map((s, index) => {
@@ -318,11 +306,11 @@ export default class ServicesStore extends Store {
318 // Check if workspace needs to be kept loaded 306 // Check if workspace needs to be kept loaded
319 if (workspace.services.includes(KEEP_WS_LOADED_USID)) { 307 if (workspace.services.includes(KEEP_WS_LOADED_USID)) {
320 // Get services for workspace 308 // Get services for workspace
321 const serviceIDs = workspace.services.filter( 309 const serviceIDs = new Set(
322 i => i !== KEEP_WS_LOADED_USID, 310 workspace.services.filter(i => i !== KEEP_WS_LOADED_USID),
323 ); 311 );
324 const wsServices = filteredServices.filter(service => 312 const wsServices = filteredServices.filter(service =>
325 serviceIDs.includes(service.id), 313 serviceIDs.has(service.id),
326 ); 314 );
327 315
328 displayedServices = [...displayedServices, ...wsServices]; 316 displayedServices = [...displayedServices, ...wsServices];
@@ -410,12 +398,14 @@ export default class ServicesStore extends Store {
410 customIcon: false, 398 customIcon: false,
411 isDarkModeEnabled: false, 399 isDarkModeEnabled: false,
412 spellcheckerLanguage: 400 spellcheckerLanguage:
413 SPELLCHECKER_LOCALES[this.stores.settings.app.spellcheckerLanguage], 401 SPELLCHECKER_LOCALES[this.stores.settings.app.spellcheckerLanguage],
414 userAgentPref: '', 402 userAgentPref: '',
415 ...serviceData, 403 ...serviceData,
416 }; 404 };
417 405
418 const data = skipCleanup ? serviceData : this._cleanUpTeamIdAndCustomUrl(recipeId, serviceData); 406 const data = skipCleanup
407 ? serviceData
408 : this._cleanUpTeamIdAndCustomUrl(recipeId, serviceData);
419 409
420 const response = await this.createServiceRequest.execute(recipeId, data) 410 const response = await this.createServiceRequest.execute(recipeId, data)
421 ._promise; 411 ._promise;
@@ -562,7 +552,8 @@ export default class ServicesStore extends Store {
562 // Write your scripts here 552 // Write your scripts here
563 console.log("Hello, World!", config); 553 console.log("Hello, World!", config);
564}; 554};
565`); 555`,
556 );
566 } 557 }
567 } else { 558 } else {
568 ensureFileSync(filePath); 559 ensureFileSync(filePath);
@@ -580,9 +571,9 @@ export default class ServicesStore extends Store {
580 if (!keepActiveRoute) this.stores.router.push('/'); 571 if (!keepActiveRoute) this.stores.router.push('/');
581 const service = this.one(serviceId); 572 const service = this.one(serviceId);
582 573
583 this.all.forEach(s => { 574 for (const s of this.all) {
584 s.isActive = false; 575 s.isActive = false;
585 }); 576 }
586 service.isActive = true; 577 service.isActive = true;
587 this._awake({ serviceId: service.id }); 578 this._awake({ serviceId: service.id });
588 579
@@ -618,9 +609,9 @@ export default class ServicesStore extends Store {
618 this.allDisplayed.length, 609 this.allDisplayed.length,
619 ); 610 );
620 611
621 this.all.forEach(s => { 612 for (const s of this.all) {
622 s.isActive = false; 613 s.isActive = false;
623 }); 614 }
624 this.allDisplayed[nextIndex].isActive = true; 615 this.allDisplayed[nextIndex].isActive = true;
625 } 616 }
626 617
@@ -631,9 +622,9 @@ export default class ServicesStore extends Store {
631 this.allDisplayed.length, 622 this.allDisplayed.length,
632 ); 623 );
633 624
634 this.all.forEach(s => { 625 for (const s of this.all) {
635 s.isActive = false; 626 s.isActive = false;
636 }); 627 }
637 this.allDisplayed[prevIndex].isActive = true; 628 this.allDisplayed[prevIndex].isActive = true;
638 } 629 }
639 630
@@ -699,101 +690,128 @@ export default class ServicesStore extends Store {
699 @action _handleIPCMessage({ serviceId, channel, args }) { 690 @action _handleIPCMessage({ serviceId, channel, args }) {
700 const service = this.one(serviceId); 691 const service = this.one(serviceId);
701 692
702 if (channel === 'hello') { 693 // eslint-disable-next-line default-case
703 debug('Received hello event from', serviceId); 694 switch (channel) {
704 695 case 'hello': {
705 this._initRecipePolling(service.id); 696 debug('Received hello event from', serviceId);
706 this._initializeServiceRecipeInWebview(serviceId);
707 this._shareSettingsWithServiceProcess();
708 } else if (channel === 'alive') {
709 service.lastPollAnswer = Date.now();
710 } else if (channel === 'message-counts') {
711 debug(`Received unread message info from '${serviceId}'`, args[0]);
712 697
713 this.actions.service.setUnreadMessageCount({ 698 this._initRecipePolling(service.id);
714 serviceId, 699 this._initializeServiceRecipeInWebview(serviceId);
715 count: { 700 this._shareSettingsWithServiceProcess();
716 direct: args[0].direct,
717 indirect: args[0].indirect,
718 },
719 });
720 } else if (channel === 'notification') {
721 const { options } = args[0];
722 701
723 // Check if we are in scheduled Do-not-Disturb time 702 break;
724 const { scheduledDNDEnabled, scheduledDNDStart, scheduledDNDEnd } = 703 }
725 this.stores.settings.all.app; 704 case 'alive': {
705 service.lastPollAnswer = Date.now();
726 706
727 if ( 707 break;
728 scheduledDNDEnabled &&
729 isInTimeframe(scheduledDNDStart, scheduledDNDEnd)
730 ) {
731 return;
732 } 708 }
709 case 'message-counts': {
710 debug(`Received unread message info from '${serviceId}'`, args[0]);
733 711
734 if ( 712 this.actions.service.setUnreadMessageCount({
735 service.recipe.hasNotificationSound || 713 serviceId,
736 service.isMuted || 714 count: {
737 this.stores.settings.all.app.isAppMuted 715 direct: args[0].direct,
738 ) { 716 indirect: args[0].indirect,
739 Object.assign(options, { 717 },
740 silent: true,
741 }); 718 });
719
720 break;
742 } 721 }
722 case 'notification': {
723 const { options } = args[0];
743 724
744 if (service.isNotificationEnabled) { 725 // Check if we are in scheduled Do-not-Disturb time
745 let title = `Notification from ${service.name}`; 726 const { scheduledDNDEnabled, scheduledDNDStart, scheduledDNDEnd } =
746 if (!this.stores.settings.all.app.privateNotifications) { 727 this.stores.settings.all.app;
747 options.body = typeof options.body === 'string' ? options.body : ''; 728
748 title = 729 if (
749 typeof args[0].title === 'string' ? args[0].title : service.name; 730 scheduledDNDEnabled &&
750 } else { 731 isInTimeframe(scheduledDNDStart, scheduledDNDEnd)
751 // Remove message data from notification in private mode 732 ) {
752 options.body = ''; 733 return;
753 options.icon = '/assets/img/notification-badge.gif';
754 } 734 }
755 735
756 console.log(title, options); 736 if (
737 service.recipe.hasNotificationSound ||
738 service.isMuted ||
739 this.stores.settings.all.app.isAppMuted
740 ) {
741 Object.assign(options, {
742 silent: true,
743 });
744 }
757 745
758 this.actions.app.notify({ 746 if (service.isNotificationEnabled) {
759 notificationId: args[0].notificationId, 747 let title = `Notification from ${service.name}`;
760 title, 748 if (!this.stores.settings.all.app.privateNotifications) {
761 options, 749 options.body = typeof options.body === 'string' ? options.body : '';
762 serviceId, 750 title =
763 }); 751 typeof args[0].title === 'string' ? args[0].title : service.name;
752 } else {
753 // Remove message data from notification in private mode
754 options.body = '';
755 options.icon = '/assets/img/notification-badge.gif';
756 }
757
758 console.log(title, options);
759
760 this.actions.app.notify({
761 notificationId: args[0].notificationId,
762 title,
763 options,
764 serviceId,
765 });
766 }
767
768 break;
764 } 769 }
765 } else if (channel === 'avatar') { 770 case 'avatar': {
766 const url = args[0]; 771 const url = args[0];
767 if (service.iconUrl !== url && !service.hasCustomUploadedIcon) { 772 if (service.iconUrl !== url && !service.hasCustomUploadedIcon) {
768 service.customIconUrl = url; 773 service.customIconUrl = url;
774
775 this.actions.service.updateService({
776 serviceId,
777 serviceData: {
778 customIconUrl: url,
779 },
780 redirect: false,
781 });
782 }
769 783
770 this.actions.service.updateService({ 784 break;
771 serviceId,
772 serviceData: {
773 customIconUrl: url,
774 },
775 redirect: false,
776 });
777 } 785 }
778 } else if (channel === 'new-window') { 786 case 'new-window': {
779 const url = args[0]; 787 const url = args[0];
780 788
781 this.actions.app.openExternalUrl({ url }); 789 this.actions.app.openExternalUrl({ url });
782 } else if (channel === 'set-service-spellchecker-language') { 790
783 if (!args) { 791 break;
784 console.warn('Did not receive locale'); 792 }
785 } else { 793 case 'set-service-spellchecker-language': {
786 this.actions.service.updateService({ 794 if (!args) {
787 serviceId, 795 console.warn('Did not receive locale');
788 serviceData: { 796 } else {
789 spellcheckerLanguage: args[0] === 'reset' ? '' : args[0], 797 this.actions.service.updateService({
790 }, 798 serviceId,
791 redirect: false, 799 serviceData: {
792 }); 800 spellcheckerLanguage: args[0] === 'reset' ? '' : args[0],
801 },
802 redirect: false,
803 });
804 }
805
806 break;
807 }
808 case 'feature:todos': {
809 Object.assign(args[0].data, { serviceId });
810 this.actions.todos.handleHostMessage(args[0]);
811
812 break;
793 } 813 }
794 } else if (channel === 'feature:todos') { 814 // No default
795 Object.assign(args[0].data, { serviceId });
796 this.actions.todos.handleHostMessage(args[0]);
797 } 815 }
798 } 816 }
799 817
@@ -809,13 +827,13 @@ export default class ServicesStore extends Store {
809 } 827 }
810 828
811 @action _sendIPCMessageToAllServices({ channel, args }) { 829 @action _sendIPCMessageToAllServices({ channel, args }) {
812 this.all.forEach(s => 830 for (const s of this.all) {
813 this.actions.service.sendIPCMessage({ 831 this.actions.service.sendIPCMessage({
814 serviceId: s.id, 832 serviceId: s.id,
815 channel, 833 channel,
816 args, 834 args,
817 }), 835 });
818 ); 836 }
819 } 837 }
820 838
821 @action _openWindow({ event }) { 839 @action _openWindow({ event }) {
@@ -863,11 +881,11 @@ export default class ServicesStore extends Store {
863 } 881 }
864 882
865 @action _reloadAll() { 883 @action _reloadAll() {
866 this.enabled.forEach(s => 884 for (const s of this.enabled) {
867 this._reload({ 885 this._reload({
868 serviceId: s.id, 886 serviceId: s.id,
869 }), 887 });
870 ); 888 }
871 } 889 }
872 890
873 @action _reloadUpdatedServices() { 891 @action _reloadUpdatedServices() {
@@ -901,17 +919,17 @@ export default class ServicesStore extends Store {
901 919
902 const services = {}; 920 const services = {};
903 // TODO: simplify this 921 // TODO: simplify this
904 this.all.forEach((s, index) => { 922 for (const [index] of this.all.entries()) {
905 services[this.all[index].id] = index; 923 services[this.all[index].id] = index;
906 }); 924 }
907 925
908 this.reorderServicesRequest.execute(services); 926 this.reorderServicesRequest.execute(services);
909 this.allServicesRequest.patch(data => { 927 this.allServicesRequest.patch(data => {
910 data.forEach(s => { 928 for (const s of data) {
911 const service = s; 929 const service = s;
912 930
913 service.order = services[s.id]; 931 service.order = services[s.id];
914 }); 932 }
915 }); 933 });
916 } 934 }
917 935
@@ -1001,13 +1019,14 @@ export default class ServicesStore extends Store {
1001 }`, 1019 }`,
1002 ); 1020 );
1003 1021
1022 // eslint-disable-next-line unicorn/consistent-function-scoping
1004 const resetTimer = service => { 1023 const resetTimer = service => {
1005 service.lastPollAnswer = Date.now(); 1024 service.lastPollAnswer = Date.now();
1006 service.lastPoll = Date.now(); 1025 service.lastPoll = Date.now();
1007 }; 1026 };
1008 1027
1009 if (!serviceId) { 1028 if (!serviceId) {
1010 this.allDisplayed.forEach(service => resetTimer(service)); 1029 for (const service of this.allDisplayed) resetTimer(service);
1011 } else { 1030 } else {
1012 const service = this.one(serviceId); 1031 const service = this.one(serviceId);
1013 if (service) { 1032 if (service) {
@@ -1043,7 +1062,7 @@ export default class ServicesStore extends Store {
1043 1062
1044 _mapActiveServiceToServiceModelReaction() { 1063 _mapActiveServiceToServiceModelReaction() {
1045 const { activeService } = this.stores.settings.all.service; 1064 const { activeService } = this.stores.settings.all.service;
1046 if (this.allDisplayed.length) { 1065 if (this.allDisplayed.length > 0) {
1047 this.allDisplayed.map(service => 1066 this.allDisplayed.map(service =>
1048 Object.assign(service, { 1067 Object.assign(service, {
1049 isActive: activeService 1068 isActive: activeService
@@ -1102,14 +1121,14 @@ export default class ServicesStore extends Store {
1102 const { enabled } = this; 1121 const { enabled } = this;
1103 const { isAppMuted } = this.stores.settings.app; 1122 const { isAppMuted } = this.stores.settings.app;
1104 1123
1105 enabled.forEach(service => { 1124 for (const service of enabled) {
1106 const { isAttached } = service; 1125 const { isAttached } = service;
1107 const isMuted = isAppMuted || service.isMuted; 1126 const isMuted = isAppMuted || service.isMuted;
1108 1127
1109 if (isAttached) { 1128 if (isAttached) {
1110 service.webview.audioMuted = isMuted; 1129 service.webview.audioMuted = isMuted;
1111 } 1130 }
1112 }); 1131 }
1113 } 1132 }
1114 1133
1115 _shareSettingsWithServiceProcess() { 1134 _shareSettingsWithServiceProcess() {
@@ -1151,7 +1170,7 @@ export default class ServicesStore extends Store {
1151 1170
1152 if ( 1171 if (
1153 this.allDisplayed.findIndex(service => service.isActive) === -1 && 1172 this.allDisplayed.findIndex(service => service.isActive) === -1 &&
1154 this.allDisplayed.length !== 0 1173 this.allDisplayed.length > 0
1155 ) { 1174 ) {
1156 debug('No active service found, setting active service to index 0'); 1175 debug('No active service found, setting active service to index 0');
1157 1176
diff --git a/src/stores/SettingsStore.js b/src/stores/SettingsStore.js
index 9aade974c..690a18374 100644
--- a/src/stores/SettingsStore.js
+++ b/src/stores/SettingsStore.js
@@ -1,11 +1,11 @@
1import { ipcRenderer } from 'electron'; 1import { ipcRenderer } from 'electron';
2import { getCurrentWindow } from '@electron/remote'; 2import { getCurrentWindow } from '@electron/remote';
3import { 3import { action, computed, observable, reaction } from 'mobx';
4 action, computed, observable, reaction,
5} from 'mobx';
6import localStorage from 'mobx-localstorage'; 4import localStorage from 'mobx-localstorage';
7import { 5import {
8 FILE_SYSTEM_SETTINGS_TYPES, LOCAL_SERVER, SEARCH_ENGINE_DDG, 6 FILE_SYSTEM_SETTINGS_TYPES,
7 LOCAL_SERVER,
8 SEARCH_ENGINE_DDG,
9} from '../config'; 9} from '../config';
10import { API, DEFAULT_APP_SETTINGS } from '../environment'; 10import { API, DEFAULT_APP_SETTINGS } from '../environment';
11import { getLocale } from '../helpers/i18n-helpers'; 11import { getLocale } from '../helpers/i18n-helpers';
@@ -17,7 +17,10 @@ import Store from './lib/Store';
17const debug = require('debug')('Ferdi:SettingsStore'); 17const debug = require('debug')('Ferdi:SettingsStore');
18 18
19export default class SettingsStore extends Store { 19export default class SettingsStore extends Store {
20 @observable updateAppSettingsRequest = new Request(this.api.local, 'updateAppSettings'); 20 @observable updateAppSettingsRequest = new Request(
21 this.api.local,
22 'updateAppSettings',
23 );
21 24
22 startup = true; 25 startup = true;
23 26
@@ -40,9 +43,7 @@ export default class SettingsStore extends Store {
40 await this._migrate(); 43 await this._migrate();
41 44
42 reaction( 45 reaction(
43 () => ( 46 () => this.all.app.autohideMenuBar,
44 this.all.app.autohideMenuBar
45 ),
46 () => { 47 () => {
47 const currentWindow = getCurrentWindow(); 48 const currentWindow = getCurrentWindow();
48 currentWindow.setMenuBarVisibility(!this.all.app.autohideMenuBar); 49 currentWindow.setMenuBarVisibility(!this.all.app.autohideMenuBar);
@@ -51,10 +52,8 @@ export default class SettingsStore extends Store {
51 ); 52 );
52 53
53 reaction( 54 reaction(
54 () => ( 55 () => this.all.app.server,
55 this.all.app.server 56 server => {
56 ),
57 (server) => {
58 if (server === LOCAL_SERVER) { 57 if (server === LOCAL_SERVER) {
59 ipcRenderer.send('startLocalServer'); 58 ipcRenderer.send('startLocalServer');
60 } 59 }
@@ -65,7 +64,10 @@ export default class SettingsStore extends Store {
65 // Inactivity lock timer 64 // Inactivity lock timer
66 let inactivityTimer; 65 let inactivityTimer;
67 getCurrentWindow().on('blur', () => { 66 getCurrentWindow().on('blur', () => {
68 if (this.all.app.lockingFeatureEnabled && this.all.app.inactivityLock !== 0) { 67 if (
68 this.all.app.lockingFeatureEnabled &&
69 this.all.app.inactivityLock !== 0
70 ) {
69 inactivityTimer = setTimeout(() => { 71 inactivityTimer = setTimeout(() => {
70 this.actions.settings.update({ 72 this.actions.settings.update({
71 type: 'app', 73 type: 'app',
@@ -84,7 +86,11 @@ export default class SettingsStore extends Store {
84 86
85 ipcRenderer.on('appSettings', (event, resp) => { 87 ipcRenderer.on('appSettings', (event, resp) => {
86 // Lock on startup if enabled in settings 88 // Lock on startup if enabled in settings
87 if (this.startup && resp.type === 'app' && resp.data.lockingFeatureEnabled) { 89 if (
90 this.startup &&
91 resp.type === 'app' &&
92 resp.data.lockingFeatureEnabled
93 ) {
88 this.startup = false; 94 this.startup = false;
89 process.nextTick(() => { 95 process.nextTick(() => {
90 if (!this.all.app.locked) { 96 if (!this.all.app.locked) {
@@ -97,9 +103,9 @@ export default class SettingsStore extends Store {
97 ipcRenderer.send('initialAppSettings', resp); 103 ipcRenderer.send('initialAppSettings', resp);
98 }); 104 });
99 105
100 this.fileSystemSettingsTypes.forEach((type) => { 106 for (const type of this.fileSystemSettingsTypes) {
101 ipcRenderer.send('getAppSettings', type); 107 ipcRenderer.send('getAppSettings', type);
102 }); 108 }
103 } 109 }
104 110
105 @computed get app() { 111 @computed get app() {
@@ -111,15 +117,19 @@ export default class SettingsStore extends Store {
111 } 117 }
112 118
113 @computed get service() { 119 @computed get service() {
114 return localStorage.getItem('service') || { 120 return (
115 activeService: '', 121 localStorage.getItem('service') || {
116 }; 122 activeService: '',
123 }
124 );
117 } 125 }
118 126
119 @computed get stats() { 127 @computed get stats() {
120 return localStorage.getItem('stats') || { 128 return (
121 activeService: '', 129 localStorage.getItem('stats') || {
122 }; 130 activeService: '',
131 }
132 );
123 } 133 }
124 134
125 @computed get migration() { 135 @computed get migration() {
@@ -230,9 +240,7 @@ export default class SettingsStore extends Store {
230 }); 240 });
231 241
232 this._ensureMigrationAndMarkDone('5.4.4-beta.2-settings', () => { 242 this._ensureMigrationAndMarkDone('5.4.4-beta.2-settings', () => {
233 const { 243 const { showServiceNavigationBar } = this.all.app;
234 showServiceNavigationBar,
235 } = this.all.app;
236 244
237 this.actions.settings.update({ 245 this.actions.settings.update({
238 type: 'app', 246 type: 'app',
@@ -248,7 +256,7 @@ export default class SettingsStore extends Store {
248 data: { 256 data: {
249 todoServer: 'isUsingCustomTodoService', 257 todoServer: 'isUsingCustomTodoService',
250 customTodoServer: legacySettings.todoServer, 258 customTodoServer: legacySettings.todoServer,
251 automaticUpdates: !(legacySettings.noUpdates), 259 automaticUpdates: !legacySettings.noUpdates,
252 }, 260 },
253 }); 261 });
254 262
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index 2e009893a..e3d57c662 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -46,7 +46,10 @@ export default class UserStore extends Store {
46 46
47 @observable updateUserInfoRequest = new Request(this.api.user, 'updateInfo'); 47 @observable updateUserInfoRequest = new Request(this.api.user, 'updateInfo');
48 48
49 @observable getLegacyServicesRequest = new CachedRequest(this.api.user, 'getLegacyServices'); 49 @observable getLegacyServicesRequest = new CachedRequest(
50 this.api.user,
51 'getLegacyServices',
52 );
50 53
51 @observable deleteAccountRequest = new CachedRequest(this.api.user, 'delete'); 54 @observable deleteAccountRequest = new CachedRequest(this.api.user, 'delete');
52 55
@@ -81,13 +84,17 @@ export default class UserStore extends Store {
81 84
82 // Register action handlers 85 // Register action handlers
83 this.actions.user.login.listen(this._login.bind(this)); 86 this.actions.user.login.listen(this._login.bind(this));
84 this.actions.user.retrievePassword.listen(this._retrievePassword.bind(this)); 87 this.actions.user.retrievePassword.listen(
88 this._retrievePassword.bind(this),
89 );
85 this.actions.user.logout.listen(this._logout.bind(this)); 90 this.actions.user.logout.listen(this._logout.bind(this));
86 this.actions.user.signup.listen(this._signup.bind(this)); 91 this.actions.user.signup.listen(this._signup.bind(this));
87 this.actions.user.invite.listen(this._invite.bind(this)); 92 this.actions.user.invite.listen(this._invite.bind(this));
88 this.actions.user.update.listen(this._update.bind(this)); 93 this.actions.user.update.listen(this._update.bind(this));
89 this.actions.user.resetStatus.listen(this._resetStatus.bind(this)); 94 this.actions.user.resetStatus.listen(this._resetStatus.bind(this));
90 this.actions.user.importLegacyServices.listen(this._importLegacyServices.bind(this)); 95 this.actions.user.importLegacyServices.listen(
96 this._importLegacyServices.bind(this),
97 );
91 this.actions.user.delete.listen(this._delete.bind(this)); 98 this.actions.user.delete.listen(this._delete.bind(this));
92 99
93 // Reactions 100 // Reactions
@@ -176,7 +183,14 @@ export default class UserStore extends Store {
176 } 183 }
177 184
178 @action async _signup({ 185 @action async _signup({
179 firstname, lastname, email, password, accountType, company, plan, currency, 186 firstname,
187 lastname,
188 email,
189 password,
190 accountType,
191 company,
192 plan,
193 currency,
180 }) { 194 }) {
181 const authToken = await this.signupRequest.execute({ 195 const authToken = await this.signupRequest.execute({
182 firstname, 196 firstname,
@@ -205,7 +219,7 @@ export default class UserStore extends Store {
205 } 219 }
206 220
207 @action async _invite({ invites }) { 221 @action async _invite({ invites }) {
208 const data = invites.filter((invite) => invite.email !== ''); 222 const data = invites.filter(invite => invite.email !== '');
209 223
210 const response = await this.inviteRequest.execute(data)._promise; 224 const response = await this.inviteRequest.execute(data)._promise;
211 225
@@ -220,7 +234,8 @@ export default class UserStore extends Store {
220 @action async _update({ userData }) { 234 @action async _update({ userData }) {
221 if (!this.isLoggedIn) return; 235 if (!this.isLoggedIn) return;
222 236
223 const response = await this.updateUserInfoRequest.execute(userData)._promise; 237 const response = await this.updateUserInfoRequest.execute(userData)
238 ._promise;
224 239
225 this.getUserInfoRequest.patch(() => response.data); 240 this.getUserInfoRequest.patch(() => response.data);
226 this.actionStatus = response.status || []; 241 this.actionStatus = response.status || [];
@@ -250,19 +265,27 @@ export default class UserStore extends Store {
250 this.isImportLegacyServicesExecuting = true; 265 this.isImportLegacyServicesExecuting = true;
251 266
252 // Reduces recipe duplicates 267 // Reduces recipe duplicates
253 const recipes = services.filter((obj, pos, arr) => arr.map((mapObj) => mapObj.recipe.id).indexOf(obj.recipe.id) === pos).map((s) => s.recipe.id); 268 const recipes = services
269 .filter(
270 (obj, pos, arr) =>
271 arr.map(mapObj => mapObj.recipe.id).indexOf(obj.recipe.id) === pos,
272 )
273 .map(s => s.recipe.id);
254 274
255 // Install recipes 275 // Install recipes
256 for (const recipe of recipes) { // eslint-disable-line no-unused-vars 276 for (const recipe of recipes) {
257 // eslint-disable-next-line 277 // eslint-disable-line no-unused-vars
278 // eslint-disable-next-line no-await-in-loop
258 await this.stores.recipes._install({ recipeId: recipe }); 279 await this.stores.recipes._install({ recipeId: recipe });
259 } 280 }
260 281
261 for (const service of services) { // eslint-disable-line no-unused-vars 282 for (const service of services) {
283 // eslint-disable-line no-unused-vars
262 this.actions.service.createFromLegacyService({ 284 this.actions.service.createFromLegacyService({
263 data: service, 285 data: service,
264 }); 286 });
265 await this.stores.services.createServiceRequest._promise; // eslint-disable-line 287 // eslint-disable-next-line no-await-in-loop
288 await this.stores.services.createServiceRequest._promise;
266 } 289 }
267 290
268 this.isImportLegacyServicesExecuting = false; 291 this.isImportLegacyServicesExecuting = false;
@@ -281,8 +304,7 @@ export default class UserStore extends Store {
281 304
282 const { router } = this.stores; 305 const { router } = this.stores;
283 const currentRoute = window.location.hash; 306 const currentRoute = window.location.hash;
284 if (!this.isLoggedIn 307 if (!this.isLoggedIn && currentRoute.includes('token=')) {
285 && currentRoute.includes('token=')) {
286 router.push(this.WELCOME_ROUTE); 308 router.push(this.WELCOME_ROUTE);
287 const token = currentRoute.split('=')[1]; 309 const token = currentRoute.split('=')[1];
288 310
@@ -293,20 +315,18 @@ export default class UserStore extends Store {
293 this._tokenLogin(token); 315 this._tokenLogin(token);
294 }, 1000); 316 }, 1000);
295 } 317 }
296 } else if (!this.isLoggedIn 318 } else if (!this.isLoggedIn && !currentRoute.includes(this.BASE_ROUTE)) {
297 && !currentRoute.includes(this.BASE_ROUTE)) {
298 router.push(this.WELCOME_ROUTE); 319 router.push(this.WELCOME_ROUTE);
299 } else if (this.isLoggedIn 320 } else if (this.isLoggedIn && currentRoute === this.LOGOUT_ROUTE) {
300 && currentRoute === this.LOGOUT_ROUTE) {
301 this.actions.user.logout(); 321 this.actions.user.logout();
302 router.push(this.LOGIN_ROUTE); 322 router.push(this.LOGIN_ROUTE);
303 } else if (this.isLoggedIn 323 } else if (
304 && currentRoute.includes(this.BASE_ROUTE) 324 this.isLoggedIn &&
305 && (this.hasCompletedSignup 325 currentRoute.includes(this.BASE_ROUTE) &&
306 || this.hasCompletedSignup === null)) { 326 (this.hasCompletedSignup || this.hasCompletedSignup === null) &&
307 if (!isDevMode) { 327 !isDevMode
308 this.stores.router.push('/'); 328 ) {
309 } 329 this.stores.router.push('/');
310 } 330 }
311 }; 331 };
312 332
@@ -316,7 +336,7 @@ export default class UserStore extends Store {
316 let data; 336 let data;
317 try { 337 try {
318 data = await this.getUserInfoRequest.execute()._promise; 338 data = await this.getUserInfoRequest.execute()._promise;
319 } catch (e) { 339 } catch {
320 return false; 340 return false;
321 } 341 }
322 342
@@ -336,12 +356,12 @@ export default class UserStore extends Store {
336 try { 356 try {
337 const decoded = jwt.decode(authToken); 357 const decoded = jwt.decode(authToken);
338 358
339 return ({ 359 return {
340 id: decoded.userId, 360 id: decoded.userId,
341 tokenExpiry: moment.unix(decoded.exp).toISOString(), 361 tokenExpiry: moment.unix(decoded.exp).toISOString(),
342 authToken, 362 authToken,
343 }); 363 };
344 } catch (err) { 364 } catch {
345 this._logout(); 365 this._logout();
346 return false; 366 return false;
347 } 367 }
@@ -372,7 +392,7 @@ export default class UserStore extends Store {
372 async _migrateUserLocale() { 392 async _migrateUserLocale() {
373 try { 393 try {
374 await this.getUserInfoRequest._promise; 394 await this.getUserInfoRequest._promise;
375 } catch (e) { 395 } catch {
376 return false; 396 return false;
377 } 397 }
378 398
diff --git a/src/stores/index.ts b/src/stores/index.ts
index 4cd4e92ea..1760ddfa2 100644
--- a/src/stores/index.ts
+++ b/src/stores/index.ts
@@ -34,10 +34,10 @@ export default (api, actions, router) => {
34 }); 34 });
35 35
36 // Initialize all stores 36 // Initialize all stores
37 Object.keys(stores).forEach(name => { 37 for (const name of Object.keys(stores)) {
38 if (stores[name] && stores[name].initialize) { 38 if (stores[name] && stores[name].initialize) {
39 stores[name].initialize(); 39 stores[name].initialize();
40 } 40 }
41 }); 41 }
42 return stores; 42 return stores;
43}; 43};
diff --git a/src/stores/lib/CachedRequest.js b/src/stores/lib/CachedRequest.js
index 94f615144..a6dd47f7d 100644
--- a/src/stores/lib/CachedRequest.js
+++ b/src/stores/lib/CachedRequest.js
@@ -1,4 +1,3 @@
1// @flow
2import { action } from 'mobx'; 1import { action } from 'mobx';
3import { isEqual, remove } from 'lodash'; 2import { isEqual, remove } from 'lodash';
4import Request from './Request'; 3import Request from './Request';
@@ -30,48 +29,60 @@ export default class CachedRequest extends Request {
30 29
31 // This timeout is necessary to avoid warnings from mobx 30 // This timeout is necessary to avoid warnings from mobx
32 // regarding triggering actions as side-effect of getters 31 // regarding triggering actions as side-effect of getters
33 setTimeout(action(() => { 32 setTimeout(
34 this.isExecuting = true; 33 action(() => {
35 // Apply the previous result from this call immediately (cached) 34 this.isExecuting = true;
36 if (existingApiCall) { 35 // Apply the previous result from this call immediately (cached)
37 this.result = existingApiCall.result; 36 if (existingApiCall) {
38 } 37 this.result = existingApiCall.result;
39 }), 0); 38 }
39 }),
40 0,
41 );
40 42
41 // Issue api call & save it as promise that is handled to update the results of the operation 43 // Issue api call & save it as promise that is handled to update the results of the operation
42 this._promise = new Promise((resolve) => { 44 this._promise = new Promise(resolve => {
43 this._api[this._method](...callArgs) 45 this._api[this._method](...callArgs)
44 .then((result) => { 46 .then(result => {
45 setTimeout(action(() => { 47 setTimeout(
46 this.result = result; 48 action(() => {
47 if (this._currentApiCall) this._currentApiCall.result = result; 49 this.result = result;
48 this.isExecuting = false; 50 if (this._currentApiCall) this._currentApiCall.result = result;
49 this.isError = false; 51 this.isExecuting = false;
50 this.wasExecuted = true; 52 this.isError = false;
51 this._isInvalidated = false; 53 this.wasExecuted = true;
52 this._isWaitingForResponse = false; 54 this._isInvalidated = false;
53 this._triggerHooks(); 55 this._isWaitingForResponse = false;
54 resolve(result); 56 this._triggerHooks();
55 }), 1); 57 resolve(result);
58 }),
59 1,
60 );
56 return result; 61 return result;
57 }) 62 })
58 .catch(action((error) => { 63 .catch(
59 setTimeout(action(() => { 64 action(error => {
60 this.error = error; 65 setTimeout(
61 this.isExecuting = false; 66 action(() => {
62 this.isError = true; 67 this.error = error;
63 this.wasExecuted = true; 68 this.isExecuting = false;
64 this._isWaitingForResponse = false; 69 this.isError = true;
65 this._triggerHooks(); 70 this.wasExecuted = true;
66 // reject(error); 71 this._isWaitingForResponse = false;
67 }), 1); 72 this._triggerHooks();
68 })); 73 // reject(error);
74 }),
75 1,
76 );
77 }),
78 );
69 }); 79 });
70 80
71 this._isWaitingForResponse = true; 81 this._isWaitingForResponse = true;
72 return this; 82 return this;
73 } 83 }
74 84
85 // eslint-disable-next-line unicorn/no-object-as-default-parameter
75 invalidate(options = { immediately: false }) { 86 invalidate(options = { immediately: false }) {
76 this._isInvalidated = true; 87 this._isInvalidated = true;
77 if (options.immediately && this._currentApiCall) { 88 if (options.immediately && this._currentApiCall) {
@@ -81,18 +92,21 @@ export default class CachedRequest extends Request {
81 } 92 }
82 93
83 patch(modify) { 94 patch(modify) {
84 return new Promise((resolve) => { 95 return new Promise(resolve => {
85 setTimeout(action(() => { 96 setTimeout(
86 const override = modify(this.result); 97 action(() => {
87 if (override !== undefined) this.result = override; 98 const override = modify(this.result);
88 if (this._currentApiCall) this._currentApiCall.result = this.result; 99 if (override !== undefined) this.result = override;
89 resolve(this); 100 if (this._currentApiCall) this._currentApiCall.result = this.result;
90 }), 0); 101 resolve(this);
102 }),
103 0,
104 );
91 }); 105 });
92 } 106 }
93 107
94 removeCacheForCallWith(...args) { 108 removeCacheForCallWith(...args) {
95 remove(this._apiCalls, (c) => isEqual(c.args, args)); 109 remove(this._apiCalls, c => isEqual(c.args, args));
96 } 110 }
97 111
98 _addApiCall(args) { 112 _addApiCall(args) {
@@ -102,6 +116,6 @@ export default class CachedRequest extends Request {
102 } 116 }
103 117
104 _findApiCall(args) { 118 _findApiCall(args) {
105 return this._apiCalls.find((c) => isEqual(c.args, args)); 119 return this._apiCalls.find(c => isEqual(c.args, args));
106 } 120 }
107} 121}
diff --git a/src/stores/lib/Request.js b/src/stores/lib/Request.js
index 32ffe4367..39f32729a 100644
--- a/src/stores/lib/Request.js
+++ b/src/stores/lib/Request.js
@@ -107,7 +107,7 @@ export default class Request {
107 } 107 }
108 108
109 _triggerHooks() { 109 _triggerHooks() {
110 Request._hooks.forEach((hook) => hook(this)); 110 for (const hook of Request._hooks) hook(this);
111 } 111 }
112 112
113 reset = () => { 113 reset = () => {
diff --git a/src/stores/lib/Store.js b/src/stores/lib/Store.js
index b03a7e725..b39070ce8 100644
--- a/src/stores/lib/Store.js
+++ b/src/stores/lib/Store.js
@@ -28,18 +28,18 @@ export default class Store {
28 } 28 }
29 29
30 registerReactions(reactions) { 30 registerReactions(reactions) {
31 reactions.forEach((reaction) => this._reactions.push(new Reaction(reaction))); 31 for (const reaction of reactions) this._reactions.push(new Reaction(reaction));
32 } 32 }
33 33
34 setup() {} 34 setup() {}
35 35
36 initialize() { 36 initialize() {
37 this.setup(); 37 this.setup();
38 this._reactions.forEach((reaction) => reaction.start()); 38 for (const reaction of this._reactions) reaction.start();
39 } 39 }
40 40
41 teardown() { 41 teardown() {
42 this._reactions.forEach((reaction) => reaction.stop()); 42 for (const reaction of this._reactions) reaction.stop();
43 } 43 }
44 44
45 resetStatus() { 45 resetStatus() {
diff --git a/src/webview/badge.ts b/src/webview/badge.ts
index 024e29b3f..8e8b66c0c 100644
--- a/src/webview/badge.ts
+++ b/src/webview/badge.ts
@@ -21,7 +21,7 @@ export class BadgeHandler {
21 // Parse number to integer 21 // Parse number to integer
22 // This will correct errors that recipes may introduce, e.g. 22 // This will correct errors that recipes may introduce, e.g.
23 // by sending a String instead of an integer 23 // by sending a String instead of an integer
24 const parsedNumber = parseInt(text.toString(), 10); 24 const parsedNumber = Number.parseInt(text.toString(), 10);
25 const adjustedNumber = Number.isNaN(parsedNumber) ? 0 : parsedNumber; 25 const adjustedNumber = Number.isNaN(parsedNumber) ? 0 : parsedNumber;
26 return Math.max(adjustedNumber, 0); 26 return Math.max(adjustedNumber, 0);
27 } 27 }
diff --git a/src/webview/contextMenuBuilder.js b/src/webview/contextMenuBuilder.js
index 8c39e6d04..eb15aa138 100644
--- a/src/webview/contextMenuBuilder.js
+++ b/src/webview/contextMenuBuilder.js
@@ -6,7 +6,8 @@
6 * 6 *
7 * Source: https://github.com/electron-userland/electron-spellchecker/blob/master/src/context-menu-builder.js 7 * Source: https://github.com/electron-userland/electron-spellchecker/blob/master/src/context-menu-builder.js
8 */ 8 */
9import { clipboard, ipcRenderer, nativeImage } from 'electron'; 9// eslint-disable-next-line no-unused-vars
10import { clipboard, ipcRenderer, nativeImage, WebContents } from 'electron';
10import { Menu, MenuItem } from '@electron/remote'; 11import { Menu, MenuItem } from '@electron/remote';
11import { cmdOrCtrlShortcutKey, isMac } from '../environment'; 12import { cmdOrCtrlShortcutKey, isMac } from '../environment';
12 13
@@ -16,7 +17,8 @@ import { openExternalUrl } from '../helpers/url-helpers';
16const { URL } = require('url'); 17const { URL } = require('url');
17 18
18function matchesWord(string) { 19function matchesWord(string) {
19 const regex = /[\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]+/g; 20 const regex =
21 /[A-Za-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]+/g;
20 22
21 return string.match(regex); 23 return string.match(regex);
22} 24}
@@ -56,12 +58,12 @@ module.exports = class ContextMenuBuilder {
56 /** 58 /**
57 * Creates an instance of ContextMenuBuilder 59 * Creates an instance of ContextMenuBuilder
58 * 60 *
59 * @param {webContents} webContents Current webContents 61 * @param {WebContents} webContents Current webContents
60 * @param {Boolean} debugMode If true, display the "Inspect Element" menu item. 62 * @param {Boolean} debugMode If true, display the "Inspect Element" menu item.
61 * @param {function} processMenu If passed, this method will be passed the menu to change 63 * @param {function} processMenu If passed, this method will be passed the menu to change
62 * it prior to display. Signature: (menu, info) => menu 64 * it prior to display. Signature: (menu, info) => menu
63 */ 65 */
64 constructor(webContents, debugMode = false, processMenu = (m) => m) { 66 constructor(webContents, debugMode = false, processMenu = m => m) {
65 this.debugMode = debugMode; 67 this.debugMode = debugMode;
66 this.processMenu = processMenu; 68 this.processMenu = processMenu;
67 this.menu = null; 69 this.menu = null;
@@ -115,7 +117,10 @@ module.exports = class ContextMenuBuilder {
115 return this.buildMenuForImage(info); 117 return this.buildMenuForImage(info);
116 } 118 }
117 119
118 if (info.isEditable || (info.inputFieldType && info.inputFieldType !== 'none')) { 120 if (
121 info.isEditable ||
122 (info.inputFieldType && info.inputFieldType !== 'none')
123 ) {
119 return this.buildMenuForTextInput(info); 124 return this.buildMenuForTextInput(info);
120 } 125 }
121 126
@@ -157,12 +162,17 @@ module.exports = class ContextMenuBuilder {
157 const isEmailAddress = menuInfo.linkURL.startsWith('mailto:'); 162 const isEmailAddress = menuInfo.linkURL.startsWith('mailto:');
158 163
159 const copyLink = new MenuItem({ 164 const copyLink = new MenuItem({
160 label: isEmailAddress ? this.stringTable.copyMail() : this.stringTable.copyLinkUrl(), 165 label: isEmailAddress
166 ? this.stringTable.copyMail()
167 : this.stringTable.copyLinkUrl(),
161 click: () => { 168 click: () => {
162 // Omit the mailto: portion of the link; we just want the address 169 // Omit the mailto: portion of the link; we just want the address
163 const url = isEmailAddress ? menuInfo.linkText : menuInfo.linkURL; 170 const url = isEmailAddress ? menuInfo.linkText : menuInfo.linkURL;
164 clipboard.writeText(url); 171 clipboard.writeText(url);
165 this._sendNotificationOnClipboardEvent(menuInfo.clipboardNotifications, () => `Link URL copied: ${url}`); 172 this._sendNotificationOnClipboardEvent(
173 menuInfo.clipboardNotifications,
174 () => `Link URL copied: ${url}`,
175 );
166 }, 176 },
167 }); 177 });
168 178
@@ -250,11 +260,13 @@ module.exports = class ContextMenuBuilder {
250 const webContents = this.getWebContents(); 260 const webContents = this.getWebContents();
251 // Add each spelling suggestion 261 // Add each spelling suggestion
252 for (const suggestion of menuInfo.dictionarySuggestions) { 262 for (const suggestion of menuInfo.dictionarySuggestions) {
253 menu.append(new MenuItem({ 263 menu.append(
254 label: suggestion, 264 new MenuItem({
255 // eslint-disable-next-line no-loop-func 265 label: suggestion,
256 click: () => webContents.replaceMisspelling(suggestion), 266 // eslint-disable-next-line no-loop-func
257 })); 267 click: () => webContents.replaceMisspelling(suggestion),
268 }),
269 );
258 } 270 }
259 271
260 // Allow users to add the misspelled word to the dictionary 272 // Allow users to add the misspelled word to the dictionary
@@ -262,7 +274,10 @@ module.exports = class ContextMenuBuilder {
262 menu.append( 274 menu.append(
263 new MenuItem({ 275 new MenuItem({
264 label: this.stringTable.addToDictionary(), 276 label: this.stringTable.addToDictionary(),
265 click: () => webContents.session.addWordToSpellCheckerDictionary(menuInfo.misspelledWord), 277 click: () =>
278 webContents.session.addWordToSpellCheckerDictionary(
279 menuInfo.misspelledWord,
280 ),
266 }), 281 }),
267 ); 282 );
268 } 283 }
@@ -274,7 +289,7 @@ module.exports = class ContextMenuBuilder {
274 * Adds search-related menu items. 289 * Adds search-related menu items.
275 */ 290 */
276 addSearchItems(menu, menuInfo) { 291 addSearchItems(menu, menuInfo) {
277 if (!menuInfo.selectionText || menuInfo.selectionText.length < 1) { 292 if (!menuInfo.selectionText || menuInfo.selectionText.length === 0) {
278 return menu; 293 return menu;
279 } 294 }
280 295
@@ -287,7 +302,9 @@ module.exports = class ContextMenuBuilder {
287 const webContents = this.getWebContents(); 302 const webContents = this.getWebContents();
288 303
289 const lookUpDefinition = new MenuItem({ 304 const lookUpDefinition = new MenuItem({
290 label: this.stringTable.lookUpDefinition({ word: menuInfo.selectionText.trim() }), 305 label: this.stringTable.lookUpDefinition({
306 word: menuInfo.selectionText.trim(),
307 }),
291 click: () => webContents.showDefinitionForSelection(), 308 click: () => webContents.showDefinitionForSelection(),
292 }); 309 });
293 310
@@ -295,9 +312,13 @@ module.exports = class ContextMenuBuilder {
295 } 312 }
296 313
297 const search = new MenuItem({ 314 const search = new MenuItem({
298 label: this.stringTable.searchWith({ searchEngine: SEARCH_ENGINE_NAMES[menuInfo.searchEngine] }), 315 label: this.stringTable.searchWith({
316 searchEngine: SEARCH_ENGINE_NAMES[menuInfo.searchEngine],
317 }),
299 click: () => { 318 click: () => {
300 const url = SEARCH_ENGINE_URLS[menuInfo.searchEngine]({ searchTerm: encodeURIComponent(menuInfo.selectionText) }); 319 const url = SEARCH_ENGINE_URLS[menuInfo.searchEngine]({
320 searchTerm: encodeURIComponent(menuInfo.selectionText),
321 });
301 openExternalUrl(url, true); 322 openExternalUrl(url, true);
302 }, 323 },
303 }); 324 });
@@ -319,10 +340,14 @@ module.exports = class ContextMenuBuilder {
319 const copyImage = new MenuItem({ 340 const copyImage = new MenuItem({
320 label: this.stringTable.copyImage(), 341 label: this.stringTable.copyImage(),
321 click: () => { 342 click: () => {
322 const result = this.convertImageToBase64(menuInfo.srcURL, 343 const result = this.convertImageToBase64(menuInfo.srcURL, dataURL =>
323 (dataURL) => clipboard.writeImage(nativeImage.createFromDataURL(dataURL))); 344 clipboard.writeImage(nativeImage.createFromDataURL(dataURL)),
324 345 );
325 this._sendNotificationOnClipboardEvent(menuInfo.clipboardNotifications, () => `Image copied from URL: ${menuInfo.srcURL}`); 346
347 this._sendNotificationOnClipboardEvent(
348 menuInfo.clipboardNotifications,
349 () => `Image copied from URL: ${menuInfo.srcURL}`,
350 );
326 return result; 351 return result;
327 }, 352 },
328 }); 353 });
@@ -333,7 +358,10 @@ module.exports = class ContextMenuBuilder {
333 label: this.stringTable.copyImageUrl(), 358 label: this.stringTable.copyImageUrl(),
334 click: () => { 359 click: () => {
335 const result = clipboard.writeText(menuInfo.srcURL); 360 const result = clipboard.writeText(menuInfo.srcURL);
336 this._sendNotificationOnClipboardEvent(menuInfo.clipboardNotifications, () => `Image URL copied: ${menuInfo.srcURL}`); 361 this._sendNotificationOnClipboardEvent(
362 menuInfo.clipboardNotifications,
363 () => `Image URL copied: ${menuInfo.srcURL}`,
364 );
337 return result; 365 return result;
338 }, 366 },
339 }); 367 });
@@ -345,20 +373,22 @@ module.exports = class ContextMenuBuilder {
345 const downloadImage = new MenuItem({ 373 const downloadImage = new MenuItem({
346 label: this.stringTable.downloadImage(), 374 label: this.stringTable.downloadImage(),
347 click: () => { 375 click: () => {
348 const urlWithoutBlob = menuInfo.srcURL.substr(5); 376 const urlWithoutBlob = menuInfo.srcURL.slice(5);
349 this.convertImageToBase64(menuInfo.srcURL, 377 this.convertImageToBase64(menuInfo.srcURL, dataURL => {
350 (dataURL) => { 378 const url = new window.URL(urlWithoutBlob);
351 const url = new window.URL(urlWithoutBlob); 379 const fileName = url.pathname.slice(1);
352 const fileName = url.pathname.substr(1); 380 ipcRenderer.send('download-file', {
353 ipcRenderer.send('download-file', { 381 content: dataURL,
354 content: dataURL, 382 fileOptions: {
355 fileOptions: { 383 name: fileName,
356 name: fileName, 384 mime: 'image/png',
357 mime: 'image/png', 385 },
358 },
359 });
360 }); 386 });
361 this._sendNotificationOnClipboardEvent(menuInfo.clipboardNotifications, () => `Image downloaded: ${urlWithoutBlob}`); 387 });
388 this._sendNotificationOnClipboardEvent(
389 menuInfo.clipboardNotifications,
390 () => `Image downloaded: ${urlWithoutBlob}`,
391 );
362 }, 392 },
363 }); 393 });
364 394
@@ -373,12 +403,14 @@ module.exports = class ContextMenuBuilder {
373 */ 403 */
374 addCut(menu, menuInfo) { 404 addCut(menu, menuInfo) {
375 const webContents = this.getWebContents(); 405 const webContents = this.getWebContents();
376 menu.append(new MenuItem({ 406 menu.append(
377 label: this.stringTable.cut(), 407 new MenuItem({
378 accelerator: `${cmdOrCtrlShortcutKey()}+X`, 408 label: this.stringTable.cut(),
379 enabled: menuInfo.editFlags.canCut, 409 accelerator: `${cmdOrCtrlShortcutKey()}+X`,
380 click: () => webContents.cut(), 410 enabled: menuInfo.editFlags.canCut,
381 })); 411 click: () => webContents.cut(),
412 }),
413 );
382 414
383 return menu; 415 return menu;
384 } 416 }
@@ -388,12 +420,14 @@ module.exports = class ContextMenuBuilder {
388 */ 420 */
389 addCopy(menu, menuInfo) { 421 addCopy(menu, menuInfo) {
390 const webContents = this.getWebContents(); 422 const webContents = this.getWebContents();
391 menu.append(new MenuItem({ 423 menu.append(
392 label: this.stringTable.copy(), 424 new MenuItem({
393 accelerator: `${cmdOrCtrlShortcutKey()}+C`, 425 label: this.stringTable.copy(),
394 enabled: menuInfo.editFlags.canCopy, 426 accelerator: `${cmdOrCtrlShortcutKey()}+C`,
395 click: () => webContents.copy(), 427 enabled: menuInfo.editFlags.canCopy,
396 })); 428 click: () => webContents.copy(),
429 }),
430 );
397 431
398 return menu; 432 return menu;
399 } 433 }
@@ -403,21 +437,23 @@ module.exports = class ContextMenuBuilder {
403 */ 437 */
404 addPaste(menu, menuInfo) { 438 addPaste(menu, menuInfo) {
405 const webContents = this.getWebContents(); 439 const webContents = this.getWebContents();
406 menu.append(new MenuItem({ 440 menu.append(
407 label: this.stringTable.paste(), 441 new MenuItem({
408 accelerator: `${cmdOrCtrlShortcutKey()}+V`, 442 label: this.stringTable.paste(),
409 enabled: menuInfo.editFlags.canPaste, 443 accelerator: `${cmdOrCtrlShortcutKey()}+V`,
410 click: () => webContents.paste(), 444 enabled: menuInfo.editFlags.canPaste,
411 })); 445 click: () => webContents.paste(),
446 }),
447 );
412 448
413 return menu; 449 return menu;
414 } 450 }
415 451
416 addPastePlain(menu, menuInfo) { 452 addPastePlain(menu, menuInfo) {
417 if ( 453 if (
418 menuInfo.editFlags.canPaste 454 menuInfo.editFlags.canPaste &&
419 && !menuInfo.linkText 455 !menuInfo.linkText &&
420 && !menuInfo.hasImageContents 456 !menuInfo.hasImageContents
421 ) { 457 ) {
422 const webContents = this.getWebContents(); 458 const webContents = this.getWebContents();
423 menu.append( 459 menu.append(
@@ -463,21 +499,21 @@ module.exports = class ContextMenuBuilder {
463 * @param {String} outputFormat The image format to use, defaults to 'image/png' 499 * @param {String} outputFormat The image format to use, defaults to 'image/png'
464 */ 500 */
465 convertImageToBase64(url, callback, outputFormat = 'image/png') { 501 convertImageToBase64(url, callback, outputFormat = 'image/png') {
466 let canvas = document.createElement('CANVAS'); 502 let canvas = document.createElement('canvas');
467 const ctx = canvas.getContext('2d'); 503 const ctx = canvas.getContext('2d');
468 // eslint-disable-next-line no-undef 504 // eslint-disable-next-line no-undef
469 const img = new Image(); 505 const img = new Image();
470 img.crossOrigin = 'Anonymous'; 506 img.crossOrigin = 'Anonymous';
471 507
472 img.onload = () => { 508 img.addEventListener('load', () => {
473 canvas.height = img.height; 509 canvas.height = img.height;
474 canvas.width = img.width; 510 canvas.width = img.width;
475 ctx.drawImage(img, 0, 0); 511 ctx?.drawImage(img, 0, 0);
476 512
477 const dataURL = canvas.toDataURL(outputFormat); 513 const dataURL = canvas.toDataURL(outputFormat);
478 canvas = null; 514 canvas = null;
479 callback(dataURL); 515 callback(dataURL);
480 }; 516 });
481 517
482 img.src = url; 518 img.src = url;
483 } 519 }
@@ -487,12 +523,14 @@ module.exports = class ContextMenuBuilder {
487 */ 523 */
488 goBack(menu) { 524 goBack(menu) {
489 const webContents = this.getWebContents(); 525 const webContents = this.getWebContents();
490 menu.append(new MenuItem({ 526 menu.append(
491 label: this.stringTable.goBack(), 527 new MenuItem({
492 accelerator: `${cmdOrCtrlShortcutKey()}+left`, 528 label: this.stringTable.goBack(),
493 enabled: webContents.canGoBack(), 529 accelerator: `${cmdOrCtrlShortcutKey()}+left`,
494 click: () => webContents.goBack(), 530 enabled: webContents.canGoBack(),
495 })); 531 click: () => webContents.goBack(),
532 }),
533 );
496 534
497 return menu; 535 return menu;
498 } 536 }
@@ -502,12 +540,14 @@ module.exports = class ContextMenuBuilder {
502 */ 540 */
503 goForward(menu) { 541 goForward(menu) {
504 const webContents = this.getWebContents(); 542 const webContents = this.getWebContents();
505 menu.append(new MenuItem({ 543 menu.append(
506 label: this.stringTable.goForward(), 544 new MenuItem({
507 accelerator: `${cmdOrCtrlShortcutKey()}+right`, 545 label: this.stringTable.goForward(),
508 enabled: webContents.canGoForward(), 546 accelerator: `${cmdOrCtrlShortcutKey()}+right`,
509 click: () => webContents.goForward(), 547 enabled: webContents.canGoForward(),
510 })); 548 click: () => webContents.goForward(),
549 }),
550 );
511 551
512 return menu; 552 return menu;
513 } 553 }
@@ -516,14 +556,19 @@ module.exports = class ContextMenuBuilder {
516 * Adds the 'copy page url' menu item. 556 * Adds the 'copy page url' menu item.
517 */ 557 */
518 copyPageUrl(menu, menuInfo) { 558 copyPageUrl(menu, menuInfo) {
519 menu.append(new MenuItem({ 559 menu.append(
520 label: this.stringTable.copyPageUrl(), 560 new MenuItem({
521 enabled: true, 561 label: this.stringTable.copyPageUrl(),
522 click: () => { 562 enabled: true,
523 clipboard.writeText(window.location.href); 563 click: () => {
524 this._sendNotificationOnClipboardEvent(menuInfo.clipboardNotifications, () => `Page URL copied: ${window.location.href}`); 564 clipboard.writeText(window.location.href);
525 }, 565 this._sendNotificationOnClipboardEvent(
526 })); 566 menuInfo.clipboardNotifications,
567 () => `Page URL copied: ${window.location.href}`,
568 );
569 },
570 }),
571 );
527 572
528 return menu; 573 return menu;
529 } 574 }
@@ -533,15 +578,17 @@ module.exports = class ContextMenuBuilder {
533 */ 578 */
534 goToHomePage(menu, menuInfo) { 579 goToHomePage(menu, menuInfo) {
535 const baseURL = new URL(menuInfo.pageURL); 580 const baseURL = new URL(menuInfo.pageURL);
536 menu.append(new MenuItem({ 581 menu.append(
537 label: this.stringTable.goToHomePage(), 582 new MenuItem({
538 accelerator: `${cmdOrCtrlShortcutKey()}+Home`, 583 label: this.stringTable.goToHomePage(),
539 enabled: true, 584 accelerator: `${cmdOrCtrlShortcutKey()}+Home`,
540 click: () => { 585 enabled: true,
541 // webContents.loadURL(baseURL.origin); 586 click: () => {
542 window.location.href = baseURL.origin; 587 // webContents.loadURL(baseURL.origin);
543 }, 588 window.location.href = baseURL.origin;
544 })); 589 },
590 }),
591 );
545 592
546 return menu; 593 return menu;
547 } 594 }
@@ -550,13 +597,15 @@ module.exports = class ContextMenuBuilder {
550 * Adds the 'open in browser' menu item. 597 * Adds the 'open in browser' menu item.
551 */ 598 */
552 openInBrowser(menu, menuInfo) { 599 openInBrowser(menu, menuInfo) {
553 menu.append(new MenuItem({ 600 menu.append(
554 label: this.stringTable.openInBrowser(), 601 new MenuItem({
555 enabled: true, 602 label: this.stringTable.openInBrowser(),
556 click: () => { 603 enabled: true,
557 openExternalUrl(menuInfo.pageURL, true); 604 click: () => {
558 }, 605 openExternalUrl(menuInfo.pageURL, true);
559 })); 606 },
607 }),
608 );
560 609
561 return menu; 610 return menu;
562 } 611 }
@@ -566,9 +615,8 @@ module.exports = class ContextMenuBuilder {
566 return; 615 return;
567 } 616 }
568 // eslint-disable-next-line no-new 617 // eslint-disable-next-line no-new
569 new window.Notification('Data copied into Clipboard', 618 new window.Notification('Data copied into Clipboard', {
570 { 619 body: notificationText(),
571 body: notificationText(), 620 });
572 });
573 } 621 }
574}; 622};
diff --git a/src/webview/darkmode.ts b/src/webview/darkmode.ts
index e06c22f11..7b9407049 100644
--- a/src/webview/darkmode.ts
+++ b/src/webview/darkmode.ts
@@ -1,5 +1,3 @@
1/* eslint no-bitwise: ["error", { "int32Hint": true }] */
2
3import { join } from 'path'; 1import { join } from 'path';
4import { pathExistsSync, readFileSync } from 'fs-extra'; 2import { pathExistsSync, readFileSync } from 'fs-extra';
5 3
@@ -7,7 +5,9 @@ const debug = require('debug')('Ferdi:DarkMode');
7 5
8const chars = [...'abcdefghijklmnopqrstuvwxyz']; 6const chars = [...'abcdefghijklmnopqrstuvwxyz'];
9 7
10const ID = [...Array(20)].map(() => chars[Math.random() * chars.length | 0]).join(''); 8const ID = [...Array.from({ length: 20 })]
9 .map(() => chars[Math.trunc(Math.random() * chars.length)])
10 .join('');
11 11
12export function injectDarkModeStyle(recipePath: string) { 12export function injectDarkModeStyle(recipePath: string) {
13 const darkModeStyle = join(recipePath, 'darkmode.css'); 13 const darkModeStyle = join(recipePath, 'darkmode.css');
diff --git a/src/webview/lib/RecipeWebview.js b/src/webview/lib/RecipeWebview.js
index 157da7693..4085b925b 100644
--- a/src/webview/lib/RecipeWebview.js
+++ b/src/webview/lib/RecipeWebview.js
@@ -1,5 +1,5 @@
1import { ipcRenderer } from 'electron'; 1import { ipcRenderer } from 'electron';
2import { exists, pathExistsSync, readFileSync } from 'fs-extra'; 2import { pathExistsSync, readFileSync, existsSync } from 'fs-extra';
3 3
4const debug = require('debug')('Ferdi:Plugin:RecipeWebview'); 4const debug = require('debug')('Ferdi:Plugin:RecipeWebview');
5 5
@@ -62,12 +62,13 @@ class RecipeWebview {
62 * be an absolute path to the file 62 * be an absolute path to the file
63 */ 63 */
64 injectCSS(...files) { 64 injectCSS(...files) {
65 files.forEach(async (file) => { 65 // eslint-disable-next-line unicorn/no-array-for-each
66 files.forEach(file => {
66 if (pathExistsSync(file)) { 67 if (pathExistsSync(file)) {
67 const styles = document.createElement('style'); 68 const styles = document.createElement('style');
68 styles.innerHTML = readFileSync(file, 'utf8'); 69 styles.innerHTML = readFileSync(file, 'utf8');
69 70
70 document.querySelector('head').appendChild(styles); 71 document.querySelector('head').append(styles);
71 72
72 debug('Append styles', styles); 73 debug('Append styles', styles);
73 } 74 }
@@ -75,14 +76,16 @@ class RecipeWebview {
75 } 76 }
76 77
77 injectJSUnsafe(...files) { 78 injectJSUnsafe(...files) {
78 Promise.all(files.map(async (file) => { 79 Promise.all(
79 if (await exists(file)) { 80 files.map(file => {
80 return readFileSync(file, 'utf8'); 81 if (existsSync(file)) {
81 } 82 return readFileSync(file, 'utf8');
82 debug('Script not found', file); 83 }
83 return null; 84 debug('Script not found', file);
84 })).then(async (scripts) => { 85 return null;
85 const scriptsFound = scripts.filter((script) => script !== null); 86 }),
87 ).then(scripts => {
88 const scriptsFound = scripts.filter(script => script !== null);
86 if (scriptsFound.length > 0) { 89 if (scriptsFound.length > 0) {
87 debug('Inject scripts to main world', scriptsFound); 90 debug('Inject scripts to main world', scriptsFound);
88 ipcRenderer.sendToHost('inject-js-unsafe', ...scriptsFound); 91 ipcRenderer.sendToHost('inject-js-unsafe', ...scriptsFound);
diff --git a/src/webview/lib/Userscript.js b/src/webview/lib/Userscript.js
index 2043d9fff..bed2b1ff8 100644
--- a/src/webview/lib/Userscript.js
+++ b/src/webview/lib/Userscript.js
@@ -28,7 +28,7 @@ export default class Userscript {
28 * 28 *
29 * @param {*} settings 29 * @param {*} settings
30 */ 30 */
31 // eslint-disable-next-line 31 // eslint-disable-next-line camelcase
32 internal_setSettings(settings) { 32 internal_setSettings(settings) {
33 // This is needed to get a clean JS object from the settings itself to provide better accessibility 33 // This is needed to get a clean JS object from the settings itself to provide better accessibility
34 // Otherwise this will be a mobX instance 34 // Otherwise this will be a mobX instance
@@ -95,9 +95,7 @@ export default class Userscript {
95 * @param {*} value 95 * @param {*} value
96 */ 96 */
97 set(key, value) { 97 set(key, value) {
98 window.localStorage.setItem( 98 window.localStorage.setItem(`ferdi-user-${key}`, JSON.stringify(value));
99 `ferdi-user-${key}`, JSON.stringify(value),
100 );
101 } 99 }
102 100
103 /** 101 /**
@@ -107,9 +105,7 @@ export default class Userscript {
107 * @return Value of the key 105 * @return Value of the key
108 */ 106 */
109 get(key) { 107 get(key) {
110 return JSON.parse(window.localStorage.getItem( 108 return JSON.parse(window.localStorage.getItem(`ferdi-user-${key}`));
111 `ferdi-user-${key}`,
112 ));
113 } 109 }
114 110
115 /** 111 /**
diff --git a/src/webview/recipe.js b/src/webview/recipe.js
index a3ae4513f..d7032da3f 100644
--- a/src/webview/recipe.js
+++ b/src/webview/recipe.js
@@ -1,3 +1,4 @@
1/* eslint-disable global-require */
1/* eslint-disable import/first */ 2/* eslint-disable import/first */
2import { contextBridge, desktopCapturer, ipcRenderer } from 'electron'; 3import { contextBridge, desktopCapturer, ipcRenderer } from 'electron';
3import { BrowserWindow, getCurrentWebContents } from '@electron/remote'; 4import { BrowserWindow, getCurrentWebContents } from '@electron/remote';
@@ -104,16 +105,13 @@ window.open = (url, frameName, features) => {
104// then overwrite the corresponding field of the window object by injected JS. 105// then overwrite the corresponding field of the window object by injected JS.
105contextBridge.exposeInMainWorld('ferdi', { 106contextBridge.exposeInMainWorld('ferdi', {
106 open: window.open, 107 open: window.open,
107 setBadge: (direct, indirect) => 108 setBadge: (direct, indirect) => badgeHandler.setBadge(direct, indirect),
108 badgeHandler.setBadge(direct, indirect), 109 safeParseInt: text => badgeHandler.safeParseInt(text),
109 safeParseInt: (text) =>
110 badgeHandler.safeParseInt(text),
111 displayNotification: (title, options) => 110 displayNotification: (title, options) =>
112 notificationsHandler.displayNotification(title, options), 111 notificationsHandler.displayNotification(title, options),
113 clearStorageData: (storageLocations) => 112 clearStorageData: storageLocations =>
114 sessionHandler.clearStorageData(storageLocations), 113 sessionHandler.clearStorageData(storageLocations),
115 releaseServiceWorkers: () => 114 releaseServiceWorkers: () => sessionHandler.releaseServiceWorkers(),
116 sessionHandler.releaseServiceWorkers(),
117 getDisplayMediaSelector, 115 getDisplayMediaSelector,
118 getCurrentWebContents, 116 getCurrentWebContents,
119 BrowserWindow, 117 BrowserWindow,
@@ -173,12 +171,12 @@ class RecipeController {
173 findInPage = null; 171 findInPage = null;
174 172
175 async initialize() { 173 async initialize() {
176 Object.keys(this.ipcEvents).forEach(channel => { 174 for (const channel of Object.keys(this.ipcEvents)) {
177 ipcRenderer.on(channel, (...args) => { 175 ipcRenderer.on(channel, (...args) => {
178 debug('Received IPC event for channel', channel, 'with', ...args); 176 debug('Received IPC event for channel', channel, 'with', ...args);
179 this[this.ipcEvents[channel]](...args); 177 this[this.ipcEvents[channel]](...args);
180 }); 178 });
181 }); 179 }
182 180
183 debug('Send "hello" to host'); 181 debug('Send "hello" to host');
184 setTimeout(() => ipcRenderer.sendToHost('hello'), 100); 182 setTimeout(() => ipcRenderer.sendToHost('hello'), 100);
@@ -210,7 +208,7 @@ class RecipeController {
210 delete require.cache[require.resolve(modulePath)]; 208 delete require.cache[require.resolve(modulePath)];
211 try { 209 try {
212 this.recipe = new RecipeWebview(badgeHandler, notificationsHandler); 210 this.recipe = new RecipeWebview(badgeHandler, notificationsHandler);
213 // eslint-disable-next-line 211 // eslint-disable-next-line import/no-dynamic-require
214 require(modulePath)(this.recipe, { ...config, recipe }); 212 require(modulePath)(this.recipe, { ...config, recipe });
215 debug('Initialize Recipe', config, recipe); 213 debug('Initialize Recipe', config, recipe);
216 214
@@ -218,8 +216,8 @@ class RecipeController {
218 216
219 // Make sure to update the WebView, otherwise the custom darkmode handler may not be used 217 // Make sure to update the WebView, otherwise the custom darkmode handler may not be used
220 this.update(); 218 this.update();
221 } catch (err) { 219 } catch (error) {
222 console.error('Recipe initialization failed', err); 220 console.error('Recipe initialization failed', error);
223 } 221 }
224 222
225 this.loadUserFiles(recipe, config); 223 this.loadUserFiles(recipe, config);
@@ -234,12 +232,12 @@ class RecipeController {
234 const data = readFileSync(userCss); 232 const data = readFileSync(userCss);
235 styles.innerHTML += data.toString(); 233 styles.innerHTML += data.toString();
236 } 234 }
237 document.querySelector('head').appendChild(styles); 235 document.querySelector('head').append(styles);
238 236
239 const userJs = join(recipe.path, 'user.js'); 237 const userJs = join(recipe.path, 'user.js');
240 if (pathExistsSync(userJs)) { 238 if (pathExistsSync(userJs)) {
241 const loadUserJs = () => { 239 const loadUserJs = () => {
242 // eslint-disable-next-line 240 // eslint-disable-next-line import/no-dynamic-require
243 const userJsModule = require(userJs); 241 const userJsModule = require(userJs);
244 242
245 if (typeof userJsModule === 'function') { 243 if (typeof userJsModule === 'function') {
@@ -392,15 +390,14 @@ class RecipeController {
392 } 390 }
393 391
394 // Remove dark reader if (universal) dark mode was just disabled 392 // Remove dark reader if (universal) dark mode was just disabled
395 if (this.universalDarkModeInjected) { 393 if (
396 if ( 394 this.universalDarkModeInjected &&
397 !this.settings.app.darkMode || 395 (!this.settings.app.darkMode ||
398 !this.settings.service.isDarkModeEnabled || 396 !this.settings.service.isDarkModeEnabled ||
399 !this.settings.app.universalDarkMode 397 !this.settings.app.universalDarkMode)
400 ) { 398 ) {
401 disableDarkMode(); 399 disableDarkMode();
402 this.universalDarkModeInjected = false; 400 this.universalDarkModeInjected = false;
403 }
404 } 401 }
405 } 402 }
406 403
diff --git a/src/webview/sessionHandler.ts b/src/webview/sessionHandler.ts
index 6a7e62ac5..4961da12b 100644
--- a/src/webview/sessionHandler.ts
+++ b/src/webview/sessionHandler.ts
@@ -9,20 +9,21 @@ export class SessionHandler {
9 const { session } = getCurrentWebContents(); 9 const { session } = getCurrentWebContents();
10 session.flushStorageData(); 10 session.flushStorageData();
11 session.clearStorageData({ storages: storageLocations }); 11 session.clearStorageData({ storages: storageLocations });
12 } catch (err) { 12 } catch (error) {
13 debug(err); 13 debug(error);
14 } 14 }
15 } 15 }
16 16
17 async releaseServiceWorkers() { 17 async releaseServiceWorkers() {
18 try { 18 try {
19 const registrations = await window.navigator.serviceWorker.getRegistrations(); 19 const registrations =
20 registrations.forEach(r => { 20 await window.navigator.serviceWorker.getRegistrations();
21 r.unregister(); 21 for (const registration of registrations) {
22 registration.unregister();
22 debug('ServiceWorker unregistered'); 23 debug('ServiceWorker unregistered');
23 }); 24 }
24 } catch (err) { 25 } catch (error) {
25 debug(err); 26 debug(error);
26 } 27 }
27 } 28 }
28} 29}
diff --git a/src/webview/spellchecker.ts b/src/webview/spellchecker.ts
index 30b4ef075..d0f6663d5 100644
--- a/src/webview/spellchecker.ts
+++ b/src/webview/spellchecker.ts
@@ -11,7 +11,7 @@ debug('Spellchecker default locale is', defaultLocale);
11export function getSpellcheckerLocaleByFuzzyIdentifier(identifier: string) { 11export function getSpellcheckerLocaleByFuzzyIdentifier(identifier: string) {
12 const locales = Object.keys(SPELLCHECKER_LOCALES).filter((key) => key.toLocaleLowerCase() === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase()); 12 const locales = Object.keys(SPELLCHECKER_LOCALES).filter((key) => key.toLocaleLowerCase() === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase());
13 13
14 return locales.length >= 1 ? locales[0] : null; 14 return locales.length > 0 ? locales[0] : null;
15} 15}
16 16
17export function switchDict(locale: string) { 17export function switchDict(locale: string) {