diff options
Diffstat (limited to 'src/stores/ServicesStore.js')
-rw-r--r-- | src/stores/ServicesStore.js | 133 |
1 files changed, 109 insertions, 24 deletions
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js index 65e68e4d7..934a8a6e0 100644 --- a/src/stores/ServicesStore.js +++ b/src/stores/ServicesStore.js | |||
@@ -1,23 +1,27 @@ | |||
1 | import { shell, remote } from 'electron'; | ||
1 | import { | 2 | import { |
2 | action, | 3 | action, |
3 | reaction, | 4 | reaction, |
4 | computed, | 5 | computed, |
5 | observable, | 6 | observable, |
6 | } from 'mobx'; | 7 | } from 'mobx'; |
7 | import { debounce, remove } from 'lodash'; | 8 | import { remove } from 'lodash'; |
8 | import ms from 'ms'; | 9 | import ms from 'ms'; |
9 | import { remote } from 'electron'; | 10 | import fs from 'fs-extra'; |
11 | import path from 'path'; | ||
10 | 12 | ||
11 | import Store from './lib/Store'; | 13 | import Store from './lib/Store'; |
12 | import Request from './lib/Request'; | 14 | import Request from './lib/Request'; |
13 | import CachedRequest from './lib/CachedRequest'; | 15 | import CachedRequest from './lib/CachedRequest'; |
14 | import { matchRoute } from '../helpers/routing-helpers'; | 16 | import { matchRoute } from '../helpers/routing-helpers'; |
15 | import { gaEvent, statsEvent } from '../lib/analytics'; | 17 | import { isInTimeframe } from '../helpers/schedule-helpers'; |
18 | import { getRecipeDirectory, getDevRecipeDirectory } from '../helpers/recipe-helpers'; | ||
16 | import { workspaceStore } from '../features/workspaces'; | 19 | import { workspaceStore } from '../features/workspaces'; |
17 | import { serviceLimitStore } from '../features/serviceLimit'; | 20 | import { serviceLimitStore } from '../features/serviceLimit'; |
18 | import { RESTRICTION_TYPES } from '../models/Service'; | 21 | import { RESTRICTION_TYPES } from '../models/Service'; |
22 | import { KEEP_WS_LOADED_USID } from '../config'; | ||
19 | 23 | ||
20 | const debug = require('debug')('Franz:ServiceStore'); | 24 | const debug = require('debug')('Ferdi:ServiceStore'); |
21 | 25 | ||
22 | const { app } = remote; | 26 | const { app } = remote; |
23 | 27 | ||
@@ -36,6 +40,11 @@ export default class ServicesStore extends Store { | |||
36 | 40 | ||
37 | @observable filterNeedle = null; | 41 | @observable filterNeedle = null; |
38 | 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 | |||
39 | constructor(...args) { | 48 | constructor(...args) { |
40 | super(...args); | 49 | super(...args); |
41 | 50 | ||
@@ -49,6 +58,7 @@ export default class ServicesStore extends Store { | |||
49 | this.actions.service.createFromLegacyService.listen(this._createFromLegacyService.bind(this)); | 58 | this.actions.service.createFromLegacyService.listen(this._createFromLegacyService.bind(this)); |
50 | this.actions.service.updateService.listen(this._updateService.bind(this)); | 59 | this.actions.service.updateService.listen(this._updateService.bind(this)); |
51 | this.actions.service.deleteService.listen(this._deleteService.bind(this)); | 60 | this.actions.service.deleteService.listen(this._deleteService.bind(this)); |
61 | this.actions.service.openDarkmodeCss.listen(this._openDarkmodeCss.bind(this)); | ||
52 | this.actions.service.clearCache.listen(this._clearCache.bind(this)); | 62 | this.actions.service.clearCache.listen(this._clearCache.bind(this)); |
53 | this.actions.service.setWebviewReference.listen(this._setWebviewReference.bind(this)); | 63 | this.actions.service.setWebviewReference.listen(this._setWebviewReference.bind(this)); |
54 | this.actions.service.detachService.listen(this._detachService.bind(this)); | 64 | this.actions.service.detachService.listen(this._detachService.bind(this)); |
@@ -72,6 +82,7 @@ export default class ServicesStore extends Store { | |||
72 | this.actions.service.toggleAudio.listen(this._toggleAudio.bind(this)); | 82 | this.actions.service.toggleAudio.listen(this._toggleAudio.bind(this)); |
73 | this.actions.service.openDevTools.listen(this._openDevTools.bind(this)); | 83 | this.actions.service.openDevTools.listen(this._openDevTools.bind(this)); |
74 | this.actions.service.openDevToolsForActiveService.listen(this._openDevToolsForActiveService.bind(this)); | 84 | this.actions.service.openDevToolsForActiveService.listen(this._openDevToolsForActiveService.bind(this)); |
85 | this.actions.service.setHibernation.listen(this._setHibernation.bind(this)); | ||
75 | 86 | ||
76 | this.registerReactions([ | 87 | this.registerReactions([ |
77 | this._focusServiceReaction.bind(this), | 88 | this._focusServiceReaction.bind(this), |
@@ -98,6 +109,16 @@ export default class ServicesStore extends Store { | |||
98 | () => this.stores.settings.app.spellcheckerLanguage, | 109 | () => this.stores.settings.app.spellcheckerLanguage, |
99 | () => this._shareSettingsWithServiceProcess(), | 110 | () => this._shareSettingsWithServiceProcess(), |
100 | ); | 111 | ); |
112 | |||
113 | reaction( | ||
114 | () => this.stores.settings.app.darkMode, | ||
115 | () => this._shareSettingsWithServiceProcess(), | ||
116 | ); | ||
117 | |||
118 | reaction( | ||
119 | () => this.stores.settings.app.universalDarkMode, | ||
120 | () => this._shareSettingsWithServiceProcess(), | ||
121 | ); | ||
101 | } | 122 | } |
102 | 123 | ||
103 | @computed get all() { | 124 | @computed get all() { |
@@ -128,7 +149,35 @@ export default class ServicesStore extends Store { | |||
128 | const { keepAllWorkspacesLoaded } = this.stores.workspaces.settings; | 149 | const { keepAllWorkspacesLoaded } = this.stores.workspaces.settings; |
129 | const services = this.allServicesRequest.execute().result || []; | 150 | const services = this.allServicesRequest.execute().result || []; |
130 | const filteredServices = showDisabledServices ? services : services.filter(service => service.isEnabled); | 151 | const filteredServices = showDisabledServices ? services : services.filter(service => service.isEnabled); |
131 | return keepAllWorkspacesLoaded ? filteredServices : workspaceStore.filterServicesByActiveWorkspace(filteredServices); | 152 | |
153 | let displayedServices; | ||
154 | if (keepAllWorkspacesLoaded) { | ||
155 | // Keep all enabled services loaded | ||
156 | displayedServices = filteredServices; | ||
157 | } else { | ||
158 | // Keep all services in current workspace loaded | ||
159 | displayedServices = workspaceStore.filterServicesByActiveWorkspace(filteredServices); | ||
160 | |||
161 | // Keep all services active in workspaces that should be kept loaded | ||
162 | for (const workspace of this.stores.workspaces.workspaces) { | ||
163 | // Check if workspace needs to be kept loaded | ||
164 | if (workspace.services.includes(KEEP_WS_LOADED_USID)) { | ||
165 | // Get services for workspace | ||
166 | const serviceIDs = workspace.services.filter(i => i !== KEEP_WS_LOADED_USID); | ||
167 | const wsServices = filteredServices.filter(service => serviceIDs.includes(service.id)); | ||
168 | |||
169 | displayedServices = [ | ||
170 | ...displayedServices, | ||
171 | ...wsServices, | ||
172 | ]; | ||
173 | } | ||
174 | } | ||
175 | |||
176 | // Make sure every service is in the list only once | ||
177 | displayedServices = displayedServices.filter((v, i, a) => a.indexOf(v) === i); | ||
178 | } | ||
179 | |||
180 | return displayedServices; | ||
132 | } | 181 | } |
133 | 182 | ||
134 | @computed get filtered() { | 183 | @computed get filtered() { |
@@ -185,7 +234,6 @@ export default class ServicesStore extends Store { | |||
185 | 234 | ||
186 | if (redirect) { | 235 | if (redirect) { |
187 | this.stores.router.push('/settings/recipes'); | 236 | this.stores.router.push('/settings/recipes'); |
188 | gaEvent('Service', 'create', recipeId); | ||
189 | } | 237 | } |
190 | } | 238 | } |
191 | 239 | ||
@@ -262,7 +310,6 @@ export default class ServicesStore extends Store { | |||
262 | 310 | ||
263 | if (redirect) { | 311 | if (redirect) { |
264 | this.stores.router.push('/settings/services'); | 312 | this.stores.router.push('/settings/services'); |
265 | gaEvent('Service', 'update', service.recipe.id); | ||
266 | } | 313 | } |
267 | } | 314 | } |
268 | 315 | ||
@@ -277,19 +324,35 @@ export default class ServicesStore extends Store { | |||
277 | remove(result, c => c.id === serviceId); | 324 | remove(result, c => c.id === serviceId); |
278 | }); | 325 | }); |
279 | 326 | ||
280 | const service = this.one(serviceId); | ||
281 | |||
282 | await request._promise; | 327 | await request._promise; |
283 | this.actionStatus = request.result.status; | 328 | this.actionStatus = request.result.status; |
329 | } | ||
330 | |||
331 | @action async _openDarkmodeCss({ recipe }) { | ||
332 | // Get directory for recipe | ||
333 | const normalDirectory = getRecipeDirectory(recipe); | ||
334 | const devDirectory = getDevRecipeDirectory(recipe); | ||
335 | let directory; | ||
336 | |||
337 | if (await fs.pathExists(normalDirectory)) { | ||
338 | directory = normalDirectory; | ||
339 | } else if (await fs.pathExists(devDirectory)) { | ||
340 | directory = devDirectory; | ||
341 | } else { | ||
342 | // Recipe cannot be found on drive | ||
343 | return; | ||
344 | } | ||
284 | 345 | ||
285 | gaEvent('Service', 'delete', service.recipe.id); | 346 | // Create and open darkmode.css |
347 | const file = path.join(directory, 'darkmode.css'); | ||
348 | await fs.ensureFile(file); | ||
349 | shell.showItemInFolder(file); | ||
286 | } | 350 | } |
287 | 351 | ||
288 | @action async _clearCache({ serviceId }) { | 352 | @action async _clearCache({ serviceId }) { |
289 | this.clearCacheRequest.reset(); | 353 | this.clearCacheRequest.reset(); |
290 | const request = this.clearCacheRequest.execute(serviceId); | 354 | const request = this.clearCacheRequest.execute(serviceId); |
291 | await request._promise; | 355 | await request._promise; |
292 | gaEvent('Service', 'clear cache'); | ||
293 | } | 356 | } |
294 | 357 | ||
295 | @action _setActive({ serviceId, keepActiveRoute }) { | 358 | @action _setActive({ serviceId, keepActiveRoute }) { |
@@ -301,7 +364,9 @@ export default class ServicesStore extends Store { | |||
301 | }); | 364 | }); |
302 | service.isActive = true; | 365 | service.isActive = true; |
303 | 366 | ||
304 | statsEvent('activate-service', service.recipe.id); | 367 | // Update list of last used services |
368 | this.lastUsedServices = this.lastUsedServices.filter(id => id !== serviceId); | ||
369 | this.lastUsedServices.unshift(serviceId); | ||
305 | 370 | ||
306 | this._focusActiveService(); | 371 | this._focusActiveService(); |
307 | } | 372 | } |
@@ -406,7 +471,19 @@ export default class ServicesStore extends Store { | |||
406 | }, | 471 | }, |
407 | }); | 472 | }); |
408 | } else if (channel === 'notification') { | 473 | } else if (channel === 'notification') { |
409 | const options = args[0].options; | 474 | const { options } = args[0]; |
475 | |||
476 | // Check if we are in scheduled Do-not-Disturb time | ||
477 | const { | ||
478 | scheduledDNDEnabled, | ||
479 | scheduledDNDStart, | ||
480 | scheduledDNDEnd, | ||
481 | } = this.stores.settings.all.app; | ||
482 | |||
483 | if (scheduledDNDEnabled && isInTimeframe(scheduledDNDStart, scheduledDNDEnd)) { | ||
484 | return; | ||
485 | } | ||
486 | |||
410 | if (service.recipe.hasNotificationSound || service.isMuted || this.stores.settings.all.app.isAppMuted) { | 487 | if (service.recipe.hasNotificationSound || service.isMuted || this.stores.settings.all.app.isAppMuted) { |
411 | Object.assign(options, { | 488 | Object.assign(options, { |
412 | silent: true, | 489 | silent: true, |
@@ -414,8 +491,17 @@ export default class ServicesStore extends Store { | |||
414 | } | 491 | } |
415 | 492 | ||
416 | if (service.isNotificationEnabled) { | 493 | if (service.isNotificationEnabled) { |
417 | const title = typeof args[0].title === 'string' ? args[0].title : service.name; | 494 | let title = `Notification from ${service.name}`; |
418 | options.body = typeof options.body === 'string' ? options.body : ''; | 495 | if (!this.stores.settings.all.app.privateNotifications) { |
496 | options.body = typeof options.body === 'string' ? options.body : ''; | ||
497 | title = typeof args[0].title === 'string' ? args[0].title : service.name; | ||
498 | } else { | ||
499 | // Remove message data from notification in private mode | ||
500 | options.body = ''; | ||
501 | options.icon = '/assets/img/notification-badge.gif'; | ||
502 | } | ||
503 | |||
504 | console.log(title, options); | ||
419 | 505 | ||
420 | this.actions.app.notify({ | 506 | this.actions.app.notify({ |
421 | notificationId: args[0].notificationId, | 507 | notificationId: args[0].notificationId, |
@@ -533,7 +619,7 @@ export default class ServicesStore extends Store { | |||
533 | } | 619 | } |
534 | 620 | ||
535 | @action _reorderService({ oldIndex, newIndex }) { | 621 | @action _reorderService({ oldIndex, newIndex }) { |
536 | const showDisabledServices = this.stores.settings.all.app.showDisabledServices; | 622 | const { showDisabledServices } = this.stores.settings.all.app; |
537 | const oldEnabledSortIndex = showDisabledServices ? oldIndex : this.all.indexOf(this.enabled[oldIndex]); | 623 | const oldEnabledSortIndex = showDisabledServices ? oldIndex : this.all.indexOf(this.enabled[oldIndex]); |
538 | const newEnabledSortIndex = showDisabledServices ? newIndex : this.all.indexOf(this.enabled[newIndex]); | 624 | const newEnabledSortIndex = showDisabledServices ? newIndex : this.all.indexOf(this.enabled[newIndex]); |
539 | 625 | ||
@@ -552,8 +638,6 @@ export default class ServicesStore extends Store { | |||
552 | service.order = services[s.id]; | 638 | service.order = services[s.id]; |
553 | }); | 639 | }); |
554 | }); | 640 | }); |
555 | |||
556 | this._reorderAnalytics(); | ||
557 | } | 641 | } |
558 | 642 | ||
559 | @action _toggleNotifications({ serviceId }) { | 643 | @action _toggleNotifications({ serviceId }) { |
@@ -598,6 +682,11 @@ export default class ServicesStore extends Store { | |||
598 | } | 682 | } |
599 | } | 683 | } |
600 | 684 | ||
685 | @action _setHibernation({ serviceId, hibernating }) { | ||
686 | const service = this.one(serviceId); | ||
687 | service.isHibernating = hibernating; | ||
688 | } | ||
689 | |||
601 | // Reactions | 690 | // Reactions |
602 | _focusServiceReaction() { | 691 | _focusServiceReaction() { |
603 | const service = this.active; | 692 | const service = this.active; |
@@ -629,8 +718,8 @@ export default class ServicesStore extends Store { | |||
629 | } | 718 | } |
630 | 719 | ||
631 | _getUnreadMessageCountReaction() { | 720 | _getUnreadMessageCountReaction() { |
632 | const showMessageBadgeWhenMuted = this.stores.settings.all.app.showMessageBadgeWhenMuted; | 721 | const { showMessageBadgeWhenMuted } = this.stores.settings.all.app; |
633 | const showMessageBadgesEvenWhenMuted = this.stores.ui.showMessageBadgesEvenWhenMuted; | 722 | const { showMessageBadgesEvenWhenMuted } = this.stores.ui; |
634 | 723 | ||
635 | const unreadDirectMessageCount = this.allDisplayed | 724 | const unreadDirectMessageCount = this.allDisplayed |
636 | .filter(s => (showMessageBadgeWhenMuted || s.isNotificationEnabled) && showMessageBadgesEvenWhenMuted && s.isBadgeEnabled) | 725 | .filter(s => (showMessageBadgeWhenMuted || s.isNotificationEnabled) && showMessageBadgesEvenWhenMuted && s.isBadgeEnabled) |
@@ -759,10 +848,6 @@ export default class ServicesStore extends Store { | |||
759 | } | 848 | } |
760 | } | 849 | } |
761 | 850 | ||
762 | _reorderAnalytics = debounce(() => { | ||
763 | gaEvent('Service', 'order'); | ||
764 | }, ms('5s')); | ||
765 | |||
766 | _wrapIndex(index, delta, size) { | 851 | _wrapIndex(index, delta, size) { |
767 | return (((index + delta) % size) + size) % size; | 852 | return (((index + delta) % size) + size) % size; |
768 | } | 853 | } |