Merge branch 'history' into staging

pull/1/head
yflory 4 years ago
commit 08ba54b5cb

@ -165,7 +165,7 @@
margin-bottom: @alertify_padding-base;
margin: 0;
overflow: auto;
:last-child {
&:last-child {
margin-bottom: 0;
}
}
@ -197,7 +197,7 @@
background-color: @alertify-light-bg;
}
&.disabled {
color: #949494;
color: @colortheme_alertify-cancel-border;
cursor: not-allowed;
}
&:not(.alertify-tabs-active) {

@ -98,6 +98,7 @@
text-decoration: none;
cursor: pointer;
border-radius: 0;
transition: none;
.fa, .cptools {
margin-right: 0.2em;
@ -160,6 +161,15 @@
}
}
&.btn-light {
border-color: @cryptpad_text_col;
color: @cryptpad_text_col;
background-color: transparent;
&:hover, &:hover, &:focus {
background-color: fade(@cryptpad_text_col, 25%);
}
}
&.cancel, &.btn-cancel {
border-color: @colortheme_alertify-cancel-border;
color: @colortheme_alertify-cancel-border;

@ -141,6 +141,69 @@
margin-left: 10px;
}
.cp-snapshots-modal {
& > input:last-child {
margin-bottom: 0 !important;
}
}
.cp-snapshots-container {
@snapshot_spacing: 10px;
display: flex;
flex-flow: column;
color: @cryptpad_text_col;
margin-bottom: @snapshot_spacing;
max-height: 245px;
overflow: auto;
outline: none;
.cp-snapshot-spinner {
min-height: 90px;
text-align: center;
}
.cp-snapshot-element {
display: flex;
align-items: center;
padding: 5px 0;
outline: none;
& > i {
margin-left: @snapshot_spacing;
text-align: center;
}
.cp-snapshot-title {
margin-left: @snapshot_spacing;
display: flex;
flex-flow: column;
flex: 1;
min-width: 0;
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cp-snapshot-time {
font-size: 12px;
}
}
.cp-snapshot-buttons {
margin-left: @snapshot_spacing;
display: none;
align-items: flex-start;
margin-bottom: -3px;
.cp-button-confirm {
margin-right: @snapshot_spacing;
}
button {
margin-right: @snapshot_spacing;
}
}
&:hover, &:focus, &:focus-within {
.cp-snapshot-buttons {
display: flex;
}
background-color: #DDD;
}
}
}
// mediatag preview
#cp-mediatag-preview-modal {
.cp-modal {

@ -5,92 +5,256 @@
}
& {
.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-history-filler {
flex: 1;
@media screen and (max-width: 870px) {
flex-flow: column;
.cp-toolbar-history-actions {
width: 100%;
.cp-history-actions-first {
margin-right: 0 !important;
}
}
.cp-toolbar-history-timeline {
width: ~"calc(100% - 20px)";
margin-right: 10px !important;
}
}
.cp-toolbar-history-close,
.cp-toolbar-history-revert {
background: white;
color: black;
//margin-top: 5px;
&:hover {
background-color: #e6e6e6;
@media screen and (max-height: 500px) {
padding-top: 0px;
.cp-history-timeline-line {
display: none !important;
}
.cp-toolbar-history-timeline {
width: 100% !important;
margin: 0 !important;
}
.cp-history-timeline-actions {
margin-left: 0 !important;
}
}
.cp-toolbar-history-loadmore {
height: 100%;
color: black;
width: 25px;
position: absolute;
left: 0;
&.cp-history-init {
padding: 0;
height: 32px;
}
.cp-toolbar-history-version {
position: absolute;
height: 25px;
line-height: 25px;
width: 100%;
text-align: center;
color: black;
}
.cp-toolbar-history-goto {
display: inline-block;
vertical-align: middle;
text-align: center;
.cp-toolbar-history-timeline {
display: flex;
flex-flow: column;
flex: 1;
flex-basis: 80%;
min-width: 0;
max-width: 600px;
input { width: 75px; }
margin-left: 10px;
margin-right: @fill-width;
.cp-history-timeline-time {
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
text-align: center;
}
}
.cp-toolbar-history-goto-input {
padding-left: 5px;
margin-left: 5px;
vertical-align: middle;
.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;
display: inline-flex;
align-items: center;
.fa:not(:last-child) {
margin-right: 5px;
}
&:disabled {
cursor: not-allowed !important;
opacity: 0.6;
&:hover, &:active {
background-color: transparent;
}
}
}
}
.cp-toolbar-history-bar {
width: 100%;
background: white;
height: 25px;
margin: auto;
position: relative;
&.cp-history-drive {
.cp-history-timeline-container {
height: 20px !important;
}
.cp-history-timeline-users {
display: none !important;
}
.cp-history-timeline-legend {
display: none !important;
}
.cp-history-timeline-pos {
height: 18px !important;
}
.cp-toolbar-history-loadmore {
font-size: 10px !important;
}
.cp-history-timeline-actions {
margin-left: 21px !important;
}
}
.cp-toolbar-history-pos-container {
width: ~"calc(100% - 2px)";
height: 25px;
position: relative;
&.cp-smallpatch {
.cp-history-snapshot {
border: none !important;
width: 2px !important;
background: @pos-color;
}
.cp-history-timeline-pos {
border-right: none;
border-left: none;
width: 2px !important;
background: @pos-color;
&:before {
left: -6px;
}
}
}
@pos-color: #55FF55;
.cp-toolbar-history-pos {
width: 2px;
height: 25px;
background: @pos-color;
&:after {
content: '';
border: 6px solid transparent;
border-top-color: @pos-color;
margin-left: -5px;
.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;
cursor: pointer;
.cp-history-snapshot {
position: absolute;
border: 2px solid @cryptpad_text_col;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
button {
color: @cryptpad_text_col;
background-color: rgba(0,0,0,0.2);
&:hover {
background-color: rgba(0,0,0,0.4);
.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;
}
&:disabled {
cursor: not-allowed !important;
opacity: 0.6;
&:hover, &:active {
background-color: transparent;
}
}
}
.cp-history-timeline-next {
button:last-child {
margin-right: 0;
}
}
.cp-history-timeline-prev {
button:first-child {
margin-left: 0;
}
}
}
.fa-spinner {
font-size: 66px;
.cp-history-timeline-pos {
//width: 2px;
border: 2px solid @cryptpad_text_col;
height: 37px;
//background: @pos-color;
position: absolute;
&:before {
top: -17px;
font-size: 24px;
position: absolute;
left: ~"calc(50% - 6px)";
}
}
}
}

@ -483,6 +483,7 @@
height: @toolbar_line-height;
box-sizing: border-box;
line-height: @toolbar_line-height;
border: 1px solid transparent;
}
.cp-toolbar-title-readonly {
font-size: 14px;
@ -490,7 +491,7 @@
.cp-toolbar-title-value {
padding: 5px;
line-height: @toolbar_line-height - 10px;
border: 0;
//border: 0;
}
.cp-toolbar-title-edit, .cp-toolbar-title-save {
box-sizing: border-box;
@ -663,6 +664,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 +901,45 @@
}
}
.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: 5px 0;
align-items: center;
justify-content: space-between;
.cp-toolbar-snapshots-info {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
i {
width: 30px;
}
}
@media screen and (max-width: @browser_media-medium-screen) {
flex-flow: column;
.cp-toolbar-snapshots-info {
max-width: 100%;
}
}
.cp-toolbar-snapshots-actions {
button {
margin: 0 5px;
border: 1px solid @cryptpad_text_col;
text-transform: uppercase;
i:not(:last-child) {
margin-right: 5px;
}
}
}
}
.cp-toolbar-bottom {
background-color: @toolbar-bg-color-light;
background-color: var(--toolbar-bg-color-light);

@ -128,7 +128,6 @@ define([
};
var mkHelpMenu = function (framework) {
var $codeMirrorContainer = $('#cp-app-code-container');
$codeMirrorContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning());
var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'code']);
$codeMirrorContainer.prepend(helpMenu.menu);

@ -169,6 +169,17 @@ Version 1
/code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI
*/
var getVersionHash = function (hashArr) {
var k;
// Check if we have a ownerKey for this pad
hashArr.some(function (data) {
if (/^hash=/.test(data)) {
k = data.slice(5);
return true;
}
});
return k ? Crypto.b64AddSlashes(k) : '';
};
var getOwnerKey = function (hashArr) {
var k;
// Check if we have a ownerKey for this pad
@ -190,6 +201,7 @@ Version 1
parsed.password = options.indexOf('p') !== -1;
parsed.present = options.indexOf('present') !== -1;
parsed.embed = options.indexOf('embed') !== -1;
parsed.versionHash = getVersionHash(options);
parsed.ownerKey = getOwnerKey(options);
};
@ -201,6 +213,7 @@ Version 1
embed: parsed.embed,
present: parsed.present,
ownerKey: parsed.ownerKey,
versionHash: parsed.versionHash,
password: parsed.password
};
};
@ -220,6 +233,10 @@ Version 1
if (parsed.password || opts.password) { hash += 'p/'; }
if (opts.embed) { hash += 'embed/'; }
if (opts.present) { hash += 'present/'; }
var versionHash = typeof(opts.versionHash) !== "undefined" ? opts.versionHash : parsed.versionHash;
if (versionHash) {
hash += 'hash=' + Crypto.b64RemoveSlashes(versionHash) + '/';
}
return hash;
};

@ -758,23 +758,16 @@ define([
button = $('<span>');
break;
}
var active = $(".cp-toolbar-history:visible").length !== 0;
button = $('<button>', {
title: active ? Messages.history_closeTitle : Messages.historyButton,
title: Messages.historyButton,
'class': "fa fa-history cp-toolbar-icon-history",
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.historyText));
button.toggleClass("active", active);
if (data.histConfig) {
if (active) {
button.click(function () { $(".cp-toolbar-history-close").trigger("click"); });
}
else {
button
.click(common.prepareFeedback(type))
.on('click', function () {
common.getHistory(data.histConfig);
});
}
button
.click(common.prepareFeedback(type))
.on('click', function () {
common.getHistory(data.histConfig);
});
}
break;
case 'mediatag':
@ -876,6 +869,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, data.remove);
});
break;
default:
data = data || {};
var drawerCls = data.drawer === false ? '' : '.cp-toolbar-drawer-element';
@ -3301,5 +3309,117 @@ define([
return (pos.bottom < size) && (pos.y > 0);
};
UIElements.openSnapshotsModal = function (common, load, make, remove) {
var modal;
var readOnly = common.getMetadataMgr().getPrivateData().readOnly;
var container = h('div.cp-snapshots-container', {tabindex:1});
var $container = $(container);
var input = h('input', {
tabindex: 1,
placeholder: Messages.snapshots_placeholder
});
var $input = $(input);
var content = h('div.cp-snapshots-modal', [
h('h5', Messages.snapshots_button),
container,
readOnly ? undefined : h('label', Messages.snapshots_new),
readOnly ? undefined : input
]);
var refresh = function () {
var metadataMgr = common.getMetadataMgr();
var md = metadataMgr.getMetadata();
var snapshots = md.snapshots || {};
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 openButton = h('button.cp-snapshot-view.btn.btn-light', {
tabindex: 1,
}, [
h('i.fa.fa-eye'),
h('span', Messages.snapshots_open)
]);
$(openButton).click(function () {
load(hash, s);
if (modal && modal.closeModal) {
modal.closeModal();
}
});
var deleteButton = h('button.cp-snapshot-delete.btn.btn-light', {
tabindex: 1,
}, [
h('i.fa.fa-trash'),
h('span', Messages.snapshots_delete)
]);
UI.confirmButton(deleteButton, {
classes: 'btn-danger'
}, function () {
remove(hash, s);
refresh();
});
return h('span.cp-snapshot-element', {tabindex:1}, [
h('i.fa.fa-camera'),
h('span.cp-snapshot-title', [
h('span', s.title),
h('span.cp-snapshot-time', new Date(s.time).toLocaleString())
]),
h('span.cp-snapshot-buttons', [
readOnly ? undefined : deleteButton,
openButton,
])
]);
});
$container.html('').append(list);
setTimeout(function () {
if (list.length) { return void $container.focus(); }
$input.focus();
});
};
refresh();
var buttons = [{
className: 'cancel',
name: Messages.filePicker_close,
onClick: function () {},
keys: [27],
}];
if (!readOnly) {
buttons.push({
className: 'primary',
iconClass: '.fa.fa-camera',
name: Messages.snapshots_new,
onClick: function () {
var val = $input.val();
if (!val) { return true; }
$container.html('').append(h('div.cp-snapshot-spinner'));
var to = setTimeout(function () {
UI.spinner($container.find('div')).get().show();
});
make(val, function (err) {
clearTimeout(to);
if (err) {
return void UI.alert(Messages.snapshots_cantMake);
}
refresh();
});
return true;
},
keys: [],
});
}
modal = UI.openCustomModal(UI.dialog.customModal(content, {buttons: buttons }));
};
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();

