aboutsummaryrefslogtreecommitdiffstats
path: root/src/stores
diff options
context:
space:
mode:
Diffstat (limited to 'src/stores')
-rw-r--r--src/stores/AppStore.js84
-rw-r--r--src/stores/FeaturesStore.js35
-rw-r--r--src/stores/PaymentStore.js17
-rw-r--r--src/stores/RecipePreviewsStore.js3
-rw-r--r--src/stores/RequestStore.js3
-rw-r--r--src/stores/ServicesStore.js30
-rw-r--r--src/stores/UIStore.js14
-rw-r--r--src/stores/UserStore.js32
-rw-r--r--src/stores/index.js4
-rw-r--r--src/stores/lib/Reaction.js19
-rw-r--r--src/stores/lib/Request.js6
11 files changed, 158 insertions, 89 deletions
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
index dd4642d70..72c4b4d0b 100644
--- a/src/stores/AppStore.js
+++ b/src/stores/AppStore.js
@@ -1,21 +1,25 @@
1import { remote, ipcRenderer, shell } from 'electron'; 1import { remote, ipcRenderer, shell } from 'electron';
2import { action, computed, observable } from 'mobx'; 2import {
3 action, computed, observable, reaction,
4} from 'mobx';
3import moment from 'moment'; 5import moment from 'moment';
4import key from 'keymaster';
5import { getDoNotDisturb } from '@meetfranz/electron-notification-state'; 6import { getDoNotDisturb } from '@meetfranz/electron-notification-state';
6import AutoLaunch from 'auto-launch'; 7import AutoLaunch from 'auto-launch';
7import prettyBytes from 'pretty-bytes'; 8import prettyBytes from 'pretty-bytes';
9import ms from 'ms';
10import { URL } from 'url';
8 11
9import Store from './lib/Store'; 12import Store from './lib/Store';
10import Request from './lib/Request'; 13import Request from './lib/Request';
11import { CHECK_INTERVAL, DEFAULT_APP_SETTINGS } from '../config'; 14import { CHECK_INTERVAL, DEFAULT_APP_SETTINGS } from '../config';
12import { isMac, isLinux, isWindows } from '../environment'; 15import { isMac } from '../environment';
13import locales from '../i18n/translations'; 16import locales from '../i18n/translations';
14import { gaEvent } from '../lib/analytics'; 17import { gaEvent, gaPage, statsEvent } from '../lib/analytics';
15import { onVisibilityChange } from '../helpers/visibility-helper'; 18import { onVisibilityChange } from '../helpers/visibility-helper';
16import { getLocale } from '../helpers/i18n-helpers'; 19import { getLocale } from '../helpers/i18n-helpers';
17 20
18import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js'; 21import { getServiceIdsFromPartitions, removeServicePartitionDirectory } from '../helpers/service-helpers.js';
22import { isValidExternalURL } from '../helpers/url-helpers';
19 23
20const debug = require('debug')('Franz:AppStore'); 24const debug = require('debug')('Franz:AppStore');
21 25
@@ -63,6 +67,8 @@ export default class AppStore extends Store {
63 67
64 @observable isFocused = true; 68 @observable isFocused = true;
65 69
70 @observable nextAppReleaseVersion = null;
71
66 dictionaries = []; 72 dictionaries = [];
67 73
68 constructor(...args) { 74 constructor(...args) {
@@ -110,16 +116,16 @@ export default class AppStore extends Store {
110 // Check if system is muted 116 // Check if system is muted
111 // There are no events to subscribe so we need to poll everey 5s 117 // There are no events to subscribe so we need to poll everey 5s
112 this._systemDND(); 118 this._systemDND();
113 setInterval(() => this._systemDND(), 5000); 119 setInterval(() => this._systemDND(), ms('5s'));
114 120
115 // Check for updates once every 4 hours 121 // Check for updates once every 4 hours
116 setInterval(() => this._checkForUpdates(), CHECK_INTERVAL); 122 setInterval(() => this._checkForUpdates(), CHECK_INTERVAL);
117 // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues) 123 // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues)
118 setTimeout(() => this._checkForUpdates(), 30000); 124 setTimeout(() => this._checkForUpdates(), ms('30s'));
119 ipcRenderer.on('autoUpdate', (event, data) => { 125 ipcRenderer.on('autoUpdate', (event, data) => {
120 if (data.available) { 126 if (data.available) {
121 this.updateStatus = this.updateStatusTypes.AVAILABLE; 127 this.updateStatus = this.updateStatusTypes.AVAILABLE;
122 128 this.nextAppReleaseVersion = data.version;
123 if (isMac) { 129 if (isMac) {
124 app.dock.bounce(); 130 app.dock.bounce();
125 } 131 }
@@ -143,32 +149,14 @@ export default class AppStore extends Store {
143 149
144 // Handle deep linking (franz://) 150 // Handle deep linking (franz://)
145 ipcRenderer.on('navigateFromDeepLink', (event, data) => { 151 ipcRenderer.on('navigateFromDeepLink', (event, data) => {
146 const { url } = data; 152 debug('Navigate from deep link', data);
153 let { url } = data;
147 if (!url) return; 154 if (!url) return;
148 155
149 this.stores.router.push(data.url); 156 url = url.replace(/\/$/, '');
150 });
151
152 // Set active the next service
153 key(
154 '⌘+pagedown, ctrl+pagedown, ⌘+alt+right, ctrl+tab', () => {
155 this.actions.service.setActiveNext();
156 },
157 );
158 157
159 // Set active the prev service 158 this.stores.router.push(url);
160 key( 159 });
161 '⌘+pageup, ctrl+pageup, ⌘+alt+left, ctrl+shift+tab', () => {
162 this.actions.service.setActivePrev();
163 },
164 );
165
166 // Global Mute
167 key(
168 '⌘+shift+m ctrl+shift+m', () => {
169 this.actions.app.toggleMuteApp();
170 },
171 );
172 160
173 this.locale = this._getDefaultLocale(); 161 this.locale = this._getDefaultLocale();
174 162
@@ -181,6 +169,13 @@ export default class AppStore extends Store {
181 169
182 debug('Window is visible/focused', isVisible); 170 debug('Window is visible/focused', isVisible);
183 }); 171 });
172
173 // analytics autorun
174 reaction(() => this.stores.router.location.pathname, (pathname) => {
175 gaPage(pathname);
176 });
177
178 statsEvent('app-start');
184 } 179 }
185 180
186 @computed get cacheSize() { 181 @computed get cacheSize() {
@@ -193,7 +188,15 @@ export default class AppStore extends Store {
193 }) { 188 }) {
194 if (this.stores.settings.all.app.isAppMuted) return; 189 if (this.stores.settings.all.app.isAppMuted) return;
195 190
191 // TODO: is there a simple way to use blobs for notifications without storing them on disk?
192 if (options.icon.startsWith('blob:')) {
193 delete options.icon;
194 }
195
196 const notification = new window.Notification(title, options); 196 const notification = new window.Notification(title, options);
197
198 debug('New notification', title, options);
199
197 notification.onclick = (e) => { 200 notification.onclick = (e) => {
198 if (serviceId) { 201 if (serviceId) {
199 this.actions.service.sendIPCMessage({ 202 this.actions.service.sendIPCMessage({
@@ -203,12 +206,13 @@ export default class AppStore extends Store {
203 }); 206 });
204 207
205 this.actions.service.setActive({ serviceId }); 208 this.actions.service.setActive({ serviceId });
206 209 mainWindow.show();
207 if (isWindows) { 210 if (app.mainWindow.isMinimized()) {
208 mainWindow.restore(); 211 mainWindow.restore();
209 } else if (isLinux) {
210 mainWindow.show();
211 } 212 }
213 mainWindow.focus();
214
215 debug('Notification click handler');
212 } 216 }
213 }; 217 };
214 } 218 }
@@ -244,7 +248,14 @@ export default class AppStore extends Store {
244 } 248 }
245 249
246 @action _openExternalUrl({ url }) { 250 @action _openExternalUrl({ url }) {
247 shell.openExternal(url); 251 const parsedUrl = new URL(url);
252 debug('open external url', parsedUrl);
253
254 if (isValidExternalURL(url)) {
255 shell.openExternal(url);
256 }
257
258 gaEvent('External URL', 'open', parsedUrl.host);
248 } 259 }
249 260
250 @action _checkForUpdates() { 261 @action _checkForUpdates() {
@@ -268,7 +279,6 @@ export default class AppStore extends Store {
268 279
269 @action _muteApp({ isMuted, overrideSystemMute = true }) { 280 @action _muteApp({ isMuted, overrideSystemMute = true }) {
270 this.isSystemMuteOverridden = overrideSystemMute; 281 this.isSystemMuteOverridden = overrideSystemMute;
271
272 this.actions.settings.update({ 282 this.actions.settings.update({
273 type: 'app', 283 type: 'app',
274 data: { 284 data: {
@@ -305,7 +315,7 @@ export default class AppStore extends Store {
305 } else { 315 } else {
306 const deltaTime = moment().diff(this.timeOfflineStart); 316 const deltaTime = moment().diff(this.timeOfflineStart);
307 317
308 if (deltaTime > 30 * 60 * 1000) { 318 if (deltaTime > ms('30m')) {
309 this.actions.service.reloadAll(); 319 this.actions.service.reloadAll();
310 } 320 }
311 } 321 }
diff --git a/src/stores/FeaturesStore.js b/src/stores/FeaturesStore.js
index 0adee6adf..e7832088b 100644
--- a/src/stores/FeaturesStore.js
+++ b/src/stores/FeaturesStore.js
@@ -1,4 +1,9 @@
1import { computed, observable, reaction } from 'mobx'; 1import {
2 computed,
3 observable,
4 reaction,
5 runInAction,
6} from 'mobx';
2 7
3import Store from './lib/Store'; 8import Store from './lib/Store';
4import CachedRequest from './lib/CachedRequest'; 9import CachedRequest from './lib/CachedRequest';
@@ -7,6 +12,10 @@ import delayApp from '../features/delayApp';
7import spellchecker from '../features/spellchecker'; 12import spellchecker from '../features/spellchecker';
8import serviceProxy from '../features/serviceProxy'; 13import serviceProxy from '../features/serviceProxy';
9import basicAuth from '../features/basicAuth'; 14import basicAuth from '../features/basicAuth';
15import workspaces from '../features/workspaces';
16import shareFranz from '../features/shareFranz';
17import announcements from '../features/announcements';
18import settingsWS from '../features/settingsWS';
10 19
11import { DEFAULT_FEATURES_CONFIG } from '../config'; 20import { DEFAULT_FEATURES_CONFIG } from '../config';
12 21
@@ -15,13 +24,16 @@ export default class FeaturesStore extends Store {
15 24
16 @observable featuresRequest = new CachedRequest(this.api.features, 'features'); 25 @observable featuresRequest = new CachedRequest(this.api.features, 'features');
17 26
27 @observable features = Object.assign({}, DEFAULT_FEATURES_CONFIG);
28
18 async setup() { 29 async setup() {
19 this.registerReactions([ 30 this.registerReactions([
31 this._updateFeatures,
20 this._monitorLoginStatus.bind(this), 32 this._monitorLoginStatus.bind(this),
21 ]); 33 ]);
22 34
23 await this.featuresRequest._promise; 35 await this.featuresRequest._promise;
24 setTimeout(this._enableFeatures.bind(this), 1); 36 setTimeout(this._setupFeatures.bind(this), 1);
25 37
26 // single key reaction 38 // single key reaction
27 reaction(() => this.stores.user.data.isPremium, () => { 39 reaction(() => this.stores.user.data.isPremium, () => {
@@ -35,13 +47,16 @@ export default class FeaturesStore extends Store {
35 return this.defaultFeaturesRequest.execute().result || DEFAULT_FEATURES_CONFIG; 47 return this.defaultFeaturesRequest.execute().result || DEFAULT_FEATURES_CONFIG;
36 } 48 }
37 49
38 @computed get features() { 50 _updateFeatures = () => {
51 const features = Object.assign({}, DEFAULT_FEATURES_CONFIG);
39 if (this.stores.user.isLoggedIn) { 52 if (this.stores.user.isLoggedIn) {
40 return this.featuresRequest.execute().result || DEFAULT_FEATURES_CONFIG; 53 const requestResult = this.featuresRequest.execute().result;
54 Object.assign(features, requestResult);
41 } 55 }
42 56 runInAction('FeaturesStore::_updateFeatures', () => {
43 return DEFAULT_FEATURES_CONFIG; 57 this.features = features;
44 } 58 });
59 };
45 60
46 _monitorLoginStatus() { 61 _monitorLoginStatus() {
47 if (this.stores.user.isLoggedIn) { 62 if (this.stores.user.isLoggedIn) {
@@ -51,10 +66,14 @@ export default class FeaturesStore extends Store {
51 } 66 }
52 } 67 }
53 68
54 _enableFeatures() { 69 _setupFeatures() {
55 delayApp(this.stores, this.actions); 70 delayApp(this.stores, this.actions);
56 spellchecker(this.stores, this.actions); 71 spellchecker(this.stores, this.actions);
57 serviceProxy(this.stores, this.actions); 72 serviceProxy(this.stores, this.actions);
58 basicAuth(this.stores, this.actions); 73 basicAuth(this.stores, this.actions);
74 workspaces(this.stores, this.actions);
75 shareFranz(this.stores, this.actions);
76 announcements(this.stores, this.actions);
77 settingsWS(this.stores, this.actions);
59 } 78 }
60} 79}
diff --git a/src/stores/PaymentStore.js b/src/stores/PaymentStore.js
index 4cabee194..d4de476c8 100644
--- a/src/stores/PaymentStore.js
+++ b/src/stores/PaymentStore.js
@@ -10,15 +10,10 @@ export default class PaymentStore extends Store {
10 10
11 @observable createHostedPageRequest = new Request(this.api.payment, 'getHostedPage'); 11 @observable createHostedPageRequest = new Request(this.api.payment, 'getHostedPage');
12 12
13 @observable createDashboardUrlRequest = new Request(this.api.payment, 'getDashboardUrl');
14
15 @observable ordersDataRequest = new CachedRequest(this.api.payment, 'getOrders');
16
17 constructor(...args) { 13 constructor(...args) {
18 super(...args); 14 super(...args);
19 15
20 this.actions.payment.createHostedPage.listen(this._createHostedPage.bind(this)); 16 this.actions.payment.createHostedPage.listen(this._createHostedPage.bind(this));
21 this.actions.payment.createDashboardUrl.listen(this._createDashboardUrl.bind(this));
22 } 17 }
23 18
24 @computed get plan() { 19 @computed get plan() {
@@ -28,10 +23,6 @@ export default class PaymentStore extends Store {
28 return this.plansRequest.execute().result || {}; 23 return this.plansRequest.execute().result || {};
29 } 24 }
30 25
31 @computed get orders() {
32 return this.ordersDataRequest.execute().result || [];
33 }
34
35 @action _createHostedPage({ planId }) { 26 @action _createHostedPage({ planId }) {
36 const request = this.createHostedPageRequest.execute(planId); 27 const request = this.createHostedPageRequest.execute(planId);
37 28
@@ -39,12 +30,4 @@ export default class PaymentStore extends Store {
39 30
40 return request; 31 return request;
41 } 32 }
42
43 @action _createDashboardUrl() {
44 const request = this.createDashboardUrlRequest.execute();
45
46 gaEvent('Payment', 'createDashboardUrl');
47
48 return request;
49 }
50} 33}
diff --git a/src/stores/RecipePreviewsStore.js b/src/stores/RecipePreviewsStore.js
index 10b2928e3..382820d58 100644
--- a/src/stores/RecipePreviewsStore.js
+++ b/src/stores/RecipePreviewsStore.js
@@ -1,5 +1,6 @@
1import { action, computed, observable } from 'mobx'; 1import { action, computed, observable } from 'mobx';
2import { debounce } from 'lodash'; 2import { debounce } from 'lodash';
3import ms from 'ms';
3 4
4import Store from './lib/Store'; 5import Store from './lib/Store';
5import CachedRequest from './lib/CachedRequest'; 6import CachedRequest from './lib/CachedRequest';
@@ -48,5 +49,5 @@ export default class RecipePreviewsStore extends Store {
48 // Helper 49 // Helper
49 _analyticsSearch = debounce((needle) => { 50 _analyticsSearch = debounce((needle) => {
50 gaEvent('Recipe', 'search', needle); 51 gaEvent('Recipe', 'search', needle);
51 }, 3000); 52 }, ms('3s'));
52} 53}
diff --git a/src/stores/RequestStore.js b/src/stores/RequestStore.js
index 2629e0a38..9254e3223 100644
--- a/src/stores/RequestStore.js
+++ b/src/stores/RequestStore.js
@@ -1,4 +1,5 @@
1import { action, computed, observable } from 'mobx'; 1import { action, computed, observable } from 'mobx';
2import ms from 'ms';
2 3
3import Store from './lib/Store'; 4import Store from './lib/Store';
4 5
@@ -13,7 +14,7 @@ export default class RequestStore extends Store {
13 14
14 retries = 0; 15 retries = 0;
15 16
16 retryDelay = 2000; 17 retryDelay = ms('2s');
17 18
18 constructor(...args) { 19 constructor(...args) {
19 super(...args); 20 super(...args);
diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js
index efd57a09d..13f929c2f 100644
--- a/src/stores/ServicesStore.js
+++ b/src/stores/ServicesStore.js
@@ -5,12 +5,14 @@ import {
5 observable, 5 observable,
6} from 'mobx'; 6} from 'mobx';
7import { debounce, remove } from 'lodash'; 7import { debounce, remove } from 'lodash';
8import ms from 'ms';
8 9
9import Store from './lib/Store'; 10import Store from './lib/Store';
10import Request from './lib/Request'; 11import Request from './lib/Request';
11import CachedRequest from './lib/CachedRequest'; 12import CachedRequest from './lib/CachedRequest';
12import { matchRoute } from '../helpers/routing-helpers'; 13import { matchRoute } from '../helpers/routing-helpers';
13import { gaEvent } from '../lib/analytics'; 14import { gaEvent, statsEvent } from '../lib/analytics';
15import { workspaceStore } from '../features/workspaces';
14 16
15const debug = require('debug')('Franz:ServiceStore'); 17const debug = require('debug')('Franz:ServiceStore');
16 18
@@ -34,6 +36,7 @@ export default class ServicesStore extends Store {
34 36
35 // Register action handlers 37 // Register action handlers
36 this.actions.service.setActive.listen(this._setActive.bind(this)); 38 this.actions.service.setActive.listen(this._setActive.bind(this));
39 this.actions.service.blurActive.listen(this._blurActive.bind(this));
37 this.actions.service.setActiveNext.listen(this._setActiveNext.bind(this)); 40 this.actions.service.setActiveNext.listen(this._setActiveNext.bind(this));
38 this.actions.service.setActivePrev.listen(this._setActivePrev.bind(this)); 41 this.actions.service.setActivePrev.listen(this._setActivePrev.bind(this));
39 this.actions.service.showAddServiceInterface.listen(this._showAddServiceInterface.bind(this)); 42 this.actions.service.showAddServiceInterface.listen(this._showAddServiceInterface.bind(this));
@@ -43,6 +46,7 @@ export default class ServicesStore extends Store {
43 this.actions.service.deleteService.listen(this._deleteService.bind(this)); 46 this.actions.service.deleteService.listen(this._deleteService.bind(this));
44 this.actions.service.clearCache.listen(this._clearCache.bind(this)); 47 this.actions.service.clearCache.listen(this._clearCache.bind(this));
45 this.actions.service.setWebviewReference.listen(this._setWebviewReference.bind(this)); 48 this.actions.service.setWebviewReference.listen(this._setWebviewReference.bind(this));
49 this.actions.service.detachService.listen(this._detachService.bind(this));
46 this.actions.service.focusService.listen(this._focusService.bind(this)); 50 this.actions.service.focusService.listen(this._focusService.bind(this));
47 this.actions.service.focusActiveService.listen(this._focusActiveService.bind(this)); 51 this.actions.service.focusActiveService.listen(this._focusActiveService.bind(this));
48 this.actions.service.toggleService.listen(this._toggleService.bind(this)); 52 this.actions.service.toggleService.listen(this._toggleService.bind(this));
@@ -97,7 +101,6 @@ export default class ServicesStore extends Store {
97 return observable(services.slice().slice().sort((a, b) => a.order - b.order)); 101 return observable(services.slice().slice().sort((a, b) => a.order - b.order));
98 } 102 }
99 } 103 }
100
101 return []; 104 return [];
102 } 105 }
103 106
@@ -106,13 +109,16 @@ export default class ServicesStore extends Store {
106 } 109 }
107 110
108 @computed get allDisplayed() { 111 @computed get allDisplayed() {
109 return this.stores.settings.all.app.showDisabledServices ? this.all : this.enabled; 112 const services = this.stores.settings.all.app.showDisabledServices ? this.all : this.enabled;
113 return workspaceStore.filterServicesByActiveWorkspace(services);
110 } 114 }
111 115
112 // This is just used to avoid unnecessary rerendering of resource-heavy webviews 116 // This is just used to avoid unnecessary rerendering of resource-heavy webviews
113 @computed get allDisplayedUnordered() { 117 @computed get allDisplayedUnordered() {
118 const { showDisabledServices } = this.stores.settings.all.app;
114 const services = this.allServicesRequest.execute().result || []; 119 const services = this.allServicesRequest.execute().result || [];
115 return this.stores.settings.all.app.showDisabledServices ? services : services.filter(service => service.isEnabled); 120 const filteredServices = showDisabledServices ? services : services.filter(service => service.isEnabled);
121 return workspaceStore.filterServicesByActiveWorkspace(filteredServices);
116 } 122 }
117 123
118 @computed get filtered() { 124 @computed get filtered() {
@@ -293,9 +299,16 @@ export default class ServicesStore extends Store {
293 }); 299 });
294 service.isActive = true; 300 service.isActive = true;
295 301
302 statsEvent('activate-service', service.recipe.id);
303
296 this._focusActiveService(); 304 this._focusActiveService();
297 } 305 }
298 306
307 @action _blurActive() {
308 if (!this.active) return;
309 this.active.isActive = false;
310 }
311
299 @action _setActiveNext() { 312 @action _setActiveNext() {
300 const nextIndex = this._wrapIndex(this.allDisplayed.findIndex(service => service.isActive), 1, this.allDisplayed.length); 313 const nextIndex = this._wrapIndex(this.allDisplayed.findIndex(service => service.isActive), 1, this.allDisplayed.length);
301 314
@@ -340,6 +353,11 @@ export default class ServicesStore extends Store {
340 service.isAttached = true; 353 service.isAttached = true;
341 } 354 }
342 355
356 @action _detachService({ service }) {
357 service.webview = null;
358 service.isAttached = false;
359 }
360
343 @action _focusService({ serviceId }) { 361 @action _focusService({ serviceId }) {
344 const service = this.one(serviceId); 362 const service = this.one(serviceId);
345 363
@@ -679,7 +697,7 @@ export default class ServicesStore extends Store {
679 _initRecipePolling(serviceId) { 697 _initRecipePolling(serviceId) {
680 const service = this.one(serviceId); 698 const service = this.one(serviceId);
681 699
682 const delay = 2000; 700 const delay = ms('2s');
683 701
684 if (service) { 702 if (service) {
685 if (service.timer !== null) { 703 if (service.timer !== null) {
@@ -700,7 +718,7 @@ export default class ServicesStore extends Store {
700 718
701 _reorderAnalytics = debounce(() => { 719 _reorderAnalytics = debounce(() => {
702 gaEvent('Service', 'order'); 720 gaEvent('Service', 'order');
703 }, 5000); 721 }, ms('5s'));
704 722
705 _wrapIndex(index, delta, size) { 723 _wrapIndex(index, delta, size) {
706 return (((index + delta) % size) + size) % size; 724 return (((index + delta) % size) + size) % size;
diff --git a/src/stores/UIStore.js b/src/stores/UIStore.js
index d37ebe4c7..a95a8e1e0 100644
--- a/src/stores/UIStore.js
+++ b/src/stores/UIStore.js
@@ -1,8 +1,7 @@
1import { action, observable, computed } from 'mobx'; 1import { action, observable, computed } from 'mobx';
2import { theme } from '@meetfranz/theme';
2 3
3import Store from './lib/Store'; 4import Store from './lib/Store';
4import * as themeDefault from '../theme/default';
5import * as themeDark from '../theme/dark';
6 5
7export default class UIStore extends Store { 6export default class UIStore extends Store {
8 @observable showServicesUpdatedInfoBar = false; 7 @observable showServicesUpdatedInfoBar = false;
@@ -22,12 +21,13 @@ export default class UIStore extends Store {
22 return (settings.app.isAppMuted && settings.app.showMessageBadgeWhenMuted) || !settings.isAppMuted; 21 return (settings.app.isAppMuted && settings.app.showMessageBadgeWhenMuted) || !settings.isAppMuted;
23 } 22 }
24 23
25 @computed get theme() { 24 @computed get isDarkThemeActive() {
26 if (this.stores.settings.all.app.darkMode) { 25 return this.stores.settings.all.app.darkMode;
27 return Object.assign({}, themeDefault, themeDark); 26 }
28 }
29 27
30 return themeDefault; 28 @computed get theme() {
29 if (this.isDarkThemeActive) return theme('dark');
30 return theme('default');
31 } 31 }
32 32
33 // Actions 33 // Actions
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
index 7addb5760..31555dd5c 100644
--- a/src/stores/UserStore.js
+++ b/src/stores/UserStore.js
@@ -129,10 +129,6 @@ export default class UserStore extends Store {
129 return Boolean(localStorage.getItem('authToken')); 129 return Boolean(localStorage.getItem('authToken'));
130 } 130 }
131 131
132 // @computed get isTokenValid() {
133 // return this.authToken !== null && moment(this.tokenExpiry).isAfter(moment());
134 // }
135
136 @computed get isTokenExpired() { 132 @computed get isTokenExpired() {
137 if (!this.authToken) return false; 133 if (!this.authToken) return false;
138 134
@@ -146,6 +142,10 @@ export default class UserStore extends Store {
146 return this.getUserInfoRequest.execute().result || {}; 142 return this.getUserInfoRequest.execute().result || {};
147 } 143 }
148 144
145 @computed get isPremium() {
146 return !!this.data.isPremium;
147 }
148
149 @computed get legacyServices() { 149 @computed get legacyServices() {
150 return this.getLegacyServicesRequest.execute() || {}; 150 return this.getLegacyServicesRequest.execute() || {};
151 } 151 }
@@ -160,6 +160,14 @@ export default class UserStore extends Store {
160 gaEvent('User', 'login'); 160 gaEvent('User', 'login');
161 } 161 }
162 162
163 @action _tokenLogin(authToken) {
164 this._setUserData(authToken);
165
166 this.stores.router.push('/');
167
168 gaEvent('User', 'tokenLogin');
169 }
170
163 @action async _signup({ 171 @action async _signup({
164 firstname, lastname, email, password, accountType, company, 172 firstname, lastname, email, password, accountType, company,
165 }) { 173 }) {
@@ -170,6 +178,7 @@ export default class UserStore extends Store {
170 password, 178 password,
171 accountType, 179 accountType,
172 company, 180 company,
181 locale: this.stores.app.locale,
173 }); 182 });
174 183
175 this.hasCompletedSignup = false; 184 this.hasCompletedSignup = false;
@@ -206,6 +215,8 @@ export default class UserStore extends Store {
206 } 215 }
207 216
208 @action async _update({ userData }) { 217 @action async _update({ userData }) {
218 if (!this.isLoggedIn) return;
219
209 const response = await this.updateUserInfoRequest.execute(userData)._promise; 220 const response = await this.updateUserInfoRequest.execute(userData)._promise;
210 221
211 this.getUserInfoRequest.patch(() => response.data); 222 this.getUserInfoRequest.patch(() => response.data);
@@ -222,6 +233,7 @@ export default class UserStore extends Store {
222 // workaround mobx issue 233 // workaround mobx issue
223 localStorage.removeItem('authToken'); 234 localStorage.removeItem('authToken');
224 window.localStorage.removeItem('authToken'); 235 window.localStorage.removeItem('authToken');
236
225 this.getUserInfoRequest.invalidate().reset(); 237 this.getUserInfoRequest.invalidate().reset();
226 this.authToken = null; 238 this.authToken = null;
227 } 239 }
@@ -262,6 +274,18 @@ export default class UserStore extends Store {
262 const { router } = this.stores; 274 const { router } = this.stores;
263 const currentRoute = router.location.pathname; 275 const currentRoute = router.location.pathname;
264 if (!this.isLoggedIn 276 if (!this.isLoggedIn
277 && currentRoute.includes('token=')) {
278 router.push(this.WELCOME_ROUTE);
279 const token = currentRoute.split('=')[1];
280
281 const data = this._parseToken(token);
282 if (data) {
283 // Give this some time to sink
284 setTimeout(() => {
285 this._tokenLogin(token);
286 }, 1000);
287 }
288 } else if (!this.isLoggedIn
265 && !currentRoute.includes(this.BASE_ROUTE)) { 289 && !currentRoute.includes(this.BASE_ROUTE)) {
266 router.push(this.WELCOME_ROUTE); 290 router.push(this.WELCOME_ROUTE);
267 } else if (this.isLoggedIn 291 } else if (this.isLoggedIn
diff --git a/src/stores/index.js b/src/stores/index.js
index 96b844c95..1912418a2 100644
--- a/src/stores/index.js
+++ b/src/stores/index.js
@@ -10,6 +10,8 @@ import PaymentStore from './PaymentStore';
10import NewsStore from './NewsStore'; 10import NewsStore from './NewsStore';
11import RequestStore from './RequestStore'; 11import RequestStore from './RequestStore';
12import GlobalErrorStore from './GlobalErrorStore'; 12import GlobalErrorStore from './GlobalErrorStore';
13import { workspaceStore } from '../features/workspaces';
14import { announcementsStore } from '../features/announcements';
13 15
14export default (api, actions, router) => { 16export default (api, actions, router) => {
15 const stores = {}; 17 const stores = {};
@@ -27,6 +29,8 @@ export default (api, actions, router) => {
27 news: new NewsStore(stores, api, actions), 29 news: new NewsStore(stores, api, actions),
28 requests: new RequestStore(stores, api, actions), 30 requests: new RequestStore(stores, api, actions),
29 globalError: new GlobalErrorStore(stores, api, actions), 31 globalError: new GlobalErrorStore(stores, api, actions),
32 workspaces: workspaceStore,
33 announcements: announcementsStore,
30 }); 34 });
31 // Initialize all stores 35 // Initialize all stores
32 Object.keys(stores).forEach((name) => { 36 Object.keys(stores).forEach((name) => {
diff --git a/src/stores/lib/Reaction.js b/src/stores/lib/Reaction.js
index 46aa4dae6..f2642908f 100644
--- a/src/stores/lib/Reaction.js
+++ b/src/stores/lib/Reaction.js
@@ -1,24 +1,31 @@
1// @flow
2import { autorun } from 'mobx'; 1import { autorun } from 'mobx';
3 2
4export default class Reaction { 3export default class Reaction {
5 reaction; 4 reaction;
6 5
7 hasBeenStarted; 6 isRunning = false;
8 7
9 dispose; 8 dispose;
10 9
11 constructor(reaction) { 10 constructor(reaction) {
12 this.reaction = reaction; 11 this.reaction = reaction;
13 this.hasBeenStarted = false;
14 } 12 }
15 13
16 start() { 14 start() {
17 this.dispose = autorun(() => this.reaction()); 15 if (!this.isRunning) {
18 this.hasBeenStarted = true; 16 this.dispose = autorun(() => this.reaction());
17 this.isActive = true;
18 }
19 } 19 }
20 20
21 stop() { 21 stop() {
22 if (this.hasBeenStarted) this.dispose(); 22 if (this.isRunning) {
23 this.dispose();
24 this.isActive = false;
25 }
23 } 26 }
24} 27}
28
29export const createReactions = reactions => (
30 reactions.map(r => new Reaction(r))
31);
diff --git a/src/stores/lib/Request.js b/src/stores/lib/Request.js
index 04f528156..486de8a49 100644
--- a/src/stores/lib/Request.js
+++ b/src/stores/lib/Request.js
@@ -85,6 +85,8 @@ export default class Request {
85 return this.execute(...this._currentApiCall.args); 85 return this.execute(...this._currentApiCall.args);
86 } 86 }
87 87
88 retry = () => this.reload();
89
88 isExecutingWithArgs(...args) { 90 isExecutingWithArgs(...args) {
89 return this.isExecuting && this._currentApiCall && isEqual(this._currentApiCall.args, args); 91 return this.isExecuting && this._currentApiCall && isEqual(this._currentApiCall.args, args);
90 } 92 }
@@ -107,7 +109,7 @@ export default class Request {
107 Request._hooks.forEach(hook => hook(this)); 109 Request._hooks.forEach(hook => hook(this));
108 } 110 }
109 111
110 reset() { 112 reset = () => {
111 this.result = null; 113 this.result = null;
112 this.isExecuting = false; 114 this.isExecuting = false;
113 this.isError = false; 115 this.isError = false;
@@ -116,5 +118,5 @@ export default class Request {
116 this._promise = Promise; 118 this._promise = Promise;
117 119
118 return this; 120 return this;
119 } 121 };
120} 122}