Merge branch 'history' into historyOO

pull/1/head
yflory 4 years ago
commit 2287c81d86

@ -5,15 +5,180 @@
}
& {
.cp-toolbar-history {
@history_lineBg: #FFFFFF;
@history_userBg1: #DDD;
@history_userBg2: #BBB;
@pos-color: @cryptpad_text_col;
@fill-width: 40px;
display: none;
text-align: center;
width: 100%;
padding: 10px 0;
padding: 10px 0 0;
align-items: center;
justify-content: center;
color: @cryptpad_text_col;
* {
font: @colortheme_app-font;
}
.cp-toolbar-history-timeline {
display: flex;
flex-flow: column;
flex: 1;
margin-left: 10px;
margin-right: @fill-width;
}
.cp-toolbar-history-actions {
display: flex;
justify-content: space-between;
align-items: center;
height: 39px;
align-self: baseline;
margin-right: 5px;
.cp-history-actions-first {
margin-right: @fill-width;
}
button {
margin: 0 5px;
border: 1px solid @cryptpad_text_col;
text-transform: uppercase;
.fa:not(:last-child) {
margin-right: 5px;
}
}
}
&.cp-smallpatch {
.cp-history-snapshot {
border: none !important;
width: 2px !important;
background: @pos-color;
}
.cp-history-timeline-pos {
border: none !important;
width: 2px !important;
background: @pos-color;
&:after {
margin-left: -8px !important;
top: -8px !important;
}
}
}
.cp-history-timeline-line {
display: flex;
.cp-history-timeline-legend {
display: flex;
flex-flow: column;
justify-content: space-around;
align-items: center;
margin-right: 4px;
}
.cp-history-timeline-loadmore {
width: 20px;
display: flex;
align-items: center;
justify-content: center;
button {
padding: 0;
width: 100%;
height: 100%;
background: @history_lineBg;
margin-right: 1px;
.fa-refresh {
font-size: 13px;
}
}
}
.cp-history-timeline-container {
flex: 1;
position: relative;
background-color: @history_lineBg;
height: 39px;
}
.cp-history-timeline-bar {
display: flex;
flex-flow: column;
padding: 1px;
& > span {
height: 18px;
display: flex;
}
.cp-history-timeline-users {
margin-bottom: 1px;
.cp-history-bar-el {
background-color: @history_userBg1;
&:nth-child(2n) {
background-color: @history_userBg2;
}
}
}
.cp-history-timeline-user {
.cp-history-bar-el {
background-color: @history_userBg2;
&:nth-child(2n) {
background-color: @history_userBg1;
}
}
}
.cp-history-snapshots {
position: absolute;
left: 1px;
right: 1px;
top: 1px;
bottom: 1px;
.cp-history-snapshot {
position: absolute;
border: 2px solid @cryptpad_text_col;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
.cp-history-timeline-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-left: 40px;
button {
width: 50px;
.fa:first-child:not(:last-child) {
margin-right: 5px;
}
}
.cp-history-timeline-next {
button:last-child {
margin-right: 0;
}
}
.cp-history-timeline-prev {
button:first-child {
margin-left: 0;
}
}
}
.cp-history-timeline-pos {
//width: 2px;
border: 2px solid @cryptpad_text_col;
height: 37px;
//background: @pos-color;
position: absolute;
&:after {
content: '';
border: 9px solid transparent;
border-top-color: @pos-color;
position: absolute;
top: -9px;
margin-left: -1px;
width: 100%;
}
}
/*
.cp-history-filler {
flex: 1;
}
@ -92,6 +257,7 @@
.fa-spinner {
font-size: 66px;
}
*/
}
}

@ -663,6 +663,20 @@
display: inline-flex;
align-items: center;
}
&.cp-toolbar-unsync {
.cp-toolbar-title-edit, .cp-toolbar-title-save {
display: none !important;
}
.cp-toolbar-title-unsync {
display: inline-flex;
}
.cp-toolbar-title-editable, .cp-toolbar-title-edit {
border: none !important;
}
}
.cp-toolbar-title-unsync {
display: none;
}
.cp-toolbar-title-hoverable {
display: inline-flex;
overflow: hidden;
@ -886,11 +900,19 @@
}
}
.cp-toolbar-history {
.cp-toolbar-history, .cp-toolbar-snapshots {
background-color: @toolbar-bg-color-light;
background-color: var(--toolbar-bg-color-light);
color: @cryptpad_text_col;
}
.cp-toolbar-snapshots {
display: none;
text-align: center;
width: 100%;
padding: 10px 0;
align-items: center;
justify-content: center;
}
.cp-toolbar-bottom {
background-color: @toolbar-bg-color-light;
background-color: var(--toolbar-bg-color-light);

@ -876,6 +876,21 @@ define([
common.createNewPadModal();
});
break;
case 'snapshots':
button = $('<button>', {
title: Messages.snapshots_button,
'class': 'fa fa-camera cp-toolbar-icon-snapshots',
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.snapshots_button));
button
.click(common.prepareFeedback(type))
.click(function () {
data = data || {};
if (typeof(data.load) !== "function" || typeof(data.make) !== "function") {
return;
}
UIElements.openSnapshotsModal(common, data.load, data.make);
});
break;
default:
data = data || {};
var drawerCls = data.drawer === false ? '' : '.cp-toolbar-drawer-element';
@ -3301,5 +3316,67 @@ define([
return (pos.bottom < size) && (pos.y > 0);
};
Messages.snapshots_button = "Snapshots";
Messages.snapshots_new = "New snapshot"; // XXX
Messages.snapshots_placeholder = "Snapshot title"; // XXX
Messages.snapshots_open = "Open";
UIElements.openSnapshotsModal = function (common, load, make) {
var metadataMgr = common.getMetadataMgr();
var md = metadataMgr.getMetadata();
var snapshots = md.snapshots || {};
var modal;
var list = Object.keys(snapshots).sort(function (h1, h2) {
var s1 = snapshots[h1];
var s2 = snapshots[h2];
return s1.time - s2.time;
}).map(function (hash) {
var s = snapshots[hash];
var button = h('button.btn.btn-secondary', Messages.snapshots_open);
$(button).click(function () {
load(hash, s);
if (modal && modal.closeModal) { modal.closeModal(); }
});
return h('span.cp-snapshot-element', [
h('i.fa.fa-camera'),
h('span.cp-snapshot-title', s.title),
button
]);
});
var input = h('input', {
placeholder: Messages.snapshots_placeholder
});
var $input = $(input);
var content = h('div', [
h('h4', Messages.snapshots_button),
h('div.cp-snapshots-container', list),
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; }
make(val);
},
keys: [],
}];
modal = UI.openCustomModal(UI.dialog.customModal(content, {buttons: buttons }));
setTimeout(function () {
$input.focus();
});
};
return UIElements;
});