@ -329,26 +329,29 @@ define([
}
// warning about sharing links
var localStore = window.cryptpadStore;
var dismissButton = h('span.fa.fa-times');
var shareLinkWarning = h('div.alert.alert-warning.dismissable',
{ style: 'display: none;' },
[
h('span.cp-inline-alert-text', Messages.share_linkWarning),
dismissButton
]);
linkContent.push(shareLinkWarning);
localStore.get('hide-alert-shareLinkWarning', function (val) {
if (val === '1') { return; }
$(shareLinkWarning).css('display', 'flex');
$(dismissButton).on('click', function () {
localStore.put('hide-alert-shareLinkWarning', '1');
$(shareLinkWarning).remove();
// when sharing a version hash, there is a similar warning and we want
// to avoid alert fatigue
if (!opts.versionHash) {
var localStore = window.cryptpadStore;
var dismissButton = h('span.fa.fa-times');
var shareLinkWarning = h('div.alert.alert-warning.dismissable',
{ style: 'display: none;' },
[
h('span.cp-inline-alert-text', Messages.share_linkWarning),
dismissButton
]);
linkContent.push(shareLinkWarning);
localStore.get('hide-alert-shareLinkWarning', function (val) {
if (val === '1') { return; }
$(shareLinkWarning).show();
$(dismissButton).on('click', function () {
localStore.put('hide-alert-shareLinkWarning', '1');
$(shareLinkWarning).remove();
});
});
});
}
// Burn after reading
if (opts.barAlert) { linkContent.push(opts.barAlert.cloneNode(true)); }
@ -362,9 +365,6 @@ define([
}));
});
//Messages.share_bar = "Generate link"; // XXX XXX XXX XXX XXX XXX XXX XXX XXX XXX XXX
Messages.share_bar = Messages.team_inviteLinkCreate; // XXX
var linkButtons = [
makeCancelButton(),
!opts.sharedFolder && {
@ -462,7 +462,8 @@ define([
var pathname = opts.pathname;
var parsed = Hash.parsePadUrl(pathname);
var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1;
var canBAR = parsed.type !== 'drive';
var versionHash = hashes.viewHash && opts.versionHash;
var canBAR = parsed.type !== 'drive' && !versionHash;
var burnAfterReading = (hashes.viewHash && canBAR) ?
UI.createRadio('accessRights', 'cp-share-bar', Messages.burnAfterReading_linkBurnAfterReading, false, {
@ -521,6 +522,12 @@ define([
var embed = val.embed;
var present = val.present !== undefined ? val.present : Util.isChecked($rights.find('#cp-share-present'));
var burnAfterReading = Util.isChecked($rights.find('#cp-share-bar'));
if (versionHash) {
edit = false;
embed = false;
present = false;
burnAfterReading = false;
}
if (burnAfterReading && !opts.burnAfterReadingUrl) {
if (cb) { // Called from the contacts tab, "share" button
var barHref = origin + pathname + '#' + (hashes.viewHash || hashes.editHash);
@ -535,7 +542,7 @@ define([
var href = burnAfterReading ? opts.burnAfterReadingUrl
: (origin + pathname + '#' + hash);
var parsed = Hash.parsePadUrl(href);
return origin + parsed.getUrl({embed: embed, present: present});
return origin + parsed.getUrl({embed: embed, present: present, versionHash: versionHash});
};
opts.getEmbedValue = function () {
var url = opts.getLinkValue({
@ -545,7 +552,11 @@ define([
};
// disable edit share options if you don't have edit rights
if (!hashes.editHash) {
if (versionHash) {
$rights.find('#cp-share-editable-false').attr('checked', true);
$rights.find('#cp-share-present').removeAttr('checked').attr('disabled', true);
$rights.find('#cp-share-editable-true').removeAttr('checked').attr('disabled', true);
} else if (!hashes.editHash) {
$rights.find('#cp-share-editable-false').attr('checked', true);
$rights.find('#cp-share-editable-true').removeAttr('checked').attr('disabled', true);
} else if (!hashes.viewHash) {
@ -584,7 +595,9 @@ define([
// Set default values
common.getAttribute(['general', 'share'], function (err, val) {
val = val || {};
if (val.present && canPresent) {
if (versionHash) {
$rights.find('#cp-share-editable-false').prop('checked', true);
} else if (val.present && canPresent) {
$rights.find('#cp-share-editable-false').prop('checked', false);
$rights.find('#cp-share-editable-true').prop('checked', false);
$rights.find('#cp-share-present').prop('checked', true);
@ -673,9 +686,21 @@ define([
onHide: resetTab
}];
Modal.getModal(common, opts, tabs, function (err, modal) {
$(modal).find('.cp-bar').hide();
// Hide the burn-after-reading option by default
var $modal = $(modal);
$modal.find('.cp-bar').hide();
// Prepend the "rights" radio selection
$(modal).find('.alertify-tabs-titles').after($rights);
$modal.find('.alertify-tabs-titles').after($rights);
// Add the versionHash warning if needed
if (opts.versionHash) {
$rights.after(h('div.alert.alert-warning', [
h('i.fa.fa-history'),
UI.setHTML(h('span'), Messages.share_versionHash)
]));
}
// callback
cb(err, modal);
});

@ -0,0 +1,118 @@
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"); }
if (!obj.length) { return void console.error("Empty channel"); }
var checkLast = obj[obj.length - 1].serverHash === config.hash;
if (!checkLast) {
$snap.find('.cp-toolbar-snapshots-close').click();
return void UI.alert(Messages.snapshots_notFound);
}
var messages = obj;
var chainpad = createChainPad();
messages.forEach(function (m) {
chainpad.message(m);
});
snapshot = chainpad.getAuthDoc();
config.applyVal(snapshot);
chainpad.abort();
});
};
var display = function () {
var data = config.data || {};
var actions = h('span.cp-toolbar-snapshots-actions');
var $actions = $(actions);
var content = [
h('span.cp-toolbar-snapshots-info', [
h('i.fa.fa-camera'),
h('span.cp-toolbar-snapshots-title', data.title + ' - ' + new Date(data.time).toLocaleString()),
]),
actions
];
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($actions);
}
$(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($actions);
$snap.append(content);
};
display();
getData();
};
return Snapshots;
});

@ -14,6 +14,7 @@ define(['json.sortify'], function (Sortify) {
var metadataLazyObj = UNINIT;
var priv = {};
var dirty = true;
var history = false;
var changeHandlers = [];
var lazyChangeHandlers = [];
var titleChangeHandlers = [];
@ -60,7 +61,7 @@ define(['json.sortify'], function (Sortify) {
var mdo = {};
// We don't want to add our user data to the object multiple times.
Object.keys(metadataObj.users).forEach(function (x) {
if (members.indexOf(x) === -1) { return; }
if (members.indexOf(x) === -1 && !history) { return; }
mdo[x] = metadataObj.users[x];
});
if (!priv.readOnly) {
@ -161,6 +162,9 @@ define(['json.sortify'], function (Sortify) {
metadataLazyObj = JSON.parse(JSON.stringify(m));
change(false);
},
refresh : function () {
change(true);
},
updateTitle: function (t) {
metadataObj.title = t;
change(true);
@ -207,6 +211,9 @@ define(['json.sortify'], function (Sortify) {
if (isReady) { return void f(); }
readyHandlers.push(f);
},
setHistory: function (bool) {
history = bool;
}
});
};
return Object.freeze({ create: create });

@ -1455,7 +1455,57 @@ 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 validateKey;
var fakeNetflux = Hash.createChannelId();
nThen(function (waitFor) {
Store.getPadMetadata(null, {
channel: data.channel
}, waitFor(function (md) {
validateKey = md.validateKey;
}));
}).nThen(function () {
Store.getHistoryRange(clientId, {
cpCount: 1,
channel: data.channel,
lastKnownHash: data.versionHash
}, function (obj) {
if (obj && obj.error) {
postMessage(clientId, "PAD_ERROR", obj.error);
return;
}
postMessage(clientId, "PAD_CONNECT", {
myID: fakeNetflux,
id: data.channel,
members: [fakeNetflux]
});
(obj.messages || []).forEach(function (data) {
postMessage(clientId, "PAD_MESSAGE", {
msg: data.msg,
time: data.time,
user: fakeNetflux.slice(0,16), // fake history keeper to avoid validate
});
});
if (validateKey && store.messenger) {
store.messenger.storeValidateKey(data.channel, validateKey);
}
postMessage(clientId, "PAD_READY");
});
});
};
Store.joinPad = function (clientId, data) {
if (data.versionHash) {
return void getVersionHash(clientId, data);
}
var isNew = typeof channels[data.channel] === "undefined";
var channel = channels[data.channel] = channels[data.channel] || {
queue: [],
@ -1529,7 +1579,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,
@ -1615,6 +1666,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,
@ -1750,6 +1802,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) {
@ -2035,6 +2096,7 @@ define([
}
msg = msg.replace(/cp\|(([A-Za-z0-9+\/=]+)\|)?/, '');
msgs.push({
serverHash: msg.slice(0,64),
msg: msg,
author: parsed[2][1],
time: parsed[2][5]
@ -2045,7 +2107,7 @@ define([
network.on('message', onMsg);
network.sendto(hk, JSON.stringify(['GET_HISTORY_RANGE', data.channel, {
from: data.lastKnownHash,
cpCount: 2,
cpCount: data.cpCount || 2,
txid: txid
}]));
};

@ -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,51 @@ define([
return;
};
var deleteSnapshot = function (hash) {
var md = Util.clone(cpNfInner.metadataMgr.getMetadata());
var snapshots = md.snapshots = md.snapshots || {};
delete snapshots[hash];
cpNfInner.metadataMgr.updateMetadata(md);
onLocal();
};
var makeSnapshot = function (title, cb) {
if (state !== STATE.READY) {
return void cb('NOT_READY');
}
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) { cb('NO_HASH'); return void UI.warn(Messages.error); }
var md = Util.clone(cpNfInner.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: +new Date()
};
cpNfInner.metadataMgr.updateMetadata(md);
onLocal();
cpNfInner.chainpad.onSettle(cb);
});
};
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 +223,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 +242,8 @@ define([
}
};
var onLocal;
var onRemote = function () {
if (unsyncMode) { return; }
if (state !== STATE.READY) { return; }
var oldContent = normalize(contentGetter());
@ -261,9 +298,85 @@ 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) {
stateChange((bool) ? STATE.HISTORY_MODE : STATE.READY);
if (!bool && !update && state !== STATE.READY) { return false; }
cpNfInner.metadataMgr.setHistory(bool);
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; }
if (state !== STATE.READY) { 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;
}
};
/*
@ -281,6 +394,7 @@ define([
*/
onLocal = function (/*padChange*/) {
if (unsyncMode) { return; }
if (state !== STATE.READY) { return; }
if (readOnly) { return; }
@ -324,6 +438,35 @@ define([
window.dispatchEvent(evt);
};
var versionHashEl;
var onInit = function () {
UI.updateLoadingProgress({
state: 2,
progress: 0.1
}, false);
stateChange(STATE.INITIALIZING);
if ($('.cp-help-container').length) {
var privateDat = cpNfInner.metadataMgr.getPrivateData();
// Burn after reading warning
$('.cp-help-container').before(common.getBurnAfterReadingWarning());
// Versioned link warning
if (privateDat.isHistoryVersion) {
versionHashEl = h('div.alert.alert-warning.cp-burn-after-reading');
$('.cp-help-container').before(versionHashEl);
}
}
common.getSframeChannel().on('EV_VERSION_TIME', function (time) {
if (!versionHashEl) { return; }
var vTime = time;
var vTimeStr = vTime ? new Date(vTime).toLocaleString()
: 'v' + privateDat.isHistoryVersion;
var vTxt = Messages._getKey('infobar_versionHash',  [vTimeStr]);
versionHashEl.innerText = vTxt;
versionHashEl = undefined;
});
};
var onReady = function () {
var newContentStr = cpNfInner.chainpad.getUserDoc();
if (state === STATE.DELETED) { return; }
@ -341,6 +484,7 @@ define([
var privateDat = cpNfInner.metadataMgr.getPrivateData();
var type = privateDat.app;
// contentUpdate may be async so we need an nthen here
nThen(function (waitFor) {
if (!newPad) {
@ -354,7 +498,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
@ -590,13 +736,7 @@ define([
},
onRemote: onRemote,
onLocal: onLocal,
onInit: function () {
UI.updateLoadingProgress({
state: 2,
progress: 0.1
}, false);
stateChange(STATE.INITIALIZING);
},
onInit: onInit,
onReady: function () { evStart.reg(onReady); },
onConnectionChange: onConnectionChange,
onError: onError,
@ -691,17 +831,29 @@ 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) {
contentUpdate(JSON.parse(val) || ["BODY",{},[]], function (h) {
var newContent = JSON.parse(val);
var meta = extractMetadata(newContent);
cpNfInner.metadataMgr.updateMetadata(meta);
contentUpdate(normalize(newContent) || ["BODY",{},[]], function (h) {
return h;
});
},
$toolbar: $(toolbarContainer)
};
var $hist = common.createButton('history', true, {histConfig: histConfig});
$hist.addClass('cp-hidden-if-readonly');
toolbar.$drawer.append($hist);
var $snapshot = common.createButton('snapshots', true, {
remove: deleteSnapshot,
make: makeSnapshot,
load: loadSnapshot
});
toolbar.$drawer.append($snapshot);
var $copy = common.createButton('copy', true);
toolbar.$drawer.append($copy);
@ -767,7 +919,7 @@ define([
onEditableChange: evEditableStateChange.reg,
// Determine whether the UI should be locked for editing.
isLocked: function () { return state !== STATE.READY; },
isLocked: function () { return state !== STATE.READY || unsyncMode; },
// Determine whether the pad is a "read only" pad and cannot be changed.
isReadOnly: function () { return readOnly; },

@ -28,12 +28,19 @@ define([], function () {
var padRpc = conf.padRpc;
var sframeChan = conf.sframeChan;
var metadata= conf.metadata || {};
var versionHash = conf.versionHash;
var validateKey = metadata.validateKey;
var onConnect = conf.onConnect || function () { };
var lastTime; // Time of last patch (if versioned link);
conf = undefined;
if (versionHash) { readOnly = true; }
padRpc.onReadyEvent.reg(function () {
sframeChan.event('EV_RT_READY', null);
if (lastTime && versionHash) {
sframeChan.event('EV_VERSION_TIME', lastTime);
}
});
// shim between chainpad and netflux
@ -83,6 +90,7 @@ define([], function () {
}
var message = msgIn(msgObj.user, msgObj.msg);
if (!message) { return; }
lastTime = msgObj.time;
verbose(message);
@ -132,6 +140,7 @@ define([], function () {
padRpc.joinPad({
channel: channel || null,
readOnly: readOnly,
versionHash: versionHash,
metadata: metadata
});
};

@ -1,19 +1,26 @@
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, 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');
$hist.addClass('cp-history-init');
if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) {
throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory");
@ -54,6 +61,140 @@ 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')));
}
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;
@ -71,11 +212,18 @@ define([
lastKnownHash = data.lastKnownHash;
isComplete = data.isFull;
var messages = (data.messages || []).map(function (obj) {
if (!config.debug) {
return obj.msg;
}
return obj;
});
// We're supposed to receive 2 checkpoints. If the result is only ONE message
// and this message is a checkpoint, it means it's the last message of the history
// (and this is a trimmed history)
if (messages.length === 1) {
var parsed = JSON.parse(messages[0].msg);
if (parsed[0] === 4) {
isComplete = true;
}
}
if (config.debug) { console.log(data.messages); }
Array.prototype.unshift.apply(allMessages, messages); // Destructive concat
fillChainPad(realtime, allMessages);
@ -95,23 +243,38 @@ define([
console.error(e);
}
};
var onClose = function () { config.setHistory(false, true); };
var onClose = function () {
config.setHistory(false, true);
};
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 _users = lastMd.users;
var md = Util.clone(metadataMgr.getMetadata());
md.snapshots = _snapshots;
md.users = _users;
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 $hist = $toolbar.find('.cp-toolbar-history');
var $bottom = $toolbar.find('.cp-toolbar-bottom');
var $cke = $toolbar.find('.cke_toolbox_main');
@ -121,46 +284,46 @@ 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;
};
var $loadMore, $version, get;
var $loadMore, $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.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') {
$loadMore.off('click').hide();
get(c);
$version.show();
return;
}
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();
$version.show();
}
if (cb) { cb(); }
});
};
get = function (i) {
// 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; }
@ -168,29 +331,45 @@ define([
if (i <= -(states.length - 11)) {
loadMore();
}
var idx = states.length - 1 + i;
var idx = getIndex(i);
if (semantic && i !== c) {
// If semantic is true, 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;
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
// 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]);
@ -203,77 +382,186 @@ 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.alert(Messages.snapshots_cantMake); }
refreshBar();
} catch (e) {
console.error(e);
}
};
// 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_fastNext }, [
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 $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'); }
$('<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);
$('<span>', {'class': 'cp-history-filler'}).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 = $(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.fa.fa-caret-down');
var time = h('div.cp-history-timeline-time');
$time = $(time);
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
])
])
]);
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 restoreTitle = config.drive ? Messages.history_restoreDriveTitle
: Messages.history_restoreTitle;
var restore = h('button', {
title: 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);
// 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();
@ -283,33 +571,93 @@ define([
};
// 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)); });
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 ([37, 40].indexOf(e.which) >= 0) { p(); return render(getPrevious()); } // Left
if ([38, 39].indexOf(e.which) >= 0) { p(); return render(getNext()); } // Right
if (e.which === 33) { p(); return render(getNext(10)); } // PageUp
if (e.which === 34) { p(); return render(getPrevious(10)); } // PageUp
if (e.which === 27) { p(); $close.click(); }
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',
iconClass: '.fa.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 () {
$(close).click(function () {
states = [];
close();
onClose();
closeUI();
});
$rev.click(function () {
UI.confirm(Messages.history_restorePrompt, function (yes) {
$(restore).click(function () {
var restorePrompt = config.drive ? Messages.history_restoreDrivePrompt
: Messages.history_restorePrompt;
UI.confirm(restorePrompt, function (yes) {
if (!yes) { return; }
close();
onRevert();
UI.log(Messages.history_restoreDone);
var done = onRevert();
if (done) {
closeUI();
var restoreDone = config.drive ? Messages.history_restoreDriveDone
: Messages.history_restoreDone;
UI.log(restoreDone);
}
});
});
@ -328,11 +676,9 @@ define([
History.loading = false;
if (err) { throw new Error(err); }
update(newRt);
c = states.length - 1;
display();
if (isFull) {
$loadMore.off('click').hide();
$version.show();
}
});
};

@ -465,6 +465,7 @@ define([
feedbackAllowed: Utils.Feedback.state,
isPresent: parsed.hashData && parsed.hashData.present,
isEmbed: parsed.hashData && parsed.hashData.embed,
isHistoryVersion: parsed.hashData && parsed.hashData.versionHash,
accounts: {
donateURL: Cryptpad.donateURL,
upgradeURL: Cryptpad.upgradeURL
@ -1116,6 +1117,7 @@ define([
// We don't need it since the message is already validated serverside by hk
return {
msg: crypto.decrypt(obj.msg, true, true),
serverHash: obj.serverHash,
author: obj.author,
time: obj.time
};
@ -1451,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();
@ -1530,6 +1552,7 @@ define([
var cpNfCfg = {
sframeChan: sframeChan,
channel: secret.channel,
versionHash: parsed.hashData && parsed.hashData.versionHash,
padRpc: Cryptpad.padRpc,
validateKey: secret.keys.validateKey || undefined,
isNewHash: isNewHash,

@ -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,27 @@ MessengerUI, Messages) {
}
};
Messages.snaphot_title = "Snapshot"; //XXX
toolbar.setSnapshot = function (bool) {
toolbar.history = bool;
toolbar.title.toggleClass('cp-toolbar-unsync', bool);
if (bool && toolbar.spinner) {
toolbar.spinner.text(Messages.snaphot_title);
} 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(Messages.historyText);
} 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,52 @@ define([
console.log(doc);
};
var 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;
};
// 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);
};
config.onInit = function (info) {
Title = common.createTitle({});
@ -620,12 +663,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,
};

@ -1013,7 +1013,6 @@ define([
var mkHelpMenu = function (framework) {
var $toolbarContainer = $('#cp-app-kanban-container');
$toolbarContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning());
var helpMenu = framework._.sfCommon.createHelpMenu(['kanban']);
$toolbarContainer.prepend(helpMenu.menu);

@ -206,7 +206,6 @@ define([
var mkHelpMenu = function(framework) {
var $toolbarContainer = $('.cke_toolbox_main');
$toolbarContainer.before(framework._.sfCommon.getBurnAfterReadingWarning());
var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'pad']);
$toolbarContainer.before(helpMenu.menu);

@ -428,7 +428,6 @@ define([
var mkHelpMenu = function (framework) {
var $codeMirrorContainer = $('#cp-app-slide-editor-container');
$codeMirrorContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning());
var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'slide']);
$codeMirrorContainer.prepend(helpMenu.menu);

@ -250,7 +250,6 @@ define([
var mkHelpMenu = function (framework) {
var $appContainer = $('#cp-app-whiteboard-container');
$appContainer.prepend(framework._.sfCommon.getBurnAfterReadingWarning());
var helpMenu = framework._.sfCommon.createHelpMenu(['whiteboard']);
$appContainer.prepend(helpMenu.menu);
framework._.toolbar.$drawer.append(helpMenu.button);

Loading…
Cancel
Save