diff options
author | Ricardo Cino <ricardo@cino.io> | 2022-06-23 18:10:39 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-06-23 16:10:39 +0000 |
commit | 6b2c2b8dfb86245a1747bf7977159f5129461863 (patch) | |
tree | 28944f62a962d8a658262ea902f8554d4419fa9e /src | |
parent | chore: featureStore and GlobalErrorStore JS => TS (diff) | |
download | ferdium-app-6b2c2b8dfb86245a1747bf7977159f5129461863.tar.gz ferdium-app-6b2c2b8dfb86245a1747bf7977159f5129461863.tar.zst ferdium-app-6b2c2b8dfb86245a1747bf7977159f5129461863.zip |
chore: servicesStore + models into typescript (#344)
Diffstat (limited to 'src')
-rw-r--r-- | src/containers/auth/SetupAssistantScreen.tsx | 26 | ||||
-rw-r--r-- | src/features/todos/store.js | 4 | ||||
-rw-r--r-- | src/features/workspaces/models/Workspace.ts | 2 | ||||
-rw-r--r-- | src/index.ts | 4 | ||||
-rw-r--r-- | src/jsUtils.test.ts | 41 | ||||
-rw-r--r-- | src/jsUtils.ts | 23 | ||||
-rw-r--r-- | src/models/Recipe.ts | 56 | ||||
-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.ts | 31 | ||||
-rw-r--r-- | src/stores/ServicesStore.ts (renamed from src/stores/ServicesStore.js) | 101 | ||||
-rw-r--r-- | src/webview/recipe.js | 17 |
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 | ||
12 | interface IProps { | 12 | interface 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 | ||
26 | class SetupAssistantScreen extends Component<IProps> { | 26 | class 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'; | |||
18 | import { IPC, TODOS_ROUTES } from './constants'; | 18 | import { IPC, TODOS_ROUTES } from './constants'; |
19 | import UserAgent from '../../models/UserAgent'; | 19 | import UserAgent from '../../models/UserAgent'; |
20 | 20 | ||
21 | const debug = require('../../preload-safe-debug')('Ferdium:feature:todos:store'); | 21 | const debug = require('../../preload-safe-debug')( |
22 | 'Ferdium:feature:todos:store', | ||
23 | ); | ||
22 | 24 | ||
23 | export default class TodoStore extends FeatureStore { | 25 | export 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'; |
33 | import { ifUndefinedBoolean } from './jsUtils'; | 33 | import { ifUndefined } from './jsUtils'; |
34 | 34 | ||
35 | import { mainIpcHandler as basicAuthHandler } from './features/basicAuth'; | 35 | import { mainIpcHandler as basicAuthHandler } from './features/basicAuth'; |
36 | import ipcApi from './electron/ipc-api'; | 36 | import ipcApi from './electron/ipc-api'; |
@@ -91,7 +91,7 @@ const settings = new Settings('app', DEFAULT_APP_SETTINGS); | |||
91 | const proxySettings = new Settings('proxy'); | 91 | const proxySettings = new Settings('proxy'); |
92 | 92 | ||
93 | const retrieveSettingValue = (key: string, defaultValue: boolean) => | 93 | const retrieveSettingValue = (key: string, defaultValue: boolean) => |
94 | ifUndefinedBoolean(settings.get(key), defaultValue); | 94 | ifUndefined<boolean>(settings.get(key), defaultValue); |
95 | 95 | ||
96 | const liftSingleInstanceLock = retrieveSettingValue( | 96 | const 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 @@ | |||
1 | export 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. |
2 | export const ifUndefinedString = ( | ||
3 | source: string | undefined | null, | ||
4 | defaultValue: string, | ||
5 | ): string => (source !== undefined && source !== null ? source : defaultValue); | ||
2 | 6 | ||
3 | export const ifUndefinedBoolean = (source: boolean | undefined | null, defaultValue: boolean): boolean => Boolean(source !== undefined && source !== null ? source : defaultValue); | 7 | export 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 | ||
5 | export const ifUndefinedNumber = (source: number | undefined | null, defaultValue: number): number => Number(source !== undefined && source !== null ? source : defaultValue); | 15 | return defaultValue; |
16 | }; | ||
6 | 17 | ||
7 | export const convertToJSON = (data: string | any | undefined | null) => data && typeof data === 'string' && data.length > 0 ? JSON.parse(data) : data | 18 | export const convertToJSON = (data: string | any | undefined | null) => |
19 | data && typeof data === 'string' && data.length > 0 ? JSON.parse(data) : data; | ||
8 | 20 | ||
9 | export const cleanseJSObject = (data: any | undefined | null) => JSON.parse(JSON.stringify(data)) | 21 | export 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'; | |||
2 | import { pathExistsSync } from 'fs-extra'; | 2 | import { pathExistsSync } from 'fs-extra'; |
3 | import { join } from 'path'; | 3 | import { join } from 'path'; |
4 | import { DEFAULT_SERVICE_SETTINGS } from '../config'; | 4 | import { DEFAULT_SERVICE_SETTINGS } from '../config'; |
5 | import { ifUndefinedString, ifUndefinedBoolean } from '../jsUtils'; | 5 | import { ifUndefined } from '../jsUtils'; |
6 | 6 | ||
7 | interface IRecipe { | 7 | interface 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'; | |||
3 | import { webContents } from '@electron/remote'; | 3 | import { webContents } from '@electron/remote'; |
4 | import normalizeUrl from 'normalize-url'; | 4 | import normalizeUrl from 'normalize-url'; |
5 | import { join } from 'path'; | 5 | import { join } from 'path'; |
6 | import ElectronWebView from 'react-electron-web-view'; | ||
6 | 7 | ||
7 | import { todosStore } from '../features/todos'; | 8 | import { todosStore } from '../features/todos'; |
8 | import { isValidExternalURL } from '../helpers/url-helpers'; | 9 | import { isValidExternalURL } from '../helpers/url-helpers'; |
9 | import UserAgent from './UserAgent'; | 10 | import UserAgent from './UserAgent'; |
10 | import { DEFAULT_SERVICE_ORDER } from '../config'; | 11 | import { DEFAULT_SERVICE_ORDER } from '../config'; |
11 | import { | 12 | import { ifUndefined } from '../jsUtils'; |
12 | ifUndefinedString, | 13 | import Recipe from './Recipe'; |
13 | ifUndefinedBoolean, | ||
14 | ifUndefinedNumber, | ||
15 | } from '../jsUtils'; | ||
16 | 14 | ||
17 | const debug = require('../preload-safe-debug')('Ferdium:Service'); | 15 | const 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? |
20 | export default class Service { | 18 | export 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 @@ | |||
1 | import { action, computed, observe, observable } from 'mobx'; | 1 | import { action, computed, observe, observable } from 'mobx'; |
2 | 2 | ||
3 | import ElectronWebView from 'react-electron-web-view'; | ||
3 | import defaultUserAgent from '../helpers/userAgent-helpers'; | 4 | import defaultUserAgent from '../helpers/userAgent-helpers'; |
4 | 5 | ||
5 | const debug = require('../preload-safe-debug')('Ferdium:UserAgent'); | 6 | const debug = require('../preload-safe-debug')('Ferdium:UserAgent'); |
6 | 7 | ||
7 | export default class UserAgent { | 8 | export 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 | ||
77 | interface AppStore extends TypedStore { | 77 | interface 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 | ||
151 | interface RequestsStore extends TypedStore { | 163 | interface 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'; | |||
5 | import { ensureFileSync, pathExistsSync, writeFileSync } from 'fs-extra'; | 5 | import { ensureFileSync, pathExistsSync, writeFileSync } from 'fs-extra'; |
6 | import { join } from 'path'; | 6 | import { join } from 'path'; |
7 | 7 | ||
8 | import Store from './lib/Store'; | 8 | import { Stores } from 'src/stores.types'; |
9 | import { ApiInterface } from 'src/api'; | ||
10 | import { Actions } from 'src/actions/lib/actions'; | ||
9 | import Request from './lib/Request'; | 11 | import Request from './lib/Request'; |
10 | import CachedRequest from './lib/CachedRequest'; | 12 | import CachedRequest from './lib/CachedRequest'; |
11 | import { matchRoute } from '../helpers/routing-helpers'; | 13 | import { 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'; |
19 | import Service from '../models/Service'; | ||
17 | import { workspaceStore } from '../features/workspaces'; | 20 | import { workspaceStore } from '../features/workspaces'; |
18 | import { DEFAULT_SERVICE_SETTINGS, KEEP_WS_LOADED_USID } from '../config'; | 21 | import { DEFAULT_SERVICE_SETTINGS, KEEP_WS_LOADED_USID } from '../config'; |
19 | import { cleanseJSObject } from '../jsUtils'; | 22 | import { cleanseJSObject } from '../jsUtils'; |
20 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; | 23 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; |
21 | import { ferdiumVersion } from '../environment-remote'; | 24 | import { ferdiumVersion } from '../environment-remote'; |
25 | import TypedStore from './lib/TypedStore'; | ||
22 | 26 | ||
23 | const debug = require('../preload-safe-debug')('Ferdium:ServiceStore'); | 27 | const debug = require('../preload-safe-debug')('Ferdium:ServiceStore'); |
24 | 28 | ||
25 | export default class ServicesStore extends Store { | 29 | export 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 | } |