From 035002ceedf78d5ec73eabc0df7f06139939b967 Mon Sep 17 00:00:00 2001 From: Amine El Mouafik <412895+kytwb@users.noreply.github.com> Date: Mon, 8 Feb 2021 10:34:45 +0100 Subject: Synchronize with Franz 5.6.0 (#1033) Co-authored-by: FranzBot Co-authored-by: vantezzen Co-authored-by: Makazzz Co-authored-by: Stefan Malzner Co-authored-by: Amine Mouafik --- src/stores/AppStore.js | 136 ++++++++++++++++++++++++++++++++------------ src/stores/RecipesStore.js | 2 +- src/stores/ServicesStore.js | 131 ++++++++++++++++++++++++++++++++++++------ src/stores/UserStore.js | 19 ++++++- 4 files changed, 234 insertions(+), 54 deletions(-) (limited to 'src/stores') diff --git a/src/stores/AppStore.js b/src/stores/AppStore.js index 153fdb2c8..869cfa9d6 100644 --- a/src/stores/AppStore.js +++ b/src/stores/AppStore.js @@ -3,7 +3,6 @@ import { action, computed, observable, } from 'mobx'; import moment from 'moment'; -import { getDoNotDisturb } from '@meetfranz/electron-notification-state'; import AutoLaunch from 'auto-launch'; import prettyBytes from 'pretty-bytes'; import ms from 'ms'; @@ -27,7 +26,10 @@ import { sleep } from '../helpers/async-helpers'; const debug = require('debug')('Ferdi:AppStore'); const { - app, nativeTheme, screen, powerMonitor, + app, + screen, + powerMonitor, + nativeTheme, } = remote; const mainWindow = remote.getCurrentWindow(); @@ -63,7 +65,7 @@ export default class AppStore extends Store { @observable authRequestFailed = false; - @observable timeSuspensionStart; + @observable timeSuspensionStart = moment(); @observable timeOfflineStart; @@ -118,11 +120,19 @@ export default class AppStore extends Store { window.addEventListener('focus', this.actions.service.focusActiveService); // Online/Offline handling - window.addEventListener('online', () => { this.isOnline = true; }); - window.addEventListener('offline', () => { this.isOnline = false; }); + window.addEventListener('online', () => { + this.isOnline = true; + }); + window.addEventListener('offline', () => { + this.isOnline = false; + }); - mainWindow.on('enter-full-screen', () => { this.isFullScreen = true; }); - mainWindow.on('leave-full-screen', () => { this.isFullScreen = false; }); + mainWindow.on('enter-full-screen', () => { + this.isFullScreen = true; + }); + mainWindow.on('leave-full-screen', () => { + this.isFullScreen = false; + }); this.isOnline = navigator.onLine; @@ -137,10 +147,16 @@ export default class AppStore extends Store { setInterval(() => this._systemDND(), ms('5s')); this.fetchDataInterval = setInterval(() => { - this.stores.user.getUserInfoRequest.invalidate({ immediately: true }); - this.stores.features.featuresRequest.invalidate({ immediately: true }); - this.stores.news.latestNewsRequest.invalidate({ immediately: true }); - }, ms('10m')); + this.stores.user.getUserInfoRequest.invalidate({ + immediately: true, + }); + this.stores.features.featuresRequest.invalidate({ + immediately: true, + }); + this.stores.news.latestNewsRequest.invalidate({ + immediately: true, + }); + }, ms('60m')); // Check for updates once every 4 hours setInterval(() => this._checkForUpdates(), CHECK_INTERVAL); @@ -174,7 +190,9 @@ export default class AppStore extends Store { // Handle deep linking (franz://) ipcRenderer.on('navigateFromDeepLink', (event, data) => { debug('Navigate from deep link', data); - let { url } = data; + let { + url, + } = data; if (!url) return; url = url.replace(/\/$/, ''); @@ -207,13 +225,17 @@ export default class AppStore extends Store { }); powerMonitor.on('resume', () => { - debug('System resumed, last suspended on', this.timeSuspensionStart.toString()); + debug('System resumed, last suspended on', this.timeSuspensionStart); + this.actions.service.resetLastPollTimer(); if (this.timeSuspensionStart.add(10, 'm').isBefore(moment()) && this.stores.settings.app.get('reloadAfterResume')) { debug('Reloading services, user info and features'); - setTimeout(() => { - window.location.reload(); + setInterval(() => { + debug('Reload app interval is starting'); + if (this.isOnline) { + window.location.reload(); + } }, ms('2s')); } }); @@ -251,8 +273,14 @@ export default class AppStore extends Store { ferdi: { version: app.getVersion(), electron: process.versions.electron, - installedRecipes: this.stores.recipes.all.map(recipe => ({ id: recipe.id, version: recipe.version })), - devRecipes: this.stores.recipePreviews.dev.map(recipe => ({ id: recipe.id, version: recipe.version })), + installedRecipes: this.stores.recipes.all.map(recipe => ({ + id: recipe.id, + version: recipe.version, + })), + devRecipes: this.stores.recipePreviews.dev.map(recipe => ({ + id: recipe.id, + version: recipe.version, + })), services: this.stores.services.all.map(service => ({ id: service.id, recipe: service.recipe.id, @@ -264,7 +292,10 @@ export default class AppStore extends Store { isDarkModeEnabled: service.isDarkModeEnabled, })), messages: this.stores.globalError.messages, - workspaces: this.stores.workspaces.workspaces.map(workspace => ({ id: workspace.id, services: workspace.services })), + workspaces: this.stores.workspaces.workspaces.map(workspace => ({ + id: workspace.id, + services: workspace.services, + })), windowSettings: readJsonSync(path.join(app.getPath('userData'), 'window-state.json')), settings, features: this.stores.features.features, @@ -275,7 +306,10 @@ export default class AppStore extends Store { // Actions @action _notify({ - title, options, notificationId, serviceId = null, + title, + options, + notificationId, + serviceId = null, }) { if (this.stores.settings.all.app.isAppMuted) return; @@ -288,15 +322,17 @@ export default class AppStore extends Store { debug('New notification', title, options); - notification.onclick = (e) => { + notification.onclick = () => { if (serviceId) { this.actions.service.sendIPCMessage({ channel: `notification-onclick:${notificationId}`, - args: e, + args: {}, serviceId, }); - this.actions.service.setActive({ serviceId }); + this.actions.service.setActive({ + serviceId, + }); mainWindow.show(); if (app.mainWindow.isMinimized()) { mainWindow.restore(); @@ -308,7 +344,10 @@ export default class AppStore extends Store { }; } - @action _setBadge({ unreadDirectMessageCount, unreadIndirectMessageCount }) { + @action _setBadge({ + unreadDirectMessageCount, + unreadIndirectMessageCount, + }) { let indicator = unreadDirectMessageCount; if (indicator === 0 && unreadIndirectMessageCount !== 0) { @@ -319,10 +358,14 @@ export default class AppStore extends Store { indicator = parseInt(indicator, 10); } - ipcRenderer.send('updateAppIndicator', { indicator }); + ipcRenderer.send('updateAppIndicator', { + indicator, + }); } - @action _launchOnStartup({ enable }) { + @action _launchOnStartup({ + enable, + }) { this.autoLaunchOnStart = enable; try { @@ -338,7 +381,9 @@ export default class AppStore extends Store { } } - @action _openExternalUrl({ url }) { + @action _openExternalUrl({ + url, + }) { const parsedUrl = new URL(url); debug('open external url', parsedUrl); @@ -348,14 +393,20 @@ export default class AppStore extends Store { } @action _checkForUpdates() { - this.updateStatus = this.updateStatusTypes.CHECKING; - ipcRenderer.send('autoUpdate', { action: 'check' }); + if (this.isOnline) { + this.updateStatus = this.updateStatusTypes.CHECKING; + ipcRenderer.send('autoUpdate', { + action: 'check', + }); - this.actions.recipe.update(); + this.actions.recipe.update(); + } } @action _installUpdate() { - ipcRenderer.send('autoUpdate', { action: 'install' }); + ipcRenderer.send('autoUpdate', { + action: 'install', + }); } @action _resetUpdateStatus() { @@ -366,7 +417,10 @@ export default class AppStore extends Store { this.healthCheckRequest.execute(); } - @action _muteApp({ isMuted, overrideSystemMute = true }) { + @action _muteApp({ + isMuted, + overrideSystemMute = true, + }) { this.isSystemMuteOverridden = overrideSystemMute; this.actions.settings.update({ type: 'app', @@ -377,7 +431,9 @@ export default class AppStore extends Store { } @action _toggleMuteApp() { - this._muteApp({ isMuted: !this.stores.settings.all.app.isAppMuted }); + this._muteApp({ + isMuted: !this.stores.settings.all.app.isAppMuted, + }); } @action async _clearAllCache() { @@ -391,7 +447,9 @@ export default class AppStore extends Store { } catch (ex) { console.log('Error while deleting service partition directory - ', ex); } - await Promise.all(this.stores.services.all.map(s => this.actions.service.clearCache({ serviceId: s.id }))); + await Promise.all(this.stores.services.all.map(s => this.actions.service.clearCache({ + serviceId: s.id, + }))); await clearAppCache._promise; @@ -446,7 +504,10 @@ export default class AppStore extends Store { const { showMessageBadgesEvenWhenMuted } = this.stores.ui; if (!showMessageBadgesEvenWhenMuted) { - this.actions.app.setBadge({ unreadDirectMessageCount: 0, unreadIndirectMessageCount: 0 }); + this.actions.app.setBadge({ + unreadDirectMessageCount: 0, + unreadIndirectMessageCount: 0, + }); } } @@ -491,8 +552,11 @@ export default class AppStore extends Store { return autoLauncher.isEnabled() || false; } - _systemDND() { - const dnd = getDoNotDisturb(); + async _systemDND() { + debug('Checking if Do Not Disturb Mode is on'); + const dnd = await ipcRenderer.invoke('get-dnd'); + debug('Do not disturb mode is', dnd); + // ipcRenderer.on('autoUpdate', (event, data) => { if (dnd !== this.stores.settings.all.app.isAppMuted && !this.isSystemMuteOverridden) { this.actions.app.muteApp({ isMuted: dnd, diff --git a/src/stores/RecipesStore.js b/src/stores/RecipesStore.js index cf5d0a074..965aa3a0a 100644 --- a/src/stores/RecipesStore.js +++ b/src/stores/RecipesStore.js @@ -66,7 +66,7 @@ export default class RecipesStore extends Store { } // Actions - @action async _install({ recipeId }) { + async _install({ recipeId }) { const recipe = await this.installRecipeRequest.execute(recipeId)._promise; await this.allRecipesRequest.invalidate({ immediately: true })._promise; diff --git a/src/stores/ServicesStore.js b/src/stores/ServicesStore.js index 448260638..21ed0a234 100644 --- a/src/stores/ServicesStore.js +++ b/src/stores/ServicesStore.js @@ -20,6 +20,8 @@ import { workspaceStore } from '../features/workspaces'; import { serviceLimitStore } from '../features/serviceLimit'; import { RESTRICTION_TYPES } from '../models/Service'; import { KEEP_WS_LOADED_USID } from '../config'; +import { TODOS_RECIPE_ID } from '../features/todos'; +import { SPELLCHECKER_LOCALES } from '../i18n/languages'; const debug = require('debug')('Ferdi:ServiceStore'); @@ -82,7 +84,9 @@ export default class ServicesStore extends Store { this.actions.service.toggleAudio.listen(this._toggleAudio.bind(this)); this.actions.service.openDevTools.listen(this._openDevTools.bind(this)); this.actions.service.openDevToolsForActiveService.listen(this._openDevToolsForActiveService.bind(this)); - this.actions.service.setHibernation.listen(this._setHibernation.bind(this)); + this.actions.service.hibernate.listen(this._hibernate.bind(this)); + this.actions.service.awake.listen(this._awake.bind(this)); + this.actions.service.resetLastPollTimer.listen(this._resetLastPollTimer.bind(this)); this.actions.service.shareSettingsWithServiceProcess.listen(this._shareSettingsWithServiceProcess.bind(this)); this.registerReactions([ @@ -93,6 +97,7 @@ export default class ServicesStore extends Store { this._logoutReaction.bind(this), this._handleMuteSettings.bind(this), this._restrictServiceAccess.bind(this), + this._checkForActiveService.bind(this), ]); // Just bind this @@ -155,16 +160,24 @@ export default class ServicesStore extends Store { */ _serviceMaintenance() { this.all.forEach((service) => { - if (service.lastPoll && (service.lastPoll) - service.lastPollAnswer > ms('30s')) { - // If service did not reply for more than 30s try to reload. + // Defines which services should be hibernated. + if (!service.isActive && (Date.now() - service.lastUsed > ms('5m'))) { + // If service is stale for 5 min, hibernate it. + this._hibernate({ serviceId: service.id }); + } + + if (service.lastPoll && (service.lastPoll - service.lastPollAnswer > ms('1m'))) { + // If service did not reply for more than 1m try to reload. if (!service.isActive) { if (this.stores.app.isOnline && service.lostRecipeReloadAttempt < 3) { - service.webview.reload(); + debug(`Reloading service: ${service.name} (${service.id}). Attempt: ${service.lostRecipeReloadAttempt}`); + // service.webview.reload(); service.lostRecipeReloadAttempt += 1; service.lostRecipeConnection = false; } } else { + debug(`Service lost connection: ${service.name} (${service.id}).`); service.lostRecipeConnection = true; } } else { @@ -256,6 +269,14 @@ export default class ServicesStore extends Store { return null; } + @computed get isTodosServiceAdded() { + return this.allDisplayed.find(service => service.recipe.id === TODOS_RECIPE_ID && service.isEnabled) || null; + } + + @computed get isTodosServiceActive() { + return this.active && this.active.recipe.id === TODOS_RECIPE_ID; + } + one(id) { return this.all.find(service => service.id === id); } @@ -265,10 +286,34 @@ export default class ServicesStore extends Store { } // Actions - @action async _createService({ recipeId, serviceData, redirect = true }) { + async _createService({ + recipeId, serviceData, redirect = true, skipCleanup = false, + }) { if (serviceLimitStore.userHasReachedServiceLimit) return; - const data = this._cleanUpTeamIdAndCustomUrl(recipeId, serviceData); + if (!this.stores.recipes.isInstalled(recipeId)) { + debug(`Recipe "${recipeId}" is not installed, installing recipe`); + await this.stores.recipes._install({ recipeId }); + debug(`Recipe "${recipeId}" installed`); + } + + // set default values for serviceData + Object.assign({ + isEnabled: true, + isHibernationEnabled: false, + isNotificationEnabled: true, + isBadgeEnabled: true, + isMuted: false, + customIcon: false, + isDarkModeEnabled: false, + spellcheckerLanguage: SPELLCHECKER_LOCALES[this.stores.settings.app.spellcheckerLanguage], + }, serviceData); + + let data = serviceData; + + if (!skipCleanup) { + data = this._cleanUpTeamIdAndCustomUrl(recipeId, serviceData); + } const response = await this.createServiceRequest.execute(recipeId, data)._promise; @@ -427,8 +472,13 @@ export default class ServicesStore extends Store { this.all[index].isActive = false; }); service.isActive = true; + this._awake({ serviceId: service.id }); service.lastUsed = Date.now(); + if (this.active.recipe.id === TODOS_RECIPE_ID && !this.stores.todos.settings.isFeatureEnabledByUser) { + this.actions.todos.toggleTodosFeatureVisibility(); + } + // Update list of last used services this.lastUsedServices = this.lastUsedServices.filter(id => id !== serviceId); this.lastUsedServices.unshift(serviceId); @@ -617,10 +667,10 @@ export default class ServicesStore extends Store { @action _sendIPCMessage({ serviceId, channel, args }) { const service = this.one(serviceId); - if (service.webview) { - // Make sure the args are clean, otherwise ElectronJS can't transmit them - const cleanArgs = JSON.parse(JSON.stringify(args)); + // Make sure the args are clean, otherwise ElectronJS can't transmit them + const cleanArgs = JSON.parse(JSON.stringify(args)); + if (service.webview) { service.webview.send(channel, cleanArgs); } } @@ -659,8 +709,11 @@ export default class ServicesStore extends Store { service.resetMessageCount(); service.lostRecipeConnection = false; - // service.webview.loadURL(service.url); - service.webview.reload(); + if (service.recipe.id === TODOS_RECIPE_ID) { + return this.actions.todos.reload(); + } + + return service.webview.loadURL(service.url); } @action _reloadActive() { @@ -743,23 +796,57 @@ export default class ServicesStore extends Store { @action _openDevTools({ serviceId }) { const service = this.one(serviceId); - - service.webview.openDevTools(); + if (service.recipe.id === TODOS_RECIPE_ID) { + this.actions.todos.openDevTools(); + } else { + service.webview.openDevTools(); + } } @action _openDevToolsForActiveService() { const service = this.active; if (service) { - service.webview.openDevTools(); + this._openDevTools({ serviceId: service.id }); } else { debug('No service is active'); } } - @action _setHibernation({ serviceId, hibernating }) { + @action _hibernate({ serviceId }) { + const service = this.one(serviceId); + if (service.isActive || !service.isHibernationEnabled) { + debug('Skipping service hibernation'); + return; + } + + debug(`Hibernate ${service.name}`); + + service.isHibernating = true; + } + + @action _awake({ serviceId }) { const service = this.one(serviceId); - service.isHibernating = hibernating; + service.isHibernating = false; + service.liveFrom = Date.now(); + } + + @action _resetLastPollTimer({ serviceId = null }) { + debug(`Reset last poll timer for ${serviceId ? `service: "${serviceId}"` : 'all services'}`); + + const resetTimer = (service) => { + service.lastPollAnswer = Date.now(); + service.lastPoll = Date.now(); + }; + + if (!serviceId) { + this.allDisplayed.forEach(service => resetTimer(service)); + } else { + const service = this.one(serviceId); + if (service) { + resetTimer(service); + } + } } // Reactions @@ -893,6 +980,18 @@ export default class ServicesStore extends Store { }); } + _checkForActiveService() { + if (!this.stores.router.location || this.stores.router.location.pathname.includes('auth/signup')) { + return; + } + + if (this.allDisplayed.findIndex(service => service.isActive) === -1 && this.allDisplayed.length !== 0) { + debug('No active service found, setting active service to index 0'); + + this._setActive({ serviceId: this.allDisplayed[0].id }); + } + } + // Helper _initializeServiceRecipeInWebview(serviceId) { const service = this.one(serviceId); diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js index c1ed2944a..7b4d39524 100644 --- a/src/stores/UserStore.js +++ b/src/stores/UserStore.js @@ -3,6 +3,7 @@ import moment from 'moment'; import jwt from 'jsonwebtoken'; import localStorage from 'mobx-localstorage'; import ms from 'ms'; +import { remote } from 'electron'; import { isDevMode } from '../environment'; import Store from './lib/Store'; @@ -11,6 +12,9 @@ import CachedRequest from './lib/CachedRequest'; import { sleep } from '../helpers/async-helpers'; import { getPlan } from '../helpers/plan-helpers'; import { PLANS } from '../config'; +import { TODOS_PARTITION_ID } from '../features/todos'; + +const { session } = remote; const debug = require('debug')('Ferdi:UserStore'); @@ -28,6 +32,8 @@ export default class UserStore extends Store { PRICING_ROUTE = `${this.BASE_ROUTE}/signup/pricing`; + SETUP_ROUTE = `${this.BASE_ROUTE}/signup/setup`; + IMPORT_ROUTE = `${this.BASE_ROUTE}/signup/import`; INVITE_ROUTE = `${this.BASE_ROUTE}/signup/invite`; @@ -127,6 +133,10 @@ export default class UserStore extends Store { return this.PRICING_ROUTE; } + get setupRoute() { + return this.SETUP_ROUTE; + } + get inviteRoute() { return this.INVITE_ROUTE; } @@ -227,7 +237,7 @@ export default class UserStore extends Store { this._setUserData(authToken); - this.stores.router.push('/'); + this.stores.router.push(this.SETUP_ROUTE); } @action async _retrievePassword({ email }) { @@ -285,6 +295,13 @@ export default class UserStore extends Store { this.getUserInfoRequest.invalidate().reset(); this.authToken = null; + + this.stores.services.allServicesRequest.invalidate().reset(); + + if (this.stores.todos.isTodosEnabled) { + const sess = session.fromPartition(TODOS_PARTITION_ID); + sess.clearStorageData(); + } } @action async _importLegacyServices({ services }) { -- cgit v1.2.3-70-g09d2