@ -924,6 +924,12 @@ define([
// -1 ==> no timeout, we may receive the callback only when we reconnect
postMessage("SEND_PAD_MSG", data, cb, { timeout: -1 });
};
pad.getLastHash = function (data, cb) {
postMessage("GET_LAST_HASH", data, cb);
};
pad.getSnapshot = function (data, cb) {
postMessage("GET_SNAPSHOT", data, cb);
};
pad.onReadyEvent = Util.mkEvent();
pad.onMessageEvent = Util.mkEvent();
pad.onJoinEvent = Util.mkEvent();

@ -0,0 +1,104 @@
define([
'jquery',
'/common/common-interface.js',
'/common/hyperscript.js',
'/customize/messages.js',
'/bower_components/nthen/index.js',
'/bower_components/chainpad/chainpad.dist.js',
], function ($, UI, h, Messages, nThen, ChainPad /* JsonOT */) {
var Snapshots = {};
Snapshots.create = function (common, config) {
if (!config.$toolbar) { return void console.error("config.$toolbar is undefined");}
if (Snapshots.loading) { return void console.error("Snapshot is already being loaded..."); }
Snapshots.loading = true;
var sframeChan = common.getSframeChannel();
var $toolbar = config.$toolbar;
var $snap = $toolbar.find('.cp-toolbar-snapshots');
var $bottom = $toolbar.find('.cp-toolbar-bottom');
var $cke = $toolbar.find('.cke_toolbox_main');
$snap.html('').css('display', 'flex');
$bottom.hide();
$cke.hide();
var createChainPad = function () {
return ChainPad.create({
userName: 'snapshot',
validateContent: function (content) {
try {
JSON.parse(content);
return true;
} catch (e) {
console.log('Failed to parse, rejecting patch');
return false;
}
},
initialState: '',
logLevel: 0
});
};
var snapshot;
var getData = function () {
sframeChan.query("Q_GET_SNAPSHOT", {hash: config.hash}, function (err, obj) {
if (err || (obj && obj.error)) { return void console.error(err || obj.error); }
if (!Array.isArray(obj)) { return void console.error("invalid type"); }
var messages = obj;
var chainpad = createChainPad();
messages.forEach(function (m) {
chainpad.message(m);
});
snapshot = chainpad.getAuthDoc();
config.applyVal(snapshot);
chainpad.abort();
});
};
Messages.snapshots_restore = "Restore"; // XXX
Messages.snapshots_close = "Close";
Messages.snapshots_cantRestore = "Can't restore now. Disconnected...";
var display = function () {
var data = config.data || {};
if (!config.readOnly) {
$(h('button.cp-toolbar-snapshots-restore', [
h('i.fa.fa-check'),
h('spap.cp-button-name', Messages.snapshots_restore)
])).click(function () {
var closed = config.close(true, snapshot);
if (!closed) {
return void UI.alert(Messages.snapshots_cantRestore);
}
$snap.hide();
$bottom.show();
$cke.show();
Snapshots.loading = false;
}).appendTo($snap);
}
$(h('span.cp-toolbar-snapshots-title', data.title)).appendTo($snap);
$(h('button.cp-toolbar-snapshots-close', [
h('i.fa.fa-times'),
h('spap.cp-button-name', Messages.snapshots_close)
])).click(function () {
$snap.hide();
$bottom.show();
$cke.show();
Snapshots.loading = false;
config.close(false);
}).appendTo($snap);
};
display();
getData();
};
return Snapshots;
});

@ -1455,6 +1455,14 @@ define([
var channels = Store.channels = store.channels = {};
Store.getSnapshot = function (clientId, data, cb) {
Store.getHistoryRange(clientId, {
cpCount: 1,
channel: data.channel,
lastKnownHash: data.hash
}, cb);
};
var getVersionHash = function (clientId, data) {
var fakeNetflux = Hash.createChannelId();
Store.getHistoryRange(clientId, {
@ -1558,7 +1566,8 @@ define([
}
postMessage(clientId, "PAD_READY");
},
onMessage: function (m, user, validateKey, isCp) {
onMessage: function (m, user, validateKey, isCp, hash) {
channel.lastHash = hash;
channel.pushHistory(m, isCp);
channel.bcast("PAD_MESSAGE", {
user: user,
@ -1644,6 +1653,7 @@ define([
return void cb({ error: err });
}
// Broadcast to other tabs
channel.lastHash = msg.slice(0,64);
channel.pushHistory(CpNetflux.removeCp(msg), /^cp\|/.test(msg));
channel.bcast("PAD_MESSAGE", {
user: wc.myID,
@ -1779,6 +1789,15 @@ define([
cb();
};
Store.getLastHash = function (clientId, data, cb) {
var chan = channels[data.channel];
if (!chan) { return void cb({error: 'ENOCHAN'}); }
if (!chan.lastHash) { return void cb({error: 'EINVAL'}); }
cb({
hash: chan.lastHash
});
};
// Delete a pad received with a burn after reading URL
var notifyOwnerPadRemoved = function (data, obj) {

@ -86,6 +86,8 @@ define([
GET_PAD_METADATA: Store.getPadMetadata,
SET_PAD_METADATA: Store.setPadMetadata,
CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin,
GET_LAST_HASH: Store.getLastHash,
GET_SNAPSHOT: Store.getSnapshot,
// Drive
DRIVE_USEROBJECT: Store.userObjectCommand,
// Settings,

@ -13,6 +13,7 @@ define([
'/common/common-ui-elements.js',
'/common/common-thumbnail.js',
'/common/common-feedback.js',
'/common/inner/snapshots.js',
'/customize/application_config.js',
'/bower_components/chainpad/chainpad.dist.js',
'/common/test.js',
@ -35,6 +36,7 @@ define([
UIElements,
Thumb,
Feedback,
Snapshots,
AppConfig,
ChainPad,
Test)
@ -43,6 +45,9 @@ define([
var UNINITIALIZED = 'UNINITIALIZED';
// History and snapshots mode shouldn't receive realtime data or push to chainpad
var unsyncMode = false;
var STATE = Object.freeze({
DISCONNECTED: 'DISCONNECTED',
FORGOTTEN: 'FORGOTTEN',
@ -50,7 +55,6 @@ define([
INFINITE_SPINNER: 'INFINITE_SPINNER',
ERROR: 'ERROR',
INITIALIZING: 'INITIALIZING',
HISTORY_MODE: 'HISTORY_MODE',
READY: 'READY'
});
@ -93,6 +97,7 @@ define([
});
});
var onLocal;
var textContentGetter;
var titleRecommender = function () { return false; };
var contentGetter = function () { return UNINITIALIZED; };
@ -125,20 +130,40 @@ define([
return;
};
var makeSnapshot = function (title) {
var sframeChan = common.getSframeChannel();
sframeChan.query("Q_GET_LAST_HASH", null, function (err, obj) {
if (err || (obj && obj.error)) { return void UI.warn(Messages.error); }
var hash = obj.hash;
if (!hash) { return void UI.warn(Messages.error); }
var md = Util.clone(cpNfInner.metadataMgr.getMetadata());
var snapshots = md.snapshots = md.snapshots || {};
if (snapshots[hash]) { return void UI.warn(Messages.error); } // XXX EEXISTS
snapshots[hash] = {
title: title,
time: +new Date()
};
cpNfInner.metadataMgr.updateMetadata(md);
onLocal();
});
};
var stateChange = function (newState, text) {
var wasEditable = (state === STATE.READY);
if (state === STATE.DELETED || state === STATE.ERROR) { return; }
if (state === STATE.INFINITE_SPINNER && newState !== STATE.READY) { return; }
if (newState === STATE.INFINITE_SPINNER || newState === STATE.DELETED) {
state = newState;
} else if (newState === STATE.ERROR) {
state = newState;
} else if (state === STATE.DISCONNECTED && newState !== STATE.INITIALIZING) {
throw new Error("Cannot transition from DISCONNECTED to " + newState); // FIXME we are getting "DISCONNECTED to READY" on prod
} else if (state !== STATE.READY && newState === STATE.HISTORY_MODE) {
throw new Error("Cannot transition from " + state + " to " + newState);
} else {
state = newState;
var wasEditable = (state === STATE.READY && !unsyncMode);
if (newState !== state) {
if (state === STATE.DELETED || state === STATE.ERROR) { return; }
if (state === STATE.INFINITE_SPINNER && newState !== STATE.READY) { return; }
if (newState === STATE.INFINITE_SPINNER || newState === STATE.DELETED) {
state = newState;
} else if (newState === STATE.ERROR) {
state = newState;
} else if (state === STATE.DISCONNECTED && newState !== STATE.INITIALIZING) {
throw new Error("Cannot transition from DISCONNECTED to " + newState); // FIXME we are getting "DISCONNECTED to READY" on prod
} else {
state = newState;
}
} else if (state === STATE.READY) {
// Refreshing ready state
}
switch (state) {
case STATE.DISCONNECTED:
@ -187,8 +212,9 @@ define([
}
default:
}
if (wasEditable !== (state === STATE.READY)) {
evEditableStateChange.fire(state === STATE.READY);
var isEditable = (state === STATE.READY && !unsyncMode);
if (wasEditable !== isEditable) {
evEditableStateChange.fire(isEditable);
}
};
@ -205,8 +231,8 @@ define([
}
};
var onLocal;
var onRemote = function () {
if (unsyncMode) { return; }
if (state !== STATE.READY) { return; }
var oldContent = normalize(contentGetter());
@ -261,13 +287,84 @@ define([
});
};
var setUnsyncMode = function (bool) {
if (unsyncMode === bool) { return; }
unsyncMode = bool;
evEditableStateChange.fire(state === STATE.READY && !unsyncMode);
stateChange(state);
};
// History mode:
// When "bool" is true, we're entering in history mode
// When "bool" is false and "update" is true, it means we're closing the history
// and should update the content
// When "bool" is false and "update" is false, it means we're restoring an old version,
// no need to refresh
var setHistoryMode = function (bool, update) {
if (!bool && !update && state !== STATE.READY) { return false; }
cpNfInner.metadataMgr.setHistory(bool);
stateChange((bool) ? STATE.HISTORY_MODE : STATE.READY);
toolbar.setHistory(bool);
setUnsyncMode(bool);
if (!bool && update) { onRemote(); }
else {
setTimeout(cpNfInner.metadataMgr.refresh);
}
return true;
};
var closeSnapshot = function (restore) {
if (restore && state !== STATE.READY) { return false; }
toolbar.setSnapshot(false);
setUnsyncMode(false); // Unlock onLocal and onRemote
if (restore) { onLocal(); } // Restore? commit the content
onRemote(); // Make sure we're back to the realtime content
return true;
};
var loadSnapshot = function (hash, data) {
setUnsyncMode(true);
toolbar.setSnapshot(true);
Snapshots.create(common, {
readOnly: readOnly,
$toolbar: $(toolbarContainer),
hash: hash,
data: data,
close: closeSnapshot,
applyVal: function (val) {
var newContent = JSON.parse(val);
var meta = extractMetadata(newContent);
cpNfInner.metadataMgr.updateMetadata(meta);
contentUpdate(normalize(newContent) || ["BODY",{},[]], function (h) {
return h;
});
},
});
};
// Get the realtime metadata when in history mode
var getLastMetadata = function () {
if (!unsyncMode) { return; }
var newContentStr = cpNfInner.chainpad.getUserDoc();
var newContent = JSON.parse(newContentStr);
var meta = extractMetadata(newContent);
return meta;
};
var setLastMetadata = function (md) {
if (!unsyncMode) { return; }
var newContentStr = cpNfInner.chainpad.getAuthDoc();
var newContent = JSON.parse(newContentStr);
if (Array.isArray(newContent)) {
newContent[3] = {
metadata: md
};
} else {
newContent.metadata = md;
}
try {
cpNfInner.chainpad.contentUpdate(JSONSortify(newContent));
return true;
} catch (e) {
console.error(e);
return false;
}
};
/*
@ -285,6 +382,7 @@ define([
*/
onLocal = function (/*padChange*/) {
if (unsyncMode) { return; }
if (state !== STATE.READY) { return; }
if (readOnly) { return; }
@ -358,7 +456,9 @@ define([
}
cpNfInner.metadataMgr.updateMetadata(metadata);
newContent = normalize(newContent);
contentUpdate(newContent, waitFor);
if (!unsyncMode) {
contentUpdate(newContent, waitFor);
}
} else {
if (!cpNfInner.metadataMgr.getPrivateData().isNewFile) {
// We're getting 'new pad' but there is an existing file
@ -695,6 +795,9 @@ define([
onLocal: onLocal,
onRemote: onRemote,
setHistory: setHistoryMode,
extractMetadata: extractMetadata, // extract from current version
getLastMetadata: getLastMetadata, // get from authdoc
setLastMetadata: setLastMetadata, // set to userdoc/authdoc
applyVal: function (val) {
var newContent = JSON.parse(val);
var meta = extractMetadata(newContent);
@ -709,6 +812,12 @@ define([
$hist.addClass('cp-hidden-if-readonly');
toolbar.$drawer.append($hist);
var $snapshot = common.createButton('snapshots', true, {
make: makeSnapshot,
load: loadSnapshot
});
toolbar.$drawer.append($snapshot);
var $copy = common.createButton('copy', true);
toolbar.$drawer.append($copy);

@ -1,20 +1,25 @@
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, h, nThen, ChainPad /* JsonOT */) {
], 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');
if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) {
throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory");
@ -55,6 +60,132 @@ define([
});
};
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')));
}
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-snapshotsi').html('').append([
$pos,
snapshotsEl
]);
} else {
$(user.el).css('width', (100*(max + 1 - user.i)/max)+'%');
$(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;
@ -93,24 +224,37 @@ define([
console.error(e);
}
};
var onClose = function () { config.setHistory(false, true); };
var onClose = function () {
config.setHistory(false, true);
};
Messages.history_cantRestore = "Can't restore now. Disconnected."; // XXX
var onRevert = function () {
config.setHistory(false, false);
// 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 Messages = common.Messages;
var realtime;
var states = [];
var c = 0;//states.length - 1;
var semantic = false;
var $hist = $toolbar.find('.cp-toolbar-history');
var $bottom = $toolbar.find('.cp-toolbar-bottom');
var $cke = $toolbar.find('.cke_toolbox_main');
@ -120,13 +264,11 @@ define([
UI.spinner($hist).get().show();
var onUpdate;
var update = function (newRt) {
realtime = newRt;
if (!realtime) { return []; }
states = getStates(realtime);
if (typeof onUpdate === "function") { onUpdate(); }
refreshBar();
return states;
};
@ -137,8 +279,8 @@ define([
var loadMore = function (cb) {
if (loading) { return; }
loading = true;
$loadMore.removeClass('fa fa-ellipsis-h')
.append($('<span>', {'class': 'fa fa-refresh fa-spin fa-3x fa-fw'}));
$loadMore.find('.fa-ellipsis-h').hide();
$loadMore.find('.fa-refresh').show();
loadMoreHistory(config, common, function (err, newRt, isFull) {
if (err === 'EFULL') {
@ -150,7 +292,8 @@ define([
loading = false;
if (err) { return void console.error(err); }
update(newRt);
$loadMore.addClass('fa fa-ellipsis-h').html('');
$loadMore.find('.fa-ellipsis-h').show();
$loadMore.find('.fa-refresh').hide();
get(c);
if (isFull) {
$loadMore.off('click').hide();
@ -160,14 +303,9 @@ define([
});
};
var getIndex = function (i) {
return states.length - 1 + i;
};
var getRank = function (idx) {
return idx - states.length + 1;
};
get = function (i, blockOnly) {
// 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; }
@ -177,15 +315,16 @@ define([
}
var idx = getIndex(i);
if (semantic) {
if (semantic && i !== c) {
// If semantic is truc, jump to the next patch from a different netflux ID
var author = states[idx].author;
for (var j = idx; (j > 1 && j < (states.length - 1)); ((i > c) ? j++ : j--)) {
idx = j;
i = getRank(idx);
if (author !== states[j].author) {
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);
}
}
@ -193,21 +332,18 @@ define([
var val = states[idx].getContent().doc;
c = i;
if (typeof onUpdate === "function") { onUpdate(); }
$hist.find('.cp-toolbar-history-next, .cp-toolbar-history-previous, ' +
'.cp-toolbar-history-fast-next, .cp-toolbar-history-fast-previous')
.css('visibility', '');
$hist.find('.cp-toolbar-history-next, .cp-toolbar-history-previous')
.prop('disabled', '');
if (c === -(states.length-1)) {
$hist.find('.cp-toolbar-history-previous').css('visibility', 'hidden');
$hist.find('.cp-toolbar-history-fast-previous').css('visibility', 'hidden');
$hist.find('.cp-toolbar-history-previous').prop('disabled', 'disabled');
}
if (c === 0) {
$hist.find('.cp-toolbar-history-next').css('visibility', 'hidden');
$hist.find('.cp-toolbar-history-fast-next').css('visibility', 'hidden');
$hist.find('.cp-toolbar-history-next').prop('disabled', 'disabled');
}
var $pos = $hist.find('.cp-toolbar-history-pos');
var p = 100 * (1 - (-c / (states.length-2)));
$pos.css('margin-left', p+'%');
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
@ -229,87 +365,182 @@ define([
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('');
var $rev = $('<button>', {
'class':'cp-toolbar-history-revert buttonSuccess fa fa-check-circle-o',
title: Messages.history_restoreTitle
}).appendTo($hist);//.text(Messages.history_restore);
if (History.readOnly) { $rev.css('visibility', 'hidden'); }
Messages.history_session = "Group by user"; // XXX
var $cbox = $(UI.createCheckbox('cp-history-session',
Messages.history_session,
false, { label: { class: 'noTitle' } })).appendTo($hist);
$('<span>', {'class': 'cp-history-filler'}).appendTo($hist);
var $fastPrev = $('<button>', {
'class': 'cp-toolbar-history-fast-previous fa fa-fast-backward buttonPrimary',
title: Messages.history_prev
}).appendTo($hist);
var $prev =$('<button>', {
'class': 'cp-toolbar-history-previous fa fa-step-backward buttonPrimary',
title: Messages.history_prev
}).appendTo($hist);
var $nav = $('<div>', {'class': 'cp-toolbar-history-goto'}).appendTo($hist);
var $next = $('<button>', {
'class': 'cp-toolbar-history-next fa fa-step-forward buttonPrimary',
title: Messages.history_next
}).appendTo($hist);
var $fastNext = $('<button>', {
'class': 'cp-toolbar-history-fast-next fa fa-fast-forward buttonPrimary',
title: Messages.history_next
}).appendTo($hist);
var $share = $('<button>', {
'class': 'fa fa-shhare-alt buttonPrimary',
title: Messages.shareButton
}).appendTo($hist);
$('<span>', {'class': 'cp-history-filler'}).appendTo($hist);
$time = $(h('div')).appendTo($hist);
var $close = $('<button>', {
'class':'cp-toolbar-history-close fa fa-window-close',
title: Messages.history_closeTitle
}).appendTo($hist);
var $bar = $('<div>', {'class': 'cp-toolbar-history-bar'}).appendTo($nav);
var $container = $('<div>', {'class':'cp-toolbar-history-pos-container'}).appendTo($bar);
$('<div>', {'class': 'cp-toolbar-history-pos'}).appendTo($container);
$version = $('<span>', {
'class': 'cp-toolbar-history-version'
}).prependTo($bar).hide();
$loadMore = $('<button>', {
'class':'cp-toolbar-history-loadmore fa fa-ellipsis-h',
title: Messages.history_loadMore
}).click(function () {
loadMore(function () {
get(c);
});
}).prependTo($container);
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_next }, [
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_fastNext }, [
h('i.fa.fa-step-forward')
]);
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,
userPrev,
prev,
]),
time,
h('span.cp-history-timeline-next', [
next,
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) {
snapshot.disabled = true;
share.disabled = true;
}
$hist.append([timeline, actions]);
onResize();
$(window).on('resize', onResize);
// Load a version when clicking on the bar
$container.click(function (e) {
var $bar = $(bar);
$bar.find('.cp-history-snapshots').append(pos);
$bar.click(function (e) {
e.stopPropagation();
if (!$(e.target).is('.cp-toolbar-history-pos-container')) { return; }
var p = e.offsetX / $container.width();
var v = -Math.round((states.length - 1) * (1 - p));
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));
});
onUpdate = function () {
// Called when a new version is loaded
};
$loadMore = $(_loadMore).click(function () {
loadMore(function () {
get(c);
});
});
var onKeyDown, onKeyUp;
var close = function () {
var closeUI = function () {
History.state = false;
$hist.hide();
$bottom.show();
$cke.show();
@ -318,35 +549,62 @@ define([
$(window).off('keyup', onKeyUp);
};
var $checkbox = $cbox.find('input').on('change', function () {
semantic = $checkbox.is(':checked');
if (semantic) {
$fastPrev.hide();
$fastNext.hide();
} else {
$fastPrev.show();
$fastNext.show();
}
});
// Version buttons
$prev.click(function () { render(getPrevious()); });
$next.click(function () { render(getNext()); });
$fastPrev.click(function () { render(getPrevious(10)); });
$fastNext.click(function () { render(getNext(10)); });
$prev.click(function () { render(get(c - 1)); });
$next.click(function () { render(get(c + 1)); });
$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 ([37, 40].indexOf(e.which) >= 0) { p(); return $prev.click(); } // Left
if ([38, 39].indexOf(e.which) >= 0) { p(); return $next.click(); } // Right
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(); }
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 () {
$(share).click(function () {
var block = get(c, true);
common.getSframeChannel().event('EV_SHARE_OPEN', {
versionHash: block.serverHash,
@ -355,17 +613,19 @@ define([
});
// Close & restore buttons
$close.click(function () {
$(close).click(function () {
states = [];
close();
onClose();
closeUI();
});
$rev.click(function () {
$(restore).click(function () {
UI.confirm(Messages.history_restorePrompt, function (yes) {
if (!yes) { return; }
close();
onRevert();
UI.log(Messages.history_restoreDone);
var done = onRevert();
if (done) {
closeUI();
UI.log(Messages.history_restoreDone);
}
});
});
@ -384,7 +644,6 @@ define([
History.loading = false;
if (err) { throw new Error(err); }
update(newRt);
c = states.length - 1;
display();
if (isFull) {
$loadMore.off('click').hide();

@ -1454,6 +1454,26 @@ define([
});
});
sframeChan.on('Q_GET_LAST_HASH', function (data, cb) {
Cryptpad.padRpc.getLastHash({
channel: secret.channel
}, cb);
});
sframeChan.on('Q_GET_SNAPSHOT', function (data, cb) {
var crypto = Crypto.createEncryptor(secret.keys);
Cryptpad.padRpc.getSnapshot({
channel: secret.channel,
hash: data.hash
}, function (obj) {
if (obj && obj.error) { return void cb(obj); }
var messages = obj.messages || [];
messages.forEach(function (patch) {
patch.msg = crypto.decrypt(patch.msg, true, true);
});
cb(messages);
});
});
if (cfg.messaging) {
Notifier.getPermission();

@ -33,6 +33,7 @@ MessengerUI, Messages) {
var FILE_CLS = Bar.constants.file = 'cp-toolbar-file';
var DRAWER_CLS = Bar.constants.drawer = 'cp-toolbar-drawer-content';
var HISTORY_CLS = Bar.constants.history = 'cp-toolbar-history';
var SNAPSHOTS_CLS = Bar.constants.history = 'cp-toolbar-snapshots';
// Userlist
var USERLIST_CLS = Bar.constants.userlist = "cp-toolbar-users";
@ -87,6 +88,7 @@ MessengerUI, Messages) {
h('div.'+BOTTOM_RIGHT_CLS)
])).appendTo($toolbar);
$toolbar.append(h('div.'+HISTORY_CLS));
$toolbar.append(h('div.'+SNAPSHOTS_CLS));
var $file = $toolbar.find('.'+BOTTOM_LEFT_CLS);
@ -659,6 +661,8 @@ MessengerUI, Messages) {
.text('('+Messages.readonly+')'));
return $titleContainer;
}
$hoverable.append($('<span>', {'class': 'cp-toolbar-title-readonly cp-toolbar-title-unsync'})
.text('('+Messages.readonly+')'));
var $input = $('<input>', {
type: 'text',
placeholder: placeholder
@ -687,6 +691,7 @@ MessengerUI, Messages) {
return true;
});
var save = function () {
if (toolbar.history) { return; }
var name = $input.val().trim();
if (name === "") {
name = $input.attr('placeholder');
@ -717,6 +722,7 @@ MessengerUI, Messages) {
var displayInput = function () {
if (toolbar.connected === false) { return; }
if (toolbar.history) { return; }
$input.width(Math.max(($text.width() + 10), 300)+'px');
$text.hide();
//$pencilIcon.css('display', 'none');
@ -1273,12 +1279,14 @@ MessengerUI, Messages) {
//checkLag(toolbar, config);
};
toolbar.initializing = function (/*userId*/) {
if (toolbar.history) { return; }
toolbar.connected = false;
if (toolbar.spinner) {
toolbar.spinner.text(Messages.initializing);
}
};
toolbar.reconnecting = function (/*userId*/) {
if (toolbar.history) { return; }
toolbar.connected = false;
if (toolbar.spinner) {
var state = -1;
@ -1342,6 +1350,25 @@ MessengerUI, Messages) {
}
};
toolbar.setSnapshot = function (bool) {
toolbar.history = bool;
toolbar.title.toggleClass('cp-toolbar-unsync', bool);
if (bool && toolbar.spinner) {
toolbar.spinner.text("SNAPSHOT"); // XXX
} else {
kickSpinner(toolbar, config);
}
};
toolbar.setHistory = function (bool) {
toolbar.history = bool;
toolbar.title.toggleClass('cp-toolbar-unsync', bool);
if (bool && toolbar.spinner) {
toolbar.spinner.text("HISTORY"); // XXX
} else {
kickSpinner(toolbar, config);
}
};
// On log out, remove permanently the realtime elements of the toolbar
Common.onLogout(function () {
failed();

@ -587,6 +587,10 @@ define([
var setHistory = function (bool, update) {
history = bool;
if (!bool && update) { config.onRemote(); }
else {
setTimeout(cpNfInner.metadataMgr.refresh);
}
return true;
};
var displayDoc = function (doc) {
@ -594,13 +598,51 @@ define([
console.log(doc);
};
var toRestore;
// Get the realtime metadata when in history mode
var getLastMetadata = function () {
var newContentStr = cpNfInner.chainpad.getUserDoc();
var newContent = JSON.parse(newContentStr);
var meta = extractMetadata(newContent);
return meta;
};
var setLastMetadata = function (md) {
var newContentStr = cpNfInner.chainpad.getAuthDoc();
var newContent = JSON.parse(newContentStr);
if (Array.isArray(newContent)) {
newContent[3] = {
metadata: md
};
} else {
newContent.metadata = md;
}
try {
cpNfInner.chainpad.contentUpdate(JSONSortify(newContent));
return true;
} catch (e) {
console.error(e);
return false;
}
};
var toRestore;
config.onLocal = function (a, restore) {
if (!toRestore || !restore) { return; }
cpNfInner.chainpad.contentUpdate(toRestore);
};
var extractMetadata = function (content) {
if (Array.isArray(content)) {
var m = content[content.length - 1];
if (typeof(m.metadata) === 'object') {
// pad
return m.metadata;
}
} else if (typeof(content.metadata) === 'object') {
return content.metadata;
}
return;
};
config.onInit = function (info) {
Title = common.createTitle({});
@ -620,12 +662,35 @@ define([
/* add a history button */
var histConfig = {
onLocal: function () {
// The following lines allow us to restore an old version from the debug app
// without affecting the snapshots.
// It's parsing, updating and stringifying text data which is not a clean way
// to change metadata, so we're disabling it by default.
if (window.cp_snapshots) {
var md = Util.clone(cpNfInner.metadataMgr.getMetadata());
var _snapshots = md.snapshots;
var newContent = JSON.parse(toRestore);
try {
if (Array.isArray(newContent)) {
newContent[3].metadata.snapshots = _snapshots;
} else {
newContent.metadata.snapshots = _snapshots;
}
} catch (e) { console.error(e); }
toRestore = JSONSortify(newContent);
}
config.onLocal(null, true);
},
onRemote: config.onRemote,
setHistory: setHistory,
extractMetadata: extractMetadata,
getLastMetadata: getLastMetadata, // get from authdoc
setLastMetadata: setLastMetadata, // set to userdoc/authdoc
applyVal: function (val) {
toRestore = val;
var newContent = JSON.parse(val);
var meta = extractMetadata(newContent);
cpNfInner.metadataMgr.updateMetadata(meta);
displayDoc(JSON.parse(val) || {});
},
$toolbar: $bar,

@ -128,6 +128,7 @@ define([
if (!bool && update) {
history.onLeaveHistory();
}
return true;
};
var main = function () {
@ -251,6 +252,7 @@ define([
history.currentObj = obj;
history.onEnterHistory(obj);
},
drive: true,
$toolbar: APP.$bar,
};

Loading…
Cancel
Save