Implement snapshots

pull/1/head
yflory 4 years ago
parent 57d18e9a9a
commit c8f16d427d

@ -886,11 +886,19 @@
} }
} }
.cp-toolbar-history { .cp-toolbar-history, .cp-toolbar-snapshots {
background-color: @toolbar-bg-color-light; background-color: @toolbar-bg-color-light;
background-color: var(--toolbar-bg-color-light); background-color: var(--toolbar-bg-color-light);
color: @cryptpad_text_col; 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 { .cp-toolbar-bottom {
background-color: @toolbar-bg-color-light; background-color: @toolbar-bg-color-light;
background-color: var(--toolbar-bg-color-light); background-color: var(--toolbar-bg-color-light);

@ -876,6 +876,21 @@ define([
common.createNewPadModal(); common.createNewPadModal();
}); });
break; 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: default:
data = data || {}; data = data || {};
var drawerCls = data.drawer === false ? '' : '.cp-toolbar-drawer-element'; var drawerCls = data.drawer === false ? '' : '.cp-toolbar-drawer-element';
@ -3301,5 +3316,67 @@ define([
return (pos.bottom < size) && (pos.y > 0); 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; return UIElements;
}); });

@ -924,6 +924,12 @@ define([
// -1 ==> no timeout, we may receive the callback only when we reconnect // -1 ==> no timeout, we may receive the callback only when we reconnect
postMessage("SEND_PAD_MSG", data, cb, { timeout: -1 }); 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.onReadyEvent = Util.mkEvent();
pad.onMessageEvent = Util.mkEvent(); pad.onMessageEvent = Util.mkEvent();
pad.onJoinEvent = Util.mkEvent(); pad.onJoinEvent = Util.mkEvent();

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

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

@ -13,6 +13,7 @@ define([
'/common/common-ui-elements.js', '/common/common-ui-elements.js',
'/common/common-thumbnail.js', '/common/common-thumbnail.js',
'/common/common-feedback.js', '/common/common-feedback.js',
'/common/inner/snapshots.js',
'/customize/application_config.js', '/customize/application_config.js',
'/bower_components/chainpad/chainpad.dist.js', '/bower_components/chainpad/chainpad.dist.js',
'/common/test.js', '/common/test.js',
@ -35,6 +36,7 @@ define([
UIElements, UIElements,
Thumb, Thumb,
Feedback, Feedback,
Snapshots,
AppConfig, AppConfig,
ChainPad, ChainPad,
Test) Test)
@ -43,6 +45,9 @@ define([
var UNINITIALIZED = 'UNINITIALIZED'; var UNINITIALIZED = 'UNINITIALIZED';
// History and snapshots mode shouldn't receive realtime data or push to chainpad
var unsyncMode = false;
var STATE = Object.freeze({ var STATE = Object.freeze({
DISCONNECTED: 'DISCONNECTED', DISCONNECTED: 'DISCONNECTED',
FORGOTTEN: 'FORGOTTEN', FORGOTTEN: 'FORGOTTEN',
@ -50,7 +55,6 @@ define([
INFINITE_SPINNER: 'INFINITE_SPINNER', INFINITE_SPINNER: 'INFINITE_SPINNER',
ERROR: 'ERROR', ERROR: 'ERROR',
INITIALIZING: 'INITIALIZING', INITIALIZING: 'INITIALIZING',
HISTORY_MODE: 'HISTORY_MODE',
READY: 'READY' READY: 'READY'
}); });
@ -93,6 +97,7 @@ define([
}); });
}); });
var onLocal;
var textContentGetter; var textContentGetter;
var titleRecommender = function () { return false; }; var titleRecommender = function () { return false; };
var contentGetter = function () { return UNINITIALIZED; }; var contentGetter = function () { return UNINITIALIZED; };
@ -125,8 +130,27 @@ define([
return; 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 stateChange = function (newState, text) {
var wasEditable = (state === STATE.READY); var wasEditable = (state === STATE.READY && !unsyncMode);
if (newState !== state) {
if (state === STATE.DELETED || state === STATE.ERROR) { return; } if (state === STATE.DELETED || state === STATE.ERROR) { return; }
if (state === STATE.INFINITE_SPINNER && newState !== STATE.READY) { return; } if (state === STATE.INFINITE_SPINNER && newState !== STATE.READY) { return; }
if (newState === STATE.INFINITE_SPINNER || newState === STATE.DELETED) { if (newState === STATE.INFINITE_SPINNER || newState === STATE.DELETED) {
@ -135,11 +159,10 @@ define([
state = newState; state = newState;
} else if (state === STATE.DISCONNECTED && newState !== STATE.INITIALIZING) { } 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 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 { } else {
state = newState; state = newState;
} }
}
switch (state) { switch (state) {
case STATE.DISCONNECTED: case STATE.DISCONNECTED:
case STATE.INITIALIZING: { case STATE.INITIALIZING: {
@ -187,8 +210,9 @@ define([
} }
default: default:
} }
if (wasEditable !== (state === STATE.READY)) { var isEditable = (state === STATE.READY && !unsyncMode);
evEditableStateChange.fire(state === STATE.READY); if (wasEditable !== isEditable) {
evEditableStateChange.fire(isEditable);
} }
}; };
@ -205,8 +229,8 @@ define([
} }
}; };
var onLocal;
var onRemote = function () { var onRemote = function () {
if (unsyncMode) { return; }
if (state !== STATE.READY) { return; } if (state !== STATE.READY) { return; }
var oldContent = normalize(contentGetter()); var oldContent = normalize(contentGetter());
@ -261,15 +285,47 @@ define([
}); });
}; };
var setUnsyncMode = function (bool) {
if (unsyncMode === bool) { return; }
unsyncMode = bool;
evEditableStateChange.fire(state === STATE.READY && !unsyncMode);
stateChange(state);
};
var setHistoryMode = function (bool, update) { var setHistoryMode = function (bool, update) {
cpNfInner.metadataMgr.setHistory(bool); cpNfInner.metadataMgr.setHistory(bool);
toolbar.setHistory(bool); toolbar.setHistory(bool);
stateChange((bool) ? STATE.HISTORY_MODE : STATE.READY); setUnsyncMode(bool);
if (!bool && update) { onRemote(); } if (!bool && update) { onRemote(); }
else { else {
setTimeout(cpNfInner.metadataMgr.refresh); setTimeout(cpNfInner.metadataMgr.refresh);
} }
}; };
var closeSnapshot = function (restore) {
setUnsyncMode(false);
if (restore) {
onLocal();
}
onRemote();
};
var loadSnapshot = function (hash, data) {
setUnsyncMode(true);
Snapshots.create(common, {
$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;
});
},
});
};
/* /*
var hasChanged = function (content) { var hasChanged = function (content) {
@ -286,6 +342,7 @@ define([
*/ */
onLocal = function (/*padChange*/) { onLocal = function (/*padChange*/) {
if (unsyncMode) { return; }
if (state !== STATE.READY) { return; } if (state !== STATE.READY) { return; }
if (readOnly) { return; } if (readOnly) { return; }
@ -359,7 +416,7 @@ define([
} }
cpNfInner.metadataMgr.updateMetadata(metadata); cpNfInner.metadataMgr.updateMetadata(metadata);
newContent = normalize(newContent); newContent = normalize(newContent);
if (state !== STATE.HISTORY_MODE) { if (!unsyncMode) {
contentUpdate(newContent, waitFor); contentUpdate(newContent, waitFor);
} }
} else { } else {
@ -379,7 +436,7 @@ define([
evOnDefaultContentNeeded.fire(); evOnDefaultContentNeeded.fire();
} }
}).nThen(function () { }).nThen(function () {
if (state !== STATE.HISTORY_MODE) { if (!unsyncMode) {
stateChange(STATE.READY); stateChange(STATE.READY);
} }
firstConnection = false; firstConnection = false;
@ -419,7 +476,6 @@ define([
}); });
}; };
var onConnectionChange = function (info) { var onConnectionChange = function (info) {
if (state === STATE.HISTORY_MODE) { return; }
if (state === STATE.DELETED) { return; } if (state === STATE.DELETED) { return; }
stateChange(info.state ? STATE.INITIALIZING : STATE.DISCONNECTED, info.permanent); stateChange(info.state ? STATE.INITIALIZING : STATE.DISCONNECTED, info.permanent);
/*if (info.state) { /*if (info.state) {
@ -716,6 +772,12 @@ define([
$hist.addClass('cp-hidden-if-readonly'); $hist.addClass('cp-hidden-if-readonly');
toolbar.$drawer.append($hist); toolbar.$drawer.append($hist);
var $snapshot = common.createButton('snapshots', true, {
make: makeSnapshot,
load: loadSnapshot
});
toolbar.$drawer.append($snapshot);
var $copy = common.createButton('copy', true); var $copy = common.createButton('copy', true);
toolbar.$drawer.append($copy); toolbar.$drawer.append($copy);

@ -1453,6 +1453,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) { if (cfg.messaging) {
Notifier.getPermission(); Notifier.getPermission();

@ -33,6 +33,7 @@ MessengerUI, Messages) {
var FILE_CLS = Bar.constants.file = 'cp-toolbar-file'; var FILE_CLS = Bar.constants.file = 'cp-toolbar-file';
var DRAWER_CLS = Bar.constants.drawer = 'cp-toolbar-drawer-content'; var DRAWER_CLS = Bar.constants.drawer = 'cp-toolbar-drawer-content';
var HISTORY_CLS = Bar.constants.history = 'cp-toolbar-history'; var HISTORY_CLS = Bar.constants.history = 'cp-toolbar-history';
var SNAPSHOTS_CLS = Bar.constants.history = 'cp-toolbar-snapshots';
// Userlist // Userlist
var USERLIST_CLS = Bar.constants.userlist = "cp-toolbar-users"; var USERLIST_CLS = Bar.constants.userlist = "cp-toolbar-users";
@ -87,6 +88,7 @@ MessengerUI, Messages) {
h('div.'+BOTTOM_RIGHT_CLS) h('div.'+BOTTOM_RIGHT_CLS)
])).appendTo($toolbar); ])).appendTo($toolbar);
$toolbar.append(h('div.'+HISTORY_CLS)); $toolbar.append(h('div.'+HISTORY_CLS));
$toolbar.append(h('div.'+SNAPSHOTS_CLS));
var $file = $toolbar.find('.'+BOTTOM_LEFT_CLS); var $file = $toolbar.find('.'+BOTTOM_LEFT_CLS);

Loading…
Cancel
Save