diff options
author | 2022-06-23 18:10:39 +0200 | |
---|---|---|
committer | 2022-06-23 16:10:39 +0000 | |
commit | 6b2c2b8dfb86245a1747bf7977159f5129461863 (patch) | |
tree | 28944f62a962d8a658262ea902f8554d4419fa9e /src/stores/ServicesStore.js | |
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/stores/ServicesStore.js')
-rw-r--r-- | src/stores/ServicesStore.js | 1319 |
1 files changed, 0 insertions, 1319 deletions
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js deleted file mode 100644 index 999b48d92..000000000 --- a/src/stores/ServicesStore.js +++ /dev/null | |||
@@ -1,1319 +0,0 @@ | |||
1 | import { shell } from 'electron'; | ||
2 | import { action, reaction, computed, observable } from 'mobx'; | ||
3 | import { debounce, remove } from 'lodash'; | ||
4 | import ms from 'ms'; | ||
5 | import { ensureFileSync, pathExistsSync, writeFileSync } from 'fs-extra'; | ||
6 | import { join } from 'path'; | ||
7 | |||
8 | import Store from './lib/Store'; | ||
9 | import Request from './lib/Request'; | ||
10 | import CachedRequest from './lib/CachedRequest'; | ||
11 | import { matchRoute } from '../helpers/routing-helpers'; | ||
12 | import { isInTimeframe } from '../helpers/schedule-helpers'; | ||
13 | import { | ||
14 | getRecipeDirectory, | ||
15 | getDevRecipeDirectory, | ||
16 | } from '../helpers/recipe-helpers'; | ||
17 | import { workspaceStore } from '../features/workspaces'; | ||
18 | import { DEFAULT_SERVICE_SETTINGS, KEEP_WS_LOADED_USID } from '../config'; | ||
19 | import { cleanseJSObject } from '../jsUtils'; | ||
20 | import { SPELLCHECKER_LOCALES } from '../i18n/languages'; | ||
21 | import { ferdiumVersion } from '../environment-remote'; | ||
22 | |||
23 | const debug = require('../preload-safe-debug')('Ferdium:ServiceStore'); | ||
24 | |||
25 | export default class ServicesStore extends Store { | ||
26 | @observable allServicesRequest = new CachedRequest(this.api.services, 'all'); | ||
27 | |||
28 | @observable createServiceRequest = new Request(this.api.services, 'create'); | ||
29 | |||
30 | @observable updateServiceRequest = new Request(this.api.services, 'update'); | ||
31 | |||
32 | @observable reorderServicesRequest = new Request( | ||
33 | this.api.services, | ||
34 | 'reorder', | ||
35 | ); | ||
36 | |||
37 | @observable deleteServiceRequest = new Request(this.api.services, 'delete'); | ||
38 | |||
39 | @observable clearCacheRequest = new Request(this.api.services, 'clearCache'); | ||
40 | |||
41 | @observable filterNeedle = null; | ||
42 | |||
43 | // Array of service IDs that have recently been used | ||
44 | // [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 | ||
46 | @observable lastUsedServices = []; | ||
47 | |||
48 | constructor(...args) { | ||
49 | super(...args); | ||
50 | |||
51 | // Register action handlers | ||
52 | this.actions.service.setActive.listen(this._setActive.bind(this)); | ||
53 | this.actions.service.blurActive.listen(this._blurActive.bind(this)); | ||
54 | this.actions.service.setActiveNext.listen(this._setActiveNext.bind(this)); | ||
55 | this.actions.service.setActivePrev.listen(this._setActivePrev.bind(this)); | ||
56 | this.actions.service.showAddServiceInterface.listen( | ||
57 | this._showAddServiceInterface.bind(this), | ||
58 | ); | ||
59 | this.actions.service.createService.listen(this._createService.bind(this)); | ||
60 | this.actions.service.createFromLegacyService.listen( | ||
61 | this._createFromLegacyService.bind(this), | ||
62 | ); | ||
63 | this.actions.service.updateService.listen(this._updateService.bind(this)); | ||
64 | this.actions.service.deleteService.listen(this._deleteService.bind(this)); | ||
65 | this.actions.service.openRecipeFile.listen(this._openRecipeFile.bind(this)); | ||
66 | this.actions.service.clearCache.listen(this._clearCache.bind(this)); | ||
67 | this.actions.service.setWebviewReference.listen( | ||
68 | this._setWebviewReference.bind(this), | ||
69 | ); | ||
70 | this.actions.service.detachService.listen(this._detachService.bind(this)); | ||
71 | this.actions.service.focusService.listen(this._focusService.bind(this)); | ||
72 | this.actions.service.focusActiveService.listen( | ||
73 | this._focusActiveService.bind(this), | ||
74 | ); | ||
75 | this.actions.service.toggleService.listen(this._toggleService.bind(this)); | ||
76 | this.actions.service.handleIPCMessage.listen( | ||
77 | this._handleIPCMessage.bind(this), | ||
78 | ); | ||
79 | this.actions.service.sendIPCMessage.listen(this._sendIPCMessage.bind(this)); | ||
80 | this.actions.service.sendIPCMessageToAllServices.listen( | ||
81 | this._sendIPCMessageToAllServices.bind(this), | ||
82 | ); | ||
83 | this.actions.service.setUnreadMessageCount.listen( | ||
84 | this._setUnreadMessageCount.bind(this), | ||
85 | ); | ||
86 | this.actions.service.setDialogTitle.listen(this._setDialogTitle.bind(this)); | ||
87 | this.actions.service.openWindow.listen(this._openWindow.bind(this)); | ||
88 | this.actions.service.filter.listen(this._filter.bind(this)); | ||
89 | this.actions.service.resetFilter.listen(this._resetFilter.bind(this)); | ||
90 | this.actions.service.resetStatus.listen(this._resetStatus.bind(this)); | ||
91 | this.actions.service.reload.listen(this._reload.bind(this)); | ||
92 | this.actions.service.reloadActive.listen(this._reloadActive.bind(this)); | ||
93 | this.actions.service.reloadAll.listen(this._reloadAll.bind(this)); | ||
94 | this.actions.service.reloadUpdatedServices.listen( | ||
95 | this._reloadUpdatedServices.bind(this), | ||
96 | ); | ||
97 | this.actions.service.reorder.listen(this._reorder.bind(this)); | ||
98 | this.actions.service.toggleNotifications.listen( | ||
99 | this._toggleNotifications.bind(this), | ||
100 | ); | ||
101 | this.actions.service.toggleAudio.listen(this._toggleAudio.bind(this)); | ||
102 | this.actions.service.toggleDarkMode.listen(this._toggleDarkMode.bind(this)); | ||
103 | this.actions.service.openDevTools.listen(this._openDevTools.bind(this)); | ||
104 | this.actions.service.openDevToolsForActiveService.listen( | ||
105 | this._openDevToolsForActiveService.bind(this), | ||
106 | ); | ||
107 | this.actions.service.hibernate.listen(this._hibernate.bind(this)); | ||
108 | this.actions.service.awake.listen(this._awake.bind(this)); | ||
109 | this.actions.service.resetLastPollTimer.listen( | ||
110 | this._resetLastPollTimer.bind(this), | ||
111 | ); | ||
112 | this.actions.service.shareSettingsWithServiceProcess.listen( | ||
113 | this._shareSettingsWithServiceProcess.bind(this), | ||
114 | ); | ||
115 | |||
116 | this.registerReactions([ | ||
117 | this._focusServiceReaction.bind(this), | ||
118 | this._getUnreadMessageCountReaction.bind(this), | ||
119 | this._mapActiveServiceToServiceModelReaction.bind(this), | ||
120 | this._saveActiveService.bind(this), | ||
121 | this._logoutReaction.bind(this), | ||
122 | this._handleMuteSettings.bind(this), | ||
123 | this._checkForActiveService.bind(this), | ||
124 | ]); | ||
125 | |||
126 | // Just bind this | ||
127 | this._initializeServiceRecipeInWebview.bind(this); | ||
128 | } | ||
129 | |||
130 | setup() { | ||
131 | // Single key reactions for the sake of your CPU | ||
132 | reaction( | ||
133 | () => this.stores.settings.app.enableSpellchecking, | ||
134 | () => { | ||
135 | this._shareSettingsWithServiceProcess(); | ||
136 | }, | ||
137 | ); | ||
138 | |||
139 | reaction( | ||
140 | () => this.stores.settings.app.spellcheckerLanguage, | ||
141 | () => { | ||
142 | this._shareSettingsWithServiceProcess(); | ||
143 | }, | ||
144 | ); | ||
145 | |||
146 | reaction( | ||
147 | () => this.stores.settings.app.darkMode, | ||
148 | () => { | ||
149 | this._shareSettingsWithServiceProcess(); | ||
150 | }, | ||
151 | ); | ||
152 | |||
153 | reaction( | ||
154 | () => this.stores.settings.app.adaptableDarkMode, | ||
155 | () => { | ||
156 | this._shareSettingsWithServiceProcess(); | ||
157 | }, | ||
158 | ); | ||
159 | |||
160 | reaction( | ||
161 | () => this.stores.settings.app.universalDarkMode, | ||
162 | () => { | ||
163 | this._shareSettingsWithServiceProcess(); | ||
164 | }, | ||
165 | ); | ||
166 | |||
167 | reaction( | ||
168 | () => this.stores.settings.app.splitMode, | ||
169 | () => { | ||
170 | this._shareSettingsWithServiceProcess(); | ||
171 | }, | ||
172 | ); | ||
173 | |||
174 | reaction( | ||
175 | () => this.stores.settings.app.splitColumns, | ||
176 | () => { | ||
177 | this._shareSettingsWithServiceProcess(); | ||
178 | }, | ||
179 | ); | ||
180 | |||
181 | reaction( | ||
182 | () => this.stores.settings.app.searchEngine, | ||
183 | () => { | ||
184 | this._shareSettingsWithServiceProcess(); | ||
185 | }, | ||
186 | ); | ||
187 | |||
188 | reaction( | ||
189 | () => this.stores.settings.app.clipboardNotifications, | ||
190 | () => { | ||
191 | this._shareSettingsWithServiceProcess(); | ||
192 | }, | ||
193 | ); | ||
194 | } | ||
195 | |||
196 | initialize() { | ||
197 | super.initialize(); | ||
198 | |||
199 | // Check services to become hibernated | ||
200 | this.serviceMaintenanceTick(); | ||
201 | } | ||
202 | |||
203 | teardown() { | ||
204 | super.teardown(); | ||
205 | |||
206 | // Stop checking services for hibernation | ||
207 | this.serviceMaintenanceTick.cancel(); | ||
208 | } | ||
209 | |||
210 | /** | ||
211 | * Сheck for services to become hibernated. | ||
212 | */ | ||
213 | serviceMaintenanceTick = debounce(() => { | ||
214 | this._serviceMaintenance(); | ||
215 | this.serviceMaintenanceTick(); | ||
216 | debug('Service maintenance tick'); | ||
217 | }, ms('10s')); | ||
218 | |||
219 | /** | ||
220 | * Run various maintenance tasks on services | ||
221 | */ | ||
222 | _serviceMaintenance() { | ||
223 | for (const service of this.enabled) { | ||
224 | // Defines which services should be hibernated or woken up | ||
225 | if (!service.isActive) { | ||
226 | if ( | ||
227 | !service.lastHibernated && | ||
228 | Date.now() - service.lastUsed > | ||
229 | ms(`${this.stores.settings.all.app.hibernationStrategy}s`) | ||
230 | ) { | ||
231 | // If service is stale, hibernate it. | ||
232 | this._hibernate({ serviceId: service.id }); | ||
233 | } | ||
234 | |||
235 | if ( | ||
236 | service.isWakeUpEnabled && | ||
237 | service.lastHibernated && | ||
238 | Number(this.stores.settings.all.app.wakeUpStrategy) > 0 && | ||
239 | Date.now() - service.lastHibernated > | ||
240 | ms(`${this.stores.settings.all.app.wakeUpStrategy}s`) | ||
241 | ) { | ||
242 | // If service is in hibernation and the wakeup time has elapsed, wake it. | ||
243 | this._awake({ serviceId: service.id, automatic: true }); | ||
244 | } | ||
245 | } | ||
246 | |||
247 | if ( | ||
248 | service.lastPoll && | ||
249 | service.lastPoll - service.lastPollAnswer > ms('1m') | ||
250 | ) { | ||
251 | // If service did not reply for more than 1m try to reload. | ||
252 | if (!service.isActive) { | ||
253 | if (this.stores.app.isOnline && service.lostRecipeReloadAttempt < 3) { | ||
254 | debug( | ||
255 | `Reloading service: ${service.name} (${service.id}). Attempt: ${service.lostRecipeReloadAttempt}`, | ||
256 | ); | ||
257 | // service.webview.reload(); | ||
258 | service.lostRecipeReloadAttempt += 1; | ||
259 | |||
260 | service.lostRecipeConnection = false; | ||
261 | } | ||
262 | } else { | ||
263 | debug(`Service lost connection: ${service.name} (${service.id}).`); | ||
264 | service.lostRecipeConnection = true; | ||
265 | } | ||
266 | } else { | ||
267 | service.lostRecipeConnection = false; | ||
268 | service.lostRecipeReloadAttempt = 0; | ||
269 | } | ||
270 | } | ||
271 | } | ||
272 | |||
273 | // Computed props | ||
274 | @computed get all() { | ||
275 | if (this.stores.user.isLoggedIn) { | ||
276 | const services = this.allServicesRequest.execute().result; | ||
277 | if (services) { | ||
278 | return observable( | ||
279 | [...services] | ||
280 | .slice() | ||
281 | .sort((a, b) => a.order - b.order) | ||
282 | .map((s, index) => { | ||
283 | s.index = index; | ||
284 | return s; | ||
285 | }), | ||
286 | ); | ||
287 | } | ||
288 | } | ||
289 | return []; | ||
290 | } | ||
291 | |||
292 | @computed get enabled() { | ||
293 | return this.all.filter(service => service.isEnabled); | ||
294 | } | ||
295 | |||
296 | @computed get allDisplayed() { | ||
297 | const services = this.stores.settings.all.app.showDisabledServices | ||
298 | ? this.all | ||
299 | : this.enabled; | ||
300 | return workspaceStore.filterServicesByActiveWorkspace(services); | ||
301 | } | ||
302 | |||
303 | // This is just used to avoid unnecessary rerendering of resource-heavy webviews | ||
304 | @computed get allDisplayedUnordered() { | ||
305 | const { showDisabledServices } = this.stores.settings.all.app; | ||
306 | const { keepAllWorkspacesLoaded } = this.stores.workspaces.settings; | ||
307 | const services = this.allServicesRequest.execute().result || []; | ||
308 | const filteredServices = showDisabledServices | ||
309 | ? services | ||
310 | : services.filter(service => service.isEnabled); | ||
311 | |||
312 | let displayedServices; | ||
313 | if (keepAllWorkspacesLoaded) { | ||
314 | // Keep all enabled services loaded | ||
315 | displayedServices = filteredServices; | ||
316 | } else { | ||
317 | // Keep all services in current workspace loaded | ||
318 | displayedServices = | ||
319 | workspaceStore.filterServicesByActiveWorkspace(filteredServices); | ||
320 | |||
321 | // Keep all services active in workspaces that should be kept loaded | ||
322 | for (const workspace of this.stores.workspaces.workspaces) { | ||
323 | // Check if workspace needs to be kept loaded | ||
324 | if (workspace.services.includes(KEEP_WS_LOADED_USID)) { | ||
325 | // Get services for workspace | ||
326 | const serviceIDs = new Set( | ||
327 | workspace.services.filter(i => i !== KEEP_WS_LOADED_USID), | ||
328 | ); | ||
329 | const wsServices = filteredServices.filter(service => | ||
330 | serviceIDs.has(service.id), | ||
331 | ); | ||
332 | |||
333 | displayedServices = [...displayedServices, ...wsServices]; | ||
334 | } | ||
335 | } | ||
336 | |||
337 | // Make sure every service is in the list only once | ||
338 | displayedServices = displayedServices.filter( | ||
339 | (v, i, a) => a.indexOf(v) === i, | ||
340 | ); | ||
341 | } | ||
342 | |||
343 | return displayedServices; | ||
344 | } | ||
345 | |||
346 | @computed get filtered() { | ||
347 | return this.all.filter(service => | ||
348 | service.name.toLowerCase().includes(this.filterNeedle.toLowerCase()), | ||
349 | ); | ||
350 | } | ||
351 | |||
352 | @computed get active() { | ||
353 | return this.all.find(service => service.isActive); | ||
354 | } | ||
355 | |||
356 | @computed get activeSettings() { | ||
357 | const match = matchRoute( | ||
358 | '/settings/services/edit/:id', | ||
359 | this.stores.router.location.pathname, | ||
360 | ); | ||
361 | if (match) { | ||
362 | const activeService = this.one(match.id); | ||
363 | if (activeService) { | ||
364 | return activeService; | ||
365 | } | ||
366 | |||
367 | debug('Service not available'); | ||
368 | } | ||
369 | |||
370 | return null; | ||
371 | } | ||
372 | |||
373 | @computed get isTodosServiceAdded() { | ||
374 | return ( | ||
375 | this.allDisplayed.find( | ||
376 | service => service.isTodosService && service.isEnabled, | ||
377 | ) || false | ||
378 | ); | ||
379 | } | ||
380 | |||
381 | @computed get isTodosServiceActive() { | ||
382 | return this.active && this.active.isTodosService; | ||
383 | } | ||
384 | |||
385 | one(id) { | ||
386 | return this.all.find(service => service.id === id); | ||
387 | } | ||
388 | |||
389 | async _showAddServiceInterface({ recipeId }) { | ||
390 | this.stores.router.push(`/settings/services/add/${recipeId}`); | ||
391 | } | ||
392 | |||
393 | // Actions | ||
394 | async _createService({ | ||
395 | recipeId, | ||
396 | serviceData, | ||
397 | redirect = true, | ||
398 | skipCleanup = false, | ||
399 | }) { | ||
400 | if (!this.stores.recipes.isInstalled(recipeId)) { | ||
401 | debug(`Recipe "${recipeId}" is not installed, installing recipe`); | ||
402 | await this.stores.recipes._install({ recipeId }); | ||
403 | debug(`Recipe "${recipeId}" installed`); | ||
404 | } | ||
405 | |||
406 | // set default values for serviceData | ||
407 | serviceData = { | ||
408 | isEnabled: DEFAULT_SERVICE_SETTINGS.isEnabled, | ||
409 | isHibernationEnabled: DEFAULT_SERVICE_SETTINGS.isHibernationEnabled, | ||
410 | isWakeUpEnabled: DEFAULT_SERVICE_SETTINGS.isWakeUpEnabled, | ||
411 | isNotificationEnabled: DEFAULT_SERVICE_SETTINGS.isNotificationEnabled, | ||
412 | isBadgeEnabled: DEFAULT_SERVICE_SETTINGS.isBadgeEnabled, | ||
413 | trapLinkClicks: DEFAULT_SERVICE_SETTINGS.trapLinkClicks, | ||
414 | isMuted: DEFAULT_SERVICE_SETTINGS.isMuted, | ||
415 | customIcon: DEFAULT_SERVICE_SETTINGS.customIcon, | ||
416 | isDarkModeEnabled: DEFAULT_SERVICE_SETTINGS.isDarkModeEnabled, | ||
417 | isProgressbarEnabled: DEFAULT_SERVICE_SETTINGS.isProgressbarEnabled, | ||
418 | spellcheckerLanguage: | ||
419 | SPELLCHECKER_LOCALES[this.stores.settings.app.spellcheckerLanguage], | ||
420 | userAgentPref: '', | ||
421 | ...serviceData, | ||
422 | }; | ||
423 | |||
424 | const data = skipCleanup | ||
425 | ? serviceData | ||
426 | : this._cleanUpTeamIdAndCustomUrl(recipeId, serviceData); | ||
427 | |||
428 | const response = await this.createServiceRequest.execute(recipeId, data) | ||
429 | ._promise; | ||
430 | |||
431 | this.allServicesRequest.patch(result => { | ||
432 | if (!result) return; | ||
433 | result.push(response.data); | ||
434 | }); | ||
435 | |||
436 | this.actions.settings.update({ | ||
437 | type: 'proxy', | ||
438 | data: { | ||
439 | [`${response.data.id}`]: data.proxy, | ||
440 | }, | ||
441 | }); | ||
442 | |||
443 | this.actionStatus = response.status || []; | ||
444 | |||
445 | if (redirect) { | ||
446 | this.stores.router.push('/settings/recipes'); | ||
447 | } | ||
448 | } | ||
449 | |||
450 | @action async _createFromLegacyService({ data }) { | ||
451 | const { id } = data.recipe; | ||
452 | const serviceData = {}; | ||
453 | |||
454 | if (data.name) { | ||
455 | serviceData.name = data.name; | ||
456 | } | ||
457 | |||
458 | if (data.team) { | ||
459 | if (!data.customURL) { | ||
460 | serviceData.team = data.team; | ||
461 | } else { | ||
462 | // TODO: Is this correct? | ||
463 | serviceData.customUrl = data.team; | ||
464 | } | ||
465 | } | ||
466 | |||
467 | this.actions.service.createService({ | ||
468 | recipeId: id, | ||
469 | serviceData, | ||
470 | redirect: false, | ||
471 | }); | ||
472 | } | ||
473 | |||
474 | @action async _updateService({ serviceId, serviceData, redirect = true }) { | ||
475 | const service = this.one(serviceId); | ||
476 | const data = this._cleanUpTeamIdAndCustomUrl( | ||
477 | service.recipe.id, | ||
478 | serviceData, | ||
479 | ); | ||
480 | const request = this.updateServiceRequest.execute(serviceId, data); | ||
481 | |||
482 | const newData = serviceData; | ||
483 | if (serviceData.iconFile) { | ||
484 | await request._promise; | ||
485 | |||
486 | newData.iconUrl = request.result.data.iconUrl; | ||
487 | newData.hasCustomUploadedIcon = true; | ||
488 | } | ||
489 | |||
490 | this.allServicesRequest.patch(result => { | ||
491 | if (!result) return; | ||
492 | |||
493 | // patch custom icon deletion | ||
494 | if (data.customIcon === 'delete') { | ||
495 | newData.iconUrl = ''; | ||
496 | newData.hasCustomUploadedIcon = false; | ||
497 | } | ||
498 | |||
499 | // patch custom icon url | ||
500 | if (data.customIconUrl) { | ||
501 | newData.iconUrl = data.customIconUrl; | ||
502 | } | ||
503 | |||
504 | Object.assign( | ||
505 | result.find(c => c.id === serviceId), | ||
506 | newData, | ||
507 | ); | ||
508 | }); | ||
509 | |||
510 | await request._promise; | ||
511 | this.actionStatus = request.result.status; | ||
512 | |||
513 | if (service.isEnabled) { | ||
514 | this._sendIPCMessage({ | ||
515 | serviceId, | ||
516 | channel: 'service-settings-update', | ||
517 | args: newData, | ||
518 | }); | ||
519 | } | ||
520 | |||
521 | this.actions.settings.update({ | ||
522 | type: 'proxy', | ||
523 | data: { | ||
524 | [`${serviceId}`]: data.proxy, | ||
525 | }, | ||
526 | }); | ||
527 | |||
528 | if (redirect) { | ||
529 | this.stores.router.push('/settings/services'); | ||
530 | } | ||
531 | } | ||
532 | |||
533 | @action async _deleteService({ serviceId, redirect }) { | ||
534 | const request = this.deleteServiceRequest.execute(serviceId); | ||
535 | |||
536 | if (redirect) { | ||
537 | this.stores.router.push(redirect); | ||
538 | } | ||
539 | |||
540 | this.allServicesRequest.patch(result => { | ||
541 | remove(result, c => c.id === serviceId); | ||
542 | }); | ||
543 | |||
544 | await request._promise; | ||
545 | this.actionStatus = request.result.status; | ||
546 | } | ||
547 | |||
548 | @action async _openRecipeFile({ recipe, file }) { | ||
549 | // Get directory for recipe | ||
550 | const normalDirectory = getRecipeDirectory(recipe); | ||
551 | const devDirectory = getDevRecipeDirectory(recipe); | ||
552 | let directory; | ||
553 | |||
554 | if (pathExistsSync(normalDirectory)) { | ||
555 | directory = normalDirectory; | ||
556 | } else if (pathExistsSync(devDirectory)) { | ||
557 | directory = devDirectory; | ||
558 | } else { | ||
559 | // Recipe cannot be found on drive | ||
560 | return; | ||
561 | } | ||
562 | |||
563 | // Create and open file | ||
564 | const filePath = join(directory, file); | ||
565 | if (file === 'user.js') { | ||
566 | if (!pathExistsSync(filePath)) { | ||
567 | writeFileSync( | ||
568 | filePath, | ||
569 | `module.exports = (config, Ferdium) => { | ||
570 | // Write your scripts here | ||
571 | console.log("Hello, World!", config); | ||
572 | }; | ||
573 | `, | ||
574 | ); | ||
575 | } | ||
576 | } else { | ||
577 | ensureFileSync(filePath); | ||
578 | } | ||
579 | shell.showItemInFolder(filePath); | ||
580 | } | ||
581 | |||
582 | @action async _clearCache({ serviceId }) { | ||
583 | this.clearCacheRequest.reset(); | ||
584 | const request = this.clearCacheRequest.execute(serviceId); | ||
585 | await request._promise; | ||
586 | } | ||
587 | |||
588 | @action _setActive({ serviceId, keepActiveRoute = null }) { | ||
589 | if (!keepActiveRoute) this.stores.router.push('/'); | ||
590 | const service = this.one(serviceId); | ||
591 | |||
592 | for (const s of this.all) { | ||
593 | if (s.isActive) { | ||
594 | s.lastUsed = Date.now(); | ||
595 | s.isActive = false; | ||
596 | } | ||
597 | } | ||
598 | service.isActive = true; | ||
599 | this._awake({ serviceId: service.id }); | ||
600 | |||
601 | if ( | ||
602 | this.isTodosServiceActive && | ||
603 | !this.stores.todos.settings.isFeatureEnabledByUser | ||
604 | ) { | ||
605 | this.actions.todos.toggleTodosFeatureVisibility(); | ||
606 | } | ||
607 | |||
608 | // Update list of last used services | ||
609 | this.lastUsedServices = this.lastUsedServices.filter( | ||
610 | id => id !== serviceId, | ||
611 | ); | ||
612 | this.lastUsedServices.unshift(serviceId); | ||
613 | |||
614 | this._focusActiveService(); | ||
615 | } | ||
616 | |||
617 | @action _blurActive() { | ||
618 | const service = this.active; | ||
619 | if (service) { | ||
620 | service.isActive = false; | ||
621 | } else { | ||
622 | debug('No service is active'); | ||
623 | } | ||
624 | } | ||
625 | |||
626 | @action _setActiveNext() { | ||
627 | const nextIndex = this._wrapIndex( | ||
628 | this.allDisplayed.findIndex(service => service.isActive), | ||
629 | 1, | ||
630 | this.allDisplayed.length, | ||
631 | ); | ||
632 | |||
633 | this._setActive({ serviceId: this.allDisplayed[nextIndex].id }); | ||
634 | } | ||
635 | |||
636 | @action _setActivePrev() { | ||
637 | const prevIndex = this._wrapIndex( | ||
638 | this.allDisplayed.findIndex(service => service.isActive), | ||
639 | -1, | ||
640 | this.allDisplayed.length, | ||
641 | ); | ||
642 | |||
643 | this._setActive({ serviceId: this.allDisplayed[prevIndex].id }); | ||
644 | } | ||
645 | |||
646 | @action _setUnreadMessageCount({ serviceId, count }) { | ||
647 | const service = this.one(serviceId); | ||
648 | |||
649 | service.unreadDirectMessageCount = count.direct; | ||
650 | service.unreadIndirectMessageCount = count.indirect; | ||
651 | } | ||
652 | |||
653 | @action _setDialogTitle({ serviceId, dialogTitle }) { | ||
654 | const service = this.one(serviceId); | ||
655 | |||
656 | service.dialogTitle = dialogTitle; | ||
657 | } | ||
658 | |||
659 | @action _setWebviewReference({ serviceId, webview }) { | ||
660 | const service = this.one(serviceId); | ||
661 | if (service) { | ||
662 | service.webview = webview; | ||
663 | |||
664 | if (!service.isAttached) { | ||
665 | debug('Webview is not attached, initializing'); | ||
666 | service.initializeWebViewEvents({ | ||
667 | handleIPCMessage: this.actions.service.handleIPCMessage, | ||
668 | openWindow: this.actions.service.openWindow, | ||
669 | stores: this.stores, | ||
670 | }); | ||
671 | service.initializeWebViewListener(); | ||
672 | } | ||
673 | service.isAttached = true; | ||
674 | } | ||
675 | } | ||
676 | |||
677 | @action _detachService({ service }) { | ||
678 | service.webview = null; | ||
679 | service.isAttached = false; | ||
680 | } | ||
681 | |||
682 | @action _focusService({ serviceId }) { | ||
683 | const service = this.one(serviceId); | ||
684 | |||
685 | if (service.webview) { | ||
686 | service.webview.blur(); | ||
687 | service.webview.focus(); | ||
688 | } | ||
689 | } | ||
690 | |||
691 | @action _focusActiveService(focusEvent = null) { | ||
692 | if (this.stores.user.isLoggedIn) { | ||
693 | // TODO: add checks to not focus service when router path is /settings or /auth | ||
694 | const service = this.active; | ||
695 | if (service) { | ||
696 | if (service._webview) { | ||
697 | document.title = `Ferdium - ${service.name} ${ | ||
698 | service.dialogTitle ? ` - ${service.dialogTitle}` : '' | ||
699 | } ${service._webview ? `- ${service._webview.getTitle()}` : ''}`; | ||
700 | this._focusService({ serviceId: service.id }); | ||
701 | if (this.stores.settings.app.splitMode && !focusEvent) { | ||
702 | setTimeout(() => { | ||
703 | document | ||
704 | .querySelector('.services__webview-wrapper.is-active') | ||
705 | .scrollIntoView({ | ||
706 | behavior: 'smooth', | ||
707 | block: 'end', | ||
708 | inline: 'nearest', | ||
709 | }); | ||
710 | }, 10); | ||
711 | } | ||
712 | } | ||
713 | } else { | ||
714 | debug('No service is active'); | ||
715 | } | ||
716 | } else { | ||
717 | this.allServicesRequest.invalidate(); | ||
718 | } | ||
719 | } | ||
720 | |||
721 | @action _toggleService({ serviceId }) { | ||
722 | const service = this.one(serviceId); | ||
723 | |||
724 | service.isEnabled = !service.isEnabled; | ||
725 | } | ||
726 | |||
727 | @action _handleIPCMessage({ serviceId, channel, args }) { | ||
728 | const service = this.one(serviceId); | ||
729 | |||
730 | // eslint-disable-next-line default-case | ||
731 | switch (channel) { | ||
732 | case 'hello': { | ||
733 | debug('Received hello event from', serviceId); | ||
734 | |||
735 | this._initRecipePolling(service.id); | ||
736 | this._initializeServiceRecipeInWebview(serviceId); | ||
737 | this._shareSettingsWithServiceProcess(); | ||
738 | |||
739 | break; | ||
740 | } | ||
741 | case 'alive': { | ||
742 | service.lastPollAnswer = Date.now(); | ||
743 | |||
744 | break; | ||
745 | } | ||
746 | case 'message-counts': { | ||
747 | debug(`Received unread message info from '${serviceId}'`, args[0]); | ||
748 | |||
749 | this.actions.service.setUnreadMessageCount({ | ||
750 | serviceId, | ||
751 | count: { | ||
752 | direct: args[0].direct, | ||
753 | indirect: args[0].indirect, | ||
754 | }, | ||
755 | }); | ||
756 | |||
757 | break; | ||
758 | } | ||
759 | case 'active-dialog-title': { | ||
760 | debug(`Received active dialog title from '${serviceId}'`, args[0]); | ||
761 | |||
762 | this.actions.service.setDialogTitle({ | ||
763 | serviceId, | ||
764 | dialogTitle: args[0], | ||
765 | }); | ||
766 | |||
767 | break; | ||
768 | } | ||
769 | case 'notification': { | ||
770 | const { options } = args[0]; | ||
771 | |||
772 | // Check if we are in scheduled Do-not-Disturb time | ||
773 | const { scheduledDNDEnabled, scheduledDNDStart, scheduledDNDEnd } = | ||
774 | this.stores.settings.all.app; | ||
775 | |||
776 | if ( | ||
777 | scheduledDNDEnabled && | ||
778 | isInTimeframe(scheduledDNDStart, scheduledDNDEnd) | ||
779 | ) { | ||
780 | return; | ||
781 | } | ||
782 | |||
783 | if ( | ||
784 | service.recipe.hasNotificationSound || | ||
785 | service.isMuted || | ||
786 | this.stores.settings.all.app.isAppMuted | ||
787 | ) { | ||
788 | Object.assign(options, { | ||
789 | silent: true, | ||
790 | }); | ||
791 | } | ||
792 | |||
793 | if (service.isNotificationEnabled) { | ||
794 | let title = `Notification from ${service.name}`; | ||
795 | if (!this.stores.settings.all.app.privateNotifications) { | ||
796 | options.body = typeof options.body === 'string' ? options.body : ''; | ||
797 | title = | ||
798 | typeof args[0].title === 'string' ? args[0].title : service.name; | ||
799 | } else { | ||
800 | // Remove message data from notification in private mode | ||
801 | options.body = ''; | ||
802 | options.icon = '/assets/img/notification-badge.gif'; | ||
803 | } | ||
804 | |||
805 | this.actions.app.notify({ | ||
806 | notificationId: args[0].notificationId, | ||
807 | title, | ||
808 | options, | ||
809 | serviceId, | ||
810 | }); | ||
811 | } | ||
812 | |||
813 | break; | ||
814 | } | ||
815 | case 'avatar': { | ||
816 | const url = args[0]; | ||
817 | if (service.iconUrl !== url && !service.hasCustomUploadedIcon) { | ||
818 | service.customIconUrl = url; | ||
819 | |||
820 | this.actions.service.updateService({ | ||
821 | serviceId, | ||
822 | serviceData: { | ||
823 | customIconUrl: url, | ||
824 | }, | ||
825 | redirect: false, | ||
826 | }); | ||
827 | } | ||
828 | |||
829 | break; | ||
830 | } | ||
831 | case 'new-window': { | ||
832 | const url = args[0]; | ||
833 | |||
834 | this.actions.app.openExternalUrl({ url }); | ||
835 | |||
836 | break; | ||
837 | } | ||
838 | case 'set-service-spellchecker-language': { | ||
839 | if (!args) { | ||
840 | console.warn('Did not receive locale'); | ||
841 | } else { | ||
842 | this.actions.service.updateService({ | ||
843 | serviceId, | ||
844 | serviceData: { | ||
845 | spellcheckerLanguage: args[0] === 'reset' ? '' : args[0], | ||
846 | }, | ||
847 | redirect: false, | ||
848 | }); | ||
849 | } | ||
850 | |||
851 | break; | ||
852 | } | ||
853 | case 'feature:todos': { | ||
854 | Object.assign(args[0].data, { serviceId }); | ||
855 | this.actions.todos.handleHostMessage(args[0]); | ||
856 | |||
857 | break; | ||
858 | } | ||
859 | // No default | ||
860 | } | ||
861 | } | ||
862 | |||
863 | @action _sendIPCMessage({ serviceId, channel, args }) { | ||
864 | const service = this.one(serviceId); | ||
865 | |||
866 | // Make sure the args are clean, otherwise ElectronJS can't transmit them | ||
867 | const cleanArgs = cleanseJSObject(args); | ||
868 | |||
869 | if (service.webview) { | ||
870 | service.webview.send(channel, cleanArgs); | ||
871 | } | ||
872 | } | ||
873 | |||
874 | @action _sendIPCMessageToAllServices({ channel, args }) { | ||
875 | for (const s of this.all) { | ||
876 | this.actions.service.sendIPCMessage({ | ||
877 | serviceId: s.id, | ||
878 | channel, | ||
879 | args, | ||
880 | }); | ||
881 | } | ||
882 | } | ||
883 | |||
884 | @action _openWindow({ event }) { | ||
885 | if (event.url !== 'about:blank') { | ||
886 | event.preventDefault(); | ||
887 | this.actions.app.openExternalUrl({ url: event.url }); | ||
888 | } | ||
889 | } | ||
890 | |||
891 | @action _filter({ needle }) { | ||
892 | this.filterNeedle = needle; | ||
893 | } | ||
894 | |||
895 | @action _resetFilter() { | ||
896 | this.filterNeedle = null; | ||
897 | } | ||
898 | |||
899 | @action _resetStatus() { | ||
900 | this.actionStatus = []; | ||
901 | } | ||
902 | |||
903 | @action _reload({ serviceId }) { | ||
904 | const service = this.one(serviceId); | ||
905 | if (!service.isEnabled) return; | ||
906 | |||
907 | service.resetMessageCount(); | ||
908 | service.lostRecipeConnection = false; | ||
909 | |||
910 | if (service.isTodosService) { | ||
911 | return this.actions.todos.reload(); | ||
912 | } | ||
913 | |||
914 | if (!service.webview) return; | ||
915 | return service.webview.loadURL(service.url); | ||
916 | } | ||
917 | |||
918 | @action _reloadActive() { | ||
919 | const service = this.active; | ||
920 | if (service) { | ||
921 | this._reload({ | ||
922 | serviceId: service.id, | ||
923 | }); | ||
924 | } else { | ||
925 | debug('No service is active'); | ||
926 | } | ||
927 | } | ||
928 | |||
929 | @action _reloadAll() { | ||
930 | for (const s of this.enabled) { | ||
931 | this._reload({ | ||
932 | serviceId: s.id, | ||
933 | }); | ||
934 | } | ||
935 | } | ||
936 | |||
937 | @action _reloadUpdatedServices() { | ||
938 | this._reloadAll(); | ||
939 | this.actions.ui.toggleServiceUpdatedInfoBar({ visible: false }); | ||
940 | } | ||
941 | |||
942 | @action _reorder(params) { | ||
943 | const { workspaces } = this.stores; | ||
944 | if (workspaces.isAnyWorkspaceActive) { | ||
945 | workspaces.reorderServicesOfActiveWorkspace(params); | ||
946 | } else { | ||
947 | this._reorderService(params); | ||
948 | } | ||
949 | } | ||
950 | |||
951 | @action _reorderService({ oldIndex, newIndex }) { | ||
952 | const { showDisabledServices } = this.stores.settings.all.app; | ||
953 | const oldEnabledSortIndex = showDisabledServices | ||
954 | ? oldIndex | ||
955 | : this.all.indexOf(this.enabled[oldIndex]); | ||
956 | const newEnabledSortIndex = showDisabledServices | ||
957 | ? newIndex | ||
958 | : this.all.indexOf(this.enabled[newIndex]); | ||
959 | |||
960 | this.all.splice( | ||
961 | newEnabledSortIndex, | ||
962 | 0, | ||
963 | this.all.splice(oldEnabledSortIndex, 1)[0], | ||
964 | ); | ||
965 | |||
966 | const services = {}; | ||
967 | // TODO: simplify this | ||
968 | for (const [index] of this.all.entries()) { | ||
969 | services[this.all[index].id] = index; | ||
970 | } | ||
971 | |||
972 | this.reorderServicesRequest.execute(services); | ||
973 | this.allServicesRequest.patch(data => { | ||
974 | for (const s of data) { | ||
975 | const service = s; | ||
976 | |||
977 | service.order = services[s.id]; | ||
978 | } | ||
979 | }); | ||
980 | } | ||
981 | |||
982 | @action _toggleNotifications({ serviceId }) { | ||
983 | const service = this.one(serviceId); | ||
984 | |||
985 | this.actions.service.updateService({ | ||
986 | serviceId, | ||
987 | serviceData: { | ||
988 | isNotificationEnabled: !service.isNotificationEnabled, | ||
989 | }, | ||
990 | redirect: false, | ||
991 | }); | ||
992 | } | ||
993 | |||
994 | @action _toggleAudio({ serviceId }) { | ||
995 | const service = this.one(serviceId); | ||
996 | |||
997 | this.actions.service.updateService({ | ||
998 | serviceId, | ||
999 | serviceData: { | ||
1000 | isMuted: !service.isMuted, | ||
1001 | }, | ||
1002 | redirect: false, | ||
1003 | }); | ||
1004 | } | ||
1005 | |||
1006 | @action _toggleDarkMode({ serviceId }) { | ||
1007 | const service = this.one(serviceId); | ||
1008 | |||
1009 | this.actions.service.updateService({ | ||
1010 | serviceId, | ||
1011 | serviceData: { | ||
1012 | isDarkModeEnabled: !service.isDarkModeEnabled, | ||
1013 | }, | ||
1014 | redirect: false, | ||
1015 | }); | ||
1016 | } | ||
1017 | |||
1018 | @action _openDevTools({ serviceId }) { | ||
1019 | const service = this.one(serviceId); | ||
1020 | if (service.isTodosService) { | ||
1021 | this.actions.todos.openDevTools(); | ||
1022 | } else if (service.webview) { | ||
1023 | service.webview.openDevTools(); | ||
1024 | } | ||
1025 | } | ||
1026 | |||
1027 | @action _openDevToolsForActiveService() { | ||
1028 | const service = this.active; | ||
1029 | |||
1030 | if (service) { | ||
1031 | this._openDevTools({ serviceId: service.id }); | ||
1032 | } else { | ||
1033 | debug('No service is active'); | ||
1034 | } | ||
1035 | } | ||
1036 | |||
1037 | @action _hibernate({ serviceId }) { | ||
1038 | const service = this.one(serviceId); | ||
1039 | if (!service.canHibernate) { | ||
1040 | return; | ||
1041 | } | ||
1042 | |||
1043 | debug(`Hibernate ${service.name}`); | ||
1044 | |||
1045 | service.isHibernationRequested = true; | ||
1046 | service.lastHibernated = Date.now(); | ||
1047 | } | ||
1048 | |||
1049 | @action _awake({ serviceId, automatic }) { | ||
1050 | const now = Date.now(); | ||
1051 | const service = this.one(serviceId); | ||
1052 | const automaticTag = automatic ? ' automatically ' : ' '; | ||
1053 | debug( | ||
1054 | `Waking up${automaticTag}from service hibernation for ${service.name}`, | ||
1055 | ); | ||
1056 | |||
1057 | if (automatic) { | ||
1058 | // if this is an automatic wake up, use the wakeUpHibernationStrategy | ||
1059 | // which sets the lastUsed time to an offset from now rather than to now. | ||
1060 | // Also add an optional random splay to desync the wakeups and | ||
1061 | // potentially reduce load. | ||
1062 | // | ||
1063 | // offsetNow = now - (hibernationStrategy - wakeUpHibernationStrategy) | ||
1064 | // | ||
1065 | // if wUHS = hS = 60, offsetNow = now. hibernation again in 60 seconds. | ||
1066 | // | ||
1067 | // if wUHS = 20 and hS = 60, offsetNow = now - 40. hibernation again in | ||
1068 | // 20 seconds. | ||
1069 | // | ||
1070 | // possibly also include splay in wUHS before subtracting from hS. | ||
1071 | // | ||
1072 | const mainStrategy = this.stores.settings.all.app.hibernationStrategy; | ||
1073 | let strategy = this.stores.settings.all.app.wakeUpHibernationStrategy; | ||
1074 | debug(`wakeUpHibernationStrategy = ${strategy}`); | ||
1075 | debug(`hibernationStrategy = ${mainStrategy}`); | ||
1076 | if (!strategy || strategy < 1) { | ||
1077 | strategy = this.stores.settings.all.app.hibernationStrategy; | ||
1078 | } | ||
1079 | let splay = 0; | ||
1080 | // Add splay. This will keep the service awake a little longer. | ||
1081 | if ( | ||
1082 | this.stores.settings.all.app.wakeUpHibernationSplay && | ||
1083 | Math.random() >= 0.5 | ||
1084 | ) { | ||
1085 | // Add 10 additional seconds 50% of the time. | ||
1086 | splay = 10; | ||
1087 | debug('Added splay'); | ||
1088 | } else { | ||
1089 | debug('skipping splay'); | ||
1090 | } | ||
1091 | // wake up again in strategy + splay seconds instead of mainStrategy seconds. | ||
1092 | service.lastUsed = now - ms(`${mainStrategy - (strategy + splay)}s`); | ||
1093 | } else { | ||
1094 | service.lastUsed = now; | ||
1095 | } | ||
1096 | debug( | ||
1097 | `Setting service.lastUsed to ${service.lastUsed} (${ | ||
1098 | (now - service.lastUsed) / 1000 | ||
1099 | }s ago)`, | ||
1100 | ); | ||
1101 | service.isHibernationRequested = false; | ||
1102 | service.lastHibernated = null; | ||
1103 | } | ||
1104 | |||
1105 | @action _resetLastPollTimer({ serviceId = null }) { | ||
1106 | debug( | ||
1107 | `Reset last poll timer for ${ | ||
1108 | serviceId ? `service: "${serviceId}"` : 'all services' | ||
1109 | }`, | ||
1110 | ); | ||
1111 | |||
1112 | // eslint-disable-next-line unicorn/consistent-function-scoping | ||
1113 | const resetTimer = service => { | ||
1114 | service.lastPollAnswer = Date.now(); | ||
1115 | service.lastPoll = Date.now(); | ||
1116 | }; | ||
1117 | |||
1118 | if (!serviceId) { | ||
1119 | for (const service of this.allDisplayed) resetTimer(service); | ||
1120 | } else { | ||
1121 | const service = this.one(serviceId); | ||
1122 | if (service) { | ||
1123 | resetTimer(service); | ||
1124 | } | ||
1125 | } | ||
1126 | } | ||
1127 | |||
1128 | // Reactions | ||
1129 | _focusServiceReaction() { | ||
1130 | const service = this.active; | ||
1131 | if (service) { | ||
1132 | this.actions.service.focusService({ serviceId: service.id }); | ||
1133 | document.title = `Ferdium - ${service.name} ${ | ||
1134 | service.dialogTitle ? ` - ${service.dialogTitle}` : '' | ||
1135 | } ${service._webview ? `- ${service._webview.getTitle()}` : ''}`; | ||
1136 | } else { | ||
1137 | debug('No service is active'); | ||
1138 | } | ||
1139 | } | ||
1140 | |||
1141 | _saveActiveService() { | ||
1142 | const service = this.active; | ||
1143 | if (service) { | ||
1144 | this.actions.settings.update({ | ||
1145 | type: 'service', | ||
1146 | data: { | ||
1147 | activeService: service.id, | ||
1148 | }, | ||
1149 | }); | ||
1150 | } else { | ||
1151 | debug('No service is active'); | ||
1152 | } | ||
1153 | } | ||
1154 | |||
1155 | _mapActiveServiceToServiceModelReaction() { | ||
1156 | const { activeService } = this.stores.settings.all.service; | ||
1157 | if (this.allDisplayed.length > 0) { | ||
1158 | this.allDisplayed.map(service => | ||
1159 | Object.assign(service, { | ||
1160 | isActive: activeService | ||
1161 | ? activeService === service.id | ||
1162 | : this.allDisplayed[0].id === service.id, | ||
1163 | }), | ||
1164 | ); | ||
1165 | } | ||
1166 | } | ||
1167 | |||
1168 | _getUnreadMessageCountReaction() { | ||
1169 | const { showMessageBadgeWhenMuted } = this.stores.settings.all.app; | ||
1170 | const { showMessageBadgesEvenWhenMuted } = this.stores.ui; | ||
1171 | |||
1172 | const unreadDirectMessageCount = this.allDisplayed | ||
1173 | .filter( | ||
1174 | s => | ||
1175 | (showMessageBadgeWhenMuted || s.isNotificationEnabled) && | ||
1176 | showMessageBadgesEvenWhenMuted && | ||
1177 | s.isBadgeEnabled, | ||
1178 | ) | ||
1179 | .map(s => s.unreadDirectMessageCount) | ||
1180 | .reduce((a, b) => a + b, 0); | ||
1181 | |||
1182 | const unreadIndirectMessageCount = this.allDisplayed | ||
1183 | .filter( | ||
1184 | s => | ||
1185 | showMessageBadgeWhenMuted && | ||
1186 | showMessageBadgesEvenWhenMuted && | ||
1187 | s.isBadgeEnabled && | ||
1188 | s.isIndirectMessageBadgeEnabled, | ||
1189 | ) | ||
1190 | .map(s => s.unreadIndirectMessageCount) | ||
1191 | .reduce((a, b) => a + b, 0); | ||
1192 | |||
1193 | // We can't just block this earlier, otherwise the mobx reaction won't be aware of the vars to watch in some cases | ||
1194 | if (showMessageBadgesEvenWhenMuted) { | ||
1195 | this.actions.app.setBadge({ | ||
1196 | unreadDirectMessageCount, | ||
1197 | unreadIndirectMessageCount, | ||
1198 | }); | ||
1199 | } | ||
1200 | } | ||
1201 | |||
1202 | _logoutReaction() { | ||
1203 | if (!this.stores.user.isLoggedIn) { | ||
1204 | this.actions.settings.remove({ | ||
1205 | type: 'service', | ||
1206 | key: 'activeService', | ||
1207 | }); | ||
1208 | this.allServicesRequest.invalidate().reset(); | ||
1209 | } | ||
1210 | } | ||
1211 | |||
1212 | _handleMuteSettings() { | ||
1213 | const { enabled } = this; | ||
1214 | const { isAppMuted } = this.stores.settings.app; | ||
1215 | |||
1216 | for (const service of enabled) { | ||
1217 | const { isAttached } = service; | ||
1218 | const isMuted = isAppMuted || service.isMuted; | ||
1219 | |||
1220 | if (isAttached && service.webview) { | ||
1221 | service.webview.audioMuted = isMuted; | ||
1222 | } | ||
1223 | } | ||
1224 | } | ||
1225 | |||
1226 | _shareSettingsWithServiceProcess() { | ||
1227 | const settings = { | ||
1228 | ...this.stores.settings.app, | ||
1229 | isDarkThemeActive: this.stores.ui.isDarkThemeActive, | ||
1230 | }; | ||
1231 | this.actions.service.sendIPCMessageToAllServices({ | ||
1232 | channel: 'settings-update', | ||
1233 | args: settings, | ||
1234 | }); | ||
1235 | } | ||
1236 | |||
1237 | _cleanUpTeamIdAndCustomUrl(recipeId, data) { | ||
1238 | const serviceData = data; | ||
1239 | const recipe = this.stores.recipes.one(recipeId); | ||
1240 | |||
1241 | if (!recipe) return; | ||
1242 | |||
1243 | if ( | ||
1244 | recipe.hasTeamId && | ||
1245 | recipe.hasCustomUrl && | ||
1246 | data.team && | ||
1247 | data.customUrl | ||
1248 | ) { | ||
1249 | delete serviceData.team; | ||
1250 | } | ||
1251 | |||
1252 | return serviceData; | ||
1253 | } | ||
1254 | |||
1255 | _checkForActiveService() { | ||
1256 | if ( | ||
1257 | !this.stores.router.location || | ||
1258 | this.stores.router.location.pathname.includes('auth/signup') | ||
1259 | ) { | ||
1260 | return; | ||
1261 | } | ||
1262 | |||
1263 | if ( | ||
1264 | this.allDisplayed.findIndex(service => service.isActive) === -1 && | ||
1265 | this.allDisplayed.length > 0 | ||
1266 | ) { | ||
1267 | debug('No active service found, setting active service to index 0'); | ||
1268 | |||
1269 | this._setActive({ serviceId: this.allDisplayed[0].id }); | ||
1270 | } | ||
1271 | } | ||
1272 | |||
1273 | // Helper | ||
1274 | _initializeServiceRecipeInWebview(serviceId) { | ||
1275 | const service = this.one(serviceId); | ||
1276 | |||
1277 | if (service.webview) { | ||
1278 | // We need to completely clone the object, otherwise Electron won't be able to send the object via IPC | ||
1279 | const shareWithWebview = cleanseJSObject(service.shareWithWebview); | ||
1280 | |||
1281 | debug('Initialize recipe', service.recipe.id, service.name); | ||
1282 | service.webview.send( | ||
1283 | 'initialize-recipe', | ||
1284 | { | ||
1285 | ...shareWithWebview, | ||
1286 | franzVersion: ferdiumVersion, | ||
1287 | }, | ||
1288 | service.recipe, | ||
1289 | ); | ||
1290 | } | ||
1291 | } | ||
1292 | |||
1293 | _initRecipePolling(serviceId) { | ||
1294 | const service = this.one(serviceId); | ||
1295 | |||
1296 | const delay = ms('2s'); | ||
1297 | |||
1298 | if (service) { | ||
1299 | if (service.timer !== null) { | ||
1300 | clearTimeout(service.timer); | ||
1301 | } | ||
1302 | |||
1303 | const loop = () => { | ||
1304 | if (!service.webview) return; | ||
1305 | |||
1306 | service.webview.send('poll'); | ||
1307 | |||
1308 | service.timer = setTimeout(loop, delay); | ||
1309 | service.lastPoll = Date.now(); | ||
1310 | }; | ||
1311 | |||
1312 | loop(); | ||
1313 | } | ||
1314 | } | ||
1315 | |||
1316 | _wrapIndex(index, delta, size) { | ||
1317 | return (((index + delta) % size) + size) % size; | ||
1318 | } | ||
1319 | } | ||