cryptpad/www/common/sframe-common-history.js

685 lines
26 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

define([
'jquery',
'/common/common-interface.js',
'/common/common-util.js',
'/common/hyperscript.js',
'/customize/messages.js',
'/bower_components/nthen/index.js',
//'/bower_components/chainpad-json-validator/json-ot.js',
'/bower_components/chainpad/chainpad.dist.js',
], function ($, UI, Util, h, Messages, nThen, ChainPad /* JsonOT */) {
//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..."); }
if (History.state) { return void console.error("Already loaded"); }
History.loading = true;
History.state = true;
var $toolbar = config.$toolbar;
var $hist = $toolbar.find('.cp-toolbar-history');
$hist.addClass('cp-history-init');
if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) {
throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory");
}
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) {
try {
JSON.parse(content);
return true;
} catch (e) {
console.log('Failed to parse, rejecting patch');
return false;
}
},
initialState: '',
logLevel: config.debug ? 2 : 0,
noPrune: true
});
};
var fillChainPad = function (realtime, messages) {
messages.forEach(function (m) {
realtime.message(m);
});
};
var realtime;
var states = [];
var patchWidth = 0;
var c = 0;//states.length - 1;
var getIndex = function (i) {
return states.length - 1 + i;
};
var getRank = function (idx) {
return idx - states.length + 1;
};
// Get the author or group of author linked to a state
var getAuthor = function (idx, semantic) {
if (semantic === 1 || !config.extractMetadata) {
return states[idx].author;
}
try {
var val = JSON.parse(states[idx].getContent().doc);
var md = config.extractMetadata(val);
var users = Object.keys(md.users).sort();
return users.join();
} catch (e) {
console.error(e);
return states[idx].author;
}
};
var bar = h('span.cp-history-timeline-bar');
var onResize = function () {
var $bar = $(bar);
if (!$bar.width() || !$bar.length) { return; }
var widthPx = patchWidth * $bar.width() / 100;
$hist.removeClass('cp-smallpatch');
$bar.find('.cp-history-snapshot').css('margin-left', "");
var $pos = $hist.find('.cp-history-timeline-pos');
$pos.css('margin-left', "");
if (widthPx < 18) {
$hist.addClass('cp-smallpatch');
$bar.find('.cp-history-snapshot').css('margin-left', (widthPx/2-2)+"px");
$pos.css('margin-left', (widthPx/2-2)+"px");
}
};
// Refresh the timeline UI with the block states
var refreshBar = function (snapshotsOnly) {
var $pos = $hist.find('.cp-history-timeline-pos');
var $bar = $(bar);
var users = {
list: [],
author: '',
el: undefined,
i: 0
};
var user = {
list: [],
author: '',
el: undefined,
i: 0
};
var snapshotsData = {};
var snapshots = [];
if (config.getLastMetadata) {
try {
var md = config.getLastMetadata();
if (md.snapshots) {
snapshotsData = md.snapshots;
snapshots = Object.keys(md.snapshots);
}
} catch (e) { console.error(e); }
}
var max = states.length - 1;
var snapshotsEl = [];
patchWidth = 100 / max;
// Check if we need a new block on the index i for the "obj" type (user or users)
var check = function (obj, author, i) {
if (snapshotsOnly) { return; }
if (obj.author !== author) {
obj.author = author;
if (obj.el) {
$(obj.el).css('width', (100*(i - obj.i)/max)+'%');
}
obj.el = h('span.cp-history-bar-el');
obj.list.push(obj.el);
obj.i = i;
}
};
var hash;
for (var i = 1; i < states.length; i++) {
hash = states[i].serverHash;
if (snapshots.indexOf(hash) !== -1) {
snapshotsEl.push(h('div.cp-history-snapshot', {
style: 'width:'+patchWidth+'%;left:'+(patchWidth * (i-1))+'%;',
title: snapshotsData[hash].title
}, h('i.fa.fa-camera')));
}
if (config.drive) {
// Display only one bar, split by patch
check(user, i, i);
} else {
// Display two bars, split by author(s)
check(user, getAuthor(i, 1), i);
check(users, getAuthor(i, 2), i);
}
}
if (snapshotsOnly) {
// We only want to redraw the snapshots
$bar.find('.cp-history-snapshots').html('').append([
$pos,
snapshotsEl
]);
} else {
$(user.el).css('width', (100*(max + 1 - user.i)/max)+'%');
if (!config.drive) {
$(users.el).css('width', (100*(max + 1 - users.i)/max)+'%');
}
$bar.html('').append([
h('span.cp-history-timeline-users', users.list),
h('span.cp-history-timeline-user', user.list),
h('div.cp-history-snapshots', [
$pos[0],
snapshotsEl
]),
]);
}
onResize();
};
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,
sharedFolder: config.sharedFolder
}, 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;
var messages = (data.messages || []).map(function (obj) {
return obj;
});
if (config.debug) { console.log(data.messages); }
Array.prototype.unshift.apply(allMessages, messages); // Destructive concat
fillChainPad(realtime, allMessages);
cb (null, realtime, data.isFull);
});
};
// config.setHistory(bool, bool)
// - bool1: history value
// - bool2: reset old content?
var render = function (val) {
if (typeof val === "undefined") { return; }
try {
config.applyVal(val);
} catch (e) {
// Probably a parse error
console.error(e);
}
};
var onClose = function () {
config.setHistory(false, true);
};
Messages.history_cantRestore = "Can't restore now. Disconnected."; // XXX
var onRevert = function () {
// Before we can restore the current version, we need to update metadataMgr
// so that it will uses the snapshots from the realtime version!
// Restoring the snapshots to their old version would go against the
// goal of having snapshots
if (config.getLastMetadata) {
var metadataMgr = common.getMetadataMgr();
var lastMd = config.getLastMetadata();
var _snapshots = lastMd.snapshots;
var md = Util.clone(metadataMgr.getMetadata());
md.snapshots = _snapshots;
metadataMgr.updateMetadata(md);
}
// And now we can properly restore the content
var closed = config.setHistory(false, false);
if (!closed) {
return void UI.alert(Messages.history_cantRestore);
}
config.onLocal();
config.onRemote();
return true;
};
config.setHistory(true);
var $bottom = $toolbar.find('.cp-toolbar-bottom');
var $cke = $toolbar.find('.cke_toolbox_main');
$hist.html('').css('display', 'flex');
$bottom.hide();
$cke.hide();
UI.spinner($hist).get().show();
var update = function (newRt) {
realtime = newRt;
if (!realtime) { return []; }
states = getStates(realtime);
refreshBar();
return states;
};
var $loadMore, $version, $time, get;
// Get the content of the selected version, and change the version number
var loading = false;
var loadMore = function (cb) {
if (loading) { return; }
loading = true;
$loadMore.find('.fa-ellipsis-h').hide();
$loadMore.find('.fa-refresh').show();
loadMoreHistory(config, common, function (err, newRt, isFull) {
if (err === 'EFULL') {
$loadMore.off('click').hide();
get(c);
$version.show();
return;
}
loading = false;
if (err) { return void console.error(err); }
update(newRt);
$loadMore.find('.fa-ellipsis-h').show();
$loadMore.find('.fa-refresh').hide();
get(c);
if (isFull) {
$loadMore.off('click').hide();
$version.show();
}
if (cb) { cb(); }
});
};
// semantic === 1 : group by user
// semantic === 2 : group by "group of users"
get = function (i, blockOnly, semantic) {
i = parseInt(i);
if (isNaN(i)) { return; }
if (i > 0) { i = 0; }
if (i < -(states.length - 2)) { i = -(states.length - 2); }
if (i <= -(states.length - 11)) {
loadMore();
}
var idx = getIndex(i);
if (semantic && i !== c) {
// If semantic is truc, jump to the next patch from a different netflux ID
var author = getAuthor(idx, semantic);
var forward = i > c;
for (var j = idx; (j > 0 && j < states.length ); (forward ? j++ : j--)) {
if (author !== getAuthor(j, semantic)) {
break;
}
idx = j;
i = getRank(idx);
}
}
if (blockOnly) { return states[idx]; }
var val = states[idx].getContent().doc;
c = i;
$hist.find('.cp-toolbar-history-next, .cp-toolbar-history-previous')
.prop('disabled', '');
if (c === -(states.length-1)) {
$hist.find('.cp-toolbar-history-previous').prop('disabled', 'disabled');
}
if (c === 0) {
$hist.find('.cp-toolbar-history-next').prop('disabled', 'disabled');
}
var $pos = $hist.find('.cp-history-timeline-pos');
var p = 100 * (1 - (-(c - 1) / (states.length-1)));
$pos.css('left', p+'%');
$pos.css('width', patchWidth+'%');
// 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));
var time = states[idx].time;
if (time) {
$time.text(new Date(time).toLocaleString());
} else { $time.text(''); }
if (config.debug) {
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); });
}
}
return val || '';
};
/*
var getNext = function (step) {
return typeof step === "number" ? get(c + step) : get(c + 1);
};
var getPrevious = function (step) {
return typeof step === "number" ? get(c - step) : get(c - 1);
};
*/
var makeSnapshot = function (title) {
var idx = getIndex(c);
if (!config.getLastMetadata || !config.setLastMetadata) { return; }
try {
var block = states[idx];
var hash = block.serverHash;
var md = config.getLastMetadata();
md.snapshots = md.snapshots || {};
if (md.snapshots[hash]) { return; }
md.snapshots[hash] = {
title: title,
time: block.time ? (+new Date(block.time)) : +new Date()
};
var sent = config.setLastMetadata(md);
if (!sent) { return void UI.warn(Messages.error); }
refreshBar();
} catch (e) {
console.error(e);
}
};
Messages.history_fastPrev = "Previous editing session"; // XXX
Messages.history_userPrev = "Previous user"; // XXX
Messages.history_fastNext = "Next editing session"; // XXX
Messages.history_userNext = "Next user"; // XXX
// Create the history toolbar
var display = function () {
$hist.html('');
$hist.removeClass('cp-history-init');
var fastPrev = h('button.cp-toolbar-history-previous', { title: Messages.history_fastPrev }, [
h('i.fa.fa-step-backward'),
h('i.fa.fa-users')
]);
var userPrev = h('button.cp-toolbar-history-previous', { title: Messages.history_userPrev }, [
h('i.fa.fa-step-backward'),
h('i.fa.fa-user')
]);
var prev = h('button.cp-toolbar-history-previous', { title: Messages.history_prev }, [
h('i.fa.fa-step-backward')
]);
var fastNext = h('button.cp-toolbar-history-next', { title: Messages.history_fastNnext }, [
h('i.fa.fa-users'),
h('i.fa.fa-step-forward'),
]);
var userNext = h('button.cp-toolbar-history-next', { title: Messages.history_userNext }, [
h('i.fa.fa-user'),
h('i.fa.fa-step-forward'),
]);
var next = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
h('i.fa.fa-step-forward')
]);
if (config.drive) {
fastNext = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
h('i.fa.fa-fast-forward'),
]);
fastPrev = h('button.cp-toolbar-history-previous', {title: Messages.history_prev}, [
h('i.fa.fa-fast-backward'),
]);
}
var $fastPrev = $(fastPrev);
var $userPrev = $(userPrev);
var $prev = $(prev);
var $fastNext = $(fastNext);
var $userNext = $(userNext);
var $next = $(next);
var _loadMore = h('button.cp-toolbar-history-loadmore', { title: Messages.history_loadMore }, [
h('i.fa.fa-ellipsis-h'),
h('i.fa.fa-refresh.fa-spin.fa-3x.fa-fw', { style: 'display: none;' })
]);
var pos = h('span.cp-history-timeline-pos');
var time = h('div.cp-history-timeline-time');
$time = $(time);
$version = $(); // XXX
var timeline = h('div.cp-toolbar-history-timeline', [
h('div.cp-history-timeline-line', [
h('span.cp-history-timeline-legend', [
h('i.fa.fa-users'),
h('i.fa.fa-user')
]),
h('span.cp-history-timeline-loadmore', _loadMore),
h('span.cp-history-timeline-container', [
bar
])
]),
h('div.cp-history-timeline-actions', [
h('span.cp-history-timeline-prev', [
fastPrev,
config.drive ? undefined : userPrev,
prev,
]),
time,
h('span.cp-history-timeline-next', [
next,
config.drive ? undefined : userNext,
fastNext
])
])
]);
Messages.history_restore = "Restore";// XXX
Messages.history_close = "Close";// XXX
Messages.history_shareTitle = "Share a link to this version"; // XXX
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;
}
if (config.drive) {
$hist.addClass('cp-history-drive');
$(snapshot).hide();
$(share).hide();
}
$hist.append([timeline, actions]);
onResize();
$(window).on('resize', onResize);
var $bar = $(bar);
$bar.find('.cp-history-snapshots').append(pos);
$bar.click(function (e) {
e.stopPropagation();
var $t = $(e.target);
if ($t.closest('.cp-history-snapshot').length) {
$t = $t.closest('.cp-history-snapshot');
}
var isEl = $t.is('.cp-history-snapshot');
if (!$t.is('.cp-history-snapshots') && !isEl) { return; }
var x = e.offsetX;
if (isEl) {
x += $t.position().left;
}
var p = x / $bar.width();
var v = 1-Math.ceil((states.length - 1) * (1 - p));
render(get(v));
});
$loadMore = $(_loadMore).click(function () {
loadMore(function () {
get(c);
});
});
var onKeyDown, onKeyUp;
var closeUI = function () {
History.state = false;
$hist.hide();
$bottom.show();
$cke.show();
$(window).trigger('resize');
$(window).off('keydown', onKeyDown);
$(window).off('keyup', onKeyUp);
};
// Version buttons
$prev.click(function () { render(get(c - 1)); });
$next.click(function () { render(get(c + 1)); });
if (config.drive) {
$fastPrev.click(function () { render(get(c - 10)); });
$fastNext.click(function () { render(get(c + 10)); });
$userPrev.click(function () { render(get(c - 10)); });
$userNext.click(function () { render(get(c + 10)); });
} else {
$userPrev.click(function () { render(get(c - 1, false, 1)); });
$userNext.click(function () { render(get(c + 1, false, 1)); });
$fastPrev.click(function () { render(get(c - 1, false, 2)); });
$fastNext.click(function () { render(get(c + 1, false, 2)); });
}
onKeyDown = function (e) {
var p = function () { e.preventDefault(); };
if (e.which === 39) { p(); return $next.click(); } // Right
if (e.which === 37) { p(); return $prev.click(); } // Left
if (e.which === 38) { p(); return $userNext.click(); } // Up
if (e.which === 40) { p(); return $userPrev.click(); } // Down
if (e.which === 33) { p(); return $fastNext.click(); } // PageUp
if (e.which === 34) { p(); return $fastPrev.click(); } // PageUp
if (e.which === 27) { p(); $(close).click(); }
};
onKeyUp = function (e) { e.stopPropagation(); };
$(window).on('keydown', onKeyDown).on('keyup', onKeyUp).focus();
// Snapshots
$(snapshot).click(function () {
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',
icon: 'fa-camera',
name: Messages.snapshots_new,
onClick: function () {
var val = $input.val();
if (!val) { return true; }
makeSnapshot(val);
},
keys: [],
}];
UI.openCustomModal(UI.dialog.customModal(content, {buttons: buttons }));
setTimeout(function () {
$input.focus();
});
});
// Share
$(share).click(function () {
var block = get(c, true);
common.getSframeChannel().event('EV_SHARE_OPEN', {
versionHash: block.serverHash,
//title: title
});
});
// Close & restore buttons
$(close).click(function () {
states = [];
onClose();
closeUI();
});
$(restore).click(function () {
UI.confirm(Messages.history_restorePrompt, function (yes) {
if (!yes) { return; }
var done = onRevert();
if (done) {
closeUI();
UI.log(Messages.history_restoreDone);
}
});
});
// Display the latest content
render(get(c));
$(window).trigger('resize');
};
if (config.onOpen) {
config.onOpen();
}
// Load all the history messages into a new chainpad object
loadMoreHistory(config, common, function (err, newRt, isFull) {
History.readOnly = common.getMetadataMgr().getPrivateData().readOnly;
History.loading = false;
if (err) { throw new Error(err); }
update(newRt);
display();
if (isFull) {
$loadMore.off('click').hide();
$version.show();
}
});
};
return History;
});