Merge branch 'soon'
commit
595fa4c2e4
Binary file not shown.
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 66 KiB |
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
@ -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>
|
@ -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;
|
||||||
|
});
|
@ -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.
|
@ -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;
|
||||||
|
});
|
@ -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;
|
||||||
|
});
|
@ -0,0 +1,31 @@
|
|||||||
|
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);
|
||||||
|
require([
|
||||||
|
'/customize/messages.js',
|
||||||
|
|
||||||
|
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||||
|
], function (Messages) {
|
||||||
|
document.getElementById('cp-loading-message').innerText = Messages.loading;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
intr = setInterval(append, 100);
|
||||||
|
append();
|
||||||
|
});
|
File diff suppressed because one or more lines are too long
@ -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));
|
||||||
|
};
|
||||||
|
});
|
@ -0,0 +1,30 @@
|
|||||||
|
// 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 req = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
|
||||||
|
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);
|
||||||
|
}());
|
@ -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')]);
|
||||||
|
});
|
@ -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);
|
||||||
|
});
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
@ -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) {
|
||||||
|
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 (window !== window.top) {
|
||||||
|
// we're in the sandbox
|
||||||
|
otherWindow = ow;
|
||||||
|
cb(chan);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { create: create };
|
||||||
|
});
|
@ -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;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,263 @@
|
|||||||
|
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 updateButton = function () {
|
||||||
|
var myData = metadataMgr.getUserData();
|
||||||
|
if (!myData) { return; }
|
||||||
|
var newName = myData.name;
|
||||||
|
var url = myData.avatar;
|
||||||
|
$displayName.text(newName || Messages.anonymous);
|
||||||
|
if (accountName) {
|
||||||
|
$avatar.html('');
|
||||||
|
UI.displayAvatar(Common, $avatar, url, newName, function ($img) {
|
||||||
|
if ($img) {
|
||||||
|
$userAdmin.find('button').addClass('avatar');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
metadataMgr.onChange(updateButton);
|
||||||
|
updateButton();
|
||||||
|
|
||||||
|
$userAdmin.find('a.logout').click(function () {
|
||||||
|
Common.logout(function () {
|
||||||
|
window.top.location = origin+'/';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$userAdmin.find('a.settings').click(function () {
|
||||||
|
if (padType) {
|
||||||
|
window.open(origin+'/settings/');
|
||||||
|
} else {
|
||||||
|
window.top.location = origin+'/settings/';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$userAdmin.find('a.profile').click(function () {
|
||||||
|
if (padType) {
|
||||||
|
window.open(origin+'/profile/');
|
||||||
|
} else {
|
||||||
|
window.top.location = origin+'/profile/';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$userAdmin.find('a.login').click(function () {
|
||||||
|
Common.setLoginRedirect(function () {
|
||||||
|
window.top.location = origin+'/login/';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$userAdmin.find('a.register').click(function () {
|
||||||
|
Common.setLoginRedirect(function () {
|
||||||
|
window.top.location = origin+'/register/';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return $userAdmin;
|
||||||
|
};
|
||||||
|
|
||||||
|
return UI;
|
||||||
|
});
|
@ -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;
|
||||||
|
});
|
||||||
|
|
@ -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,
|
||||||
|
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
This is to test if we have a flakey test.
|
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||||
|
});
|
@ -1,3 +1,791 @@
|
|||||||
|
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([
|
define([
|
||||||
'less!/customize/src/less/toolbar.less',
|
'jquery',
|
||||||
], function () {});
|
'/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;
|
||||||
|
editor = Ckeditor.replace('editor1', {
|
||||||
|
customConfig: '/customize/ckeditor-config.js',
|
||||||
|
});
|
||||||
|
editor.on('instanceReady', waitFor());
|
||||||
|
}).nThen(function (/*waitFor*/) {
|
||||||
|
Links.addSupportForOpeningLinksInNewTab(Ckeditor)({editor: editor});
|
||||||
|
Cryptpad.onError(function (info) {
|
||||||
|
if (info && info.type === "store") {
|
||||||
|
onConnectError();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
andThen(editor, Ckeditor, common);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
main();
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue