Merge branch 'master' of https://github.com/xwiki-labs/cryptpad
This commit is contained in:
15
www/bounce/index.html
Normal file
15
www/bounce/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="cp">
|
||||
<head>
|
||||
<title data-localization="main_title">CryptPad: Zero Knowledge, Collaborative Real Time Editing</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
|
||||
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript>
|
||||
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
|
||||
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
|
||||
</noscript>
|
||||
</html>
|
||||
10
www/bounce/main.js
Normal file
10
www/bounce/main.js
Normal file
@@ -0,0 +1,10 @@
|
||||
define([], function () {
|
||||
if (window.localStorage && window.localStorage.FS_hash) {
|
||||
window.alert('The bounce application must only be used from the sandbox domain, ' +
|
||||
'please report this issue on https://github.com/xwiki-labs/cryptpad');
|
||||
return;
|
||||
}
|
||||
var bounceTo = decodeURIComponent(window.location.hash.slice(1));
|
||||
if (!bounceTo) { return; }
|
||||
window.location.href = bounceTo;
|
||||
});
|
||||
9
www/bounce/readme.md
Normal file
9
www/bounce/readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Bounce app
|
||||
|
||||
This app redirects you to a new URL.
|
||||
This app must only be served from CryptPad's safe origin, if this app detects that it is being
|
||||
served from the unsafe origin, it will throw an alert that it is misconfigured and it will refuse
|
||||
to redirect.
|
||||
|
||||
If the URL is a javascript: URL, it will be trapped by CryptPad's Content Security Policy rules
|
||||
or in the worst case, it will run in the context of the sandboxed origin.
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
html, body {
|
||||
overflow-y: hidden;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<script async data-bootload="inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style> .loading-hidden { display: none; } </style>
|
||||
</head>
|
||||
<body class="loading-hidden">
|
||||
|
||||
@@ -10,7 +10,13 @@ define([
|
||||
|
||||
var module = { exports: {} };
|
||||
var key = Config.requireConf.urlArgs;
|
||||
var localStorage = window.localStorage || {};
|
||||
var localStorage;
|
||||
try {
|
||||
localStorage = window.localStorage || {};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
localStorage = {};
|
||||
}
|
||||
|
||||
var fixURL = function (url) {
|
||||
var mark = (url.indexOf('?') !== -1) ? '&' : '?';
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
// This is stage 1, it can be changed but you must bump the version of the project.
|
||||
define([], function () {
|
||||
// fix up locations so that relative urls work.
|
||||
require.config({
|
||||
baseUrl: window.location.pathname,
|
||||
paths: {
|
||||
// jquery declares itself as literally "jquery" so it cannot be pulled by path :(
|
||||
"jquery": "/bower_components/jquery/dist/jquery.min",
|
||||
// json.sortify same
|
||||
"json.sortify": "/bower_components/json.sortify/dist/JSON.sortify",
|
||||
//"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf",
|
||||
//"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker"
|
||||
cm: '/bower_components/codemirror'
|
||||
},
|
||||
map: {
|
||||
'*': {
|
||||
'css': '/bower_components/require-css/css.js',
|
||||
'less': '/common/RequireLess.js',
|
||||
}
|
||||
}
|
||||
});
|
||||
define([
|
||||
'/common/requireconfig.js'
|
||||
], function (RequireConfig) {
|
||||
require.config(RequireConfig());
|
||||
|
||||
// most of CryptPad breaks if you don't support isArray
|
||||
if (!Array.isArray) {
|
||||
@@ -27,6 +11,16 @@ define([], function () {
|
||||
};
|
||||
}
|
||||
|
||||
// file encryption/decryption won't work if you don't have Array.fill
|
||||
if (typeof(Array.prototype.fill) !== 'function') {
|
||||
Array.prototype.fill = function (x) { // CRYPTPAD_SHIM
|
||||
var i = 0;
|
||||
var l = this.length;
|
||||
for (;i < l; i++) { this[i] = x; }
|
||||
return this;
|
||||
};
|
||||
}
|
||||
|
||||
var failStore = function () {
|
||||
console.error(new Error('wut'));
|
||||
require(['jquery'], function ($) {
|
||||
|
||||
@@ -6,6 +6,23 @@ define([
|
||||
var Nacl = window.nacl;
|
||||
var module = {};
|
||||
|
||||
var blobToArrayBuffer = function (blob, cb) {
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = function () {
|
||||
cb(void 0, this.result);
|
||||
};
|
||||
reader.readAsArrayBuffer(blob);
|
||||
};
|
||||
|
||||
var arrayBufferToString = function (AB) {
|
||||
try {
|
||||
return Nacl.util.encodeBase64(new Uint8Array(AB));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
module.create = function (common, config) {
|
||||
var File = {};
|
||||
|
||||
@@ -227,19 +244,33 @@ define([
|
||||
queue.next();
|
||||
};
|
||||
|
||||
var handleFile = File.handleFile = function (file, e) {
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = function () {
|
||||
var handleFile = File.handleFile = function (file, e, thumbnail) {
|
||||
var thumb;
|
||||
var finish = function (arrayBuffer) {
|
||||
var metadata = {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
};
|
||||
if (thumb) { metadata.thumbnail = thumb; }
|
||||
queue.push({
|
||||
blob: this.result,
|
||||
metadata: {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
},
|
||||
blob: arrayBuffer,
|
||||
metadata: metadata,
|
||||
dropEvent: e
|
||||
});
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
|
||||
var processFile = function () {
|
||||
blobToArrayBuffer(file, function (e, buffer) {
|
||||
finish(buffer);
|
||||
});
|
||||
};
|
||||
|
||||
if (!thumbnail) { return void processFile(); }
|
||||
blobToArrayBuffer(thumbnail, function (e, buffer) {
|
||||
if (e) { console.error(e); }
|
||||
thumb = arrayBufferToString(buffer);
|
||||
processFile();
|
||||
});
|
||||
};
|
||||
|
||||
var onFileDrop = File.onFileDrop = function (file, e) {
|
||||
|
||||
@@ -214,6 +214,10 @@ Version 1
|
||||
|
||||
Hash.getHashes = function (channel, secret) {
|
||||
var hashes = {};
|
||||
if (!secret.keys) {
|
||||
console.error('e');
|
||||
return hashes;
|
||||
}
|
||||
if (secret.keys.editKeyStr) {
|
||||
hashes.editHash = getEditHashFromKeys(channel, secret.keys);
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ define([
|
||||
var parsed = config.href ? common.parsePadUrl(config.href) : {};
|
||||
var secret = common.getSecrets(parsed.type, parsed.hash);
|
||||
|
||||
History.readOnly = 1;
|
||||
History.readOnly = 0;
|
||||
if (!secret.keys) {
|
||||
secret.keys = secret.key;
|
||||
History.readOnly = 2;
|
||||
History.readOnly = 0;
|
||||
}
|
||||
else if (!secret.keys.validateKey) {
|
||||
History.readOnly = 0;
|
||||
History.readOnly = 1;
|
||||
}
|
||||
|
||||
var crypto = Crypto.createEncryptor(secret.keys);
|
||||
@@ -203,7 +203,7 @@ define([
|
||||
'class':'revertHistory buttonSuccess',
|
||||
title: Messages.history_restoreTitle
|
||||
}).text(Messages.history_restore).appendTo($nav);
|
||||
if (!History.readOnly) { $rev.hide(); }
|
||||
if (History.readOnly) { $rev.hide(); }
|
||||
|
||||
onUpdate = function () {
|
||||
$cur.attr('max', states.length);
|
||||
|
||||
@@ -64,6 +64,9 @@ define([
|
||||
});
|
||||
window.setTimeout(function () {
|
||||
findOKButton().focus();
|
||||
if (typeof(UI.notify) === 'function') {
|
||||
UI.notify();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -97,6 +100,9 @@ define([
|
||||
cb(null, ev);
|
||||
stopListening(keyHandler);
|
||||
});
|
||||
if (typeof(UI.notify) === 'function') {
|
||||
UI.notify();
|
||||
}
|
||||
};
|
||||
|
||||
UI.confirm = function (msg, cb, opt, force, styleCB) {
|
||||
@@ -141,6 +147,9 @@ define([
|
||||
styleCB($ok.closest('.dialog'));
|
||||
}
|
||||
}, 0);
|
||||
if (typeof(UI.notify) === 'function') {
|
||||
UI.notify();
|
||||
}
|
||||
};
|
||||
|
||||
UI.log = function (msg) {
|
||||
|
||||
@@ -6,62 +6,16 @@ define([
|
||||
|
||||
'/bower_components/marked/marked.min.js',
|
||||
'/common/common-realtime.js',
|
||||
|
||||
// displayAvatar
|
||||
// whenRealtimeSyncs
|
||||
// getRealtime -> removeFromFriendList
|
||||
/* UI
|
||||
Messages
|
||||
confirm
|
||||
fixHTML
|
||||
displayAvatar
|
||||
clearOwnedChannel
|
||||
alert
|
||||
|
||||
|
||||
pushMsg
|
||||
removeFromFriendList
|
||||
|
||||
onDirectMessage
|
||||
getNetwork
|
||||
getProxy
|
||||
pushMsg
|
||||
|
||||
Init
|
||||
getNetwork
|
||||
getProxy
|
||||
onDirectMessage
|
||||
removeFromFriendList
|
||||
notify
|
||||
onMessage
|
||||
|
||||
*/
|
||||
|
||||
], function ($, Crypto, Curve, Hash, Marked, Realtime) {
|
||||
var Msg = {
|
||||
inputs: [],
|
||||
};
|
||||
|
||||
|
||||
var Types = {
|
||||
message: 'MSG',
|
||||
update: 'UPDATE',
|
||||
unfriend: 'UNFRIEND',
|
||||
mapId: 'MAP_ID',
|
||||
mapIdAck: 'MAP_ID_ACK'
|
||||
};
|
||||
|
||||
// TODO
|
||||
// - mute a channel (hide notifications or don't open it?)
|
||||
|
||||
var ready = [];
|
||||
var pending = {};
|
||||
var pendingRequests = [];
|
||||
|
||||
var parseMessage = function (content) {
|
||||
return Marked(content);
|
||||
};
|
||||
|
||||
var createData = Msg.createData = function (proxy, hash) {
|
||||
return {
|
||||
channel: hash || Hash.createChannelId(),
|
||||
@@ -82,13 +36,6 @@ define([
|
||||
return proxy.friends ? proxy.friends[pubkey] : undefined;
|
||||
};
|
||||
|
||||
var removeFromFriendList = function (proxy, realtime, curvePublic, cb) {
|
||||
if (!proxy.friends) { return; }
|
||||
var friends = proxy.friends;
|
||||
delete friends[curvePublic];
|
||||
Realtime.whenRealtimeSyncs(realtime, cb);
|
||||
};
|
||||
|
||||
var getFriendList = Msg.getFriendList = function (proxy) {
|
||||
if (!proxy.friends) { proxy.friends = {}; }
|
||||
return proxy.friends;
|
||||
@@ -101,7 +48,6 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Msg.getFriendChannelsList = function (proxy) {
|
||||
var list = [];
|
||||
eachFriend(proxy, function (friend) {
|
||||
@@ -110,500 +56,9 @@ define([
|
||||
return list;
|
||||
};
|
||||
|
||||
// Messaging tools
|
||||
var avatars = {};
|
||||
|
||||
// TODO make this internal to the messenger
|
||||
var channels = Msg.channels = window.channels = {};
|
||||
|
||||
var UI = Msg.UI = {};
|
||||
|
||||
UI.init = function (common, $listContainer, $msgContainer) {
|
||||
var ui = {
|
||||
containers: {
|
||||
friendList: $listContainer,
|
||||
messages: $msgContainer,
|
||||
},
|
||||
};
|
||||
|
||||
ui.addToFriendList = function (data, display, remove) {
|
||||
var $block = ui.containers.friendBlock;
|
||||
|
||||
var $friend = $('<div>', {'class': 'friend avatar'}).appendTo($block);
|
||||
$friend.data('key', data.curvePublic);
|
||||
var $rightCol = $('<span>', {'class': 'right-col'});
|
||||
$('<span>', {'class': 'name'}).text(data.displayName).appendTo($rightCol);
|
||||
var $remove = $('<span>', {'class': 'remove fa fa-user-times'}).appendTo($rightCol);
|
||||
$remove.attr('title', common.Messages.contacts_remove);
|
||||
$friend.dblclick(function () {
|
||||
if (data.profile) {
|
||||
window.open('/profile/#' + data.profile);
|
||||
}
|
||||
});
|
||||
$friend.click(function () {
|
||||
display(data.curvePublic);
|
||||
});
|
||||
$remove.click(function (e) {
|
||||
e.stopPropagation();
|
||||
common.confirm(common.Messages._getKey('contacts_confirmRemove', [
|
||||
common.fixHTML(data.displayName)
|
||||
]), function (yes) {
|
||||
if (!yes) { return; }
|
||||
remove(data.curvePublic);
|
||||
}, null, true);
|
||||
});
|
||||
if (data.avatar && avatars[data.avatar]) {
|
||||
$friend.append(avatars[data.avatar]);
|
||||
$friend.append($rightCol);
|
||||
} else {
|
||||
common.displayAvatar($friend, data.avatar, data.displayName, function ($img) {
|
||||
if (data.avatar && $img) {
|
||||
avatars[data.avatar] = $img[0].outerHTML;
|
||||
}
|
||||
$friend.append($rightCol);
|
||||
});
|
||||
}
|
||||
$('<span>', {'class': 'status'}).appendTo($friend);
|
||||
};
|
||||
|
||||
ui.createFriendList = function (friends, display, remove) {
|
||||
var $block = ui.containers.friendBlock = $('<div>');
|
||||
eachFriend(friends, function (friend) {
|
||||
ui.addToFriendList(friend, display, remove);
|
||||
});
|
||||
$block.appendTo($listContainer);
|
||||
};
|
||||
|
||||
ui.notify = function (curvePublic) {
|
||||
var $friend = $listContainer.find('.friend').filter(function (idx, el) {
|
||||
return $(el).data('key') === curvePublic;
|
||||
});
|
||||
$friend.addClass('notify');
|
||||
};
|
||||
|
||||
ui.unnotify = function (curvePublic) {
|
||||
var $friend = $listContainer.find('.friend').filter(function (idx, el) {
|
||||
return $(el).data('key') === curvePublic;
|
||||
});
|
||||
$friend.removeClass('notify');
|
||||
};
|
||||
|
||||
ui.update = function (curvePublic, types) {
|
||||
var proxy = common.getProxy();
|
||||
var data = getFriend(proxy, curvePublic);
|
||||
var chan = channels[data.channel];
|
||||
if (!chan.ready) {
|
||||
chan.updateOnReady = (chan.updateOnReady || []).concat(types);
|
||||
return;
|
||||
}
|
||||
var $friend = $listContainer.find('.friend').filter(function (idx, el) {
|
||||
return $(el).data('key') === curvePublic;
|
||||
});
|
||||
if (types.indexOf('displayName') >= 0) {
|
||||
$friend.find('.name').text(data.displayName);
|
||||
}
|
||||
if (types.indexOf('avatar') >= 0) {
|
||||
$friend.find('.default').remove();
|
||||
$friend.find('media-tag').remove();
|
||||
if (data.avatar && avatars[data.avatar]) {
|
||||
$friend.prepend(avatars[data.avatar]);
|
||||
} else {
|
||||
common.displayAvatar($friend, data.avatar, data.displayName, function ($img) {
|
||||
if (data.avatar && $img) {
|
||||
avatars[data.avatar] = $img[0].outerHTML;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ui.updateStatus = function (curvePublic, online) {
|
||||
ui.getFriend(curvePublic).find('.status')
|
||||
.attr('class', 'status ' + (online? 'online' : 'offline'));
|
||||
};
|
||||
|
||||
ui.getChannel = function (curvePublic) {
|
||||
var $chat = $msgContainer.find('.chat').filter(function (idx, el) {
|
||||
return $(el).data('key') === curvePublic;
|
||||
});
|
||||
return $chat.length? $chat: null;
|
||||
};
|
||||
|
||||
ui.hideInfo = function () {
|
||||
$msgContainer.find('.info').hide();
|
||||
};
|
||||
|
||||
ui.showInfo = function () {
|
||||
$msgContainer.find('.info').show();
|
||||
};
|
||||
|
||||
ui.createChat = function (curvePublic) {
|
||||
return $('<div>', {'class':'chat'})
|
||||
.data('key', curvePublic).appendTo($msgContainer);
|
||||
};
|
||||
|
||||
ui.hideChat = function () {
|
||||
$msgContainer.find('.chat').hide();
|
||||
};
|
||||
|
||||
ui.getFriend = function (curvePublic) {
|
||||
return $listContainer.find('.friend').filter(function (idx, el) {
|
||||
return $(el).data('key') === curvePublic;
|
||||
});
|
||||
};
|
||||
|
||||
ui.remove = function (curvePublic) {
|
||||
var $friend = ui.getFriend(curvePublic);
|
||||
var $chat = ui.getChannel(curvePublic);
|
||||
$friend.remove();
|
||||
if ($chat) { $chat.remove(); }
|
||||
ui.showInfo();
|
||||
};
|
||||
|
||||
ui.createMessage = function (msg, name) {
|
||||
var $msg = $('<div>', {'class': 'message'})
|
||||
.attr('title', msg.time ? new Date(msg.time).toLocaleString(): '?');
|
||||
|
||||
if (name) {
|
||||
$('<div>', {'class':'sender'}).text(name).appendTo($msg);
|
||||
}
|
||||
|
||||
$('<div>', {'class':'content'}).html(parseMessage(msg.text)).appendTo($msg);
|
||||
return $msg;
|
||||
};
|
||||
|
||||
ui.setEditable = function (bool) {
|
||||
bool = !bool;
|
||||
var input = ui.input;
|
||||
if (!input) { return; }
|
||||
|
||||
if (bool) {
|
||||
input.setAttribute('disabled', bool);
|
||||
} else {
|
||||
input.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
if (common.Messages) {
|
||||
// set placeholder
|
||||
var placeholder = bool?
|
||||
common.Messages.disconnected:
|
||||
common.Messages.contacts_typeHere;
|
||||
input.setAttribute('placeholder', placeholder);
|
||||
}
|
||||
};
|
||||
|
||||
ui.createChatBox = function (proxy, $container, curvePublic) {
|
||||
var data = getFriend(proxy, curvePublic);
|
||||
|
||||
// Input
|
||||
var channel = channels[data.channel];
|
||||
|
||||
var $header = $('<div>', {
|
||||
'class': 'header',
|
||||
}).appendTo($container);
|
||||
|
||||
var $avatar = $('<div>', {'class': 'avatar'}).appendTo($header);
|
||||
|
||||
// more history...
|
||||
$('<span>', {
|
||||
'class': 'more-history',
|
||||
})
|
||||
.text('get more history')
|
||||
.click(function () {
|
||||
console.log("GETTING HISTORY");
|
||||
channel.getPreviousMessages();
|
||||
})
|
||||
.appendTo($header);
|
||||
|
||||
var $removeHistory = $('<span>', {
|
||||
'class': 'remove-history fa fa-eraser',
|
||||
title: common.Messages.contacts_removeHistoryTitle
|
||||
})
|
||||
.click(function () {
|
||||
common.confirm(common.Messages.contacts_confirmRemoveHistory, function (yes) {
|
||||
if (!yes) { return; }
|
||||
common.clearOwnedChannel(data.channel, function (e) {
|
||||
if (e) {
|
||||
console.error(e);
|
||||
common.alert(common.Messages.contacts_removeHistoryServerError);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$removeHistory.appendTo($header);
|
||||
|
||||
$('<div>', {'class': 'messages'}).appendTo($container);
|
||||
var $inputBlock = $('<div>', {'class': 'input'}).appendTo($container);
|
||||
|
||||
var $input = $('<textarea>').appendTo($inputBlock);
|
||||
$input.attr('placeholder', common.Messages.contacts_typeHere);
|
||||
ui.input = $input[0];
|
||||
|
||||
var send = function () {
|
||||
// TODO implement sending queue
|
||||
// TODO separate message logic from UI
|
||||
var channel = channels[data.channel];
|
||||
if (channel.sending) {
|
||||
console.error("still sending");
|
||||
return;
|
||||
}
|
||||
if (!$input.val()) {
|
||||
console.error("nothing to send");
|
||||
return;
|
||||
}
|
||||
if ($input.attr('disabled')) {
|
||||
console.error("input is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = $input.val();
|
||||
// Send the message
|
||||
channel.sending = true;
|
||||
channel.send(payload, function (e) {
|
||||
if (e) {
|
||||
channel.sending = false;
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
$input.val('');
|
||||
channel.refresh();
|
||||
channel.sending = false;
|
||||
});
|
||||
};
|
||||
$('<button>', {
|
||||
'class': 'btn btn-primary fa fa-paper-plane',
|
||||
title: common.Messages.contacts_send,
|
||||
}).appendTo($inputBlock).click(send);
|
||||
|
||||
var onKeyDown = function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
if (e.ctrlKey || e.shiftKey) {
|
||||
var val = this.value;
|
||||
if (typeof this.selectionStart === "number" && typeof this.selectionEnd === "number") {
|
||||
var start = this.selectionStart;
|
||||
this.value = val.slice(0, start) + "\n" + val.slice(this.selectionEnd);
|
||||
this.selectionStart = this.selectionEnd = start + 1;
|
||||
} else if (document.selection && document.selection.createRange) {
|
||||
this.focus();
|
||||
var range = document.selection.createRange();
|
||||
range.text = "\r\n";
|
||||
range.collapse(false);
|
||||
range.select();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
send();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
$input.on('keydown', onKeyDown);
|
||||
|
||||
// Header
|
||||
var $rightCol = $('<span>', {'class': 'right-col'});
|
||||
$('<span>', {'class': 'name'}).text(data.displayName).appendTo($rightCol);
|
||||
if (data.avatar && avatars[data.avatar]) {
|
||||
$avatar.append(avatars[data.avatar]);
|
||||
$avatar.append($rightCol);
|
||||
} else {
|
||||
common.displayAvatar($avatar, data.avatar, data.displayName, function ($img) {
|
||||
if (data.avatar && $img) {
|
||||
avatars[data.avatar] = $img[0].outerHTML;
|
||||
}
|
||||
$avatar.append($rightCol);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return ui;
|
||||
};
|
||||
|
||||
var msgAlreadyKnown = function (channel, sig) {
|
||||
return channel.messages.some(function (message) {
|
||||
return message[0] === sig;
|
||||
});
|
||||
};
|
||||
|
||||
// TODO remove dependency on common
|
||||
var pushMsg = function (realtime, proxy, common, channel, cryptMsg) {
|
||||
var msg = channel.encryptor.decrypt(cryptMsg);
|
||||
|
||||
var sig = cryptMsg.slice(0, 64);
|
||||
if (msgAlreadyKnown(channel, sig)) { return; }
|
||||
|
||||
var parsedMsg = JSON.parse(msg);
|
||||
if (parsedMsg[0] === Types.message) {
|
||||
// TODO validate messages here
|
||||
var res = {
|
||||
type: parsedMsg[0],
|
||||
sig: sig,
|
||||
channel: parsedMsg[1],
|
||||
time: parsedMsg[2],
|
||||
text: parsedMsg[3],
|
||||
};
|
||||
|
||||
channel.messages.push(res);
|
||||
return true;
|
||||
}
|
||||
if (parsedMsg[0] === Types.update) {
|
||||
if (parsedMsg[1] === proxy.curvePublic) { return; }
|
||||
var newdata = parsedMsg[3];
|
||||
var data = getFriend(proxy, parsedMsg[1]);
|
||||
var types = [];
|
||||
Object.keys(newdata).forEach(function (k) {
|
||||
if (data[k] !== newdata[k]) {
|
||||
types.push(k);
|
||||
data[k] = newdata[k];
|
||||
}
|
||||
});
|
||||
channel.updateUI(types);
|
||||
return;
|
||||
}
|
||||
if (parsedMsg[0] === Types.unfriend) {
|
||||
removeFromFriendList(proxy, realtime, channel.friendEd, function () {
|
||||
channel.wc.leave(Types.unfriend);
|
||||
channel.removeUI();
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/* Broadcast a display name, profile, or avatar change to all contacts
|
||||
*/
|
||||
var updateMyData = function (proxy) {
|
||||
var friends = getFriendList(proxy);
|
||||
var mySyncData = friends.me;
|
||||
var myData = createData(proxy);
|
||||
if (!mySyncData || mySyncData.displayName !== myData.displayName
|
||||
|| mySyncData.profile !== myData.profile
|
||||
|| mySyncData.avatar !== myData.avatar) {
|
||||
delete myData.channel;
|
||||
Object.keys(channels).forEach(function (chan) {
|
||||
var channel = channels[chan];
|
||||
var msg = [Types.update, myData.curvePublic, +new Date(), myData];
|
||||
var msgStr = JSON.stringify(msg);
|
||||
var cryptMsg = channel.encryptor.encrypt(msgStr);
|
||||
channel.wc.bcast(cryptMsg).then(function () {
|
||||
channel.refresh();
|
||||
}, function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
friends.me = myData;
|
||||
}
|
||||
};
|
||||
|
||||
var onChannelReady = function (proxy, chanId) {
|
||||
if (ready.indexOf(chanId) !== -1) { return; }
|
||||
ready.push(chanId);
|
||||
channels[chanId].updateStatus(); // c'est quoi?
|
||||
var friends = getFriendList(proxy);
|
||||
if (ready.length === Object.keys(friends).length) {
|
||||
// All channels are ready
|
||||
updateMyData(proxy);
|
||||
}
|
||||
return ready.length;
|
||||
};
|
||||
|
||||
// Id message allows us to map a netfluxId with a public curve key
|
||||
var onIdMessage = function (proxy, network, msg, sender) {
|
||||
var channel;
|
||||
var isId = Object.keys(channels).some(function (chanId) {
|
||||
if (channels[chanId].userList.indexOf(sender) !== -1) {
|
||||
channel = channels[chanId];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isId) { return; }
|
||||
|
||||
var decryptedMsg = channel.encryptor.decrypt(msg);
|
||||
|
||||
if (decryptedMsg === null) {
|
||||
// console.error('unable to decrypt message');
|
||||
// console.error('potentially meant for yourself');
|
||||
|
||||
// message failed to parse, meaning somebody sent it to you but
|
||||
// encrypted it with the wrong key, or you're sending a message to
|
||||
// yourself in a different tab.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!decryptedMsg) {
|
||||
console.error('decrypted message was falsey but not null');
|
||||
return;
|
||||
}
|
||||
|
||||
var parsed;
|
||||
try {
|
||||
parsed = JSON.parse(decryptedMsg);
|
||||
} catch (e) {
|
||||
console.error(decryptedMsg);
|
||||
return;
|
||||
}
|
||||
if (parsed[0] !== Types.mapId && parsed[0] !== Types.mapIdAck) { return; }
|
||||
|
||||
// check that the responding peer's encrypted netflux id matches
|
||||
// the sender field. This is to prevent replay attacks.
|
||||
if (parsed[2] !== sender || !parsed[1]) { return; }
|
||||
channel.mapId[sender] = parsed[1];
|
||||
|
||||
channel.updateStatus();
|
||||
|
||||
if (parsed[0] !== Types.mapId) { return; } // Don't send your key if it's already an ACK
|
||||
// Answer with your own key
|
||||
var rMsg = [Types.mapIdAck, proxy.curvePublic, channel.wc.myID];
|
||||
var rMsgStr = JSON.stringify(rMsg);
|
||||
var cryptMsg = channel.encryptor.encrypt(rMsgStr);
|
||||
network.sendto(sender, cryptMsg);
|
||||
};
|
||||
|
||||
// HERE
|
||||
var onDirectMessage = function (common, msg, sender) {
|
||||
var proxy = common.getProxy();
|
||||
var network = common.getNetwork();
|
||||
var realtime = common.getRealtime();
|
||||
|
||||
if (sender !== Msg.hk) { return void onIdMessage(proxy, network, msg, sender); }
|
||||
var parsed = JSON.parse(msg);
|
||||
if ((parsed.validateKey || parsed.owners) && parsed.channel) {
|
||||
return;
|
||||
}
|
||||
if (parsed.state && parsed.state === 1 && parsed.channel) {
|
||||
if (channels[parsed.channel]) {
|
||||
// parsed.channel is Ready
|
||||
// TODO: call a function that shows that the channel is ready? (remove a spinner, ...)
|
||||
// channel[parsed.channel].ready();
|
||||
channels[parsed.channel].ready = true;
|
||||
onChannelReady(proxy, parsed.channel);
|
||||
var updateTypes = channels[parsed.channel].updateOnReady;
|
||||
if (updateTypes) {
|
||||
channels[parsed.channel].updateUI(updateTypes);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
var chan = parsed[3];
|
||||
if (!chan || !channels[chan]) { return; }
|
||||
pushMsg(realtime, proxy, common, channels[chan], parsed[4]);
|
||||
channels[chan].refresh();
|
||||
};
|
||||
var onMessage = function (common, msg, sender, chan) {
|
||||
if (!channels[chan.id]) { return; }
|
||||
|
||||
var realtime = common.getRealtime();
|
||||
var proxy = common.getProxy();
|
||||
|
||||
var isMessage = pushMsg(realtime, proxy, common, channels[chan.id], msg);
|
||||
if (isMessage) {
|
||||
// Don't notify for your own messages
|
||||
if (channels[chan.id].wc.myID !== sender) {
|
||||
channels[chan.id].notify();
|
||||
}
|
||||
channels[chan.id].refresh();
|
||||
}
|
||||
};
|
||||
|
||||
Msg.getLatestMessages = function () {
|
||||
Object.keys(channels).forEach(function (id) {
|
||||
if (id === 'me') { return; }
|
||||
@@ -613,280 +68,6 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
var getMoreHistory = function (network, chan, hash, count) {
|
||||
var msg = [ 'GET_HISTORY_RANGE', chan.id, {
|
||||
from: hash,
|
||||
count: count,
|
||||
}
|
||||
];
|
||||
|
||||
network.sendto(network.historyKeeper, JSON.stringify(msg)).then(function () {
|
||||
}, function (err) {
|
||||
throw new Error(err);
|
||||
});
|
||||
};
|
||||
|
||||
var getChannelMessagesSince = function (network, proxy, chan, data, keys) {
|
||||
var cfg = {
|
||||
validateKey: keys.validateKey,
|
||||
owners: [proxy.edPublic, data.edPublic],
|
||||
lastKnownHash: data.lastKnownHash
|
||||
};
|
||||
var msg = ['GET_HISTORY', chan.id, cfg];
|
||||
network.sendto(network.historyKeeper, JSON.stringify(msg))
|
||||
.then($.noop, function (err) {
|
||||
throw new Error(err);
|
||||
});
|
||||
};
|
||||
|
||||
/* TODO remove dependency on common
|
||||
*/
|
||||
Msg.init = function (common, ui) {
|
||||
// declare common variables
|
||||
var network = common.getNetwork();
|
||||
var proxy = common.getProxy();
|
||||
var realtime = common.getRealtime();
|
||||
Msg.hk = network.historyKeeper;
|
||||
var friends = getFriendList(proxy);
|
||||
|
||||
// listen for messages...
|
||||
network.on('message', function(msg, sender) {
|
||||
onDirectMessage(common, msg, sender);
|
||||
});
|
||||
|
||||
// declare messenger and common methods
|
||||
var messenger = {
|
||||
ui: ui,
|
||||
};
|
||||
|
||||
messenger.setActive = function (id) {
|
||||
// TODO validate id
|
||||
messenger.active = id;
|
||||
};
|
||||
|
||||
// Refresh the active channel
|
||||
// TODO extract into UI method
|
||||
var refresh = function (curvePublic) {
|
||||
if (messenger.active !== curvePublic) { return; }
|
||||
var data = friends[curvePublic];
|
||||
if (!data) { return; }
|
||||
var channel = channels[data.channel];
|
||||
if (!channel) { return; }
|
||||
|
||||
var $chat = ui.getChannel(curvePublic);
|
||||
|
||||
if (!$chat) { return; }
|
||||
|
||||
// Add new messages
|
||||
var messages = channel.messages;
|
||||
var $messages = $chat.find('.messages');
|
||||
var msg, name;
|
||||
var last = typeof(channel.lastDisplayed) === 'number'? channel.lastDisplayed: -1;
|
||||
for (var i = last + 1; i<messages.length; i++) {
|
||||
msg = messages[i];
|
||||
name = (msg.channel !== channel.lastSender)?
|
||||
getFriend(proxy, msg.channel).displayName: undefined;
|
||||
|
||||
ui.createMessage(msg, name).appendTo($messages);
|
||||
channel.lastSender = msg.channel;
|
||||
}
|
||||
$messages.scrollTop($messages[0].scrollHeight);
|
||||
channel.lastDisplayed = i-1;
|
||||
channel.unnotify();
|
||||
|
||||
// return void channel.notify();
|
||||
if (messages.length > 10) {
|
||||
var lastKnownMsg = messages[messages.length - 11];
|
||||
channel.setLastMessageRead(lastKnownMsg.sig);
|
||||
}
|
||||
};
|
||||
// Display a new channel
|
||||
// TODO extract into UI method
|
||||
var display = function (curvePublic) {
|
||||
ui.hideInfo();
|
||||
var $chat = ui.getChannel(curvePublic);
|
||||
if (!$chat) {
|
||||
$chat = ui.createChat(curvePublic);
|
||||
ui.createChatBox(proxy, $chat, curvePublic);
|
||||
}
|
||||
// Show the correct div
|
||||
ui.hideChat();
|
||||
$chat.show();
|
||||
|
||||
// TODO set this attr per-messenger
|
||||
messenger.setActive(curvePublic);
|
||||
// TODO don't mark messages as read unless you have displayed them
|
||||
|
||||
refresh(curvePublic);
|
||||
};
|
||||
|
||||
// TODO take a callback
|
||||
var remove = function (curvePublic) {
|
||||
var data = getFriend(proxy, curvePublic);
|
||||
var channel = channels[data.channel];
|
||||
var msg = [Types.unfriend, proxy.curvePublic, +new Date()];
|
||||
var msgStr = JSON.stringify(msg);
|
||||
var cryptMsg = channel.encryptor.encrypt(msgStr);
|
||||
channel.wc.bcast(cryptMsg).then(function () {
|
||||
removeFromFriendList(common, curvePublic, function () {
|
||||
channel.wc.leave(Types.unfriend);
|
||||
channel.removeUI();
|
||||
});
|
||||
}, function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
// Display friend list
|
||||
ui.createFriendList(friends, display, remove);
|
||||
|
||||
// Open the channels
|
||||
|
||||
// TODO extract this into an external function
|
||||
var openFriendChannel = function (data, f) {
|
||||
var keys = Curve.deriveKeys(data.curvePublic, proxy.curvePrivate);
|
||||
var encryptor = Curve.createEncryptor(keys);
|
||||
network.join(data.channel).then(function (chan) {
|
||||
var channel = channels[data.channel] = {
|
||||
sending: false,
|
||||
friendEd: f,
|
||||
keys: keys,
|
||||
encryptor: encryptor,
|
||||
messages: [],
|
||||
refresh: function () { refresh(data.curvePublic); },
|
||||
notify: function () {
|
||||
ui.notify(data.curvePublic);
|
||||
common.notify(); // HERE
|
||||
},
|
||||
unnotify: function () { ui.unnotify(data.curvePublic); },
|
||||
removeUI: function () { ui.remove(data.curvePublic); },
|
||||
updateUI: function (types) { ui.update(data.curvePublic, types); },
|
||||
updateStatus: function () {
|
||||
ui.updateStatus(data.curvePublic,
|
||||
channel.getStatus(data.curvePublic));
|
||||
},
|
||||
setLastMessageRead: function (hash) {
|
||||
data.lastKnownHash = hash;
|
||||
},
|
||||
getLastMessageRead: function () {
|
||||
return data.lastKnownHash;
|
||||
},
|
||||
isActive: function () {
|
||||
return data.curvePublic === messenger.active;
|
||||
},
|
||||
getMessagesSinceDisconnect: function () {
|
||||
getChannelMessagesSince(network, proxy, chan, data, keys);
|
||||
},
|
||||
wc: chan,
|
||||
userList: [],
|
||||
mapId: {},
|
||||
getStatus: function (curvePublic) {
|
||||
return channel.userList.some(function (nId) {
|
||||
return channel.mapId[nId] === curvePublic;
|
||||
});
|
||||
},
|
||||
getPreviousMessages: function () {
|
||||
var history = channel.messages;
|
||||
if (!history || !history.length) {
|
||||
// TODO ask for default history?
|
||||
return;
|
||||
}
|
||||
|
||||
var oldestMessage = history[0];
|
||||
if (!oldestMessage) {
|
||||
return; // nothing to fetch
|
||||
}
|
||||
|
||||
var messageHash = oldestMessage[0];
|
||||
getMoreHistory(network, chan, messageHash, 10);
|
||||
},
|
||||
send: function (payload, cb) {
|
||||
if (!network.webChannels.some(function (wc) {
|
||||
if (wc.id === channel.wc.id) { return true; }
|
||||
})) {
|
||||
return void cb('NO_SUCH_CHANNEL');
|
||||
}
|
||||
|
||||
var msg = [Types.message, proxy.curvePublic, +new Date(), payload];
|
||||
var msgStr = JSON.stringify(msg);
|
||||
var cryptMsg = channel.encryptor.encrypt(msgStr);
|
||||
|
||||
channel.wc.bcast(cryptMsg).then(function () {
|
||||
pushMsg(realtime, proxy, common, channel, cryptMsg);
|
||||
cb();
|
||||
}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
},
|
||||
};
|
||||
chan.on('message', function (msg, sender) {
|
||||
onMessage(common, msg, sender, chan);
|
||||
});
|
||||
|
||||
var onJoining = function (peer) {
|
||||
if (peer === Msg.hk) { return; }
|
||||
if (channel.userList.indexOf(peer) !== -1) { return; }
|
||||
channel.userList.push(peer);
|
||||
var msg = [Types.mapId, proxy.curvePublic, chan.myID];
|
||||
var msgStr = JSON.stringify(msg);
|
||||
var cryptMsg = channel.encryptor.encrypt(msgStr);
|
||||
network.sendto(peer, cryptMsg);
|
||||
channel.updateStatus();
|
||||
};
|
||||
chan.members.forEach(function (peer) {
|
||||
if (peer === Msg.hk) { return; }
|
||||
if (channel.userList.indexOf(peer) !== -1) { return; }
|
||||
channel.userList.push(peer);
|
||||
});
|
||||
chan.on('join', onJoining);
|
||||
chan.on('leave', function (peer) {
|
||||
var i = channel.userList.indexOf(peer);
|
||||
while (i !== -1) {
|
||||
channel.userList.splice(i, 1);
|
||||
i = channel.userList.indexOf(peer);
|
||||
}
|
||||
channel.updateStatus();
|
||||
});
|
||||
|
||||
getChannelMessagesSince(network, proxy, chan, data, keys);
|
||||
}, function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
messenger.cleanFriendChannels = function () {
|
||||
Object.keys(channels).forEach(function (id) {
|
||||
delete channels[id];
|
||||
});
|
||||
};
|
||||
|
||||
var openFriendChannels = messenger.openFriendChannels = function () {
|
||||
eachFriend(friends, openFriendChannel);
|
||||
};
|
||||
|
||||
messenger.setEditable = ui.setEditable;
|
||||
|
||||
openFriendChannels();
|
||||
|
||||
// TODO split loop innards into ui methods
|
||||
var checkNewFriends = function () {
|
||||
eachFriend(friends, function (friend, id) {
|
||||
var $friend = ui.getFriend(id);
|
||||
if (!$friend.length) {
|
||||
openFriendChannel(friend, id);
|
||||
ui.addToFriendList(friend, display, remove);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
common.onDisplayNameChanged(function () {
|
||||
checkNewFriends();
|
||||
updateMyData(proxy);
|
||||
});
|
||||
|
||||
return messenger;
|
||||
};
|
||||
|
||||
// Invitation
|
||||
// FIXME there are too many functions with this name
|
||||
var addToFriendList = Msg.addToFriendList = function (common, data, cb) {
|
||||
@@ -898,9 +79,11 @@ define([
|
||||
|
||||
friends[pubKey] = data;
|
||||
|
||||
Realtime.whenRealtimeSyncs(common.getRealtime(), function () {
|
||||
Realtime.whenRealtimeSyncs(common, common.getRealtime(), function () {
|
||||
cb();
|
||||
common.pinPads([data.channel]);
|
||||
common.pinPads([data.channel], function (e) {
|
||||
if (e) { console.error(e); }
|
||||
});
|
||||
});
|
||||
common.changeDisplayName(proxy[common.displayNameKey]);
|
||||
};
|
||||
@@ -951,7 +134,8 @@ define([
|
||||
var confirmMsg = common.Messages._getKey('contacts_request', [
|
||||
common.fixHTML(msgData.displayName)
|
||||
]);
|
||||
common.confirm(confirmMsg, todo, null, true);
|
||||
common.onFriendRequest(confirmMsg, todo);
|
||||
//common.confirm(confirmMsg, todo, null, true);
|
||||
return;
|
||||
}
|
||||
if (msg[0] === "FRIEND_REQ_OK") {
|
||||
@@ -961,9 +145,15 @@ define([
|
||||
// FIXME clarify this function's name
|
||||
addToFriendList(common, msgData, function (err) {
|
||||
if (err) {
|
||||
return void common.log(common.Messages.contacts_addError);
|
||||
return void common.onFriendComplete({
|
||||
logText: common.Messages.contacts_addError,
|
||||
netfluxId: sender
|
||||
});
|
||||
}
|
||||
common.log(common.Messages.contacts_added);
|
||||
common.onFriendComplete({
|
||||
logText: common.Messages.contacts_added,
|
||||
netfluxId: sender
|
||||
});
|
||||
var msg = ["FRIEND_REQ_ACK", chan];
|
||||
var msgStr = Crypto.encrypt(JSON.stringify(msg), key);
|
||||
network.sendto(sender, msgStr);
|
||||
@@ -973,7 +163,10 @@ define([
|
||||
if (msg[0] === "FRIEND_REQ_NOK") {
|
||||
var i = pendingRequests.indexOf(sender);
|
||||
if (i !== -1) { pendingRequests.splice(i, 1); }
|
||||
common.log(common.Messages.contacts_rejected);
|
||||
common.onFriendComplete({
|
||||
logText: common.Messages.contacts_rejected,
|
||||
netfluxId: sender
|
||||
});
|
||||
common.changeDisplayName(proxy[common.displayNameKey]);
|
||||
return;
|
||||
}
|
||||
@@ -982,9 +175,15 @@ define([
|
||||
if (!data) { return; }
|
||||
addToFriendList(common, data, function (err) {
|
||||
if (err) {
|
||||
return void common.log(common.Messages.contacts_addError);
|
||||
return void common.onFriendComplete({
|
||||
logText: common.Messages.contacts_addError,
|
||||
netfluxId: sender
|
||||
});
|
||||
}
|
||||
common.log(common.Messages.contacts_added);
|
||||
common.onFriendComplete({
|
||||
logText: common.Messages.contacts_added,
|
||||
netfluxId: sender
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
673
www/common/common-messenger.js
Normal file
673
www/common/common-messenger.js
Normal file
@@ -0,0 +1,673 @@
|
||||
define([
|
||||
'jquery',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/common/curve.js',
|
||||
'/common/common-hash.js',
|
||||
'/common/common-realtime.js'
|
||||
], function ($, Crypto, Curve, Hash, Realtime) {
|
||||
'use strict';
|
||||
var Msg = {
|
||||
inputs: [],
|
||||
};
|
||||
|
||||
var Types = {
|
||||
message: 'MSG',
|
||||
update: 'UPDATE',
|
||||
unfriend: 'UNFRIEND',
|
||||
mapId: 'MAP_ID',
|
||||
mapIdAck: 'MAP_ID_ACK'
|
||||
};
|
||||
|
||||
var clone = function (o) {
|
||||
return JSON.parse(JSON.stringify(o));
|
||||
};
|
||||
|
||||
// TODO
|
||||
// - mute a channel (hide notifications or don't open it?)
|
||||
var createData = Msg.createData = function (proxy, hash) {
|
||||
return {
|
||||
channel: hash || Hash.createChannelId(),
|
||||
displayName: proxy['cryptpad.username'],
|
||||
profile: proxy.profile && proxy.profile.view,
|
||||
edPublic: proxy.edPublic,
|
||||
curvePublic: proxy.curvePublic,
|
||||
avatar: proxy.profile && proxy.profile.avatar
|
||||
};
|
||||
};
|
||||
|
||||
var getFriend = function (proxy, pubkey) {
|
||||
if (pubkey === proxy.curvePublic) {
|
||||
var data = createData(proxy);
|
||||
delete data.channel;
|
||||
return data;
|
||||
}
|
||||
return proxy.friends ? proxy.friends[pubkey] : undefined;
|
||||
};
|
||||
|
||||
var getFriendList = Msg.getFriendList = function (proxy) {
|
||||
if (!proxy.friends) { proxy.friends = {}; }
|
||||
return proxy.friends;
|
||||
};
|
||||
|
||||
var eachFriend = function (friends, cb) {
|
||||
Object.keys(friends).forEach(function (id) {
|
||||
if (id === 'me') { return; }
|
||||
cb(friends[id], id, friends);
|
||||
});
|
||||
};
|
||||
|
||||
Msg.getFriendChannelsList = function (proxy) {
|
||||
var list = [];
|
||||
eachFriend(proxy, function (friend) {
|
||||
list.push(friend.channel);
|
||||
});
|
||||
return list;
|
||||
};
|
||||
|
||||
var msgAlreadyKnown = function (channel, sig) {
|
||||
return channel.messages.some(function (message) {
|
||||
return message[0] === sig;
|
||||
});
|
||||
};
|
||||
|
||||
Msg.messenger = function (common) {
|
||||
var messenger = {
|
||||
handlers: {
|
||||
message: [],
|
||||
join: [],
|
||||
leave: [],
|
||||
update: [],
|
||||
friend: [],
|
||||
unfriend: [],
|
||||
},
|
||||
range_requests: {},
|
||||
};
|
||||
|
||||
var eachHandler = function (type, g) {
|
||||
messenger.handlers[type].forEach(g);
|
||||
};
|
||||
|
||||
messenger.on = function (type, f) {
|
||||
var stack = messenger.handlers[type];
|
||||
if (!Array.isArray(stack)) {
|
||||
return void console.error('unsupported message type');
|
||||
}
|
||||
if (typeof(f) !== 'function') {
|
||||
return void console.error('expected function');
|
||||
}
|
||||
stack.push(f);
|
||||
};
|
||||
|
||||
var channels = messenger.channels = {};
|
||||
|
||||
var joining = {};
|
||||
|
||||
// declare common variables
|
||||
var network = common.getNetwork();
|
||||
var proxy = common.getProxy();
|
||||
var realtime = common.getRealtime();
|
||||
Msg.hk = network.historyKeeper;
|
||||
var friends = getFriendList(proxy);
|
||||
|
||||
var getChannel = function (curvePublic) {
|
||||
var friend = friends[curvePublic];
|
||||
if (!friend) { return; }
|
||||
var chanId = friend.channel;
|
||||
if (!chanId) { return; }
|
||||
return channels[chanId];
|
||||
};
|
||||
|
||||
var initRangeRequest = function (txid, curvePublic, sig, cb) {
|
||||
messenger.range_requests[txid] = {
|
||||
messages: [],
|
||||
cb: cb,
|
||||
curvePublic: curvePublic,
|
||||
sig: sig,
|
||||
};
|
||||
};
|
||||
|
||||
var getRangeRequest = function (txid) {
|
||||
return messenger.range_requests[txid];
|
||||
};
|
||||
|
||||
messenger.getMoreHistory = function (curvePublic, hash, count, cb) {
|
||||
if (typeof(cb) !== 'function') { return; }
|
||||
|
||||
if (typeof(hash) !== 'string') {
|
||||
// FIXME hash is not necessarily defined.
|
||||
// What does this mean?
|
||||
console.error("not sure what to do here");
|
||||
return;
|
||||
}
|
||||
|
||||
var chan = getChannel(curvePublic);
|
||||
if (typeof(chan) === 'undefined') {
|
||||
console.error("chan is undefined. we're going to have a problem here");
|
||||
return;
|
||||
}
|
||||
|
||||
var txid = common.uid();
|
||||
initRangeRequest(txid, curvePublic, hash, cb);
|
||||
var msg = [ 'GET_HISTORY_RANGE', chan.id, {
|
||||
from: hash,
|
||||
count: count,
|
||||
txid: txid,
|
||||
}
|
||||
];
|
||||
|
||||
network.sendto(network.historyKeeper, JSON.stringify(msg)).then(function () {
|
||||
}, function (err) {
|
||||
throw new Error(err);
|
||||
});
|
||||
};
|
||||
|
||||
var getCurveForChannel = function (id) {
|
||||
var channel = channels[id];
|
||||
if (!channel) { return; }
|
||||
return channel.curve;
|
||||
};
|
||||
|
||||
messenger.getChannelHead = function (curvePublic, cb) {
|
||||
var friend = friends[curvePublic];
|
||||
if (!friend) { return void cb('NO_SUCH_FRIEND'); }
|
||||
cb(void 0, friend.lastKnownHash);
|
||||
};
|
||||
|
||||
messenger.setChannelHead = function (curvePublic, hash, cb) {
|
||||
var friend = friends[curvePublic];
|
||||
if (!friend) { return void cb('NO_SUCH_FRIEND'); }
|
||||
friend.lastKnownHash = hash;
|
||||
cb();
|
||||
};
|
||||
|
||||
// Id message allows us to map a netfluxId with a public curve key
|
||||
var onIdMessage = function (msg, sender) {
|
||||
var channel;
|
||||
var isId = Object.keys(channels).some(function (chanId) {
|
||||
if (channels[chanId].userList.indexOf(sender) !== -1) {
|
||||
channel = channels[chanId];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isId) { return; }
|
||||
|
||||
var decryptedMsg = channel.encryptor.decrypt(msg);
|
||||
|
||||
if (decryptedMsg === null) {
|
||||
return void console.error("Failed to decrypt message");
|
||||
}
|
||||
|
||||
if (!decryptedMsg) {
|
||||
console.error('decrypted message was falsey but not null');
|
||||
return;
|
||||
}
|
||||
|
||||
var parsed;
|
||||
try {
|
||||
parsed = JSON.parse(decryptedMsg);
|
||||
} catch (e) {
|
||||
console.error(decryptedMsg);
|
||||
return;
|
||||
}
|
||||
if (parsed[0] !== Types.mapId && parsed[0] !== Types.mapIdAck) { return; }
|
||||
|
||||
// check that the responding peer's encrypted netflux id matches
|
||||
// the sender field. This is to prevent replay attacks.
|
||||
if (parsed[2] !== sender || !parsed[1]) { return; }
|
||||
channel.mapId[sender] = parsed[1];
|
||||
eachHandler('join', function (f) {
|
||||
f(parsed[1], channel.id);
|
||||
});
|
||||
|
||||
if (parsed[0] !== Types.mapId) { return; } // Don't send your key if it's already an ACK
|
||||
// Answer with your own key
|
||||
var rMsg = [Types.mapIdAck, proxy.curvePublic, channel.wc.myID];
|
||||
var rMsgStr = JSON.stringify(rMsg);
|
||||
var cryptMsg = channel.encryptor.encrypt(rMsgStr);
|
||||
network.sendto(sender, cryptMsg);
|
||||
};
|
||||
|
||||
var orderMessages = function (curvePublic, new_messages /*, sig */) {
|
||||
var channel = getChannel(curvePublic);
|
||||
var messages = channel.messages;
|
||||
|
||||
// TODO improve performance, guarantee correct ordering
|
||||
new_messages.reverse().forEach(function (msg) {
|
||||
messages.unshift(msg);
|
||||
});
|
||||
};
|
||||
|
||||
var removeFromFriendList = function (curvePublic, cb) {
|
||||
if (!proxy.friends) { return; }
|
||||
var friends = proxy.friends;
|
||||
delete friends[curvePublic];
|
||||
Realtime.whenRealtimeSyncs(common, realtime, cb);
|
||||
};
|
||||
|
||||
var pushMsg = function (channel, cryptMsg) {
|
||||
var msg = channel.encryptor.decrypt(cryptMsg);
|
||||
var sig = cryptMsg.slice(0, 64);
|
||||
if (msgAlreadyKnown(channel, sig)) { return; }
|
||||
|
||||
var parsedMsg = JSON.parse(msg);
|
||||
var curvePublic;
|
||||
if (parsedMsg[0] === Types.message) {
|
||||
// TODO validate messages here
|
||||
var res = {
|
||||
type: parsedMsg[0],
|
||||
sig: sig,
|
||||
author: parsedMsg[1],
|
||||
time: parsedMsg[2],
|
||||
text: parsedMsg[3],
|
||||
// this makes debugging a whole lot easier
|
||||
curve: getCurveForChannel(channel.id),
|
||||
};
|
||||
|
||||
channel.messages.push(res);
|
||||
eachHandler('message', function (f) {
|
||||
f(res);
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
if (parsedMsg[0] === Types.update) {
|
||||
if (parsedMsg[1] === proxy.curvePublic) { return; }
|
||||
curvePublic = parsedMsg[1];
|
||||
var newdata = parsedMsg[3];
|
||||
var data = getFriend(proxy, parsedMsg[1]);
|
||||
var types = [];
|
||||
Object.keys(newdata).forEach(function (k) {
|
||||
if (data[k] !== newdata[k]) {
|
||||
types.push(k);
|
||||
data[k] = newdata[k];
|
||||
}
|
||||
});
|
||||
|
||||
eachHandler('update', function (f) {
|
||||
f(clone(newdata), curvePublic);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (parsedMsg[0] === Types.unfriend) {
|
||||
curvePublic = parsedMsg[1];
|
||||
delete friends[curvePublic];
|
||||
|
||||
removeFromFriendList(parsedMsg[1], function () {
|
||||
channel.wc.leave(Types.unfriend);
|
||||
eachHandler('unfriend', function (f) {
|
||||
f(curvePublic);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/* Broadcast a display name, profile, or avatar change to all contacts
|
||||
*/
|
||||
|
||||
// TODO send event...
|
||||
messenger.updateMyData = function () {
|
||||
var friends = getFriendList(proxy);
|
||||
var mySyncData = friends.me;
|
||||
var myData = createData(proxy);
|
||||
if (!mySyncData || mySyncData.displayName !== myData.displayName
|
||||
|| mySyncData.profile !== myData.profile
|
||||
|| mySyncData.avatar !== myData.avatar) {
|
||||
delete myData.channel;
|
||||
Object.keys(channels).forEach(function (chan) {
|
||||
var channel = channels[chan];
|
||||
|
||||
if (!channel) {
|
||||
return void console.error('NO_SUCH_CHANNEL');
|
||||
}
|
||||
|
||||
|
||||
var msg = [Types.update, myData.curvePublic, +new Date(), myData];
|
||||
var msgStr = JSON.stringify(msg);
|
||||
var cryptMsg = channel.encryptor.encrypt(msgStr);
|
||||
channel.wc.bcast(cryptMsg).then(function () {
|
||||
// TODO send event
|
||||
//channel.refresh();
|
||||
}, function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
eachHandler('update', function (f) {
|
||||
f(myData, myData.curvePublic);
|
||||
});
|
||||
friends.me = myData;
|
||||
}
|
||||
};
|
||||
|
||||
var onChannelReady = function (chanId) {
|
||||
var cb = joining[chanId];
|
||||
if (typeof(cb) !== 'function') {
|
||||
return void console.error('channel ready without callback');
|
||||
}
|
||||
delete joining[chanId];
|
||||
return cb();
|
||||
};
|
||||
|
||||
var onDirectMessage = function (common, msg, sender) {
|
||||
if (sender !== Msg.hk) { return void onIdMessage(msg, sender); }
|
||||
var parsed = JSON.parse(msg);
|
||||
|
||||
if (/HISTORY_RANGE/.test(parsed[0])) {
|
||||
//console.log(parsed);
|
||||
var txid = parsed[1];
|
||||
var req = getRangeRequest(txid);
|
||||
var type = parsed[0];
|
||||
if (!req) {
|
||||
return void console.error("received response to unknown request");
|
||||
}
|
||||
|
||||
if (type === 'HISTORY_RANGE') {
|
||||
req.messages.push(parsed[2]);
|
||||
} else if (type === 'HISTORY_RANGE_END') {
|
||||
// process all the messages (decrypt)
|
||||
var curvePublic = req.curvePublic;
|
||||
var channel = getChannel(curvePublic);
|
||||
|
||||
var decrypted = req.messages.map(function (msg) {
|
||||
if (msg[2] !== 'MSG') { return; }
|
||||
try {
|
||||
return {
|
||||
d: JSON.parse(channel.encryptor.decrypt(msg[4])),
|
||||
sig: msg[4].slice(0, 64),
|
||||
};
|
||||
} catch (e) {
|
||||
console.log('failed to decrypt');
|
||||
return null;
|
||||
}
|
||||
}).filter(function (decrypted) {
|
||||
return decrypted;
|
||||
}).map(function (O) {
|
||||
return {
|
||||
type: O.d[0],
|
||||
sig: O.sig,
|
||||
author: O.d[1],
|
||||
time: O.d[2],
|
||||
text: O.d[3],
|
||||
curve: curvePublic,
|
||||
};
|
||||
});
|
||||
|
||||
orderMessages(curvePublic, decrypted, req.sig);
|
||||
return void req.cb(void 0, decrypted);
|
||||
} else {
|
||||
console.log(parsed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ((parsed.validateKey || parsed.owners) && parsed.channel) {
|
||||
return;
|
||||
}
|
||||
if (parsed.state && parsed.state === 1 && parsed.channel) {
|
||||
if (channels[parsed.channel]) {
|
||||
// parsed.channel is Ready
|
||||
// channel[parsed.channel].ready();
|
||||
channels[parsed.channel].ready = true;
|
||||
onChannelReady(parsed.channel);
|
||||
var updateTypes = channels[parsed.channel].updateOnReady;
|
||||
if (updateTypes) {
|
||||
|
||||
//channels[parsed.channel].updateUI(updateTypes);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
var chan = parsed[3];
|
||||
if (!chan || !channels[chan]) { return; }
|
||||
pushMsg(channels[chan], parsed[4]);
|
||||
};
|
||||
|
||||
var onMessage = function (msg, sender, chan) {
|
||||
if (!channels[chan.id]) { return; }
|
||||
|
||||
var isMessage = pushMsg(channels[chan.id], msg);
|
||||
if (isMessage) {
|
||||
if (channels[chan.id].wc.myID !== sender) {
|
||||
// Don't notify for your own messages
|
||||
//channels[chan.id].notify();
|
||||
}
|
||||
//channels[chan.id].refresh();
|
||||
// TODO emit message event
|
||||
}
|
||||
};
|
||||
|
||||
// listen for messages...
|
||||
network.on('message', function(msg, sender) {
|
||||
onDirectMessage(common, msg, sender);
|
||||
});
|
||||
|
||||
messenger.removeFriend = function (curvePublic, cb) {
|
||||
if (typeof(cb) !== 'function') { throw new Error('NO_CALLBACK'); }
|
||||
var data = getFriend(proxy, curvePublic);
|
||||
|
||||
if (!data) {
|
||||
// friend is not valid
|
||||
console.error('friend is not valid');
|
||||
return;
|
||||
}
|
||||
|
||||
var channel = channels[data.channel];
|
||||
if (!channel) {
|
||||
return void cb("NO_SUCH_CHANNEL");
|
||||
}
|
||||
|
||||
if (!network.webChannels.some(function (wc) {
|
||||
return wc.id === channel.id;
|
||||
})) {
|
||||
console.error('bad channel: ', curvePublic);
|
||||
}
|
||||
|
||||
var msg = [Types.unfriend, proxy.curvePublic, +new Date()];
|
||||
var msgStr = JSON.stringify(msg);
|
||||
var cryptMsg = channel.encryptor.encrypt(msgStr);
|
||||
|
||||
// TODO emit remove_friend event?
|
||||
try {
|
||||
channel.wc.bcast(cryptMsg).then(function () {
|
||||
delete friends[curvePublic];
|
||||
delete channels[curvePublic];
|
||||
Realtime.whenRealtimeSyncs(common, realtime, function () {
|
||||
cb();
|
||||
});
|
||||
}, function (err) {
|
||||
console.error(err);
|
||||
cb(err);
|
||||
});
|
||||
} catch (e) {
|
||||
cb(e);
|
||||
}
|
||||
};
|
||||
|
||||
var getChannelMessagesSince = function (chan, data, keys) {
|
||||
console.log('Fetching [%s] messages since [%s]', data.curvePublic, data.lastKnownHash || '');
|
||||
var cfg = {
|
||||
validateKey: keys.validateKey,
|
||||
owners: [proxy.edPublic, data.edPublic],
|
||||
lastKnownHash: data.lastKnownHash
|
||||
};
|
||||
var msg = ['GET_HISTORY', chan.id, cfg];
|
||||
network.sendto(network.historyKeeper, JSON.stringify(msg))
|
||||
.then($.noop, function (err) {
|
||||
throw new Error(err);
|
||||
});
|
||||
};
|
||||
|
||||
var openFriendChannel = function (data, f) {
|
||||
var keys = Curve.deriveKeys(data.curvePublic, proxy.curvePrivate);
|
||||
var encryptor = Curve.createEncryptor(keys);
|
||||
network.join(data.channel).then(function (chan) {
|
||||
var channel = channels[data.channel] = {
|
||||
id: data.channel,
|
||||
sending: false,
|
||||
friendEd: f,
|
||||
keys: keys,
|
||||
curve: data.curvePublic,
|
||||
encryptor: encryptor,
|
||||
messages: [],
|
||||
wc: chan,
|
||||
userList: [],
|
||||
mapId: {},
|
||||
send: function (payload, cb) {
|
||||
if (!network.webChannels.some(function (wc) {
|
||||
if (wc.id === channel.wc.id) { return true; }
|
||||
})) {
|
||||
return void cb('NO_SUCH_CHANNEL');
|
||||
}
|
||||
|
||||
var msg = [Types.message, proxy.curvePublic, +new Date(), payload];
|
||||
var msgStr = JSON.stringify(msg);
|
||||
var cryptMsg = channel.encryptor.encrypt(msgStr);
|
||||
|
||||
channel.wc.bcast(cryptMsg).then(function () {
|
||||
pushMsg(channel, cryptMsg);
|
||||
cb();
|
||||
}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
chan.on('message', function (msg, sender) {
|
||||
onMessage(msg, sender, chan);
|
||||
});
|
||||
|
||||
var onJoining = function (peer) {
|
||||
if (peer === Msg.hk) { return; }
|
||||
if (channel.userList.indexOf(peer) !== -1) { return; }
|
||||
|
||||
channel.userList.push(peer);
|
||||
var msg = [Types.mapId, proxy.curvePublic, chan.myID];
|
||||
var msgStr = JSON.stringify(msg);
|
||||
var cryptMsg = channel.encryptor.encrypt(msgStr);
|
||||
network.sendto(peer, cryptMsg);
|
||||
};
|
||||
chan.members.forEach(function (peer) {
|
||||
if (peer === Msg.hk) { return; }
|
||||
if (channel.userList.indexOf(peer) !== -1) { return; }
|
||||
channel.userList.push(peer);
|
||||
});
|
||||
chan.on('join', onJoining);
|
||||
chan.on('leave', function (peer) {
|
||||
var curvePublic = channel.mapId[peer];
|
||||
var i = channel.userList.indexOf(peer);
|
||||
while (i !== -1) {
|
||||
channel.userList.splice(i, 1);
|
||||
i = channel.userList.indexOf(peer);
|
||||
}
|
||||
// update status
|
||||
if (!curvePublic) { return; }
|
||||
eachHandler('leave', function (f) {
|
||||
f(curvePublic, channel.id);
|
||||
});
|
||||
});
|
||||
|
||||
// FIXME don't subscribe to the channel implicitly
|
||||
getChannelMessagesSince(chan, data, keys);
|
||||
}, function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
messenger.getFriendList = function (cb) {
|
||||
var friends = proxy.friends;
|
||||
if (!friends) { return void cb(void 0, []); }
|
||||
|
||||
cb(void 0, Object.keys(proxy.friends).filter(function (k) {
|
||||
return k !== 'me';
|
||||
}));
|
||||
};
|
||||
|
||||
messenger.openFriendChannel = function (curvePublic, cb) {
|
||||
if (typeof(curvePublic) !== 'string') { return void cb('INVALID_ID'); }
|
||||
if (typeof(cb) !== 'function') { throw new Error('expected callback'); }
|
||||
|
||||
var friend = clone(friends[curvePublic]);
|
||||
if (typeof(friend) !== 'object') {
|
||||
return void cb('NO_FRIEND_DATA');
|
||||
}
|
||||
var channel = friend.channel;
|
||||
if (!channel) { return void cb('E_NO_CHANNEL'); }
|
||||
joining[channel] = cb;
|
||||
openFriendChannel(friend, curvePublic);
|
||||
};
|
||||
|
||||
messenger.sendMessage = function (curvePublic, payload, cb) {
|
||||
var channel = getChannel(curvePublic);
|
||||
if (!channel) { return void cb('NO_CHANNEL'); }
|
||||
if (!network.webChannels.some(function (wc) {
|
||||
if (wc.id === channel.wc.id) { return true; }
|
||||
})) {
|
||||
return void cb('NO_SUCH_CHANNEL');
|
||||
}
|
||||
|
||||
var msg = [Types.message, proxy.curvePublic, +new Date(), payload];
|
||||
var msgStr = JSON.stringify(msg);
|
||||
var cryptMsg = channel.encryptor.encrypt(msgStr);
|
||||
|
||||
channel.wc.bcast(cryptMsg).then(function () {
|
||||
pushMsg(channel, cryptMsg);
|
||||
cb();
|
||||
}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
};
|
||||
|
||||
messenger.getStatus = function (curvePublic, cb) {
|
||||
var channel = getChannel(curvePublic);
|
||||
if (!channel) { return void cb('NO_SUCH_CHANNEL'); }
|
||||
var online = channel.userList.some(function (nId) {
|
||||
return channel.mapId[nId] === curvePublic;
|
||||
});
|
||||
cb(void 0, online);
|
||||
};
|
||||
|
||||
messenger.getFriendInfo = function (curvePublic, cb) {
|
||||
setTimeout(function () {
|
||||
var friend = friends[curvePublic];
|
||||
if (!friend) { return void cb('NO_SUCH_FRIEND'); }
|
||||
// this clone will be redundant when ui uses postmessage
|
||||
cb(void 0, clone(friend));
|
||||
});
|
||||
};
|
||||
|
||||
messenger.getMyInfo = function (cb) {
|
||||
cb(void 0, {
|
||||
curvePublic: proxy.curvePublic,
|
||||
displayName: common.getDisplayName(),
|
||||
});
|
||||
};
|
||||
|
||||
// TODO listen for changes to your friend list
|
||||
// emit 'update' events for clients
|
||||
|
||||
//var update = function (curvePublic
|
||||
proxy.on('change', ['friends'], function (o, n, p) {
|
||||
var curvePublic;
|
||||
if (o === undefined) {
|
||||
// new friend added
|
||||
curvePublic = p.slice(-1)[0];
|
||||
eachHandler('friend', function (f) {
|
||||
f(curvePublic, clone(n));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(o, n, p);
|
||||
}).on('remove', ['friends'], function (o, p) {
|
||||
eachHandler('unfriend', function (f) {
|
||||
f(p[1]); // TODO
|
||||
});
|
||||
});
|
||||
|
||||
Object.freeze(messenger);
|
||||
|
||||
return messenger;
|
||||
};
|
||||
|
||||
return Msg;
|
||||
});
|
||||
@@ -8,35 +8,55 @@ define([
|
||||
var BAD_STATE_TIMEOUT = typeof(AppConfig.badStateTimeout) === 'number'?
|
||||
AppConfig.badStateTimeout: 30000;
|
||||
|
||||
var connected = false;
|
||||
var intr;
|
||||
var infiniteSpinnerHandlers = [];
|
||||
|
||||
/*
|
||||
TODO make this not blow up when disconnected or lagging...
|
||||
*/
|
||||
common.whenRealtimeSyncs = function (realtime, cb) {
|
||||
realtime.sync();
|
||||
common.whenRealtimeSyncs = function (Cryptpad, realtime, cb) {
|
||||
if (typeof(realtime.getAuthDoc) !== 'function') {
|
||||
return void console.error('improper use of this function');
|
||||
}
|
||||
|
||||
window.setTimeout(function () {
|
||||
if (realtime.getAuthDoc() === realtime.getUserDoc()) {
|
||||
return void cb();
|
||||
} else {
|
||||
realtime.onSettle(cb);
|
||||
}
|
||||
|
||||
var to = setTimeout(function () {
|
||||
if (intr) { return; }
|
||||
intr = window.setInterval(function () {
|
||||
var l;
|
||||
try {
|
||||
l = realtime.getLag();
|
||||
} catch (e) {
|
||||
throw new Error("ChainPad.getLag() does not exist, please `bower update`");
|
||||
}
|
||||
if (l.lag < BAD_STATE_TIMEOUT || !connected) { return; }
|
||||
realtime.abort();
|
||||
// don't launch more than one popup
|
||||
if (common.infiniteSpinnerDetected) { return; }
|
||||
infiniteSpinnerHandlers.forEach(function (ish) { ish(); });
|
||||
|
||||
// inform the user their session is in a bad state
|
||||
common.confirm(Messages.realtime_unrecoverableError, function (yes) {
|
||||
Cryptpad.confirm(Messages.realtime_unrecoverableError, function (yes) {
|
||||
if (!yes) { return; }
|
||||
window.location.reload();
|
||||
window.parent.location.reload();
|
||||
});
|
||||
common.infiniteSpinnerDetected = true;
|
||||
}, BAD_STATE_TIMEOUT);
|
||||
realtime.onSettle(function () {
|
||||
clearTimeout(to);
|
||||
cb();
|
||||
});
|
||||
}, 2000);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
common.onInfiniteSpinner = function (f) { infiniteSpinnerHandlers.push(f); };
|
||||
|
||||
common.setConnectionState = function (bool) {
|
||||
if (typeof(bool) !== 'boolean') { return; }
|
||||
connected = bool;
|
||||
};
|
||||
|
||||
return common;
|
||||
});
|
||||
|
||||
51
www/common/common-thumbnail.js
Normal file
51
www/common/common-thumbnail.js
Normal file
@@ -0,0 +1,51 @@
|
||||
define([
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
], function () {
|
||||
var Nacl = window.nacl;
|
||||
var Thumb = {
|
||||
dimension: 150, // thumbnails are all 150px
|
||||
};
|
||||
|
||||
// create thumbnail image from metadata
|
||||
// return an img tag, or undefined if anything goes wrong
|
||||
Thumb.fromMetadata = function (metadata) {
|
||||
if (!metadata || typeof(metadata) !== 'object' || !metadata.thumbnail) { return; }
|
||||
try {
|
||||
var u8 = Nacl.util.decodeBase64(metadata.thumbnail);
|
||||
var blob = new Blob([u8], {
|
||||
type: 'image/png'
|
||||
});
|
||||
var url = URL.createObjectURL(blob);
|
||||
var img = new Image();
|
||||
img.src = url;
|
||||
img.width = Thumb.dimension;
|
||||
img.height = Thumb.dimension;
|
||||
return img;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// assumes that your canvas is square
|
||||
// nodeback returning blob
|
||||
Thumb.fromCanvas = function (canvas, cb) {
|
||||
canvas = canvas;
|
||||
var c2 = document.createElement('canvas');
|
||||
var d = Thumb.dimension;
|
||||
c2.width = d;
|
||||
c2.height = 2;
|
||||
|
||||
var ctx = c2.getContext('2d');
|
||||
ctx.drawImage(canvas, 0, 0, d, d);
|
||||
c2.toBlob(function (blob) {
|
||||
cb(void 0, blob);
|
||||
});
|
||||
};
|
||||
|
||||
Thumb.fromVideo = function (video, cb) {
|
||||
cb = cb; // WIP
|
||||
};
|
||||
|
||||
return Thumb;
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
define(function () {
|
||||
define(['json.sortify'], function (Sortify) {
|
||||
var module = {};
|
||||
|
||||
module.create = function (info, onLocal, Cryptget, Cryptpad) {
|
||||
@@ -15,6 +15,7 @@ define(function () {
|
||||
var parsed = Cryptpad.parsePadUrl(window.location.href);
|
||||
var appType = parsed ? parsed.type : undefined;
|
||||
|
||||
var oldUserData = {};
|
||||
var addToUserData = exp.addToUserData = function(data) {
|
||||
var users = userList.users;
|
||||
for (var attrname in data) { userData[attrname] = data[attrname]; }
|
||||
@@ -28,6 +29,10 @@ define(function () {
|
||||
}
|
||||
|
||||
if(userList && typeof userList.onChange === "function") {
|
||||
// Make sure we don't update the userlist everytime someone makes a change to the pad
|
||||
if (Sortify(oldUserData) === Sortify(userData)) { return; }
|
||||
oldUserData = JSON.parse(JSON.stringify(userData));
|
||||
|
||||
userList.onChange(userData);
|
||||
}
|
||||
};
|
||||
|
||||
112
www/common/common-userlist2.js
Normal file
112
www/common/common-userlist2.js
Normal file
@@ -0,0 +1,112 @@
|
||||
define(function () {
|
||||
var module = {};
|
||||
|
||||
module.create = function (info, onLocal, Cryptget, Cryptpad) {
|
||||
var exp = {};
|
||||
|
||||
var userData = exp.userData = {};
|
||||
var userList = exp.userList = info.userList;
|
||||
var myData = exp.myData = {};
|
||||
exp.myUserName = info.myID;
|
||||
exp.myNetfluxId = info.myID;
|
||||
|
||||
var network = Cryptpad.getNetwork();
|
||||
|
||||
var parsed = Cryptpad.parsePadUrl(window.location.href);
|
||||
var appType = parsed ? parsed.type : undefined;
|
||||
|
||||
var addToUserData = exp.addToUserData = function(data) {
|
||||
var users = userList.users;
|
||||
for (var attrname in data) { userData[attrname] = data[attrname]; }
|
||||
|
||||
if (users && users.length) {
|
||||
for (var userKey in userData) {
|
||||
if (users.indexOf(userKey) === -1) {
|
||||
delete userData[userKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(userList && typeof userList.onChange === "function") {
|
||||
userList.onChange(userData);
|
||||
}
|
||||
};
|
||||
|
||||
exp.getToolbarConfig = function () {
|
||||
return {
|
||||
data: userData,
|
||||
list: userList,
|
||||
userNetfluxId: exp.myNetfluxId
|
||||
};
|
||||
};
|
||||
|
||||
var setName = exp.setName = function (newName, cb) {
|
||||
if (typeof(newName) !== 'string') { return; }
|
||||
var myUserNameTemp = newName.trim();
|
||||
if(myUserNameTemp.length > 32) {
|
||||
myUserNameTemp = myUserNameTemp.substr(0, 32);
|
||||
}
|
||||
exp.myUserName = myUserNameTemp;
|
||||
myData = {};
|
||||
myData[exp.myNetfluxId] = {
|
||||
name: exp.myUserName,
|
||||
uid: Cryptpad.getUid(),
|
||||
avatar: Cryptpad.getAvatarUrl(),
|
||||
profile: Cryptpad.getProfileUrl(),
|
||||
curvePublic: Cryptpad.getProxy().curvePublic
|
||||
};
|
||||
addToUserData(myData);
|
||||
/*Cryptpad.setAttribute('username', exp.myUserName, function (err) {
|
||||
if (err) {
|
||||
console.log("Couldn't set username");
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
if (typeof cb === "function") { cb(); }
|
||||
});*/
|
||||
if (typeof cb === "function") { cb(); }
|
||||
};
|
||||
|
||||
exp.getLastName = function ($changeNameButton, isNew) {
|
||||
Cryptpad.getLastName(function (err, lastName) {
|
||||
if (err) {
|
||||
console.log("Could not get previous name");
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
// Update the toolbar list:
|
||||
// Add the current user in the metadata
|
||||
if (typeof(lastName) === 'string') {
|
||||
setName(lastName, onLocal);
|
||||
} else {
|
||||
myData[exp.myNetfluxId] = {
|
||||
name: "",
|
||||
uid: Cryptpad.getUid(),
|
||||
avatar: Cryptpad.getAvatarUrl(),
|
||||
profile: Cryptpad.getProfileUrl(),
|
||||
curvePublic: Cryptpad.getProxy().curvePublic
|
||||
};
|
||||
addToUserData(myData);
|
||||
onLocal();
|
||||
$changeNameButton.click();
|
||||
}
|
||||
if (isNew && appType) {
|
||||
Cryptpad.selectTemplate(appType, info.realtime, Cryptget);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Cryptpad.onDisplayNameChanged(function (newName) {
|
||||
setName(newName, onLocal);
|
||||
});
|
||||
|
||||
network.on('reconnect', function (uid) {
|
||||
exp.myNetfluxId = uid;
|
||||
exp.setName(exp.myUserName);
|
||||
});
|
||||
|
||||
return exp;
|
||||
};
|
||||
|
||||
return module;
|
||||
});
|
||||
@@ -7,6 +7,11 @@ define([], function () {
|
||||
}, map));
|
||||
};
|
||||
|
||||
Util.uid = function () {
|
||||
return Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))
|
||||
.toString(32).replace(/\./g, '');
|
||||
};
|
||||
|
||||
Util.fixHTML = function (str) {
|
||||
if (!str) { return ''; }
|
||||
return str.replace(/[<>&"']/g, function (x) {
|
||||
|
||||
@@ -98,6 +98,7 @@ define([
|
||||
common.createRandomInteger = Util.createRandomInteger;
|
||||
common.getAppType = Util.getAppType;
|
||||
common.notAgainForAnother = Util.notAgainForAnother;
|
||||
common.uid = Util.uid;
|
||||
|
||||
// import hash utilities for export
|
||||
var createRandomHash = common.createRandomHash = Hash.createRandomHash;
|
||||
@@ -121,7 +122,6 @@ define([
|
||||
common.createInviteUrl = Hash.createInviteUrl;
|
||||
|
||||
// Messaging
|
||||
common.initMessaging = Messaging.init;
|
||||
common.addDirectMessageHandler = Messaging.addDirectMessageHandler;
|
||||
common.inviteFromUserlist = Messaging.inviteFromUserlist;
|
||||
common.getFriendList = Messaging.getFriendList;
|
||||
@@ -129,10 +129,11 @@ define([
|
||||
common.createData = Messaging.createData;
|
||||
common.getPendingInvites = Messaging.getPending;
|
||||
common.getLatestMessages = Messaging.getLatestMessages;
|
||||
common.initMessagingUI = Messaging.UI.init;
|
||||
|
||||
// Realtime
|
||||
var whenRealtimeSyncs = common.whenRealtimeSyncs = Realtime.whenRealtimeSyncs;
|
||||
var whenRealtimeSyncs = common.whenRealtimeSyncs = function (realtime, cb) {
|
||||
Realtime.whenRealtimeSyncs(common, realtime, cb);
|
||||
};
|
||||
|
||||
// Userlist
|
||||
common.createUserList = UserList.create;
|
||||
@@ -198,15 +199,28 @@ define([
|
||||
}
|
||||
return '';
|
||||
};
|
||||
common.getAccountName = function () {
|
||||
return localStorage[common.userNameKey];
|
||||
};
|
||||
|
||||
var randomToken = function () {
|
||||
return Math.random().toString(16).replace(/0./, '');
|
||||
};
|
||||
|
||||
common.isFeedbackAllowed = function () {
|
||||
try {
|
||||
if (!getStore().getProxy().proxy.allowUserFeedback) { return false; }
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
var feedback = common.feedback = function (action, force) {
|
||||
if (!action) { return; }
|
||||
if (force !== true) {
|
||||
if (!action) { return; }
|
||||
try {
|
||||
if (!getStore().getProxy().proxy.allowUserFeedback) { return; }
|
||||
if (!common.isFeedbackAllowed()) { return; }
|
||||
} catch (e) { return void console.error(e); }
|
||||
}
|
||||
|
||||
@@ -800,6 +814,9 @@ define([
|
||||
|
||||
common.pinPads = function (pads, cb) {
|
||||
if (!pinsReady()) { return void cb ('RPC_NOT_READY'); }
|
||||
if (typeof(cb) !== 'function') {
|
||||
console.error('expected a callback');
|
||||
}
|
||||
|
||||
rpc.pin(pads, function (e, hash) {
|
||||
if (e) { return void cb(e); }
|
||||
@@ -827,6 +844,13 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
// SFRAME: talk to anon_rpc from the iframe
|
||||
common.anonRpcMsg = function (msg, data, cb) {
|
||||
if (!msg) { return; }
|
||||
if (!anon_rpc) { return void cb('ANON_RPC_NOT_READY'); }
|
||||
anon_rpc.send(msg, data, cb);
|
||||
};
|
||||
|
||||
common.getFileSize = function (href, cb) {
|
||||
if (!anon_rpc) { return void cb('ANON_RPC_NOT_READY'); }
|
||||
//if (!pinsReady()) { return void cb('RPC_NOT_READY'); }
|
||||
@@ -1030,6 +1054,41 @@ define([
|
||||
};
|
||||
};
|
||||
|
||||
// Forget button
|
||||
var moveToTrash = common.moveToTrash = function (cb) {
|
||||
var href = window.location.href;
|
||||
common.forgetPad(href, function (err) {
|
||||
if (err) {
|
||||
console.log("unable to forget pad");
|
||||
console.error(err);
|
||||
cb(err, null);
|
||||
return;
|
||||
}
|
||||
var n = getNetwork();
|
||||
var r = getRealtime();
|
||||
if (n && r) {
|
||||
whenRealtimeSyncs(r, function () {
|
||||
n.disconnect();
|
||||
cb();
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
};
|
||||
var saveAsTemplate = common.saveAsTemplate = function (Cryptput, data, cb) {
|
||||
var p = parsePadUrl(window.location.href);
|
||||
if (!p.type) { return; }
|
||||
var hash = createRandomHash();
|
||||
var href = '/' + p.type + '/#' + hash;
|
||||
Cryptput(hash, data.toSave, function (e) {
|
||||
if (e) { throw new Error(e); }
|
||||
common.addTemplate(makePad(href, data.title));
|
||||
whenRealtimeSyncs(getStore().getProxy().info.realtime, function () {
|
||||
cb();
|
||||
});
|
||||
});
|
||||
};
|
||||
common.createButton = function (type, rightside, data, callback) {
|
||||
var button;
|
||||
var size = "17px";
|
||||
@@ -1118,17 +1177,12 @@ define([
|
||||
console.error("Parse error while setting the title", e);
|
||||
}
|
||||
}
|
||||
var p = parsePadUrl(window.location.href);
|
||||
if (!p.type) { return; }
|
||||
var hash = createRandomHash();
|
||||
var href = '/' + p.type + '/#' + hash;
|
||||
data.Crypt.put(hash, toSave, function (e) {
|
||||
if (e) { throw new Error(e); }
|
||||
common.addTemplate(makePad(href, title));
|
||||
whenRealtimeSyncs(getStore().getProxy().info.realtime, function () {
|
||||
common.alert(Messages.templateSaved);
|
||||
common.feedback('TEMPLATE_CREATED');
|
||||
});
|
||||
saveAsTemplate(data.Crypt.put, {
|
||||
title: title,
|
||||
toSave: toSave
|
||||
}, function () {
|
||||
common.alert(Messages.templateSaved);
|
||||
common.feedback('TEMPLATE_CREATED');
|
||||
});
|
||||
};
|
||||
common.prompt(Messages.saveTemplatePrompt, title || document.title, todo);
|
||||
@@ -1151,29 +1205,14 @@ define([
|
||||
button
|
||||
.click(prepareFeedback(type))
|
||||
.click(function() {
|
||||
var href = window.location.href;
|
||||
var msg = isLoggedIn() ? Messages.forgetPrompt : Messages.fm_removePermanentlyDialog;
|
||||
common.confirm(msg, function (yes) {
|
||||
if (!yes) { return; }
|
||||
common.forgetPad(href, function (err) {
|
||||
if (err) {
|
||||
console.log("unable to forget pad");
|
||||
console.error(err);
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
var n = getNetwork();
|
||||
var r = getRealtime();
|
||||
if (n && r) {
|
||||
whenRealtimeSyncs(r, function () {
|
||||
n.disconnect();
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
moveToTrash(function (err) {
|
||||
if (err) { return void callback(err); }
|
||||
var cMsg = isLoggedIn() ? Messages.movedToTrash : Messages.deleted;
|
||||
common.alert(cMsg, undefined, true);
|
||||
callback();
|
||||
return;
|
||||
});
|
||||
});
|
||||
@@ -1254,7 +1293,7 @@ define([
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
var getFirstEmojiOrCharacter = function (str) {
|
||||
var getFirstEmojiOrCharacter = common.getFirstEmojiOrCharacter = function (str) {
|
||||
if (!str || !str.trim()) { return '?'; }
|
||||
var emojis = emojiStringToArray(str);
|
||||
return isEmoji(emojis[0])? emojis[0]: str[0];
|
||||
@@ -1305,6 +1344,7 @@ define([
|
||||
'image/jpg',
|
||||
'image/gif',
|
||||
];
|
||||
// SFRAME: copied to sframe-common-interface.js
|
||||
common.displayAvatar = function ($container, href, name, cb) {
|
||||
var MutationObserver = window.MutationObserver;
|
||||
var displayDefault = function () {
|
||||
@@ -1391,23 +1431,37 @@ define([
|
||||
return $icon;
|
||||
};
|
||||
|
||||
common.createFileDialog = function (cfg) {
|
||||
common.createModal = function (cfg) {
|
||||
var $body = cfg.$body || $('body');
|
||||
var $blockContainer = $body.find('#fileDialog');
|
||||
var $blockContainer = $body.find('#'+cfg.id);
|
||||
if (!$blockContainer.length) {
|
||||
$blockContainer = $('<div>', {id: "fileDialog"}).appendTo($body);
|
||||
$blockContainer = $('<div>', {
|
||||
'class': 'cp-modal-container',
|
||||
'id': cfg.id
|
||||
});
|
||||
}
|
||||
$blockContainer.html('');
|
||||
$blockContainer.html('').appendTo($body);
|
||||
var $block = $('<div>', {'class': 'cp-modal'}).appendTo($blockContainer);
|
||||
$('<span>', {
|
||||
'class': 'close fa fa-times',
|
||||
'class': 'cp-modal-close fa fa-times',
|
||||
'title': Messages.filePicker_close
|
||||
}).click(function () {
|
||||
$blockContainer.hide();
|
||||
}).appendTo($block);
|
||||
$body.keydown(function (e) {
|
||||
if (e.which === 27) { $blockContainer.hide(); }
|
||||
});
|
||||
return $blockContainer;
|
||||
};
|
||||
common.createFileDialog = function (cfg) {
|
||||
var $blockContainer = common.createModal({
|
||||
id: 'fileDialog',
|
||||
$body: cfg.$body
|
||||
});
|
||||
var $block = $blockContainer.find('.cp-modal');
|
||||
var $description = $('<p>').text(Messages.filePicker_description);
|
||||
$block.append($description);
|
||||
var $filter = $('<p>', {'class': 'cp-form'}).appendTo($block);
|
||||
var $filter = $('<p>', {'class': 'cp-modal-form'}).appendTo($block);
|
||||
var $container = $('<span>', {'class': 'fileContainer'}).appendTo($block);
|
||||
var updateContainer = function () {
|
||||
$container.html('');
|
||||
@@ -1447,9 +1501,6 @@ define([
|
||||
$blockContainer.hide();
|
||||
}));
|
||||
updateContainer();
|
||||
$body.keydown(function (e) {
|
||||
if (e.which === 27) { $blockContainer.hide(); }
|
||||
});
|
||||
$blockContainer.show();
|
||||
};
|
||||
|
||||
@@ -1643,6 +1694,7 @@ define([
|
||||
return $block;
|
||||
};
|
||||
|
||||
// SFRAME: moved to sframe-common-interface.js
|
||||
common.createUserAdminMenu = function (config) {
|
||||
var $displayedName = $('<span>', {'class': config.displayNameCls || 'displayName'});
|
||||
var accountName = localStorage[common.userNameKey];
|
||||
@@ -1741,12 +1793,15 @@ define([
|
||||
};
|
||||
var $userAdmin = createDropdown(dropdownConfigUser);
|
||||
|
||||
var oldUrl;
|
||||
if (account && !config.static && store) {
|
||||
var $avatar = $userAdmin.find('.buttonTitle');
|
||||
var updateButton = function (newName) {
|
||||
var profile = store.getProfile();
|
||||
var url = profile && profile.avatar;
|
||||
|
||||
if (oldUrl === url) { return; }
|
||||
oldUrl = url;
|
||||
$avatar.html('');
|
||||
common.displayAvatar($avatar, url, newName, function ($img) {
|
||||
if ($img) {
|
||||
@@ -1792,6 +1847,32 @@ define([
|
||||
return $userAdmin;
|
||||
};
|
||||
|
||||
common.getShareHashes = function (secret, cb) {
|
||||
if (!window.location.hash) {
|
||||
var hashes = common.getHashes(secret.channel, secret);
|
||||
return void cb(null, hashes);
|
||||
}
|
||||
common.getRecentPads(function (err, recent) {
|
||||
var parsed = parsePadUrl(window.location.href);
|
||||
if (!parsed.type || !parsed.hashData) { return void cb('E_INVALID_HREF'); }
|
||||
var hashes = common.getHashes(secret.channel, secret);
|
||||
|
||||
if (!hashes.editHash && !hashes.viewHash && parsed.hashData && !parsed.hashData.mode) {
|
||||
// It means we're using an old hash
|
||||
hashes.editHash = window.location.hash.slice(1);
|
||||
}
|
||||
|
||||
// If we have a stronger version in drive, add it and add a redirect button
|
||||
var stronger = recent && common.findStronger(null, recent);
|
||||
if (stronger) {
|
||||
var parsed2 = parsePadUrl(stronger);
|
||||
hashes.editHash = parsed2.hash;
|
||||
}
|
||||
|
||||
cb(null, hashes);
|
||||
});
|
||||
};
|
||||
|
||||
var CRYPTPAD_VERSION = 'cryptpad-version';
|
||||
var updateLocalVersion = function () {
|
||||
// Check for CryptPad updates
|
||||
@@ -1847,14 +1928,27 @@ define([
|
||||
delete sessionStorage[newPadPathKey];
|
||||
}
|
||||
|
||||
common.onFriendRequest = function (confirmText, cb) {
|
||||
common.confirm(confirmText, cb, null, true);
|
||||
};
|
||||
common.onFriendComplete = function (data) {
|
||||
common.log(data.logText);
|
||||
};
|
||||
|
||||
Store.ready(function (err, storeObj) {
|
||||
store = common.store = env.store = storeObj;
|
||||
|
||||
common.addDirectMessageHandler(common);
|
||||
|
||||
var proxy = getProxy();
|
||||
var network = getNetwork();
|
||||
|
||||
network.on('disconnect', function () {
|
||||
Realtime.setConnectionState(false);
|
||||
});
|
||||
network.on('reconnect', function () {
|
||||
Realtime.setConnectionState(true);
|
||||
});
|
||||
|
||||
if (Object.keys(proxy).length === 1) {
|
||||
feedback("FIRST_APP_USE", true);
|
||||
}
|
||||
@@ -1863,10 +1957,15 @@ define([
|
||||
feedback("NO_PROXIES");
|
||||
}
|
||||
|
||||
if (/CRYPTPAD_SHIM/.test(Array.isArray.toString())) {
|
||||
var shimPattern = /CRYPTPAD_SHIM/;
|
||||
if (shimPattern.test(Array.isArray.toString())) {
|
||||
feedback("NO_ISARRAY");
|
||||
}
|
||||
|
||||
if (shimPattern.test(Array.prototype.fill.toString())) {
|
||||
feedback("NO_ARRAYFILL");
|
||||
}
|
||||
|
||||
common.reportScreenDimensions();
|
||||
common.reportLanguage();
|
||||
|
||||
|
||||
@@ -1,63 +1,8 @@
|
||||
@import (once) '../customize/src/less2/include/colortheme.less';
|
||||
@import '../customize/src/less2/include/modal.less';
|
||||
|
||||
#fileDialog {
|
||||
display: none;
|
||||
|
||||
z-index: 100000;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: @colortheme_modal-dim;
|
||||
|
||||
.cp-modal {
|
||||
background-color: @colortheme_modal-bg;
|
||||
color: @colortheme_modal-fg;
|
||||
box-shadow: @colortheme_modal-shadow;
|
||||
|
||||
padding: @colortheme_modal-padding;
|
||||
|
||||
position: absolute;
|
||||
top: 15vh; bottom: 15vh;
|
||||
left: 10vw; right: 10vw;
|
||||
|
||||
overflow: auto;
|
||||
|
||||
font-family: @colortheme_font;
|
||||
text-align: center;
|
||||
|
||||
& > p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.cp-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: @colortheme_modal-input;
|
||||
color: @colortheme_modal-fg;
|
||||
border: 0;
|
||||
padding: 8px 12px;
|
||||
margin: 1em;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.close {
|
||||
text-shadow: none;
|
||||
color: inherit;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin: @colortheme_modal-padding;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fileContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
24
www/common/loading.js
Normal file
24
www/common/loading.js
Normal file
@@ -0,0 +1,24 @@
|
||||
define([
|
||||
'less!/customize/src/less/loading.less'
|
||||
], function () {
|
||||
var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; });
|
||||
var elem = document.createElement('div');
|
||||
elem.setAttribute('id', 'loading');
|
||||
elem.innerHTML = [
|
||||
'<div class="loadingContainer">',
|
||||
'<img class="cryptofist" src="/customize/cryptpad-new-logo-colors-logoonly.png?' + urlArgs + '">',
|
||||
'<div class="spinnerContainer">',
|
||||
'<span class="fa fa-circle-o-notch fa-spin fa-4x fa-fw"></span>',
|
||||
'</div>',
|
||||
'<p id="cp-loading-message"></p>',
|
||||
'</div>'
|
||||
].join('');
|
||||
var intr;
|
||||
var append = function () {
|
||||
if (!document.body) { return; }
|
||||
clearInterval(intr);
|
||||
document.body.appendChild(elem);
|
||||
};
|
||||
intr = setInterval(append, 100);
|
||||
append();
|
||||
});
|
||||
1
www/common/media-tag-nacl.min.js
vendored
Normal file
1
www/common/media-tag-nacl.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
146
www/common/metadata-manager.js
Normal file
146
www/common/metadata-manager.js
Normal file
@@ -0,0 +1,146 @@
|
||||
define(['json.sortify'], function (Sortify) {
|
||||
var UNINIT = 'uninitialized';
|
||||
var create = function (sframeChan) {
|
||||
var meta = UNINIT;
|
||||
var members = [];
|
||||
var metadataObj = UNINIT;
|
||||
// This object reflects the metadata which is in the document at this moment.
|
||||
// Normally when a person leaves the pad, everybody sees them leave and updates
|
||||
// their metadata, this causes everyone to fight to change the document and
|
||||
// operational transform doesn't like it. So this is a lazy object which is
|
||||
// only updated either:
|
||||
// 1. On changes to the metadata that come in from someone else
|
||||
// 2. On changes connects, disconnects or changes to your own metadata
|
||||
var metadataLazyObj = UNINIT;
|
||||
var priv = {};
|
||||
var dirty = true;
|
||||
var changeHandlers = [];
|
||||
var lazyChangeHandlers = [];
|
||||
var titleChangeHandlers = [];
|
||||
|
||||
var rememberedTitle;
|
||||
|
||||
var checkUpdate = function (lazy) {
|
||||
if (!dirty) { return; }
|
||||
if (meta === UNINIT) { throw new Error(); }
|
||||
if (metadataObj === UNINIT) {
|
||||
metadataObj = {
|
||||
defaultTitle: meta.doc.defaultTitle,
|
||||
//title: meta.doc.defaultTitle,
|
||||
type: meta.doc.type,
|
||||
users: {}
|
||||
};
|
||||
metadataLazyObj = JSON.parse(JSON.stringify(metadataObj));
|
||||
}
|
||||
if (!metadataObj.users) { metadataObj.users = {}; }
|
||||
if (!metadataLazyObj.users) { metadataLazyObj.users = {}; }
|
||||
var mdo = {};
|
||||
// We don't want to add our user data to the object multiple times.
|
||||
//var containsYou = false;
|
||||
//console.log(metadataObj);
|
||||
Object.keys(metadataObj.users).forEach(function (x) {
|
||||
if (members.indexOf(x) === -1) { return; }
|
||||
mdo[x] = metadataObj.users[x];
|
||||
/*if (metadataObj.users[x].uid === meta.user.uid) {
|
||||
//console.log('document already contains you');
|
||||
containsYou = true;
|
||||
}*/
|
||||
});
|
||||
//if (!containsYou) { mdo[meta.user.netfluxId] = meta.user; }
|
||||
if (!priv.readOnly) {
|
||||
mdo[meta.user.netfluxId] = meta.user;
|
||||
}
|
||||
metadataObj.users = mdo;
|
||||
var lazyUserStr = JSON.stringify(metadataLazyObj.users[meta.user.netfluxId]);
|
||||
dirty = false;
|
||||
if (lazy || lazyUserStr !== JSON.stringify(meta.user)) {
|
||||
metadataLazyObj = JSON.parse(JSON.stringify(metadataObj));
|
||||
lazyChangeHandlers.forEach(function (f) { f(); });
|
||||
}
|
||||
|
||||
if (metadataObj.title !== rememberedTitle) {
|
||||
console.log("Title update\n" + metadataObj.title + '\n');
|
||||
rememberedTitle = metadataObj.title;
|
||||
titleChangeHandlers.forEach(function (f) { f(metadataObj.title); });
|
||||
}
|
||||
|
||||
changeHandlers.forEach(function (f) { f(); });
|
||||
};
|
||||
var change = function (lazy) {
|
||||
dirty = true;
|
||||
setTimeout(function () {
|
||||
checkUpdate(lazy);
|
||||
});
|
||||
};
|
||||
|
||||
sframeChan.on('EV_METADATA_UPDATE', function (ev) {
|
||||
meta = ev;
|
||||
if (ev.priv) {
|
||||
priv = ev.priv;
|
||||
}
|
||||
change(true);
|
||||
});
|
||||
sframeChan.on('EV_RT_CONNECT', function (ev) {
|
||||
meta.user.netfluxId = ev.myID;
|
||||
members = ev.members;
|
||||
change(true);
|
||||
});
|
||||
sframeChan.on('EV_RT_JOIN', function (ev) {
|
||||
members.push(ev);
|
||||
change(false);
|
||||
});
|
||||
sframeChan.on('EV_RT_LEAVE', function (ev) {
|
||||
var idx = members.indexOf(ev);
|
||||
if (idx === -1) { console.log('Error: ' + ev + ' not in members'); return; }
|
||||
members.splice(idx, 1);
|
||||
change(false);
|
||||
});
|
||||
sframeChan.on('EV_RT_DISCONNECT', function () {
|
||||
members = [];
|
||||
change(true);
|
||||
});
|
||||
|
||||
return Object.freeze({
|
||||
updateMetadata: function (m) {
|
||||
// JSON.parse(JSON.stringify()) reorders the json, so we have to use sortify even
|
||||
// if it's on our own computer
|
||||
if (Sortify(metadataLazyObj) === Sortify(m)) { return; }
|
||||
metadataObj = JSON.parse(JSON.stringify(m));
|
||||
metadataLazyObj = JSON.parse(JSON.stringify(m));
|
||||
change(false);
|
||||
},
|
||||
updateTitle: function (t) {
|
||||
metadataObj.title = t;
|
||||
change(true);
|
||||
},
|
||||
getMetadata: function () {
|
||||
checkUpdate(false);
|
||||
return Object.freeze(JSON.parse(JSON.stringify(metadataObj)));
|
||||
},
|
||||
getMetadataLazy: function () {
|
||||
return metadataLazyObj;
|
||||
},
|
||||
onTitleChange: function (f) { titleChangeHandlers.push(f); },
|
||||
onChange: function (f) { changeHandlers.push(f); },
|
||||
onChangeLazy: function (f) { lazyChangeHandlers.push(f); },
|
||||
isConnected : function () {
|
||||
return members.indexOf(meta.user.netfluxId) !== -1;
|
||||
},
|
||||
getViewers : function () {
|
||||
checkUpdate(false);
|
||||
var list = members.slice().filter(function (m) { return m.length === 32; });
|
||||
return list.length - Object.keys(metadataObj.users).length;
|
||||
},
|
||||
getPrivateData : function () {
|
||||
return priv;
|
||||
},
|
||||
getUserData : function () {
|
||||
return meta.user;
|
||||
},
|
||||
getNetfluxId : function () {
|
||||
return meta.user.netfluxId;
|
||||
}
|
||||
});
|
||||
};
|
||||
return Object.freeze({ create: create });
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
(function () {
|
||||
define(['/api/config'], function (ApiConfig) {
|
||||
var Module = {};
|
||||
|
||||
var isSupported = Module.isSupported = function () {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
};
|
||||
|
||||
var DEFAULT_MAIN = '/customize/main-favicon.png';
|
||||
var DEFAULT_ALT = '/customize/alt-favicon.png';
|
||||
var DEFAULT_MAIN = '/customize/main-favicon.png?' + ApiConfig.requireConf.urlArgs;
|
||||
var DEFAULT_ALT = '/customize/alt-favicon.png?' + ApiConfig.requireConf.urlArgs;
|
||||
|
||||
var createFavicon = function () {
|
||||
console.log("creating favicon");
|
||||
@@ -111,13 +111,5 @@
|
||||
};
|
||||
};
|
||||
|
||||
if (typeof(module) !== 'undefined' && module.exports) {
|
||||
module.exports = Module;
|
||||
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
|
||||
define(function () {
|
||||
return Module;
|
||||
});
|
||||
} else {
|
||||
window.Visible = Module;
|
||||
}
|
||||
}());
|
||||
return Module;
|
||||
});
|
||||
@@ -341,25 +341,14 @@ function isDataSchema(url) {
|
||||
return url.substr(i, 5).toLowerCase() === 'data:';
|
||||
}
|
||||
function getPDFFileNameFromURL(url) {
|
||||
var defaultFilename = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'document.pdf';
|
||||
|
||||
if (isDataSchema(url)) {
|
||||
console.warn('getPDFFileNameFromURL: ' + 'ignoring "data:" URL for performance reasons.');
|
||||
return defaultFilename;
|
||||
var query;
|
||||
var title;
|
||||
if (/\#/.test(url)) {
|
||||
url.replace(/\#(.*)$/, function (all, t) {
|
||||
title = t;
|
||||
});
|
||||
}
|
||||
var reURI = /^(?:(?:[^:]+:)?\/\/[^\/]+)?([^?#]*)(\?[^#]*)?(#.*)?$/;
|
||||
var reFilename = /[^\/?#=]+\.pdf\b(?!.*\.pdf\b)/i;
|
||||
var splitURI = reURI.exec(url);
|
||||
var suggestedFilename = reFilename.exec(splitURI[1]) || reFilename.exec(splitURI[2]) || reFilename.exec(splitURI[3]);
|
||||
if (suggestedFilename) {
|
||||
suggestedFilename = suggestedFilename[0];
|
||||
if (suggestedFilename.indexOf('%') !== -1) {
|
||||
try {
|
||||
suggestedFilename = reFilename.exec(decodeURIComponent(suggestedFilename))[0];
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
return suggestedFilename || defaultFilename;
|
||||
return title || 'document.pdf';
|
||||
}
|
||||
function normalizeWheelEventDelta(evt) {
|
||||
var delta = Math.sqrt(evt.deltaX * evt.deltaX + evt.deltaY * evt.deltaY);
|
||||
@@ -1192,11 +1181,13 @@ var PDFViewerApplication = {
|
||||
setTitleUsingUrl: function pdfViewSetTitleUsingUrl(url) {
|
||||
this.url = url;
|
||||
this.baseUrl = url.split('#')[0];
|
||||
var title = (0, _ui_utils.getPDFFileNameFromURL)(url, '');
|
||||
|
||||
var title = _ui_utils.getPDFFileNameFromURL(url);
|
||||
if (!title) {
|
||||
try {
|
||||
title = decodeURIComponent((0, _pdfjsLib.getFilenameFromUrl)(url)) || url;
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
title = url;
|
||||
}
|
||||
}
|
||||
|
||||
27
www/common/requireconfig.js
Normal file
27
www/common/requireconfig.js
Normal file
@@ -0,0 +1,27 @@
|
||||
define([
|
||||
'/api/config'
|
||||
], function (ApiConfig) {
|
||||
var out = {
|
||||
// fix up locations so that relative urls work.
|
||||
baseUrl: window.location.pathname,
|
||||
paths: {
|
||||
// jquery declares itself as literally "jquery" so it cannot be pulled by path :(
|
||||
"jquery": "/bower_components/jquery/dist/jquery.min",
|
||||
// json.sortify same
|
||||
"json.sortify": "/bower_components/json.sortify/dist/JSON.sortify",
|
||||
//"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf",
|
||||
//"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker"
|
||||
cm: '/bower_components/codemirror'
|
||||
},
|
||||
map: {
|
||||
'*': {
|
||||
'css': '/bower_components/require-css/css.js',
|
||||
'less': '/common/RequireLess.js',
|
||||
}
|
||||
}
|
||||
};
|
||||
Object.keys(ApiConfig.requireConf).forEach(function (k) { out[k] = ApiConfig.requireConf[k]; });
|
||||
return function () {
|
||||
return JSON.parse(JSON.stringify(out));
|
||||
};
|
||||
});
|
||||
@@ -1,13 +1,10 @@
|
||||
define([
|
||||
'/common/common-util.js',
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
], function () {
|
||||
], function (Util) {
|
||||
var Nacl = window.nacl;
|
||||
|
||||
var uid = function () {
|
||||
return Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER))
|
||||
.toString(32).replace(/\./g, '');
|
||||
};
|
||||
|
||||
var uid = Util.uid;
|
||||
var signMsg = function (data, signKey) {
|
||||
var buffer = Nacl.util.decodeUTF8(JSON.stringify(data));
|
||||
return Nacl.util.encodeBase64(Nacl.sign.detached(buffer, signKey));
|
||||
@@ -27,6 +24,10 @@ types of messages:
|
||||
var hkn = network.historyKeeper;
|
||||
var txid = uid();
|
||||
|
||||
if (typeof(cb) !== 'function') {
|
||||
return console.error('expected callback');
|
||||
}
|
||||
|
||||
var pending = ctx.pending[txid] = function (err, response) {
|
||||
cb(err, response);
|
||||
};
|
||||
|
||||
40
www/common/sframe-boot.js
Normal file
40
www/common/sframe-boot.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// Stage 0, this gets cached which means we can't change it. boot2-sframe.js is changable.
|
||||
// Note that this file is meant to be executed only inside of a sandbox iframe.
|
||||
;(function () {
|
||||
var afterLoaded = function (req) {
|
||||
req.cfg = req.cfg || {};
|
||||
if (req.pfx) {
|
||||
req.cfg.onNodeCreated = function (node /*, config, module, path*/) {
|
||||
node.setAttribute('src', req.pfx + node.getAttribute('src'));
|
||||
};
|
||||
}
|
||||
require.config(req.cfg);
|
||||
var txid = Math.random().toString(16).replace('0.', '');
|
||||
var intr;
|
||||
var ready = function () {
|
||||
intr = setInterval(function () {
|
||||
if (typeof(txid) !== 'string') { return; }
|
||||
window.parent.postMessage(JSON.stringify({ q: 'READY', txid: txid }), '*');
|
||||
}, 1);
|
||||
};
|
||||
if (req.req) { require(req.req, ready); } else { ready(); }
|
||||
var onReply = function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (data.txid !== txid) { return; }
|
||||
clearInterval(intr);
|
||||
txid = {};
|
||||
window.removeEventListener('message', onReply);
|
||||
require(['/common/sframe-boot2.js'], function () { });
|
||||
};
|
||||
window.addEventListener('message', onReply);
|
||||
};
|
||||
|
||||
var intr = setInterval(function () {
|
||||
try {
|
||||
var req = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
|
||||
clearInterval(intr);
|
||||
afterLoaded(req);
|
||||
} catch (e) { console.error(e); }
|
||||
}, 100);
|
||||
|
||||
}());
|
||||
35
www/common/sframe-boot2.js
Normal file
35
www/common/sframe-boot2.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// This is stage 1, it can be changed but you must bump the version of the project.
|
||||
// Note: This must only be loaded from inside of a sandbox-iframe.
|
||||
define(['/common/requireconfig.js'], function (RequireConfig) {
|
||||
require.config(RequireConfig());
|
||||
|
||||
// most of CryptPad breaks if you don't support isArray
|
||||
if (!Array.isArray) {
|
||||
Array.isArray = function(arg) { // CRYPTPAD_SHIM
|
||||
return Object.prototype.toString.call(arg) === '[object Array]';
|
||||
};
|
||||
}
|
||||
|
||||
// In the event that someone clicks a link in the iframe, it's going to cause the iframe
|
||||
// to navigate away from the pad which is going to be a mess. Instead we'll just reload
|
||||
// the top level and then it will be simply that a link doesn't work properly.
|
||||
window.onunload = function () {
|
||||
window.parent.location.reload();
|
||||
};
|
||||
|
||||
// Make sure anything which might have leaked to the localstorage is always cleaned up.
|
||||
try { window.localStorage.clear(); } catch (e) { }
|
||||
try { window.sessionStorage.clear(); } catch (e) { }
|
||||
|
||||
var mkFakeStore = function () {
|
||||
var fakeStorage = {
|
||||
getItem: function (k) { return fakeStorage[k]; },
|
||||
setItem: function (k, v) { fakeStorage[k] = v; return v; }
|
||||
};
|
||||
return fakeStorage;
|
||||
};
|
||||
window.__defineGetter__('localStorage', function () { return mkFakeStore(); });
|
||||
window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); });
|
||||
|
||||
require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]);
|
||||
});
|
||||
101
www/common/sframe-chainpad-netflux-inner.js
Normal file
101
www/common/sframe-chainpad-netflux-inner.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2014 XWiki SAS
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
define([
|
||||
'/bower_components/chainpad/chainpad.dist.js'
|
||||
], function () {
|
||||
var ChainPad = window.ChainPad;
|
||||
var module = { exports: {} };
|
||||
|
||||
var verbose = function (x) { console.log(x); };
|
||||
verbose = function () {}; // comment out to enable verbose logging
|
||||
|
||||
module.exports.start = function (config) {
|
||||
var onConnectionChange = config.onConnectionChange || function () { };
|
||||
var onRemote = config.onRemote || function () { };
|
||||
var onInit = config.onInit || function () { };
|
||||
var onLocal = config.onLocal || function () { };
|
||||
var setMyID = config.setMyID || function () { };
|
||||
var onReady = config.onReady || function () { };
|
||||
var userName = config.userName;
|
||||
var initialState = config.initialState;
|
||||
var transformFunction = config.transformFunction;
|
||||
var validateContent = config.validateContent;
|
||||
var avgSyncMilliseconds = config.avgSyncMilliseconds;
|
||||
var logLevel = typeof(config.logLevel) !== 'undefined'? config.logLevel : 1;
|
||||
var readOnly = config.readOnly || false;
|
||||
var sframeChan = config.sframeChan;
|
||||
var metadataMgr = config.metadataMgr;
|
||||
config = undefined;
|
||||
|
||||
var chainpad;
|
||||
var myID;
|
||||
var isReady = false;
|
||||
|
||||
sframeChan.on('EV_RT_DISCONNECT', function () {
|
||||
isReady = false;
|
||||
onConnectionChange({ state: false });
|
||||
});
|
||||
sframeChan.on('EV_RT_CONNECT', function (content) {
|
||||
//content.members.forEach(userList.onJoin);
|
||||
myID = content.myID;
|
||||
isReady = false;
|
||||
if (chainpad) {
|
||||
// it's a reconnect
|
||||
onConnectionChange({ state: true, myId: myID });
|
||||
return;
|
||||
}
|
||||
chainpad = ChainPad.create({
|
||||
userName: userName,
|
||||
initialState: initialState,
|
||||
transformFunction: transformFunction,
|
||||
validateContent: validateContent,
|
||||
avgSyncMilliseconds: avgSyncMilliseconds,
|
||||
logLevel: logLevel
|
||||
});
|
||||
chainpad.onMessage(function(message, cb) {
|
||||
sframeChan.query('Q_RT_MESSAGE', message, cb);
|
||||
});
|
||||
chainpad.onPatch(function () {
|
||||
onRemote({ realtime: chainpad });
|
||||
});
|
||||
onInit({
|
||||
myID: myID,
|
||||
realtime: chainpad,
|
||||
readOnly: readOnly
|
||||
});
|
||||
});
|
||||
sframeChan.on('Q_RT_MESSAGE', function (content, cb) {
|
||||
if (isReady) {
|
||||
onLocal(); // should be onBeforeMessage
|
||||
}
|
||||
chainpad.message(content);
|
||||
cb('OK');
|
||||
});
|
||||
sframeChan.on('EV_RT_READY', function () {
|
||||
if (isReady) { return; }
|
||||
isReady = true;
|
||||
chainpad.start();
|
||||
setMyID({ myID: myID });
|
||||
onReady({ realtime: chainpad });
|
||||
});
|
||||
return Object.freeze({
|
||||
getMyID: function () { return myID; },
|
||||
metadataMgr: metadataMgr
|
||||
});
|
||||
};
|
||||
return Object.freeze(module.exports);
|
||||
});
|
||||
246
www/common/sframe-chainpad-netflux-outer.js
Normal file
246
www/common/sframe-chainpad-netflux-outer.js
Normal file
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright 2014 XWiki SAS
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
define([], function () {
|
||||
var USE_HISTORY = true;
|
||||
|
||||
var verbose = function (x) { console.log(x); };
|
||||
verbose = function () {}; // comment out to enable verbose logging
|
||||
|
||||
var unBencode = function (str) { return str.replace(/^\d+:/, ''); };
|
||||
|
||||
var start = function (conf) {
|
||||
var channel = conf.channel;
|
||||
var Crypto = conf.crypto;
|
||||
var validateKey = conf.validateKey;
|
||||
var readOnly = conf.readOnly || false;
|
||||
var network = conf.network;
|
||||
var sframeChan = conf.sframeChan;
|
||||
var onConnect = conf.onConnect || function () { };
|
||||
conf = undefined;
|
||||
|
||||
var initializing = true;
|
||||
var lastKnownHash;
|
||||
|
||||
var queue = [];
|
||||
var messageFromInner = function (m, cb) { queue.push([ m, cb ]); };
|
||||
sframeChan.on('Q_RT_MESSAGE', function (message, cb) {
|
||||
messageFromInner(message, cb);
|
||||
});
|
||||
|
||||
var onReady = function () {
|
||||
// Trigger onReady only if not ready yet. This is important because the history keeper sends a direct
|
||||
// message through "network" when it is synced, and it triggers onReady for each channel joined.
|
||||
if (!initializing) { return; }
|
||||
sframeChan.event('EV_RT_READY', null);
|
||||
// we're fully synced
|
||||
initializing = false;
|
||||
};
|
||||
|
||||
// shim between chainpad and netflux
|
||||
var msgIn = function (peerId, msg) {
|
||||
msg = msg.replace(/^cp\|/, '');
|
||||
try {
|
||||
var decryptedMsg = Crypto.decrypt(msg, validateKey);
|
||||
return decryptedMsg;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
|
||||
var msgOut = function (msg) {
|
||||
if (readOnly) { return; }
|
||||
try {
|
||||
var cmsg = Crypto.encrypt(msg);
|
||||
if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; }
|
||||
return cmsg;
|
||||
} catch (err) {
|
||||
console.log(msg);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
var onMessage = function(peer, msg, wc, network, direct) {
|
||||
// unpack the history keeper from the webchannel
|
||||
var hk = network.historyKeeper;
|
||||
|
||||
if (direct && peer !== hk) {
|
||||
return;
|
||||
}
|
||||
if (direct) {
|
||||
var parsed = JSON.parse(msg);
|
||||
if (parsed.validateKey && parsed.channel) {
|
||||
if (parsed.channel === wc.id && !validateKey) {
|
||||
validateKey = parsed.validateKey;
|
||||
}
|
||||
// We have to return even if it is not the current channel:
|
||||
// we don't want to continue with other channels messages here
|
||||
return;
|
||||
}
|
||||
if (parsed.state && parsed.state === 1 && parsed.channel) {
|
||||
if (parsed.channel === wc.id) {
|
||||
onReady(wc);
|
||||
}
|
||||
// We have to return even if it is not the current channel:
|
||||
// we don't want to continue with other channels messages here
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The history keeper is different for each channel :
|
||||
// no need to check if the message is related to the current channel
|
||||
if (peer === hk) {
|
||||
// if the peer is the 'history keeper', extract their message
|
||||
var parsed1 = JSON.parse(msg);
|
||||
msg = parsed1[4];
|
||||
// Check that this is a message for us
|
||||
if (parsed1[3] !== wc.id) { return; }
|
||||
}
|
||||
|
||||
lastKnownHash = msg.slice(0,64);
|
||||
var message = msgIn(peer, msg);
|
||||
|
||||
verbose(message);
|
||||
|
||||
// slice off the bencoded header
|
||||
// Why are we getting bencoded stuff to begin with?
|
||||
// FIXME this shouldn't be necessary
|
||||
message = unBencode(message);//.slice(message.indexOf(':[') + 1);
|
||||
|
||||
// pass the message into Chainpad
|
||||
sframeChan.query('Q_RT_MESSAGE', message, function () { });
|
||||
};
|
||||
|
||||
// We use an object to store the webchannel so that we don't have to push new handlers to chainpad
|
||||
// and remove the old ones when reconnecting and keeping the same 'realtime' object
|
||||
// See realtime.onMessage below: we call wc.bcast(...) but wc may change
|
||||
var wcObject = {};
|
||||
var onOpen = function(wc, network, firstConnection) {
|
||||
wcObject.wc = wc;
|
||||
channel = wc.id;
|
||||
|
||||
onConnect(wc);
|
||||
onConnect = function () { };
|
||||
|
||||
// Add the existing peers in the userList
|
||||
sframeChan.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly });
|
||||
|
||||
// Add the handlers to the WebChannel
|
||||
wc.on('message', function (msg, sender) { //Channel msg
|
||||
onMessage(sender, msg, wc, network);
|
||||
});
|
||||
wc.on('join', function (m) { sframeChan.event('EV_RT_JOIN', m); });
|
||||
wc.on('leave', function (m) { sframeChan.event('EV_RT_LEAVE', m); });
|
||||
|
||||
if (firstConnection) {
|
||||
// Sending a message...
|
||||
messageFromInner = function(message, cb) {
|
||||
// Filter messages sent by Chainpad to make it compatible with Netflux
|
||||
message = msgOut(message);
|
||||
if (message) {
|
||||
// Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we
|
||||
// want to keep the same chainpad (realtime) object
|
||||
try {
|
||||
wcObject.wc.bcast(message).then(function() {
|
||||
cb();
|
||||
}, function(err) {
|
||||
// The message has not been sent, display the error.
|
||||
console.error(err);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
// Just skip calling back and it will fail on the inside.
|
||||
}
|
||||
}
|
||||
};
|
||||
queue.forEach(function (arr) { messageFromInner(arr[0], arr[1]); });
|
||||
}
|
||||
|
||||
// Get the channel history
|
||||
if (USE_HISTORY) {
|
||||
var hk;
|
||||
|
||||
wc.members.forEach(function (p) {
|
||||
if (p.length === 16) { hk = p; }
|
||||
});
|
||||
network.historyKeeper = hk;
|
||||
|
||||
var msg = ['GET_HISTORY', wc.id];
|
||||
// Add the validateKey if we are the channel creator and we have a validateKey
|
||||
msg.push(validateKey);
|
||||
msg.push(lastKnownHash);
|
||||
if (hk) { network.sendto(hk, JSON.stringify(msg)); }
|
||||
} else {
|
||||
onReady(wc);
|
||||
}
|
||||
};
|
||||
|
||||
var isIntentionallyLeaving = false;
|
||||
window.addEventListener("beforeunload", function () {
|
||||
isIntentionallyLeaving = true;
|
||||
});
|
||||
|
||||
var findChannelById = function (webChannels, channelId) {
|
||||
var webChannel;
|
||||
|
||||
// Array.some terminates once a truthy value is returned
|
||||
// best case is faster than forEach, though webchannel arrays seem
|
||||
// to consistently have a length of 1
|
||||
webChannels.some(function(chan) {
|
||||
if(chan.id === channelId) { webChannel = chan; return true;}
|
||||
});
|
||||
return webChannel;
|
||||
};
|
||||
|
||||
var connectTo = function (network, firstConnection) {
|
||||
// join the netflux network, promise to handle opening of the channel
|
||||
network.join(channel || null).then(function(wc) {
|
||||
onOpen(wc, network, firstConnection);
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
network.on('disconnect', function (reason) {
|
||||
console.log('disconnect');
|
||||
if (isIntentionallyLeaving) { return; }
|
||||
if (reason === "network.disconnect() called") { return; }
|
||||
sframeChan.event('EV_RT_DISCONNECT');
|
||||
});
|
||||
|
||||
network.on('reconnect', function () {
|
||||
initializing = true;
|
||||
connectTo(network, false);
|
||||
});
|
||||
|
||||
network.on('message', function (msg, sender) { // Direct message
|
||||
var wchan = findChannelById(network.webChannels, channel);
|
||||
if (wchan) {
|
||||
onMessage(sender, msg, wchan, network, true);
|
||||
}
|
||||
});
|
||||
|
||||
connectTo(network, true);
|
||||
};
|
||||
|
||||
return {
|
||||
start: function (config) {
|
||||
config.sframeChan.whenReg('EV_RT_READY', function () {
|
||||
start(config);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
141
www/common/sframe-channel.js
Normal file
141
www/common/sframe-channel.js
Normal file
@@ -0,0 +1,141 @@
|
||||
// This file provides the API for the channel for talking to and from the sandbox iframe.
|
||||
define([
|
||||
'/common/sframe-protocol.js'
|
||||
], function (SFrameProtocol) {
|
||||
|
||||
var mkTxid = function () {
|
||||
return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', '');
|
||||
};
|
||||
|
||||
var create = function (ow, cb, isSandbox) {
|
||||
var otherWindow;
|
||||
var handlers = {};
|
||||
var queries = {};
|
||||
|
||||
// list of handlers which are registered from the other side...
|
||||
var insideHandlers = [];
|
||||
var callWhenRegistered = {};
|
||||
|
||||
var chan = {};
|
||||
|
||||
// Send a query. channel.query('Q_SOMETHING', { args: "whatever" }, function (reply) { ... });
|
||||
chan.query = function (q, content, cb) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[q]) {
|
||||
throw new Error('please only make queries are defined in sframe-protocol.js');
|
||||
}
|
||||
var txid = mkTxid();
|
||||
var timeout = setTimeout(function () {
|
||||
delete queries[txid];
|
||||
console.log("Timeout making query " + q);
|
||||
}, 30000);
|
||||
queries[txid] = function (data, msg) {
|
||||
clearTimeout(timeout);
|
||||
delete queries[txid];
|
||||
cb(undefined, data.content, msg);
|
||||
};
|
||||
otherWindow.postMessage(JSON.stringify({
|
||||
txid: txid,
|
||||
content: content,
|
||||
q: q
|
||||
}), '*');
|
||||
};
|
||||
|
||||
// Fire an event. channel.event('EV_SOMETHING', { args: "whatever" });
|
||||
var event = chan.event = function (e, content) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[e]) {
|
||||
throw new Error('please only fire events that are defined in sframe-protocol.js');
|
||||
}
|
||||
if (e.indexOf('EV_') !== 0) {
|
||||
throw new Error('please only use events (starting with EV_) for event messages');
|
||||
}
|
||||
otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*');
|
||||
};
|
||||
|
||||
// Be notified on query or event. channel.on('EV_SOMETHING', function (args, reply) { ... });
|
||||
// If the type is a query, your handler will be invoked with a reply function that takes
|
||||
// one argument (the content to reply with).
|
||||
chan.on = function (queryType, handler, quiet) {
|
||||
if (!otherWindow && !quiet) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[queryType]) {
|
||||
throw new Error('please only register handlers which are defined in sframe-protocol.js');
|
||||
}
|
||||
(handlers[queryType] = handlers[queryType] || []).push(function (data, msg) {
|
||||
handler(data.content, function (replyContent) {
|
||||
if (queryType.indexOf('Q_') !== 0) { throw new Error("replies to events are invalid"); }
|
||||
msg.source.postMessage(JSON.stringify({
|
||||
txid: data.txid,
|
||||
content: replyContent
|
||||
}), '*');
|
||||
}, msg);
|
||||
});
|
||||
if (!quiet) {
|
||||
event('EV_REGISTER_HANDLER', queryType);
|
||||
}
|
||||
};
|
||||
|
||||
// If a particular handler is registered, call the callback immediately, otherwise it will be called
|
||||
// when that handler is first registered.
|
||||
// channel.whenReg('Q_SOMETHING', function () { ...query Q_SOMETHING?... });
|
||||
chan.whenReg = function (queryType, cb, always) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[queryType]) {
|
||||
throw new Error('please only register handlers which are defined in sframe-protocol.js');
|
||||
}
|
||||
var reg = always;
|
||||
if (insideHandlers.indexOf(queryType) > -1) {
|
||||
cb();
|
||||
} else {
|
||||
reg = true;
|
||||
}
|
||||
if (reg) {
|
||||
(callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(cb);
|
||||
}
|
||||
};
|
||||
|
||||
// Same as whenReg except it will invoke every time there is another registration, not just once.
|
||||
chan.onReg = function (queryType, cb) { chan.whenReg(queryType, cb, true); };
|
||||
|
||||
chan.on('EV_REGISTER_HANDLER', function (content) {
|
||||
if (callWhenRegistered[content]) {
|
||||
callWhenRegistered[content].forEach(function (f) { f(); });
|
||||
delete callWhenRegistered[content];
|
||||
}
|
||||
insideHandlers.push(content);
|
||||
}, true);
|
||||
|
||||
var txid;
|
||||
window.addEventListener('message', function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (ow !== msg.source) {
|
||||
console.log("DROP Message from unexpected source");
|
||||
console.log(msg);
|
||||
} else if (!otherWindow) {
|
||||
otherWindow = ow;
|
||||
ow.postMessage(JSON.stringify({ txid: data.txid }), '*');
|
||||
cb(chan);
|
||||
} else if (typeof(data.q) === 'string' && handlers[data.q]) {
|
||||
handlers[data.q].forEach(function (f) {
|
||||
f(data || JSON.parse(msg.data), msg);
|
||||
data = undefined;
|
||||
});
|
||||
} else if (typeof(data.q) === 'undefined' && queries[data.txid]) {
|
||||
queries[data.txid](data, msg);
|
||||
} else if (data.txid === txid) {
|
||||
// stray message from init
|
||||
return;
|
||||
} else {
|
||||
console.log("DROP Unhandled message");
|
||||
console.log(msg);
|
||||
}
|
||||
});
|
||||
if (isSandbox) {
|
||||
// we're in the sandbox
|
||||
otherWindow = ow;
|
||||
cb(chan);
|
||||
}
|
||||
};
|
||||
|
||||
return { create: create };
|
||||
});
|
||||
223
www/common/sframe-common-history.js
Normal file
223
www/common/sframe-common-history.js
Normal file
@@ -0,0 +1,223 @@
|
||||
define([
|
||||
'jquery',
|
||||
'/bower_components/chainpad-json-validator/json-ot.js',
|
||||
'/bower_components/chainpad/chainpad.dist.js',
|
||||
], function ($, JsonOT) {
|
||||
var ChainPad = window.ChainPad;
|
||||
var History = {};
|
||||
|
||||
var getStates = function (rt) {
|
||||
var states = [];
|
||||
var b = rt.getAuthBlock();
|
||||
if (b) { states.unshift(b); }
|
||||
while (b.getParent()) {
|
||||
b = b.getParent();
|
||||
states.unshift(b);
|
||||
}
|
||||
return states;
|
||||
};
|
||||
|
||||
var loadHistory = function (config, common, cb) {
|
||||
var createRealtime = function () {
|
||||
return ChainPad.create({
|
||||
userName: 'history',
|
||||
initialState: '',
|
||||
transformFunction: JsonOT.validate,
|
||||
logLevel: 0,
|
||||
noPrune: true
|
||||
});
|
||||
};
|
||||
var realtime = createRealtime();
|
||||
|
||||
History.readOnly = common.getMetadataMgr().getPrivateData().readOnly;
|
||||
|
||||
var to = window.setTimeout(function () {
|
||||
cb('[GET_FULL_HISTORY_TIMEOUT]');
|
||||
}, 30000);
|
||||
|
||||
common.getFullHistory(realtime, function () {
|
||||
window.clearTimeout(to);
|
||||
cb(null, realtime);
|
||||
});
|
||||
};
|
||||
|
||||
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..."); }
|
||||
History.loading = true;
|
||||
var $toolbar = config.$toolbar;
|
||||
|
||||
if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) {
|
||||
throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory");
|
||||
}
|
||||
|
||||
// config.setHistory(bool, bool)
|
||||
// - bool1: history value
|
||||
// - bool2: reset old content?
|
||||
var render = function (val) {
|
||||
if (typeof val === "undefined") { return; }
|
||||
try {
|
||||
config.applyVal(val);
|
||||
} catch (e) {
|
||||
// Probably a parse error
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
var onClose = function () { config.setHistory(false, true); };
|
||||
var onRevert = function () {
|
||||
config.setHistory(false, false);
|
||||
config.onLocal();
|
||||
config.onRemote();
|
||||
};
|
||||
var onReady = function () {
|
||||
config.setHistory(true);
|
||||
};
|
||||
|
||||
var Messages = common.Messages;
|
||||
var Cryptpad = common.getCryptpadCommon();
|
||||
|
||||
var realtime;
|
||||
|
||||
var states = [];
|
||||
var c = states.length - 1;
|
||||
|
||||
var $hist = $toolbar.find('.cryptpad-toolbar-history');
|
||||
var $left = $toolbar.find('.cryptpad-toolbar-leftside');
|
||||
var $right = $toolbar.find('.cryptpad-toolbar-rightside');
|
||||
var $cke = $toolbar.find('.cke_toolbox_main');
|
||||
|
||||
$hist.html('').show();
|
||||
$left.hide();
|
||||
$right.hide();
|
||||
$cke.hide();
|
||||
|
||||
Cryptpad.spinner($hist).get().show();
|
||||
|
||||
var onUpdate;
|
||||
|
||||
var update = function () {
|
||||
if (!realtime) { return []; }
|
||||
states = getStates(realtime);
|
||||
if (typeof onUpdate === "function") { onUpdate(); }
|
||||
return states;
|
||||
};
|
||||
|
||||
// Get the content of the selected version, and change the version number
|
||||
var get = function (i) {
|
||||
i = parseInt(i);
|
||||
if (isNaN(i)) { return; }
|
||||
if (i < 0) { i = 0; }
|
||||
if (i > states.length - 1) { i = states.length - 1; }
|
||||
var val = states[i].getContent().doc;
|
||||
c = i;
|
||||
if (typeof onUpdate === "function") { onUpdate(); }
|
||||
$hist.find('.next, .previous').css('visibility', '');
|
||||
if (c === states.length - 1) { $hist.find('.next').css('visibility', 'hidden'); }
|
||||
if (c === 0) { $hist.find('.previous').css('visibility', 'hidden'); }
|
||||
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);
|
||||
};
|
||||
|
||||
// Create the history toolbar
|
||||
var display = function () {
|
||||
$hist.html('');
|
||||
var $prev =$('<button>', {
|
||||
'class': 'previous fa fa-step-backward buttonPrimary',
|
||||
title: Messages.history_prev
|
||||
}).appendTo($hist);
|
||||
var $nav = $('<div>', {'class': 'goto'}).appendTo($hist);
|
||||
var $next = $('<button>', {
|
||||
'class': 'next fa fa-step-forward buttonPrimary',
|
||||
title: Messages.history_next
|
||||
}).appendTo($hist);
|
||||
|
||||
$('<label>').text(Messages.history_version).appendTo($nav);
|
||||
var $cur = $('<input>', {
|
||||
'class' : 'gotoInput',
|
||||
'type' : 'number',
|
||||
'min' : '1',
|
||||
'max' : states.length
|
||||
}).val(c + 1).appendTo($nav).mousedown(function (e) {
|
||||
// stopPropagation because the event would be cancelled by the dropdown menus
|
||||
e.stopPropagation();
|
||||
});
|
||||
var $label2 = $('<label>').text(' / '+ states.length).appendTo($nav);
|
||||
$('<br>').appendTo($nav);
|
||||
var $close = $('<button>', {
|
||||
'class':'closeHistory',
|
||||
title: Messages.history_closeTitle
|
||||
}).text(Messages.history_closeTitle).appendTo($nav);
|
||||
var $rev = $('<button>', {
|
||||
'class':'revertHistory buttonSuccess',
|
||||
title: Messages.history_restoreTitle
|
||||
}).text(Messages.history_restore).appendTo($nav);
|
||||
if (History.readOnly) { $rev.hide(); }
|
||||
|
||||
onUpdate = function () {
|
||||
$cur.attr('max', states.length);
|
||||
$cur.val(c+1);
|
||||
$label2.text(' / ' + states.length);
|
||||
};
|
||||
|
||||
var close = function () {
|
||||
$hist.hide();
|
||||
$left.show();
|
||||
$right.show();
|
||||
$cke.show();
|
||||
};
|
||||
|
||||
// Buttons actions
|
||||
$prev.click(function () { render(getPrevious()); });
|
||||
$next.click(function () { render(getNext()); });
|
||||
$cur.keydown(function (e) {
|
||||
var p = function () { e.preventDefault(); };
|
||||
if (e.which === 13) { p(); return render( get($cur.val() - 1) ); } // Enter
|
||||
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(); }
|
||||
}).keyup(function (e) { e.stopPropagation(); }).focus();
|
||||
$cur.on('change', function () {
|
||||
render( get($cur.val() - 1) );
|
||||
});
|
||||
$close.click(function () {
|
||||
states = [];
|
||||
close();
|
||||
onClose();
|
||||
});
|
||||
$rev.click(function () {
|
||||
Cryptpad.confirm(Messages.history_restorePrompt, function (yes) {
|
||||
if (!yes) { return; }
|
||||
close();
|
||||
onRevert();
|
||||
Cryptpad.log(Messages.history_restoreDone);
|
||||
});
|
||||
});
|
||||
|
||||
// Display the latest content
|
||||
render(get(c));
|
||||
};
|
||||
|
||||
// Load all the history messages into a new chainpad object
|
||||
loadHistory(config, common, function (err, newRt) {
|
||||
History.loading = false;
|
||||
if (err) { throw new Error(err); }
|
||||
realtime = newRt;
|
||||
update();
|
||||
c = states.length - 1;
|
||||
display();
|
||||
onReady();
|
||||
});
|
||||
};
|
||||
|
||||
return History;
|
||||
});
|
||||
|
||||
|
||||
265
www/common/sframe-common-interface.js
Normal file
265
www/common/sframe-common-interface.js
Normal file
@@ -0,0 +1,265 @@
|
||||
define([
|
||||
'jquery',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/media-tag.js',
|
||||
], function ($, Cryptpad, MediaTag) {
|
||||
var UI = {};
|
||||
var Messages = Cryptpad.Messages;
|
||||
|
||||
/**
|
||||
* Requirements from cryptpad-common.js
|
||||
* getFileSize
|
||||
* - hrefToHexChannelId
|
||||
* displayAvatar
|
||||
* - getFirstEmojiOrCharacter
|
||||
* - parsePadUrl
|
||||
* - getSecrets
|
||||
* - base64ToHex
|
||||
* - getBlobPathFromHex
|
||||
* - bytesToMegabytes
|
||||
* createUserAdminMenu
|
||||
* - fixHTML
|
||||
* - createDropdown
|
||||
*/
|
||||
|
||||
UI.getFileSize = function (Common, href, cb) {
|
||||
var channelId = Cryptpad.hrefToHexChannelId(href);
|
||||
Common.sendAnonRpcMsg("GET_FILE_SIZE", channelId, function (data) {
|
||||
if (!data) { return void cb("No response"); }
|
||||
if (data.error) { return void cb(data.error); }
|
||||
if (data.response && data.response.length && typeof(data.response[0]) === 'number') {
|
||||
return void cb(void 0, data.response[0]);
|
||||
} else {
|
||||
cb('INVALID_RESPONSE');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
UI.displayAvatar = function (Common, $container, href, name, cb) {
|
||||
var MutationObserver = window.MutationObserver;
|
||||
var displayDefault = function () {
|
||||
var text = Cryptpad.getFirstEmojiOrCharacter(name);
|
||||
var $avatar = $('<span>', {'class': 'default'}).text(text);
|
||||
$container.append($avatar);
|
||||
if (cb) { cb(); }
|
||||
};
|
||||
if (!href) { return void displayDefault(); }
|
||||
var parsed = Cryptpad.parsePadUrl(href);
|
||||
var secret = Cryptpad.getSecrets('file', parsed.hash);
|
||||
if (secret.keys && secret.channel) {
|
||||
var cryptKey = secret.keys && secret.keys.fileKeyStr;
|
||||
var hexFileName = Cryptpad.base64ToHex(secret.channel);
|
||||
var src = Cryptpad.getBlobPathFromHex(hexFileName);
|
||||
UI.getFileSize(Common, href, function (e, data) {
|
||||
if (e) {
|
||||
displayDefault();
|
||||
return void console.error(e);
|
||||
}
|
||||
if (typeof data !== "number") { return void displayDefault(); }
|
||||
if (Cryptpad.bytesToMegabytes(data) > 0.5) { return void displayDefault(); }
|
||||
var $img = $('<media-tag>').appendTo($container);
|
||||
$img.attr('src', src);
|
||||
$img.attr('data-crypto-key', 'cryptpad:' + cryptKey);
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
||||
if (mutation.addedNodes.length > 1 ||
|
||||
mutation.addedNodes[0].nodeName !== 'IMG') {
|
||||
$img.remove();
|
||||
return void displayDefault();
|
||||
}
|
||||
var $image = $img.find('img');
|
||||
var onLoad = function () {
|
||||
var img = new Image();
|
||||
img.onload = function () {
|
||||
var w = img.width;
|
||||
var h = img.height;
|
||||
if (w>h) {
|
||||
$image.css('max-height', '100%');
|
||||
$img.css('flex-direction', 'column');
|
||||
if (cb) { cb($img); }
|
||||
return;
|
||||
}
|
||||
$image.css('max-width', '100%');
|
||||
$img.css('flex-direction', 'row');
|
||||
if (cb) { cb($img); }
|
||||
};
|
||||
img.src = $image.attr('src');
|
||||
};
|
||||
if ($image[0].complete) { onLoad(); }
|
||||
$image.on('load', onLoad);
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe($img[0], {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
characterData: false
|
||||
});
|
||||
MediaTag($img[0]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
UI.createUserAdminMenu = function (config) {
|
||||
var Common = config.Common;
|
||||
var metadataMgr = config.metadataMgr;
|
||||
|
||||
var displayNameCls = config.displayNameCls || 'displayName';
|
||||
var $displayedName = $('<span>', {'class': displayNameCls});
|
||||
|
||||
var accountName = metadataMgr.getPrivateData().accountName;
|
||||
var origin = metadataMgr.getPrivateData().origin;
|
||||
var padType = metadataMgr.getMetadata().type;
|
||||
|
||||
var $userName = $('<span>', {'class': 'userDisplayName'});
|
||||
var options = [];
|
||||
if (config.displayNameCls) {
|
||||
var $userAdminContent = $('<p>');
|
||||
if (accountName) {
|
||||
var $userAccount = $('<span>', {'class': 'userAccount'}).append(Messages.user_accountName + ': ' + Cryptpad.fixHTML(accountName));
|
||||
$userAdminContent.append($userAccount);
|
||||
$userAdminContent.append($('<br>'));
|
||||
}
|
||||
if (config.displayName) {
|
||||
// Hide "Display name:" in read only mode
|
||||
$userName.append(Messages.user_displayName + ': ');
|
||||
$userName.append($displayedName);
|
||||
}
|
||||
$userAdminContent.append($userName);
|
||||
options.push({
|
||||
tag: 'p',
|
||||
attributes: {'class': 'accountData'},
|
||||
content: $userAdminContent.html()
|
||||
});
|
||||
}
|
||||
if (padType !== 'drive') {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {
|
||||
'target': '_blank',
|
||||
'href': origin+'/drive/'
|
||||
},
|
||||
content: Messages.login_accessDrive
|
||||
});
|
||||
}
|
||||
// Add the change display name button if not in read only mode
|
||||
if (config.changeNameButtonCls && config.displayChangeName) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': config.changeNameButtonCls},
|
||||
content: Messages.user_rename
|
||||
});
|
||||
}
|
||||
if (accountName) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'profile'},
|
||||
content: Messages.profileButton
|
||||
});
|
||||
}
|
||||
if (padType !== 'settings') {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'settings'},
|
||||
content: Messages.settingsButton
|
||||
});
|
||||
}
|
||||
// Add login or logout button depending on the current status
|
||||
if (accountName) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'logout'},
|
||||
content: Messages.logoutButton
|
||||
});
|
||||
} else {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'login'},
|
||||
content: Messages.login_login
|
||||
});
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'register'},
|
||||
content: Messages.login_register
|
||||
});
|
||||
}
|
||||
var $icon = $('<span>', {'class': 'fa fa-user-secret'});
|
||||
//var $userbig = $('<span>', {'class': 'big'}).append($displayedName.clone());
|
||||
var $userButton = $('<div>').append($icon);//.append($userbig);
|
||||
if (accountName) {
|
||||
$userButton = $('<div>').append(accountName);
|
||||
}
|
||||
/*if (account && config.displayNameCls) {
|
||||
$userbig.append($('<span>', {'class': 'account-name'}).text('(' + accountName + ')'));
|
||||
} else if (account) {
|
||||
// If no display name, do not display the parentheses
|
||||
$userbig.append($('<span>', {'class': 'account-name'}).text(accountName));
|
||||
}*/
|
||||
var dropdownConfigUser = {
|
||||
text: $userButton.html(), // Button initial text
|
||||
options: options, // Entries displayed in the menu
|
||||
left: true, // Open to the left of the button
|
||||
container: config.$initBlock, // optional
|
||||
feedback: "USER_ADMIN",
|
||||
};
|
||||
var $userAdmin = Cryptpad.createDropdown(dropdownConfigUser);
|
||||
|
||||
var $displayName = $userAdmin.find('.'+displayNameCls);
|
||||
|
||||
var $avatar = $userAdmin.find('.buttonTitle');
|
||||
var oldUrl;
|
||||
var updateButton = function () {
|
||||
var myData = metadataMgr.getUserData();
|
||||
if (!myData) { return; }
|
||||
var newName = myData.name;
|
||||
var url = myData.avatar;
|
||||
$displayName.text(newName || Messages.anonymous);
|
||||
if (accountName && oldUrl !== url) {
|
||||
$avatar.html('');
|
||||
UI.displayAvatar(Common, $avatar, url, newName, function ($img) {
|
||||
oldUrl = url;
|
||||
if ($img) {
|
||||
$userAdmin.find('button').addClass('avatar');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
metadataMgr.onChange(updateButton);
|
||||
updateButton();
|
||||
|
||||
$userAdmin.find('a.logout').click(function () {
|
||||
Common.logout(function () {
|
||||
window.parent.location = origin+'/';
|
||||
});
|
||||
});
|
||||
$userAdmin.find('a.settings').click(function () {
|
||||
if (padType) {
|
||||
window.open(origin+'/settings/');
|
||||
} else {
|
||||
window.parent.location = origin+'/settings/';
|
||||
}
|
||||
});
|
||||
$userAdmin.find('a.profile').click(function () {
|
||||
if (padType) {
|
||||
window.open(origin+'/profile/');
|
||||
} else {
|
||||
window.parent.location = origin+'/profile/';
|
||||
}
|
||||
});
|
||||
$userAdmin.find('a.login').click(function () {
|
||||
Common.setLoginRedirect(function () {
|
||||
window.parent.location = origin+'/login/';
|
||||
});
|
||||
});
|
||||
$userAdmin.find('a.register').click(function () {
|
||||
Common.setLoginRedirect(function () {
|
||||
window.parent.location = origin+'/register/';
|
||||
});
|
||||
});
|
||||
|
||||
return $userAdmin;
|
||||
};
|
||||
|
||||
return UI;
|
||||
});
|
||||
84
www/common/sframe-common-title.js
Normal file
84
www/common/sframe-common-title.js
Normal file
@@ -0,0 +1,84 @@
|
||||
define(['jquery'], function ($) {
|
||||
var module = {};
|
||||
|
||||
module.create = function (cfg, onLocal, Common, metadataMgr) {
|
||||
var exp = {};
|
||||
|
||||
exp.defaultTitle = Common.getDefaultTitle();
|
||||
|
||||
exp.title = document.title;
|
||||
|
||||
cfg = cfg || {};
|
||||
|
||||
var getHeadingText = cfg.getHeadingText || function () { return; };
|
||||
|
||||
/* var updateLocalTitle = function (newTitle) {
|
||||
console.error(newTitle);
|
||||
exp.title = newTitle;
|
||||
onLocal();
|
||||
if (typeof cfg.updateLocalTitle === "function") {
|
||||
cfg.updateLocalTitle(newTitle);
|
||||
} else {
|
||||
document.title = newTitle;
|
||||
}
|
||||
};*/
|
||||
|
||||
var $title;
|
||||
exp.setToolbar = function (toolbar) {
|
||||
$title = toolbar && toolbar.title;
|
||||
};
|
||||
|
||||
exp.getTitle = function () { return exp.title; };
|
||||
var isDefaultTitle = exp.isDefaultTitle = function (){return exp.title === exp.defaultTitle;};
|
||||
|
||||
var suggestTitle = exp.suggestTitle = function (fallback) {
|
||||
if (isDefaultTitle()) {
|
||||
return getHeadingText() || fallback || "";
|
||||
} else {
|
||||
var title = metadataMgr.getMetadata().title;
|
||||
return title || getHeadingText() || exp.defaultTitle;
|
||||
}
|
||||
};
|
||||
|
||||
/*var renameCb = function (err, newTitle) {
|
||||
if (err) { return; }
|
||||
onLocal();
|
||||
//updateLocalTitle(newTitle);
|
||||
};*/
|
||||
|
||||
// update title: href is optional; if not specified, we use window.location.href
|
||||
exp.updateTitle = function (newTitle, cb) {
|
||||
cb = cb || $.noop;
|
||||
if (newTitle === exp.title) { return; }
|
||||
Common.updateTitle(newTitle, cb);
|
||||
};
|
||||
|
||||
// TODO not needed?
|
||||
/*exp.updateDefaultTitle = function (newDefaultTitle) {
|
||||
exp.defaultTitle = newDefaultTitle;
|
||||
if (!$title) { return; }
|
||||
$title.find('input').attr("placeholder", exp.defaultTitle);
|
||||
};*/
|
||||
|
||||
metadataMgr.onChange(function () {
|
||||
var md = metadataMgr.getMetadata();
|
||||
$title.find('span.title').text(md.title || md.defaultTitle);
|
||||
$title.find('input').val(md.title || md.defaultTitle);
|
||||
exp.title = md.title;
|
||||
//exp.updateTitle(md.title || md.defaultTitle);
|
||||
});
|
||||
|
||||
exp.getTitleConfig = function () {
|
||||
return {
|
||||
updateTitle: exp.updateTitle,
|
||||
suggestName: suggestTitle,
|
||||
defaultName: exp.defaultTitle
|
||||
};
|
||||
};
|
||||
|
||||
return exp;
|
||||
};
|
||||
|
||||
return module;
|
||||
});
|
||||
|
||||
328
www/common/sframe-common.js
Normal file
328
www/common/sframe-common.js
Normal file
@@ -0,0 +1,328 @@
|
||||
define([
|
||||
'jquery',
|
||||
'/bower_components/nthen/index.js',
|
||||
'/customize/messages.js',
|
||||
'/common/sframe-chainpad-netflux-inner.js',
|
||||
'/common/sframe-channel.js',
|
||||
'/common/sframe-common-title.js',
|
||||
'/common/sframe-common-interface.js',
|
||||
'/common/sframe-common-history.js',
|
||||
'/common/metadata-manager.js',
|
||||
|
||||
'/customize/application_config.js',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/common-realtime.js'
|
||||
], function ($, nThen, Messages, CpNfInner, SFrameChannel, Title, UI, History, MetadataMgr,
|
||||
AppConfig, Cryptpad, CommonRealtime) {
|
||||
|
||||
// Chainpad Netflux Inner
|
||||
var funcs = {};
|
||||
var ctx = {};
|
||||
|
||||
funcs.Messages = Messages;
|
||||
|
||||
funcs.startRealtime = function (options) {
|
||||
if (ctx.cpNfInner) { return ctx.cpNfInner; }
|
||||
options.sframeChan = ctx.sframeChan;
|
||||
options.metadataMgr = ctx.metadataMgr;
|
||||
ctx.cpNfInner = CpNfInner.start(options);
|
||||
ctx.cpNfInner.metadataMgr.onChangeLazy(options.onLocal);
|
||||
return ctx.cpNfInner;
|
||||
};
|
||||
|
||||
funcs.getMetadataMgr = function () {
|
||||
return ctx.metadataMgr;
|
||||
};
|
||||
funcs.getCryptpadCommon = function () {
|
||||
return Cryptpad;
|
||||
};
|
||||
|
||||
var isLoggedIn = funcs.isLoggedIn = function () {
|
||||
if (!ctx.cpNfInner) { throw new Error("cpNfInner is not ready!"); }
|
||||
return ctx.cpNfInner.metadataMgr.getPrivateData().accountName;
|
||||
};
|
||||
|
||||
var titleUpdated;
|
||||
funcs.updateTitle = function (title, cb) {
|
||||
ctx.metadataMgr.updateTitle(title);
|
||||
titleUpdated = cb;
|
||||
};
|
||||
|
||||
// UI
|
||||
funcs.createUserAdminMenu = UI.createUserAdminMenu;
|
||||
funcs.displayAvatar = UI.displayAvatar;
|
||||
|
||||
// History
|
||||
funcs.getHistory = function (config) { return History.create(funcs, config); };
|
||||
|
||||
// Title module
|
||||
funcs.createTitle = Title.create;
|
||||
|
||||
funcs.getDefaultTitle = function () {
|
||||
if (!ctx.cpNfInner) { throw new Error("cpNfInner is not ready!"); }
|
||||
return ctx.cpNfInner.metadataMgr.getMetadata().defaultTitle;
|
||||
};
|
||||
|
||||
funcs.setDisplayName = function (name, cb) {
|
||||
ctx.sframeChan.query('Q_SETTINGS_SET_DISPLAY_NAME', name, function (err) {
|
||||
if (cb) { cb(err); }
|
||||
});
|
||||
};
|
||||
|
||||
funcs.logout = function (cb) {
|
||||
ctx.sframeChan.query('Q_LOGOUT', null, function (err) {
|
||||
if (cb) { cb(err); }
|
||||
});
|
||||
};
|
||||
|
||||
funcs.notify = function () {
|
||||
ctx.sframeChan.event('EV_NOTIFY');
|
||||
};
|
||||
|
||||
funcs.setLoginRedirect = function (cb) {
|
||||
ctx.sframeChan.query('Q_SET_LOGIN_REDIRECT', null, function (err) {
|
||||
if (cb) { cb(err); }
|
||||
});
|
||||
};
|
||||
|
||||
funcs.sendAnonRpcMsg = function (msg, content, cb) {
|
||||
ctx.sframeChan.query('Q_ANON_RPC_MESSAGE', {
|
||||
msg: msg,
|
||||
content: content
|
||||
}, function (err, data) {
|
||||
if (cb) { cb(data); }
|
||||
});
|
||||
};
|
||||
|
||||
funcs.isOverPinLimit = function (cb) {
|
||||
ctx.sframeChan.query('Q_GET_PIN_LIMIT_STATUS', null, function (err, data) {
|
||||
cb(data.error, data.overLimit, data.limits);
|
||||
});
|
||||
};
|
||||
|
||||
funcs.getFullHistory = function (realtime, cb) {
|
||||
ctx.sframeChan.on('EV_RT_HIST_MESSAGE', function (content) {
|
||||
realtime.message(content);
|
||||
});
|
||||
ctx.sframeChan.query('Q_GET_FULL_HISTORY', null, cb);
|
||||
};
|
||||
|
||||
// Friends
|
||||
var pendingFriends = [];
|
||||
funcs.getPendingFriends = function () {
|
||||
return pendingFriends.slice();
|
||||
};
|
||||
funcs.sendFriendRequest = function (netfluxId) {
|
||||
ctx.sframeChan.query('Q_SEND_FRIEND_REQUEST', netfluxId, $.noop);
|
||||
pendingFriends.push(netfluxId);
|
||||
};
|
||||
|
||||
// Feedback
|
||||
funcs.feedback = function (action, force) {
|
||||
if (force !== true) {
|
||||
if (!action) { return; }
|
||||
try {
|
||||
if (!ctx.metadataMgr.getPrivateData().feedbackAllowed) { return; }
|
||||
} catch (e) { return void console.error(e); }
|
||||
}
|
||||
var randomToken = Math.random().toString(16).replace(/0./, '');
|
||||
//var origin = ctx.metadataMgr.getPrivateData().origin;
|
||||
var href = /*origin +*/ '/common/feedback.html?' + action + '=' + randomToken;
|
||||
$.ajax({
|
||||
type: "HEAD",
|
||||
url: href,
|
||||
});
|
||||
};
|
||||
var prepareFeedback = function (key) {
|
||||
if (typeof(key) !== 'string') { return $.noop; }
|
||||
|
||||
var type = ctx.metadataMgr.getMetadata().type;
|
||||
return function () {
|
||||
funcs.feedback((key + (type? '_' + type: '')).toUpperCase());
|
||||
};
|
||||
};
|
||||
|
||||
// BUTTONS
|
||||
var isStrongestStored = function () {
|
||||
var data = ctx.metadataMgr.getPrivateData();
|
||||
return !data.readOnly || !data.availableHashes.editHash;
|
||||
};
|
||||
funcs.createButton = function (type, rightside, data, callback) {
|
||||
var button;
|
||||
var size = "17px";
|
||||
switch (type) {
|
||||
case 'export':
|
||||
button = $('<button>', {
|
||||
'class': 'fa fa-download',
|
||||
title: Messages.exportButtonTitle,
|
||||
}).append($('<span>', {'class': 'drawer'}).text(Messages.exportButton));
|
||||
|
||||
button.click(prepareFeedback(type));
|
||||
if (callback) {
|
||||
button.click(callback);
|
||||
}
|
||||
break;
|
||||
case 'import':
|
||||
button = $('<button>', {
|
||||
'class': 'fa fa-upload',
|
||||
title: Messages.importButtonTitle,
|
||||
}).append($('<span>', {'class': 'drawer'}).text(Messages.importButton));
|
||||
if (callback) {
|
||||
button
|
||||
.click(prepareFeedback(type))
|
||||
.click(Cryptpad.importContent('text/plain', function (content, file) {
|
||||
callback(content, file);
|
||||
}, {accept: data ? data.accept : undefined}));
|
||||
}
|
||||
break;
|
||||
case 'template':
|
||||
if (!AppConfig.enableTemplates) { return; }
|
||||
button = $('<button>', {
|
||||
title: Messages.saveTemplateButton,
|
||||
}).append($('<span>', {'class':'fa fa-bookmark', style: 'font:'+size+' FontAwesome'}));
|
||||
if (data.rt) {
|
||||
button
|
||||
.click(function () {
|
||||
var title = data.getTitle() || document.title;
|
||||
var todo = function (val) {
|
||||
if (typeof(val) !== "string") { return; }
|
||||
var toSave = data.rt.getUserDoc();
|
||||
if (val.trim()) {
|
||||
val = val.trim();
|
||||
title = val;
|
||||
try {
|
||||
var parsed = JSON.parse(toSave);
|
||||
var meta;
|
||||
if (Array.isArray(parsed) && typeof(parsed[3]) === "object") {
|
||||
meta = parsed[3].metadata; // pad
|
||||
} else if (parsed.info) {
|
||||
meta = parsed.info; // poll
|
||||
} else {
|
||||
meta = parsed.metadata;
|
||||
}
|
||||
if (typeof(meta) === "object") {
|
||||
meta.title = val;
|
||||
meta.defaultTitle = val;
|
||||
delete meta.users;
|
||||
}
|
||||
toSave = JSON.stringify(parsed);
|
||||
} catch(e) {
|
||||
console.error("Parse error while setting the title", e);
|
||||
}
|
||||
}
|
||||
ctx.sframeChan.query('Q_SAVE_AS_TEMPLATE', {
|
||||
title: title,
|
||||
toSave: toSave
|
||||
}, function () {
|
||||
Cryptpad.alert(Messages.templateSaved);
|
||||
funcs.feedback('TEMPLATE_CREATED');
|
||||
});
|
||||
};
|
||||
Cryptpad.prompt(Messages.saveTemplatePrompt, title, todo);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'forget':
|
||||
button = $('<button>', {
|
||||
id: 'cryptpad-forget',
|
||||
title: Messages.forgetButtonTitle,
|
||||
'class': "fa fa-trash cryptpad-forget",
|
||||
style: 'font:'+size+' FontAwesome'
|
||||
});
|
||||
if (!isStrongestStored()) {
|
||||
button.addClass('hidden');
|
||||
}
|
||||
if (callback) {
|
||||
button
|
||||
.click(prepareFeedback(type))
|
||||
.click(function() {
|
||||
var msg = isLoggedIn() ? Messages.forgetPrompt : Messages.fm_removePermanentlyDialog;
|
||||
Cryptpad.confirm(msg, function (yes) {
|
||||
if (!yes) { return; }
|
||||
ctx.sframeChan.query('Q_MOVE_TO_TRASH', null, function (err) {
|
||||
if (err) { return void callback(err); }
|
||||
var cMsg = isLoggedIn() ? Messages.movedToTrash : Messages.deleted;
|
||||
Cryptpad.alert(cMsg, undefined, true);
|
||||
callback();
|
||||
return;
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'history':
|
||||
if (!AppConfig.enableHistory) {
|
||||
button = $('<span>');
|
||||
break;
|
||||
}
|
||||
button = $('<button>', {
|
||||
title: Messages.historyButton,
|
||||
'class': "fa fa-history history",
|
||||
}).append($('<span>', {'class': 'drawer'}).text(Messages.historyText));
|
||||
if (data.histConfig) {
|
||||
button
|
||||
.click(prepareFeedback(type))
|
||||
.on('click', function () {
|
||||
funcs.getHistory(data.histConfig);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'more':
|
||||
button = $('<button>', {
|
||||
title: Messages.moreActions || 'TODO',
|
||||
'class': "drawer-button fa fa-ellipsis-h",
|
||||
style: 'font:'+size+' FontAwesome'
|
||||
});
|
||||
break;
|
||||
default:
|
||||
button = $('<button>', {
|
||||
'class': "fa fa-question",
|
||||
style: 'font:'+size+' FontAwesome'
|
||||
})
|
||||
.click(prepareFeedback(type));
|
||||
}
|
||||
if (rightside) {
|
||||
button.addClass('rightside-button');
|
||||
}
|
||||
return button;
|
||||
|
||||
};
|
||||
/* funcs.storeLinkToClipboard = function (readOnly, cb) {
|
||||
ctx.sframeChan.query('Q_STORE_LINK_TO_CLIPBOARD', readOnly, function (err) {
|
||||
if (cb) { cb(err); }
|
||||
});
|
||||
};
|
||||
*/
|
||||
|
||||
Object.freeze(funcs);
|
||||
return { create: function (cb) {
|
||||
nThen(function (waitFor) {
|
||||
SFrameChannel.create(window.parent, waitFor(function (sfc) { ctx.sframeChan = sfc; }), true);
|
||||
// CpNfInner.start() should be here....
|
||||
}).nThen(function () {
|
||||
ctx.metadataMgr = MetadataMgr.create(ctx.sframeChan);
|
||||
ctx.metadataMgr.onTitleChange(function (title) {
|
||||
ctx.sframeChan.query('Q_SET_PAD_TITLE_IN_DRIVE', title, function (err) {
|
||||
if (err) { return; }
|
||||
if (titleUpdated) { titleUpdated(undefined, title); }
|
||||
});
|
||||
});
|
||||
|
||||
ctx.sframeChan.on('EV_RT_CONNECT', function () { CommonRealtime.setConnectionState(true); });
|
||||
ctx.sframeChan.on('EV_RT_DISCONNECT', function () { CommonRealtime.setConnectionState(false); });
|
||||
|
||||
|
||||
ctx.sframeChan.on('Q_INCOMING_FRIEND_REQUEST', function (confirmMsg, cb) {
|
||||
Cryptpad.confirm(confirmMsg, cb, null, true);
|
||||
});
|
||||
ctx.sframeChan.on('EV_FRIEND_REQUEST', function (data) {
|
||||
var i = pendingFriends.indexOf(data.sender);
|
||||
if (i !== -1) { pendingFriends.splice(i, 1); }
|
||||
Cryptpad.log(data.logText);
|
||||
});
|
||||
|
||||
cb(funcs);
|
||||
});
|
||||
} };
|
||||
});
|
||||
85
www/common/sframe-protocol.js
Normal file
85
www/common/sframe-protocol.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// This file defines all of the RPC calls which are used between the inner and outer iframe.
|
||||
// Define *querys* (which expect a response) using Q_<query name>
|
||||
// Define *events* (which expect no response) using EV_<event name>
|
||||
// Please document the queries and events you create, and please please avoid making generic
|
||||
// "do stuff" events/queries which are used for many different things because it makes the
|
||||
// protocol unclear.
|
||||
//
|
||||
// WARNING: At this point, this protocol is still EXPERIMENTAL. This is not it's final form.
|
||||
// We need to define protocol one piece at a time and then when we are satisfied that we
|
||||
// fully understand the problem, we will define the *right* protocol and this file will be dynomited.
|
||||
//
|
||||
define({
|
||||
// When the iframe first launches, this query is sent repeatedly by the controller
|
||||
// to wait for it to awake and give it the requirejs config to use.
|
||||
'Q_INIT': true,
|
||||
|
||||
// When either the outside or inside registers a query handler, this is sent.
|
||||
'EV_REGISTER_HANDLER': true,
|
||||
|
||||
// Realtime events called from the outside.
|
||||
// When someone joins the pad, argument is a string with their netflux id.
|
||||
'EV_RT_JOIN': true,
|
||||
// When someone leaves the pad, argument is a string with their netflux id.
|
||||
'EV_RT_LEAVE': true,
|
||||
// When you have been disconnected, no arguments.
|
||||
'EV_RT_DISCONNECT': true,
|
||||
// When you have connected, argument is an object with myID: string, members: list, readOnly: boolean.
|
||||
'EV_RT_CONNECT': true,
|
||||
// Called after the history is finished synchronizing, no arguments.
|
||||
'EV_RT_READY': true,
|
||||
// Called from both outside and inside, argument is a (string) chainpad message.
|
||||
'Q_RT_MESSAGE': true,
|
||||
|
||||
// Called from the outside, this informs the inside whenever the user's data has been changed.
|
||||
// The argument is the object representing the content of the user profile minus the netfluxID
|
||||
// which changes per-reconnect.
|
||||
'EV_METADATA_UPDATE': true,
|
||||
|
||||
// Takes one argument only, the title to set for the CURRENT pad which the user is looking at.
|
||||
// This changes the pad title in drive ONLY, the pad title needs to be changed inside of the
|
||||
// iframe and synchronized with the other users. This will not trigger a EV_METADATA_UPDATE
|
||||
// because the metadata contained in EV_METADATA_UPDATE does not contain the pad title.
|
||||
'Q_SET_PAD_TITLE_IN_DRIVE': true,
|
||||
|
||||
// Update the user's display-name which will be shown to contacts and people in the same pads.
|
||||
'Q_SETTINGS_SET_DISPLAY_NAME': true,
|
||||
|
||||
// Log the user out in all the tabs
|
||||
'Q_LOGOUT': true,
|
||||
|
||||
// When moving to the login or register page from a pad, we need to redirect to that pad at the
|
||||
// end of the login process. This query set the current href to the sessionStorage.
|
||||
'Q_SET_LOGIN_REDIRECT': true,
|
||||
|
||||
// Store the editing or readonly link of the current pad to the clipboard (share button).
|
||||
'Q_STORE_LINK_TO_CLIPBOARD': true,
|
||||
|
||||
// Use anonymous rpc from inside the iframe (for avatars & pin usage).
|
||||
'Q_ANON_RPC_MESSAGE': true,
|
||||
|
||||
// Check the pin limit to determine if we can store the pad in the drive or if we should.
|
||||
// display a warning
|
||||
'Q_GET_PIN_LIMIT_STATUS': true,
|
||||
|
||||
// Move a pad to the trash when using the forget button.
|
||||
'Q_MOVE_TO_TRASH': true,
|
||||
|
||||
// Request the full history from the server when the users clicks on the history button.
|
||||
// Callback is called when the FULL_HISTORY_END message is received in the outside.
|
||||
'Q_GET_FULL_HISTORY': true,
|
||||
// When a (full) history message is received from the server.
|
||||
'EV_RT_HIST_MESSAGE': true,
|
||||
|
||||
// Save a pad as a template using the toolbar button
|
||||
'Q_SAVE_AS_TEMPLATE': true,
|
||||
|
||||
// Friend requests from the userlist
|
||||
'Q_SEND_FRIEND_REQUEST': true, // Up query
|
||||
'Q_INCOMING_FRIEND_REQUEST': true, // Down query
|
||||
'EV_FRIEND_REQUEST': true, // Down event when the request is complete
|
||||
|
||||
// Set the tab notification when the content of the pad changes
|
||||
'EV_NOTIFY': true,
|
||||
|
||||
});
|
||||
1
www/common/test.md
Normal file
1
www/common/test.md
Normal file
@@ -0,0 +1 @@
|
||||
This is to test if we have a flakey test.
|
||||
1001
www/common/toolbar3.js
Normal file
1001
www/common/toolbar3.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -413,6 +413,15 @@ define([
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
exp.getRecentPads = function () {
|
||||
var allFiles = files[FILES_DATA];
|
||||
var sorted = Object.keys(allFiles)
|
||||
.sort(function (a,b) {
|
||||
return allFiles[a].atime < allFiles[b].atime;
|
||||
})
|
||||
.map(function (str) { return Number(str); });
|
||||
return sorted;
|
||||
};
|
||||
|
||||
/**
|
||||
* OPERATIONS
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<title>CryptPad</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<script async data-bootload="/contacts/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/contacts/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>.loading-hidden, .loading-hidden * {display: none !important;}</style>
|
||||
</head>
|
||||
<body class="loading-hidden">
|
||||
|
||||
@@ -4,9 +4,12 @@ define([
|
||||
'/common/toolbar2.js',
|
||||
'/common/cryptpad-common.js',
|
||||
|
||||
'/common/common-messenger.js',
|
||||
'/contacts/messenger-ui.js',
|
||||
|
||||
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||
'less!/customize/src/less/cryptpad.less',
|
||||
], function ($, Crypto, Toolbar, Cryptpad) {
|
||||
], function ($, Crypto, Toolbar, Cryptpad, Messenger, UI) {
|
||||
var Messages = Cryptpad.Messages;
|
||||
|
||||
var APP = window.APP = {
|
||||
@@ -20,7 +23,6 @@ define([
|
||||
|
||||
var ifrw = $('#pad-iframe')[0].contentWindow;
|
||||
var $iframe = $('#pad-iframe').contents();
|
||||
//var $appContainer = $iframe.find('#app');
|
||||
var $list = $iframe.find('#friendList');
|
||||
var $messages = $iframe.find('#messaging');
|
||||
var $bar = $iframe.find('.toolbar-container');
|
||||
@@ -40,28 +42,20 @@ define([
|
||||
|
||||
Cryptpad.getProxy().on('disconnect', function () {
|
||||
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
|
||||
Cryptpad.enableMessaging(false);
|
||||
});
|
||||
Cryptpad.getProxy().on('reconnect', function (uid) {
|
||||
console.error('reconnecting: ', uid);
|
||||
Cryptpad.findOKButton().click();
|
||||
|
||||
APP.messenger.cleanFriendChannels();
|
||||
APP.messenger.openFriendChannels();
|
||||
APP.messenger.setEditable(true);
|
||||
});
|
||||
|
||||
var ui = APP.ui = Cryptpad.initMessagingUI(Cryptpad, $list, $messages);
|
||||
APP.messenger = Cryptpad.initMessaging(Cryptpad, ui);
|
||||
|
||||
var $infoBlock = $('<div>', {'class': 'info'}).appendTo($messages);
|
||||
$('<h2>').text(Messages.contacts_info1).appendTo($infoBlock);
|
||||
var $ul = $('<ul>').appendTo($infoBlock);
|
||||
$('<li>').text(Messages.contacts_info2).appendTo($ul);
|
||||
$('<li>').text(Messages.contacts_info3).appendTo($ul);
|
||||
//$('<li>').text(Messages.contacts_info4).appendTo($ul);
|
||||
|
||||
Cryptpad.removeLoadingScreen();
|
||||
var messenger = window.messenger = Messenger.messenger(Cryptpad);
|
||||
UI.create(messenger, $list, $messages);
|
||||
};
|
||||
|
||||
Cryptpad.ready(function () {
|
||||
|
||||
@@ -52,6 +52,7 @@ body {
|
||||
width: 350px;
|
||||
height: 100%;
|
||||
background-color: lighten(@bg-color, 10%);
|
||||
overflow-y: auto;
|
||||
.friend {
|
||||
background: rgba(0,0,0,0.1);
|
||||
padding: 5px;
|
||||
@@ -169,8 +170,11 @@ body {
|
||||
margin: 10px;
|
||||
}
|
||||
.more-history {
|
||||
display: none;
|
||||
//.hover;
|
||||
//display: none;
|
||||
.hover;
|
||||
&.faded {
|
||||
color: darken(@bg-color, 5%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.chat {
|
||||
|
||||
521
www/contacts/messenger-ui.js
Normal file
521
www/contacts/messenger-ui.js
Normal file
@@ -0,0 +1,521 @@
|
||||
define([
|
||||
'jquery',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/hyperscript.js',
|
||||
'/bower_components/marked/marked.min.js',
|
||||
], function ($, Cryptpad, h, Marked) {
|
||||
'use strict';
|
||||
// TODO use our fancy markdown and support media-tags
|
||||
Marked.setOptions({ sanitize: true, });
|
||||
|
||||
var UI = {};
|
||||
var Messages = Cryptpad.Messages;
|
||||
|
||||
var m = function (md) {
|
||||
var d = h('div.content');
|
||||
try {
|
||||
d.innerHTML = Marked(md || '');
|
||||
} catch (e) {
|
||||
console.error(md);
|
||||
console.error(e);
|
||||
}
|
||||
return d;
|
||||
};
|
||||
|
||||
var dataQuery = function (curvePublic) {
|
||||
return '[data-key="' + curvePublic + '"]';
|
||||
};
|
||||
|
||||
var initChannel = function (state, curvePublic, info) {
|
||||
console.log('initializing channel for [%s]', curvePublic);
|
||||
state.channels[curvePublic] = {
|
||||
messages: [],
|
||||
HEAD: info.lastKnownHash,
|
||||
TAIL: null,
|
||||
};
|
||||
};
|
||||
|
||||
UI.create = function (messenger, $userlist, $messages) {
|
||||
var state = window.state = {
|
||||
active: '',
|
||||
};
|
||||
|
||||
state.channels = {};
|
||||
var displayNames = state.displayNames = {};
|
||||
|
||||
var avatars = state.avatars = {};
|
||||
var setActive = function (curvePublic) {
|
||||
state.active = curvePublic;
|
||||
};
|
||||
var isActive = function (curvePublic) {
|
||||
return curvePublic === state.active;
|
||||
};
|
||||
|
||||
var find = {};
|
||||
find.inList = function (curvePublic) {
|
||||
return $userlist.find(dataQuery(curvePublic));
|
||||
};
|
||||
|
||||
var notify = function (curvePublic) {
|
||||
find.inList(curvePublic).addClass('notify');
|
||||
};
|
||||
var unnotify = function (curvePublic) {
|
||||
find.inList(curvePublic).removeClass('notify');
|
||||
};
|
||||
|
||||
var markup = {};
|
||||
markup.message = function (msg) {
|
||||
var curvePublic = msg.author;
|
||||
var name = displayNames[msg.author];
|
||||
return h('div.message', {
|
||||
title: msg.time? new Date(msg.time).toLocaleString(): '?',
|
||||
'data-key': curvePublic,
|
||||
}, [
|
||||
name? h('div.sender', name): undefined,
|
||||
m(msg.text),
|
||||
]);
|
||||
};
|
||||
|
||||
var getChat = function (curvePublic) {
|
||||
return $messages.find(dataQuery(curvePublic));
|
||||
};
|
||||
|
||||
var normalizeLabels = function ($messagebox) {
|
||||
$messagebox.find('div.message').toArray().reduce(function (a, b) {
|
||||
var $b = $(b);
|
||||
if ($(a).data('key') === $b.data('key')) {
|
||||
$b.find('.sender').hide();
|
||||
return a;
|
||||
}
|
||||
return b;
|
||||
}, []);
|
||||
};
|
||||
|
||||
markup.chatbox = function (curvePublic, data) {
|
||||
var moreHistory = h('span.more-history.fa.fa-history', {
|
||||
title: Messages.contacts_fetchHistory,
|
||||
});
|
||||
var displayName = data.displayName;
|
||||
|
||||
var fetching = false;
|
||||
var $moreHistory = $(moreHistory).click(function () {
|
||||
if (fetching) { return; }
|
||||
|
||||
// get oldest known message...
|
||||
var channel = state.channels[curvePublic];
|
||||
|
||||
if (channel.exhausted) {
|
||||
return void $moreHistory.addClass('faded');
|
||||
}
|
||||
|
||||
console.log('getting history');
|
||||
var sig = channel.TAIL || channel.HEAD;
|
||||
|
||||
fetching = true;
|
||||
var $messagebox = $(getChat(curvePublic)).find('.messages');
|
||||
messenger.getMoreHistory(curvePublic, sig, 10, function (e, history) {
|
||||
fetching = false;
|
||||
if (e) { return void console.error(e); }
|
||||
|
||||
if (history.length === 0) {
|
||||
channel.exhausted = true;
|
||||
return;
|
||||
}
|
||||
|
||||
history.forEach(function (msg) {
|
||||
if (channel.exhausted) { return; }
|
||||
if (msg.sig) {
|
||||
if (msg.sig === channel.TAIL) {
|
||||
console.error('No more messages to fetch');
|
||||
channel.exhausted = true;
|
||||
console.log(channel);
|
||||
return;
|
||||
} else {
|
||||
channel.TAIL = msg.sig;
|
||||
}
|
||||
} else {
|
||||
return void console.error('expected signature');
|
||||
}
|
||||
if (msg.type !== 'MSG') { return; }
|
||||
|
||||
// FIXME Schlameil the painter (performance does not scale well)
|
||||
if (channel.messages.some(function (old) {
|
||||
return msg.sig === old.sig;
|
||||
})) { return; }
|
||||
|
||||
channel.messages.unshift(msg);
|
||||
var el_message = markup.message(msg);
|
||||
$messagebox.prepend(el_message);
|
||||
});
|
||||
normalizeLabels($messagebox);
|
||||
});
|
||||
});
|
||||
|
||||
var removeHistory = h('span.remove-history.fa.fa-eraser', {
|
||||
title: Messages.contacts_removeHistoryTitle
|
||||
});
|
||||
|
||||
$(removeHistory).click(function () {
|
||||
Cryptpad.confirm(Messages.contacts_confirmRemoveHistory, function (yes) {
|
||||
if (!yes) { return; }
|
||||
Cryptpad.clearOwnedChannel(data.channel, function (e) {
|
||||
if (e) {
|
||||
console.error(e);
|
||||
Cryptpad.alert(Messages.contacts_removeHistoryServerError);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var avatar = h('div.avatar');
|
||||
var header = h('div.header', [
|
||||
avatar,
|
||||
moreHistory,
|
||||
removeHistory,
|
||||
]);
|
||||
var messages = h('div.messages');
|
||||
var input = h('textarea', {
|
||||
placeholder: Messages.contacts_typeHere
|
||||
});
|
||||
var sendButton = h('button.btn.btn-primary.fa.fa-paper-plane', {
|
||||
title: Messages.contacts_send,
|
||||
});
|
||||
|
||||
var rightCol = h('span.right-col', [
|
||||
h('span.name', displayName),
|
||||
]);
|
||||
|
||||
var $avatar = $(avatar);
|
||||
if (data.avatar && avatars[data.avatar]) {
|
||||
$avatar.append(avatars[data.avatar]).append(rightCol);
|
||||
} else {
|
||||
Cryptpad.displayAvatar($avatar, data.avatar, data.displayName, function ($img) {
|
||||
if (data.avatar && $img) {
|
||||
avatars[data.avatar] = $img[0].outerHTML;
|
||||
}
|
||||
$avatar.append(rightCol);
|
||||
});
|
||||
}
|
||||
|
||||
var sending = false;
|
||||
var send = function (content) {
|
||||
if (typeof(content) !== 'string' || !content.trim()) { return; }
|
||||
if (sending) { return false; }
|
||||
sending = true;
|
||||
messenger.sendMessage(curvePublic, content, function (e) {
|
||||
if (e) {
|
||||
// failed to send
|
||||
return void console.error('failed to send');
|
||||
}
|
||||
input.value = '';
|
||||
sending = false;
|
||||
console.log('sent successfully');
|
||||
var $messagebox = $(messages);
|
||||
|
||||
var height = $messagebox[0].scrollHeight;
|
||||
$messagebox.scrollTop(height);
|
||||
});
|
||||
};
|
||||
|
||||
var onKeyDown = function (e) {
|
||||
// ignore anything that isn't 'enter'
|
||||
if (e.keyCode !== 13) { return; }
|
||||
// send unless they're holding a ctrl-key or shift
|
||||
if (!e.ctrlKey && !e.shiftKey) {
|
||||
send(this.value);
|
||||
return false;
|
||||
}
|
||||
|
||||
// insert a newline if they're holding either
|
||||
var val = this.value;
|
||||
var start = this.selectionState;
|
||||
var end = this.selectionEnd;
|
||||
|
||||
if (![start,end].some(function (x) {
|
||||
return typeof(x) !== 'number';
|
||||
})) {
|
||||
this.value = val.slice(0, start) + '\n' + val.slice(end);
|
||||
this.selectionStart = this.selectionEnd = start + 1;
|
||||
} else if (document.selection && document.selection.createRange) {
|
||||
this.focus();
|
||||
var range = document.selection.createRange();
|
||||
range.text = '\r\n';
|
||||
range.collapse(false);
|
||||
range.select();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
$(input).on('keydown', onKeyDown);
|
||||
$(sendButton).click(function () { send(input.value); });
|
||||
|
||||
return h('div.chat', {
|
||||
'data-key': curvePublic,
|
||||
}, [
|
||||
header,
|
||||
messages,
|
||||
h('div.input', [
|
||||
input,
|
||||
sendButton,
|
||||
]),
|
||||
]);
|
||||
};
|
||||
|
||||
var hideInfo = function () {
|
||||
$messages.find('.info').hide();
|
||||
};
|
||||
|
||||
var updateStatus = function (curvePublic) {
|
||||
var $status = find.inList(curvePublic).find('.status');
|
||||
// FIXME this stopped working :(
|
||||
messenger.getStatus(curvePublic, function (e, online) {
|
||||
// if error maybe you shouldn't display this friend...
|
||||
if (e) {
|
||||
find.inList(curvePublic).hide();
|
||||
getChat(curvePublic).hide();
|
||||
|
||||
return void console.error(curvePublic, e);
|
||||
}
|
||||
if (online) {
|
||||
return void $status
|
||||
.removeClass('offline').addClass('online');
|
||||
}
|
||||
$status.removeClass('online').addClass('offline');
|
||||
});
|
||||
};
|
||||
|
||||
var display = function (curvePublic) {
|
||||
var channel = state.channels[curvePublic];
|
||||
var lastMsg = channel.messages.slice(-1)[0];
|
||||
|
||||
if (lastMsg) {
|
||||
channel.HEAD = lastMsg.sig;
|
||||
messenger.setChannelHead(curvePublic, channel.HEAD, function (e) {
|
||||
if (e) { console.error(e); }
|
||||
});
|
||||
}
|
||||
|
||||
setActive(curvePublic);
|
||||
unnotify(curvePublic);
|
||||
var $chat = getChat(curvePublic);
|
||||
hideInfo();
|
||||
$messages.find('div.chat[data-key]').hide();
|
||||
if ($chat.length) {
|
||||
var $chat_messages = $chat.find('div.message');
|
||||
if (!$chat_messages.length) {
|
||||
var $more = $chat.find('.more-history');
|
||||
$more.click();
|
||||
}
|
||||
return void $chat.show();
|
||||
}
|
||||
messenger.getFriendInfo(curvePublic, function (e, info) {
|
||||
if (e) { return void console.error(e); } // FIXME
|
||||
var chatbox = markup.chatbox(curvePublic, info);
|
||||
$messages.append(chatbox);
|
||||
});
|
||||
};
|
||||
|
||||
var removeFriend = function (curvePublic) {
|
||||
messenger.removeFriend(curvePublic, function (e, removed) {
|
||||
if (e) { return void console.error(e); }
|
||||
find.inList(curvePublic).remove();
|
||||
console.log(removed);
|
||||
});
|
||||
};
|
||||
|
||||
markup.friend = function (data) {
|
||||
var curvePublic = data.curvePublic;
|
||||
var friend = h('div.friend.avatar', {
|
||||
'data-key': curvePublic,
|
||||
});
|
||||
|
||||
var remove = h('span.remove.fa.fa-user-times', {
|
||||
title: Messages.contacts_remove
|
||||
});
|
||||
var status = h('span.status');
|
||||
var rightCol = h('span.right-col', [
|
||||
h('span.name', [data.displayName]),
|
||||
remove,
|
||||
]);
|
||||
|
||||
var $friend = $(friend)
|
||||
.click(function () {
|
||||
display(curvePublic);
|
||||
})
|
||||
.dblclick(function () {
|
||||
if (data.profile) { window.open('/profile/#' + data.profile); }
|
||||
});
|
||||
|
||||
$(remove).click(function (e) {
|
||||
e.stopPropagation();
|
||||
Cryptpad.confirm(Messages._getKey('contacts_confirmRemove', [
|
||||
Cryptpad.fixHTML(data.displayName)
|
||||
]), function (yes) {
|
||||
if (!yes) { return; }
|
||||
removeFriend(curvePublic, function (e) {
|
||||
if (e) { return void console.error(e); }
|
||||
});
|
||||
// TODO remove friend from userlist ui
|
||||
// FIXME seems to trigger EJOINED from netflux-websocket (from server);
|
||||
// (tried to join a channel in which you were already present)
|
||||
}, undefined, true);
|
||||
});
|
||||
|
||||
if (data.avatar && avatars[data.avatar]) {
|
||||
$friend.append(avatars[data.avatar]);
|
||||
$friend.append(rightCol);
|
||||
} else {
|
||||
Cryptpad.displayAvatar($friend, data.avatar, data.displayName, function ($img) {
|
||||
if (data.avatar && $img) {
|
||||
avatars[data.avatar] = $img[0].outerHTML;
|
||||
}
|
||||
$friend.append(rightCol);
|
||||
});
|
||||
}
|
||||
$friend.append(status);
|
||||
return $friend;
|
||||
};
|
||||
|
||||
var isBottomedOut = function ($elem) {
|
||||
return ($elem[0].scrollHeight - $elem.scrollTop() === $elem.outerHeight());
|
||||
};
|
||||
|
||||
var initializing = true;
|
||||
messenger.on('message', function (message) {
|
||||
if (!initializing) { Cryptpad.notify(); }
|
||||
var curvePublic = message.curve;
|
||||
|
||||
var name = displayNames[curvePublic];
|
||||
var chat = getChat(curvePublic, name);
|
||||
|
||||
console.log(message);
|
||||
|
||||
var el_message = markup.message(message);
|
||||
|
||||
state.channels[curvePublic].messages.push(message);
|
||||
|
||||
var $chat = $(chat);
|
||||
|
||||
if (!$chat.length) {
|
||||
console.error("Got a message but the chat isn't open");
|
||||
}
|
||||
|
||||
var $messagebox = $chat.find('.messages');
|
||||
var shouldScroll = isBottomedOut($messagebox);
|
||||
|
||||
$messagebox.append(el_message);
|
||||
|
||||
if (shouldScroll) {
|
||||
$messagebox.scrollTop($messagebox.outerHeight());
|
||||
}
|
||||
normalizeLabels($messagebox);
|
||||
|
||||
var channel = state.channels[curvePublic];
|
||||
if (!channel) {
|
||||
console.error('expected channel [%s] to be open', curvePublic);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isActive(curvePublic)) {
|
||||
channel.HEAD = message.sig;
|
||||
messenger.setChannelHead(curvePublic, message.sig, function (e) {
|
||||
if (e) { return void console.error(e); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
var lastMsg = channel.messages.slice(-1)[0];
|
||||
if (lastMsg.sig !== channel.HEAD) {
|
||||
return void notify(curvePublic);
|
||||
}
|
||||
unnotify(curvePublic);
|
||||
});
|
||||
|
||||
messenger.on('join', function (curvePublic, channel) {
|
||||
channel = channel;
|
||||
updateStatus(curvePublic);
|
||||
});
|
||||
messenger.on('leave', function (curvePublic, channel) {
|
||||
channel = channel;
|
||||
updateStatus(curvePublic);
|
||||
});
|
||||
|
||||
// change in your friend list
|
||||
messenger.on('update', function (info, curvePublic) {
|
||||
var name = displayNames[curvePublic] = info.displayName;
|
||||
|
||||
// update label in friend list
|
||||
find.inList(curvePublic).find('.name').text(name);
|
||||
|
||||
// update title bar and messages
|
||||
$messages.find(dataQuery(curvePublic) + ' .header .name, div.message'+
|
||||
dataQuery(curvePublic) + ' div.sender').text(name).text(name);
|
||||
});
|
||||
|
||||
var connectToFriend = function (curvePublic, cb) {
|
||||
messenger.getFriendInfo(curvePublic, function (e, info) {
|
||||
if (e) { return void console.error(e); }
|
||||
var name = displayNames[curvePublic] = info.displayName;
|
||||
initChannel(state, curvePublic, info);
|
||||
|
||||
var chatbox = markup.chatbox(curvePublic, info);
|
||||
$(chatbox).hide();
|
||||
$messages.append(chatbox);
|
||||
|
||||
var friend = markup.friend(info, name);
|
||||
$userlist.append(friend);
|
||||
messenger.openFriendChannel(curvePublic, function (e) {
|
||||
if (e) { return void console.error(e); }
|
||||
cb();
|
||||
updateStatus(curvePublic);
|
||||
// don't add friends that are already in your userlist
|
||||
//if (friendExistsInUserList(k)) { return; }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
messenger.on('friend', function (curvePublic) {
|
||||
console.log('new friend: ', curvePublic);
|
||||
//console.error("TODO redraw user list");
|
||||
//console.error("TODO connect to new friend");
|
||||
// FIXME this doesn't work right now because the friend hasn't been fully added?
|
||||
connectToFriend(curvePublic, function () {
|
||||
//console.error('connected');
|
||||
});
|
||||
});
|
||||
|
||||
messenger.on('unfriend', function (curvePublic) {
|
||||
console.log('unfriend', curvePublic);
|
||||
find.inList(curvePublic).remove();
|
||||
console.error('TODO remove chatbox');
|
||||
console.error('TODO show something if that chatbox was active');
|
||||
});
|
||||
|
||||
Cryptpad.onDisplayNameChanged(function () {
|
||||
//messenger.checkNewFriends();
|
||||
messenger.updateMyData();
|
||||
});
|
||||
|
||||
// FIXME dirty hack
|
||||
messenger.getMyInfo(function (e, info) {
|
||||
displayNames[info.curvePublic] = info.displayName;
|
||||
});
|
||||
|
||||
messenger.getFriendList(function (e, keys) {
|
||||
var count = keys.length + 1;
|
||||
var ready = function () {
|
||||
count--;
|
||||
if (count === 0) {
|
||||
initializing = false;
|
||||
Cryptpad.removeLoadingScreen();
|
||||
}
|
||||
};
|
||||
ready();
|
||||
|
||||
keys.forEach(function (curvePublic) {
|
||||
connectToFriend(curvePublic, ready);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return UI;
|
||||
});
|
||||
@@ -190,7 +190,8 @@ span {
|
||||
}
|
||||
.docTree {
|
||||
margin-top: 20px;
|
||||
padding: 0 0 0 20px;
|
||||
//padding: 0 0 0 20px;
|
||||
padding: 0;
|
||||
cursor: auto;
|
||||
&li, li {
|
||||
padding: 0;
|
||||
@@ -304,6 +305,24 @@ span {
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
.docTree {
|
||||
.root > .element-row > .expcol {
|
||||
position: relative;
|
||||
top:0;
|
||||
left: -10px;
|
||||
}
|
||||
.root > .element-row > .folder {
|
||||
margin-left: -5px;
|
||||
}
|
||||
.root {
|
||||
&> .element-row {
|
||||
padding-left: 20px;
|
||||
}
|
||||
&> ul {
|
||||
padding-left: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expand/collapse lines
|
||||
.docTree ul {
|
||||
@@ -478,9 +497,26 @@ span {
|
||||
.listElement {
|
||||
display: none;
|
||||
}
|
||||
.addpad {
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
padding: 0;
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.fa {
|
||||
cursor: pointer;
|
||||
font-size: 90px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
.grid-element {
|
||||
display: none;
|
||||
}
|
||||
// Make it act as a table!
|
||||
padding-left: 20px;
|
||||
ul {
|
||||
@@ -561,6 +597,34 @@ span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#addPadDialog.cp-modal-container {
|
||||
.fileIcon;
|
||||
|
||||
li:not(.selected):hover {
|
||||
border: 1px solid white;
|
||||
}
|
||||
.cp-modal {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
li, li .fa {
|
||||
cursor: pointer;
|
||||
}
|
||||
&> p {
|
||||
margin: 50px;
|
||||
}
|
||||
&> div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
.uploadFile {
|
||||
break-after: always;
|
||||
page-break-after: always;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Toolbar */
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<title>CryptDrive</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<script async data-bootload="/drive/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/drive/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body style="display: none;">
|
||||
<div id="toolbar" class="toolbar-container"></div>
|
||||
|
||||
@@ -51,6 +51,8 @@ define([
|
||||
var TEMPLATE_NAME = Messages.fm_templateName;
|
||||
var TRASH = "trash";
|
||||
var TRASH_NAME = Messages.fm_trashName;
|
||||
var RECENT = "recent";
|
||||
var RECENT_NAME = Messages.fm_recentPadsName;
|
||||
|
||||
var LOCALSTORAGE_LAST = "cryptpad-file-lastOpened";
|
||||
var LOCALSTORAGE_OPENED = "cryptpad-file-openedFolders";
|
||||
@@ -161,6 +163,7 @@ define([
|
||||
//var $upIcon = $('<span>', {"class": "fa fa-arrow-circle-up"});
|
||||
var $unsortedIcon = $('<span>', {"class": "fa fa-files-o"});
|
||||
var $templateIcon = $('<span>', {"class": "fa fa-cubes"});
|
||||
var $recentIcon = $('<span>', {"class": "fa fa-clock-o"});
|
||||
var $trashIcon = $('<span>', {"class": "fa fa-trash-o"});
|
||||
var $trashEmptyIcon = $('<span>', {"class": "fa fa-trash-o"});
|
||||
//var $collapseIcon = $('<span>', {"class": "fa fa-minus-square-o expcol"});
|
||||
@@ -172,6 +175,7 @@ define([
|
||||
var $closeIcon = $('<span>', {"class": "fa fa-window-close"});
|
||||
var $backupIcon = $('<span>', {"class": "fa fa-life-ring"});
|
||||
var $searchIcon = $('<span>', {"class": "fa fa-search searchIcon"});
|
||||
var $addIcon = $('<span>', {"class": "fa fa-plus"});
|
||||
|
||||
var history = {
|
||||
isHistoryMode: false,
|
||||
@@ -233,9 +237,10 @@ define([
|
||||
|
||||
// Categories dislayed in the menu
|
||||
// _WORKGROUP_ : do not display unsorted
|
||||
var displayedCategories = [ROOT, TRASH, SEARCH];
|
||||
var displayedCategories = [ROOT, TRASH, SEARCH, RECENT];
|
||||
if (AppConfig.enableTemplates) { displayedCategories.push(TEMPLATE); }
|
||||
if (isWorkgroup()) { displayedCategories = [ROOT, TRASH, SEARCH]; }
|
||||
var virtualCategories = [SEARCH, RECENT];
|
||||
|
||||
if (!APP.loggedIn) {
|
||||
displayedCategories = [FILES_DATA];
|
||||
@@ -315,6 +320,7 @@ define([
|
||||
}
|
||||
};
|
||||
$content.on('mousedown', function (e) {
|
||||
if (currentPath[0] === SEARCH) { return; }
|
||||
if (e.which !== 1) { return; }
|
||||
$content.focus();
|
||||
sel.down = true;
|
||||
@@ -1157,6 +1163,7 @@ define([
|
||||
if (!href) { return $icon; }
|
||||
|
||||
if (href.indexOf('/pad/') !== -1) { $icon = Cryptpad.getIcon('pad'); }
|
||||
else if (href.indexOf('/pad2/') !== -1) { $icon = Cryptpad.getIcon('pad'); } // SFRAME
|
||||
else if (href.indexOf('/code/') !== -1) { $icon = Cryptpad.getIcon('code'); }
|
||||
else if (href.indexOf('/slide/') !== -1) { $icon = Cryptpad.getIcon('slide'); }
|
||||
else if (href.indexOf('/poll/') !== -1) { $icon = Cryptpad.getIcon('poll'); }
|
||||
@@ -1258,6 +1265,7 @@ define([
|
||||
case TEMPLATE: pName = TEMPLATE_NAME; break;
|
||||
case FILES_DATA: pName = FILES_DATA_NAME; break;
|
||||
case SEARCH: pName = SEARCH_NAME; break;
|
||||
case RECENT: pName = RECENT_NAME; break;
|
||||
default: pName = name;
|
||||
}
|
||||
return pName;
|
||||
@@ -1317,6 +1325,9 @@ define([
|
||||
case FILES_DATA:
|
||||
msg = Messages.fm_info_allFiles;
|
||||
break;
|
||||
case RECENT:
|
||||
msg = Messages.fm_info_recent;
|
||||
break;
|
||||
default:
|
||||
msg = undefined;
|
||||
}
|
||||
@@ -1379,10 +1390,63 @@ define([
|
||||
$container.append($listButton).append($gridButton);
|
||||
};
|
||||
|
||||
var getNewPadTypes = function () {
|
||||
var arr = [];
|
||||
AppConfig.availablePadTypes.forEach(function (type) {
|
||||
if (type === 'drive') { return; }
|
||||
if (type === 'contacts') { return; }
|
||||
if (type === 'todo') { return; }
|
||||
if (type === 'file') { return; }
|
||||
if (!Cryptpad.isLoggedIn() && AppConfig.registeredOnlyTypes &&
|
||||
AppConfig.registeredOnlyTypes.indexOf(type) !== -1) {
|
||||
return;
|
||||
}
|
||||
arr.push(type);
|
||||
});
|
||||
return arr;
|
||||
};
|
||||
var addNewPadHandlers = function ($block, isInRoot) {
|
||||
// Handlers
|
||||
if (isInRoot) {
|
||||
var onCreated = function (err, info) {
|
||||
if (err) {
|
||||
if (err === E_OVER_LIMIT) {
|
||||
return void Cryptpad.alert(Messages.pinLimitDrive, null, true);
|
||||
}
|
||||
return void Cryptpad.alert(Messages.fm_error_cantPin);
|
||||
}
|
||||
module.newFolder = info.newPath;
|
||||
refresh();
|
||||
};
|
||||
$block.find('a.newFolder, li.newFolder').click(function () {
|
||||
filesOp.addFolder(currentPath, null, onCreated);
|
||||
});
|
||||
$block.find('a.uploadFile, li.uploadFile').click(function () {
|
||||
var $input = $('<input>', {
|
||||
'type': 'file',
|
||||
'style': 'display: none;'
|
||||
}).on('change', function (e) {
|
||||
var file = e.target.files[0];
|
||||
var ev = {
|
||||
target: $content[0]
|
||||
};
|
||||
APP.FM.handleFile(file, ev);
|
||||
});
|
||||
$input.click();
|
||||
});
|
||||
}
|
||||
$block.find('a.newdoc, li.newdoc').click(function () {
|
||||
var type = $(this).attr('data-type') || 'pad';
|
||||
sessionStorage[Cryptpad.newPadPathKey] = filesOp.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
|
||||
window.open('/' + type + '/');
|
||||
});
|
||||
};
|
||||
var createNewButton = function (isInRoot, $container) {
|
||||
if (!APP.editable) { return; }
|
||||
if (!APP.loggedIn) { return; } // Anonymous users can use the + menu in the toolbar
|
||||
|
||||
if (!filesOp.isPathIn(currentPath, [ROOT, 'hrefArray'])) { return; }
|
||||
|
||||
// Create dropdown
|
||||
var options = [];
|
||||
if (isInRoot) {
|
||||
@@ -1399,13 +1463,7 @@ define([
|
||||
});
|
||||
options.push({tag: 'hr'});
|
||||
}
|
||||
AppConfig.availablePadTypes.forEach(function (type) {
|
||||
if (type === 'drive') { return; }
|
||||
if (type === 'contacts') { return; }
|
||||
if (!Cryptpad.isLoggedIn() && AppConfig.registeredOnlyTypes &&
|
||||
AppConfig.registeredOnlyTypes.indexOf(type) !== -1) {
|
||||
return;
|
||||
}
|
||||
getNewPadTypes().forEach(function (type) {
|
||||
var attributes = {
|
||||
'class': 'newdoc',
|
||||
'data-type': type,
|
||||
@@ -1431,40 +1489,7 @@ define([
|
||||
$block.find('button').addClass('new');
|
||||
$block.find('button').attr('title', Messages.fm_newButtonTitle);
|
||||
|
||||
// Handlers
|
||||
if (isInRoot) {
|
||||
var onCreated = function (err, info) {
|
||||
if (err) {
|
||||
if (err === E_OVER_LIMIT) {
|
||||
return void Cryptpad.alert(Messages.pinLimitDrive, null, true);
|
||||
}
|
||||
return void Cryptpad.alert(Messages.fm_error_cantPin);
|
||||
}
|
||||
module.newFolder = info.newPath;
|
||||
refresh();
|
||||
};
|
||||
$block.find('a.newFolder').click(function () {
|
||||
filesOp.addFolder(currentPath, null, onCreated);
|
||||
});
|
||||
$block.find('a.uploadFile').click(function () {
|
||||
var $input = $('<input>', {
|
||||
'type': 'file',
|
||||
'style': 'display: none;'
|
||||
}).on('change', function (e) {
|
||||
var file = e.target.files[0];
|
||||
var ev = {
|
||||
target: $content[0]
|
||||
};
|
||||
APP.FM.handleFile(file, ev);
|
||||
});
|
||||
$input.click();
|
||||
});
|
||||
}
|
||||
$block.find('a.newdoc').click(function () {
|
||||
var type = $(this).attr('data-type') || 'pad';
|
||||
sessionStorage[Cryptpad.newPadPathKey] = filesOp.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
|
||||
window.open('/' + type + '/');
|
||||
});
|
||||
addNewPadHandlers($block, isInRoot);
|
||||
|
||||
$container.append($block);
|
||||
};
|
||||
@@ -1637,6 +1662,58 @@ define([
|
||||
return keys;
|
||||
};
|
||||
|
||||
// Create the ghost icon to add pads/folders
|
||||
var createNewPadIcons = function ($block, isInRoot) {
|
||||
var $container = $('<div>');
|
||||
if (isInRoot) {
|
||||
// Folder
|
||||
var $element1 = $('<li>', {
|
||||
'class': 'newFolder element-row grid-element'
|
||||
}).prepend($folderIcon.clone()).appendTo($container);
|
||||
$element1.append($('<span>', {'class': 'name'}).text(Messages.fm_folder));
|
||||
// File
|
||||
var $element2 = $('<li>', {
|
||||
'class': 'uploadFile element-row grid-element'
|
||||
}).prepend(getIcon('file')).appendTo($container);
|
||||
$element2.append($('<span>', {'class': 'name'}).text(Messages.uploadButton));
|
||||
}
|
||||
// Pads
|
||||
getNewPadTypes().forEach(function (type) {
|
||||
var $element = $('<li>', {
|
||||
'class': 'newdoc element-row grid-element'
|
||||
}).prepend(getIcon(type)).appendTo($container);
|
||||
$element.append($('<span>', {'class': 'name'}).text(Messages.type[type]));
|
||||
$element.attr('data-type', type);
|
||||
});
|
||||
|
||||
$container.find('.element-row').click(function () {
|
||||
$block.hide();
|
||||
});
|
||||
return $container;
|
||||
};
|
||||
var createGhostIcon = function ($list) {
|
||||
var isInRoot = currentPath[0] === ROOT;
|
||||
var $element = $('<li>', {
|
||||
'class': 'element-row grid-element addpad'
|
||||
}).prepend($addIcon.clone()).appendTo($list);
|
||||
$element.append($('<span>', {'class': 'name'}).text(Messages.fm_newFile));
|
||||
$element.attr('title', Messages.fm_newFile);
|
||||
$element.click(function () {
|
||||
var $modal = Cryptpad.createModal({
|
||||
id: 'addPadDialog',
|
||||
$body: $iframe.find('body')
|
||||
});
|
||||
var $title = $('<h3>').text(Messages.fm_newFile);
|
||||
var $description = $('<p>').text(Messages.fm_newButtonTitle);
|
||||
$modal.find('.cp-modal').append($title);
|
||||
$modal.find('.cp-modal').append($description);
|
||||
var $content = createNewPadIcons($modal, isInRoot);
|
||||
$modal.find('.cp-modal').append($content);
|
||||
$modal.show();
|
||||
addNewPadHandlers($modal, isInRoot);
|
||||
});
|
||||
};
|
||||
|
||||
// Drive content toolbar
|
||||
var createToolbar = function () {
|
||||
var $toolbar = $driveToolbar;
|
||||
@@ -1695,6 +1772,7 @@ define([
|
||||
}
|
||||
$container.append($element);
|
||||
});
|
||||
createGhostIcon($container);
|
||||
};
|
||||
|
||||
var displayAllFiles = function ($container) {
|
||||
@@ -1804,6 +1882,50 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
var displayRecent = function ($list) {
|
||||
var filesList = filesOp.getRecentPads();
|
||||
var limit = 20;
|
||||
var i = 0;
|
||||
filesList.forEach(function (id) {
|
||||
if (i >= limit) { return; }
|
||||
// Check path (pad exists and not in trash)
|
||||
var paths = filesOp.findFile(id);
|
||||
if (!paths.length) { return; }
|
||||
var path = paths[0];
|
||||
if (filesOp.isPathIn(path, [TRASH])) { return; }
|
||||
// Display the pad
|
||||
var file = filesOp.getFileData(id);
|
||||
if (!file) {
|
||||
//debug("Unsorted or template returns an element not present in filesData: ", href);
|
||||
file = { title: Messages.fm_noname };
|
||||
//return;
|
||||
}
|
||||
var $icon = getFileIcon(id);
|
||||
var ro = filesOp.isReadOnlyFile(id);
|
||||
// ro undefined mens it's an old hash which doesn't support read-only
|
||||
var roClass = typeof(ro) === 'undefined' ? ' noreadonly' : ro ? ' readonly' : '';
|
||||
var $element = $('<li>', {
|
||||
'class': 'file-element element element-row' + roClass,
|
||||
});
|
||||
addFileData(id, $element);
|
||||
$element.prepend($icon).dblclick(function () {
|
||||
openFile(id);
|
||||
});
|
||||
$element.data('path', path);
|
||||
$element.click(function(e) {
|
||||
e.stopPropagation();
|
||||
onElementClick(e, $element, path);
|
||||
});
|
||||
$element.contextmenu(openDefaultContextMenu);
|
||||
$element.data('context', $defaultContextMenu);
|
||||
/*if (draggable) {
|
||||
addDragAndDropHandlers($element, path, false, false);
|
||||
}*/
|
||||
$list.append($element);
|
||||
i++;
|
||||
});
|
||||
};
|
||||
|
||||
// Display the selected directory into the content part (rightside)
|
||||
// NOTE: Elements in the trash are not using the same storage structure as the others
|
||||
// _WORKGROUP_ : do not change the lastOpenedFolder value in localStorage
|
||||
@@ -1832,9 +1954,11 @@ define([
|
||||
var isTemplate = filesOp.comparePath(path, [TEMPLATE]);
|
||||
var isAllFiles = filesOp.comparePath(path, [FILES_DATA]);
|
||||
var isSearch = path[0] === SEARCH;
|
||||
var isRecent = path[0] === RECENT;
|
||||
var isVirtual = virtualCategories.indexOf(path[0]) !== -1;
|
||||
|
||||
var root = isSearch ? undefined : filesOp.find(path);
|
||||
if (!isSearch && typeof(root) === "undefined") {
|
||||
var root = isVirtual ? undefined : filesOp.find(path);
|
||||
if (!isVirtual && typeof(root) === "undefined") {
|
||||
log(Messages.fm_unknownFolderError);
|
||||
debug("Unable to locate the selected directory: ", path);
|
||||
var parentPath = path.slice();
|
||||
@@ -1920,6 +2044,8 @@ define([
|
||||
displayTrashRoot($list, $folderHeader, $fileHeader);
|
||||
} else if (isSearch) {
|
||||
displaySearch($list, path[1]);
|
||||
} else if (isRecent) {
|
||||
displayRecent($list);
|
||||
} else {
|
||||
$dirContent.contextmenu(openContentContextMenu);
|
||||
if (filesOp.hasSubfolder(root)) { $list.append($folderHeader); }
|
||||
@@ -1939,8 +2065,9 @@ define([
|
||||
var $element = createElement(path, key, root, false);
|
||||
$element.appendTo($list);
|
||||
});
|
||||
|
||||
createGhostIcon($list);
|
||||
}
|
||||
//$content.append($toolbar).append($title).append($info).append($dirContent);
|
||||
$content.append($info).append($dirContent);
|
||||
|
||||
var $truncated = $('<span>', {'class': 'truncated'}).text('...');
|
||||
@@ -2036,7 +2163,10 @@ define([
|
||||
var $rootIcon = filesOp.isFolderEmpty(files[ROOT]) ?
|
||||
(isRootOpened ? $folderOpenedEmptyIcon : $folderEmptyIcon) :
|
||||
(isRootOpened ? $folderOpenedIcon : $folderIcon);
|
||||
var $rootElement = createTreeElement(ROOT_NAME, $rootIcon.clone(), [ROOT], false, true, false, isRootOpened);
|
||||
var $rootElement = createTreeElement(ROOT_NAME, $rootIcon.clone(), [ROOT], false, true, true, isRootOpened);
|
||||
if (!filesOp.hasSubfolder(root)) {
|
||||
$rootElement.find('.expcol').css('visibility', 'hidden');
|
||||
}
|
||||
$rootElement.addClass('root');
|
||||
$rootElement.find('>.element-row').contextmenu(openDirectoryContextMenu);
|
||||
$('<ul>', {'class': 'docTree'}).append($rootElement).appendTo($container);
|
||||
@@ -2092,6 +2222,15 @@ define([
|
||||
$container.append($trashList);
|
||||
};
|
||||
|
||||
var createRecent = function ($container, path) {
|
||||
var $icon = $recentIcon.clone();
|
||||
var isOpened = filesOp.comparePath(path, currentPath);
|
||||
var $element = createTreeElement(RECENT_NAME, $icon, [RECENT], false, false, false, isOpened);
|
||||
$element.addClass('root');
|
||||
var $list = $('<ul>', { id: 'recentTree', 'class': 'category' }).append($element);
|
||||
$container.append($list);
|
||||
};
|
||||
|
||||
var search = APP.Search = {};
|
||||
var createSearch = function ($container) {
|
||||
var isInSearch = currentPath[0] === SEARCH;
|
||||
@@ -2144,6 +2283,7 @@ define([
|
||||
$tree.html('');
|
||||
if (displayedCategories.indexOf(SEARCH) !== -1) { createSearch($tree); }
|
||||
var $div = $('<div>', {'class': 'categories-container'}).appendTo($tree);
|
||||
if (displayedCategories.indexOf(RECENT) !== -1) { createRecent($div, [RECENT]); }
|
||||
if (displayedCategories.indexOf(ROOT) !== -1) { createTree($div, [ROOT]); }
|
||||
if (displayedCategories.indexOf(TEMPLATE) !== -1) { createTemplate($div, [TEMPLATE]); }
|
||||
if (displayedCategories.indexOf(FILES_DATA) !== -1) { createAllFiles($div, [FILES_DATA]); }
|
||||
@@ -2625,7 +2765,8 @@ define([
|
||||
console.error(e);
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
body: $iframe.find('body')
|
||||
};
|
||||
APP.FM = Cryptpad.createFileManager(fmConfig);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ body {
|
||||
}
|
||||
|
||||
#app.ready {
|
||||
background: url('/customize/bg3.jpg') no-repeat center center;
|
||||
//background: url('/customize/bg3.jpg') no-repeat center center;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<title>CryptPad</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<script async data-bootload="/file/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/file/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>.loading-hidden, .loading-hidden * {display: none !important;}</style>
|
||||
</head>
|
||||
<body class="loading-hidden">
|
||||
|
||||
@@ -91,7 +91,14 @@ define([
|
||||
Title.updateTitle(title || Title.defaultTitle);
|
||||
toolbar.addElement(['pageTitle'], {pageTitle: title});
|
||||
|
||||
var displayFile = function (ev, sizeMb) {
|
||||
var displayFile = function (ev, sizeMb, CB) {
|
||||
var called_back;
|
||||
var cb = function (e) {
|
||||
if (called_back) { return; }
|
||||
called_back = true;
|
||||
if (CB) { CB(e); }
|
||||
};
|
||||
|
||||
var $mt = $dlview.find('media-tag');
|
||||
var cryptKey = secret.keys && secret.keys.fileKeyStr;
|
||||
var hexFileName = Cryptpad.base64ToHex(secret.channel);
|
||||
@@ -127,7 +134,7 @@ define([
|
||||
|
||||
// make pdfs big
|
||||
var toolbarHeight = $iframe.find('#toolbar').height();
|
||||
$iframe.find('media-tag iframe').css({
|
||||
var $another_iframe = $iframe.find('media-tag iframe').css({
|
||||
'height': 'calc(100vh - ' + toolbarHeight + 'px)',
|
||||
'width': '100vw',
|
||||
'position': 'absolute',
|
||||
@@ -135,10 +142,19 @@ define([
|
||||
'left': 0,
|
||||
'border': 0
|
||||
});
|
||||
|
||||
if ($another_iframe.length) {
|
||||
$another_iframe.load(function () {
|
||||
cb();
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
})
|
||||
.on('decryptionError', function (e) {
|
||||
var error = e.originalEvent;
|
||||
Cryptpad.alert(error.message);
|
||||
//Cryptpad.alert(error.message);
|
||||
cb(error.message);
|
||||
})
|
||||
.on('decryptionProgress', function (e) {
|
||||
var progress = e.originalEvent;
|
||||
@@ -188,7 +204,9 @@ define([
|
||||
var onClick = function (ev) {
|
||||
if (decrypting) { return; }
|
||||
decrypting = true;
|
||||
displayFile(ev, sizeMb);
|
||||
displayFile(ev, sizeMb, function (err) {
|
||||
if (err) { Cryptpad.alert(err); }
|
||||
});
|
||||
};
|
||||
if (typeof(sizeMb) === 'number' && sizeMb < 5) { return void onClick(); }
|
||||
$dlform.find('#dl, #progress').click(onClick);
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<html class="cp">
|
||||
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
|
||||
<head>
|
||||
<title data-localization="main_title">Cryptpad: Zero Knowledge, Collaborative Real Time Editing</title>
|
||||
<title data-localization="main_title">CryptPad: Zero Knowledge, Collaborative Real Time Editing</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
|
||||
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
|
||||
<link rel="stylesheet" href="/bower_components/codemirror/addon/dialog/dialog.css">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html class="cp pad">
|
||||
<html>
|
||||
<head>
|
||||
<title>CryptPad</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
#pad-iframe {
|
||||
#sbox-iframe {
|
||||
position:fixed;
|
||||
top:0px;
|
||||
left:0px;
|
||||
@@ -27,5 +27,4 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe id="pad-iframe"></iframe><script src="/common/noscriptfix.js"></script>
|
||||
|
||||
<iframe id="sbox-iframe">
|
||||
@@ -1,13 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html class="cp pad">
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
|
||||
<script async data-bootload="/pad/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script src="/bower_components/ckeditor/ckeditor.js"></script>
|
||||
<script src="/pad/wysiwygarea-plugin.js"></script>
|
||||
<script async data-bootload="/pad/inner.js" data-main="/common/sframe-boot.js?ver=1.2" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
|
||||
799
www/pad/inner.js
799
www/pad/inner.js
@@ -1,3 +1,798 @@
|
||||
require(['/api/config'], function (ApiConfig) {
|
||||
// see ckeditor_base.js getUrl()
|
||||
window.CKEDITOR_GETURL = function (resource) {
|
||||
if (resource.indexOf( '/' ) === 0) {
|
||||
resource = window.CKEDITOR.basePath.replace(/\/bower_components\/.*/, '') + resource;
|
||||
} else if (resource.indexOf(':/') === -1) {
|
||||
resource = window.CKEDITOR.basePath + resource;
|
||||
}
|
||||
if (resource[resource.length - 1] !== '/' && resource.indexOf('ver=') === -1) {
|
||||
var args = ApiConfig.requireConf.urlArgs;
|
||||
if (resource.indexOf('/bower_components/') !== -1) {
|
||||
args = 'ver=' + window.CKEDITOR.timestamp;
|
||||
}
|
||||
resource += (resource.indexOf('?') >= 0 ? '&' : '?') + args;
|
||||
}
|
||||
return resource;
|
||||
};
|
||||
require(['/bower_components/ckeditor/ckeditor.js']);
|
||||
});
|
||||
define([
|
||||
'less!/customize/src/less/toolbar.less',
|
||||
], function () {});
|
||||
'jquery',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/bower_components/hyperjson/hyperjson.js',
|
||||
'/common/toolbar3.js',
|
||||
'/common/cursor.js',
|
||||
'/bower_components/chainpad-json-validator/json-ot.js',
|
||||
'/common/TypingTests.js',
|
||||
'json.sortify',
|
||||
'/bower_components/textpatcher/TextPatcher.js',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/cryptget.js',
|
||||
'/pad/links.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
'/common/sframe-common.js',
|
||||
'/api/config',
|
||||
'/common/common-realtime.js',
|
||||
|
||||
'/bower_components/file-saver/FileSaver.min.js',
|
||||
'/bower_components/diff-dom/diffDOM.js',
|
||||
|
||||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
|
||||
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||
'less!/customize/src/less/cryptpad.less',
|
||||
'less!/customize/src/less/toolbar.less'
|
||||
], function (
|
||||
$,
|
||||
Crypto,
|
||||
Hyperjson,
|
||||
Toolbar,
|
||||
Cursor,
|
||||
JsonOT,
|
||||
TypingTest,
|
||||
JSONSortify,
|
||||
TextPatcher,
|
||||
Cryptpad,
|
||||
Cryptget,
|
||||
Links,
|
||||
nThen,
|
||||
SFCommon,
|
||||
ApiConfig,
|
||||
CommonRealtime)
|
||||
{
|
||||
var saveAs = window.saveAs;
|
||||
var Messages = Cryptpad.Messages;
|
||||
var DiffDom = window.diffDOM;
|
||||
|
||||
var stringify = function (obj) { return JSONSortify(obj); };
|
||||
|
||||
window.Toolbar = Toolbar;
|
||||
window.Hyperjson = Hyperjson;
|
||||
|
||||
var slice = function (coll) {
|
||||
return Array.prototype.slice.call(coll);
|
||||
};
|
||||
|
||||
var removeListeners = function (root) {
|
||||
slice(root.attributes).map(function (attr) {
|
||||
if (/^on/.test(attr.name)) {
|
||||
root.attributes.removeNamedItem(attr.name);
|
||||
}
|
||||
});
|
||||
slice(root.children).forEach(removeListeners);
|
||||
};
|
||||
|
||||
var hjsonToDom = function (H) {
|
||||
var dom = Hyperjson.toDOM(H);
|
||||
removeListeners(dom);
|
||||
return dom;
|
||||
};
|
||||
|
||||
var module = window.REALTIME_MODULE = window.APP = {
|
||||
Hyperjson: Hyperjson,
|
||||
TextPatcher: TextPatcher,
|
||||
logFights: true,
|
||||
fights: [],
|
||||
Cryptpad: Cryptpad,
|
||||
Cursor: Cursor,
|
||||
};
|
||||
|
||||
var emitResize = module.emitResize = function () {
|
||||
var evt = window.document.createEvent('UIEvents');
|
||||
evt.initUIEvent('resize', true, false, window, 0);
|
||||
window.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
var toolbar;
|
||||
|
||||
var isNotMagicLine = function (el) {
|
||||
return !(el && typeof(el.getAttribute) === 'function' &&
|
||||
el.getAttribute('class') &&
|
||||
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1);
|
||||
};
|
||||
|
||||
/* catch `type="_moz"` before it goes over the wire */
|
||||
var brFilter = function (hj) {
|
||||
if (hj[1].type === '_moz') { hj[1].type = undefined; }
|
||||
return hj;
|
||||
};
|
||||
|
||||
var onConnectError = function () {
|
||||
Cryptpad.errorLoadingScreen(Messages.websocketError);
|
||||
};
|
||||
|
||||
var domFromHTML = function (html) {
|
||||
return new DOMParser().parseFromString(html, 'text/html');
|
||||
};
|
||||
|
||||
var forbiddenTags = [
|
||||
'SCRIPT',
|
||||
'IFRAME',
|
||||
'OBJECT',
|
||||
'APPLET',
|
||||
'VIDEO',
|
||||
'AUDIO'
|
||||
];
|
||||
|
||||
var getHTML = function (inner) {
|
||||
return ('<!DOCTYPE html>\n' + '<html>\n' + inner.innerHTML);
|
||||
};
|
||||
|
||||
var CKEDITOR_CHECK_INTERVAL = 100;
|
||||
var ckEditorAvailable = function (cb) {
|
||||
var intr;
|
||||
var check = function () {
|
||||
if (window.CKEDITOR) {
|
||||
clearTimeout(intr);
|
||||
cb(window.CKEDITOR);
|
||||
}
|
||||
};
|
||||
intr = setInterval(function () {
|
||||
console.log("Ckeditor was not defined. Trying again in %sms", CKEDITOR_CHECK_INTERVAL);
|
||||
check();
|
||||
}, CKEDITOR_CHECK_INTERVAL);
|
||||
check();
|
||||
};
|
||||
|
||||
var mkDiffOptions = function (cursor, readOnly) {
|
||||
return {
|
||||
preDiffApply: function (info) {
|
||||
/*
|
||||
Don't accept attributes that begin with 'on'
|
||||
these are probably listeners, and we don't want to
|
||||
send scripts over the wire.
|
||||
*/
|
||||
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) {
|
||||
if (info.diff.name === 'href') {
|
||||
// console.log(info.diff);
|
||||
//var href = info.diff.newValue;
|
||||
|
||||
// TODO normalize HTML entities
|
||||
if (/javascript *: */.test(info.diff.newValue)) {
|
||||
// TODO remove javascript: links
|
||||
}
|
||||
}
|
||||
|
||||
if (/^on/.test(info.diff.name)) {
|
||||
console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/*
|
||||
Also reject any elements which would insert any one of
|
||||
our forbidden tag types: script, iframe, object,
|
||||
applet, video, or audio
|
||||
*/
|
||||
if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) {
|
||||
if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) {
|
||||
console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName);
|
||||
return true;
|
||||
} else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) {
|
||||
console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.node && info.node.tagName === 'BODY') {
|
||||
if (info.diff.action === 'removeAttribute' &&
|
||||
['class', 'spellcheck'].indexOf(info.diff.name) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* DiffDOM will filter out magicline plugin elements
|
||||
in practice this will make it impossible to use it
|
||||
while someone else is typing, which could be annoying.
|
||||
|
||||
we should check when such an element is going to be
|
||||
removed, and prevent that from happening. */
|
||||
if (info.node && info.node.tagName === 'SPAN' &&
|
||||
info.node.getAttribute('contentEditable') === "false") {
|
||||
// it seems to be a magicline plugin element...
|
||||
if (info.diff.action === 'removeElement') {
|
||||
// and you're about to remove it...
|
||||
// this probably isn't what you want
|
||||
|
||||
/*
|
||||
I have never seen this in the console, but the
|
||||
magic line is still getting removed on remote
|
||||
edits. This suggests that it's getting removed
|
||||
by something other than diffDom.
|
||||
*/
|
||||
console.log("preventing removal of the magic line!");
|
||||
|
||||
// return true to prevent diff application
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not change the contenteditable value in view mode
|
||||
if (readOnly && info.node && info.node.tagName === 'BODY' &&
|
||||
info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// no use trying to recover the cursor if it doesn't exist
|
||||
if (!cursor.exists()) { return; }
|
||||
|
||||
/* frame is either 0, 1, 2, or 3, depending on which
|
||||
cursor frames were affected: none, first, last, or both
|
||||
*/
|
||||
var frame = info.frame = cursor.inNode(info.node);
|
||||
|
||||
if (!frame) { return; }
|
||||
|
||||
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
|
||||
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);
|
||||
|
||||
if (frame & 1) {
|
||||
// push cursor start if necessary
|
||||
if (pushes.commonStart < cursor.Range.start.offset) {
|
||||
cursor.Range.start.offset += pushes.delta;
|
||||
}
|
||||
}
|
||||
if (frame & 2) {
|
||||
// push cursor end if necessary
|
||||
if (pushes.commonStart < cursor.Range.end.offset) {
|
||||
cursor.Range.end.offset += pushes.delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
postDiffApply: function (info) {
|
||||
if (info.frame) {
|
||||
if (info.node) {
|
||||
if (info.frame & 1) { cursor.fixStart(info.node); }
|
||||
if (info.frame & 2) { cursor.fixEnd(info.node); }
|
||||
} else { console.error("info.node did not exist"); }
|
||||
|
||||
var sel = cursor.makeSelection();
|
||||
var range = cursor.makeRange();
|
||||
|
||||
cursor.fixSelection(sel, range);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
var andThen = function (editor, Ckeditor, common) {
|
||||
//var $iframe = $('#pad-iframe').contents();
|
||||
//var secret = Cryptpad.getSecrets();
|
||||
//var readOnly = secret.keys && !secret.keys.editKeyStr;
|
||||
//if (!secret.keys) {
|
||||
// secret.keys = secret.key;
|
||||
//}
|
||||
var readOnly = false; // TODO
|
||||
var cpNfInner;
|
||||
var metadataMgr;
|
||||
var onLocal;
|
||||
|
||||
var $bar = $('#cke_1_toolbox');
|
||||
|
||||
var $html = $bar.closest('html');
|
||||
var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]');
|
||||
if ($faLink.length) {
|
||||
$html.find('iframe').contents().find('head').append($faLink.clone());
|
||||
}
|
||||
var isHistoryMode = false;
|
||||
|
||||
if (readOnly) {
|
||||
$('#cke_1_toolbox > .cke_toolbox_main').hide();
|
||||
}
|
||||
|
||||
/* add a class to the magicline plugin so we can pick it out more easily */
|
||||
|
||||
var ml = Ckeditor.instances.editor1.plugins.magicline.backdoor.that.line.$;
|
||||
[ml, ml.parentElement].forEach(function (el) {
|
||||
el.setAttribute('class', 'non-realtime');
|
||||
});
|
||||
|
||||
var ifrWindow = $html.find('iframe')[0].contentWindow;
|
||||
|
||||
var documentBody = ifrWindow.document.body;
|
||||
|
||||
var inner = window.inner = documentBody;
|
||||
|
||||
var cursor = module.cursor = Cursor(inner);
|
||||
|
||||
var openLink = function (e) {
|
||||
var el = e.currentTarget;
|
||||
if (!el || el.nodeName !== 'A') { return; }
|
||||
var href = el.getAttribute('href');
|
||||
var bounceHref = window.location.origin + '/bounce/#' + encodeURIComponent(href);
|
||||
if (href) { ifrWindow.open(bounceHref, '_blank'); }
|
||||
};
|
||||
|
||||
var setEditable = module.setEditable = function (bool) {
|
||||
if (bool) {
|
||||
$(inner).css({
|
||||
color: '#333',
|
||||
});
|
||||
}
|
||||
if (!readOnly || !bool) {
|
||||
inner.setAttribute('contenteditable', bool);
|
||||
}
|
||||
};
|
||||
|
||||
CommonRealtime.onInfiniteSpinner(function () { setEditable(false); });
|
||||
|
||||
// don't let the user edit until the pad is ready
|
||||
setEditable(false);
|
||||
|
||||
var initializing = true;
|
||||
|
||||
var Title;
|
||||
//var UserList;
|
||||
//var Metadata;
|
||||
|
||||
var getHeadingText = function () {
|
||||
var text;
|
||||
if (['h1', 'h2', 'h3'].some(function (t) {
|
||||
var $header = $(inner).find(t + ':first-of-type');
|
||||
if ($header.length && $header.text()) {
|
||||
text = $header.text();
|
||||
return true;
|
||||
}
|
||||
})) { return text; }
|
||||
};
|
||||
|
||||
var DD = new DiffDom(mkDiffOptions(cursor, readOnly));
|
||||
|
||||
// apply patches, and try not to lose the cursor in the process!
|
||||
var applyHjson = function (shjson) {
|
||||
var userDocStateDom = hjsonToDom(JSON.parse(shjson));
|
||||
|
||||
if (!readOnly && !initializing) {
|
||||
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
|
||||
}
|
||||
var patch = (DD).diff(inner, userDocStateDom);
|
||||
(DD).apply(inner, patch);
|
||||
if (readOnly) {
|
||||
var $links = $(inner).find('a');
|
||||
// off so that we don't end up with multiple identical handlers
|
||||
$links.off('click', openLink).on('click', openLink);
|
||||
}
|
||||
};
|
||||
|
||||
var stringifyDOM = module.stringifyDOM = function (dom) {
|
||||
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
|
||||
hjson[3] = {
|
||||
metadata: metadataMgr.getMetadataLazy()
|
||||
};
|
||||
/*hjson[3] = { TODO
|
||||
users: UserList.userData,
|
||||
defaultTitle: Title.defaultTitle,
|
||||
type: 'pad'
|
||||
}
|
||||
};
|
||||
if (!initializing) {
|
||||
hjson[3].metadata.title = Title.title;
|
||||
} else if (Cryptpad.initialName && !hjson[3].metadata.title) {
|
||||
hjson[3].metadata.title = Cryptpad.initialName;
|
||||
}*/
|
||||
return stringify(hjson);
|
||||
};
|
||||
|
||||
var realtimeOptions = {
|
||||
readOnly: readOnly,
|
||||
// really basic operational transform
|
||||
transformFunction : JsonOT.validate,
|
||||
// cryptpad debug logging (default is 1)
|
||||
// logLevel: 0,
|
||||
validateContent: function (content) {
|
||||
try {
|
||||
JSON.parse(content);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log("Failed to parse, rejecting patch");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var setHistory = function (bool, update) {
|
||||
isHistoryMode = bool;
|
||||
setEditable(!bool);
|
||||
if (!bool && update) {
|
||||
realtimeOptions.onRemote();
|
||||
}
|
||||
};
|
||||
|
||||
realtimeOptions.onRemote = function () {
|
||||
if (initializing) { return; }
|
||||
if (isHistoryMode) { return; }
|
||||
|
||||
var oldShjson = stringifyDOM(inner);
|
||||
|
||||
var shjson = module.realtime.getUserDoc();
|
||||
|
||||
// remember where the cursor is
|
||||
cursor.update();
|
||||
|
||||
// Update the user list (metadata) from the hyperjson
|
||||
// TODO Metadata.update(shjson);
|
||||
|
||||
var newInner = JSON.parse(shjson);
|
||||
var newSInner;
|
||||
if (newInner.length > 2) {
|
||||
newSInner = stringify(newInner[2]);
|
||||
}
|
||||
|
||||
if (newInner[3]) {
|
||||
metadataMgr.updateMetadata(newInner[3].metadata);
|
||||
}
|
||||
|
||||
// build a dom from HJSON, diff, and patch the editor
|
||||
applyHjson(shjson);
|
||||
|
||||
if (!readOnly) {
|
||||
var shjson2 = stringifyDOM(inner);
|
||||
|
||||
// TODO
|
||||
//shjson = JSON.stringify(JSON.parse(shjson).slice(0,3));
|
||||
|
||||
if (shjson2 !== shjson) {
|
||||
console.error("shjson2 !== shjson");
|
||||
module.patchText(shjson2);
|
||||
|
||||
/* pushing back over the wire is necessary, but it can
|
||||
result in a feedback loop, which we call a browser
|
||||
fight */
|
||||
if (module.logFights) {
|
||||
// what changed?
|
||||
var op = TextPatcher.diff(shjson, shjson2);
|
||||
// log the changes
|
||||
TextPatcher.log(shjson, op);
|
||||
var sop = JSON.stringify(TextPatcher.format(shjson, op));
|
||||
|
||||
var index = module.fights.indexOf(sop);
|
||||
if (index === -1) {
|
||||
module.fights.push(sop);
|
||||
console.log("Found a new type of browser disagreement");
|
||||
console.log("You can inspect the list in your " +
|
||||
"console at `REALTIME_MODULE.fights`");
|
||||
console.log(module.fights);
|
||||
} else {
|
||||
console.log("Encountered a known browser disagreement: " +
|
||||
"available at `REALTIME_MODULE.fights[%s]`", index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify only when the content has changed, not when someone has joined/left
|
||||
var oldSInner = stringify(JSON.parse(oldShjson)[2]);
|
||||
if (newSInner && newSInner !== oldSInner) {
|
||||
common.notify();
|
||||
}
|
||||
};
|
||||
|
||||
var exportFile = function () {
|
||||
var html = getHTML(inner);
|
||||
var suggestion = Title.suggestTitle('cryptpad-document');
|
||||
Cryptpad.prompt(Messages.exportPrompt,
|
||||
Cryptpad.fixFileName(suggestion) + '.html', function (filename) {
|
||||
if (!(typeof(filename) === 'string' && filename)) { return; }
|
||||
var blob = new Blob([html], {type: "text/html;charset=utf-8"});
|
||||
saveAs(blob, filename);
|
||||
});
|
||||
};
|
||||
var importFile = function (content) {
|
||||
var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body));
|
||||
applyHjson(shjson);
|
||||
realtimeOptions.onLocal();
|
||||
};
|
||||
|
||||
realtimeOptions.onInit = function (info) {
|
||||
readOnly = metadataMgr.getPrivateData().readOnly;
|
||||
console.log('onInit');
|
||||
var titleCfg = { getHeadingText: getHeadingText };
|
||||
Title = common.createTitle(titleCfg, realtimeOptions.onLocal, common, metadataMgr);
|
||||
var configTb = {
|
||||
displayed: ['userlist', 'title', 'useradmin', 'spinner', 'newpad', 'share', 'limit'],
|
||||
title: Title.getTitleConfig(),
|
||||
metadataMgr: metadataMgr,
|
||||
readOnly: readOnly,
|
||||
ifrw: window,
|
||||
realtime: info.realtime,
|
||||
common: Cryptpad,
|
||||
sfCommon: common,
|
||||
$container: $bar,
|
||||
$contentContainer: $('#cke_1_contents'),
|
||||
};
|
||||
toolbar = info.realtime.toolbar = Toolbar.create(configTb);
|
||||
Title.setToolbar(toolbar);
|
||||
|
||||
var $rightside = toolbar.$rightside;
|
||||
var $drawer = toolbar.$drawer;
|
||||
|
||||
var src = 'less!/customize/src/less/toolbar.less';
|
||||
require([
|
||||
src
|
||||
], function () {
|
||||
var $html = $bar.closest('html');
|
||||
$html
|
||||
.find('head style[data-original-src="' + src.replace(/less!/, '') + '"]')
|
||||
.appendTo($html.find('head'));
|
||||
});
|
||||
|
||||
$bar.find('#cke_1_toolbar_collapser').hide();
|
||||
if (!readOnly) {
|
||||
// Expand / collapse the toolbar
|
||||
var $collapse = Cryptpad.createButton(null, true);
|
||||
$collapse.removeClass('fa-question');
|
||||
var updateIcon = function () {
|
||||
$collapse.removeClass('fa-caret-down').removeClass('fa-caret-up');
|
||||
var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible');
|
||||
if (isCollapsed) {
|
||||
if (!initializing) { common.feedback('HIDETOOLBAR_PAD'); }
|
||||
$collapse.addClass('fa-caret-down');
|
||||
}
|
||||
else {
|
||||
if (!initializing) { common.feedback('SHOWTOOLBAR_PAD'); }
|
||||
$collapse.addClass('fa-caret-up');
|
||||
}
|
||||
};
|
||||
updateIcon();
|
||||
$collapse.click(function () {
|
||||
$(window).trigger('resize');
|
||||
$('.cke_toolbox_main').toggle();
|
||||
$(window).trigger('cryptpad-ck-toolbar');
|
||||
updateIcon();
|
||||
});
|
||||
$rightside.append($collapse);
|
||||
} else {
|
||||
$('.cke_toolbox_main').hide();
|
||||
}
|
||||
|
||||
/* add a history button */
|
||||
var histConfig = {
|
||||
onLocal: realtimeOptions.onLocal,
|
||||
onRemote: realtimeOptions.onRemote,
|
||||
setHistory: setHistory,
|
||||
applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); },
|
||||
$toolbar: $bar
|
||||
};
|
||||
var $hist = common.createButton('history', true, {histConfig: histConfig});
|
||||
$drawer.append($hist);
|
||||
|
||||
if (!metadataMgr.getPrivateData().isTemplate) {
|
||||
var templateObj = {
|
||||
rt: info.realtime,
|
||||
getTitle: function () { return metadataMgr.getMetadata().title; }
|
||||
};
|
||||
var $templateButton = common.createButton('template', true, templateObj);
|
||||
$rightside.append($templateButton);
|
||||
}
|
||||
|
||||
/* add an export button */
|
||||
var $export = Cryptpad.createButton('export', true, {}, exportFile);
|
||||
$drawer.append($export);
|
||||
|
||||
if (!readOnly) {
|
||||
/* add an import button */
|
||||
var $import = Cryptpad.createButton('import', true, {
|
||||
accept: 'text/html'
|
||||
}, importFile);
|
||||
$drawer.append($import);
|
||||
}
|
||||
|
||||
/* add a forget button */
|
||||
var forgetCb = function (err) {
|
||||
if (err) { return; }
|
||||
setEditable(false);
|
||||
};
|
||||
var $forgetPad = common.createButton('forget', true, {}, forgetCb);
|
||||
$rightside.append($forgetPad);
|
||||
};
|
||||
|
||||
// this should only ever get called once, when the chain syncs
|
||||
realtimeOptions.onReady = function (info) {
|
||||
console.log('onReady');
|
||||
if (!module.isMaximized) {
|
||||
module.isMaximized = true;
|
||||
$('iframe.cke_wysiwyg_frame').css('width', '');
|
||||
$('iframe.cke_wysiwyg_frame').css('height', '');
|
||||
}
|
||||
$('body').addClass('app-pad');
|
||||
|
||||
if (module.realtime !== info.realtime) {
|
||||
module.patchText = TextPatcher.create({
|
||||
realtime: info.realtime,
|
||||
//logging: true,
|
||||
});
|
||||
}
|
||||
|
||||
module.realtime = info.realtime;
|
||||
|
||||
var shjson = module.realtime.getUserDoc();
|
||||
|
||||
var newPad = false;
|
||||
if (shjson === '') { newPad = true; }
|
||||
|
||||
if (!newPad) {
|
||||
applyHjson(shjson);
|
||||
|
||||
// Update the user list (metadata) from the hyperjson
|
||||
// XXX Metadata.update(shjson);
|
||||
var parsed = JSON.parse(shjson);
|
||||
if (parsed[3] && parsed[3].metadata) {
|
||||
metadataMgr.updateMetadata(parsed[3].metadata);
|
||||
}
|
||||
|
||||
if (!readOnly) {
|
||||
var shjson2 = stringifyDOM(inner);
|
||||
var hjson2 = JSON.parse(shjson2).slice(0,3);
|
||||
var hjson = JSON.parse(shjson).slice(0,3);
|
||||
if (stringify(hjson2) !== stringify(hjson)) {
|
||||
console.log('err');
|
||||
console.error("shjson2 !== shjson");
|
||||
console.log(stringify(hjson2));
|
||||
console.log(stringify(hjson));
|
||||
Cryptpad.errorLoadingScreen(Messages.wrongApp);
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Title.updateTitle(Cryptpad.initialName || Title.defaultTitle);
|
||||
documentBody.innerHTML = Messages.initialState;
|
||||
}
|
||||
|
||||
Cryptpad.removeLoadingScreen(emitResize);
|
||||
setEditable(!readOnly);
|
||||
initializing = false;
|
||||
|
||||
if (readOnly) { return; }
|
||||
//TODO UserList.getLastName(toolbar.$userNameButton, newPad);
|
||||
onLocal();
|
||||
editor.focus();
|
||||
if (newPad) {
|
||||
cursor.setToEnd();
|
||||
} else {
|
||||
cursor.setToStart();
|
||||
}
|
||||
};
|
||||
|
||||
realtimeOptions.onConnectionChange = function (info) {
|
||||
setEditable(info.state);
|
||||
//toolbar.failed(); TODO
|
||||
if (info.state) {
|
||||
initializing = true;
|
||||
//toolbar.reconnecting(info.myId); // TODO
|
||||
Cryptpad.findOKButton().click();
|
||||
} else {
|
||||
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
|
||||
}
|
||||
};
|
||||
|
||||
realtimeOptions.onError = onConnectError;
|
||||
|
||||
onLocal = realtimeOptions.onLocal = function () {
|
||||
console.log('onlocal');
|
||||
if (initializing) { return; }
|
||||
if (isHistoryMode) { return; }
|
||||
if (readOnly) { return; }
|
||||
|
||||
// stringify the json and send it into chainpad
|
||||
var shjson = stringifyDOM(inner);
|
||||
|
||||
module.patchText(shjson);
|
||||
if (module.realtime.getUserDoc() !== shjson) {
|
||||
console.error("realtime.getUserDoc() !== shjson");
|
||||
}
|
||||
};
|
||||
|
||||
cpNfInner = common.startRealtime(realtimeOptions);
|
||||
metadataMgr = cpNfInner.metadataMgr;
|
||||
|
||||
Cryptpad.onLogout(function () { setEditable(false); });
|
||||
|
||||
/* hitting enter makes a new line, but places the cursor inside
|
||||
of the <br> instead of the <p>. This makes it such that you
|
||||
cannot type until you click, which is rather unnacceptable.
|
||||
If the cursor is ever inside such a <br>, you probably want
|
||||
to push it out to the parent element, which ought to be a
|
||||
paragraph tag. This needs to be done on keydown, otherwise
|
||||
the first such keypress will not be inserted into the P. */
|
||||
inner.addEventListener('keydown', cursor.brFix);
|
||||
|
||||
editor.on('change', onLocal);
|
||||
|
||||
// export the typing tests to the window.
|
||||
// call like `test = easyTest()`
|
||||
// terminate the test like `test.cancel()`
|
||||
window.easyTest = function () {
|
||||
cursor.update();
|
||||
var start = cursor.Range.start;
|
||||
var test = TypingTest.testInput(inner, start.el, start.offset, onLocal);
|
||||
onLocal();
|
||||
return test;
|
||||
};
|
||||
|
||||
$bar.find('.cke_button').click(function () {
|
||||
var e = this;
|
||||
var classString = e.getAttribute('class');
|
||||
var classes = classString.split(' ').filter(function (c) {
|
||||
return /cke_button__/.test(c);
|
||||
});
|
||||
|
||||
var id = classes[0];
|
||||
if (typeof(id) === 'string') {
|
||||
common.feedback(id.toUpperCase());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var main = function () {
|
||||
var Ckeditor;
|
||||
var editor;
|
||||
var common;
|
||||
|
||||
nThen(function (waitFor) {
|
||||
ckEditorAvailable(waitFor(function (ck) {
|
||||
Ckeditor = ck;
|
||||
require(['/pad/wysiwygarea-plugin.js'], waitFor());
|
||||
}));
|
||||
$(waitFor(function () {
|
||||
Cryptpad.addLoadingScreen();
|
||||
}));
|
||||
SFCommon.create(waitFor(function (c) { module.common = common = c; }));
|
||||
}).nThen(function (waitFor) {
|
||||
Ckeditor.config.toolbarCanCollapse = true;
|
||||
if (screen.height < 800) {
|
||||
Ckeditor.config.toolbarStartupExpanded = false;
|
||||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no');
|
||||
} else {
|
||||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes');
|
||||
}
|
||||
// Used in ckeditor-config.js
|
||||
Ckeditor.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs;
|
||||
module.ckeditor = editor = Ckeditor.replace('editor1', {
|
||||
customConfig: '/customize/ckeditor-config.js',
|
||||
});
|
||||
editor.on('instanceReady', waitFor());
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
/*if (Ckeditor.env.safari) {
|
||||
var fixIframe = function () {
|
||||
$('iframe.cke_wysiwyg_frame').height($('#cke_1_contents').height());
|
||||
};
|
||||
$(window).resize(fixIframe);
|
||||
fixIframe();
|
||||
}*/
|
||||
Links.addSupportForOpeningLinksInNewTab(Ckeditor)({editor: editor});
|
||||
Cryptpad.onError(function (info) {
|
||||
if (info && info.type === "store") {
|
||||
onConnectError();
|
||||
}
|
||||
});
|
||||
andThen(editor, Ckeditor, common);
|
||||
});
|
||||
};
|
||||
main();
|
||||
});
|
||||
|
||||
@@ -29,7 +29,8 @@ define(['/common/cryptpad-common.js'], function (Cryptpad) {
|
||||
if (anchor) {
|
||||
var href = anchor.getAttribute('href');
|
||||
if (href) {
|
||||
window.open(href);
|
||||
var bounceHref = window.location.origin + '/bounce/#' + encodeURIComponent(href);
|
||||
window.open(bounceHref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
971
www/pad/main.js
971
www/pad/main.js
@@ -1,768 +1,243 @@
|
||||
// Load #1, load as little as possible because we are in a race to get the loading screen up.
|
||||
define([
|
||||
'/bower_components/nthen/index.js',
|
||||
'/api/config',
|
||||
'jquery',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/bower_components/chainpad-netflux/chainpad-netflux.js',
|
||||
'/bower_components/hyperjson/hyperjson.js',
|
||||
'/common/toolbar2.js',
|
||||
'/common/cursor.js',
|
||||
'/bower_components/chainpad-json-validator/json-ot.js',
|
||||
'/common/TypingTests.js',
|
||||
'json.sortify',
|
||||
'/bower_components/textpatcher/TextPatcher.js',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/cryptget.js',
|
||||
'/pad/links.js',
|
||||
'/bower_components/file-saver/FileSaver.min.js',
|
||||
'/bower_components/diff-dom/diffDOM.js',
|
||||
'/common/requireconfig.js'
|
||||
], function (nThen, ApiConfig, $, RequireConfig) {
|
||||
var requireConfig = RequireConfig();
|
||||
|
||||
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||
'less!/customize/src/less/cryptpad.less',
|
||||
], function ($, Crypto, realtimeInput, Hyperjson,
|
||||
Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links) {
|
||||
var saveAs = window.saveAs;
|
||||
var Messages = Cryptpad.Messages;
|
||||
// Loaded in load #2
|
||||
var CpNfOuter;
|
||||
var Cryptpad;
|
||||
var Crypto;
|
||||
var Cryptget;
|
||||
|
||||
$(function () {
|
||||
var sframeChan;
|
||||
var secret;
|
||||
var hashes;
|
||||
|
||||
var ifrw = $('#pad-iframe')[0].contentWindow;
|
||||
var Ckeditor; // to be initialized later...
|
||||
var DiffDom = window.diffDOM;
|
||||
nThen(function (waitFor) {
|
||||
$(waitFor());
|
||||
}).nThen(function (waitFor) {
|
||||
var req = {
|
||||
cfg: requireConfig,
|
||||
req: [ '/common/loading.js' ],
|
||||
pfx: window.location.origin
|
||||
};
|
||||
window.rc = requireConfig;
|
||||
window.apiconf = ApiConfig;
|
||||
$('#sbox-iframe').attr('src',
|
||||
ApiConfig.httpSafeOrigin + '/pad/inner.html?' + requireConfig.urlArgs +
|
||||
'#' + encodeURIComponent(JSON.stringify(req)));
|
||||
|
||||
Cryptpad.addLoadingScreen();
|
||||
// This is a cheap trick to avoid loading sframe-channel in parallel with the
|
||||
// loading screen setup.
|
||||
var done = waitFor();
|
||||
var onMsg = function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (data.q !== 'READY') { return; }
|
||||
window.removeEventListener('message', onMsg);
|
||||
var _done = done;
|
||||
done = function () { };
|
||||
_done();
|
||||
};
|
||||
window.addEventListener('message', onMsg);
|
||||
|
||||
var stringify = function (obj) {
|
||||
return JSONSortify(obj);
|
||||
};
|
||||
|
||||
window.Toolbar = Toolbar;
|
||||
window.Hyperjson = Hyperjson;
|
||||
|
||||
var slice = function (coll) {
|
||||
return Array.prototype.slice.call(coll);
|
||||
};
|
||||
|
||||
var removeListeners = function (root) {
|
||||
slice(root.attributes).map(function (attr) {
|
||||
if (/^on/i.test(attr.name)) {
|
||||
root.attributes.removeNamedItem(attr.name);
|
||||
}
|
||||
});
|
||||
slice(root.children).forEach(removeListeners);
|
||||
};
|
||||
|
||||
var hjsonToDom = function (H) {
|
||||
var dom = Hyperjson.toDOM(H);
|
||||
removeListeners(dom);
|
||||
return dom;
|
||||
};
|
||||
|
||||
var module = window.REALTIME_MODULE = window.APP = {
|
||||
Hyperjson: Hyperjson,
|
||||
TextPatcher: TextPatcher,
|
||||
logFights: true,
|
||||
fights: [],
|
||||
Cryptpad: Cryptpad,
|
||||
Cursor: Cursor,
|
||||
};
|
||||
|
||||
var emitResize = module.emitResize = function () {
|
||||
var cw = $('#pad-iframe')[0].contentWindow;
|
||||
|
||||
var evt = cw.document.createEvent('UIEvents');
|
||||
evt.initUIEvent('resize', true, false, cw, 0);
|
||||
cw.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
var toolbar;
|
||||
|
||||
var isNotMagicLine = function (el) {
|
||||
return !(el && typeof(el.getAttribute) === 'function' &&
|
||||
el.getAttribute('class') &&
|
||||
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1);
|
||||
};
|
||||
|
||||
/* catch `type="_moz"` before it goes over the wire */
|
||||
var brFilter = function (hj) {
|
||||
if (hj[1].type === '_moz') { hj[1].type = undefined; }
|
||||
return hj;
|
||||
};
|
||||
|
||||
var onConnectError = function () {
|
||||
Cryptpad.errorLoadingScreen(Messages.websocketError);
|
||||
};
|
||||
|
||||
var andThen = function (Ckeditor) {
|
||||
var $iframe = $('#pad-iframe').contents();
|
||||
var secret = Cryptpad.getSecrets();
|
||||
}).nThen(function (waitFor) {
|
||||
// Load #2, the loading screen is up so grab whatever you need...
|
||||
require([
|
||||
'/common/sframe-chainpad-netflux-outer.js',
|
||||
'/common/cryptpad-common.js',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/common/cryptget.js',
|
||||
'/common/sframe-channel.js',
|
||||
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, SFrameChannel) {
|
||||
CpNfOuter = _CpNfOuter;
|
||||
Cryptpad = _Cryptpad;
|
||||
Crypto = _Crypto;
|
||||
Cryptget = _Cryptget;
|
||||
SFrameChannel.create($('#sbox-iframe')[0].contentWindow, waitFor(function (sfc) {
|
||||
sframeChan = sfc;
|
||||
}));
|
||||
Cryptpad.ready(waitFor());
|
||||
}));
|
||||
}).nThen(function (waitFor) {
|
||||
secret = Cryptpad.getSecrets();
|
||||
if (!secret.channel) {
|
||||
// New pad: create a new random channel id
|
||||
secret.channel = Cryptpad.createChannelId();
|
||||
}
|
||||
Cryptpad.getShareHashes(secret, waitFor(function (err, h) { hashes = h; }));
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
var readOnly = secret.keys && !secret.keys.editKeyStr;
|
||||
if (!secret.keys) {
|
||||
secret.keys = secret.key;
|
||||
}
|
||||
|
||||
var editor = window.editor = Ckeditor.replace('editor1', {
|
||||
customConfig: '/customize/ckeditor-config.js',
|
||||
});
|
||||
|
||||
editor.on('instanceReady', Links.addSupportForOpeningLinksInNewTab(Ckeditor));
|
||||
editor.on('instanceReady', function () {
|
||||
var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox');
|
||||
|
||||
var $html = $bar.closest('html');
|
||||
var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]');
|
||||
if ($faLink.length) {
|
||||
$html.find('iframe').contents().find('head').append($faLink.clone());
|
||||
}
|
||||
var isHistoryMode = false;
|
||||
|
||||
if (readOnly) {
|
||||
$('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox > .cke_toolbox_main').hide();
|
||||
}
|
||||
|
||||
/* add a class to the magicline plugin so we can pick it out more easily */
|
||||
|
||||
var ml = $('iframe')[0].contentWindow.CKEDITOR.instances.editor1.plugins.magicline
|
||||
.backdoor.that.line.$;
|
||||
|
||||
[ml, ml.parentElement].forEach(function (el) {
|
||||
el.setAttribute('class', 'non-realtime');
|
||||
});
|
||||
|
||||
var documentBody = ifrw.$('iframe')[0].contentDocument.body;
|
||||
|
||||
var inner = window.inner = documentBody;
|
||||
|
||||
// hide all content until the realtime doc is ready
|
||||
$(inner).css({
|
||||
color: '#fff',
|
||||
});
|
||||
|
||||
var cursor = module.cursor = Cursor(inner);
|
||||
|
||||
var setEditable = module.setEditable = function (bool) {
|
||||
if (bool) {
|
||||
$(inner).css({
|
||||
color: '#333',
|
||||
});
|
||||
}
|
||||
if (!readOnly || !bool) {
|
||||
inner.setAttribute('contenteditable', bool);
|
||||
}
|
||||
};
|
||||
|
||||
// don't let the user edit until the pad is ready
|
||||
setEditable(false);
|
||||
|
||||
var forbiddenTags = [
|
||||
'SCRIPT',
|
||||
'IFRAME',
|
||||
'OBJECT',
|
||||
'APPLET',
|
||||
'VIDEO',
|
||||
'AUDIO'
|
||||
];
|
||||
|
||||
var diffOptions = {
|
||||
preDiffApply: function (info) {
|
||||
/*
|
||||
Don't accept attributes that begin with 'on'
|
||||
these are probably listeners, and we don't want to
|
||||
send scripts over the wire.
|
||||
*/
|
||||
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) {
|
||||
if (info.diff.name === 'href') {
|
||||
// console.log(info.diff);
|
||||
//var href = info.diff.newValue;
|
||||
|
||||
// TODO normalize HTML entities
|
||||
if (/javascript *: */.test(info.diff.newValue)) {
|
||||
// TODO remove javascript: links
|
||||
}
|
||||
}
|
||||
|
||||
if (/^on/.test(info.diff.name)) {
|
||||
console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/*
|
||||
Also reject any elements which would insert any one of
|
||||
our forbidden tag types: script, iframe, object,
|
||||
applet, video, or audio
|
||||
*/
|
||||
if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) {
|
||||
if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) {
|
||||
console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName);
|
||||
return true;
|
||||
} else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) {
|
||||
console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.node && info.node.tagName === 'BODY') {
|
||||
if (info.diff.action === 'removeAttribute' &&
|
||||
['class', 'spellcheck'].indexOf(info.diff.name) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* DiffDOM will filter out magicline plugin elements
|
||||
in practice this will make it impossible to use it
|
||||
while someone else is typing, which could be annoying.
|
||||
|
||||
we should check when such an element is going to be
|
||||
removed, and prevent that from happening. */
|
||||
if (info.node && info.node.tagName === 'SPAN' &&
|
||||
info.node.getAttribute('contentEditable') === "false") {
|
||||
// it seems to be a magicline plugin element...
|
||||
if (info.diff.action === 'removeElement') {
|
||||
// and you're about to remove it...
|
||||
// this probably isn't what you want
|
||||
|
||||
/*
|
||||
I have never seen this in the console, but the
|
||||
magic line is still getting removed on remote
|
||||
edits. This suggests that it's getting removed
|
||||
by something other than diffDom.
|
||||
*/
|
||||
console.log("preventing removal of the magic line!");
|
||||
|
||||
// return true to prevent diff application
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not change the contenteditable value in view mode
|
||||
if (readOnly && info.node && info.node.tagName === 'BODY' &&
|
||||
info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// no use trying to recover the cursor if it doesn't exist
|
||||
if (!cursor.exists()) { return; }
|
||||
|
||||
/* frame is either 0, 1, 2, or 3, depending on which
|
||||
cursor frames were affected: none, first, last, or both
|
||||
*/
|
||||
var frame = info.frame = cursor.inNode(info.node);
|
||||
|
||||
if (!frame) { return; }
|
||||
|
||||
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
|
||||
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);
|
||||
|
||||
if (frame & 1) {
|
||||
// push cursor start if necessary
|
||||
if (pushes.commonStart < cursor.Range.start.offset) {
|
||||
cursor.Range.start.offset += pushes.delta;
|
||||
}
|
||||
}
|
||||
if (frame & 2) {
|
||||
// push cursor end if necessary
|
||||
if (pushes.commonStart < cursor.Range.end.offset) {
|
||||
cursor.Range.end.offset += pushes.delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
postDiffApply: function (info) {
|
||||
if (info.frame) {
|
||||
if (info.node) {
|
||||
if (info.frame & 1) { cursor.fixStart(info.node); }
|
||||
if (info.frame & 2) { cursor.fixEnd(info.node); }
|
||||
} else { console.error("info.node did not exist"); }
|
||||
|
||||
var sel = cursor.makeSelection();
|
||||
var range = cursor.makeRange();
|
||||
|
||||
cursor.fixSelection(sel, range);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var initializing = true;
|
||||
|
||||
var Title;
|
||||
var UserList;
|
||||
var Metadata;
|
||||
|
||||
var getHeadingText = function () {
|
||||
var text;
|
||||
if (['h1', 'h2', 'h3'].some(function (t) {
|
||||
var $header = $(inner).find(t + ':first-of-type');
|
||||
if ($header.length && $header.text()) {
|
||||
text = $header.text();
|
||||
return true;
|
||||
}
|
||||
})) { return text; }
|
||||
};
|
||||
|
||||
var DD = new DiffDom(diffOptions);
|
||||
|
||||
var openLink = function (e) {
|
||||
var el = e.currentTarget;
|
||||
if (!el || el.nodeName !== 'A') { return; }
|
||||
var href = el.getAttribute('href');
|
||||
if (href) { window.open(href, '_blank'); }
|
||||
};
|
||||
|
||||
// apply patches, and try not to lose the cursor in the process!
|
||||
var applyHjson = function (shjson) {
|
||||
var userDocStateDom = hjsonToDom(JSON.parse(shjson));
|
||||
|
||||
if (!readOnly && !initializing) {
|
||||
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
|
||||
}
|
||||
$(userDocStateDom).find('script, applet, object, iframe').remove();
|
||||
$(userDocStateDom).find('a').filter(function (i, x) {
|
||||
return ! /^(https|http|ftp):\/\/[^\s\n]*$/.test(x.getAttribute('href'));
|
||||
}).remove();
|
||||
var patch = (DD).diff(inner, userDocStateDom);
|
||||
(DD).apply(inner, patch);
|
||||
if (readOnly) {
|
||||
var $links = $(inner).find('a');
|
||||
// off so that we don't end up with multiple identical handlers
|
||||
$links.off('click', openLink).on('click', openLink);
|
||||
}
|
||||
};
|
||||
|
||||
var stringifyDOM = module.stringifyDOM = function (dom) {
|
||||
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
|
||||
hjson[3] = {
|
||||
metadata: {
|
||||
users: UserList.userData,
|
||||
defaultTitle: Title.defaultTitle,
|
||||
type: 'pad'
|
||||
}
|
||||
};
|
||||
if (!initializing) {
|
||||
hjson[3].metadata.title = Title.title;
|
||||
} else if (Cryptpad.initialName && !hjson[3].metadata.title) {
|
||||
hjson[3].metadata.title = Cryptpad.initialName;
|
||||
}
|
||||
return stringify(hjson);
|
||||
};
|
||||
|
||||
var realtimeOptions = {
|
||||
// the websocket URL
|
||||
websocketURL: Cryptpad.getWebsocketURL(),
|
||||
|
||||
// the channel we will communicate over
|
||||
channel: secret.channel,
|
||||
|
||||
// the nework used for the file store if it exists
|
||||
network: Cryptpad.getNetwork(),
|
||||
|
||||
// our public key
|
||||
validateKey: secret.keys.validateKey || undefined,
|
||||
readOnly: readOnly,
|
||||
|
||||
// Pass in encrypt and decrypt methods
|
||||
crypto: Crypto.createEncryptor(secret.keys),
|
||||
|
||||
// really basic operational transform
|
||||
transformFunction : JsonOT.validate,
|
||||
|
||||
// cryptpad debug logging (default is 1)
|
||||
// logLevel: 0,
|
||||
|
||||
validateContent: function (content) {
|
||||
try {
|
||||
JSON.parse(content);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log("Failed to parse, rejecting patch");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var setHistory = function (bool, update) {
|
||||
isHistoryMode = bool;
|
||||
setEditable(!bool);
|
||||
if (!bool && update) {
|
||||
realtimeOptions.onRemote();
|
||||
}
|
||||
};
|
||||
|
||||
realtimeOptions.onRemote = function () {
|
||||
if (initializing) { return; }
|
||||
if (isHistoryMode) { return; }
|
||||
|
||||
var oldShjson = stringifyDOM(inner);
|
||||
|
||||
var shjson = module.realtime.getUserDoc();
|
||||
|
||||
// remember where the cursor is
|
||||
cursor.update();
|
||||
|
||||
// Update the user list (metadata) from the hyperjson
|
||||
Metadata.update(shjson);
|
||||
|
||||
var newInner = JSON.parse(shjson);
|
||||
var newSInner;
|
||||
if (newInner.length > 2) {
|
||||
newSInner = stringify(newInner[2]);
|
||||
}
|
||||
|
||||
// build a dom from HJSON, diff, and patch the editor
|
||||
applyHjson(shjson);
|
||||
|
||||
if (!readOnly) {
|
||||
var shjson2 = stringifyDOM(inner);
|
||||
if (shjson2 !== shjson) {
|
||||
console.error("shjson2 !== shjson");
|
||||
module.patchText(shjson2);
|
||||
|
||||
/* pushing back over the wire is necessary, but it can
|
||||
result in a feedback loop, which we call a browser
|
||||
fight */
|
||||
if (module.logFights) {
|
||||
// what changed?
|
||||
var op = TextPatcher.diff(shjson, shjson2);
|
||||
// log the changes
|
||||
TextPatcher.log(shjson, op);
|
||||
var sop = JSON.stringify(TextPatcher.format(shjson, op));
|
||||
|
||||
var index = module.fights.indexOf(sop);
|
||||
if (index === -1) {
|
||||
module.fights.push(sop);
|
||||
console.log("Found a new type of browser disagreement");
|
||||
console.log("You can inspect the list in your " +
|
||||
"console at `REALTIME_MODULE.fights`");
|
||||
console.log(module.fights);
|
||||
} else {
|
||||
console.log("Encountered a known browser disagreement: " +
|
||||
"available at `REALTIME_MODULE.fights[%s]`", index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify only when the content has changed, not when someone has joined/left
|
||||
var oldSInner = stringify(JSON.parse(oldShjson)[2]);
|
||||
if (newSInner && newSInner !== oldSInner) {
|
||||
Cryptpad.notify();
|
||||
}
|
||||
};
|
||||
|
||||
var getHTML = function () {
|
||||
return ('<!DOCTYPE html>\n' + '<html>\n' + inner.innerHTML);
|
||||
};
|
||||
|
||||
var domFromHTML = function (html) {
|
||||
return new DOMParser().parseFromString(html, 'text/html');
|
||||
};
|
||||
|
||||
var exportFile = function () {
|
||||
var html = getHTML();
|
||||
var suggestion = Title.suggestTitle('cryptpad-document');
|
||||
Cryptpad.prompt(Messages.exportPrompt,
|
||||
Cryptpad.fixFileName(suggestion) + '.html', function (filename) {
|
||||
if (!(typeof(filename) === 'string' && filename)) { return; }
|
||||
var blob = new Blob([html], {type: "text/html;charset=utf-8"});
|
||||
saveAs(blob, filename);
|
||||
});
|
||||
};
|
||||
var importFile = function (content) {
|
||||
var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body));
|
||||
applyHjson(shjson);
|
||||
realtimeOptions.onLocal();
|
||||
};
|
||||
|
||||
realtimeOptions.onInit = function (info) {
|
||||
UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad);
|
||||
|
||||
var titleCfg = { getHeadingText: getHeadingText };
|
||||
Title = Cryptpad.createTitle(titleCfg, realtimeOptions.onLocal, Cryptpad);
|
||||
|
||||
Metadata = Cryptpad.createMetadata(UserList, Title, null, Cryptpad);
|
||||
|
||||
var configTb = {
|
||||
displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'],
|
||||
userList: UserList.getToolbarConfig(),
|
||||
share: {
|
||||
secret: secret,
|
||||
channel: info.channel
|
||||
if (!secret.keys) { secret.keys = secret.key; }
|
||||
var parsed = Cryptpad.parsePadUrl(window.location.href);
|
||||
if (!parsed.type) { throw new Error(); }
|
||||
var defaultTitle = Cryptpad.getDefaultName(parsed);
|
||||
var updateMeta = function () {
|
||||
//console.log('EV_METADATA_UPDATE');
|
||||
var name;
|
||||
nThen(function (waitFor) {
|
||||
Cryptpad.getLastName(waitFor(function (err, n) {
|
||||
if (err) { console.log(err); }
|
||||
name = n;
|
||||
}));
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
sframeChan.event('EV_METADATA_UPDATE', {
|
||||
doc: {
|
||||
defaultTitle: defaultTitle,
|
||||
type: parsed.type
|
||||
},
|
||||
title: Title.getTitleConfig(),
|
||||
common: Cryptpad,
|
||||
readOnly: readOnly,
|
||||
ifrw: ifrw,
|
||||
realtime: info.realtime,
|
||||
network: info.network,
|
||||
$container: $bar,
|
||||
$contentContainer: $iframe.find('#cke_1_contents'),
|
||||
};
|
||||
toolbar = info.realtime.toolbar = Toolbar.create(configTb);
|
||||
|
||||
var src = 'less!/customize/src/less/toolbar.less';
|
||||
require([
|
||||
src
|
||||
], function () {
|
||||
var $html = $bar.closest('html');
|
||||
$html
|
||||
.find('head style[data-original-src="' + src.replace(/less!/, '') + '"]')
|
||||
.appendTo($html.find('head'));
|
||||
});
|
||||
|
||||
Title.setToolbar(toolbar);
|
||||
|
||||
var $rightside = toolbar.$rightside;
|
||||
var $drawer = toolbar.$drawer;
|
||||
|
||||
var editHash;
|
||||
|
||||
if (!readOnly) {
|
||||
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys);
|
||||
}
|
||||
|
||||
$bar.find('#cke_1_toolbar_collapser').hide();
|
||||
if (!readOnly) {
|
||||
// Expand / collapse the toolbar
|
||||
var $collapse = Cryptpad.createButton(null, true);
|
||||
$collapse.removeClass('fa-question');
|
||||
var updateIcon = function () {
|
||||
$collapse.removeClass('fa-caret-down').removeClass('fa-caret-up');
|
||||
$collapse.attr('title', '');
|
||||
var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible');
|
||||
if (isCollapsed) {
|
||||
if (!initializing) { Cryptpad.feedback('HIDETOOLBAR_PAD'); }
|
||||
$collapse.addClass('fa-caret-down');
|
||||
$collapse.attr('title', Messages.pad_showToolbar);
|
||||
}
|
||||
else {
|
||||
if (!initializing) { Cryptpad.feedback('SHOWTOOLBAR_PAD'); }
|
||||
$collapse.addClass('fa-caret-up');
|
||||
$collapse.attr('title', Messages.pad_hideToolbar);
|
||||
}
|
||||
};
|
||||
updateIcon();
|
||||
$collapse.click(function () {
|
||||
$(window).trigger('resize');
|
||||
$iframe.find('.cke_toolbox_main').toggle();
|
||||
$(window).trigger('cryptpad-ck-toolbar');
|
||||
updateIcon();
|
||||
});
|
||||
$rightside.append($collapse);
|
||||
}
|
||||
|
||||
/* add a history button */
|
||||
var histConfig = {
|
||||
onLocal: realtimeOptions.onLocal,
|
||||
onRemote: realtimeOptions.onRemote,
|
||||
setHistory: setHistory,
|
||||
applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); },
|
||||
$toolbar: $bar
|
||||
};
|
||||
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig});
|
||||
$drawer.append($hist);
|
||||
|
||||
/* save as template */
|
||||
if (!Cryptpad.isTemplate(window.location.href)) {
|
||||
var templateObj = {
|
||||
rt: info.realtime,
|
||||
Crypt: Cryptget,
|
||||
getTitle: function () { return document.title; }
|
||||
};
|
||||
var $templateButton = Cryptpad.createButton('template', true, templateObj);
|
||||
$rightside.append($templateButton);
|
||||
}
|
||||
|
||||
/* add an export button */
|
||||
var $export = Cryptpad.createButton('export', true, {}, exportFile);
|
||||
$drawer.append($export);
|
||||
|
||||
if (!readOnly) {
|
||||
/* add an import button */
|
||||
var $import = Cryptpad.createButton('import', true, {
|
||||
accept: 'text/html'
|
||||
}, importFile);
|
||||
$drawer.append($import);
|
||||
}
|
||||
|
||||
/* add a forget button */
|
||||
var forgetCb = function (err) {
|
||||
if (err) { return; }
|
||||
setEditable(false);
|
||||
};
|
||||
var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb);
|
||||
$rightside.append($forgetPad);
|
||||
|
||||
// set the hash
|
||||
if (!readOnly) { Cryptpad.replaceHash(editHash); }
|
||||
};
|
||||
|
||||
// this should only ever get called once, when the chain syncs
|
||||
realtimeOptions.onReady = function (info) {
|
||||
if (!module.isMaximized) {
|
||||
module.isMaximized = true;
|
||||
$iframe.find('iframe.cke_wysiwyg_frame').css('width', '');
|
||||
$iframe.find('iframe.cke_wysiwyg_frame').css('height', '');
|
||||
}
|
||||
$iframe.find('body').addClass('app-pad');
|
||||
|
||||
if (module.realtime !== info.realtime) {
|
||||
module.patchText = TextPatcher.create({
|
||||
realtime: info.realtime,
|
||||
//logging: true,
|
||||
});
|
||||
}
|
||||
|
||||
module.realtime = info.realtime;
|
||||
|
||||
var shjson = module.realtime.getUserDoc();
|
||||
|
||||
var newPad = false;
|
||||
if (shjson === '') { newPad = true; }
|
||||
|
||||
if (!newPad) {
|
||||
applyHjson(shjson);
|
||||
|
||||
// Update the user list (metadata) from the hyperjson
|
||||
Metadata.update(shjson);
|
||||
|
||||
if (!readOnly) {
|
||||
var shjson2 = stringifyDOM(inner);
|
||||
var hjson2 = JSON.parse(shjson2).slice(0,-1);
|
||||
var hjson = JSON.parse(shjson).slice(0,-1);
|
||||
if (stringify(hjson2) !== stringify(hjson)) {
|
||||
console.log('err');
|
||||
console.error("shjson2 !== shjson");
|
||||
// TODO(cjd): This is removed because the XSS filter in applyHjson()
|
||||
// is applied on incoming content so it causes this to fail.
|
||||
//Cryptpad.errorLoadingScreen(Messages.wrongApp);
|
||||
//throw new Error();
|
||||
}
|
||||
user: {
|
||||
name: name,
|
||||
uid: Cryptpad.getUid(),
|
||||
avatar: Cryptpad.getAvatarUrl(),
|
||||
profile: Cryptpad.getProfileUrl(),
|
||||
curvePublic: Cryptpad.getProxy().curvePublic,
|
||||
netfluxId: Cryptpad.getNetwork().webChannels[0].myID,
|
||||
},
|
||||
priv: {
|
||||
accountName: Cryptpad.getAccountName(),
|
||||
origin: window.location.origin,
|
||||
pathname: window.location.pathname,
|
||||
readOnly: readOnly,
|
||||
availableHashes: hashes,
|
||||
isTemplate: Cryptpad.isTemplate(window.location.href),
|
||||
feedbackAllowed: Cryptpad.isFeedbackAllowed(),
|
||||
friends: Cryptpad.getProxy().friends || {}
|
||||
}
|
||||
} else {
|
||||
Title.updateTitle(Cryptpad.initialName || Title.defaultTitle);
|
||||
documentBody.innerHTML = Messages.initialState;
|
||||
}
|
||||
|
||||
Cryptpad.removeLoadingScreen(emitResize);
|
||||
setEditable(!readOnly);
|
||||
initializing = false;
|
||||
|
||||
if (readOnly) { return; }
|
||||
UserList.getLastName(toolbar.$userNameButton, newPad);
|
||||
editor.focus();
|
||||
if (newPad) {
|
||||
cursor.setToEnd();
|
||||
} else {
|
||||
cursor.setToStart();
|
||||
}
|
||||
};
|
||||
|
||||
realtimeOptions.onAbort = function () {
|
||||
console.log("Aborting the session!");
|
||||
// stop the user from continuing to edit
|
||||
setEditable(false);
|
||||
toolbar.failed();
|
||||
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
|
||||
};
|
||||
|
||||
realtimeOptions.onConnectionChange = function (info) {
|
||||
setEditable(info.state);
|
||||
toolbar.failed();
|
||||
if (info.state) {
|
||||
initializing = true;
|
||||
toolbar.reconnecting(info.myId);
|
||||
Cryptpad.findOKButton().click();
|
||||
} else {
|
||||
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
|
||||
}
|
||||
};
|
||||
|
||||
realtimeOptions.onError = onConnectError;
|
||||
|
||||
var onLocal = realtimeOptions.onLocal = function () {
|
||||
if (initializing) { return; }
|
||||
if (isHistoryMode) { return; }
|
||||
if (readOnly) { return; }
|
||||
|
||||
// stringify the json and send it into chainpad
|
||||
var shjson = stringifyDOM(inner);
|
||||
|
||||
module.patchText(shjson);
|
||||
if (module.realtime.getUserDoc() !== shjson) {
|
||||
console.error("realtime.getUserDoc() !== shjson");
|
||||
}
|
||||
};
|
||||
|
||||
module.realtimeInput = realtimeInput.start(realtimeOptions);
|
||||
|
||||
Cryptpad.onLogout(function () { setEditable(false); });
|
||||
|
||||
/* hitting enter makes a new line, but places the cursor inside
|
||||
of the <br> instead of the <p>. This makes it such that you
|
||||
cannot type until you click, which is rather unnacceptable.
|
||||
If the cursor is ever inside such a <br>, you probably want
|
||||
to push it out to the parent element, which ought to be a
|
||||
paragraph tag. This needs to be done on keydown, otherwise
|
||||
the first such keypress will not be inserted into the P. */
|
||||
inner.addEventListener('keydown', cursor.brFix);
|
||||
|
||||
editor.on('change', onLocal);
|
||||
|
||||
// export the typing tests to the window.
|
||||
// call like `test = easyTest()`
|
||||
// terminate the test like `test.cancel()`
|
||||
window.easyTest = function () {
|
||||
cursor.update();
|
||||
var start = cursor.Range.start;
|
||||
var test = TypingTest.testInput(inner, start.el, start.offset, onLocal);
|
||||
onLocal();
|
||||
return test;
|
||||
};
|
||||
|
||||
$bar.find('.cke_button').click(function () {
|
||||
var e = this;
|
||||
var classString = e.getAttribute('class');
|
||||
var classes = classString.split(' ').filter(function (c) {
|
||||
return /cke_button__/.test(c);
|
||||
});
|
||||
});
|
||||
};
|
||||
Cryptpad.onDisplayNameChanged(updateMeta);
|
||||
sframeChan.onReg('EV_METADATA_UPDATE', updateMeta);
|
||||
|
||||
var id = classes[0];
|
||||
if (typeof(id) === 'string') {
|
||||
Cryptpad.feedback(id.toUpperCase());
|
||||
}
|
||||
Cryptpad.onError(function (info) {
|
||||
console.log('error');
|
||||
console.log(info);
|
||||
if (info && info.type === "store") {
|
||||
//onConnectError();
|
||||
}
|
||||
});
|
||||
|
||||
sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) {
|
||||
Cryptpad.anonRpcMsg(data.msg, data.content, function (err, response) {
|
||||
cb({error: err, response: response});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var interval = 100;
|
||||
var second = function (Ckeditor) {
|
||||
Cryptpad.ready(function () {
|
||||
andThen(Ckeditor);
|
||||
Cryptpad.reportAppUsage();
|
||||
sframeChan.on('Q_SET_PAD_TITLE_IN_DRIVE', function (newTitle, cb) {
|
||||
document.title = newTitle;
|
||||
Cryptpad.renamePad(newTitle, undefined, function (err) {
|
||||
if (err) { cb('ERROR'); } else { cb(); }
|
||||
});
|
||||
});
|
||||
Cryptpad.onError(function (info) {
|
||||
if (info && info.type === "store") {
|
||||
onConnectError();
|
||||
|
||||
sframeChan.on('Q_SETTINGS_SET_DISPLAY_NAME', function (newName, cb) {
|
||||
Cryptpad.setAttribute('username', newName, function (err) {
|
||||
if (err) {
|
||||
console.log("Couldn't set username");
|
||||
console.error(err);
|
||||
cb('ERROR');
|
||||
return;
|
||||
}
|
||||
Cryptpad.changeDisplayName(newName, true);
|
||||
cb();
|
||||
});
|
||||
});
|
||||
|
||||
sframeChan.on('Q_LOGOUT', function (data, cb) {
|
||||
Cryptpad.logout(cb);
|
||||
});
|
||||
|
||||
sframeChan.on('EV_NOTIFY', function () {
|
||||
Cryptpad.notify();
|
||||
});
|
||||
|
||||
sframeChan.on('Q_SET_LOGIN_REDIRECT', function (data, cb) {
|
||||
sessionStorage.redirectTo = window.location.href;
|
||||
cb();
|
||||
});
|
||||
|
||||
sframeChan.on('Q_GET_PIN_LIMIT_STATUS', function (data, cb) {
|
||||
Cryptpad.isOverPinLimit(function (e, overLimit, limits) {
|
||||
cb({
|
||||
error: e,
|
||||
overLimit: overLimit,
|
||||
limits: limits
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
sframeChan.on('Q_MOVE_TO_TRASH', function (data, cb) {
|
||||
Cryptpad.moveToTrash(cb);
|
||||
});
|
||||
|
||||
sframeChan.on('Q_SAVE_AS_TEMPLATE', function (data, cb) {
|
||||
Cryptpad.saveAsTemplate(Cryptget.put, data, cb);
|
||||
});
|
||||
|
||||
sframeChan.on('Q_SEND_FRIEND_REQUEST', function (netfluxId, cb) {
|
||||
Cryptpad.inviteFromUserlist(Cryptpad, netfluxId);
|
||||
cb();
|
||||
});
|
||||
Cryptpad.onFriendRequest = function (confirmText, cb) {
|
||||
sframeChan.query('Q_INCOMING_FRIEND_REQUEST', confirmText, function (err, data) {
|
||||
cb(data);
|
||||
});
|
||||
};
|
||||
Cryptpad.onFriendComplete = function (data) {
|
||||
sframeChan.event('EV_FRIEND_REQUEST', data);
|
||||
};
|
||||
|
||||
sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) {
|
||||
var network = Cryptpad.getNetwork();
|
||||
var hkn = network.historyKeeper;
|
||||
var crypto = Crypto.createEncryptor(secret.keys);
|
||||
// Get the history messages and send them to the iframe
|
||||
var parse = function (msg) {
|
||||
try {
|
||||
return JSON.parse(msg);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
var onMsg = function (msg) {
|
||||
var parsed = parse(msg);
|
||||
if (parsed[0] === 'FULL_HISTORY_END') {
|
||||
console.log('END');
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
if (parsed[0] !== 'FULL_HISTORY') { return; }
|
||||
if (parsed[1] && parsed[1].validateKey) { // First message
|
||||
secret.keys.validateKey = parsed[1].validateKey;
|
||||
return;
|
||||
}
|
||||
msg = parsed[1][4];
|
||||
if (msg) {
|
||||
msg = msg.replace(/^cp\|/, '');
|
||||
var decryptedMsg = crypto.decrypt(msg, secret.keys.validateKey);
|
||||
sframeChan.event('EV_RT_HIST_MESSAGE', decryptedMsg);
|
||||
}
|
||||
};
|
||||
network.on('message', onMsg);
|
||||
network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', secret.channel, secret.keys.validateKey]));
|
||||
});
|
||||
|
||||
CpNfOuter.start({
|
||||
sframeChan: sframeChan,
|
||||
channel: secret.channel,
|
||||
network: Cryptpad.getNetwork(),
|
||||
validateKey: secret.keys.validateKey || undefined,
|
||||
readOnly: readOnly,
|
||||
crypto: Crypto.createEncryptor(secret.keys),
|
||||
onConnect: function (wc) {
|
||||
if (readOnly) { return; }
|
||||
Cryptpad.replaceHash(Cryptpad.getEditHashFromKeys(wc.id, secret.keys));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var first = function () {
|
||||
Ckeditor = ifrw.CKEDITOR;
|
||||
if (Ckeditor) {
|
||||
// mobile configuration
|
||||
Ckeditor.config.toolbarCanCollapse = true;
|
||||
if (screen.height < 800) {
|
||||
Ckeditor.config.toolbarStartupExpanded = false;
|
||||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no');
|
||||
} else {
|
||||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes');
|
||||
}
|
||||
second(Ckeditor);
|
||||
} else {
|
||||
console.log("Ckeditor was not defined. Trying again in %sms",interval);
|
||||
setTimeout(first, interval);
|
||||
}
|
||||
};
|
||||
|
||||
$(first);
|
||||
|
||||
Cryptpad.reportAppUsage();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
* mode, which handles the main editing area space.
|
||||
*/
|
||||
|
||||
( function() {
|
||||
define(['/api/config'], function (ApiConfig) {
|
||||
var framedWysiwyg;
|
||||
var iframe;
|
||||
var iframe;
|
||||
|
||||
CKEDITOR.plugins.registered.wysiwygarea.init = function( editor ) {
|
||||
if ( editor.config.fullPage ) {
|
||||
@@ -39,7 +39,7 @@
|
||||
}
|
||||
|
||||
// CryptPad
|
||||
src = '/pad/ckeditor-inner.html';
|
||||
src = '/pad/ckeditor-inner.html?' + ApiConfig.requireConf.urlArgs;
|
||||
|
||||
iframe = CKEDITOR.dom.element.createFromHtml( '<iframe src="' + src + '" frameBorder="0"></iframe>' );
|
||||
iframe.setStyles( { width: '100%', height: '100%' } );
|
||||
@@ -55,7 +55,12 @@
|
||||
|
||||
// Asynchronous iframe loading is only required in IE>8 and Gecko (other reasons probably).
|
||||
// Do not use it on WebKit as it'll break the browser-back navigation.
|
||||
var useOnloadEvent = ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) || CKEDITOR.env.gecko;
|
||||
var useOnloadEvent = ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) || CKEDITOR.env.gecko;
|
||||
|
||||
// CryptPad
|
||||
// This breaks Edge so lets use async all of the time
|
||||
useOnloadEvent = true;
|
||||
|
||||
if ( useOnloadEvent )
|
||||
iframe.on( 'load', onLoad );
|
||||
|
||||
@@ -641,7 +646,7 @@
|
||||
|
||||
return css.join( '\n' );
|
||||
}
|
||||
} )();
|
||||
});
|
||||
|
||||
/**
|
||||
* Disables the ability to resize objects (images and tables) in the editing area.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<title data-localization="poll_title">Zero Knowledge Date Picker</title>
|
||||
<script async data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"
|
||||
<script async data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"
|
||||
data-bootload="/customize/template.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
<html class="cp">
|
||||
<!-- If this file is not called customize.dist/src/template.html, it is generated -->
|
||||
<head>
|
||||
<title data-localization="main_title">Cryptpad: Zero Knowledge, Collaborative Real Time Editing</title>
|
||||
<title data-localization="main_title">CryptPad: Zero Knowledge, Collaborative Real Time Editing</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
|
||||
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
|
||||
<link rel="stylesheet" href="/bower_components/codemirror/addon/dialog/dialog.css">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
html, body {
|
||||
overflow-y: hidden;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<link rel="stylesheet" href="/bower_components/components-font-awesome/css/font-awesome.min.css">
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<script async data-bootload="inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>.loading-hidden { display: none; } </style>
|
||||
</head>
|
||||
<body class="loading-hidden">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<title>CryptPad</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<script src="/bower_components/jquery/dist/jquery.min.js"></script>
|
||||
<script async data-bootload="/todo/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/todo/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>.loading-hidden, .loading-hidden * {display: none !important;}</style>
|
||||
</head>
|
||||
<body class="loading-hidden">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body class="html">
|
||||
<noscript>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ define([
|
||||
'/common/cryptget.js',
|
||||
'/whiteboard/colors.js',
|
||||
'/customize/application_config.js',
|
||||
'/common/common-thumbnail.js',
|
||||
'/bower_components/secure-fabric.js/dist/fabric.min.js',
|
||||
'/bower_components/file-saver/FileSaver.min.js',
|
||||
|
||||
@@ -19,7 +20,7 @@ define([
|
||||
'less!/customize/src/less/cryptpad.less',
|
||||
'less!/whiteboard/whiteboard.less',
|
||||
'less!/customize/src/less/toolbar.less',
|
||||
], function ($, Config, Realtime, Crypto, Toolbar, TextPatcher, JSONSortify, JsonOT, Cryptpad, Cryptget, Colors, AppConfig) {
|
||||
], function ($, Config, Realtime, Crypto, Toolbar, TextPatcher, JSONSortify, JsonOT, Cryptpad, Cryptget, Colors, AppConfig, Thumb) {
|
||||
var saveAs = window.saveAs;
|
||||
var Messages = Cryptpad.Messages;
|
||||
|
||||
@@ -212,13 +213,18 @@ window.canvas = canvas;
|
||||
|
||||
module.FM = Cryptpad.createFileManager({});
|
||||
module.upload = function (title) {
|
||||
$canvas[0].toBlob(function (blob) {
|
||||
blob.name = title;
|
||||
var reader = new FileReader();
|
||||
reader.onloadend = function () {
|
||||
module.FM.handleFile(blob);
|
||||
};
|
||||
reader.readAsArrayBuffer(blob);
|
||||
var canvas = $canvas[0];
|
||||
var finish = function (thumb) {
|
||||
canvas.toBlob(function (blob) {
|
||||
blob.name = title;
|
||||
module.FM.handleFile(blob, void 0, thumb);
|
||||
});
|
||||
};
|
||||
|
||||
Thumb.fromCanvas(canvas, function (e, blob) {
|
||||
// carry on even if you can't get a thumbnail
|
||||
if (e) { console.error(e); }
|
||||
finish(blob);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user