mirror of https://github.com/xwiki-labs/cryptpad
Merge branch 'historyOO' into staging
This commit is contained in:
commit
e698241ee9
|
@ -71,6 +71,10 @@
|
|||
min-width: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.cp-history-timeline-version {
|
||||
font-size: 12px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
.cp-toolbar-history-actions {
|
||||
display: flex;
|
||||
|
@ -120,6 +124,9 @@
|
|||
.cp-history-timeline-actions {
|
||||
margin-left: 21px !important;
|
||||
}
|
||||
.cp-toolbar-history-actions {
|
||||
align-items: baseline !important;
|
||||
}
|
||||
}
|
||||
&.cp-smallpatch {
|
||||
.cp-history-snapshot {
|
||||
|
@ -137,6 +144,17 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
&.cp-history-oo {
|
||||
.cp-history-timeline-container {
|
||||
height: 20px !important;
|
||||
.cp-history-timeline-pos {
|
||||
height: 20px !important;
|
||||
}
|
||||
}
|
||||
.cp-history-timeline-actions {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
.cp-history-timeline-line {
|
||||
display: flex;
|
||||
.cp-history-timeline-legend {
|
||||
|
@ -229,6 +247,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.cp-history-timeline-version:empty {
|
||||
display: none;
|
||||
}
|
||||
.cp-history-timeline-time:empty {
|
||||
display: none;
|
||||
}
|
||||
.cp-history-timeline-next {
|
||||
button:last-child {
|
||||
margin-right: 0;
|
||||
|
@ -254,7 +278,6 @@
|
|||
left: ~"calc(50% - 6px)";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -662,10 +662,11 @@ const handleGetHistoryRange = function (Env, Server, seq, userId, parsed) {
|
|||
}
|
||||
|
||||
var oldestKnownHash = map.from;
|
||||
var untilHash = map.to;
|
||||
var desiredMessages = map.count;
|
||||
var desiredCheckpoint = map.cpCount;
|
||||
var txid = map.txid;
|
||||
if (typeof(desiredMessages) !== 'number' && typeof(desiredCheckpoint) !== 'number') {
|
||||
if (typeof(desiredMessages) !== 'number' && typeof(desiredCheckpoint) !== 'number' && !untilHash) {
|
||||
return void Server.send(userId, [seq, 'ERROR', 'UNSPECIFIED_COUNT', HISTORY_KEEPER_ID]);
|
||||
}
|
||||
|
||||
|
@ -674,7 +675,7 @@ const handleGetHistoryRange = function (Env, Server, seq, userId, parsed) {
|
|||
}
|
||||
|
||||
Server.send(userId, [seq, 'ACK']);
|
||||
Env.getOlderHistory(channelName, oldestKnownHash, desiredMessages, desiredCheckpoint, function (err, toSend) {
|
||||
Env.getOlderHistory(channelName, oldestKnownHash, untilHash, desiredMessages, desiredCheckpoint, function (err, toSend) {
|
||||
if (err && err.code !== 'ENOENT') {
|
||||
Env.Log.error("HK_GET_OLDER_HISTORY", err);
|
||||
}
|
||||
|
|
|
@ -230,6 +230,7 @@ const computeMetadata = function (data, cb) {
|
|||
|
||||
const getOlderHistory = function (data, cb) {
|
||||
const oldestKnownHash = data.hash;
|
||||
const untilHash = data.toHash;
|
||||
const channelName = data.channel;
|
||||
const desiredMessages = data.desiredMessages;
|
||||
const desiredCheckpoint = data.desiredCheckpoint;
|
||||
|
@ -260,6 +261,13 @@ const getOlderHistory = function (data, cb) {
|
|||
var toSend = [];
|
||||
if (typeof (desiredMessages) === "number") {
|
||||
toSend = messages.slice(-desiredMessages);
|
||||
} else if (untilHash) {
|
||||
for (var j = messages.length - 1; j >= 0; j--) {
|
||||
toSend.unshift(messages[j]);
|
||||
if (Array.isArray(messages[j]) && HK.getHash(messages[j][4]) === untilHash) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let cpCount = 0;
|
||||
for (var i = messages.length - 1; i >= 0; i--) {
|
||||
|
|
|
@ -281,12 +281,13 @@ Workers.initialize = function (Env, config, _cb) {
|
|||
});
|
||||
};
|
||||
|
||||
Env.getOlderHistory = function (channel, oldestKnownHash, desiredMessages, desiredCheckpoint, cb) {
|
||||
Env.getOlderHistory = function (channel, oldestKnownHash, toHash, desiredMessages, desiredCheckpoint, cb) {
|
||||
Env.store.getWeakLock(channel, function (next) {
|
||||
sendCommand({
|
||||
channel: channel,
|
||||
command: "GET_OLDER_HISTORY",
|
||||
hash: oldestKnownHash,
|
||||
toHash: toHash,
|
||||
desiredMessages: desiredMessages,
|
||||
desiredCheckpoint: desiredCheckpoint,
|
||||
}, Util.both(next, cb));
|
||||
|
|
|
@ -0,0 +1,384 @@
|
|||
define([
|
||||
'jquery',
|
||||
'/common/common-interface.js',
|
||||
'/common/hyperscript.js',
|
||||
], function ($, UI, h) {
|
||||
//var ChainPad = window.ChainPad;
|
||||
var History = {};
|
||||
|
||||
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 sframeChan = common.getSframeChannel();
|
||||
History.readOnly = common.getMetadataMgr().getPrivateData().readOnly || !common.isLoggedIn();
|
||||
|
||||
if (!config.onlyoffice || !config.setHistory || !config.onCheckpoint || !config.onPatch || !config.makeSnapshot) {
|
||||
throw new Error("Missing config element");
|
||||
}
|
||||
|
||||
var cpIndex = -1;
|
||||
var msgIndex = -1;
|
||||
var ooMessages = {};
|
||||
var loading = false;
|
||||
var update = function () {};
|
||||
var currentTime;
|
||||
|
||||
// Get an array of the checkpoint IDs sorted their patch index
|
||||
var hashes = config.onlyoffice.hashes;
|
||||
var sortedCp = Object.keys(hashes).map(Number).sort(function (a, b) {
|
||||
return hashes[a].index - hashes[b].index;
|
||||
});
|
||||
|
||||
var endWithCp = sortedCp.length &&
|
||||
config.onlyoffice.lastHash === hashes[sortedCp[sortedCp.length - 1]].hash;
|
||||
|
||||
var fillOO = function (id, messages) {
|
||||
if (!id) { return; }
|
||||
if (ooMessages[id]) { return; }
|
||||
ooMessages[id] = messages;
|
||||
update();
|
||||
};
|
||||
|
||||
if (endWithCp) { cpIndex = 0; }
|
||||
|
||||
var $version, $time, $share;
|
||||
var $hist = $toolbar.find('.cp-toolbar-history');
|
||||
$hist.addClass('cp-smallpatch');
|
||||
$hist.addClass('cp-history-oo');
|
||||
var $bottom = $toolbar.find('.cp-toolbar-bottom');
|
||||
|
||||
var getVersion = function () {
|
||||
var major = sortedCp.length - cpIndex;
|
||||
return major + '.' + (msgIndex+1);
|
||||
};
|
||||
var showVersion = function (initial) {
|
||||
var v = getVersion();
|
||||
if (initial) {
|
||||
v = "Latest"; // XXX
|
||||
}
|
||||
$version.text("Version: " + v); // XXX
|
||||
|
||||
var $pos = $hist.find('.cp-history-timeline-pos');
|
||||
var cps = sortedCp.length;
|
||||
var id = sortedCp[cps - cpIndex -1] || -1;
|
||||
if (!ooMessages[id]) { return; }
|
||||
var msgs = ooMessages[id];
|
||||
console.log(ooMessages);
|
||||
var p = 100*((msgIndex+1) / (msgs.length));
|
||||
$pos.css('margin-left', p+'%');
|
||||
|
||||
var time = currentTime = msgs[msgIndex] && msgs[msgIndex].time;
|
||||
if (time) { $time.text(new Date(time).toLocaleString()); }
|
||||
else { $time.text(''); }
|
||||
};
|
||||
|
||||
// We want to load a checkpoint (or initial state)
|
||||
var loadMoreOOHistory = function () {
|
||||
if (!Array.isArray(sortedCp)) { return void console.error("Wrong type"); }
|
||||
|
||||
var cp = {};
|
||||
|
||||
// Get the checkpoint ID
|
||||
var id = -1;
|
||||
if (cpIndex < sortedCp.length) {
|
||||
id = sortedCp[sortedCp.length - 1 - cpIndex];
|
||||
cp = hashes[id];
|
||||
}
|
||||
var nextId = sortedCp[sortedCp.length - cpIndex];
|
||||
|
||||
// Get the history between "toHash" and "fromHash". This function is using
|
||||
// "getOlderHistory", that's why we start from the more recent hash
|
||||
// and we go back in time to an older hash
|
||||
|
||||
// We need to get all the patches between the current cp hash and the next cp hash
|
||||
|
||||
// Current cp or initial hash (invalid hash ==> initial hash)
|
||||
var toHash = cp.hash || 'NONE';
|
||||
// Next cp or last hash
|
||||
var fromHash = nextId ? hashes[nextId].hash : config.onlyoffice.lastHash;
|
||||
|
||||
msgIndex = -1;
|
||||
|
||||
showVersion();
|
||||
if (ooMessages[id]) {
|
||||
// Cp already loaded: reload OO
|
||||
loading = false;
|
||||
return void config.onCheckpoint(cp);
|
||||
}
|
||||
|
||||
sframeChan.query('Q_GET_HISTORY_RANGE', {
|
||||
channel: config.onlyoffice.channel,
|
||||
lastKnownHash: fromHash,
|
||||
toHash: toHash,
|
||||
}, function (err, data) {
|
||||
if (err) { return void console.error(err); }
|
||||
if (!Array.isArray(data.messages)) { return void console.error('Not an array!'); }
|
||||
|
||||
// The first "cp" in history is the empty doc. It doesn't include the first patch
|
||||
// of the history
|
||||
var initialCp = cpIndex === sortedCp.length;
|
||||
|
||||
var messages = (data.messages || []).slice(initialCp ? 0 : 1);
|
||||
|
||||
if (config.debug) { console.log(data.messages); }
|
||||
fillOO(id, messages);
|
||||
loading = false;
|
||||
config.onCheckpoint(cp);
|
||||
$share.show();
|
||||
});
|
||||
};
|
||||
|
||||
var onClose = function () { config.setHistory(false); };
|
||||
var onRevert = function () {
|
||||
config.onRevert();
|
||||
};
|
||||
|
||||
config.setHistory(true);
|
||||
|
||||
var Messages = common.Messages;
|
||||
|
||||
$hist.html('').css('display', 'flex');
|
||||
$bottom.hide();
|
||||
|
||||
UI.spinner($hist).get().show();
|
||||
|
||||
var $fastPrev, $fastNext, $next;
|
||||
|
||||
var getId = function () {
|
||||
var cps = sortedCp.length;
|
||||
return sortedCp[cps - cpIndex -1] || -1;
|
||||
};
|
||||
|
||||
update = function () {
|
||||
var cps = sortedCp.length;
|
||||
$fastPrev.show();
|
||||
$next.show();
|
||||
$fastNext.show();
|
||||
if (cpIndex >= cps && msgIndex === 0) {
|
||||
$fastPrev.hide();
|
||||
}
|
||||
if (cpIndex === 0) {
|
||||
$fastNext.hide();
|
||||
}
|
||||
var id = getId();
|
||||
var msgs = (ooMessages[id] || []).length;
|
||||
if (msgIndex >= (msgs-1)) {
|
||||
$next.hide();
|
||||
}
|
||||
};
|
||||
|
||||
var next = function () {
|
||||
var id = getId();
|
||||
if (!ooMessages[id]) { loading = false; return; }
|
||||
var msgs = ooMessages[id];
|
||||
msgIndex++;
|
||||
var patch = msgs[msgIndex];
|
||||
if (!patch) { loading = false; return; }
|
||||
config.onPatch(patch);
|
||||
showVersion();
|
||||
setTimeout(function () {
|
||||
$('iframe').blur();
|
||||
loading = false;
|
||||
}, 200);
|
||||
};
|
||||
|
||||
|
||||
// Create the history toolbar
|
||||
var display = function () {
|
||||
$hist.html('');
|
||||
|
||||
var fastPrev = h('button.cp-toolbar-history-previous', { title: Messages.history_prev }, [
|
||||
h('i.fa.fa-fast-backward'),
|
||||
]);
|
||||
var fastNext = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
|
||||
h('i.fa.fa-fast-forward'),
|
||||
]);
|
||||
var _next = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
|
||||
h('i.fa.fa-step-forward')
|
||||
]);
|
||||
$fastPrev = $(fastPrev);
|
||||
$fastNext = $(fastNext).hide();
|
||||
$next = $(_next).hide();
|
||||
|
||||
var pos = h('span.cp-history-timeline-pos.fa.fa-caret-down');
|
||||
var time = h('div.cp-history-timeline-time');
|
||||
var version = h('div.cp-history-timeline-version');
|
||||
$time = $(time);
|
||||
$version = $(version);
|
||||
var bar;
|
||||
var timeline = h('div.cp-toolbar-history-timeline', [
|
||||
h('div.cp-history-timeline-line', [
|
||||
bar = h('span.cp-history-timeline-container')
|
||||
]),
|
||||
h('div.cp-history-timeline-actions', [
|
||||
h('span.cp-history-timeline-prev', [
|
||||
fastPrev,
|
||||
]),
|
||||
time,
|
||||
version,
|
||||
h('span.cp-history-timeline-next', [
|
||||
_next,
|
||||
fastNext
|
||||
])
|
||||
])
|
||||
]);
|
||||
var snapshot = h('button', {
|
||||
title: Messages.snapshots_new,
|
||||
}, [
|
||||
h('i.fa.fa-camera')
|
||||
]);
|
||||
var share = h('button', { title: Messages.history_shareTitle }, [
|
||||
h('i.fa.fa-shhare-alt'),
|
||||
h('span', Messages.shareButton)
|
||||
]);
|
||||
var restore = h('button', {
|
||||
title: Messages.history_restoreTitle,
|
||||
}, [
|
||||
h('i.fa.fa-check'),
|
||||
h('span', Messages.history_restore)
|
||||
]);
|
||||
var close = h('button', { title: Messages.history_closeTitle }, [
|
||||
h('i.fa.fa-times'),
|
||||
h('span', Messages.history_close)
|
||||
]);
|
||||
var actions = h('div.cp-toolbar-history-actions', [
|
||||
h('span.cp-history-actions-first', [
|
||||
snapshot,
|
||||
share
|
||||
]),
|
||||
h('span.cp-history-actions-last', [
|
||||
restore,
|
||||
close
|
||||
])
|
||||
]);
|
||||
|
||||
if (History.readOnly) {
|
||||
snapshot.disabled = true;
|
||||
restore.disabled = true;
|
||||
}
|
||||
|
||||
$share = $(share);
|
||||
$hist.append([timeline, actions]);
|
||||
$(bar).append(pos);
|
||||
|
||||
var onKeyDown, onKeyUp;
|
||||
var closeUI = function () {
|
||||
$hist.hide();
|
||||
$bottom.show();
|
||||
$(window).trigger('resize');
|
||||
$(window).off('keydown', onKeyDown);
|
||||
$(window).off('keyup', onKeyUp);
|
||||
};
|
||||
|
||||
// Push one patch
|
||||
$next.click(function () {
|
||||
if (loading) { return; }
|
||||
loading = true;
|
||||
next();
|
||||
update();
|
||||
});
|
||||
// Go to previous checkpoint
|
||||
$fastNext.click(function () {
|
||||
if (loading) { return; }
|
||||
loading = true;
|
||||
cpIndex--;
|
||||
loadMoreOOHistory();
|
||||
update();
|
||||
});
|
||||
// Go to next checkpoint
|
||||
$fastPrev.click(function () {
|
||||
if (loading) { return; }
|
||||
loading = true;
|
||||
if (msgIndex === -1) {
|
||||
cpIndex++;
|
||||
}
|
||||
loadMoreOOHistory();
|
||||
update();
|
||||
});
|
||||
onKeyDown = function (e) {
|
||||
var p = function () { e.preventDefault(); };
|
||||
if ([38, 39].indexOf(e.which) >= 0) { p(); return $next.click(); } // Right
|
||||
if (e.which === 33) { p(); return $fastNext.click(); } // PageUp
|
||||
if (e.which === 34) { p(); return $fastPrev.click(); } // PageUp
|
||||
if (e.which === 27) { p(); return $(close).click(); }
|
||||
};
|
||||
onKeyUp = function (e) { e.stopPropagation(); };
|
||||
$(window).on('keydown', onKeyDown).on('keyup', onKeyUp).focus();
|
||||
|
||||
// Versioned link
|
||||
$share.click(function () {
|
||||
common.getSframeChannel().event('EV_SHARE_OPEN', {
|
||||
versionHash: getVersion()
|
||||
});
|
||||
});
|
||||
Messages.snapshots_ooPickVersion = "You must select a version before creating a snapshot"; // XXX
|
||||
$(snapshot).click(function () {
|
||||
if (cpIndex === -1 && msgIndex === -1) { return void UI.warn(Messages.snapshots_ooPickVersion); }
|
||||
var input = h('input', {
|
||||
placeholder: Messages.snapshots_placeholder
|
||||
});
|
||||
var $input = $(input);
|
||||
var content = h('div', [
|
||||
h('h5', Messages.snapshots_new),
|
||||
input
|
||||
]);
|
||||
|
||||
var buttons = [{
|
||||
className: 'cancel',
|
||||
name: Messages.filePicker_close,
|
||||
onClick: function () {},
|
||||
keys: [27],
|
||||
}, {
|
||||
className: 'primary',
|
||||
iconClass: '.fa.fa-camera',
|
||||
name: Messages.snapshots_new,
|
||||
onClick: function () {
|
||||
var val = $input.val();
|
||||
if (!val) { return true; }
|
||||
config.makeSnapshot(val, function (err) {
|
||||
if (err) { return; }
|
||||
UI.log(Messages.saved);
|
||||
}, {
|
||||
hash: getVersion(),
|
||||
time: currentTime || 0
|
||||
});
|
||||
},
|
||||
keys: [13],
|
||||
}];
|
||||
|
||||
UI.openCustomModal(UI.dialog.customModal(content, {buttons: buttons }));
|
||||
setTimeout(function () {
|
||||
$input.focus();
|
||||
});
|
||||
});
|
||||
|
||||
// Close & restore buttons
|
||||
$(close).click(function () {
|
||||
History.loading = false;
|
||||
onClose();
|
||||
closeUI();
|
||||
});
|
||||
$(restore).click(function () {
|
||||
UI.confirm(Messages.history_restorePrompt, function (yes) {
|
||||
if (!yes) { return; }
|
||||
closeUI();
|
||||
History.loading = false;
|
||||
onRevert();
|
||||
UI.log(Messages.history_restoreDone);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
display();
|
||||
|
||||
showVersion(true);
|
||||
|
||||
//return void loadMoreOOHistory();
|
||||
};
|
||||
|
||||
return History;
|
||||
});
|
||||
|
||||
|
|
@ -14,6 +14,7 @@ define([
|
|||
'/customize/application_config.js',
|
||||
'/bower_components/chainpad/chainpad.dist.js',
|
||||
'/file/file-crypto.js',
|
||||
'/common/onlyoffice/history.js',
|
||||
'/common/onlyoffice/oocell_base.js',
|
||||
'/common/onlyoffice/oodoc_base.js',
|
||||
'/common/onlyoffice/ooslide_base.js',
|
||||
|
@ -40,6 +41,7 @@ define([
|
|||
AppConfig,
|
||||
ChainPad,
|
||||
FileCrypto,
|
||||
History,
|
||||
EmptyCell,
|
||||
EmptyDoc,
|
||||
EmptySlide,
|
||||
|
@ -243,8 +245,17 @@ define([
|
|||
ready: false,
|
||||
readyCb: undefined,
|
||||
sendCmd: function (data, cb) {
|
||||
if (APP.history) { return; }
|
||||
sframeChan.query('Q_OO_COMMAND', data, cb);
|
||||
},
|
||||
getHistory: function (cb) {
|
||||
rtChannel.sendCmd({
|
||||
cmd: 'GET_HISTORY',
|
||||
data: {}
|
||||
}, function () {
|
||||
APP.onHistorySynced = cb;
|
||||
});
|
||||
},
|
||||
sendMsg: function (msg, cp, cb) {
|
||||
rtChannel.sendCmd({
|
||||
cmd: 'SEND_MESSAGE',
|
||||
|
@ -320,10 +331,14 @@ define([
|
|||
// Get the last cp idx
|
||||
var all = sortCpIndex(content.hashes || {});
|
||||
var current = all[all.length - 1] || 0;
|
||||
|
||||
// XXX Keep all cp now
|
||||
// Get the expected cp idx
|
||||
var _i = Math.floor(ev.index / CHECKPOINT_INTERVAL);
|
||||
//var _i = Math.floor(ev.index / CHECKPOINT_INTERVAL);
|
||||
// Take the max of both
|
||||
var i = Math.max(_i, current);
|
||||
//var i = Math.max(_i, current);
|
||||
|
||||
var i = current + 1;
|
||||
content.hashes[i] = {
|
||||
file: data.url,
|
||||
hash: ev.hash,
|
||||
|
@ -396,7 +411,7 @@ define([
|
|||
}
|
||||
myUniqueOOId = undefined;
|
||||
setMyId();
|
||||
APP.docEditor.destroyEditor(); // Kill the old editor
|
||||
if (APP.docEditor) { APP.docEditor.destroyEditor(); } // Kill the old editor
|
||||
$('iframe[name="frameEditor"]').after(h('div#cp-app-oo-placeholder')).remove();
|
||||
ooLoaded = false;
|
||||
oldLocks = {};
|
||||
|
@ -404,6 +419,7 @@ define([
|
|||
clearTimeout(pendingChanges[key]);
|
||||
delete pendingChanges[key];
|
||||
});
|
||||
if (APP.stopHistory) { APP.history = false; }
|
||||
startOO(blob, type, true);
|
||||
};
|
||||
|
||||
|
@ -413,8 +429,8 @@ define([
|
|||
var file = getFileType();
|
||||
blob.name = (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type;
|
||||
var data = {
|
||||
hash: ooChannel.lastHash,
|
||||
index: ooChannel.cpIndex
|
||||
hash: APP.history ? ooChannel.historyLastHash : ooChannel.lastHash,
|
||||
index: APP.history ? ooChannel.currentIndex : ooChannel.cpIndex
|
||||
};
|
||||
fixSheets();
|
||||
|
||||
|
@ -532,6 +548,9 @@ define([
|
|||
return new Blob([newText], {type: 'text/plain'});
|
||||
};
|
||||
var loadLastDocument = function (lastCp, onCpError, cb) {
|
||||
if (!lastCp || !lastCp.file) {
|
||||
return void onCpError('EEMPTY');
|
||||
}
|
||||
ooChannel.cpIndex = lastCp.index || 0;
|
||||
ooChannel.lastHash = lastCp.hash;
|
||||
var parsed = Hash.parsePadUrl(lastCp.file);
|
||||
|
@ -597,6 +616,94 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
var openVersionHash = function (version) {
|
||||
readOnly = true;
|
||||
var hashes = content.hashes || {};
|
||||
var sortedCp = Object.keys(hashes).map(Number).sort(function (a, b) {
|
||||
return hashes[a].index - hashes[b].index;
|
||||
});
|
||||
var s = version.split('.');
|
||||
if (s.length !== 2) { return UI.errorLoadingScreen(Messages.error); }
|
||||
|
||||
var major = Number(s[0]);
|
||||
var cpId = sortedCp[major - 1];
|
||||
var nextCpId = sortedCp[major];
|
||||
var cp = hashes[cpId] || {};
|
||||
|
||||
var minor = Number(s[1]) + 1;
|
||||
|
||||
var toHash = cp.hash || 'NONE';
|
||||
var fromHash = nextCpId ? hashes[nextCpId].hash : 'NONE';
|
||||
|
||||
sframeChan.query('Q_GET_HISTORY_RANGE', {
|
||||
channel: content.channel,
|
||||
lastKnownHash: fromHash,
|
||||
toHash: toHash,
|
||||
}, function (err, data) {
|
||||
if (err) { console.error(err); return void UI.errorLoadingScreen(Messages.error); }
|
||||
if (!Array.isArray(data.messages)) {
|
||||
console.error('Not an array');
|
||||
return void UI.errorLoadingScreen(Messages.error);
|
||||
}
|
||||
|
||||
// The first "cp" in history is the empty doc. It doesn't include the first patch
|
||||
// of the history
|
||||
var initialCp = major === 0;
|
||||
var messages = (data.messages || []).slice(initialCp ? 0 : 1, minor);
|
||||
|
||||
messages.forEach(function (obj) {
|
||||
try { obj.msg = JSON.parse(obj.msg); } catch (e) { console.error(e); }
|
||||
});
|
||||
|
||||
// The version exists if we have results in the "messages" array
|
||||
// or if we requested a x.0 version
|
||||
var exists = !Number(s[1]) || messages.length;
|
||||
var vHashEl;
|
||||
Messages.oo_deletedVersion = "This version no longer exists in the history."; // XXX
|
||||
|
||||
if (!privateData.embed) {
|
||||
Messages.infobar_versionHash = "You're currently viewing an old version of this document ({0})."; // XXX (duplicate from history branch)
|
||||
var vTime = (messages[messages.length - 1] || {}).time;
|
||||
var vTimeStr = vTime ? new Date(vTime).toLocaleString()
|
||||
: 'v' + privateData.ooVersionHash;
|
||||
var vTxt = Messages._getKey('infobar_versionHash', [vTimeStr]);
|
||||
|
||||
// If we expected patched and we don't have any, it means this part
|
||||
// of the history has been deleted
|
||||
var vType = "warning";
|
||||
if (!exists) {
|
||||
vTxt = Messages.oo_deletedVersion
|
||||
vType = "danger";
|
||||
}
|
||||
|
||||
vHashEl = h('div.alert.alert-'+vType+'.cp-burn-after-reading', vTxt);
|
||||
$('#cp-app-oo-editor').prepend(vHashEl);
|
||||
}
|
||||
|
||||
if (!exists) { return void UI.removeLoadingScreen(); }
|
||||
|
||||
loadLastDocument(cp, function () {
|
||||
if (cp.hash && vHashEl) {
|
||||
// We requested a checkpoint but we can't find it...
|
||||
UI.removeLoadingScreen();
|
||||
vHashEl.innerText = Messages.oo_deletedVersion;
|
||||
$(vHashEl).removeClass('alert-warning').addClass('alert-danger');
|
||||
return;
|
||||
}
|
||||
var file = getFileType();
|
||||
var type = common.getMetadataMgr().getPrivateData().ooType;
|
||||
var blob = loadInitDocument(type, true);
|
||||
ooChannel.queue = messages;
|
||||
resetData(blob, file);
|
||||
UI.removeLoadingScreen();
|
||||
}, function (blob, file) {
|
||||
ooChannel.queue = messages;
|
||||
resetData(blob, file);
|
||||
UI.removeLoadingScreen();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var openRtChannel = function (cb) {
|
||||
if (rtChannel.ready) { return void cb(); }
|
||||
var chan = content.channel || Hash.createChannelId();
|
||||
|
@ -619,10 +726,15 @@ define([
|
|||
removeClient(obj.data);
|
||||
break;
|
||||
case 'MESSAGE':
|
||||
if (APP.history) {
|
||||
ooChannel.historyLastHash = obj.data.hash;
|
||||
ooChannel.currentIndex++;
|
||||
return;
|
||||
}
|
||||
if (ooChannel.ready) {
|
||||
// In read-only mode, push the message to the queue and prompt
|
||||
// the user to refresh OO (without reloading the page)
|
||||
if (readOnly) {
|
||||
/*if (readOnly) {
|
||||
ooChannel.queue.push(obj.data);
|
||||
if (APP.refreshPopup) { return; }
|
||||
APP.refreshPopup = true;
|
||||
|
@ -631,7 +743,7 @@ define([
|
|||
// 1 popup every 15s
|
||||
APP.refreshRoTo = setTimeout(refreshReadOnly, READONLY_REFRESH_TO);
|
||||
return;
|
||||
}
|
||||
}*/
|
||||
ooChannel.send(obj.data.msg);
|
||||
ooChannel.lastHash = obj.data.hash;
|
||||
ooChannel.cpIndex++;
|
||||
|
@ -639,6 +751,12 @@ define([
|
|||
ooChannel.queue.push(obj.data);
|
||||
}
|
||||
break;
|
||||
case 'HISTORY_SYNCED':
|
||||
if (typeof(APP.onHistorySynced) !== "function") { return; }
|
||||
APP.onHistorySynced();
|
||||
delete APP.onHistorySynced;
|
||||
break;
|
||||
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -812,6 +930,8 @@ define([
|
|||
};
|
||||
|
||||
var handleLock = function (obj, send) {
|
||||
if (APP.history) { return; }
|
||||
|
||||
if (content.saveLock) {
|
||||
if (!isLockedModal.modal) {
|
||||
isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
|
||||
|
@ -842,7 +962,9 @@ define([
|
|||
if (isLockedModal.modal) {
|
||||
isLockedModal.modal.closeModal();
|
||||
delete isLockedModal.modal;
|
||||
$('#cp-app-oo-editor > iframe')[0].contentWindow.focus();
|
||||
if (!APP.history) {
|
||||
$('#cp-app-oo-editor > iframe')[0].contentWindow.focus();
|
||||
}
|
||||
}
|
||||
send({
|
||||
type: "getLock",
|
||||
|
@ -1041,7 +1163,7 @@ define([
|
|||
startOO = function (blob, file, force) {
|
||||
if (APP.ooconfig && !force) { return void console.error('already started'); }
|
||||
var url = URL.createObjectURL(blob);
|
||||
var lock = readOnly || APP.migrate;
|
||||
var lock = !APP.history && (APP.migrate);
|
||||
|
||||
// Starting from version 3, we can use the view mode again
|
||||
// defined but never used
|
||||
|
@ -1170,6 +1292,10 @@ define([
|
|||
|
||||
if (lock) {
|
||||
getEditor().setViewModeDisconnect();
|
||||
} else if (readOnly) {
|
||||
try {
|
||||
getEditor().asc_setRestriction(true);
|
||||
} catch (e) {}
|
||||
} else {
|
||||
setEditable(true);
|
||||
}
|
||||
|
@ -1177,7 +1303,15 @@ define([
|
|||
if (isLockedModal.modal && force) {
|
||||
isLockedModal.modal.closeModal();
|
||||
delete isLockedModal.modal;
|
||||
$('#cp-app-oo-editor > iframe')[0].contentWindow.focus();
|
||||
if (!APP.history) {
|
||||
$('#cp-app-oo-editor > iframe')[0].contentWindow.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (APP.history) {
|
||||
try {
|
||||
getEditor().asc_setRestriction(true);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (APP.migrate && !readOnly) {
|
||||
|
@ -1733,6 +1867,125 @@ define([
|
|||
});
|
||||
$save.appendTo(toolbar.$bottomM);
|
||||
}
|
||||
|
||||
if (!privateData.ooVersionHash) {
|
||||
(function () {
|
||||
/* add a history button */
|
||||
var commit = function () {
|
||||
// Wait for the checkpoint to be uploaded before leaving history mode
|
||||
// (race condition). We use "stopHistory" to remove the history
|
||||
// flag only when the checkpoint is ready.
|
||||
APP.stopHistory = true;
|
||||
makeCheckpoint(true);
|
||||
};
|
||||
var loadCp = function (cp, keepQueue) {
|
||||
loadLastDocument(cp, function () {
|
||||
var file = getFileType();
|
||||
var type = common.getMetadataMgr().getPrivateData().ooType;
|
||||
var blob = loadInitDocument(type, true);
|
||||
if (!keepQueue) { ooChannel.queue = []; }
|
||||
resetData(blob, file);
|
||||
}, function (blob, file) {
|
||||
if (!keepQueue) { ooChannel.queue = []; }
|
||||
resetData(blob, file);
|
||||
});
|
||||
};
|
||||
var onPatch = function (patch) {
|
||||
// Patch on the current cp
|
||||
ooChannel.send(JSON.parse(patch.msg));
|
||||
};
|
||||
var onCheckpoint = function (cp) {
|
||||
// We want to load a checkpoint:
|
||||
loadCp(cp);
|
||||
};
|
||||
var setHistoryMode = function (bool) {
|
||||
if (bool) {
|
||||
APP.history = true;
|
||||
getEditor().setViewModeDisconnect();
|
||||
return;
|
||||
}
|
||||
// Cancel button: redraw from lastCp
|
||||
APP.history = false;
|
||||
ooChannel.queue = [];
|
||||
ooChannel.ready = false;
|
||||
// Fill the queue and then load the last CP
|
||||
rtChannel.getHistory(function () {
|
||||
var lastCp = getLastCp();
|
||||
loadCp(lastCp, true);
|
||||
});
|
||||
};
|
||||
|
||||
var deleteSnapshot = function (hash) {
|
||||
var md = Util.clone(cpNfInner.metadataMgr.getMetadata());
|
||||
var snapshots = md.snapshots = md.snapshots || {};
|
||||
delete snapshots[hash];
|
||||
metadataMgr.updateMetadata(md);
|
||||
APP.onLocal();
|
||||
};
|
||||
var makeSnapshot = function (title, cb, obj) {
|
||||
var hash, time;
|
||||
if (obj && obj.hash && obj.time) {
|
||||
hash = obj.hash;
|
||||
time = obj.time
|
||||
} else {
|
||||
var major = Object.keys(content.hashes).length;
|
||||
var cpIndex = getLastCp().index || 0;
|
||||
var minor = ooChannel.cpIndex - cpIndex;
|
||||
hash = major+'.'+minor;
|
||||
time = +new Date();
|
||||
}
|
||||
var md = Util.clone(metadataMgr.getMetadata());
|
||||
var snapshots = md.snapshots = md.snapshots || {};
|
||||
if (snapshots[hash]) { cb('EEXISTS'); return void UI.warn(Messages.error); } // XXX
|
||||
snapshots[hash] = {
|
||||
title: title,
|
||||
time: time
|
||||
};
|
||||
metadataMgr.updateMetadata(md);
|
||||
APP.onLocal();
|
||||
APP.realtime.onSettle(cb);
|
||||
};
|
||||
var loadSnapshot = function (hash) {
|
||||
sframeChan.event('EV_OO_OPENVERSION', {
|
||||
hash: hash
|
||||
});
|
||||
};
|
||||
|
||||
common.createButton('', true, {
|
||||
name: 'history',
|
||||
icon: 'fa-history',
|
||||
text: Messages.historyText,
|
||||
tippy: Messages.historyButton
|
||||
}).click(function () {
|
||||
ooChannel.historyLastHash = ooChannel.lastHash;
|
||||
ooChannel.currentIndex = ooChannel.cpIndex;
|
||||
//Feedback.send('OO_HISTORY'); // XXX pull Feedback from require
|
||||
var histConfig = {
|
||||
onPatch: onPatch,
|
||||
onCheckpoint: onCheckpoint,
|
||||
onRevert: commit,
|
||||
setHistory: setHistoryMode,
|
||||
makeSnapshot: makeSnapshot,
|
||||
onlyoffice: {
|
||||
hashes: content.hashes || {},
|
||||
channel: content.channel,
|
||||
lastHash: ooChannel.lastHash
|
||||
},
|
||||
$toolbar: $('.cp-toolbar-container')
|
||||
};
|
||||
History.create(common, histConfig);
|
||||
}).appendTo(toolbar.$drawer);
|
||||
|
||||
// Snapshots
|
||||
var $snapshot = common.createButton('snapshots', true, {
|
||||
remove: deleteSnapshot,
|
||||
make: makeSnapshot,
|
||||
load: loadSnapshot
|
||||
});
|
||||
toolbar.$drawer.append($snapshot);
|
||||
})();
|
||||
}
|
||||
|
||||
if (window.CP_DEV_MODE || DISPLAY_RESTORE_BUTTON) {
|
||||
common.createButton('', true, {
|
||||
name: 'delete',
|
||||
|
@ -1879,6 +2132,11 @@ define([
|
|||
sframeChan.event('EV_BURN_PAD', content.channel);
|
||||
}
|
||||
|
||||
if (privateData.ooVersionHash) {
|
||||
return void openVersionHash(privateData.ooVersionHash);
|
||||
}
|
||||
|
||||
|
||||
var useNewDefault = content.version && content.version >= 2;
|
||||
openRtChannel(function () {
|
||||
setMyId();
|
||||
|
|
|
@ -4,12 +4,13 @@ define([
|
|||
'/api/config',
|
||||
'/common/dom-ready.js',
|
||||
'/common/requireconfig.js',
|
||||
'/common/common-hash.js',
|
||||
'/common/sframe-common-outer.js'
|
||||
], function (nThen, ApiConfig, DomReady, RequireConfig, SFCommonO) {
|
||||
], function (nThen, ApiConfig, DomReady, RequireConfig, Hash, SFCommonO) {
|
||||
var requireConfig = RequireConfig();
|
||||
|
||||
// Loaded in load #2
|
||||
var hash, href;
|
||||
var hash, href, version;
|
||||
nThen(function (waitFor) {
|
||||
DomReady.onReady(waitFor());
|
||||
}).nThen(function (waitFor) {
|
||||
|
@ -27,6 +28,13 @@ define([
|
|||
if (window.history && window.history.replaceState && hash) {
|
||||
window.history.replaceState({}, window.document.title, '#');
|
||||
}
|
||||
|
||||
var parsed = Hash.parsePadUrl(href);
|
||||
if (parsed && parsed.hashData) {
|
||||
var opts = parsed.getOptions();
|
||||
version = opts.versionHash;
|
||||
}
|
||||
|
||||
document.getElementById('sbox-iframe').setAttribute('src',
|
||||
ApiConfig.httpSafeOrigin + window.location.pathname + 'inner.html?' +
|
||||
requireConfig.urlArgs + '#' + encodeURIComponent(JSON.stringify(req)));
|
||||
|
@ -46,6 +54,7 @@ define([
|
|||
}).nThen(function (/*waitFor*/) {
|
||||
var addData = function (obj) {
|
||||
obj.ooType = window.location.pathname.replace(/^\//, '').replace(/\/$/, '');
|
||||
obj.ooVersionHash = version;
|
||||
obj.ooForceVersion = localStorage.CryptPad_ooVersion || sessionStorage.CryptPad_ooVersion || "";
|
||||
};
|
||||
var addRpc = function (sframeChan, Cryptpad, Utils) {
|
||||
|
@ -135,6 +144,13 @@ define([
|
|||
}
|
||||
Cryptpad.onlyoffice.execCommand(obj, cb);
|
||||
});
|
||||
sframeChan.on('EV_OO_OPENVERSION', function (obj, cb) {
|
||||
if (!obj || !obj.hash) { return; }
|
||||
var parsed = Hash.parsePadUrl(window.location.href);
|
||||
var opts = parsed.getOptions();
|
||||
opts.versionHash = obj.hash;
|
||||
window.open(parsed.getUrl(opts));
|
||||
});
|
||||
Cryptpad.onlyoffice.onEvent.reg(function (obj) {
|
||||
if (obj.ev === 'MESSAGE' && !/^cp\|/.test(obj.data)) {
|
||||
try {
|
||||
|
|
|
@ -2090,7 +2090,7 @@ define([
|
|||
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; }
|
||||
if (!/^cp\|/.test(msg) && !data.toHash) { fullHistory = true; }
|
||||
lastKnownHash = msg.slice(0,64);
|
||||
first = false;
|
||||
}
|
||||
|
@ -2107,7 +2107,8 @@ define([
|
|||
network.on('message', onMsg);
|
||||
network.sendto(hk, JSON.stringify(['GET_HISTORY_RANGE', data.channel, {
|
||||
from: data.lastKnownHash,
|
||||
cpCount: data.cpCount || 2,
|
||||
to: data.toHash,
|
||||
cpCount: data.cpCount || 2, // Ignored if "to" is provided
|
||||
txid: txid
|
||||
}]));
|
||||
};
|
||||
|
|
|
@ -2,6 +2,21 @@ define([
|
|||
], function () {
|
||||
var OO = {};
|
||||
|
||||
var getHistory = function (ctx, client, cb) {
|
||||
var c = ctx.clients[client];
|
||||
if (!c) { return void cb({error: 'ENOENT'}); }
|
||||
var chan = ctx.channels[c.channel];
|
||||
if (!chan) { return void cb({error: 'ENOCHAN'}); }
|
||||
cb();
|
||||
chan.history.forEach(function (msg) {
|
||||
ctx.emit('MESSAGE', {
|
||||
msg: msg,
|
||||
validateKey: chan.validateKey
|
||||
}, [client]);
|
||||
});
|
||||
ctx.emit('HISTORY_SYNCED', {}, [client]);
|
||||
};
|
||||
|
||||
var openChannel = function (ctx, obj, client, cb) {
|
||||
var channel = obj.channel;
|
||||
var padChan = obj.padChan;
|
||||
|
@ -24,11 +39,8 @@ define([
|
|||
// ==> Use our netflux ID to create our client ID
|
||||
if (!c.id) { c.id = chan.wc.myID + '-' + client; }
|
||||
|
||||
chan.history.forEach(function (msg) {
|
||||
ctx.emit('MESSAGE', {
|
||||
msg: msg,
|
||||
validateKey: chan.validateKey
|
||||
}, [client]);
|
||||
getHistory(ctx, client, function () {
|
||||
ctx.emit('READY', '', [client]);
|
||||
});
|
||||
|
||||
// ==> And push the new tab to the list
|
||||
|
@ -318,6 +330,9 @@ define([
|
|||
if (cmd === 'OPEN_CHANNEL') {
|
||||
return void openChannel(ctx, data, clientId, cb);
|
||||
}
|
||||
if (cmd === 'GET_HISTORY') {
|
||||
return void getHistory(ctx, clientId, cb);
|
||||
}
|
||||
if (cmd === 'REENCRYPT') {
|
||||
return void reencrypt(ctx, data, clientId, cb);
|
||||
}
|
||||
|
|
|
@ -1106,8 +1106,9 @@ define([
|
|||
var validate = nSecret.keys.validateKey;
|
||||
var crypto = Crypto.createEncryptor(nSecret.keys);
|
||||
Cryptpad.getHistoryRange({
|
||||
channel: channel,
|
||||
channel: data.channel || channel,
|
||||
validateKey: validate,
|
||||
toHash: data.toHash,
|
||||
lastKnownHash: data.lastKnownHash
|
||||
}, function (data) {
|
||||
cb({
|
||||
|
|
Loading…
Reference in New Issue