diff --git a/src/components/App.vue b/src/components/App.vue index bd4b2f7a..1b93dccd 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -14,6 +14,8 @@ import Modal from './Modal'; import Notification from './Notification'; import SplashScreen from './SplashScreen'; import syncSvc from '../services/syncSvc'; +import networkSvc from '../services/networkSvc'; +import sponsorSvc from '../services/sponsorSvc'; import timeSvc from '../services/timeSvc'; import store from '../store'; @@ -77,6 +79,8 @@ export default { created() { syncSvc.init() .then(() => { + networkSvc.init(); + sponsorSvc.init(); this.ready = true; }); }, diff --git a/src/services/localDbSvc.js b/src/services/localDbSvc.js index b287cb18..406dff36 100644 --- a/src/services/localDbSvc.js +++ b/src/services/localDbSvc.js @@ -4,7 +4,6 @@ import store from '../store'; import welcomeFile from '../data/welcomeFile.md'; const dbVersion = 1; -const dbVersionKey = `${utils.workspaceId}/localDbVersion`; const dbStoreName = 'objects'; const exportBackup = utils.queryParams.exportBackup; if (exportBackup) { @@ -15,11 +14,18 @@ const deleteMarkerMaxAge = 1000; const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec class Connection { - constructor(dbName) { + constructor() { this.getTxCbs = []; + // Make the DB name + const workspaceId = store.getters['workspace/currentWorkspace'].id; + this.dbName = 'stackedit-db'; + if (workspaceId !== 'main') { + this.dbName += `-${workspaceId}`; + } + // Init connection - const request = indexedDB.open(dbName, dbVersion); + const request = indexedDB.open(this.dbName, dbVersion); request.onerror = () => { throw new Error("Can't connect to IndexedDB."); @@ -27,7 +33,6 @@ class Connection { request.onsuccess = (event) => { this.db = event.target.result; - localStorage[dbVersionKey] = this.db.version; // Safari does not support onversionchange this.db.onversionchange = () => window.location.reload(); this.getTxCbs.forEach(({ onTx, onError }) => this.createTx(onTx, onError)); @@ -67,11 +72,6 @@ class Connection { return this.getTxCbs.push({ onTx, onError }); } - // If DB version has changed (Safari support) - if (parseInt(localStorage[dbVersionKey], 10) !== this.db.version) { - return window.location.reload(); - } - // Open transaction in read/write will prevent conflict with other tabs const tx = this.db.transaction(this.db.objectStoreNames, 'readwrite'); tx.onerror = onError; @@ -102,7 +102,7 @@ const localDbSvc = { */ init() { // Create the connection - this.connection = new Connection(store.getters['data/dbName']); + this.connection = new Connection(); // Load the DB return localDbSvc.sync() @@ -131,7 +131,7 @@ const localDbSvc = { // If app was last opened 7 days ago and synchronization is off if (!store.getters['data/loginToken'] && - (utils.lastOpened + utils.cleanTrashAfter < Date.now()) + (store.getters['workspace/lastFocus'] + utils.cleanTrashAfter < Date.now()) ) { // Clean files store.getters['file/items'] @@ -447,7 +447,6 @@ const localDbSvc = { request.onsuccess = resolve; }) .then(() => { - localStorage.removeItem(dbVersionKey); window.location.reload(); }, () => store.dispatch('notification/error', 'Could not delete local database.')); }, diff --git a/src/services/networkSvc.js b/src/services/networkSvc.js index 53dd0ab7..82e44b80 100644 --- a/src/services/networkSvc.js +++ b/src/services/networkSvc.js @@ -5,8 +5,88 @@ const scriptLoadingPromises = Object.create(null); const oauth2AuthorizationTimeout = 120 * 1000; // 2 minutes const networkTimeout = 30 * 1000; // 30 sec let isConnectionDown = false; +const userInactiveAfter = 2 * 60 * 1000; // 2 minutes + export default { + init() { + // Keep track of the last user activity + this.lastActivity = 0; + const setLastActivity = () => { + this.lastActivity = Date.now(); + }; + window.document.addEventListener('mousedown', setLastActivity); + window.document.addEventListener('keydown', setLastActivity); + window.document.addEventListener('touchstart', setLastActivity); + + // Keep track of the last window focus + this.lastFocus = 0; + const setLastFocus = () => { + this.lastFocus = Date.now(); + localStorage.setItem(store.getters['workspace/lastFocusKey'], this.lastFocus); + setLastActivity(); + }; + if (document.hasFocus()) { + setLastFocus(); + } + window.addEventListener('focus', setLastFocus); + + // Check browser is online periodically + const checkOffline = () => { + const isBrowserOffline = window.navigator.onLine === false; + if (!isBrowserOffline && + store.state.lastOfflineCheck + networkTimeout + 5000 < Date.now() && + this.isUserActive() + ) { + store.commit('updateLastOfflineCheck'); + new Promise((resolve, reject) => { + const script = document.createElement('script'); + let timeout; + let clean = (cb) => { + clearTimeout(timeout); + document.head.removeChild(script); + clean = () => {}; // Prevent from cleaning several times + cb(); + }; + script.onload = () => clean(resolve); + script.onerror = () => clean(reject); + script.src = `https://apis.google.com/js/api.js?${Date.now()}`; + try { + document.head.appendChild(script); // This can fail with bad network + timeout = setTimeout(() => clean(reject), networkTimeout); + } catch (e) { + reject(e); + } + }) + .then(() => { + isConnectionDown = false; + }, () => { + isConnectionDown = true; + }); + } + const offline = isBrowserOffline || isConnectionDown; + if (store.state.offline !== offline) { + store.commit('setOffline', offline); + if (offline) { + store.dispatch('notification/error', 'You are offline.'); + } else { + store.dispatch('notification/info', 'You are back online!'); + } + } + }; + utils.setInterval(checkOffline, 1000); + window.addEventListener('online', () => { + isConnectionDown = false; + checkOffline(); + }); + window.addEventListener('offline', checkOffline); + }, + isWindowFocused() { + return parseInt(localStorage.getItem(this.lastFocusKey), 10) === this.lastFocus; + }, + isUserActive() { + return this.lastActivity > Date.now() - userInactiveAfter && this.isWindowFocused(); + }, loadScript(url) { if (!scriptLoadingPromises[url]) { scriptLoadingPromises[url] = new Promise((resolve, reject) => { @@ -217,53 +297,3 @@ export default { return attempt(); }, }; - -function checkOffline() { - const isBrowserOffline = window.navigator.onLine === false; - if (!isBrowserOffline && - store.state.lastOfflineCheck + networkTimeout + 5000 < Date.now() && - utils.isUserActive() - ) { - store.commit('updateLastOfflineCheck'); - new Promise((resolve, reject) => { - const script = document.createElement('script'); - let timeout; - let clean = (cb) => { - clearTimeout(timeout); - document.head.removeChild(script); - clean = () => {}; // Prevent from cleaning several times - cb(); - }; - script.onload = () => clean(resolve); - script.onerror = () => clean(reject); - script.src = `https://apis.google.com/js/api.js?${Date.now()}`; - try { - document.head.appendChild(script); // This can fail with bad network - timeout = setTimeout(() => clean(reject), networkTimeout); - } catch (e) { - reject(e); - } - }) - .then(() => { - isConnectionDown = false; - }, () => { - isConnectionDown = true; - }); - } - const offline = isBrowserOffline || isConnectionDown; - if (store.state.offline !== offline) { - store.commit('setOffline', offline); - if (offline) { - store.dispatch('notification/error', 'You are offline.'); - } else { - store.dispatch('notification/info', 'You are back online!'); - } - } -} - -utils.setInterval(checkOffline, 1000); -window.addEventListener('online', () => { - isConnectionDown = false; - checkOffline(); -}); -window.addEventListener('offline', checkOffline); diff --git a/src/services/publishSvc.js b/src/services/publishSvc.js index cd6a94a6..a0886487 100644 --- a/src/services/publishSvc.js +++ b/src/services/publishSvc.js @@ -1,6 +1,7 @@ import localDbSvc from './localDbSvc'; import store from '../store'; import utils from './utils'; +import networkSvc from './networkSvc'; import exportSvc from './exportSvc'; import providerRegistry from './providers/providerRegistry'; @@ -114,7 +115,7 @@ function requestPublish() { let intervalId; const attempt = () => { // Only start publishing when these conditions are met - if (utils.isUserActive()) { + if (networkSvc.isUserActive()) { clearInterval(intervalId); if (!hasCurrentFilePublishLocations()) { // Cancel sync diff --git a/src/services/sponsorSvc.js b/src/services/sponsorSvc.js index c6f75462..b75d18bb 100644 --- a/src/services/sponsorSvc.js +++ b/src/services/sponsorSvc.js @@ -23,7 +23,7 @@ const isGoogleSponsor = () => { const checkPayment = () => { const currentDate = Date.now(); - if (!isGoogleSponsor() && utils.isUserActive() && !store.state.offline && + if (!isGoogleSponsor() && networkSvc.isUserActive() && !store.state.offline && lastCheck + checkPaymentEvery < currentDate ) { lastCheck = currentDate; @@ -39,9 +39,10 @@ const checkPayment = () => { } }; -utils.setInterval(checkPayment, 2000); - export default { + init: () => { + utils.setInterval(checkPayment, 2000); + }, getToken() { if (isGoogleSponsor() || store.state.offline) { return Promise.resolve(); diff --git a/src/services/syncSvc.js b/src/services/syncSvc.js index c53ad2e6..169da31a 100644 --- a/src/services/syncSvc.js +++ b/src/services/syncSvc.js @@ -2,6 +2,7 @@ import localDbSvc from './localDbSvc'; import store from '../store'; import utils from './utils'; import diffUtils from './diffUtils'; +import networkSvc from './networkSvc'; import providerRegistry from './providers/providerRegistry'; import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider'; @@ -14,9 +15,9 @@ let workspaceProvider; /** * Use a lock in the local storage to prevent multiple windows concurrency. */ -const lastSyncActivityKey = `${utils.workspaceId}/lastSyncActivity`; let lastSyncActivity; -const getLastStoredSyncActivity = () => parseInt(localStorage[lastSyncActivityKey], 10) || 0; +const getLastStoredSyncActivity = () => + parseInt(localStorage.getItem(store.getters['workspace/lastSyncActivityKey']), 10) || 0; /** * Return true if workspace sync is possible. @@ -51,7 +52,7 @@ function isSyncWindow() { } /** - * Return true if auto sync can start, ie that lastSyncActivity is old enough. + * Return true if auto sync can start, ie if lastSyncActivity is old enough. */ function isAutoSyncReady() { const storedLastSyncActivity = getLastStoredSyncActivity(); @@ -64,7 +65,7 @@ function isAutoSyncReady() { function setLastSyncActivity() { const currentDate = Date.now(); lastSyncActivity = currentDate; - localStorage[lastSyncActivityKey] = currentDate; + localStorage.setItem(store.getters['workspace/lastSyncActivityKey'], currentDate); } /** @@ -626,7 +627,7 @@ function requestSync() { let intervalId; const attempt = () => { // Only start syncing when these conditions are met - if (utils.isUserActive() && isSyncWindow()) { + if (networkSvc.isUserActive() && isSyncWindow()) { clearInterval(intervalId); if (!isSyncPossible()) { // Cancel sync @@ -704,7 +705,7 @@ export default { // Sync periodically utils.setInterval(() => { if (isSyncPossible() && - utils.isUserActive() && + networkSvc.isUserActive() && isSyncWindow() && isAutoSyncReady() ) { diff --git a/src/services/utils.js b/src/services/utils.js index 6975b0d9..052e94c0 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -2,7 +2,6 @@ import yaml from 'js-yaml'; import '../libs/clunderscore'; import defaultProperties from '../data/defaultFileProperties.yml'; -const workspaceId = 'main'; const origin = `${location.protocol}//${location.host}`; // For uid() @@ -12,30 +11,6 @@ const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ const radix = alphabet.length; const array = new Uint32Array(uidLength); -// For isUserActive -const inactiveAfter = 2 * 60 * 1000; // 2 minutes -let lastActivity; -const setLastActivity = () => { - lastActivity = Date.now(); -}; -window.document.addEventListener('mousedown', setLastActivity); -window.document.addEventListener('keydown', setLastActivity); -window.document.addEventListener('touchstart', setLastActivity); - -// For isWindowFocused -let lastFocus; -const lastFocusKey = `${workspaceId}/lastWindowFocus`; -const lastOpened = parseInt(localStorage[lastFocusKey], 10) || 0; -const setLastFocus = () => { - lastFocus = Date.now(); - localStorage[lastFocusKey] = lastFocus; - setLastActivity(); -}; -if (document.hasFocus()) { - setLastFocus(); -} -window.addEventListener('focus', setLastFocus); - // For parseQueryParams() const parseQueryParams = (params) => { const result = {}; @@ -50,11 +25,9 @@ const parseQueryParams = (params) => { const urlParser = window.document.createElement('a'); export default { - workspaceId, origin, queryParams: parseQueryParams(location.hash.slice(1)), oauth2RedirectUri: `${origin}/oauth2/callback`, - lastOpened, cleanTrashAfter: 7 * 24 * 60 * 60 * 1000, // 7 days types: [ 'contentState', @@ -147,12 +120,6 @@ export default { setInterval(func, interval) { return setInterval(() => func(), this.randomize(interval)); }, - isWindowFocused() { - return parseInt(localStorage[lastFocusKey], 10) === lastFocus; - }, - isUserActive() { - return lastActivity > Date.now() - inactiveAfter && this.isWindowFocused(); - }, parseQueryParams, addQueryParams(url = '', params = {}) { const keys = Object.keys(params).filter(key => params[key] != null); diff --git a/src/store/data.js b/src/store/data.js index ba9e7b58..efe2d807 100644 --- a/src/store/data.js +++ b/src/store/data.js @@ -131,17 +131,6 @@ export default { }); return result; }, - dbName: (state, getters) => { - let dbName; - Object.keys(getters.workspaces).some((id) => { - dbName = 'stackedit-db'; - if (id !== 'main') { - dbName += `-${id}`; - } - return dbName; - }); - return dbName; - }, settings: getter('settings'), computedSettings: (state, getters) => { const customSettings = yaml.safeLoad(getters.settings); diff --git a/src/store/workspace.js b/src/store/workspace.js index f5bf7dde..1bd4aea0 100644 --- a/src/store/workspace.js +++ b/src/store/workspace.js @@ -23,6 +23,15 @@ export default { const workspaces = rootGetters['data/workspaces']; return workspaces[state.currentWorkspaceId] || workspaces.main; }, + lastSyncActivityKey: (state, getters) => { + const workspaceId = getters.currentWorkspace.id; + return `${workspaceId}/lastSyncActivity`; + }, + lastFocusKey: (state, getters) => { + const workspaceId = getters.currentWorkspace.id; + return `${workspaceId}/lastWindowFocus`; + }, + lastFocus: (state, getters) => parseInt(localStorage.getItem(getters.lastFocusKey), 10) || 0, }, actions: { },