Merge branch 'master' of https://github.com/xwiki-labs/cryptpad
commit
1ae7076790
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,24 @@
|
||||
define([
|
||||
'less!/customize/src/less/loading.less'
|
||||
], function () {
|
||||
var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; });
|
||||
var elem = document.createElement('div');
|
||||
elem.setAttribute('id', 'loading');
|
||||
elem.innerHTML = [
|
||||
'<div class="loadingContainer">',
|
||||
'<img class="cryptofist" src="/customize/cryptpad-new-logo-colors-logoonly.png?' + urlArgs + '">',
|
||||
'<div class="spinnerContainer">',
|
||||
'<span class="fa fa-circle-o-notch fa-spin fa-4x fa-fw"></span>',
|
||||
'</div>',
|
||||
'<p id="cp-loading-message"></p>',
|
||||
'</div>'
|
||||
].join('');
|
||||
var intr;
|
||||
var append = function () {
|
||||
if (!document.body) { return; }
|
||||
clearInterval(intr);
|
||||
document.body.appendChild(elem);
|
||||
};
|
||||
intr = setInterval(append, 100);
|
||||
append();
|
||||
});
|
File diff suppressed because one or more lines are too long
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,40 @@
|
||||
// Stage 0, this gets cached which means we can't change it. boot2-sframe.js is changable.
|
||||
// Note that this file is meant to be executed only inside of a sandbox iframe.
|
||||
;(function () {
|
||||
var afterLoaded = function (req) {
|
||||
req.cfg = req.cfg || {};
|
||||
if (req.pfx) {
|
||||
req.cfg.onNodeCreated = function (node /*, config, module, path*/) {
|
||||
node.setAttribute('src', req.pfx + node.getAttribute('src'));
|
||||
};
|
||||
}
|
||||
require.config(req.cfg);
|
||||
var txid = Math.random().toString(16).replace('0.', '');
|
||||
var intr;
|
||||
var ready = function () {
|
||||
intr = setInterval(function () {
|
||||
if (typeof(txid) !== 'string') { return; }
|
||||
window.parent.postMessage(JSON.stringify({ q: 'READY', txid: txid }), '*');
|
||||
}, 1);
|
||||
};
|
||||
if (req.req) { require(req.req, ready); } else { ready(); }
|
||||
var onReply = function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (data.txid !== txid) { return; }
|
||||
clearInterval(intr);
|
||||
txid = {};
|
||||
window.removeEventListener('message', onReply);
|
||||
require(['/common/sframe-boot2.js'], function () { });
|
||||
};
|
||||
window.addEventListener('message', onReply);
|
||||
};
|
||||
|
||||
var intr = setInterval(function () {
|
||||
try {
|
||||
var req = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
|
||||
clearInterval(intr);
|
||||
afterLoaded(req);
|
||||
} catch (e) { console.error(e); }
|
||||
}, 100);
|
||||
|
||||
}());
|
@ -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, isSandbox) {
|
||||
var otherWindow;
|
||||
var handlers = {};
|
||||
var queries = {};
|
||||
|
||||
// list of handlers which are registered from the other side...
|
||||
var insideHandlers = [];
|
||||
var callWhenRegistered = {};
|
||||
|
||||
var chan = {};
|
||||
|
||||
// Send a query. channel.query('Q_SOMETHING', { args: "whatever" }, function (reply) { ... });
|
||||
chan.query = function (q, content, cb) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[q]) {
|
||||
throw new Error('please only make queries are defined in sframe-protocol.js');
|
||||
}
|
||||
var txid = mkTxid();
|
||||
var timeout = setTimeout(function () {
|
||||
delete queries[txid];
|
||||
console.log("Timeout making query " + q);
|
||||
}, 30000);
|
||||
queries[txid] = function (data, msg) {
|
||||
clearTimeout(timeout);
|
||||
delete queries[txid];
|
||||
cb(undefined, data.content, msg);
|
||||
};
|
||||
otherWindow.postMessage(JSON.stringify({
|
||||
txid: txid,
|
||||
content: content,
|
||||
q: q
|
||||
}), '*');
|
||||
};
|
||||
|
||||
// Fire an event. channel.event('EV_SOMETHING', { args: "whatever" });
|
||||
var event = chan.event = function (e, content) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[e]) {
|
||||
throw new Error('please only fire events that are defined in sframe-protocol.js');
|
||||
}
|
||||
if (e.indexOf('EV_') !== 0) {
|
||||
throw new Error('please only use events (starting with EV_) for event messages');
|
||||
}
|
||||
otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*');
|
||||
};
|
||||
|
||||
// Be notified on query or event. channel.on('EV_SOMETHING', function (args, reply) { ... });
|
||||
// If the type is a query, your handler will be invoked with a reply function that takes
|
||||
// one argument (the content to reply with).
|
||||
chan.on = function (queryType, handler, quiet) {
|
||||
if (!otherWindow && !quiet) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[queryType]) {
|
||||
throw new Error('please only register handlers which are defined in sframe-protocol.js');
|
||||
}
|
||||
(handlers[queryType] = handlers[queryType] || []).push(function (data, msg) {
|
||||
handler(data.content, function (replyContent) {
|
||||
if (queryType.indexOf('Q_') !== 0) { throw new Error("replies to events are invalid"); }
|
||||
msg.source.postMessage(JSON.stringify({
|
||||
txid: data.txid,
|
||||
content: replyContent
|
||||
}), '*');
|
||||
}, msg);
|
||||
});
|
||||
if (!quiet) {
|
||||
event('EV_REGISTER_HANDLER', queryType);
|
||||
}
|
||||
};
|
||||
|
||||
// If a particular handler is registered, call the callback immediately, otherwise it will be called
|
||||
// when that handler is first registered.
|
||||
// channel.whenReg('Q_SOMETHING', function () { ...query Q_SOMETHING?... });
|
||||
chan.whenReg = function (queryType, cb, always) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[queryType]) {
|
||||
throw new Error('please only register handlers which are defined in sframe-protocol.js');
|
||||
}
|
||||
var reg = always;
|
||||
if (insideHandlers.indexOf(queryType) > -1) {
|
||||
cb();
|
||||
} else {
|
||||
reg = true;
|
||||
}
|
||||
if (reg) {
|
||||
(callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(cb);
|
||||
}
|
||||
};
|
||||
|
||||
// Same as whenReg except it will invoke every time there is another registration, not just once.
|
||||
chan.onReg = function (queryType, cb) { chan.whenReg(queryType, cb, true); };
|
||||
|
||||
chan.on('EV_REGISTER_HANDLER', function (content) {
|
||||
if (callWhenRegistered[content]) {
|
||||
callWhenRegistered[content].forEach(function (f) { f(); });
|
||||
delete callWhenRegistered[content];
|
||||
}
|
||||
insideHandlers.push(content);
|
||||
}, true);
|
||||
|
||||
var txid;
|
||||
window.addEventListener('message', function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (ow !== msg.source) {
|
||||
console.log("DROP Message from unexpected source");
|
||||
console.log(msg);
|
||||
} else if (!otherWindow) {
|
||||
otherWindow = ow;
|
||||
ow.postMessage(JSON.stringify({ txid: data.txid }), '*');
|
||||
cb(chan);
|
||||
} else if (typeof(data.q) === 'string' && handlers[data.q]) {
|
||||
handlers[data.q].forEach(function (f) {
|
||||
f(data || JSON.parse(msg.data), msg);
|
||||
data = undefined;
|
||||
});
|
||||
} else if (typeof(data.q) === 'undefined' && queries[data.txid]) {
|
||||
queries[data.txid](data, msg);
|
||||
} else if (data.txid === txid) {
|
||||
// stray message from init
|
||||
return;
|
||||
} else {
|
||||
console.log("DROP Unhandled message");
|
||||
console.log(msg);
|
||||
}
|
||||
});
|
||||
if (isSandbox) {
|
||||
// we're in the sandbox
|
||||
otherWindow = ow;
|
||||
cb(chan);
|
||||
}
|
||||
};
|
||||
|
||||
return { create: create };
|
||||
});
|
@ -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,265 @@
|
||||
define([
|
||||
'jquery',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/media-tag.js',
|
||||
], function ($, Cryptpad, MediaTag) {
|
||||
var UI = {};
|
||||
var Messages = Cryptpad.Messages;
|
||||
|
||||
/**
|
||||
* Requirements from cryptpad-common.js
|
||||
* getFileSize
|
||||
* - hrefToHexChannelId
|
||||
* displayAvatar
|
||||
* - getFirstEmojiOrCharacter
|
||||
* - parsePadUrl
|
||||
* - getSecrets
|
||||
* - base64ToHex
|
||||
* - getBlobPathFromHex
|
||||
* - bytesToMegabytes
|
||||
* createUserAdminMenu
|
||||
* - fixHTML
|
||||
* - createDropdown
|
||||
*/
|
||||
|
||||
UI.getFileSize = function (Common, href, cb) {
|
||||
var channelId = Cryptpad.hrefToHexChannelId(href);
|
||||
Common.sendAnonRpcMsg("GET_FILE_SIZE", channelId, function (data) {
|
||||
if (!data) { return void cb("No response"); }
|
||||
if (data.error) { return void cb(data.error); }
|
||||
if (data.response && data.response.length && typeof(data.response[0]) === 'number') {
|
||||
return void cb(void 0, data.response[0]);
|
||||
} else {
|
||||
cb('INVALID_RESPONSE');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
UI.displayAvatar = function (Common, $container, href, name, cb) {
|
||||
var MutationObserver = window.MutationObserver;
|
||||
var displayDefault = function () {
|
||||
var text = Cryptpad.getFirstEmojiOrCharacter(name);
|
||||
var $avatar = $('<span>', {'class': 'default'}).text(text);
|
||||
$container.append($avatar);
|
||||
if (cb) { cb(); }
|
||||
};
|
||||
if (!href) { return void displayDefault(); }
|
||||
var parsed = Cryptpad.parsePadUrl(href);
|
||||
var secret = Cryptpad.getSecrets('file', parsed.hash);
|
||||
if (secret.keys && secret.channel) {
|
||||
var cryptKey = secret.keys && secret.keys.fileKeyStr;
|
||||
var hexFileName = Cryptpad.base64ToHex(secret.channel);
|
||||
var src = Cryptpad.getBlobPathFromHex(hexFileName);
|
||||
UI.getFileSize(Common, href, function (e, data) {
|
||||
if (e) {
|
||||
displayDefault();
|
||||
return void console.error(e);
|
||||
}
|
||||
if (typeof data !== "number") { return void displayDefault(); }
|
||||
if (Cryptpad.bytesToMegabytes(data) > 0.5) { return void displayDefault(); }
|
||||
var $img = $('<media-tag>').appendTo($container);
|
||||
$img.attr('src', src);
|
||||
$img.attr('data-crypto-key', 'cryptpad:' + cryptKey);
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
||||
if (mutation.addedNodes.length > 1 ||
|
||||
mutation.addedNodes[0].nodeName !== 'IMG') {
|
||||
$img.remove();
|
||||
return void displayDefault();
|
||||
}
|
||||
var $image = $img.find('img');
|
||||
var onLoad = function () {
|
||||
var img = new Image();
|
||||
img.onload = function () {
|
||||
var w = img.width;
|
||||
var h = img.height;
|
||||
if (w>h) {
|
||||
$image.css('max-height', '100%');
|
||||
$img.css('flex-direction', 'column');
|
||||
if (cb) { cb($img); }
|
||||
return;
|
||||
}
|
||||
$image.css('max-width', '100%');
|
||||
$img.css('flex-direction', 'row');
|
||||
if (cb) { cb($img); }
|
||||
};
|
||||
img.src = $image.attr('src');
|
||||
};
|
||||
if ($image[0].complete) { onLoad(); }
|
||||
$image.on('load', onLoad);
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe($img[0], {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
characterData: false
|
||||
});
|
||||
MediaTag($img[0]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
UI.createUserAdminMenu = function (config) {
|
||||
var Common = config.Common;
|
||||
var metadataMgr = config.metadataMgr;
|
||||
|
||||
var displayNameCls = config.displayNameCls || 'displayName';
|
||||
var $displayedName = $('<span>', {'class': displayNameCls});
|
||||
|
||||
var accountName = metadataMgr.getPrivateData().accountName;
|
||||
var origin = metadataMgr.getPrivateData().origin;
|
||||
var padType = metadataMgr.getMetadata().type;
|
||||
|
||||
var $userName = $('<span>', {'class': 'userDisplayName'});
|
||||
var options = [];
|
||||
if (config.displayNameCls) {
|
||||
var $userAdminContent = $('<p>');
|
||||
if (accountName) {
|
||||
var $userAccount = $('<span>', {'class': 'userAccount'}).append(Messages.user_accountName + ': ' + Cryptpad.fixHTML(accountName));
|
||||
$userAdminContent.append($userAccount);
|
||||
$userAdminContent.append($('<br>'));
|
||||
}
|
||||
if (config.displayName) {
|
||||
// Hide "Display name:" in read only mode
|
||||
$userName.append(Messages.user_displayName + ': ');
|
||||
$userName.append($displayedName);
|
||||
}
|
||||
$userAdminContent.append($userName);
|
||||
options.push({
|
||||
tag: 'p',
|
||||
attributes: {'class': 'accountData'},
|
||||
content: $userAdminContent.html()
|
||||
});
|
||||
}
|
||||
if (padType !== 'drive') {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {
|
||||
'target': '_blank',
|
||||
'href': origin+'/drive/'
|
||||
},
|
||||
content: Messages.login_accessDrive
|
||||
});
|
||||
}
|
||||
// Add the change display name button if not in read only mode
|
||||
if (config.changeNameButtonCls && config.displayChangeName) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': config.changeNameButtonCls},
|
||||
content: Messages.user_rename
|
||||
});
|
||||
}
|
||||
if (accountName) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'profile'},
|
||||
content: Messages.profileButton
|
||||
});
|
||||
}
|
||||
if (padType !== 'settings') {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'settings'},
|
||||
content: Messages.settingsButton
|
||||
});
|
||||
}
|
||||
// Add login or logout button depending on the current status
|
||||
if (accountName) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'logout'},
|
||||
content: Messages.logoutButton
|
||||
});
|
||||
} else {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'login'},
|
||||
content: Messages.login_login
|
||||
});
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'register'},
|
||||
content: Messages.login_register
|
||||
});
|
||||
}
|
||||
var $icon = $('<span>', {'class': 'fa fa-user-secret'});
|
||||
//var $userbig = $('<span>', {'class': 'big'}).append($displayedName.clone());
|
||||
var $userButton = $('<div>').append($icon);//.append($userbig);
|
||||
if (accountName) {
|
||||
$userButton = $('<div>').append(accountName);
|
||||
}
|
||||
/*if (account && config.displayNameCls) {
|
||||
$userbig.append($('<span>', {'class': 'account-name'}).text('(' + accountName + ')'));
|
||||
} else if (account) {
|
||||
// If no display name, do not display the parentheses
|
||||
$userbig.append($('<span>', {'class': 'account-name'}).text(accountName));
|
||||
}*/
|
||||
var dropdownConfigUser = {
|
||||
text: $userButton.html(), // Button initial text
|
||||
options: options, // Entries displayed in the menu
|
||||
left: true, // Open to the left of the button
|
||||
container: config.$initBlock, // optional
|
||||
feedback: "USER_ADMIN",
|
||||
};
|
||||
var $userAdmin = Cryptpad.createDropdown(dropdownConfigUser);
|
||||
|
||||
var $displayName = $userAdmin.find('.'+displayNameCls);
|
||||
|
||||
var $avatar = $userAdmin.find('.buttonTitle');
|
||||
var oldUrl;
|
||||
var updateButton = function () {
|
||||
var myData = metadataMgr.getUserData();
|
||||
if (!myData) { return; }
|
||||
var newName = myData.name;
|
||||
var url = myData.avatar;
|
||||
$displayName.text(newName || Messages.anonymous);
|
||||
if (accountName && oldUrl !== url) {
|
||||
$avatar.html('');
|
||||
UI.displayAvatar(Common, $avatar, url, newName, function ($img) {
|
||||
oldUrl = url;
|
||||
if ($img) {
|
||||
$userAdmin.find('button').addClass('avatar');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
metadataMgr.onChange(updateButton);
|
||||
updateButton();
|
||||
|
||||
$userAdmin.find('a.logout').click(function () {
|
||||
Common.logout(function () {
|
||||
window.parent.location = origin+'/';
|
||||
});
|
||||
});
|
||||
$userAdmin.find('a.settings').click(function () {
|
||||
if (padType) {
|
||||
window.open(origin+'/settings/');
|
||||
} else {
|
||||
window.parent.location = origin+'/settings/';
|
||||
}
|
||||
});
|
||||
$userAdmin.find('a.profile').click(function () {
|
||||
if (padType) {
|
||||
window.open(origin+'/profile/');
|
||||
} else {
|
||||
window.parent.location = origin+'/profile/';
|
||||
}
|
||||
});
|
||||
$userAdmin.find('a.login').click(function () {
|
||||
Common.setLoginRedirect(function () {
|
||||
window.parent.location = origin+'/login/';
|
||||
});
|
||||
});
|
||||
$userAdmin.find('a.register').click(function () {
|
||||
Common.setLoginRedirect(function () {
|
||||
window.parent.location = origin+'/register/';
|
||||
});
|
||||
});
|
||||
|
||||
return $userAdmin;
|
||||
};
|
||||
|
||||
return UI;
|
||||
});
|
@ -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,798 @@
|
||||
require(['/api/config'], function (ApiConfig) {
|
||||
// see ckeditor_base.js getUrl()
|
||||
window.CKEDITOR_GETURL = function (resource) {
|
||||
if (resource.indexOf( '/' ) === 0) {
|
||||
resource = window.CKEDITOR.basePath.replace(/\/bower_components\/.*/, '') + resource;
|
||||
} else if (resource.indexOf(':/') === -1) {
|
||||
resource = window.CKEDITOR.basePath + resource;
|
||||
}
|
||||
if (resource[resource.length - 1] !== '/' && resource.indexOf('ver=') === -1) {
|
||||
var args = ApiConfig.requireConf.urlArgs;
|
||||
if (resource.indexOf('/bower_components/') !== -1) {
|
||||
args = 'ver=' + window.CKEDITOR.timestamp;
|
||||
}
|
||||
resource += (resource.indexOf('?') >= 0 ? '&' : '?') + args;
|
||||
}
|
||||
return resource;
|
||||
};
|
||||
require(['/bower_components/ckeditor/ckeditor.js']);
|
||||
});
|
||||
define([
|
||||
'less!/customize/src/less/toolbar.less',
|
||||
], function () {});
|
||||
'jquery',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/bower_components/hyperjson/hyperjson.js',
|
||||
'/common/toolbar3.js',
|
||||
'/common/cursor.js',
|
||||
'/bower_components/chainpad-json-validator/json-ot.js',
|
||||
'/common/TypingTests.js',
|
||||
'json.sortify',
|
||||
'/bower_components/textpatcher/TextPatcher.js',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/cryptget.js',
|
||||
'/pad/links.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
'/common/sframe-common.js',
|
||||
'/api/config',
|
||||
'/common/common-realtime.js',
|
||||
|
||||
'/bower_components/file-saver/FileSaver.min.js',
|
||||
'/bower_components/diff-dom/diffDOM.js',
|
||||
|
||||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
|
||||
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||
'less!/customize/src/less/cryptpad.less',
|
||||
'less!/customize/src/less/toolbar.less'
|
||||
], function (
|
||||
$,
|
||||
Crypto,
|
||||
Hyperjson,
|
||||
Toolbar,
|
||||
Cursor,
|
||||
JsonOT,
|
||||
TypingTest,
|
||||
JSONSortify,
|
||||
TextPatcher,
|
||||
Cryptpad,
|
||||
Cryptget,
|
||||
Links,
|
||||
nThen,
|
||||
SFCommon,
|
||||
ApiConfig,
|
||||
CommonRealtime)
|
||||
{
|
||||
var saveAs = window.saveAs;
|
||||
var Messages = Cryptpad.Messages;
|
||||
var DiffDom = window.diffDOM;
|
||||
|
||||
var stringify = function (obj) { return JSONSortify(obj); };
|
||||
|
||||
window.Toolbar = Toolbar;
|
||||
window.Hyperjson = Hyperjson;
|
||||
|
||||
var slice = function (coll) {
|
||||
return Array.prototype.slice.call(coll);
|
||||
};
|
||||
|
||||
var removeListeners = function (root) {
|
||||
slice(root.attributes).map(function (attr) {
|
||||
if (/^on/.test(attr.name)) {
|
||||
root.attributes.removeNamedItem(attr.name);
|
||||
}
|
||||
});
|
||||
slice(root.children).forEach(removeListeners);
|
||||
};
|
||||
|
||||
var hjsonToDom = function (H) {
|
||||
var dom = Hyperjson.toDOM(H);
|
||||
removeListeners(dom);
|
||||
return dom;
|
||||
};
|
||||
|
||||
var module = window.REALTIME_MODULE = window.APP = {
|
||||
Hyperjson: Hyperjson,
|
||||
TextPatcher: TextPatcher,
|
||||
logFights: true,
|
||||
fights: [],
|
||||
Cryptpad: Cryptpad,
|
||||
Cursor: Cursor,
|
||||
};
|
||||
|
||||
var emitResize = module.emitResize = function () {
|
||||
var evt = window.document.createEvent('UIEvents');
|
||||
evt.initUIEvent('resize', true, false, window, 0);
|
||||
window.dispatchEvent(evt);
|
||||
};
|
||||
|
||||
var toolbar;
|
||||
|
||||
var isNotMagicLine = function (el) {
|
||||
return !(el && typeof(el.getAttribute) === 'function' &&
|
||||
el.getAttribute('class') &&
|
||||
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1);
|
||||
};
|
||||
|
||||
/* catch `type="_moz"` before it goes over the wire */
|
||||
var brFilter = function (hj) {
|
||||
if (hj[1].type === '_moz') { hj[1].type = undefined; }
|
||||
return hj;
|
||||
};
|
||||
|
||||
var onConnectError = function () {
|
||||
Cryptpad.errorLoadingScreen(Messages.websocketError);
|
||||
};
|
||||
|
||||
var domFromHTML = function (html) {
|
||||
return new DOMParser().parseFromString(html, 'text/html');
|
||||
};
|
||||
|
||||
var forbiddenTags = [
|
||||
'SCRIPT',
|
||||
'IFRAME',
|
||||
'OBJECT',
|
||||
'APPLET',
|
||||
'VIDEO',
|
||||
'AUDIO'
|
||||
];
|
||||
|
||||
var getHTML = function (inner) {
|
||||
return ('<!DOCTYPE html>\n' + '<html>\n' + inner.innerHTML);
|
||||
};
|
||||
|
||||
var CKEDITOR_CHECK_INTERVAL = 100;
|
||||
var ckEditorAvailable = function (cb) {
|
||||
var intr;
|
||||
var check = function () {
|
||||
if (window.CKEDITOR) {
|
||||
clearTimeout(intr);
|
||||
cb(window.CKEDITOR);
|
||||
}
|
||||
};
|
||||
intr = setInterval(function () {
|
||||
console.log("Ckeditor was not defined. Trying again in %sms", CKEDITOR_CHECK_INTERVAL);
|
||||
check();
|
||||
}, CKEDITOR_CHECK_INTERVAL);
|
||||
check();
|
||||
};
|
||||
|
||||
var mkDiffOptions = function (cursor, readOnly) {
|
||||
return {
|
||||
preDiffApply: function (info) {
|
||||
/*
|
||||
Don't accept attributes that begin with 'on'
|
||||
these are probably listeners, and we don't want to
|
||||
send scripts over the wire.
|
||||
*/
|
||||
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) {
|
||||
if (info.diff.name === 'href') {
|
||||
// console.log(info.diff);
|
||||
//var href = info.diff.newValue;
|
||||
|
||||
// TODO normalize HTML entities
|
||||
if (/javascript *: */.test(info.diff.newValue)) {
|
||||
// TODO remove javascript: links
|
||||
}
|
||||
}
|
||||
|
||||
if (/^on/.test(info.diff.name)) {
|
||||
console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/*
|
||||
Also reject any elements which would insert any one of
|
||||
our forbidden tag types: script, iframe, object,
|
||||
applet, video, or audio
|
||||
*/
|
||||
if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) {
|
||||
if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) {
|
||||
console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName);
|
||||
return true;
|
||||
} else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) {
|
||||
console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.node && info.node.tagName === 'BODY') {
|
||||
if (info.diff.action === 'removeAttribute' &&
|
||||
['class', 'spellcheck'].indexOf(info.diff.name) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* DiffDOM will filter out magicline plugin elements
|
||||
in practice this will make it impossible to use it
|
||||
while someone else is typing, which could be annoying.
|
||||
|
||||
we should check when such an element is going to be
|
||||
removed, and prevent that from happening. */
|
||||
if (info.node && info.node.tagName === 'SPAN' &&
|
||||
info.node.getAttribute('contentEditable') === "false") {
|
||||
// it seems to be a magicline plugin element...
|
||||
if (info.diff.action === 'removeElement') {
|
||||
// and you're about to remove it...
|
||||
// this probably isn't what you want
|
||||
|
||||
/*
|
||||
I have never seen this in the console, but the
|
||||
magic line is still getting removed on remote
|
||||
edits. This suggests that it's getting removed
|
||||
by something other than diffDom.
|
||||
*/
|
||||
console.log("preventing removal of the magic line!");
|
||||
|
||||
// return true to prevent diff application
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not change the contenteditable value in view mode
|
||||
if (readOnly && info.node && info.node.tagName === 'BODY' &&
|
||||
info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// no use trying to recover the cursor if it doesn't exist
|
||||
if (!cursor.exists()) { return; }
|
||||
|
||||
/* frame is either 0, 1, 2, or 3, depending on which
|
||||
cursor frames were affected: none, first, last, or both
|
||||
*/
|
||||
var frame = info.frame = cursor.inNode(info.node);
|
||||
|
||||
if (!frame) { return; }
|
||||
|
||||
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') {
|
||||
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue);
|
||||
|
||||
if (frame & 1) {
|
||||
// push cursor start if necessary
|
||||
if (pushes.commonStart < cursor.Range.start.offset) {
|
||||
cursor.Range.start.offset += pushes.delta;
|
||||
}
|
||||
}
|
||||
if (frame & 2) {
|
||||
// push cursor end if necessary
|
||||
if (pushes.commonStart < cursor.Range.end.offset) {
|
||||
cursor.Range.end.offset += pushes.delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
postDiffApply: function (info) {
|
||||
if (info.frame) {
|
||||
if (info.node) {
|
||||
if (info.frame & 1) { cursor.fixStart(info.node); }
|
||||
if (info.frame & 2) { cursor.fixEnd(info.node); }
|
||||
} else { console.error("info.node did not exist"); }
|
||||
|
||||
var sel = cursor.makeSelection();
|
||||
var range = cursor.makeRange();
|
||||
|
||||
cursor.fixSelection(sel, range);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
var andThen = function (editor, Ckeditor, common) {
|
||||
//var $iframe = $('#pad-iframe').contents();
|
||||
//var secret = Cryptpad.getSecrets();
|
||||
//var readOnly = secret.keys && !secret.keys.editKeyStr;
|
||||
//if (!secret.keys) {
|
||||
// secret.keys = secret.key;
|
||||
//}
|
||||
var readOnly = false; // TODO
|
||||
var cpNfInner;
|
||||
var metadataMgr;
|
||||
var onLocal;
|
||||
|
||||
var $bar = $('#cke_1_toolbox');
|
||||
|
||||
var $html = $bar.closest('html');
|
||||
var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]');
|
||||
if ($faLink.length) {
|
||||
$html.find('iframe').contents().find('head').append($faLink.clone());
|
||||
}
|
||||
var isHistoryMode = false;
|
||||
|
||||
if (readOnly) {
|
||||
$('#cke_1_toolbox > .cke_toolbox_main').hide();
|
||||
}
|
||||
|
||||
/* add a class to the magicline plugin so we can pick it out more easily */
|
||||
|
||||
var ml = Ckeditor.instances.editor1.plugins.magicline.backdoor.that.line.$;
|
||||
[ml, ml.parentElement].forEach(function (el) {
|
||||
el.setAttribute('class', 'non-realtime');
|
||||
});
|
||||
|
||||
var ifrWindow = $html.find('iframe')[0].contentWindow;
|
||||
|
||||
var documentBody = ifrWindow.document.body;
|
||||
|
||||
var inner = window.inner = documentBody;
|
||||
|
||||
var cursor = module.cursor = Cursor(inner);
|
||||
|
||||
var openLink = function (e) {
|
||||
var el = e.currentTarget;
|
||||
if (!el || el.nodeName !== 'A') { return; }
|
||||
var href = el.getAttribute('href');
|
||||
var bounceHref = window.location.origin + '/bounce/#' + encodeURIComponent(href);
|
||||
if (href) { ifrWindow.open(bounceHref, '_blank'); }
|
||||
};
|
||||
|
||||
var setEditable = module.setEditable = function (bool) {
|
||||
if (bool) {
|
||||
$(inner).css({
|
||||
color: '#333',
|
||||
});
|
||||
}
|
||||
if (!readOnly || !bool) {
|
||||
inner.setAttribute('contenteditable', bool);
|
||||
}
|
||||
};
|
||||
|
||||
CommonRealtime.onInfiniteSpinner(function () { setEditable(false); });
|
||||
|
||||
// don't let the user edit until the pad is ready
|
||||
setEditable(false);
|
||||
|
||||
var initializing = true;
|
||||
|
||||
var Title;
|
||||
//var UserList;
|
||||
//var Metadata;
|
||||
|
||||
var getHeadingText = function () {
|
||||
var text;
|
||||
if (['h1', 'h2', 'h3'].some(function (t) {
|
||||
var $header = $(inner).find(t + ':first-of-type');
|
||||
if ($header.length && $header.text()) {
|
||||
text = $header.text();
|
||||
return true;
|
||||
}
|
||||
})) { return text; }
|
||||
};
|
||||
|
||||
var DD = new DiffDom(mkDiffOptions(cursor, readOnly));
|
||||
|
||||
// apply patches, and try not to lose the cursor in the process!
|
||||
var applyHjson = function (shjson) {
|
||||
var userDocStateDom = hjsonToDom(JSON.parse(shjson));
|
||||
|
||||
if (!readOnly && !initializing) {
|
||||
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
|
||||
}
|
||||
var patch = (DD).diff(inner, userDocStateDom);
|
||||
(DD).apply(inner, patch);
|
||||
if (readOnly) {
|
||||
var $links = $(inner).find('a');
|
||||
// off so that we don't end up with multiple identical handlers
|
||||
$links.off('click', openLink).on('click', openLink);
|
||||
}
|
||||
};
|
||||
|
||||
var stringifyDOM = module.stringifyDOM = function (dom) {
|
||||
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
|
||||
hjson[3] = {
|
||||
metadata: metadataMgr.getMetadataLazy()
|
||||
};
|
||||
/*hjson[3] = { TODO
|
||||
users: UserList.userData,
|
||||
defaultTitle: Title.defaultTitle,
|
||||
type: 'pad'
|
||||
}
|
||||
};
|
||||
if (!initializing) {
|
||||
hjson[3].metadata.title = Title.title;
|
||||
} else if (Cryptpad.initialName && !hjson[3].metadata.title) {
|
||||
hjson[3].metadata.title = Cryptpad.initialName;
|
||||
}*/
|
||||
return stringify(hjson);
|
||||
};
|
||||
|
||||
var realtimeOptions = {
|
||||
readOnly: readOnly,
|
||||
// really basic operational transform
|
||||
transformFunction : JsonOT.validate,
|
||||
// cryptpad debug logging (default is 1)
|
||||
// logLevel: 0,
|
||||
validateContent: function (content) {
|
||||
try {
|
||||
JSON.parse(content);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log("Failed to parse, rejecting patch");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var setHistory = function (bool, update) {
|
||||
isHistoryMode = bool;
|
||||
setEditable(!bool);
|
||||
if (!bool && update) {
|
||||
realtimeOptions.onRemote();
|
||||
}
|
||||
};
|
||||
|
||||
realtimeOptions.onRemote = function () {
|
||||
if (initializing) { return; }
|
||||
if (isHistoryMode) { return; }
|
||||
|
||||
var oldShjson = stringifyDOM(inner);
|
||||
|
||||
var shjson = module.realtime.getUserDoc();
|
||||
|
||||
// remember where the cursor is
|
||||
cursor.update();
|
||||
|
||||
// Update the user list (metadata) from the hyperjson
|
||||
// TODO Metadata.update(shjson);
|
||||
|
||||
var newInner = JSON.parse(shjson);
|
||||
var newSInner;
|
||||
if (newInner.length > 2) {
|
||||
newSInner = stringify(newInner[2]);
|
||||
}
|
||||
|
||||
if (newInner[3]) {
|
||||
metadataMgr.updateMetadata(newInner[3].metadata);
|
||||
}
|
||||
|
||||
// build a dom from HJSON, diff, and patch the editor
|
||||
applyHjson(shjson);
|
||||
|
||||
if (!readOnly) {
|
||||
var shjson2 = stringifyDOM(inner);
|
||||
|
||||
// TODO
|
||||
//shjson = JSON.stringify(JSON.parse(shjson).slice(0,3));
|
||||
|
||||
if (shjson2 !== shjson) {
|
||||
console.error("shjson2 !== shjson");
|
||||
module.patchText(shjson2);
|
||||
|
||||
/* pushing back over the wire is necessary, but it can
|
||||
result in a feedback loop, which we call a browser
|
||||
fight */
|
||||
if (module.logFights) {
|
||||
// what changed?
|
||||
var op = TextPatcher.diff(shjson, shjson2);
|
||||
// log the changes
|
||||
TextPatcher.log(shjson, op);
|
||||
var sop = JSON.stringify(TextPatcher.format(shjson, op));
|
||||
|
||||
var index = module.fights.indexOf(sop);
|
||||
if (index === -1) {
|
||||
module.fights.push(sop);
|
||||
console.log("Found a new type of browser disagreement");
|
||||
console.log("You can inspect the list in your " +
|
||||
"console at `REALTIME_MODULE.fights`");
|
||||
console.log(module.fights);
|
||||
} else {
|
||||
console.log("Encountered a known browser disagreement: " +
|
||||
"available at `REALTIME_MODULE.fights[%s]`", index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify only when the content has changed, not when someone has joined/left
|
||||
var oldSInner = stringify(JSON.parse(oldShjson)[2]);
|
||||
if (newSInner && newSInner !== oldSInner) {
|
||||
common.notify();
|
||||
}
|
||||
};
|
||||
|
||||
var exportFile = function () {
|
||||
var html = getHTML(inner);
|
||||
var suggestion = Title.suggestTitle('cryptpad-document');
|
||||
Cryptpad.prompt(Messages.exportPrompt,
|
||||
Cryptpad.fixFileName(suggestion) + '.html', function (filename) {
|
||||
if (!(typeof(filename) === 'string' && filename)) { return; }
|
||||
var blob = new Blob([html], {type: "text/html;charset=utf-8"});
|
||||
saveAs(blob, filename);
|
||||
});
|
||||
};
|
||||
var importFile = function (content) {
|
||||
var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body));
|
||||
applyHjson(shjson);
|
||||
realtimeOptions.onLocal();
|
||||
};
|
||||
|
||||
realtimeOptions.onInit = function (info) {
|
||||
readOnly = metadataMgr.getPrivateData().readOnly;
|
||||
console.log('onInit');
|
||||
var titleCfg = { getHeadingText: getHeadingText };
|
||||
Title = common.createTitle(titleCfg, realtimeOptions.onLocal, common, metadataMgr);
|
||||
var configTb = {
|
||||
displayed: ['userlist', 'title', 'useradmin', 'spinner', 'newpad', 'share', 'limit'],
|
||||
title: Title.getTitleConfig(),
|
||||
metadataMgr: metadataMgr,
|
||||
readOnly: readOnly,
|
||||
ifrw: window,
|
||||
realtime: info.realtime,
|
||||
common: Cryptpad,
|
||||
sfCommon: common,
|
||||
$container: $bar,
|
||||
$contentContainer: $('#cke_1_contents'),
|
||||
};
|
||||
toolbar = info.realtime.toolbar = Toolbar.create(configTb);
|
||||
Title.setToolbar(toolbar);
|
||||
|
||||
var $rightside = toolbar.$rightside;
|
||||
var $drawer = toolbar.$drawer;
|
||||
|
||||
var src = 'less!/customize/src/less/toolbar.less';
|
||||
require([
|
||||
src
|
||||
], function () {
|
||||
var $html = $bar.closest('html');
|
||||
$html
|
||||
.find('head style[data-original-src="' + src.replace(/less!/, '') + '"]')
|
||||
.appendTo($html.find('head'));
|
||||
});
|
||||
|
||||
$bar.find('#cke_1_toolbar_collapser').hide();
|
||||
if (!readOnly) {
|
||||
// Expand / collapse the toolbar
|
||||
var $collapse = Cryptpad.createButton(null, true);
|
||||
$collapse.removeClass('fa-question');
|
||||
var updateIcon = function () {
|
||||
$collapse.removeClass('fa-caret-down').removeClass('fa-caret-up');
|
||||
var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible');
|
||||
if (isCollapsed) {
|
||||
if (!initializing) { common.feedback('HIDETOOLBAR_PAD'); }
|
||||
$collapse.addClass('fa-caret-down');
|
||||
}
|
||||
else {
|
||||
if (!initializing) { common.feedback('SHOWTOOLBAR_PAD'); }
|
||||
$collapse.addClass('fa-caret-up');
|
||||
}
|
||||
};
|
||||
updateIcon();
|
||||
$collapse.click(function () {
|
||||
$(window).trigger('resize');
|
||||
$('.cke_toolbox_main').toggle();
|
||||
$(window).trigger('cryptpad-ck-toolbar');
|
||||
updateIcon();
|
||||
});
|
||||
$rightside.append($collapse);
|
||||
} else {
|
||||
$('.cke_toolbox_main').hide();
|
||||
}
|
||||
|
||||
/* add a history button */
|
||||
var histConfig = {
|
||||
onLocal: realtimeOptions.onLocal,
|
||||
onRemote: realtimeOptions.onRemote,
|
||||
setHistory: setHistory,
|
||||
applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); },
|
||||
$toolbar: $bar
|
||||
};
|
||||
var $hist = common.createButton('history', true, {histConfig: histConfig});
|
||||
$drawer.append($hist);
|
||||
|
||||
if (!metadataMgr.getPrivateData().isTemplate) {
|
||||
var templateObj = {
|
||||
rt: info.realtime,
|
||||
getTitle: function () { return metadataMgr.getMetadata().title; }
|
||||
};
|
||||
var $templateButton = common.createButton('template', true, templateObj);
|
||||
$rightside.append($templateButton);
|
||||
}
|
||||
|
||||
/* add an export button */
|
||||
var $export = Cryptpad.createButton('export', true, {}, exportFile);
|
||||
$drawer.append($export);
|
||||
|
||||
if (!readOnly) {
|
||||
/* add an import button */
|
||||
var $import = Cryptpad.createButton('import', true, {
|
||||
accept: 'text/html'
|
||||
}, importFile);
|
||||
$drawer.append($import);
|
||||
}
|
||||
|
||||
/* add a forget button */
|
||||
var forgetCb = function (err) {
|
||||
if (err) { return; }
|
||||
setEditable(false);
|
||||
};
|
||||
var $forgetPad = common.createButton('forget', true, {}, forgetCb);
|
||||
$rightside.append($forgetPad);
|
||||
};
|
||||
|
||||
// this should only ever get called once, when the chain syncs
|
||||
realtimeOptions.onReady = function (info) {
|
||||
console.log('onReady');
|
||||
if (!module.isMaximized) {
|
||||
module.isMaximized = true;
|
||||
$('iframe.cke_wysiwyg_frame').css('width', '');
|
||||
$('iframe.cke_wysiwyg_frame').css('height', '');
|
||||
}
|
||||
$('body').addClass('app-pad');
|
||||
|
||||
if (module.realtime !== info.realtime) {
|
||||
module.patchText = TextPatcher.create({
|
||||
realtime: info.realtime,
|
||||
//logging: true,
|
||||
});
|
||||
}
|
||||
|
||||
module.realtime = info.realtime;
|
||||
|
||||
var shjson = module.realtime.getUserDoc();
|
||||
|
||||
var newPad = false;
|
||||
if (shjson === '') { newPad = true; }
|
||||
|
||||
if (!newPad) {
|
||||
applyHjson(shjson);
|
||||
|
||||
// Update the user list (metadata) from the hyperjson
|
||||
// XXX Metadata.update(shjson);
|
||||
var parsed = JSON.parse(shjson);
|
||||
if (parsed[3] && parsed[3].metadata) {
|
||||
metadataMgr.updateMetadata(parsed[3].metadata);
|
||||
}
|
||||
|
||||
if (!readOnly) {
|
||||
var shjson2 = stringifyDOM(inner);
|
||||
var hjson2 = JSON.parse(shjson2).slice(0,3);
|
||||
var hjson = JSON.parse(shjson).slice(0,3);
|
||||
if (stringify(hjson2) !== stringify(hjson)) {
|
||||
console.log('err');
|
||||
console.error("shjson2 !== shjson");
|
||||
console.log(stringify(hjson2));
|
||||
console.log(stringify(hjson));
|
||||
Cryptpad.errorLoadingScreen(Messages.wrongApp);
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Title.updateTitle(Cryptpad.initialName || Title.defaultTitle);
|
||||
documentBody.innerHTML = Messages.initialState;
|
||||
}
|
||||
|
||||
Cryptpad.removeLoadingScreen(emitResize);
|
||||
setEditable(!readOnly);
|
||||
initializing = false;
|
||||
|
||||
if (readOnly) { return; }
|
||||
//TODO UserList.getLastName(toolbar.$userNameButton, newPad);
|
||||
onLocal();
|
||||
editor.focus();
|
||||
if (newPad) {
|
||||
cursor.setToEnd();
|
||||
} else {
|
||||
cursor.setToStart();
|
||||
}
|
||||
};
|
||||
|
||||
realtimeOptions.onConnectionChange = function (info) {
|
||||
setEditable(info.state);
|
||||
//toolbar.failed(); TODO
|
||||
if (info.state) {
|
||||
initializing = true;
|
||||
//toolbar.reconnecting(info.myId); // TODO
|
||||
Cryptpad.findOKButton().click();
|
||||
} else {
|
||||
Cryptpad.alert(Messages.common_connectionLost, undefined, true);
|
||||
}
|
||||
};
|
||||
|
||||
realtimeOptions.onError = onConnectError;
|
||||
|
||||
onLocal = realtimeOptions.onLocal = function () {
|
||||
console.log('onlocal');
|
||||
if (initializing) { return; }
|
||||
if (isHistoryMode) { return; }
|
||||
if (readOnly) { return; }
|
||||
|
||||
// stringify the json and send it into chainpad
|
||||
var shjson = stringifyDOM(inner);
|
||||
|
||||
module.patchText(shjson);
|
||||
if (module.realtime.getUserDoc() !== shjson) {
|
||||
console.error("realtime.getUserDoc() !== shjson");
|
||||
}
|
||||
};
|
||||
|
||||
cpNfInner = common.startRealtime(realtimeOptions);
|
||||
metadataMgr = cpNfInner.metadataMgr;
|
||||
|
||||
Cryptpad.onLogout(function () { setEditable(false); });
|
||||
|
||||
/* hitting enter makes a new line, but places the cursor inside
|
||||
of the <br> instead of the <p>. This makes it such that you
|
||||
cannot type until you click, which is rather unnacceptable.
|
||||
If the cursor is ever inside such a <br>, you probably want
|
||||
to push it out to the parent element, which ought to be a
|
||||
paragraph tag. This needs to be done on keydown, otherwise
|
||||
the first such keypress will not be inserted into the P. */
|
||||
inner.addEventListener('keydown', cursor.brFix);
|
||||
|
||||
editor.on('change', onLocal);
|
||||
|
||||
// export the typing tests to the window.
|
||||
// call like `test = easyTest()`
|
||||
// terminate the test like `test.cancel()`
|
||||
window.easyTest = function () {
|
||||
cursor.update();
|
||||
var start = cursor.Range.start;
|
||||
var test = TypingTest.testInput(inner, start.el, start.offset, onLocal);
|
||||
onLocal();
|
||||
return test;
|
||||
};
|
||||
|
||||
$bar.find('.cke_button').click(function () {
|
||||
var e = this;
|
||||
var classString = e.getAttribute('class');
|
||||
var classes = classString.split(' ').filter(function (c) {
|
||||
return /cke_button__/.test(c);
|
||||
});
|
||||
|
||||
var id = classes[0];
|
||||
if (typeof(id) === 'string') {
|
||||
common.feedback(id.toUpperCase());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var main = function () {
|
||||
var Ckeditor;
|
||||
var editor;
|
||||
var common;
|
||||
|
||||
nThen(function (waitFor) {
|
||||
ckEditorAvailable(waitFor(function (ck) {
|
||||
Ckeditor = ck;
|
||||
require(['/pad/wysiwygarea-plugin.js'], waitFor());
|
||||
}));
|
||||
$(waitFor(function () {
|
||||
Cryptpad.addLoadingScreen();
|
||||
}));
|
||||
SFCommon.create(waitFor(function (c) { module.common = common = c; }));
|
||||
}).nThen(function (waitFor) {
|
||||
Ckeditor.config.toolbarCanCollapse = true;
|
||||
if (screen.height < 800) {
|
||||
Ckeditor.config.toolbarStartupExpanded = false;
|
||||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no');
|
||||
} else {
|
||||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes');
|
||||
}
|
||||
// Used in ckeditor-config.js
|
||||
Ckeditor.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs;
|
||||
module.ckeditor = editor = Ckeditor.replace('editor1', {
|
||||
customConfig: '/customize/ckeditor-config.js',
|
||||
});
|
||||
editor.on('instanceReady', waitFor());
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
/*if (Ckeditor.env.safari) {
|
||||
var fixIframe = function () {
|
||||
$('iframe.cke_wysiwyg_frame').height($('#cke_1_contents').height());
|
||||
};
|
||||
$(window).resize(fixIframe);
|
||||
fixIframe();
|
||||
}*/
|
||||
Links.addSupportForOpeningLinksInNewTab(Ckeditor)({editor: editor});
|
||||
Cryptpad.onError(function (info) {
|
||||
if (info && info.type === "store") {
|
||||
onConnectError();
|
||||
}
|
||||
});
|
||||
andThen(editor, Ckeditor, common);
|
||||
});
|
||||
};
|
||||
main();
|
||||
});
|
||||
|
Loading…
Reference in New Issue