mirror of https://github.com/xwiki-labs/cryptpad
Implement snapshots
This commit is contained in:
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: 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();
|
||||
|
|
|
@ -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,38 @@ 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;
|
||||
}
|
||||
}
|
||||
switch (state) {
|
||||
case STATE.DISCONNECTED:
|
||||
|
@ -187,8 +210,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 +229,8 @@ define([
|
|||
}
|
||||
};
|
||||
|
||||
var onLocal;
|
||||
var onRemote = function () {
|
||||
if (unsyncMode) { return; }
|
||||
if (state !== STATE.READY) { return; }
|
||||
|
||||
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) {
|
||||
cpNfInner.metadataMgr.setHistory(bool);
|
||||
toolbar.setHistory(bool);
|
||||
stateChange((bool) ? STATE.HISTORY_MODE : STATE.READY);
|
||||
setUnsyncMode(bool);
|
||||
if (!bool && update) { onRemote(); }
|
||||
else {
|
||||
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) {
|
||||
|
@ -286,6 +342,7 @@ define([
|
|||
*/
|
||||
|
||||
onLocal = function (/*padChange*/) {
|
||||
if (unsyncMode) { return; }
|
||||
if (state !== STATE.READY) { return; }
|
||||
if (readOnly) { return; }
|
||||
|
||||
|
@ -359,7 +416,7 @@ define([
|
|||
}
|
||||
cpNfInner.metadataMgr.updateMetadata(metadata);
|
||||
newContent = normalize(newContent);
|
||||
if (state !== STATE.HISTORY_MODE) {
|
||||
if (!unsyncMode) {
|
||||
contentUpdate(newContent, waitFor);
|
||||
}
|
||||
} else {
|
||||
|
@ -379,7 +436,7 @@ define([
|
|||
evOnDefaultContentNeeded.fire();
|
||||
}
|
||||
}).nThen(function () {
|
||||
if (state !== STATE.HISTORY_MODE) {
|
||||
if (!unsyncMode) {
|
||||
stateChange(STATE.READY);
|
||||
}
|
||||
firstConnection = false;
|
||||
|
@ -419,7 +476,6 @@ define([
|
|||
});
|
||||
};
|
||||
var onConnectionChange = function (info) {
|
||||
if (state === STATE.HISTORY_MODE) { return; }
|
||||
if (state === STATE.DELETED) { return; }
|
||||
stateChange(info.state ? STATE.INITIALIZING : STATE.DISCONNECTED, info.permanent);
|
||||
/*if (info.state) {
|
||||
|
@ -716,6 +772,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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue