diff --git a/customize.dist/loading.js b/customize.dist/loading.js index af73a9b42..72ebeda8b 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -325,9 +325,30 @@ button:not(.btn).primary:hover{ }; var hasErrored = false; + var isOffline = false; var updateLoadingProgress = function (data) { if (!built || !data) { return; } + // If we receive a "offline" event, show the warning text + if (data.type === "offline") { + try { + isOffline = true; + Messages.offlineError = "OFFLINE MODE NOT AVAILABLE"; // XXX + document.querySelector('#cp-loading-message').setAttribute('style', 'display:block;'); + document.querySelector('#cp-loading-message').innerText = Messages.offlineError; + } catch (e) { console.error(e); } + return; + } + + // If we receive a new event and we were offline, remove + // the offline warning text + if (isOffline) { + try { + isOffline = false; + document.querySelector('#cp-loading-message').setAttribute('style', 'display:none;'); + } catch (e) { console.error(e); } + } + // Make sure progress doesn't go backward var c = types.indexOf(data.type); if (c < current) { return console.debug(data); } diff --git a/lib/env.js b/lib/env.js index 322f629c6..97d3893f9 100644 --- a/lib/env.js +++ b/lib/env.js @@ -14,6 +14,7 @@ const Util = require("./common-util"); module.exports.create = function (config) { const Env = { + OFFLINE_MODE: false, FRESH_KEY: '', FRESH_MODE: true, DEV_MODE: false, @@ -117,6 +118,9 @@ module.exports.create = function (config) { //console.log("FRESH MODE ENABLED"); Env.FRESH_KEY = +new Date(); } + + // Offline mode is mostly for development. It lets us test clientside cache and offline support + if (process.env.OFFLINE) { Env.OFFLINE_MODE = true; } }()); Env.checkCache = function (channel) { diff --git a/package.json b/package.json index 24041cb7b..110e7d828 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "start": "node server.js", "dev": "DEV=1 node server.js", "fresh": "FRESH=1 node server.js", + "offline": "FRESH=1 OFFLINE=1 node server.js", "package": "PACKAGE=1 node server.js", "lint": "jshint --config .jshintrc --exclude-path .jshintignore . && ./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/", "lint:js": "jshint --config .jshintrc --exclude-path .jshintignore .", diff --git a/server.js b/server.js index 3869af509..9e26500ad 100644 --- a/server.js +++ b/server.js @@ -313,6 +313,7 @@ nThen(function (w) { Env.Log = _log; config.log = _log; + if (Env.OFFLINE_MODE) { return; } if (config.externalWebsocketURL) { return; } require("./lib/api").create(Env); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index d7cee2418..c0afeab10 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -223,6 +223,7 @@ define([ }; }; + UIElements.noContactsMessage = function (common) { var metadataMgr = common.getMetadataMgr(); var data = metadataMgr.getUserData(); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 006f150d7..4c64182ee 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -453,10 +453,35 @@ define([ }); }; - common.getFileSize = function (href, password, cb) { - postMessage("GET_FILE_SIZE", {href: href, password: password}, function (obj) { - if (obj && obj.error) { return void cb(obj.error); } - cb(undefined, obj.size); + common.getFileSize = function (href, password, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + var channel = Hash.hrefToHexChannelId(href, password); + var error; + Nthen(function (waitFor) { + // Blobs can't change, if it's in the cache, use it + Cache.getBlobCache(channel, waitFor(function(err, blob) { + if (err) { return; } + waitFor.abort(); + cb(null, blob.length); + })); + + }).nThen(function (waitFor) { + // If it's not in the cache or it's not a blob, try to get the value from the server + postMessage("GET_FILE_SIZE", {channel:channel}, waitFor(function (obj) { + if (obj && obj.error) { + // If disconnected, try to get the value from the channel cache (next nThen) + error = obj.error; + return; + } + waitFor.abort(); + cb(undefined, obj.size); + })); + }).nThen(function () { + Cache.getChannelCache(channel, function(err, data) { + if (err) { return void cb(error); } + var size = data && Array.isArray(data.c) && data.c.join('').length; + cb(null, size || 0); + }); }); }; @@ -467,11 +492,37 @@ define([ }); }; - common.isNewChannel = function (href, password, cb) { - postMessage('IS_NEW_CHANNEL', {href: href, password: password}, function (obj) { - if (obj.error) { return void cb(obj.error); } - if (!obj) { return void cb('INVALID_RESPONSE'); } - cb(undefined, obj.isNew); + // This function is used when we want to open a pad. We first need + // to check if it exists. With the cached drive, we need to wait for + // the network to be available before we can continue. + common.isNewChannel = function (href, password, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + var channel = Hash.hrefToHexChannelId(href, password); + var error; + Nthen(function (waitFor) { + Cache.getChannelCache(channel, waitFor(function(err, data) { + if (err || !data) { return; } + waitFor.abort(); + cb(undefined, false); + })); + }).nThen(function () { + // If it's not in the cache try to get the value from the server + var isNew = function () { + error = undefined; + postMessage('IS_NEW_CHANNEL', {channel: channel}, function (obj) { + if (obj && obj.error) { error = obj.error; } + if (!obj) { error = "INVALID_RESPONSE"; } + + if (error === "ANON_RPC_NOT_READY") { + // Try again in 1s + return void setTimeout(isNew, 100); + } else if (error) { + return void cb(error); + } + cb(undefined, obj.isNew); + }, {timeout: -1}); + }; + isNew(); }); }; @@ -927,9 +978,11 @@ define([ // Get data about a given channel: use with hidden hashes common.getPadDataFromChannel = function (obj, cb) { if (!obj || !obj.channel) { return void cb('EINVAL'); } + // Note: no timeout for this command, we may only have loaded the cached drive + // and need to wait for the fully synced drive postMessage("GET_PAD_DATA_FROM_CHANNEL", obj, function (data) { cb(void 0, data); - }); + }, {timeout: -1}); }; @@ -1911,6 +1964,64 @@ define([ }); }; + + var provideFeedback = function () { + if (typeof(window.Proxy) === 'undefined') { + Feedback.send("NO_PROXIES"); + } + + if (!common.isWebRTCSupported()) { + Feedback.send("NO_WEBRTC"); + } + + var shimPattern = /CRYPTPAD_SHIM/; + if (shimPattern.test(Array.isArray.toString())) { + Feedback.send("NO_ISARRAY"); + } + + if (shimPattern.test(Array.prototype.fill.toString())) { + Feedback.send("NO_ARRAYFILL"); + } + + if (typeof(Symbol) === 'undefined') { + Feedback.send('NO_SYMBOL'); + } + + if (typeof(SharedWorker) === "undefined") { + Feedback.send('NO_SHAREDWORKER'); + } else { + Feedback.send('SHAREDWORKER'); + } + if (typeof(Worker) === "undefined") { + Feedback.send('NO_WEBWORKER'); + } + if (!('serviceWorker' in navigator)) { + Feedback.send('NO_SERVICEWORKER'); + } + if (!common.hasCSSVariables()) { + Feedback.send('NO_CSS_VARIABLES'); + } + + Feedback.reportScreenDimensions(); + Feedback.reportLanguage(); + }; + var initFeedback = function (feedback) { + // Initialize feedback + Feedback.init(feedback); + provideFeedback(); + }; + var onStoreReady = function (data) { + if (common.userHash) { + var localToken = tryParsing(localStorage.getItem(Constants.tokenKey)); + if (localToken === null) { + // if that number hasn't been set to localStorage, do so. + localStorage.setItem(Constants.tokenKey, data[Constants.tokenKey]); + } + } + + initFeedback(data.feedback); + }; + common.startAccountDeletion = function (data, cb) { // Logout other tabs LocalStore.logout(null, true); @@ -1957,6 +2068,8 @@ define([ var localToken = tryParsing(localStorage.getItem(Constants.tokenKey)); if (localToken !== data.token) { requestLogin(); } }, + // Store + STORE_READY: onStoreReady, // Network NETWORK_DISCONNECT: common.onNetworkDisconnect.fire, NETWORK_RECONNECT: function (data) { @@ -2038,52 +2151,6 @@ define([ return void setTimeout(function () { f(void 0, env); }); } - var provideFeedback = function () { - if (typeof(window.Proxy) === 'undefined') { - Feedback.send("NO_PROXIES"); - } - - if (!common.isWebRTCSupported()) { - Feedback.send("NO_WEBRTC"); - } - - var shimPattern = /CRYPTPAD_SHIM/; - if (shimPattern.test(Array.isArray.toString())) { - Feedback.send("NO_ISARRAY"); - } - - if (shimPattern.test(Array.prototype.fill.toString())) { - Feedback.send("NO_ARRAYFILL"); - } - - if (typeof(Symbol) === 'undefined') { - Feedback.send('NO_SYMBOL'); - } - - if (typeof(SharedWorker) === "undefined") { - Feedback.send('NO_SHAREDWORKER'); - } else { - Feedback.send('SHAREDWORKER'); - } - if (typeof(Worker) === "undefined") { - Feedback.send('NO_WEBWORKER'); - } - if (!('serviceWorker' in navigator)) { - Feedback.send('NO_SERVICEWORKER'); - } - if (!common.hasCSSVariables()) { - Feedback.send('NO_CSS_VARIABLES'); - } - - Feedback.reportScreenDimensions(); - Feedback.reportLanguage(); - }; - var initFeedback = function (feedback) { - // Initialize feedback - Feedback.init(feedback); - provideFeedback(); - }; - var userHash; (function iOSFirefoxFix () { @@ -2158,8 +2225,10 @@ define([ anonHash: LocalStore.getFSHash(), localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), // TODO move this to LocalStore ? language: common.getLanguage(), + cache: rdyCfg.cache, driveEvents: true //rdyCfg.driveEvents // Boolean }; + common.userHash = userHash; // FIXME Backward compatibility if (sessionStorage.newPadFileData) { @@ -2354,15 +2423,6 @@ define([ if (data.anonHash && !cfg.userHash) { LocalStore.setFSHash(data.anonHash); } - if (cfg.userHash) { - var localToken = tryParsing(localStorage.getItem(Constants.tokenKey)); - if (localToken === null) { - // if that number hasn't been set to localStorage, do so. - localStorage.setItem(Constants.tokenKey, data[Constants.tokenKey]); - } - } - - initFeedback(data.feedback); initialized = true; channelIsReady(); }); diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 1c96ab17d..4918269a8 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -42,7 +42,7 @@ define([ var APP = window.APP = { editable: false, - online: true, + online: false, mobile: function () { if (window.matchMedia) { return !window.matchMedia('(any-pointer:fine)').matches; } else { return $('body').width() <= 600; } @@ -3936,7 +3936,8 @@ define([ var newRoot = Util.find(manager, ['folders', sfId, 'proxy', manager.user.userObject.ROOT]) || {}; subfolder = manager.hasSubfolder(newRoot); // Fix name - key = manager.getSharedFolderData(sfId).title || Messages.fm_deletedFolder; + var sfData = manager.getSharedFolderData(sfId); + key = sfData.title || sfData.lastTitle || Messages.fm_deletedFolder; // Fix icon $icon = isCurrentFolder ? $sharedFolderOpenedIcon : $sharedFolderIcon; isSharedFolder = sfId; @@ -4363,8 +4364,12 @@ define([ var anonDrive = manager.isPathIn(currentPath, [FILES_DATA]) && !APP.loggedIn; if (manager.isFolder(el) && !manager.isSharedFolder(el) && !anonDrive) { // Folder + // disconnected + if (!APP.editable) { + return void UI.warn(Messages.error); // XXX + } // if folder is inside SF - if (manager.isInSharedFolder(paths[0].path)) { + else if (manager.isInSharedFolder(paths[0].path)) { return void UI.alert(Messages.convertFolderToSF_SFParent); } // if folder already contains SF diff --git a/www/common/inner/access.js b/www/common/inner/access.js index c0967c139..f06028d8e 100644 --- a/www/common/inner/access.js +++ b/www/common/inner/access.js @@ -776,8 +776,14 @@ define([ var sframeChan = common.getSframeChannel(); var metadataMgr = common.getMetadataMgr(); + var priv = metadataMgr.getPrivateData(); var $div = $(h('div.cp-share-columns')); + + if (priv.offline) { + $div.append(h('p', Messages.access_offline)); + return void cb(void 0, $div); + } if (!data) { return void cb(void 0, $div); } var div1 = h('div.cp-usergrid-user.cp-share-column.cp-access'); @@ -807,130 +813,139 @@ define([ ])); } - $('