aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/containers/auth/SetupAssistantScreen.tsx26
-rw-r--r--src/features/todos/store.js4
-rw-r--r--src/features/workspaces/models/Workspace.ts2
-rw-r--r--src/index.ts4
-rw-r--r--src/jsUtils.test.ts41
-rw-r--r--src/jsUtils.ts23
-rw-r--r--src/models/Recipe.ts56
-rw-r--r--src/models/Service.ts (renamed from src/models/Service.js)188
-rw-r--r--src/models/UserAgent.ts (renamed from src/models/UserAgent.js)41
-rw-r--r--src/stores.types.ts31
-rw-r--r--src/stores/ServicesStore.ts (renamed from src/stores/ServicesStore.js)101
-rw-r--r--src/webview/recipe.js17
12 files changed, 330 insertions, 204 deletions
diff --git a/src/containers/auth/SetupAssistantScreen.tsx b/src/containers/auth/SetupAssistantScreen.tsx
index 44bd32772..8f1871776 100644
--- a/src/containers/auth/SetupAssistantScreen.tsx
+++ b/src/containers/auth/SetupAssistantScreen.tsx
@@ -11,22 +11,22 @@ import UserStore from '../../stores/UserStore';
11 11
12interface IProps { 12interface IProps {
13 stores: { 13 stores: {
14 services?: ServicesStore, 14 services: ServicesStore;
15 router: RouterStore, 15 router: RouterStore;
16 recipes?: RecipesStore, 16 recipes?: RecipesStore;
17 user?: UserStore, 17 user?: UserStore;
18 }, 18 };
19 actions: { 19 actions: {
20 user: UserStore, 20 user: UserStore;
21 service: ServicesStore, 21 service: ServicesStore;
22 recipe: RecipesStore, 22 recipe: RecipesStore;
23 }, 23 };
24}; 24};
25 25
26class SetupAssistantScreen extends Component<IProps> { 26class SetupAssistantScreen extends Component<IProps> {
27 state = { 27 state = {
28 isSettingUpServices: false, 28 isSettingUpServices: false,
29 } 29 };
30 30
31 // TODO: Why are these hardcoded here? Do they need to conform to specific services in the packaged recipes? If so, its more important to fix this 31 // TODO: Why are these hardcoded here? Do they need to conform to specific services in the packaged recipes? If so, its more important to fix this
32 services = { 32 services = {
@@ -69,7 +69,9 @@ class SetupAssistantScreen extends Component<IProps> {
69 }; 69 };
70 70
71 async setupServices(serviceConfig) { 71 async setupServices(serviceConfig) {
72 const { stores: { services, router } } = this.props; 72 const {
73 stores: { services, router },
74 } = this.props;
73 75
74 this.setState({ 76 this.setState({
75 isSettingUpServices: true, 77 isSettingUpServices: true,
@@ -79,7 +81,7 @@ class SetupAssistantScreen extends Component<IProps> {
79 for (const config of serviceConfig) { 81 for (const config of serviceConfig) {
80 const serviceData = { 82 const serviceData = {
81 name: this.services[config.id].name, 83 name: this.services[config.id].name,
82 team: config.team 84 team: config.team,
83 }; 85 };
84 86
85 await services._createService({ 87 await services._createService({
diff --git a/src/features/todos/store.js b/src/features/todos/store.js
index 9ece76327..8c3917cc3 100644
--- a/src/features/todos/store.js
+++ b/src/features/todos/store.js
@@ -18,7 +18,9 @@ import { createActionBindings } from '../utils/ActionBinding';
18import { IPC, TODOS_ROUTES } from './constants'; 18import { IPC, TODOS_ROUTES } from './constants';
19import UserAgent from '../../models/UserAgent'; 19import UserAgent from '../../models/UserAgent';
20 20
21const debug = require('../../preload-safe-debug')('Ferdium:feature:todos:store'); 21const debug = require('../../preload-safe-debug')(
22 'Ferdium:feature:todos:store',
23);
22 24
23export default class TodoStore extends FeatureStore { 25export default class TodoStore extends FeatureStore {
24 @observable stores = null; 26 @observable stores = null;
diff --git a/src/features/workspaces/models/Workspace.ts b/src/features/workspaces/models/Workspace.ts
index cd3918fba..bc636011d 100644
--- a/src/features/workspaces/models/Workspace.ts
+++ b/src/features/workspaces/models/Workspace.ts
@@ -9,7 +9,7 @@ export default class Workspace {
9 9
10 @observable order = null; 10 @observable order = null;
11 11
12 @observable services = []; 12 @observable services: string[] = [];
13 13
14 @observable userId = null; 14 @observable userId = null;
15 15
diff --git a/src/index.ts b/src/index.ts
index fa957bf10..0fccac6e9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -30,7 +30,7 @@ import {
30 userDataRecipesPath, 30 userDataRecipesPath,
31 userDataPath, 31 userDataPath,
32} from './environment-remote'; 32} from './environment-remote';
33import { ifUndefinedBoolean } from './jsUtils'; 33import { ifUndefined } from './jsUtils';
34 34
35import { mainIpcHandler as basicAuthHandler } from './features/basicAuth'; 35import { mainIpcHandler as basicAuthHandler } from './features/basicAuth';
36import ipcApi from './electron/ipc-api'; 36import ipcApi from './electron/ipc-api';
@@ -91,7 +91,7 @@ const settings = new Settings('app', DEFAULT_APP_SETTINGS);
91const proxySettings = new Settings('proxy'); 91const proxySettings = new Settings('proxy');
92 92
93const retrieveSettingValue = (key: string, defaultValue: boolean) => 93const retrieveSettingValue = (key: string, defaultValue: boolean) =>
94 ifUndefinedBoolean(settings.get(key), defaultValue); 94 ifUndefined<boolean>(settings.get(key), defaultValue);
95 95
96const liftSingleInstanceLock = retrieveSettingValue( 96const liftSingleInstanceLock = retrieveSettingValue(
97 'liftSingleInstanceLock', 97 'liftSingleInstanceLock',
diff --git a/src/jsUtils.test.ts b/src/jsUtils.test.ts
index 651caee5f..34cd8f098 100644
--- a/src/jsUtils.test.ts
+++ b/src/jsUtils.test.ts
@@ -18,36 +18,53 @@ describe('jsUtils', () => {
18 }); 18 });
19 }); 19 });
20 20
21 describe('ifUndefinedBoolean', () => { 21 describe('ifUndefined<string>', () => {
22 it('returns the default value for undefined input', () => { 22 it('returns the default value for undefined input', () => {
23 const result = jsUtils.ifUndefinedBoolean(undefined, false); 23 const result = jsUtils.ifUndefined<string>(undefined, 'abc');
24 expect(result).toEqual('abc');
25 });
26
27 it('returns the default value for null input', () => {
28 const result = jsUtils.ifUndefined<string>(null, 'abc');
29 expect(result).toEqual('abc');
30 });
31
32 it('returns the non-default input value for regular string input', () => {
33 const result = jsUtils.ifUndefined<string>('some random string', 'abc');
34 expect(result).toEqual('some random string');
35 });
36 });
37
38 describe('ifUndefined<boolean>', () => {
39 it('returns the default value for undefined input', () => {
40 const result = jsUtils.ifUndefined<boolean>(undefined, false);
24 expect(result).toEqual(false); 41 expect(result).toEqual(false);
25 }); 42 });
26 43
27 it('returns the default value for null input', () => { 44 it('returns the default value for null input', () => {
28 const result = jsUtils.ifUndefinedBoolean(null, true); 45 const result = jsUtils.ifUndefined<boolean>(null, true);
29 expect(result).toEqual(true); 46 expect(result).toEqual(true);
30 }); 47 });
31 48
32 it('returns the non-default input value for regular boolean input', () => { 49 it('returns the non-default input value for regular boolean input', () => {
33 const result = jsUtils.ifUndefinedBoolean(true, false); 50 const result = jsUtils.ifUndefined<boolean>(true, false);
34 expect(result).toEqual(true); 51 expect(result).toEqual(true);
35 }); 52 });
36 }); 53 });
37 54
38 describe('ifUndefinedNumber', () => { 55 describe('ifUndefined<number>', () => {
39 it('returns the default value for undefined input', () => { 56 it('returns the default value for undefined input', () => {
40 const result = jsUtils.ifUndefinedNumber(undefined, 123); 57 const result = jsUtils.ifUndefined<number>(undefined, 123);
41 expect(result).toEqual(123); 58 expect(result).toEqual(123);
42 }); 59 });
43 60
44 it('returns the default value for null input', () => { 61 it('returns the default value for null input', () => {
45 const result = jsUtils.ifUndefinedNumber(null, 234); 62 const result = jsUtils.ifUndefined<number>(null, 234);
46 expect(result).toEqual(234); 63 expect(result).toEqual(234);
47 }); 64 });
48 65
49 it('returns the non-default input value for regular Number input', () => { 66 it('returns the non-default input value for regular Number input', () => {
50 const result = jsUtils.ifUndefinedNumber(1234, 5678); 67 const result = jsUtils.ifUndefined<number>(1234, 5678);
51 expect(result).toEqual(1234); 68 expect(result).toEqual(1234);
52 }); 69 });
53 }); 70 });
@@ -70,10 +87,10 @@ describe('jsUtils', () => {
70 87
71 it('returns the parsed JSON for the string input', () => { 88 it('returns the parsed JSON for the string input', () => {
72 const result1 = jsUtils.convertToJSON('{"a":"b","c":"d"}'); 89 const result1 = jsUtils.convertToJSON('{"a":"b","c":"d"}');
73 expect(result1).toEqual({a: 'b', c: 'd'}); 90 expect(result1).toEqual({ a: 'b', c: 'd' });
74 91
75 const result2 = jsUtils.convertToJSON('[{"a":"b"},{"c":"d"}]'); 92 const result2 = jsUtils.convertToJSON('[{"a":"b"},{"c":"d"}]');
76 expect(result2).toEqual([{a: 'b'}, {c: 'd'}]); 93 expect(result2).toEqual([{ a: 'b' }, { c: 'd' }]);
77 }); 94 });
78 }); 95 });
79 96
@@ -89,8 +106,8 @@ describe('jsUtils', () => {
89 }); 106 });
90 107
91 it('returns cloned object for valid input', () => { 108 it('returns cloned object for valid input', () => {
92 const result = jsUtils.cleanseJSObject([{a: 'b'}, {c: 'd'}]); 109 const result = jsUtils.cleanseJSObject([{ a: 'b' }, { c: 'd' }]);
93 expect(result).toEqual([{a: 'b'}, {c: 'd'}]); 110 expect(result).toEqual([{ a: 'b' }, { c: 'd' }]);
94 }); 111 });
95 }); 112 });
96}); 113});
diff --git a/src/jsUtils.ts b/src/jsUtils.ts
index 250d595eb..f5b39a000 100644
--- a/src/jsUtils.ts
+++ b/src/jsUtils.ts
@@ -1,9 +1,22 @@
1export const ifUndefinedString = (source: string | undefined | null, defaultValue: string): string => (source !== undefined && source !== null ? source : defaultValue); 1// TODO: ifUndefinedString can be removed after ./src/webview/recipe.js is converted to typescript.
2export const ifUndefinedString = (
3 source: string | undefined | null,
4 defaultValue: string,
5): string => (source !== undefined && source !== null ? source : defaultValue);
2 6
3export const ifUndefinedBoolean = (source: boolean | undefined | null, defaultValue: boolean): boolean => Boolean(source !== undefined && source !== null ? source : defaultValue); 7export const ifUndefined = <T>(
8 source: undefined | null | T,
9 defaultValue: T,
10): T => {
11 if (source !== undefined && source !== null) {
12 return source;
13 }
4 14
5export const ifUndefinedNumber = (source: number | undefined | null, defaultValue: number): number => Number(source !== undefined && source !== null ? source : defaultValue); 15 return defaultValue;
16};
6 17
7export const convertToJSON = (data: string | any | undefined | null) => data && typeof data === 'string' && data.length > 0 ? JSON.parse(data) : data 18export const convertToJSON = (data: string | any | undefined | null) =>
19 data && typeof data === 'string' && data.length > 0 ? JSON.parse(data) : data;
8 20
9export const cleanseJSObject = (data: any | undefined | null) => JSON.parse(JSON.stringify(data)) 21export const cleanseJSObject = (data: any | undefined | null) =>
22 JSON.parse(JSON.stringify(data));
diff --git a/src/models/Recipe.ts b/src/models/Recipe.ts
index be889d22c..eb8b0b1ff 100644
--- a/src/models/Recipe.ts
+++ b/src/models/Recipe.ts
@@ -2,7 +2,7 @@ import semver from 'semver';
2import { pathExistsSync } from 'fs-extra'; 2import { pathExistsSync } from 'fs-extra';
3import { join } from 'path'; 3import { join } from 'path';
4import { DEFAULT_SERVICE_SETTINGS } from '../config'; 4import { DEFAULT_SERVICE_SETTINGS } from '../config';
5import { ifUndefinedString, ifUndefinedBoolean } from '../jsUtils'; 5import { ifUndefined } from '../jsUtils';
6 6
7interface IRecipe { 7interface IRecipe {
8 id: string; 8 id: string;
@@ -34,7 +34,7 @@ export default class Recipe {
34 34
35 name: string = ''; 35 name: string = '';
36 36
37 description = ''; 37 description: string = '';
38 38
39 version: string = ''; 39 version: string = '';
40 40
@@ -75,6 +75,19 @@ export default class Recipe {
75 // TODO: Is this being used? 75 // TODO: Is this being used?
76 local: boolean = false; 76 local: boolean = false;
77 77
78 // TODO Add types for this once we know if they are neccesary to pass
79 // on to the initialize-recipe ipc event.
80 overrideUserAgent: any;
81
82 buildUrl: any;
83
84 modifyRequestHeaders: any;
85
86 knownCertificateHosts: any;
87
88 events: any;
89 // End todo.
90
78 // TODO: Need to reconcile which of these are optional/mandatory 91 // TODO: Need to reconcile which of these are optional/mandatory
79 constructor(data: IRecipe) { 92 constructor(data: IRecipe) {
80 if (!data) { 93 if (!data) {
@@ -93,61 +106,64 @@ export default class Recipe {
93 } 106 }
94 107
95 // from the recipe 108 // from the recipe
96 this.id = ifUndefinedString(data.id, this.id); 109 this.id = ifUndefined<string>(data.id, this.id);
97 this.name = ifUndefinedString(data.name, this.name); 110 this.name = ifUndefined<string>(data.name, this.name);
98 this.version = ifUndefinedString(data.version, this.version); 111 this.version = ifUndefined<string>(data.version, this.version);
99 this.aliases = data.aliases || this.aliases; 112 this.aliases = data.aliases || this.aliases;
100 this.serviceURL = ifUndefinedString( 113 this.serviceURL = ifUndefined<string>(
101 data.config.serviceURL, 114 data.config.serviceURL,
102 this.serviceURL, 115 this.serviceURL,
103 ); 116 );
104 this.hasDirectMessages = ifUndefinedBoolean( 117 this.hasDirectMessages = ifUndefined<boolean>(
105 data.config.hasDirectMessages, 118 data.config.hasDirectMessages,
106 this.hasDirectMessages, 119 this.hasDirectMessages,
107 ); 120 );
108 this.hasIndirectMessages = ifUndefinedBoolean( 121 this.hasIndirectMessages = ifUndefined<boolean>(
109 data.config.hasIndirectMessages, 122 data.config.hasIndirectMessages,
110 this.hasIndirectMessages, 123 this.hasIndirectMessages,
111 ); 124 );
112 this.hasNotificationSound = ifUndefinedBoolean( 125 this.hasNotificationSound = ifUndefined<boolean>(
113 data.config.hasNotificationSound, 126 data.config.hasNotificationSound,
114 this.hasNotificationSound, 127 this.hasNotificationSound,
115 ); 128 );
116 this.hasTeamId = ifUndefinedBoolean(data.config.hasTeamId, this.hasTeamId); 129 this.hasTeamId = ifUndefined<boolean>(
117 this.hasCustomUrl = ifUndefinedBoolean( 130 data.config.hasTeamId,
131 this.hasTeamId,
132 );
133 this.hasCustomUrl = ifUndefined<boolean>(
118 data.config.hasCustomUrl, 134 data.config.hasCustomUrl,
119 this.hasCustomUrl, 135 this.hasCustomUrl,
120 ); 136 );
121 this.hasHostedOption = ifUndefinedBoolean( 137 this.hasHostedOption = ifUndefined<boolean>(
122 data.config.hasHostedOption, 138 data.config.hasHostedOption,
123 this.hasHostedOption, 139 this.hasHostedOption,
124 ); 140 );
125 this.urlInputPrefix = ifUndefinedString( 141 this.urlInputPrefix = ifUndefined<string>(
126 data.config.urlInputPrefix, 142 data.config.urlInputPrefix,
127 this.urlInputPrefix, 143 this.urlInputPrefix,
128 ); 144 );
129 this.urlInputSuffix = ifUndefinedString( 145 this.urlInputSuffix = ifUndefined<string>(
130 data.config.urlInputSuffix, 146 data.config.urlInputSuffix,
131 this.urlInputSuffix, 147 this.urlInputSuffix,
132 ); 148 );
133 this.disablewebsecurity = ifUndefinedBoolean( 149 this.disablewebsecurity = ifUndefined<boolean>(
134 data.config.disablewebsecurity, 150 data.config.disablewebsecurity,
135 this.disablewebsecurity, 151 this.disablewebsecurity,
136 ); 152 );
137 this.autoHibernate = ifUndefinedBoolean( 153 this.autoHibernate = ifUndefined<boolean>(
138 data.config.autoHibernate, 154 data.config.autoHibernate,
139 this.autoHibernate, 155 this.autoHibernate,
140 ); 156 );
141 this.local = ifUndefinedBoolean(data.config.local, this.local); 157 this.local = ifUndefined<boolean>(data.config.local, this.local);
142 this.message = ifUndefinedString(data.config.message, this.message); 158 this.message = ifUndefined<string>(data.config.message, this.message);
143 this.allowFavoritesDelineationInUnreadCount = ifUndefinedBoolean( 159 this.allowFavoritesDelineationInUnreadCount = ifUndefined<boolean>(
144 data.config.allowFavoritesDelineationInUnreadCount, 160 data.config.allowFavoritesDelineationInUnreadCount,
145 this.allowFavoritesDelineationInUnreadCount, 161 this.allowFavoritesDelineationInUnreadCount,
146 ); 162 );
147 163
148 // computed 164 // computed
149 this.path = data.path; 165 this.path = data.path;
150 this.partition = ifUndefinedString(data.config.partition, this.partition); 166 this.partition = ifUndefined<string>(data.config.partition, this.partition);
151 } 167 }
152 168
153 // TODO: Need to remove this if its not used anywhere 169 // TODO: Need to remove this if its not used anywhere
diff --git a/src/models/Service.js b/src/models/Service.ts
index 53285e440..c4165e59a 100644
--- a/src/models/Service.js
+++ b/src/models/Service.ts
@@ -3,114 +3,121 @@ import { ipcRenderer } from 'electron';
3import { webContents } from '@electron/remote'; 3import { webContents } from '@electron/remote';
4import normalizeUrl from 'normalize-url'; 4import normalizeUrl from 'normalize-url';
5import { join } from 'path'; 5import { join } from 'path';
6import ElectronWebView from 'react-electron-web-view';
6 7
7import { todosStore } from '../features/todos'; 8import { todosStore } from '../features/todos';
8import { isValidExternalURL } from '../helpers/url-helpers'; 9import { isValidExternalURL } from '../helpers/url-helpers';
9import UserAgent from './UserAgent'; 10import UserAgent from './UserAgent';
10import { DEFAULT_SERVICE_ORDER } from '../config'; 11import { DEFAULT_SERVICE_ORDER } from '../config';
11import { 12import { ifUndefined } from '../jsUtils';
12 ifUndefinedString, 13import Recipe from './Recipe';
13 ifUndefinedBoolean,
14 ifUndefinedNumber,
15} from '../jsUtils';
16 14
17const debug = require('../preload-safe-debug')('Ferdium:Service'); 15const debug = require('../preload-safe-debug')('Ferdium:Service');
18 16
19// TODO: Shouldn't most of these values default to what's defined in DEFAULT_SERVICE_SETTINGS? 17// TODO: Shouldn't most of these values default to what's defined in DEFAULT_SERVICE_SETTINGS?
20export default class Service { 18export default class Service {
21 id = ''; 19 id: string = '';
22 20
23 recipe = null; 21 recipe: Recipe;
24 22
25 _webview = null; 23 _webview: ElectronWebView | null = null;
26 24
27 timer = null; 25 timer: NodeJS.Timeout | null = null;
28 26
29 events = {}; 27 events = {};
30 28
31 @observable isAttached = false; 29 @observable isAttached: boolean = false;
32 30
33 @observable isActive = false; // Is current webview active 31 @observable isActive: boolean = false; // Is current webview active
34 32
35 @observable name = ''; 33 @observable name: string = '';
36 34
37 @observable unreadDirectMessageCount = 0; 35 @observable unreadDirectMessageCount: number = 0;
38 36
39 @observable unreadIndirectMessageCount = 0; 37 @observable unreadIndirectMessageCount: number = 0;
40 38
41 @observable dialogTitle = ''; 39 @observable dialogTitle: string = '';
42 40
43 @observable order = DEFAULT_SERVICE_ORDER; 41 @observable order: number = DEFAULT_SERVICE_ORDER;
44 42
45 @observable isEnabled = true; 43 @observable isEnabled: boolean = true;
46 44
47 @observable isMuted = false; 45 @observable isMuted: boolean = false;
48 46
49 @observable team = ''; 47 @observable team: string = '';
50 48
51 @observable customUrl = ''; 49 @observable customUrl: string = '';
52 50
53 @observable isNotificationEnabled = true; 51 @observable isNotificationEnabled: boolean = true;
54 52
55 @observable isBadgeEnabled = true; 53 @observable isBadgeEnabled: boolean = true;
56 54
57 @observable trapLinkClicks = false; 55 @observable trapLinkClicks: boolean = false;
58 56
59 @observable isIndirectMessageBadgeEnabled = true; 57 @observable isIndirectMessageBadgeEnabled: boolean = true;
60 58
61 @observable iconUrl = ''; 59 @observable iconUrl: string = '';
62 60
63 @observable hasCustomUploadedIcon = false; 61 @observable customIconUrl: string = '';
64 62
65 @observable hasCrashed = false; 63 @observable hasCustomUploadedIcon: boolean = false;
66 64
67 @observable isDarkModeEnabled = false; 65 @observable hasCrashed: boolean = false;
68 66
69 @observable isProgressbarEnabled = true; 67 @observable isDarkModeEnabled: boolean = false;
70 68
71 @observable darkReaderSettings = { brightness: 100, contrast: 90, sepia: 10 }; 69 @observable isProgressbarEnabled: boolean = true;
72 70
73 @observable spellcheckerLanguage = null; 71 @observable darkReaderSettings: object = {
72 brightness: 100,
73 contrast: 90,
74 sepia: 10,
75 };
74 76
75 @observable isFirstLoad = true; 77 @observable spellcheckerLanguage: string | null = null;
76 78
77 @observable isLoading = true; 79 @observable isFirstLoad: boolean = true;
78 80
79 @observable isLoadingPage = true; 81 @observable isLoading: boolean = true;
80 82
81 @observable isError = false; 83 @observable isLoadingPage: boolean = true;
82 84
83 @observable errorMessage = ''; 85 @observable isError: boolean = false;
84 86
85 @observable isUsingCustomUrl = false; 87 @observable errorMessage: string = '';
86 88
87 @observable isServiceAccessRestricted = false; 89 @observable isUsingCustomUrl: boolean = false;
88 90
91 @observable isServiceAccessRestricted: boolean = false;
92
93 // todo is this used?
89 @observable restrictionType = null; 94 @observable restrictionType = null;
90 95
91 @observable isHibernationEnabled = false; 96 @observable isHibernationEnabled: boolean = false;
97
98 @observable isWakeUpEnabled: boolean = true;
92 99
93 @observable isWakeUpEnabled = true; 100 @observable isHibernationRequested: boolean = false;
94 101
95 @observable isHibernationRequested = false; 102 @observable onlyShowFavoritesInUnreadCount: boolean = false;
96 103
97 @observable onlyShowFavoritesInUnreadCount = false; 104 @observable lastUsed: number = Date.now(); // timestamp
98 105
99 @observable lastUsed = Date.now(); // timestamp 106 @observable lastHibernated: number | null = null; // timestamp
100 107
101 @observable lastHibernated = null; // timestamp 108 @observable lastPoll: number = Date.now();
102 109
103 @observable lastPoll = Date.now(); 110 @observable lastPollAnswer: number = Date.now();
104 111
105 @observable lastPollAnswer = Date.now(); 112 @observable lostRecipeConnection: boolean = false;
106 113
107 @observable lostRecipeConnection = false; 114 @observable lostRecipeReloadAttempt: number = 0;
108 115
109 @observable lostRecipeReloadAttempt = 0; 116 @observable userAgentModel: UserAgent;
110 117
111 @observable userAgentModel = null; 118 @observable proxy: string | null = null;
112 119
113 constructor(data, recipe) { 120 constructor(data, recipe: Recipe) {
114 if (!data) { 121 if (!data) {
115 throw new Error('Service config not valid'); 122 throw new Error('Service config not valid');
116 } 123 }
@@ -123,64 +130,64 @@ export default class Service {
123 130
124 this.userAgentModel = new UserAgent(recipe.overrideUserAgent); 131 this.userAgentModel = new UserAgent(recipe.overrideUserAgent);
125 132
126 this.id = ifUndefinedString(data.id, this.id); 133 this.id = ifUndefined<string>(data.id, this.id);
127 this.name = ifUndefinedString(data.name, this.name); 134 this.name = ifUndefined<string>(data.name, this.name);
128 this.team = ifUndefinedString(data.team, this.team); 135 this.team = ifUndefined<string>(data.team, this.team);
129 this.customUrl = ifUndefinedString(data.customUrl, this.customUrl); 136 this.customUrl = ifUndefined<string>(data.customUrl, this.customUrl);
130 this.iconUrl = ifUndefinedString(data.iconUrl, this.iconUrl); 137 this.iconUrl = ifUndefined<string>(data.iconUrl, this.iconUrl);
131 this.order = ifUndefinedNumber(data.order, this.order); 138 this.order = ifUndefined<number>(data.order, this.order);
132 this.isEnabled = ifUndefinedBoolean(data.isEnabled, this.isEnabled); 139 this.isEnabled = ifUndefined<boolean>(data.isEnabled, this.isEnabled);
133 this.isNotificationEnabled = ifUndefinedBoolean( 140 this.isNotificationEnabled = ifUndefined<boolean>(
134 data.isNotificationEnabled, 141 data.isNotificationEnabled,
135 this.isNotificationEnabled, 142 this.isNotificationEnabled,
136 ); 143 );
137 this.isBadgeEnabled = ifUndefinedBoolean( 144 this.isBadgeEnabled = ifUndefined<boolean>(
138 data.isBadgeEnabled, 145 data.isBadgeEnabled,
139 this.isBadgeEnabled, 146 this.isBadgeEnabled,
140 ); 147 );
141 this.trapLinkClicks = ifUndefinedBoolean( 148 this.trapLinkClicks = ifUndefined<boolean>(
142 data.trapLinkClicks, 149 data.trapLinkClicks,
143 this.trapLinkClicks, 150 this.trapLinkClicks,
144 ); 151 );
145 this.isIndirectMessageBadgeEnabled = ifUndefinedBoolean( 152 this.isIndirectMessageBadgeEnabled = ifUndefined<boolean>(
146 data.isIndirectMessageBadgeEnabled, 153 data.isIndirectMessageBadgeEnabled,
147 this.isIndirectMessageBadgeEnabled, 154 this.isIndirectMessageBadgeEnabled,
148 ); 155 );
149 this.isMuted = ifUndefinedBoolean(data.isMuted, this.isMuted); 156 this.isMuted = ifUndefined<boolean>(data.isMuted, this.isMuted);
150 this.isDarkModeEnabled = ifUndefinedBoolean( 157 this.isDarkModeEnabled = ifUndefined<boolean>(
151 data.isDarkModeEnabled, 158 data.isDarkModeEnabled,
152 this.isDarkModeEnabled, 159 this.isDarkModeEnabled,
153 ); 160 );
154 this.darkReaderSettings = ifUndefinedString( 161 this.darkReaderSettings = ifUndefined<object>(
155 data.darkReaderSettings, 162 data.darkReaderSettings,
156 this.darkReaderSettings, 163 this.darkReaderSettings,
157 ); 164 );
158 this.isProgressbarEnabled = ifUndefinedBoolean( 165 this.isProgressbarEnabled = ifUndefined<boolean>(
159 data.isProgressbarEnabled, 166 data.isProgressbarEnabled,
160 this.isProgressbarEnabled, 167 this.isProgressbarEnabled,
161 ); 168 );
162 this.hasCustomUploadedIcon = ifUndefinedBoolean( 169 this.hasCustomUploadedIcon = ifUndefined<boolean>(
163 data.iconId?.length > 0, 170 data.iconId?.length > 0,
164 this.hasCustomUploadedIcon, 171 this.hasCustomUploadedIcon,
165 ); 172 );
166 this.onlyShowFavoritesInUnreadCount = ifUndefinedBoolean( 173 this.onlyShowFavoritesInUnreadCount = ifUndefined<boolean>(
167 data.onlyShowFavoritesInUnreadCount, 174 data.onlyShowFavoritesInUnreadCount,
168 this.onlyShowFavoritesInUnreadCount, 175 this.onlyShowFavoritesInUnreadCount,
169 ); 176 );
170 this.proxy = ifUndefinedString(data.proxy, this.proxy); 177 this.proxy = ifUndefined<string | null>(data.proxy, this.proxy);
171 this.spellcheckerLanguage = ifUndefinedString( 178 this.spellcheckerLanguage = ifUndefined<string | null>(
172 data.spellcheckerLanguage, 179 data.spellcheckerLanguage,
173 this.spellcheckerLanguage, 180 this.spellcheckerLanguage,
174 ); 181 );
175 this.userAgentPref = ifUndefinedString( 182 this.userAgentPref = ifUndefined<string | null>(
176 data.userAgentPref, 183 data.userAgentPref,
177 this.userAgentPref, 184 this.userAgentPref,
178 ); 185 );
179 this.isHibernationEnabled = ifUndefinedBoolean( 186 this.isHibernationEnabled = ifUndefined<boolean>(
180 data.isHibernationEnabled, 187 data.isHibernationEnabled,
181 this.isHibernationEnabled, 188 this.isHibernationEnabled,
182 ); 189 );
183 this.isWakeUpEnabled = ifUndefinedBoolean( 190 this.isWakeUpEnabled = ifUndefined<boolean>(
184 data.isWakeUpEnabled, 191 data.isWakeUpEnabled,
185 this.isWakeUpEnabled, 192 this.isWakeUpEnabled,
186 ); 193 );
@@ -195,7 +202,7 @@ export default class Service {
195 this.isHibernationRequested = true; 202 this.isHibernationRequested = true;
196 } 203 }
197 204
198 autorun(() => { 205 autorun((): void => {
199 if (!this.isEnabled) { 206 if (!this.isEnabled) {
200 this.webview = null; 207 this.webview = null;
201 this.isAttached = false; 208 this.isAttached = false;
@@ -209,7 +216,7 @@ export default class Service {
209 }); 216 });
210 } 217 }
211 218
212 @computed get shareWithWebview() { 219 @computed get shareWithWebview(): object {
213 return { 220 return {
214 id: this.id, 221 id: this.id,
215 spellcheckerLanguage: this.spellcheckerLanguage, 222 spellcheckerLanguage: this.spellcheckerLanguage,
@@ -224,19 +231,19 @@ export default class Service {
224 }; 231 };
225 } 232 }
226 233
227 @computed get isTodosService() { 234 @computed get isTodosService(): boolean {
228 return this.recipe.id === todosStore.todoRecipeId; 235 return this.recipe.id === todosStore.todoRecipeId;
229 } 236 }
230 237
231 @computed get canHibernate() { 238 @computed get canHibernate(): boolean {
232 return this.isHibernationEnabled; 239 return this.isHibernationEnabled;
233 } 240 }
234 241
235 @computed get isHibernating() { 242 @computed get isHibernating(): boolean {
236 return this.canHibernate && this.isHibernationRequested; 243 return this.canHibernate && this.isHibernationRequested;
237 } 244 }
238 245
239 get webview() { 246 get webview(): ElectronWebView | null {
240 if (this.isTodosService) { 247 if (this.isTodosService) {
241 return todosStore.webview; 248 return todosStore.webview;
242 } 249 }
@@ -248,9 +255,9 @@ export default class Service {
248 this._webview = webview; 255 this._webview = webview;
249 } 256 }
250 257
251 @computed get url() { 258 @computed get url(): string {
252 if (this.recipe.hasCustomUrl && this.customUrl) { 259 if (this.recipe.hasCustomUrl && this.customUrl) {
253 let url; 260 let url: string = '';
254 try { 261 try {
255 url = normalizeUrl(this.customUrl, { 262 url = normalizeUrl(this.customUrl, {
256 stripAuthentication: false, 263 stripAuthentication: false,
@@ -277,7 +284,7 @@ export default class Service {
277 return this.recipe.serviceURL; 284 return this.recipe.serviceURL;
278 } 285 }
279 286
280 @computed get icon() { 287 @computed get icon(): string {
281 if (this.iconUrl) { 288 if (this.iconUrl) {
282 return this.iconUrl; 289 return this.iconUrl;
283 } 290 }
@@ -285,15 +292,15 @@ export default class Service {
285 return join(this.recipe.path, 'icon.svg'); 292 return join(this.recipe.path, 'icon.svg');
286 } 293 }
287 294
288 @computed get hasCustomIcon() { 295 @computed get hasCustomIcon(): boolean {
289 return Boolean(this.iconUrl); 296 return Boolean(this.iconUrl);
290 } 297 }
291 298
292 @computed get userAgent() { 299 @computed get userAgent(): string {
293 return this.userAgentModel.userAgent; 300 return this.userAgentModel.userAgent;
294 } 301 }
295 302
296 @computed get userAgentPref() { 303 @computed get userAgentPref(): string | null {
297 return this.userAgentModel.userAgentPref; 304 return this.userAgentModel.userAgentPref;
298 } 305 }
299 306
@@ -301,15 +308,15 @@ export default class Service {
301 this.userAgentModel.userAgentPref = pref; 308 this.userAgentModel.userAgentPref = pref;
302 } 309 }
303 310
304 @computed get defaultUserAgent() { 311 @computed get defaultUserAgent(): String {
305 return this.userAgentModel.defaultUserAgent; 312 return this.userAgentModel.defaultUserAgent;
306 } 313 }
307 314
308 @computed get partition() { 315 @computed get partition(): string {
309 return this.recipe.partition || `persist:service-${this.id}`; 316 return this.recipe.partition || `persist:service-${this.id}`;
310 } 317 }
311 318
312 initializeWebViewEvents({ handleIPCMessage, openWindow, stores }) { 319 initializeWebViewEvents({ handleIPCMessage, openWindow, stores }): void {
313 const webviewWebContents = webContents.fromId( 320 const webviewWebContents = webContents.fromId(
314 this.webview.getWebContentsId(), 321 this.webview.getWebContentsId(),
315 ); 322 );
@@ -401,6 +408,7 @@ export default class Service {
401 this.isLoadingPage = false; 408 this.isLoadingPage = false;
402 }); 409 });
403 410
411 // eslint-disable-next-line unicorn/consistent-function-scoping
404 const didLoad = () => { 412 const didLoad = () => {
405 this.isLoading = false; 413 this.isLoading = false;
406 this.isLoadingPage = false; 414 this.isLoadingPage = false;
@@ -437,7 +445,7 @@ export default class Service {
437 this.webview.send('found-in-page', result); 445 this.webview.send('found-in-page', result);
438 }); 446 });
439 447
440 webviewWebContents.on('login', (event, request, authInfo, callback) => { 448 webviewWebContents.on('login', (event, _, authInfo, callback) => {
441 // const authCallback = callback; 449 // const authCallback = callback;
442 debug('browser login event', authInfo); 450 debug('browser login event', authInfo);
443 event.preventDefault(); 451 event.preventDefault();
@@ -460,7 +468,7 @@ export default class Service {
460 }); 468 });
461 } 469 }
462 470
463 initializeWebViewListener() { 471 initializeWebViewListener(): void {
464 if (this.webview && this.recipe.events) { 472 if (this.webview && this.recipe.events) {
465 for (const eventName of Object.keys(this.recipe.events)) { 473 for (const eventName of Object.keys(this.recipe.events)) {
466 const eventHandler = this.recipe[this.recipe.events[eventName]]; 474 const eventHandler = this.recipe[this.recipe.events[eventName]];
@@ -471,7 +479,7 @@ export default class Service {
471 } 479 }
472 } 480 }
473 481
474 resetMessageCount() { 482 resetMessageCount(): void {
475 this.unreadDirectMessageCount = 0; 483 this.unreadDirectMessageCount = 0;
476 this.unreadIndirectMessageCount = 0; 484 this.unreadIndirectMessageCount = 0;
477 } 485 }
diff --git a/src/models/UserAgent.js b/src/models/UserAgent.ts
index 3e1394b45..1d06d72b0 100644
--- a/src/models/UserAgent.js
+++ b/src/models/UserAgent.ts
@@ -1,25 +1,28 @@
1import { action, computed, observe, observable } from 'mobx'; 1import { action, computed, observe, observable } from 'mobx';
2 2
3import ElectronWebView from 'react-electron-web-view';
3import defaultUserAgent from '../helpers/userAgent-helpers'; 4import defaultUserAgent from '../helpers/userAgent-helpers';
4 5
5const debug = require('../preload-safe-debug')('Ferdium:UserAgent'); 6const debug = require('../preload-safe-debug')('Ferdium:UserAgent');
6 7
7export default class UserAgent { 8export default class UserAgent {
8 _willNavigateListener = null; 9 // eslint-disable-next-line @typescript-eslint/no-unused-vars
10 _willNavigateListener = (_event: any): void => {};
9 11
10 _didNavigateListener = null; 12 // eslint-disable-next-line @typescript-eslint/no-unused-vars
13 _didNavigateListener = (_event: any): void => {};
11 14
12 @observable.ref webview = null; 15 @observable.ref webview: ElectronWebView = null;
13 16
14 @observable chromelessUserAgent = false; 17 @observable chromelessUserAgent: boolean = false;
15 18
16 @observable userAgentPref = null; 19 @observable userAgentPref: string | null = null;
17 20
18 @observable getUserAgent = null; 21 @observable overrideUserAgent = (): string => '';
19 22
20 constructor(overrideUserAgent = null) { 23 constructor(overrideUserAgent: any = null) {
21 if (typeof overrideUserAgent === 'function') { 24 if (typeof overrideUserAgent === 'function') {
22 this.getUserAgent = overrideUserAgent; 25 this.overrideUserAgent = overrideUserAgent;
23 } 26 }
24 27
25 observe(this, 'webview', change => { 28 observe(this, 'webview', change => {
@@ -33,10 +36,12 @@ export default class UserAgent {
33 }); 36 });
34 } 37 }
35 38
36 @computed get defaultUserAgent() { 39 @computed get defaultUserAgent(): string {
37 if (typeof this.getUserAgent === 'function') { 40 const replacedUserAgent = this.overrideUserAgent();
38 return this.getUserAgent(); 41 if (replacedUserAgent.length > 0) {
42 return replacedUserAgent;
39 } 43 }
44
40 const globalPref = window['ferdium'].stores.settings.all.app.userAgentPref; 45 const globalPref = window['ferdium'].stores.settings.all.app.userAgentPref;
41 if (typeof globalPref === 'string') { 46 if (typeof globalPref === 'string') {
42 const trimmed = globalPref.trim(); 47 const trimmed = globalPref.trim();
@@ -47,7 +52,7 @@ export default class UserAgent {
47 return defaultUserAgent(); 52 return defaultUserAgent();
48 } 53 }
49 54
50 @computed get serviceUserAgentPref() { 55 @computed get serviceUserAgentPref(): string | null {
51 if (typeof this.userAgentPref === 'string') { 56 if (typeof this.userAgentPref === 'string') {
52 const trimmed = this.userAgentPref.trim(); 57 const trimmed = this.userAgentPref.trim();
53 if (trimmed !== '') { 58 if (trimmed !== '') {
@@ -57,12 +62,12 @@ export default class UserAgent {
57 return null; 62 return null;
58 } 63 }
59 64
60 @computed get userAgentWithoutChromeVersion() { 65 @computed get userAgentWithoutChromeVersion(): string {
61 const withChrome = this.defaultUserAgent; 66 const withChrome = this.defaultUserAgent;
62 return withChrome.replace(/Chrome\/[\d.]+/, 'Chrome'); 67 return withChrome.replace(/Chrome\/[\d.]+/, 'Chrome');
63 } 68 }
64 69
65 @computed get userAgent() { 70 @computed get userAgent(): string {
66 return ( 71 return (
67 this.serviceUserAgentPref || 72 this.serviceUserAgentPref ||
68 (this.chromelessUserAgent 73 (this.chromelessUserAgent
@@ -71,11 +76,11 @@ export default class UserAgent {
71 ); 76 );
72 } 77 }
73 78
74 @action setWebviewReference(webview) { 79 @action setWebviewReference(webview: ElectronWebView): void {
75 this.webview = webview; 80 this.webview = webview;
76 } 81 }
77 82
78 @action _handleNavigate(url, forwardingHack = false) { 83 @action _handleNavigate(url: string, forwardingHack: boolean = false): void {
79 if (url.startsWith('https://accounts.google.com')) { 84 if (url.startsWith('https://accounts.google.com')) {
80 if (!this.chromelessUserAgent) { 85 if (!this.chromelessUserAgent) {
81 debug('Setting user agent to chromeless for url', url); 86 debug('Setting user agent to chromeless for url', url);
@@ -92,7 +97,7 @@ export default class UserAgent {
92 } 97 }
93 } 98 }
94 99
95 _addWebviewEvents(webview) { 100 _addWebviewEvents(webview: ElectronWebView): void {
96 debug('Adding event handlers'); 101 debug('Adding event handlers');
97 102
98 this._willNavigateListener = event => this._handleNavigate(event.url, true); 103 this._willNavigateListener = event => this._handleNavigate(event.url, true);
@@ -102,7 +107,7 @@ export default class UserAgent {
102 webview.addEventListener('did-navigate', this._didNavigateListener); 107 webview.addEventListener('did-navigate', this._didNavigateListener);
103 } 108 }
104 109
105 _removeWebviewEvents(webview) { 110 _removeWebviewEvents(webview: ElectronWebView): void {
106 debug('Removing event handlers'); 111 debug('Removing event handlers');
107 112
108 webview.removeEventListener('will-navigate', this._willNavigateListener); 113 webview.removeEventListener('will-navigate', this._willNavigateListener);
diff --git a/src/stores.types.ts b/src/stores.types.ts
index 462d862d9..24eefc416 100644
--- a/src/stores.types.ts
+++ b/src/stores.types.ts
@@ -76,25 +76,34 @@ interface TypedStore {
76 76
77interface AppStore extends TypedStore { 77interface AppStore extends TypedStore {
78 accentColor: string; 78 accentColor: string;
79 adaptableDarkMode: boolean;
79 progressbarAccentColor: string; 80 progressbarAccentColor: string;
80 authRequestFailed: () => void; 81 authRequestFailed: () => void;
81 autoLaunchOnStart: () => void; 82 autoLaunchOnStart: () => void;
82 automaticUpdates: boolean; 83 automaticUpdates: boolean;
83 clearAppCacheRequest: () => void; 84 clearAppCacheRequest: () => void;
85 clipboardNotifications: boolean;
86 darkMode: boolean;
84 dictionaries: []; 87 dictionaries: [];
88 enableSpellchecking: boolean;
85 fetchDataInterval: 4; 89 fetchDataInterval: 4;
86 get(key: string): any; 90 get(key: string): any;
87 getAppCacheSizeRequest: () => void; 91 getAppCacheSizeRequest: () => void;
88 healthCheckRequest: () => void; 92 healthCheckRequest: () => void;
89 isClearingAllCache: () => void; 93 isClearingAllCache: () => void;
94 isAppMuted: boolean;
90 isFocused: () => void; 95 isFocused: () => void;
91 isFullScreen: () => void; 96 isFullScreen: () => void;
92 isOnline: () => void; 97 isOnline: boolean;
93 isSystemDarkModeEnabled: () => void; 98 isSystemDarkModeEnabled: () => void;
94 isSystemMuteOverridden: () => void; 99 isSystemMuteOverridden: () => void;
95 locale: () => void; 100 locale: () => void;
96 reloadAfterResume: boolean; 101 reloadAfterResume: boolean;
97 reloadAfterResumeTime: number; 102 reloadAfterResumeTime: number;
103 searchEngine: string;
104 spellcheckerLanguage: string;
105 splitMode: boolean;
106 splitColumns: number;
98 timeOfflineStart: () => void; 107 timeOfflineStart: () => void;
99 timeSuspensionStart: () => void; 108 timeSuspensionStart: () => void;
100 updateStatus: () => void; 109 updateStatus: () => void;
@@ -105,6 +114,7 @@ interface AppStore extends TypedStore {
105 DOWNLOADED: 'DOWNLOADED'; 114 DOWNLOADED: 'DOWNLOADED';
106 FAILED: 'FAILED'; 115 FAILED: 'FAILED';
107 }; 116 };
117 universalDarkMode: boolean;
108 cacheSize: () => void; 118 cacheSize: () => void;
109 debugInfo: () => void; 119 debugInfo: () => void;
110} 120}
@@ -145,7 +155,9 @@ interface RecipeStore extends TypedStore {
145 isInstalled: (id: string) => boolean; 155 isInstalled: (id: string) => boolean;
146 active: () => void; 156 active: () => void;
147 all: Recipe[]; 157 all: Recipe[];
158 one: (id: string) => Recipe;
148 recipeIdForServices: () => void; 159 recipeIdForServices: () => void;
160 _install({ recipeId: string }): Promise<Recipe>;
149} 161}
150 162
151interface RequestsStore extends TypedStore { 163interface RequestsStore extends TypedStore {
@@ -183,7 +195,7 @@ export interface ServicesStore extends TypedStore {
183 createServiceRequest: () => void; 195 createServiceRequest: () => void;
184 deleteServiceRequest: () => void; 196 deleteServiceRequest: () => void;
185 allServicesRequest: CachedRequest; 197 allServicesRequest: CachedRequest;
186 filterNeedle: () => void; 198 filterNeedle: string;
187 lastUsedServices: () => void; 199 lastUsedServices: () => void;
188 reorderServicesRequest: () => void; 200 reorderServicesRequest: () => void;
189 serviceMaintenanceTick: () => void; 201 serviceMaintenanceTick: () => void;
@@ -237,7 +249,9 @@ interface TodosStore extends TypedStore {
237 isTodosPanelForceHidden: () => void; 249 isTodosPanelForceHidden: () => void;
238 isTodosPanelVisible: () => void; 250 isTodosPanelVisible: () => void;
239 isUsingPredefinedTodoServer: () => void; 251 isUsingPredefinedTodoServer: () => void;
240 settings: () => void; 252 settings: {
253 isFeatureEnabledByUser: boolean;
254 };
241 todoRecipeId: () => void; 255 todoRecipeId: () => void;
242 todoUrl: () => void; 256 todoUrl: () => void;
243 userAgent: () => void; 257 userAgent: () => void;
@@ -256,7 +270,7 @@ interface UIStore extends TypedStore {
256 isDarkThemeActive: () => void; 270 isDarkThemeActive: () => void;
257 isSplitModeActive: () => void; 271 isSplitModeActive: () => void;
258 splitColumnsNo: () => void; 272 splitColumnsNo: () => void;
259 showMessageBadgesEvenWhenMuted: () => void; 273 showMessageBadgesEvenWhenMuted: boolean;
260 theme: () => void; 274 theme: () => void;
261} 275}
262 276
@@ -315,12 +329,21 @@ export interface WorkspacesStore extends TypedStore {
315 saving: boolean; 329 saving: boolean;
316 filterServicesByActiveWorkspace: () => void; 330 filterServicesByActiveWorkspace: () => void;
317 isFeatureActive: () => void; 331 isFeatureActive: () => void;
332 isAnyWorkspaceActive: boolean;
318 isSettingsRouteActive: () => void; 333 isSettingsRouteActive: () => void;
319 isSwitchingWorkspace: () => void; 334 isSwitchingWorkspace: () => void;
320 isWorkspaceDrawerOpen: () => void; 335 isWorkspaceDrawerOpen: () => void;
321 nextWorkspace: () => void; 336 nextWorkspace: () => void;
322 workspaces: Workspace[]; 337 workspaces: Workspace[];
323 workspaceBeingEdited: () => void; 338 workspaceBeingEdited: () => void;
339 reorderServicesOfActiveWorkspace: ({
340 oldIndex,
341 newIndex,
342 }: {
343 oldIndex: string;
344 newIndex: string;
345 }) => void;
346 settings: any;
324 _activateLastUsedWorkspaceReaction: () => void; 347 _activateLastUsedWorkspaceReaction: () => void;
325 _allActions: any[]; 348 _allActions: any[];
326 _allReactions: any[]; 349 _allReactions: any[];
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.ts
index 999b48d92..caa44146f 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.ts
@@ -5,7 +5,9 @@ import ms from 'ms';
5import { ensureFileSync, pathExistsSync, writeFileSync } from 'fs-extra'; 5import { ensureFileSync, pathExistsSync, writeFileSync } from 'fs-extra';
6import { join } from 'path'; 6import { join } from 'path';
7 7
8import Store from './lib/Store'; 8import { Stores } from 'src/stores.types';
9import { ApiInterface } from 'src/api';
10import { Actions } from 'src/actions/lib/actions';
9import Request from './lib/Request'; 11import Request from './lib/Request';
10import CachedRequest from './lib/CachedRequest'; 12import CachedRequest from './lib/CachedRequest';
11import { matchRoute } from '../helpers/routing-helpers'; 13import { matchRoute } from '../helpers/routing-helpers';
@@ -14,39 +16,56 @@ import {
14 getRecipeDirectory, 16 getRecipeDirectory,
15 getDevRecipeDirectory, 17 getDevRecipeDirectory,
16} from '../helpers/recipe-helpers'; 18} from '../helpers/recipe-helpers';
19import Service from '../models/Service';
17import { workspaceStore } from '../features/workspaces'; 20import { workspaceStore } from '../features/workspaces';
18import { DEFAULT_SERVICE_SETTINGS, KEEP_WS_LOADED_USID } from '../config'; 21import { DEFAULT_SERVICE_SETTINGS, KEEP_WS_LOADED_USID } from '../config';
19import { cleanseJSObject } from '../jsUtils'; 22import { cleanseJSObject } from '../jsUtils';
20import { SPELLCHECKER_LOCALES } from '../i18n/languages'; 23import { SPELLCHECKER_LOCALES } from '../i18n/languages';
21import { ferdiumVersion } from '../environment-remote'; 24import { ferdiumVersion } from '../environment-remote';
25import TypedStore from './lib/TypedStore';
22 26
23const debug = require('../preload-safe-debug')('Ferdium:ServiceStore'); 27const debug = require('../preload-safe-debug')('Ferdium:ServiceStore');
24 28
25export default class ServicesStore extends Store { 29export default class ServicesStore extends TypedStore {
26 @observable allServicesRequest = new CachedRequest(this.api.services, 'all'); 30 @observable allServicesRequest: CachedRequest = new CachedRequest(
31 this.api.services,
32 'all',
33 );
27 34
28 @observable createServiceRequest = new Request(this.api.services, 'create'); 35 @observable createServiceRequest: Request = new Request(
36 this.api.services,
37 'create',
38 );
29 39
30 @observable updateServiceRequest = new Request(this.api.services, 'update'); 40 @observable updateServiceRequest: Request = new Request(
41 this.api.services,
42 'update',
43 );
31 44
32 @observable reorderServicesRequest = new Request( 45 @observable reorderServicesRequest: Request = new Request(
33 this.api.services, 46 this.api.services,
34 'reorder', 47 'reorder',
35 ); 48 );
36 49
37 @observable deleteServiceRequest = new Request(this.api.services, 'delete'); 50 @observable deleteServiceRequest: Request = new Request(
51 this.api.services,
52 'delete',
53 );
38 54
39 @observable clearCacheRequest = new Request(this.api.services, 'clearCache'); 55 @observable clearCacheRequest: Request = new Request(
56 this.api.services,
57 'clearCache',
58 );
40 59
41 @observable filterNeedle = null; 60 @observable filterNeedle: string | null = null;
42 61
43 // Array of service IDs that have recently been used 62 // Array of service IDs that have recently been used
44 // [0] => Most recent, [n] => Least recent 63 // [0] => Most recent, [n] => Least recent
45 // No service ID should be in the list multiple times, not all service IDs have to be in the list 64 // No service ID should be in the list multiple times, not all service IDs have to be in the list
46 @observable lastUsedServices = []; 65 @observable lastUsedServices: string[] = [];
47 66
48 constructor(...args) { 67 constructor(stores: Stores, api: ApiInterface, actions: Actions) {
49 super(...args); 68 super(stores, api, actions);
50 69
51 // Register action handlers 70 // Register action handlers
52 this.actions.service.setActive.listen(this._setActive.bind(this)); 71 this.actions.service.setActive.listen(this._setActive.bind(this));
@@ -207,14 +226,16 @@ export default class ServicesStore extends Store {
207 this.serviceMaintenanceTick.cancel(); 226 this.serviceMaintenanceTick.cancel();
208 } 227 }
209 228
210 /** 229 _serviceMaintenanceTicker() {
211 * Сheck for services to become hibernated.
212 */
213 serviceMaintenanceTick = debounce(() => {
214 this._serviceMaintenance(); 230 this._serviceMaintenance();
215 this.serviceMaintenanceTick(); 231 this.serviceMaintenanceTick();
216 debug('Service maintenance tick'); 232 debug('Service maintenance tick');
217 }, ms('10s')); 233 }
234
235 /**
236 * Сheck for services to become hibernated.
237 */
238 serviceMaintenanceTick = debounce(this._serviceMaintenanceTicker, ms('10s'));
218 239
219 /** 240 /**
220 * Run various maintenance tasks on services 241 * Run various maintenance tasks on services
@@ -271,7 +292,7 @@ export default class ServicesStore extends Store {
271 } 292 }
272 293
273 // Computed props 294 // Computed props
274 @computed get all() { 295 @computed get all(): Service[] {
275 if (this.stores.user.isLoggedIn) { 296 if (this.stores.user.isLoggedIn) {
276 const services = this.allServicesRequest.execute().result; 297 const services = this.allServicesRequest.execute().result;
277 if (services) { 298 if (services) {
@@ -289,7 +310,7 @@ export default class ServicesStore extends Store {
289 return []; 310 return [];
290 } 311 }
291 312
292 @computed get enabled() { 313 @computed get enabled(): Service[] {
293 return this.all.filter(service => service.isEnabled); 314 return this.all.filter(service => service.isEnabled);
294 } 315 }
295 316
@@ -344,9 +365,14 @@ export default class ServicesStore extends Store {
344 } 365 }
345 366
346 @computed get filtered() { 367 @computed get filtered() {
347 return this.all.filter(service => 368 if (this.filterNeedle !== null) {
348 service.name.toLowerCase().includes(this.filterNeedle.toLowerCase()), 369 return this.all.filter(service =>
349 ); 370 service.name.toLowerCase().includes(this.filterNeedle!.toLowerCase()),
371 );
372 }
373
374 // Return all if there is no filterNeedle present
375 return this.all;
350 } 376 }
351 377
352 @computed get active() { 378 @computed get active() {
@@ -382,8 +408,9 @@ export default class ServicesStore extends Store {
382 return this.active && this.active.isTodosService; 408 return this.active && this.active.isTodosService;
383 } 409 }
384 410
385 one(id) { 411 // TODO: This can actually return undefined as well
386 return this.all.find(service => service.id === id); 412 one(id: string): Service {
413 return this.all.find(service => service.id === id) as Service;
387 } 414 }
388 415
389 async _showAddServiceInterface({ recipeId }) { 416 async _showAddServiceInterface({ recipeId }) {
@@ -449,7 +476,11 @@ export default class ServicesStore extends Store {
449 476
450 @action async _createFromLegacyService({ data }) { 477 @action async _createFromLegacyService({ data }) {
451 const { id } = data.recipe; 478 const { id } = data.recipe;
452 const serviceData = {}; 479 const serviceData: {
480 name?: string;
481 team?: string;
482 customUrl?: string;
483 } = {};
453 484
454 if (data.name) { 485 if (data.name) {
455 serviceData.name = data.name; 486 serviceData.name = data.name;
@@ -530,7 +561,7 @@ export default class ServicesStore extends Store {
530 } 561 }
531 } 562 }
532 563
533 @action async _deleteService({ serviceId, redirect }) { 564 @action async _deleteService({ serviceId, redirect }): Promise<void> {
534 const request = this.deleteServiceRequest.execute(serviceId); 565 const request = this.deleteServiceRequest.execute(serviceId);
535 566
536 if (redirect) { 567 if (redirect) {
@@ -538,14 +569,14 @@ export default class ServicesStore extends Store {
538 } 569 }
539 570
540 this.allServicesRequest.patch(result => { 571 this.allServicesRequest.patch(result => {
541 remove(result, c => c.id === serviceId); 572 remove(result, (c: Service) => c.id === serviceId);
542 }); 573 });
543 574
544 await request._promise; 575 await request._promise;
545 this.actionStatus = request.result.status; 576 this.actionStatus = request.result.status;
546 } 577 }
547 578
548 @action async _openRecipeFile({ recipe, file }) { 579 @action async _openRecipeFile({ recipe, file }): Promise<void> {
549 // Get directory for recipe 580 // Get directory for recipe
550 const normalDirectory = getRecipeDirectory(recipe); 581 const normalDirectory = getRecipeDirectory(recipe);
551 const devDirectory = getDevRecipeDirectory(recipe); 582 const devDirectory = getDevRecipeDirectory(recipe);
@@ -702,7 +733,7 @@ export default class ServicesStore extends Store {
702 setTimeout(() => { 733 setTimeout(() => {
703 document 734 document
704 .querySelector('.services__webview-wrapper.is-active') 735 .querySelector('.services__webview-wrapper.is-active')
705 .scrollIntoView({ 736 ?.scrollIntoView({
706 behavior: 'smooth', 737 behavior: 'smooth',
707 block: 'end', 738 block: 'end',
708 inline: 'nearest', 739 inline: 'nearest',
@@ -1046,7 +1077,13 @@ export default class ServicesStore extends Store {
1046 service.lastHibernated = Date.now(); 1077 service.lastHibernated = Date.now();
1047 } 1078 }
1048 1079
1049 @action _awake({ serviceId, automatic }) { 1080 @action _awake({
1081 serviceId,
1082 automatic,
1083 }: {
1084 serviceId: string;
1085 automatic?: boolean;
1086 }) {
1050 const now = Date.now(); 1087 const now = Date.now();
1051 const service = this.one(serviceId); 1088 const service = this.one(serviceId);
1052 const automaticTag = automatic ? ' automatically ' : ' '; 1089 const automaticTag = automatic ? ' automatically ' : ' ';
@@ -1223,7 +1260,7 @@ export default class ServicesStore extends Store {
1223 } 1260 }
1224 } 1261 }
1225 1262
1226 _shareSettingsWithServiceProcess() { 1263 _shareSettingsWithServiceProcess(): void {
1227 const settings = { 1264 const settings = {
1228 ...this.stores.settings.app, 1265 ...this.stores.settings.app,
1229 isDarkThemeActive: this.stores.ui.isDarkThemeActive, 1266 isDarkThemeActive: this.stores.ui.isDarkThemeActive,
@@ -1234,7 +1271,7 @@ export default class ServicesStore extends Store {
1234 }); 1271 });
1235 } 1272 }
1236 1273
1237 _cleanUpTeamIdAndCustomUrl(recipeId, data) { 1274 _cleanUpTeamIdAndCustomUrl(recipeId, data): any {
1238 const serviceData = data; 1275 const serviceData = data;
1239 const recipe = this.stores.recipes.one(recipeId); 1276 const recipe = this.stores.recipes.one(recipeId);
1240 1277
diff --git a/src/webview/recipe.js b/src/webview/recipe.js
index 847a720ff..9d5a97767 100644
--- a/src/webview/recipe.js
+++ b/src/webview/recipe.js
@@ -158,7 +158,10 @@ class RecipeController {
158 } 158 }
159 159
160 @computed get spellcheckerLanguage() { 160 @computed get spellcheckerLanguage() {
161 return ifUndefinedString(this.settings.service.spellcheckerLanguage, this.settings.app.spellcheckerLanguage); 161 return ifUndefinedString(
162 this.settings.service.spellcheckerLanguage,
163 this.settings.app.spellcheckerLanguage,
164 );
162 } 165 }
163 166
164 cldIdentifier = null; 167 cldIdentifier = null;
@@ -197,13 +200,13 @@ class RecipeController {
197 // Add ability to go forward or back with mouse buttons (inside the recipe) 200 // Add ability to go forward or back with mouse buttons (inside the recipe)
198 window.addEventListener('mouseup', e => { 201 window.addEventListener('mouseup', e => {
199 if (e.button === 3) { 202 if (e.button === 3) {
200 e.preventDefault() 203 e.preventDefault();
201 e.stopPropagation() 204 e.stopPropagation();
202 window.history.back() 205 window.history.back();
203 } else if (e.button === 4) { 206 } else if (e.button === 4) {
204 e.preventDefault() 207 e.preventDefault();
205 e.stopPropagation() 208 e.stopPropagation();
206 window.history.forward() 209 window.history.forward();
207 } 210 }
208 }); 211 });
209 } 212 }