From 29f577dfc68bb4186eb548825eb9026d7e8bbb8b Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 19 Jun 2018 16:59:59 +0200 Subject: [PATCH] Improve history to load it chunk by chunk --- .../src/less2/include/toolbar-history.less | 72 +++-- customize.dist/translations/messages.fr.js | 7 +- customize.dist/translations/messages.js | 8 +- www/common/cryptpad-common.js | 3 + www/common/outer/async-store.js | 60 +++++ www/common/outer/store-rpc.js | 1 + www/common/sframe-common-history.js | 249 ++++++++++++------ www/common/sframe-common-outer.js | 18 ++ www/common/sframe-protocol.js | 1 + 9 files changed, 313 insertions(+), 106 deletions(-) diff --git a/customize.dist/src/less2/include/toolbar-history.less b/customize.dist/src/less2/include/toolbar-history.less index abf14f3ac..31da75ad1 100644 --- a/customize.dist/src/less2/include/toolbar-history.less +++ b/customize.dist/src/less2/include/toolbar-history.less @@ -5,23 +5,47 @@ display: none; text-align: center; width: 100%; + padding: 10px 0; + align-items: center; + justify-content: center; * { font: @colortheme_app-font; } - .cp-toolbar-history-next { - display: inline-block; - vertical-align: middle; - margin: 20px; + .cp-history-filler { + flex: 1; } - .cp-toolbar-history-previous { - display: inline-block; - vertical-align: middle; - margin: 20px; + .cp-toolbar-history-close, + .cp-toolbar-history-revert { + background: white; + color: black; + //margin-top: 5px; + &:hover { + background-color: #e6e6e6; + } + } + .cp-toolbar-history-loadmore { + height: 100%; + color: black; + width: 25px; + position: absolute; + left: 0; + padding: 0; + } + .cp-toolbar-history-version { + position: absolute; + height: 25px; + line-height: 25px; + width: 100%; + text-align: center; } .cp-toolbar-history-goto { display: inline-block; vertical-align: middle; text-align: center; + flex: 1; + flex-basis: 80%; + min-width: 0; + max-width: 600px; input { width: 75px; } } .cp-toolbar-history-goto-input { @@ -29,6 +53,30 @@ margin-left: 5px; vertical-align: middle; } + .cp-toolbar-history-bar { + width: 100%; + background: white; + height: 25px; + margin: auto; + position: relative; + } + .cp-toolbar-history-pos-container { + width: ~"calc(100% - 2px)"; + height: 25px; + position: relative; + } + @pos-color: #55FF55; + .cp-toolbar-history-pos { + width: 2px; + height: 25px; + background: @pos-color; + &:after { + content: ''; + border: 6px solid transparent; + border-top-color: @pos-color; + margin-left: -5px; + } + } button { color: inherit; background-color: rgba(0,0,0,0.2); @@ -36,14 +84,6 @@ background-color: rgba(0,0,0,0.4); } } - .cp-toolbar-history-close { - background: white; - color: black; - margin-top: 5px; - &:hover { - background-color: #e6e6e6; - } - } .fa-spinner { font-size: 66px; } diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 838dfcd2b..a936f592c 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -232,12 +232,11 @@ define(function () { out.historyText = "Historique"; out.historyButton = "Afficher l'historique du document"; - out.history_next = "Voir la version suivante"; - out.history_prev = "Voir la version précédente"; - out.history_goTo = "Voir la version sélectionnée"; + out.history_next = "Version plus récente"; + out.history_prev = "Version plus ancienne"; + out.history_loadMore = "Charger davantage d'historique"; out.history_close = "Retour"; out.history_closeTitle = "Fermer l'historique"; - out.history_restore = "Restaurer"; out.history_restoreTitle = "Restaurer la version du document sélectionnée"; out.history_restorePrompt = "Êtes-vous sûr de vouloir remplacer la version actuelle du document par la version affichée ?"; out.history_restoreDone = "Document restauré"; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 16e837a35..65ade7f7f 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -234,12 +234,10 @@ define(function () { out.historyText = "History"; out.historyButton = "Display the document history"; - out.history_next = "Go to the next version"; - out.history_prev = "Go to the previous version"; - out.history_goTo = "Go to the selected version"; - out.history_close = "Back"; + out.history_next = "Newer version"; + out.history_prev = "Older version"; + out.history_loadMore = "Load more history"; out.history_closeTitle = "Close the history"; - out.history_restore = "Restore"; out.history_restoreTitle = "Restore the selected version of the document"; out.history_restorePrompt = "Are you sure you want to replace the current version of the document by the displayed one?"; out.history_restoreDone = "Document restored"; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index b3f3edb63..89b46e03f 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -692,6 +692,9 @@ define([ common.getFullHistory = function (data, cb) { postMessage("GET_FULL_HISTORY", data, cb); }; + common.getHistoryRange = function (data, cb) { + postMessage("GET_HISTORY_RANGE", data, cb); + }; common.getShareHashes = function (secret, cb) { var hashes; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index e703b6a5b..53b84f395 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1101,10 +1101,13 @@ define([ } }; var msgs = []; + var completed = false; var onMsg = function (msg) { + if (completed) { return; } var parsed = parse(msg); if (parsed[0] === 'FULL_HISTORY_END') { cb(msgs); + completed = true; return; } if (parsed[0] !== 'FULL_HISTORY') { return; } @@ -1123,6 +1126,63 @@ define([ network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey])); }; + Store.getHistoryRange = function (clientId, data, cb) { + var network = store.network; + var hkn = network.historyKeeper; + var parse = function (msg) { + try { + return JSON.parse(msg); + } catch (e) { + return null; + } + }; + var msgs = []; + var first = true; + var fullHistory = false; + var completed = false; + var lastKnownHash; + var txid = Util.uid(); + + var onMsg = function (msg) { + if (completed) { return; } + var parsed = parse(msg); + if (parsed[1] !== txid) { console.log('bad txid'); return; } + if (parsed[0] === 'HISTORY_RANGE_END') { + cb({ + messages: msgs, + isFull: fullHistory, + lastKnownHash: lastKnownHash + }); + completed = true; + return; + } + if (parsed[0] !== 'HISTORY_RANGE') { return; } + if (parsed[2] && parsed[1].validateKey) { // Metadata + return; + } + if (parsed[2][3] !== data.channel) { return; } + msg = parsed[2][4]; + if (msg) { + if (first) { + // If the first message if not a checkpoint, it means it is the first + // message of the pad, so we have the full history! + if (!/^cp\|/.test(msg)) { fullHistory = true; } + lastKnownHash = msg.slice(0,64); + first = false; + } + msg = msg.replace(/cp\|(([A-Za-z0-9+\/=]+)\|)?/, ''); + msgs.push(msg); + } + }; + + network.on('message', onMsg); + network.sendto(hkn, JSON.stringify(['GET_HISTORY_RANGE', data.channel, { + from: data.lastKnownHash, + cpCount: 2, + txid: txid + }])); + }; + // Drive Store.userObjectCommand = function (clientId, cmdData, cb) { if (!cmdData || !cmdData.cmd) { return; } diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index a2b7d2404..972776abe 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -70,6 +70,7 @@ define([ JOIN_PAD: Store.joinPad, LEAVE_PAD: Store.leavePad, GET_FULL_HISTORY: Store.getFullHistory, + GET_HISTORY_RANGE: Store.getHistoryRange, IS_NEW_CHANNEL: Store.isNewChannel, // Drive DRIVE_USEROBJECT: Store.userObjectCommand, diff --git a/www/common/sframe-common-history.js b/www/common/sframe-common-history.js index 27503d07a..efaa01cc1 100644 --- a/www/common/sframe-common-history.js +++ b/www/common/sframe-common-history.js @@ -1,26 +1,36 @@ define([ 'jquery', '/common/common-interface.js', + '/bower_components/nthen/index.js', //'/bower_components/chainpad-json-validator/json-ot.js', '/bower_components/chainpad/chainpad.dist.js', -], function ($, UI, ChainPad /* JsonOT */) { +], function ($, UI, nThen, ChainPad /* JsonOT */) { //var ChainPad = window.ChainPad; var History = {}; - var getStates = function (rt) { - var states = []; - var b = rt.getAuthBlock(); - if (b) { states.unshift(b); } - while (b.getParent()) { - b = b.getParent(); - states.unshift(b); + History.create = function (common, config) { + if (!config.$toolbar) { return void console.error("config.$toolbar is undefined");} + if (History.loading) { return void console.error("History is already being loaded..."); } + History.loading = true; + var $toolbar = config.$toolbar; + + if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) { + throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory"); } - return states; - }; - var loadHistory = function (config, common, cb) { - var createRealtime = function () { + var getStates = function (rt) { + var states = []; + var b = rt.getAuthBlock(); + if (b) { states.unshift(b); } + while (b.getParent()) { + b = b.getParent(); + states.unshift(b); + } + return states; + }; + + var createRealtime = function (config) { return ChainPad.create({ userName: 'history', validateContent: function (content) { @@ -33,36 +43,45 @@ define([ } }, initialState: '', - //patchTransformer: ChainPad.NaiveJSONTransformer, - //logLevel: 0, - //transformFunction: JsonOT.validate, logLevel: config.debug ? 2 : 0, noPrune: true }); }; - var realtime = createRealtime(); - - History.readOnly = common.getMetadataMgr().getPrivateData().readOnly; - - /*var to = window.setTimeout(function () { - cb('[GET_FULL_HISTORY_TIMEOUT]'); - }, 30000);*/ - common.getFullHistory(realtime, function () { - //window.clearTimeout(to); - cb(null, realtime); - }); - }; + var loadFullHistory = function (config, common, cb) { + var realtime = createRealtime(config); + common.getFullHistory(realtime, function () { + cb(null, realtime); + }); + }; + loadFullHistory = loadFullHistory; - History.create = function (common, config) { - if (!config.$toolbar) { return void console.error("config.$toolbar is undefined");} - if (History.loading) { return void console.error("History is already being loaded..."); } - History.loading = true; - var $toolbar = config.$toolbar; + var fillChainPad = function (realtime, messages) { + messages.forEach(function (m) { + realtime.message(m); + }); + }; - if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) { - throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory"); - } + var allMessages = []; + var lastKnownHash; + var isComplete = false; + var loadMoreHistory = function (config, common, cb) { + if (isComplete) { return void cb ('EFULL'); } + var realtime = createRealtime(config); + var sframeChan = common.getSframeChannel(); + + sframeChan.query('Q_GET_HISTORY_RANGE', { + lastKnownHash: lastKnownHash + }, function (err, data) { + if (err) { return void console.error(err); } + if (!Array.isArray(data.messages)) { return void console.error('Not an array!'); } + lastKnownHash = data.lastKnownHash; + isComplete = data.isFull; + Array.prototype.unshift.apply(allMessages, data.messages); // Destructive concat + fillChainPad(realtime, allMessages); + cb (null, realtime); + }); + }; // config.setHistory(bool, bool) // - bool1: history value @@ -84,21 +103,20 @@ define([ }; config.setHistory(true); - var onReady = function () { }; var Messages = common.Messages; var realtime; var states = []; - var c = states.length - 1; + var c = 0;//states.length - 1; var $hist = $toolbar.find('.cp-toolbar-history'); var $left = $toolbar.find('.cp-toolbar-leftside'); var $right = $toolbar.find('.cp-toolbar-rightside'); var $cke = $toolbar.find('.cke_toolbox_main'); - $hist.html('').show(); + $hist.html('').css('display', 'flex'); $left.hide(); $right.hide(); $cke.hide(); @@ -107,29 +125,73 @@ define([ var onUpdate; - var update = function () { + var update = function (newRt) { + realtime = newRt; if (!realtime) { return []; } states = getStates(realtime); if (typeof onUpdate === "function") { onUpdate(); } return states; }; + var $loadMore, $version, get; + // Get the content of the selected version, and change the version number - var get = function (i) { + var loading = false; + var loadMore = function (cb) { + if (loading) { return; } + loading = true; + $loadMore.removeClass('fa fa-ellipsis-h') + .append($('', {'class': 'fa fa-refresh fa-spin fa-3x fa-fw'})); + loadMoreHistory(config, common, function (err, newRt) { + if (err === 'EFULL') { + $loadMore.off('click').hide(); + get(c); + $version.show(); + return; + } + loading = false; + if (err) { return void console.error(err); } + update(newRt); + $loadMore.addClass('fa fa-ellipsis-h').html(''); + get(c); + if (cb) { cb(); } + }); + }; + get = function (i) { i = parseInt(i); if (isNaN(i)) { return; } - if (i < 0) { i = 0; } - if (i > states.length - 1) { i = states.length - 1; } - var val = states[i].getContent().doc; + if (i > 0) { i = 0; } + if (i < -(states.length - 2)) { i = -(states.length - 2); } + if (i <= -(states.length - 11)) { + loadMore(); + } + var idx = states.length - 1 + i; + var val = states[idx].getContent().doc; c = i; if (typeof onUpdate === "function") { onUpdate(); } - $hist.find('.cp-toolbar-history-next, .cp-toolbar-history-previous').css('visibility', ''); - if (c === states.length - 1) { $hist.find('.cp-toolbar-history-next').css('visibility', 'hidden'); } - if (c === 0) { $hist.find('.cp-toolbar-history-previous').css('visibility', 'hidden'); } + $hist.find('.cp-toolbar-history-next, .cp-toolbar-history-previous, ' + + '.cp-toolbar-history-fast-next, .cp-toolbar-history-fast-previous') + .css('visibility', ''); + if (c === -(states.length-1)) { + $hist.find('.cp-toolbar-history-previous').css('visibility', 'hidden'); + $hist.find('.cp-toolbar-history-fast-previous').css('visibility', 'hidden'); + } + if (c === 0) { + $hist.find('.cp-toolbar-history-next').css('visibility', 'hidden'); + $hist.find('.cp-toolbar-history-fast-next').css('visibility', 'hidden'); + } + var $pos = $hist.find('.cp-toolbar-history-pos'); + var p = 100 * (1 - (-c / (states.length-1))); + $pos.css('margin-left', p+'%'); + + // Display the version when the full history is loaded + // Note: the first version is always empty and probably can't be displayed, so + // we can consider we have only states.length - 1 versions + $version.text(idx + ' / ' + (states.length-1)); if (config.debug) { - console.log(states[i]); - var ops = states[i] && states[i].getPatch() && states[i].getPatch().operations; + console.log(states[idx]); + var ops = states[idx] && states[idx].getPatch() && states[idx].getPatch().operations; if (Array.isArray(ops)) { ops.forEach(function (op) { console.log(op); }); } @@ -148,6 +210,17 @@ define([ // Create the history toolbar var display = function () { $hist.html(''); + + var $rev = $('