aboutsummaryrefslogtreecommitdiffstats
path: root/src/stores/AppStore.js
diff options
context:
space:
mode:
authorLibravatar Ricardo Cino <ricardo@cino.io>2022-06-22 00:32:18 +0200
committerLibravatar GitHub <noreply@github.com>2022-06-21 22:32:18 +0000
commit73ba955e344c8ccedd43235495ef8b72b5a2b6fd (patch)
tree03766ab32fefe7e83026a14393527f1dcbaed849 /src/stores/AppStore.js
parentdocs: add cino as a contributor for infra [skip ci] (#330) (diff)
downloadferdium-app-73ba955e344c8ccedd43235495ef8b72b5a2b6fd.tar.gz
ferdium-app-73ba955e344c8ccedd43235495ef8b72b5a2b6fd.tar.zst
ferdium-app-73ba955e344c8ccedd43235495ef8b72b5a2b6fd.zip
chore: Transform AppStore.js into Typescript (#329)
* turn actions into typescript * correct tsconfig * added TypedStore
Diffstat (limited to 'src/stores/AppStore.js')
-rw-r--r--src/stores/AppStore.js573
1 files changed, 0 insertions, 573 deletions
diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js
deleted file mode 100644
index a8e1ce247..000000000
--- a/src/stores/AppStore.js
+++ /dev/null
@@ -1,573 +0,0 @@
1import { ipcRenderer } from 'electron';
2import {
3 app,
4 screen,
5 powerMonitor,
6 nativeTheme,
7 getCurrentWindow,
8 process as remoteProcess,
9} from '@electron/remote';
10import { action, computed, observable } from 'mobx';
11import moment from 'moment';
12import AutoLaunch from 'auto-launch';
13import ms from 'ms';
14import { URL } from 'url';
15import { readJsonSync } from 'fs-extra';
16
17import Store from './lib/Store';
18import Request from './lib/Request';
19import { CHECK_INTERVAL, DEFAULT_APP_SETTINGS } from '../config';
20import { cleanseJSObject } from '../jsUtils';
21import { isMac, isWindows, electronVersion, osRelease } from '../environment';
22import { ferdiumVersion, userDataPath, ferdiumLocale } from '../environment-remote';
23import { generatedTranslations } from '../i18n/translations';
24import { getLocale } from '../helpers/i18n-helpers';
25
26import {
27 getServiceIdsFromPartitions,
28 removeServicePartitionDirectory,
29} from '../helpers/service-helpers';
30import { openExternalUrl } from '../helpers/url-helpers';
31import { sleep } from '../helpers/async-helpers';
32
33const debug = require('../preload-safe-debug')('Ferdium:AppStore');
34
35const mainWindow = getCurrentWindow();
36
37const executablePath = isMac ? remoteProcess.execPath : process.execPath;
38const autoLauncher = new AutoLaunch({
39 name: 'Ferdium',
40 path: executablePath,
41});
42
43const CATALINA_NOTIFICATION_HACK_KEY =
44 '_temp_askedForCatalinaNotificationPermissions';
45
46const locales = generatedTranslations();
47
48export default class AppStore extends Store {
49 updateStatusTypes = {
50 CHECKING: 'CHECKING',
51 AVAILABLE: 'AVAILABLE',
52 NOT_AVAILABLE: 'NOT_AVAILABLE',
53 DOWNLOADED: 'DOWNLOADED',
54 FAILED: 'FAILED',
55 };
56
57 @observable healthCheckRequest = new Request(this.api.app, 'health');
58
59 @observable getAppCacheSizeRequest = new Request(
60 this.api.local,
61 'getAppCacheSize',
62 );
63
64 @observable clearAppCacheRequest = new Request(this.api.local, 'clearCache');
65
66 @observable autoLaunchOnStart = true;
67
68 @observable isOnline = navigator.onLine;
69
70 @observable authRequestFailed = false;
71
72 @observable timeSuspensionStart = moment();
73
74 @observable timeOfflineStart;
75
76 @observable updateStatus = '';
77
78 @observable locale = ferdiumLocale;
79
80 @observable isSystemMuteOverridden = false;
81
82 @observable isSystemDarkModeEnabled = false;
83
84 @observable isClearingAllCache = false;
85
86 @observable isFullScreen = mainWindow.isFullScreen();
87
88 @observable isFocused = true;
89
90 dictionaries = [];
91
92 fetchDataInterval = null;
93
94 constructor(...args) {
95 super(...args);
96
97 // Register action handlers
98 this.actions.app.notify.listen(this._notify.bind(this));
99 this.actions.app.setBadge.listen(this._setBadge.bind(this));
100 this.actions.app.launchOnStartup.listen(this._launchOnStartup.bind(this));
101 this.actions.app.openExternalUrl.listen(this._openExternalUrl.bind(this));
102 this.actions.app.checkForUpdates.listen(this._checkForUpdates.bind(this));
103 this.actions.app.installUpdate.listen(this._installUpdate.bind(this));
104 this.actions.app.resetUpdateStatus.listen(
105 this._resetUpdateStatus.bind(this),
106 );
107 this.actions.app.healthCheck.listen(this._healthCheck.bind(this));
108 this.actions.app.muteApp.listen(this._muteApp.bind(this));
109 this.actions.app.toggleMuteApp.listen(this._toggleMuteApp.bind(this));
110 this.actions.app.clearAllCache.listen(this._clearAllCache.bind(this));
111
112 this.registerReactions([
113 this._offlineCheck.bind(this),
114 this._setLocale.bind(this),
115 this._muteAppHandler.bind(this),
116 this._handleFullScreen.bind(this),
117 this._handleLogout.bind(this),
118 ]);
119 }
120
121 async setup() {
122 this._appStartsCounter();
123 // Focus the active service
124 window.addEventListener('focus', this.actions.service.focusActiveService);
125
126 // Online/Offline handling
127 window.addEventListener('online', () => {
128 this.isOnline = true;
129 });
130 window.addEventListener('offline', () => {
131 this.isOnline = false;
132 });
133
134 mainWindow.on('enter-full-screen', () => {
135 this.isFullScreen = true;
136 });
137 mainWindow.on('leave-full-screen', () => {
138 this.isFullScreen = false;
139 });
140
141 this.isOnline = navigator.onLine;
142
143 // Check if Ferdium should launch on start
144 // Needs to be delayed a bit
145 this._autoStart();
146
147 // Check if system is muted
148 // There are no events to subscribe so we need to poll everey 5s
149 this._systemDND();
150 setInterval(() => this._systemDND(), ms('5s'));
151
152 this.fetchDataInterval = setInterval(() => {
153 this.stores.user.getUserInfoRequest.invalidate({
154 immediately: true,
155 });
156 this.stores.features.featuresRequest.invalidate({
157 immediately: true,
158 });
159 }, ms('60m'));
160
161 // Check for updates once every 4 hours
162 setInterval(() => this._checkForUpdates(), CHECK_INTERVAL);
163 // Check for an update in 30s (need a delay to prevent Squirrel Installer lock file issues)
164 setTimeout(() => this._checkForUpdates(), ms('30s'));
165 ipcRenderer.on('autoUpdate', (event, data) => {
166 if (this.updateStatus !== this.updateStatusTypes.FAILED) {
167 if (data.available) {
168 this.updateStatus = this.updateStatusTypes.AVAILABLE;
169 if (isMac && this.stores.settings.app.automaticUpdates) {
170 app.dock.bounce();
171 }
172 }
173
174 if (data.available !== undefined && !data.available) {
175 this.updateStatus = this.updateStatusTypes.NOT_AVAILABLE;
176 }
177
178 if (data.downloaded) {
179 this.updateStatus = this.updateStatusTypes.DOWNLOADED;
180 if (isMac && this.stores.settings.app.automaticUpdates) {
181 app.dock.bounce();
182 }
183 }
184
185 if (data.error) {
186 if (data.error.message && data.error.message.startsWith('404')) {
187 this.updateStatus = this.updateStatusTypes.NOT_AVAILABLE;
188 console.warn('Updater warning: there seems to be unpublished pre-release(s) available on GitHub', data.error);
189 } else {
190 console.error('Updater error:', data.error);
191 this.updateStatus = this.updateStatusTypes.FAILED;
192 }
193 }
194 }
195 });
196
197 // Handle deep linking (ferdium://)
198 ipcRenderer.on('navigateFromDeepLink', (event, data) => {
199 debug('Navigate from deep link', data);
200 let { url } = data;
201 if (!url) return;
202
203 url = url.replace(/\/$/, '');
204
205 this.stores.router.push(url);
206 });
207
208 ipcRenderer.on('muteApp', () => {
209 this._toggleMuteApp();
210 });
211
212 this.locale = this._getDefaultLocale();
213
214 setTimeout(() => {
215 this._healthCheck();
216 }, 1000);
217
218 this.isSystemDarkModeEnabled = nativeTheme.shouldUseDarkColors;
219
220 ipcRenderer.on('isWindowFocused', (event, isFocused) => {
221 debug('Setting is focused to', isFocused);
222 this.isFocused = isFocused;
223 });
224
225 powerMonitor.on('suspend', () => {
226 debug('System suspended starting timer');
227
228 this.timeSuspensionStart = moment();
229 });
230
231 powerMonitor.on('resume', () => {
232 debug('System resumed, last suspended on', this.timeSuspensionStart);
233 this.actions.service.resetLastPollTimer();
234
235 const idleTime = this.stores.settings.app.reloadAfterResumeTime;
236
237 if (
238 this.timeSuspensionStart.add(idleTime, 'm').isBefore(moment()) &&
239 this.stores.settings.app.reloadAfterResume
240 ) {
241 debug('Reloading services, user info and features');
242
243 setInterval(() => {
244 debug('Reload app interval is starting');
245 if (this.isOnline) {
246 window.location.reload();
247 }
248 }, ms('2s'));
249 }
250 });
251
252 // macOS catalina notifications hack
253 // notifications got stuck after upgrade but forcing a notification
254 // via `new Notification` triggered the permission request
255 if (isMac && !localStorage.getItem(CATALINA_NOTIFICATION_HACK_KEY)) {
256 debug('Triggering macOS Catalina notification permission trigger');
257 // eslint-disable-next-line no-new
258 new window.Notification('Welcome to Ferdium 5', {
259 body: 'Have a wonderful day & happy messaging.',
260 });
261
262 localStorage.setItem(CATALINA_NOTIFICATION_HACK_KEY, 'true');
263 }
264 }
265
266 @computed get cacheSize() {
267 return this.getAppCacheSizeRequest.execute().result;
268 }
269
270 @computed get debugInfo() {
271 const settings = cleanseJSObject(this.stores.settings.app);
272 settings.lockedPassword = '******';
273
274 return {
275 host: {
276 platform: process.platform,
277 release: osRelease,
278 screens: screen.getAllDisplays(),
279 },
280 ferdium: {
281 version: ferdiumVersion,
282 electron: electronVersion,
283 installedRecipes: this.stores.recipes.all.map(recipe => ({
284 id: recipe.id,
285 version: recipe.version,
286 })),
287 devRecipes: this.stores.recipePreviews.dev.map(recipe => ({
288 id: recipe.id,
289 version: recipe.version,
290 })),
291 services: this.stores.services.all.map(service => ({
292 id: service.id,
293 recipe: service.recipe.id,
294 isAttached: service.isAttached,
295 isActive: service.isActive,
296 isEnabled: service.isEnabled,
297 isHibernating: service.isHibernating,
298 hasCrashed: service.hasCrashed,
299 isDarkModeEnabled: service.isDarkModeEnabled,
300 isProgressbarEnabled: service.isProgressbarEnabled,
301 })),
302 messages: this.stores.globalError.messages,
303 workspaces: this.stores.workspaces.workspaces.map(workspace => ({
304 id: workspace.id,
305 services: workspace.services,
306 })),
307 windowSettings: readJsonSync(userDataPath('window-state.json')),
308 settings,
309 features: this.stores.features.features,
310 user: this.stores.user.data.id,
311 },
312 };
313 }
314
315 // Actions
316 @action _notify({ title, options, notificationId, serviceId = null }) {
317 if (this.stores.settings.all.app.isAppMuted) return;
318
319 // TODO: is there a simple way to use blobs for notifications without storing them on disk?
320 if (options.icon && options.icon.startsWith('blob:')) {
321 delete options.icon;
322 }
323
324 const notification = new window.Notification(title, options);
325
326 debug('New notification', title, options);
327
328 notification.addEventListener('click', () => {
329 if (serviceId) {
330 this.actions.service.sendIPCMessage({
331 channel: `notification-onclick:${notificationId}`,
332 args: {},
333 serviceId,
334 });
335
336 this.actions.service.setActive({
337 serviceId,
338 });
339
340 if (!mainWindow.isVisible()) {
341 mainWindow.show();
342 }
343 if (mainWindow.isMinimized()) {
344 mainWindow.restore();
345 }
346 mainWindow.focus();
347
348 debug('Notification click handler');
349 }
350 });
351 }
352
353 @action _setBadge({ unreadDirectMessageCount, unreadIndirectMessageCount }) {
354 let indicator = unreadDirectMessageCount;
355
356 if (indicator === 0 && unreadIndirectMessageCount !== 0) {
357 indicator = '•';
358 } else if (
359 unreadDirectMessageCount === 0 &&
360 unreadIndirectMessageCount === 0
361 ) {
362 indicator = 0;
363 } else {
364 indicator = Number.parseInt(indicator, 10);
365 }
366
367 ipcRenderer.send('updateAppIndicator', {
368 indicator,
369 });
370 }
371
372 @action _launchOnStartup({ enable }) {
373 this.autoLaunchOnStart = enable;
374
375 try {
376 if (enable) {
377 debug('enabling launch on startup', executablePath);
378 autoLauncher.enable();
379 } else {
380 debug('disabling launch on startup');
381 autoLauncher.disable();
382 }
383 } catch (error) {
384 console.warn(error);
385 }
386 }
387
388 // Ideally(?) this should be merged with the 'shell-helpers' functionality
389 @action _openExternalUrl({ url }) {
390 openExternalUrl(new URL(url));
391 }
392
393 @action _checkForUpdates() {
394 if (this.isOnline && this.stores.settings.app.automaticUpdates && (isMac || isWindows || process.env.APPIMAGE)) {
395 debug('_checkForUpdates: sending event to autoUpdate:check');
396 this.updateStatus = this.updateStatusTypes.CHECKING;
397 ipcRenderer.send('autoUpdate', {
398 action: 'check',
399 });
400 }
401
402 if (this.isOnline && this.stores.settings.app.automaticUpdates) {
403 this.actions.recipe.update();
404 }
405 }
406
407 @action _installUpdate() {
408 debug('_installUpdate: sending event to autoUpdate:install');
409 ipcRenderer.send('autoUpdate', {
410 action: 'install',
411 });
412 }
413
414 @action _resetUpdateStatus() {
415 this.updateStatus = '';
416 }
417
418 @action _healthCheck() {
419 this.healthCheckRequest.execute();
420 }
421
422 @action _muteApp({ isMuted, overrideSystemMute = true }) {
423 this.isSystemMuteOverridden = overrideSystemMute;
424 this.actions.settings.update({
425 type: 'app',
426 data: {
427 isAppMuted: isMuted,
428 },
429 });
430 }
431
432 @action _toggleMuteApp() {
433 this._muteApp({
434 isMuted: !this.stores.settings.all.app.isAppMuted,
435 });
436 }
437
438 @action async _clearAllCache() {
439 this.isClearingAllCache = true;
440 const clearAppCache = this.clearAppCacheRequest.execute();
441 const allServiceIds = await getServiceIdsFromPartitions();
442 const allOrphanedServiceIds = allServiceIds.filter(
443 id =>
444 !this.stores.services.all.some(
445 s => id.replace('service-', '') === s.id,
446 ),
447 );
448
449 try {
450 await Promise.all(
451 allOrphanedServiceIds.map(id => removeServicePartitionDirectory(id)),
452 );
453 } catch (error) {
454 console.log('Error while deleting service partition directory -', error);
455 }
456 await Promise.all(
457 this.stores.services.all.map(s =>
458 this.actions.service.clearCache({
459 serviceId: s.id,
460 }),
461 ),
462 );
463
464 await clearAppCache._promise;
465
466 await sleep(ms('1s'));
467
468 this.getAppCacheSizeRequest.execute();
469
470 this.isClearingAllCache = false;
471 }
472
473 // Reactions
474 _offlineCheck() {
475 if (!this.isOnline) {
476 this.timeOfflineStart = moment();
477 } else {
478 const deltaTime = moment().diff(this.timeOfflineStart);
479
480 if (deltaTime > ms('30m')) {
481 this.actions.service.reloadAll();
482 }
483 }
484 }
485
486 _setLocale() {
487 if (this.stores.user.isLoggedIn && this.stores.user.data.locale) {
488 this.locale = this.stores.user.data.locale;
489 } else if (!this.locale) {
490 this.locale = this._getDefaultLocale();
491 }
492
493 moment.locale(this.locale);
494 debug(`Set locale to "${this.locale}"`);
495 }
496
497 _getDefaultLocale() {
498 return getLocale({
499 locale: ferdiumLocale,
500 locales,
501 fallbackLocale: DEFAULT_APP_SETTINGS.fallbackLocale,
502 });
503 }
504
505 _muteAppHandler() {
506 const { showMessageBadgesEvenWhenMuted } = this.stores.ui;
507
508 if (!showMessageBadgesEvenWhenMuted) {
509 this.actions.app.setBadge({
510 unreadDirectMessageCount: 0,
511 unreadIndirectMessageCount: 0,
512 });
513 }
514 }
515
516 _handleFullScreen() {
517 const body = document.querySelector('body');
518
519 if (body) {
520 if (this.isFullScreen) {
521 body.classList.add('isFullScreen');
522 } else {
523 body.classList.remove('isFullScreen');
524 }
525 }
526 }
527
528 _handleLogout() {
529 if (!this.stores.user.isLoggedIn) {
530 clearInterval(this.fetchDataInterval);
531 }
532 }
533
534 // Helpers
535 _appStartsCounter() {
536 this.actions.settings.update({
537 type: 'stats',
538 data: {
539 appStarts: (this.stores.settings.all.stats.appStarts || 0) + 1,
540 },
541 });
542 }
543
544 async _autoStart() {
545 this.autoLaunchOnStart = await this._checkAutoStart();
546
547 if (this.stores.settings.all.stats.appStarts === 1) {
548 debug('Set app to launch on start');
549 this.actions.app.launchOnStartup({
550 enable: true,
551 });
552 }
553 }
554
555 async _checkAutoStart() {
556 return autoLauncher.isEnabled() || false;
557 }
558
559 async _systemDND() {
560 debug('Checking if Do Not Disturb Mode is on');
561 const dnd = await ipcRenderer.invoke('get-dnd');
562 debug('Do not disturb mode is', dnd);
563 if (
564 dnd !== this.stores.settings.all.app.isAppMuted &&
565 !this.isSystemMuteOverridden
566 ) {
567 this.actions.app.muteApp({
568 isMuted: dnd,
569 overrideSystemMute: false,
570 });
571 }
572 }
573}