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