aboutsummaryrefslogtreecommitdiffstats
path: root/src/stores/ServicesStore.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/stores/ServicesStore.js')
-rw-r--r--src/stores/ServicesStore.js503
1 files changed, 503 insertions, 0 deletions
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
new file mode 100644
index 000000000..77d2e7da4
--- /dev/null
+++ b/src/stores/ServicesStore.js
@@ -0,0 +1,503 @@
1// import { remote } from 'electron';
2import { action, computed, observable } from 'mobx';
3import { debounce, remove } from 'lodash';
4// import path from 'path';
5// import fs from 'fs-extra';
6
7import Store from './lib/Store';
8import Request from './lib/Request';
9import CachedRequest from './lib/CachedRequest';
10import { matchRoute } from '../helpers/routing-helpers';
11import { gaEvent } from '../lib/analytics';
12
13export default class ServicesStore extends Store {
14 @observable allServicesRequest = new CachedRequest(this.api.services, 'all');
15 @observable createServiceRequest = new Request(this.api.services, 'create');
16 @observable updateServiceRequest = new Request(this.api.services, 'update');
17 @observable reorderServicesRequest = new Request(this.api.services, 'reorder');
18 @observable deleteServiceRequest = new Request(this.api.services, 'delete');
19
20 @observable filterNeedle = null;
21
22 constructor(...args) {
23 super(...args);
24
25 // Register action handlers
26 this.actions.service.setActive.listen(this._setActive.bind(this));
27 this.actions.service.showAddServiceInterface.listen(this._showAddServiceInterface.bind(this));
28 this.actions.service.createService.listen(this._createService.bind(this));
29 this.actions.service.createFromLegacyService.listen(this._createFromLegacyService.bind(this));
30 this.actions.service.updateService.listen(this._updateService.bind(this));
31 this.actions.service.deleteService.listen(this._deleteService.bind(this));
32 this.actions.service.setWebviewReference.listen(this._setWebviewReference.bind(this));
33 this.actions.service.focusService.listen(this._focusService.bind(this));
34 this.actions.service.focusActiveService.listen(this._focusActiveService.bind(this));
35 this.actions.service.toggleService.listen(this._toggleService.bind(this));
36 this.actions.service.handleIPCMessage.listen(this._handleIPCMessage.bind(this));
37 this.actions.service.sendIPCMessage.listen(this._sendIPCMessage.bind(this));
38 this.actions.service.setUnreadMessageCount.listen(this._setUnreadMessageCount.bind(this));
39 this.actions.service.openWindow.listen(this._openWindow.bind(this));
40 this.actions.service.filter.listen(this._filter.bind(this));
41 this.actions.service.resetFilter.listen(this._resetFilter.bind(this));
42 this.actions.service.reload.listen(this._reload.bind(this));
43 this.actions.service.reloadActive.listen(this._reloadActive.bind(this));
44 this.actions.service.reloadAll.listen(this._reloadAll.bind(this));
45 this.actions.service.reloadUpdatedServices.listen(this._reloadUpdatedServices.bind(this));
46 this.actions.service.reorder.listen(this._reorder.bind(this));
47 this.actions.service.toggleNotifications.listen(this._toggleNotifications.bind(this));
48 this.actions.service.openDevTools.listen(this._openDevTools.bind(this));
49 this.actions.service.openDevToolsForActiveService.listen(this._openDevToolsForActiveService.bind(this));
50
51 this.registerReactions([
52 this._focusServiceReaction.bind(this),
53 this._getUnreadMessageCountReaction.bind(this),
54 this._mapActiveServiceToServiceModelReaction.bind(this),
55 this._saveActiveService.bind(this),
56 this._logoutReaction.bind(this),
57 ]);
58
59 // Just bind this
60 this._initializeServiceRecipeInWebview.bind(this);
61 }
62
63 @computed get all() {
64 if (this.stores.user.isLoggedIn) {
65 const services = this.allServicesRequest.execute().result;
66 if (services) {
67 return observable(services.slice().slice().sort((a, b) => a.order - b.order));
68 }
69 }
70
71 return [];
72 }
73
74 @computed get enabled() {
75 return this.all.filter(service => service.isEnabled);
76 }
77
78 @computed get filtered() {
79 return this.all.filter(service => service.name.toLowerCase().includes(this.filterNeedle.toLowerCase()));
80 }
81
82 @computed get active() {
83 return this.all.find(service => service.isActive);
84 }
85
86 @computed get activeSettings() {
87 const match = matchRoute('/settings/services/edit/:id', this.stores.router.location.pathname);
88 if (match) {
89 const activeService = this.one(match.id);
90 if (activeService) {
91 return activeService;
92 }
93
94 console.warn('Service not available');
95 }
96
97 return null;
98 }
99
100 one(id) {
101 return this.all.find(service => service.id === id);
102 }
103
104 async _showAddServiceInterface({ recipeId }) {
105 const recipesStore = this.stores.recipes;
106
107 if (recipesStore.isInstalled(recipeId)) {
108 console.debug('Recipe is installed');
109 this._redirectToAddServiceRoute(recipeId);
110 } else {
111 console.warn('Recipe is not installed');
112 // We access the RecipeStore action directly
113 // returns Promise instead of action
114 await this.stores.recipes._install({ recipeId });
115 this._redirectToAddServiceRoute(recipeId);
116 }
117 }
118
119 // Actions
120 @action async _createService({ recipeId, serviceData, redirect = true }) {
121 const data = this._cleanUpTeamIdAndCustomUrl(recipeId, serviceData);
122 const response = await this.createServiceRequest.execute(recipeId, data)._promise;
123
124 this.allServicesRequest.patch((result) => {
125 if (!result) return;
126 result.push(response.data);
127 });
128
129 this.actionStatus = response.status || [];
130
131 if (redirect) {
132 this.stores.router.push('/settings/recipes');
133 gaEvent('Service', 'create', recipeId);
134 }
135 }
136
137 @action async _createFromLegacyService({ data }) {
138 const { id } = data.recipe;
139 const serviceData = {};
140
141 if (data.name) {
142 serviceData.name = data.name;
143 }
144
145 if (data.team) {
146 serviceData.team = data.team;
147 }
148
149 if (data.team) {
150 serviceData.customUrl = data.customURL;
151 }
152
153 this.actions.service.createService({
154 recipeId: id,
155 serviceData,
156 redirect: false,
157 });
158
159 return 'hello world';
160 }
161
162 @action async _updateService({ serviceId, serviceData, redirect = true }) {
163 const service = this.one(serviceId);
164 const data = this._cleanUpTeamIdAndCustomUrl(service.recipe.id, serviceData);
165 const request = this.updateServiceRequest.execute(serviceId, data);
166
167 this.allServicesRequest.patch((result) => {
168 if (!result) return;
169 Object.assign(result.find(c => c.id === serviceId), serviceData);
170 });
171
172 await request._promise;
173 this.actionStatus = request.result.status;
174
175 if (redirect) {
176 this.stores.router.push('/settings/services');
177 gaEvent('Service', 'update', service.recipe.id);
178 }
179 }
180
181 @action async _deleteService({ serviceId, redirect }) {
182 const request = this.deleteServiceRequest.execute(serviceId);
183
184 if (redirect) {
185 this.stores.router.push(redirect);
186 }
187
188 this.allServicesRequest.patch((result) => {
189 remove(result, c => c.id === serviceId);
190 });
191
192 const service = this.one(serviceId);
193
194 await request._promise;
195 this.actionStatus = request.result.status;
196
197 gaEvent('Service', 'delete', service.recipe.id);
198 }
199
200 @action _setActive({ serviceId }) {
201 const service = this.one(serviceId);
202
203 this.all.forEach((s, index) => {
204 this.all[index].isActive = false;
205 });
206 service.isActive = true;
207 }
208
209 @action _setUnreadMessageCount({ serviceId, count }) {
210 const service = this.one(serviceId);
211
212 service.unreadDirectMessageCount = count.direct;
213 service.unreadIndirectMessageCount = count.indirect;
214 }
215
216 @action _setWebviewReference({ serviceId, webview }) {
217 const service = this.one(serviceId);
218
219 service.webview = webview;
220
221 if (!service.isAttached) {
222 service.initializeWebViewEvents(this);
223 service.initializeWebViewListener();
224 }
225
226 service.isAttached = true;
227 }
228
229 @action _focusService({ serviceId }) {
230 const service = this.one(serviceId);
231
232 if (service.webview) {
233 service.webview.focus();
234 }
235 }
236
237 @action _focusActiveService() {
238 if (this.stores.user.isLoggedIn) {
239 // TODO: add checks to not focus service when router path is /settings or /auth
240 const service = this.active;
241 if (service) {
242 this._focusService({ serviceId: service.id });
243 }
244 } else {
245 this.allServicesRequest.invalidate();
246 }
247 }
248
249 @action _toggleService({ serviceId }) {
250 const service = this.one(serviceId);
251
252 service.isEnabled = !service.isEnabled;
253 }
254
255 @action _handleIPCMessage({ serviceId, channel, args }) {
256 const service = this.one(serviceId);
257
258 if (channel === 'hello') {
259 this._initRecipePolling(service.id);
260 this._initializeServiceRecipeInWebview(serviceId);
261 } else if (channel === 'messages') {
262 this.actions.service.setUnreadMessageCount({
263 serviceId,
264 count: {
265 direct: args[0].direct,
266 indirect: args[0].indirect,
267 },
268 });
269 } else if (channel === 'notification') {
270 const options = args[0].options;
271 if (service.recipe.hasNotificationSound) {
272 Object.assign(options, {
273 silent: true,
274 });
275 }
276
277 if (service.isNotificationEnabled) {
278 this.actions.app.notify({
279 notificationId: args[0].notificationId,
280 title: args[0].title,
281 options,
282 serviceId,
283 });
284 }
285 } else if (channel === 'avatar') {
286 const url = args[0];
287 if (service.customIconUrl !== url) {
288 service.customIconUrl = url;
289
290 this.actions.service.updateService({
291 serviceId,
292 serviceData: {
293 customIconUrl: url,
294 },
295 redirect: false,
296 });
297 }
298 }
299 }
300
301 @action _sendIPCMessage({ serviceId, channel, args }) {
302 const service = this.one(serviceId);
303
304 service.webview.send(channel, args);
305 }
306
307 @action _openWindow({ event }) {
308 if (event.disposition !== 'new-window' && event.url !== 'about:blank') {
309 this.actions.app.openExternalUrl({ url: event.url });
310 }
311 }
312
313 @action _filter({ needle }) {
314 this.filterNeedle = needle;
315 }
316
317 @action _resetFilter() {
318 this.filterNeedle = null;
319 }
320
321 @action _reload({ serviceId }) {
322 const service = this.one(serviceId);
323 service.resetMessageCount();
324
325 service.webview.reload();
326 }
327
328 @action _reloadActive() {
329 if (this.active) {
330 const service = this.one(this.active.id);
331
332 this._reload({
333 serviceId: service.id,
334 });
335 }
336 }
337
338 @action _reloadAll() {
339 this.enabled.forEach(s => this._reload({
340 serviceId: s.id,
341 }));
342 }
343
344 @action _reloadUpdatedServices() {
345 this._reloadAll();
346 this.actions.ui.toggleServiceUpdatedInfoBar({ visible: false });
347 }
348
349 @action _reorder({ oldIndex, newIndex }) {
350 const oldEnabledSortIndex = this.all.indexOf(this.enabled[oldIndex]);
351 const newEnabledSortIndex = this.all.indexOf(this.enabled[newIndex]);
352
353
354 this.all.splice(newEnabledSortIndex, 0, this.all.splice(oldEnabledSortIndex, 1)[0]);
355
356 const services = {};
357 this.all.forEach((s, index) => {
358 services[this.all[index].id] = index;
359 });
360
361 this.reorderServicesRequest.execute(services);
362 this.allServicesRequest.patch((data) => {
363 data.forEach((s) => {
364 const service = s;
365
366 service.order = this.one(s.id).order;
367 });
368 });
369
370 this._reorderAnalytics();
371 }
372
373 @action _toggleNotifications({ serviceId }) {
374 const service = this.one(serviceId);
375
376 service.isNotificationEnabled = !service.isNotificationEnabled;
377
378 this.actions.service.updateService({
379 serviceId,
380 serviceData: service,
381 redirect: false,
382 });
383 }
384
385 @action _openDevTools({ serviceId }) {
386 const service = this.one(serviceId);
387
388 service.webview.openDevTools();
389 }
390
391 @action _openDevToolsForActiveService() {
392 const service = this.active;
393
394 if (service) {
395 service.webview.openDevTools();
396 } else {
397 console.warn('No service is active');
398 }
399 }
400
401 // Reactions
402 _focusServiceReaction() {
403 const service = this.active;
404 if (service) {
405 this.actions.service.focusService({ serviceId: service.id });
406 }
407 }
408
409 _saveActiveService() {
410 const service = this.active;
411
412 if (service) {
413 this.stores.settings.updateSettingsRequest.execute({
414 activeService: service.id,
415 });
416 }
417 }
418
419 _mapActiveServiceToServiceModelReaction() {
420 const { activeService } = this.stores.settings.all;
421 const services = this.enabled;
422 if (services.length) {
423 services.map(service => Object.assign(service, {
424 isActive: activeService ? activeService === service.id : services[0].id === service.id,
425 }));
426
427 // if (!services.active) {
428 //
429 // }
430 }
431 // else if (!activeService && services.length) {
432 // services[0].isActive = true;
433 // }
434 }
435
436 _getUnreadMessageCountReaction() {
437 const unreadDirectMessageCount = this.enabled
438 .map(s => s.unreadDirectMessageCount)
439 .reduce((a, b) => a + b, 0);
440
441 const unreadIndirectMessageCount = this.enabled
442 .filter(s => s.isIndirectMessageBadgeEnabled)
443 .map(s => s.unreadIndirectMessageCount)
444 .reduce((a, b) => a + b, 0);
445
446 this.actions.app.setBadge({
447 unreadDirectMessageCount,
448 unreadIndirectMessageCount,
449 });
450 }
451
452 _logoutReaction() {
453 if (!this.stores.user.isLoggedIn) {
454 this.actions.settings.remove({ key: 'activeService' });
455 this.allServicesRequest.invalidate().reset();
456 }
457 }
458
459 _cleanUpTeamIdAndCustomUrl(recipeId, data) {
460 const serviceData = data;
461 const recipe = this.stores.recipes.one(recipeId);
462
463 if (recipe.hasTeamId && recipe.hasCustomUrl && data.team && data.customUrl) {
464 delete serviceData.team;
465 }
466
467 return serviceData;
468 }
469
470 // Helper
471 _redirectToAddServiceRoute(recipeId) {
472 const route = `/settings/services/add/${recipeId}`;
473 this.stores.router.push(route);
474 }
475
476 _initializeServiceRecipeInWebview(serviceId) {
477 const service = this.one(serviceId);
478
479 if (service.webview) {
480 service.webview.send('initializeRecipe', service);
481 }
482 }
483
484 _initRecipePolling(serviceId) {
485 const service = this.one(serviceId);
486
487 const delay = 1000;
488
489 if (service) {
490 const loop = () => {
491 service.webview.send('poll');
492
493 setTimeout(loop, delay);
494 };
495
496 loop();
497 }
498 }
499
500 _reorderAnalytics = debounce(() => {
501 gaEvent('Service', 'order');
502 }, 5000);
503}