pull/1/head
Caleb James DeLisle 8 years ago
parent a612f02be2
commit 4b25ab80d6

@ -39,6 +39,7 @@
"require-css": "0.1.10", "require-css": "0.1.10",
"less": "^2.7.2", "less": "^2.7.2",
"bootstrap": "#v4.0.0-alpha.6", "bootstrap": "#v4.0.0-alpha.6",
"diff-dom": "2.1.1" "diff-dom": "2.1.1",
"nthen": "^0.1.5"
} }
} }

@ -3,7 +3,7 @@
window.addEventListener('message', function (msg) { window.addEventListener('message', function (msg) {
var data = JSON.parse(msg.data); var data = JSON.parse(msg.data);
if (data.q !== 'INIT') { return; } if (data.q !== 'INIT') { return; }
msg.source.postMessage({ txid: data.txid, content: 'OK' }, '*'); msg.source.postMessage(JSON.stringify({ txid: data.txid, content: 'OK' }), '*');
if (data.content && data.content.requireConf) { require.config(data.content.requireConf); } if (data.content && data.content.requireConf) { require.config(data.content.requireConf); }
require(['/common/sframe-boot2.js'], function () { }); require(['/common/sframe-boot2.js'], function () { });
}); });

@ -1,8 +1,9 @@
// This is stage 1, it can be changed but you must bump the version of the project. // 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. // Note: This must only be loaded from inside of a sandbox-iframe.
define([ define([
'/common/requireconfig.js' '/common/requireconfig.js',
], function (RequireConfig) { '/common/sframe-channel.js'
], function (RequireConfig, SFrameChannel) {
require.config(RequireConfig); require.config(RequireConfig);
console.log('boot2'); console.log('boot2');
// most of CryptPad breaks if you don't support isArray // most of CryptPad breaks if you don't support isArray
@ -22,5 +23,7 @@ console.log('boot2');
window.__defineGetter__('localStorage', function () { return mkFakeStore(); }); window.__defineGetter__('localStorage', function () { return mkFakeStore(); });
window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); }); window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); });
SFrameChannel.init(window.top, function () { });
require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]); require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]);
}); });

@ -15,39 +15,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
define([ define([
'/bower_components/netflux-websocket/netflux-client.js', '/common/sframe-channel.js',
'/bower_components/chainpad/chainpad.dist.js', '/bower_components/chainpad/chainpad.dist.js',
], function (Netflux) { ], function (SFrameChannel) {
var ChainPad = window.ChainPad; var ChainPad = window.ChainPad;
var USE_HISTORY = true;
var module = { exports: {} }; var module = { exports: {} };
var verbose = function (x) { console.log(x); }; var verbose = function (x) { console.log(x); };
verbose = function () {}; // comment out to enable verbose logging verbose = function () {}; // comment out to enable verbose logging
var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; var mkUserList = function () {
var userList = Object.freeze({
module.exports.start = function (config) {
console.log(config);
var websocketUrl = config.websocketURL;
var userName = config.userName;
var channel = config.channel;
var Crypto = config.crypto;
var validateKey = config.validateKey;
var readOnly = config.readOnly || false;
// make sure configuration is defined
config = config || {};
var initializing = true;
var toReturn = {};
var messagesHistory = [];
var chainpadAdapter = {};
var realtime;
var network = config.network;
var lastKnownHash;
var userList = {
change : [], change : [],
onChange : function(newData) { onChange : function(newData) {
userList.change.forEach(function (el) { userList.change.forEach(function (el) {
@ -55,7 +33,7 @@ define([
}); });
}, },
users: [] users: []
}; });
var onJoining = function (peer) { var onJoining = function (peer) {
if(peer.length !== 32) { return; } if(peer.length !== 32) { return; }
@ -67,98 +45,6 @@ define([
userList.onChange(); userList.onChange();
}; };
var onReady = function(wc, network) {
// 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; }
realtime.start();
if(config.setMyID) {
config.setMyID({
myID: wc.myID
});
}
// Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced
if (!readOnly) {
onJoining(wc.myID);
}
// we're fully synced
initializing = false;
if (config.onReady) {
config.onReady({
realtime: realtime,
network: network,
userList: userList,
myId: wc.myID,
leave: wc.leave
});
}
};
var onMessage = function(peer, msg, wc, network, direct) {
// unpack the history keeper from the webchannel
var hk = network.historyKeeper;
// Old server
if(wc && (msg === 0 || msg === '0')) {
onReady(wc, network);
return;
}
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, network);
}
// 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 = chainpadAdapter.msgIn(peer, msg);
verbose(message);
if (!initializing) {
if (config.onLocal) {
config.onLocal();
}
}
// 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
realtime.message(message);
};
// update UI components to show that one of the other peers has left // update UI components to show that one of the other peers has left
var onLeaving = function (peer) { var onLeaving = function (peer) {
var list = userList.users; var list = userList.users;
@ -169,246 +55,93 @@ define([
userList.onChange(); userList.onChange();
}; };
// shim between chainpad and netflux var onReset = function () {
chainpadAdapter = { userList.users.forEach(onLeaving);
msgIn : function(peerId, msg) {
msg = msg.replace(/^cp\|/, '');
try {
var decryptedMsg = Crypto.decrypt(msg, validateKey);
messagesHistory.push(decryptedMsg);
return decryptedMsg;
} catch (err) {
console.error(err);
return msg;
}
},
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 createRealtime = function() { return Object.freeze({
return ChainPad.create({ list: userList,
userName: userName, onJoin: onJoining,
initialState: config.initialState, onLeave: onLeaving,
transformFunction: config.transformFunction, onReset: onReset
validateContent: config.validateContent,
avgSyncMilliseconds: config.avgSyncMilliseconds,
logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1
}); });
}; };
// We use an object to store the webchannel so that we don't have to push new handlers to chainpad module.exports.start = function (config) {
// and remove the old ones when reconnecting and keeping the same 'realtime' object var onConnectionChange = config.onConnectionChange || function () { };
// See realtime.onMessage below: we call wc.bcast(...) but wc may change var onRemote = config.onRemote || function () { };
var wcObject = {}; var onInit = config.onInit || function () { };
var onOpen = function(wc, network, initialize) { var onLocal = config.onLocal || function () { };
wcObject.wc = wc; var setMyID = config.setMyID || function () { };
channel = wc.id; var onReady = config.onReady || function () { };
var userName = config.userName;
// Add the existing peers in the userList var initialState = config.initialState;
wc.members.forEach(onJoining); var transformFunction = config.transformFunction;
var validateContent = config.validateContent;
// Add the handlers to the WebChannel var avgSyncMilliseconds = config.avgSyncMilliseconds;
wc.on('message', function (msg, sender) { //Channel msg var logLevel = typeof(config.logLevel) !== 'undefined'? config.logLevel : 1;
onMessage(sender, msg, wc, network); var readOnly = config.readOnly || false;
}); config = undefined;
wc.on('join', onJoining);
wc.on('leave', onLeaving); var chainpad;
var userList = mkUserList();
if (initialize) { var myID;
toReturn.realtime = realtime = createRealtime(); var isReady = false;
realtime._patch = realtime.patch; SFrameChannel.on('EV_RT_JOIN', userList.onJoin);
realtime.patch = function (patch, x, y) { SFrameChannel.on('EV_RT_LEAVE', userList.onLeave);
if (initializing) { SFrameChannel.on('EV_RT_DISCONNECT', function () {
console.error("attempted to change the content before chainpad was synced"); isReady = false;
} userList.onReset();
return realtime._patch(patch, x, y); onConnectionChange({ state: false });
}; });
realtime._change = realtime.change; SFrameChannel.on('EV_RT_CONNECT', function (content) {
realtime.change = function (offset, count, chars) { content.members.forEach(userList.onJoin);
if (initializing) { myID = content.myID;
console.error("attempted to change the content before chainpad was synced"); isReady = false;
} if (chainpad) {
return realtime._change(offset, count, chars); // it's a reconnect
}; onConnectionChange({ state: true, myId: myID });
return;
if (config.onInit) {
config.onInit({
myID: wc.myID,
realtime: realtime,
getLag: network.getLag,
userList: userList,
network: network,
channel: channel
});
}
// Sending a message...
realtime.onMessage(function(message, cb) {
// Filter messages sent by Chainpad to make it compatible with Netflux
message = chainpadAdapter.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
wcObject.wc.bcast(message).then(function() {
cb();
}, function(err) {
// The message has not been sent, display the error.
console.error(err);
});
}
});
realtime.onPatch(function () {
if (config.onRemote) {
config.onRemote({
realtime: realtime
});
}
});
}
// 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, network);
} }
}; chainpad = ChainPad.create({
userName: userName,
// Set a flag to avoid calling onAbort or onConnectionChange when the user is leaving the page initialState: initialState,
var isIntentionallyLeaving = false; transformFunction: transformFunction,
window.addEventListener("beforeunload", function () { validateContent: validateContent,
isIntentionallyLeaving = true; avgSyncMilliseconds: avgSyncMilliseconds,
}); logLevel: logLevel
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; chainpad.onMessage(function(message, cb) {
}; SFrameChannel.query('Q_RT_MESSAGE', message, cb);
var onConnectError = function (err) {
if (config.onError) {
config.onError({
error: err.type
}); });
} chainpad.onPatch(function () {
}; onRemote({ realtime: chainpad });
var joinSession = function (endPoint, cb) {
// a websocket URL has been provided
// connect to it with Netflux.
if (typeof(endPoint) === 'string') {
Netflux.connect(endPoint).then(cb, onConnectError);
} else if (typeof(endPoint.then) === 'function') {
// a netflux network promise was provided
// connect to it and use a channel
endPoint.then(cb, onConnectError);
} else {
// assume it's a network and try to connect.
cb(endPoint);
}
};
var firstConnection = true;
/* Connect to the Netflux network, or fall back to a WebSocket
in theory this lets us connect to more netflux channels using only
one network. */
var connectTo = function (network) {
// join the netflux network, promise to handle opening of the channel
network.join(channel || null).then(function(wc) {
onOpen(wc, network, firstConnection);
firstConnection = false;
}, function(error) {
console.error(error);
}); });
}; onInit({
myID: content.myID,
joinSession(network || websocketUrl, function (network) { realtime: chainpad,
// pass messages that come out of netflux into our local handler userList: userList,
if (firstConnection) { readOnly: readOnly
toReturn.network = network;
network.on('disconnect', function (reason) {
if (isIntentionallyLeaving) { return; }
if (reason === "network.disconnect() called") { return; }
if (config.onConnectionChange) {
config.onConnectionChange({
state: false
}); });
return;
}
if (config.onAbort) {
config.onAbort({
reason: reason
}); });
SFrameChannel.on('Q_RT_MESSAGE', function (content, cb) {
if (isReady) {
onLocal(); // should be onBeforeMessage
} }
chainpad.message(content);
cb('OK');
}); });
SFrameChannel.on('EV_RT_READY', function () {
network.on('reconnect', function (uid) { if (isReady) { return; }
if (config.onConnectionChange) { isReady = true;
config.onConnectionChange({ chainpad.start();
state: true, setMyID({ myID: myID });
myId: uid // Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced
}); if (!readOnly) { userList.onJoin(myID); }
var afterReconnecting = function () { onReady({ realtime: chainpad });
initializing = true;
userList.users=[];
joinSession(network, connectTo);
};
if (config.beforeReconnecting) {
config.beforeReconnecting(function (newKey, newContent) {
channel = newKey;
config.initialState = newContent;
afterReconnecting();
}); });
return; return;
}
afterReconnecting();
}
});
network.on('message', function (msg, sender) { // Direct message
var wchan = findChannelById(network.webChannels, channel);
if(wchan) {
onMessage(sender, msg, wchan, network, true);
}
});
}
connectTo(network);
}, onConnectError);
return toReturn;
}; };
return module.exports; return module.exports;
}); });

@ -15,10 +15,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
define([ define([
'/bower_components/netflux-websocket/netflux-client.js', '/common/sframe-channel.js',
'/bower_components/chainpad/chainpad.dist.js', ], function (SFrameChannel) {
], function (Netflux) {
var ChainPad = window.ChainPad;
var USE_HISTORY = true; var USE_HISTORY = true;
var module = { exports: {} }; var module = { exports: {} };
@ -27,50 +25,53 @@ define([
var unBencode = function (str) { return str.replace(/^\d+:/, ''); }; var unBencode = function (str) { return str.replace(/^\d+:/, ''); };
module.exports.start = function (conf) { var start = function (conf) {
var websocketUrl = conf.websocketURL;
var userName = conf.userName;
var channel = conf.channel; var channel = conf.channel;
var Crypto = conf.crypto; var Crypto = conf.crypto;
var validateKey = conf.validateKey; var validateKey = conf.validateKey;
var readOnly = conf.readOnly || false; var readOnly = conf.readOnly || false;
var websocketURL = conf.websocketURL;
var network = conf.network; var network = conf.network;
conf = undefined; conf = undefined;
var initializing = true; var initializing = true;
var toReturn = {};
var messagesHistory = [];
var chainpadAdapter = {};
var realtime;
var lastKnownHash; var lastKnownHash;
var onReady = function(wc, network) { var queue = [];
var messageFromInner = function (m, cb) { queue.push([ m, cb ]); };
SFrameChannel.on('Q_RT_MESSAGE', function (message, cb) {
messageFromInner(message, cb);
});
var onReady = function(wc) {
// Trigger onReady only if not ready yet. This is important because the history keeper sends a direct // 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. // message through "network" when it is synced, and it triggers onReady for each channel joined.
if (!initializing) { return; } if (!initializing) { return; }
SFrameChannel.event('EV_RT_READY', null);
realtime.start();
if(setMyID) {
setMyID({ myID: wc.myID });
}
// Trigger onJoining with our own Cryptpad username to tell the toolbar that we are synced
if (!readOnly) {
onJoining(wc.myID);
}
// we're fully synced // we're fully synced
initializing = false; initializing = false;
};
if (config.onReady) { // shim between chainpad and netflux
config.onReady({ var msgIn = function (peerId, msg) {
realtime: realtime, msg = msg.replace(/^cp\|/, '');
network: network, try {
userList: userList, var decryptedMsg = Crypto.decrypt(msg, validateKey);
myId: wc.myID, return decryptedMsg;
leave: wc.leave } 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;
} }
}; };
@ -78,11 +79,6 @@ define([
// unpack the history keeper from the webchannel // unpack the history keeper from the webchannel
var hk = network.historyKeeper; var hk = network.historyKeeper;
// Old server
if(wc && (msg === 0 || msg === '0')) {
onReady(wc, network);
return;
}
if (direct && peer !== hk) { if (direct && peer !== hk) {
return; return;
} }
@ -98,7 +94,7 @@ define([
} }
if (parsed.state && parsed.state === 1 && parsed.channel) { if (parsed.state && parsed.state === 1 && parsed.channel) {
if (parsed.channel === wc.id) { if (parsed.channel === wc.id) {
onReady(wc, network); onReady(wc);
} }
// We have to return even if it is not the current channel: // We have to return even if it is not the current channel:
// we don't want to continue with other channels messages here // we don't want to continue with other channels messages here
@ -116,142 +112,54 @@ define([
} }
lastKnownHash = msg.slice(0,64); lastKnownHash = msg.slice(0,64);
var message = chainpadAdapter.msgIn(peer, msg); var message = msgIn(peer, msg);
verbose(message); verbose(message);
if (!initializing) {
if (config.onLocal) {
config.onLocal();
}
}
// slice off the bencoded header // slice off the bencoded header
// Why are we getting bencoded stuff to begin with? // Why are we getting bencoded stuff to begin with?
// FIXME this shouldn't be necessary // FIXME this shouldn't be necessary
message = unBencode(message);//.slice(message.indexOf(':[') + 1); message = unBencode(message);//.slice(message.indexOf(':[') + 1);
// pass the message into Chainpad // pass the message into Chainpad
realtime.message(message); SFrameChannel.query('Q_RT_MESSAGE', message, function () { });
};
// update UI components to show that one of the other peers has left
var onLeaving = function(peer) {
var list = userList.users;
var index = list.indexOf(peer);
if(index !== -1) {
userList.users.splice(index, 1);
}
userList.onChange();
};
// shim between chainpad and netflux
chainpadAdapter = {
msgIn : function(peerId, msg) {
msg = msg.replace(/^cp\|/, '');
try {
var decryptedMsg = Crypto.decrypt(msg, validateKey);
messagesHistory.push(decryptedMsg);
return decryptedMsg;
} catch (err) {
console.error(err);
return msg;
}
},
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 createRealtime = function() {
return ChainPad.create({
userName: userName,
initialState: config.initialState,
transformFunction: config.transformFunction,
validateContent: config.validateContent,
avgSyncMilliseconds: config.avgSyncMilliseconds,
logLevel: typeof(config.logLevel) !== 'undefined'? config.logLevel : 1
});
}; };
// We use an object to store the webchannel so that we don't have to push new handlers to chainpad // 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 // 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 // See realtime.onMessage below: we call wc.bcast(...) but wc may change
var wcObject = {}; var wcObject = {};
var onOpen = function(wc, network, initialize) { var onOpen = function(wc, network, firstConnection) {
wcObject.wc = wc; wcObject.wc = wc;
channel = wc.id; channel = wc.id;
// Add the existing peers in the userList // Add the existing peers in the userList
wc.members.forEach(onJoining); SFrameChannel.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly });
// Add the handlers to the WebChannel // Add the handlers to the WebChannel
wc.on('message', function (msg, sender) { //Channel msg wc.on('message', function (msg, sender) { //Channel msg
onMessage(sender, msg, wc, network); onMessage(sender, msg, wc, network);
}); });
wc.on('join', onJoining); wc.on('join', function (m) { SFrameChannel.event('EV_RT_JOIN', m); });
wc.on('leave', onLeaving); wc.on('leave', function (m) { SFrameChannel.event('EV_RT_LEAVE', m); });
if (initialize) {
toReturn.realtime = realtime = createRealtime();
realtime._patch = realtime.patch;
realtime.patch = function (patch, x, y) {
if (initializing) {
console.error("attempted to change the content before chainpad was synced");
}
return realtime._patch(patch, x, y);
};
realtime._change = realtime.change;
realtime.change = function (offset, count, chars) {
if (initializing) {
console.error("attempted to change the content before chainpad was synced");
}
return realtime._change(offset, count, chars);
};
if (config.onInit) {
config.onInit({
myID: wc.myID,
realtime: realtime,
getLag: network.getLag,
userList: userList,
network: network,
channel: channel
});
}
if (firstConnection) {
// Sending a message... // Sending a message...
realtime.onMessage(function(message, cb) { messageFromInner = function(message, cb) {
// Filter messages sent by Chainpad to make it compatible with Netflux // Filter messages sent by Chainpad to make it compatible with Netflux
message = chainpadAdapter.msgOut(message); message = msgOut(message);
if (message) { if (message) {
// Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we // 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 // want to keep the same chainpad (realtime) object
wcObject.wc.bcast(message).then(function() { wcObject.wc.bcast(message).then(function() {
cb(); cb('OK');
}, function(err) { }, function(err) {
// The message has not been sent, display the error. // The message has not been sent, display the error.
console.error(err); console.error(err);
}); });
} }
}); };
queue.forEach(function (arr) { messageFromInner(arr[0], arr[1]); });
realtime.onPatch(function () {
if (config.onRemote) {
config.onRemote({
realtime: realtime
});
}
});
} }
// Get the channel history // Get the channel history
@ -268,13 +176,11 @@ define([
msg.push(validateKey); msg.push(validateKey);
msg.push(lastKnownHash); msg.push(lastKnownHash);
if (hk) { network.sendto(hk, JSON.stringify(msg)); } if (hk) { network.sendto(hk, JSON.stringify(msg)); }
} } else {
else { onReady(wc);
onReady(wc, network);
} }
}; };
// Set a flag to avoid calling onAbort or onConnectionChange when the user is leaving the page
var isIntentionallyLeaving = false; var isIntentionallyLeaving = false;
window.addEventListener("beforeunload", function () { window.addEventListener("beforeunload", function () {
isIntentionallyLeaving = true; isIntentionallyLeaving = true;
@ -292,85 +198,24 @@ define([
return webChannel; return webChannel;
}; };
var onConnectError = function (err) { var connectTo = function (network, firstConnection) {
if (config.onError) {
config.onError({
error: err.type
});
}
};
var joinSession = function (endPoint, cb) {
// a websocket URL has been provided
// connect to it with Netflux.
if (typeof(endPoint) === 'string') {
Netflux.connect(endPoint).then(cb, onConnectError);
} else if (typeof(endPoint.then) === 'function') {
// a netflux network promise was provided
// connect to it and use a channel
endPoint.then(cb, onConnectError);
} else {
// assume it's a network and try to connect.
cb(endPoint);
}
};
var firstConnection = true;
/* Connect to the Netflux network, or fall back to a WebSocket
in theory this lets us connect to more netflux channels using only
one network. */
var connectTo = function (network) {
// join the netflux network, promise to handle opening of the channel // join the netflux network, promise to handle opening of the channel
network.join(channel || null).then(function(wc) { network.join(channel || null).then(function(wc) {
onOpen(wc, network, firstConnection); onOpen(wc, network, firstConnection);
firstConnection = false;
}, function(error) { }, function(error) {
console.error(error); console.error(error);
}); });
}; };
joinSession(network || websocketUrl, function (network) {
// pass messages that come out of netflux into our local handler
if (firstConnection) {
toReturn.network = network;
network.on('disconnect', function (reason) { network.on('disconnect', function (reason) {
if (isIntentionallyLeaving) { return; } if (isIntentionallyLeaving) { return; }
if (reason === "network.disconnect() called") { return; } if (reason === "network.disconnect() called") { return; }
if (config.onConnectionChange) { SFrameChannel.event('EV_RT_DISCONNECT');
config.onConnectionChange({
state: false
});
return;
}
if (config.onAbort) {
config.onAbort({
reason: reason
});
}
}); });
network.on('reconnect', function (uid) { network.on('reconnect', function (uid) {
if (config.onConnectionChange) {
config.onConnectionChange({
state: true,
myId: uid
});
var afterReconnecting = function () {
initializing = true; initializing = true;
userList.users=[]; connectTo(network, false);
joinSession(network, connectTo);
};
if (config.beforeReconnecting) {
config.beforeReconnecting(function (newKey, newContent) {
channel = newKey;
config.initialState = newContent;
afterReconnecting();
});
return;
}
afterReconnecting();
}
}); });
network.on('message', function (msg, sender) { // Direct message network.on('message', function (msg, sender) { // Direct message
@ -379,12 +224,13 @@ define([
onMessage(sender, msg, wchan, network, true); onMessage(sender, msg, wchan, network, true);
} }
}); });
}
connectTo(network); connectTo(network, true);
}, onConnectError); };
return toReturn; return {
start: function (config) {
SFrameChannel.whenReg('EV_RT_READY', function () { start(config); });
}
}; };
return module.exports;
}); });

@ -1,38 +1,108 @@
// This file provides the internal API for talking from inside of the sandbox iframe // This file provides the API for the channel for talking to and from the sandbox iframe.
// The external API is in sframe-ctrl.js define([
define([], function () { '/common/sframe-protocol.js'
var iframe; ], function (SFrameProtocol) {
var otherWindow;
var handlers = {}; var handlers = {};
var queries = {}; var queries = {};
// list of handlers which are registered from the other side...
var insideHandlers = [];
var callWhenRegistered = {};
var module = { exports: {} }; var module = { exports: {} };
var mkTxid = function () { var mkTxid = function () {
return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', ''); return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', '');
}; };
module.exports.init = function (ow, cb) {
if (otherWindow) { throw new Error('already initialized'); }
var intr;
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) {
if (data.txid !== txid) {
console.log("DROP Message with weird txid");
return;
}
clearInterval(intr);
otherWindow = ow;
cb();
} else if (typeof(data.q) === 'string' && handlers[data.q]) {
handlers[data.q](data, msg);
} else if (typeof(data.q) === 'undefined' && queries[data.txid]) {
queries[data.txid](data, msg);
} else if (data.txid === txid) {
// stray message from init
return;
} else {
console.log("DROP Unhandled message");
console.log(msg);
}
});
if (window !== window.top) {
// we're in the sandbox
otherWindow = ow;
cb();
} else {
require(['/common/requireconfig.js'], function (RequireConfig) {
txid = mkTxid();
intr = setInterval(function () {
ow.postMessage(JSON.stringify({
txid: txid,
content: { requireConf: RequireConfig },
q: 'INIT'
}), '*');
});
});
}
};
module.exports.query = function (q, content, cb) { module.exports.query = function (q, content, cb) {
if (!iframe) { throw new Error('not yet initialized'); } 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 txid = mkTxid();
var timeout = setTimeout(function () { var timeout = setTimeout(function () {
delete queries[txid]; delete queries[txid];
cb("Timeout making query " + q); console.log("Timeout making query " + q);
}); }, 30000);
queries[txid] = function (data, msg) { queries[txid] = function (data, msg) {
clearTimeout(timeout); clearTimeout(timeout);
delete queries[txid]; delete queries[txid];
cb(undefined, data.content, msg); cb(undefined, data.content, msg);
}; };
iframe.contentWindow.postMessage(JSON.stringify({ otherWindow.postMessage(JSON.stringify({
txid: txid, txid: txid,
content: content, content: content,
q: q q: q
}), '*'); }), '*');
}; };
module.exports.registerHandler = function (queryType, handler) { var event = module.exports.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 }), '*');
};
module.exports.on = function (queryType, handler) {
if (!otherWindow) { throw new Error('not yet initialized'); }
if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); } if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); }
handlers[queryType] = function (msg) { if (!SFrameProtocol[queryType]) {
var data = JSON.parse(msg.data); throw new Error('please only register handlers which are defined in sframe-protocol.js');
}
handlers[queryType] = function (data, msg) {
handler(data.content, function (replyContent) { handler(data.content, function (replyContent) {
msg.source.postMessage(JSON.stringify({ msg.source.postMessage(JSON.stringify({
txid: data.txid, txid: data.txid,
@ -40,6 +110,27 @@ define([], function () {
}), '*'); }), '*');
}, msg); }, msg);
}; };
event('EV_REGISTER_HANDLER', queryType);
};
module.exports.whenReg = function (queryType, handler) {
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');
}
if (insideHandlers.indexOf(queryType) > -1) {
handler();
} else {
(callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(handler);
}
};
handlers['EV_REGISTER_HANDLER'] = function (data) {
if (callWhenRegistered[data.content]) {
callWhenRegistered[data.content].forEach(function (f) { f(); });
delete callWhenRegistered[data.content];
}
insideHandlers.push(data.content);
}; };
return module.exports; return module.exports;

@ -1,76 +0,0 @@
// This file provides the external API for launching and talking to the sandboxed iframe.
// The internal API is in sframe-channel.js
define([
'/common/requireconfig.js'
], function (RequireConfig) {
var iframe;
var handlers = {};
var queries = {};
var module = { exports: {} };
var mkTxid = function () {
return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', '');
};
module.exports.init = function (frame, cb) {
if (iframe) { throw new Error('already initialized'); }
var txid = mkTxid();
var intr = setInterval(function () {
frame.contentWindow.postMessage(JSON.stringify({
txid: txid,
content: { requireConf: RequireConfig },
q: 'INIT'
}), '*');
});
window.addEventListener('message', function (msg) {
var data = JSON.parse(msg.data);
if (!iframe) {
if (data.txid !== txid) { return; }
clearInterval(intr);
iframe = frame;
cb();
} else if (typeof(data.q) === 'string' && handlers[data.q]) {
handlers[data.q](data, msg);
} else if (typeof(data.q) === 'undefined' && queries[data.txid]) {
queries[data.txid](data, msg);
} else {
console.log("Unhandled message");
console.log(msg);
}
});
};
module.exports.query = function (q, content, cb) {
if (!iframe) { throw new Error('not yet initialized'); }
var txid = mkTxid();
var timeout = setTimeout(function () {
delete queries[txid];
cb("Timeout making query " + q);
});
queries[txid] = function (data, msg) {
clearTimeout(timeout);
delete queries[txid];
cb(undefined, data.content, msg);
};
iframe.contentWindow.postMessage(JSON.stringify({
txid: txid,
content: content,
q: q
}), '*');
};
module.exports.registerHandler = function (queryType, handler) {
if (typeof(handlers[queryType]) !== 'undefined') { throw new Error('already registered'); }
handlers[queryType] = function (msg) {
var data = JSON.parse(msg.data);
handler(data.content, function (replyContent) {
msg.source.postMessage(JSON.stringify({
txid: data.txid,
content: replyContent
}), '*');
}, msg);
};
};
return module.exports;
});

@ -1,5 +1,33 @@
// This file defines all of the RPC calls // This file defines all of the RPC calls which are used between the inner and outer iframe.
// The internal API is in sframe-channel.js // 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.
define({ 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_USERDATA_UPDATE': true
}); });

@ -13,6 +13,7 @@ define([
'/common/cryptpad-common.js', '/common/cryptpad-common.js',
'/common/cryptget.js', '/common/cryptget.js',
'/pad/links.js', '/pad/links.js',
'/bower_components/nthen/index.js',
'/bower_components/file-saver/FileSaver.min.js', '/bower_components/file-saver/FileSaver.min.js',
'/bower_components/diff-dom/diffDOM.js', '/bower_components/diff-dom/diffDOM.js',
@ -21,18 +22,12 @@ define([
'less!/customize/src/less/cryptpad.less', 'less!/customize/src/less/cryptpad.less',
'less!/customize/src/less/toolbar.less' 'less!/customize/src/less/toolbar.less'
], function ($, Crypto, realtimeInput, Hyperjson, ], function ($, Crypto, realtimeInput, Hyperjson,
Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links) { Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links, nThen) {
var saveAs = window.saveAs; var saveAs = window.saveAs;
var Messages = Cryptpad.Messages; var Messages = Cryptpad.Messages;
console.log('two');
var Ckeditor; // to be initialized later...
var DiffDom = window.diffDOM; var DiffDom = window.diffDOM;
var stringify = function (obj) { var stringify = function (obj) { return JSONSortify(obj); };
return JSONSortify(obj);
};
window.Toolbar = Toolbar; window.Toolbar = Toolbar;
window.Hyperjson = Hyperjson; window.Hyperjson = Hyperjson;
@ -89,7 +84,7 @@ define([
Cryptpad.errorLoadingScreen(Messages.websocketError); Cryptpad.errorLoadingScreen(Messages.websocketError);
}; };
var andThen = function (Ckeditor) { var andThen = function (editor) {
//var $iframe = $('#pad-iframe').contents(); //var $iframe = $('#pad-iframe').contents();
//var secret = Cryptpad.getSecrets(); //var secret = Cryptpad.getSecrets();
//var readOnly = secret.keys && !secret.keys.editKeyStr; //var readOnly = secret.keys && !secret.keys.editKeyStr;
@ -98,12 +93,7 @@ define([
//} //}
var readOnly = false; // TODO var readOnly = false; // TODO
var editor = window.editor = Ckeditor.replace('editor1', {
customConfig: '/customize/ckeditor-config.js',
});
editor.on('instanceReady', Links.addSupportForOpeningLinksInNewTab(Ckeditor));
editor.on('instanceReady', function () {
var $bar = $('#cke_1_toolbox'); var $bar = $('#cke_1_toolbox');
var $html = $bar.closest('html'); var $html = $bar.closest('html');
@ -125,15 +115,10 @@ define([
el.setAttribute('class', 'non-realtime'); el.setAttribute('class', 'non-realtime');
}); });
var documentBody = document.body; var documentBody = $html.find('iframe')[0].contentWindow.document.body;
var inner = window.inner = documentBody; var inner = window.inner = documentBody;
// hide all content until the realtime doc is ready
$(inner).css({
color: '#fff',
});
var cursor = module.cursor = Cursor(inner); var cursor = module.cursor = Cursor(inner);
var setEditable = module.setEditable = function (bool) { var setEditable = module.setEditable = function (bool) {
@ -322,15 +307,15 @@ define([
var stringifyDOM = module.stringifyDOM = function (dom) { var stringifyDOM = module.stringifyDOM = function (dom) {
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter);
hjson[3] = {
metadata: { /*hjson[3] = { TODO
users: UserList.userData, users: UserList.userData,
defaultTitle: Title.defaultTitle, defaultTitle: Title.defaultTitle,
type: 'pad' type: 'pad'
} }
}; };*/
if (!initializing) { if (!initializing) {
hjson[3].metadata.title = Title.title; //TODO hjson[3].metadata.title = Title.title;
} else if (Cryptpad.initialName && !hjson[3].metadata.title) { } else if (Cryptpad.initialName && !hjson[3].metadata.title) {
hjson[3].metadata.title = Cryptpad.initialName; hjson[3].metadata.title = Cryptpad.initialName;
} }
@ -379,6 +364,9 @@ define([
} }
}; };
var meta;
var metaStr;
realtimeOptions.onRemote = function () { realtimeOptions.onRemote = function () {
if (initializing) { return; } if (initializing) { return; }
if (isHistoryMode) { return; } if (isHistoryMode) { return; }
@ -391,7 +379,7 @@ define([
cursor.update(); cursor.update();
// Update the user list (metadata) from the hyperjson // Update the user list (metadata) from the hyperjson
Metadata.update(shjson); // TODO Metadata.update(shjson);
var newInner = JSON.parse(shjson); var newInner = JSON.parse(shjson);
var newSInner; var newSInner;
@ -404,6 +392,10 @@ define([
if (!readOnly) { if (!readOnly) {
var shjson2 = stringifyDOM(inner); var shjson2 = stringifyDOM(inner);
// TODO
//shjson = JSON.stringify(JSON.parse(shjson).slice(0,3));
if (shjson2 !== shjson) { if (shjson2 !== shjson) {
console.error("shjson2 !== shjson"); console.error("shjson2 !== shjson");
module.patchText(shjson2); module.patchText(shjson2);
@ -438,6 +430,14 @@ define([
if (newSInner && newSInner !== oldSInner) { if (newSInner && newSInner !== oldSInner) {
Cryptpad.notify(); Cryptpad.notify();
} }
var newMeta = newInner[3];
var newMetaStr = JSON.stringify(newMeta);
if (newMetaStr !== metaStr) {
metaStr = newMetaStr;
meta = newMeta;
//meta[] HERE
}
}; };
var getHTML = function () { var getHTML = function () {
@ -465,6 +465,10 @@ define([
}; };
realtimeOptions.onInit = function (info) { realtimeOptions.onInit = function (info) {
// TODO
return;
UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad); UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad);
var titleCfg = { getHeadingText: getHeadingText }; var titleCfg = { getHeadingText: getHeadingText };
@ -531,7 +535,7 @@ define([
updateIcon(); updateIcon();
$collapse.click(function () { $collapse.click(function () {
$(window).trigger('resize'); $(window).trigger('resize');
$iframe.find('.cke_toolbox_main').toggle(); $('.cke_toolbox_main').toggle();
$(window).trigger('cryptpad-ck-toolbar'); $(window).trigger('cryptpad-ck-toolbar');
updateIcon(); updateIcon();
}); });
@ -588,10 +592,10 @@ define([
realtimeOptions.onReady = function (info) { realtimeOptions.onReady = function (info) {
if (!module.isMaximized) { if (!module.isMaximized) {
module.isMaximized = true; module.isMaximized = true;
$iframe.find('iframe.cke_wysiwyg_frame').css('width', ''); $('iframe.cke_wysiwyg_frame').css('width', '');
$iframe.find('iframe.cke_wysiwyg_frame').css('height', ''); $('iframe.cke_wysiwyg_frame').css('height', '');
} }
$iframe.find('body').addClass('app-pad'); $('body').addClass('app-pad');
if (module.realtime !== info.realtime) { if (module.realtime !== info.realtime) {
module.patchText = TextPatcher.create({ module.patchText = TextPatcher.create({
@ -611,15 +615,17 @@ define([
applyHjson(shjson); applyHjson(shjson);
// Update the user list (metadata) from the hyperjson // Update the user list (metadata) from the hyperjson
Metadata.update(shjson); // XXX Metadata.update(shjson);
if (!readOnly) { if (!readOnly) {
var shjson2 = stringifyDOM(inner); var shjson2 = stringifyDOM(inner);
var hjson2 = JSON.parse(shjson2).slice(0,-1); var hjson2 = JSON.parse(shjson2).slice(0,3);
var hjson = JSON.parse(shjson).slice(0,-1); var hjson = JSON.parse(shjson).slice(0,3);
if (stringify(hjson2) !== stringify(hjson)) { if (stringify(hjson2) !== stringify(hjson)) {
console.log('err'); console.log('err');
console.error("shjson2 !== shjson"); console.error("shjson2 !== shjson");
console.log(stringify(hjson2));
console.log(stringify(hjson));
Cryptpad.errorLoadingScreen(Messages.wrongApp); Cryptpad.errorLoadingScreen(Messages.wrongApp);
throw new Error(); throw new Error();
} }
@ -634,7 +640,7 @@ define([
initializing = false; initializing = false;
if (readOnly) { return; } if (readOnly) { return; }
UserList.getLastName(toolbar.$userNameButton, newPad); //TODO UserList.getLastName(toolbar.$userNameButton, newPad);
editor.focus(); editor.focus();
if (newPad) { if (newPad) {
cursor.setToEnd(); cursor.setToEnd();
@ -642,14 +648,14 @@ define([
cursor.setToStart(); cursor.setToStart();
} }
}; };
/* unreachable
realtimeOptions.onAbort = function () { realtimeOptions.onAbort = function () {
console.log("Aborting the session!"); console.log("Aborting the session!");
// stop the user from continuing to edit // stop the user from continuing to edit
setEditable(false); setEditable(false);
toolbar.failed(); toolbar.failed();
Cryptpad.alert(Messages.common_connectionLost, undefined, true); Cryptpad.alert(Messages.common_connectionLost, undefined, true);
}; }; */
realtimeOptions.onConnectionChange = function (info) { realtimeOptions.onConnectionChange = function (info) {
setEditable(info.state); setEditable(info.state);
@ -717,26 +723,35 @@ define([
Cryptpad.feedback(id.toUpperCase()); Cryptpad.feedback(id.toUpperCase());
} }
}); });
});
}; };
var interval = 100;
var second = function (Ckeditor) { var CKEDITOR_CHECK_INTERVAL = 100;
//Cryptpad.ready(function () { var ckEditorAvailable = function (cb) {
andThen(Ckeditor); var intr;
//Cryptpad.reportAppUsage(); var check = function () {
//}); if (window.CKEDITOR) {
Cryptpad.onError(function (info) { clearTimeout(intr);
if (info && info.type === "store") { cb(window.CKEDITOR);
onConnectError();
} }
});
}; };
intr = setInterval(function () {
console.log("Ckeditor was not defined. Trying again in %sms", CKEDITOR_CHECK_INTERVAL);
check();
}, CKEDITOR_CHECK_INTERVAL);
check();
};
var main = function () {
var Ckeditor;
var editor;
var first = function () { nThen(function (waitFor) {
Ckeditor = window.CKEDITOR; ckEditorAvailable(waitFor(function (ck) { Ckeditor = ck; }));
if (Ckeditor) { $(waitFor(function () {
// mobile configuration Cryptpad.addLoadingScreen();
}));
}).nThen(function (waitFor) {
Ckeditor.config.toolbarCanCollapse = true; Ckeditor.config.toolbarCanCollapse = true;
if (screen.height < 800) { if (screen.height < 800) {
Ckeditor.config.toolbarStartupExpanded = false; Ckeditor.config.toolbarStartupExpanded = false;
@ -744,15 +759,19 @@ define([
} else { } else {
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes'); $('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes');
} }
second(Ckeditor); editor = Ckeditor.replace('editor1', {
} else { customConfig: '/customize/ckeditor-config.js',
//console.log("Ckeditor was not defined. Trying again in %sms",interval); });
setTimeout(first, interval); editor.on('instanceReady', waitFor());
}).nThen(function (waitFor) {
Links.addSupportForOpeningLinksInNewTab(Ckeditor);
Cryptpad.onError(function (info) {
if (info && info.type === "store") {
onConnectError();
} }
};
$(function () {
Cryptpad.addLoadingScreen();
first();
}); });
andThen(editor);
});
};
main();
}); });

@ -1,13 +1,39 @@
define([ define([
'/common/sframe-ctrl.js', '/common/sframe-channel.js',
'jquery' 'jquery',
], function (SFrameCtrl, $) { '/common/sframe-chainpad-netflux-outer.js',
'/bower_components/nthen/index.js',
'/common/cryptpad-common.js',
'/bower_components/chainpad-crypto/crypto.js'
], function (SFrameChannel, $, CpNfOuter, nThen, Cryptpad, Crypto) {
console.log('xxx'); console.log('xxx');
$(function () { nThen(function (waitFor) {
console.log('go'); $(waitFor());
SFrameCtrl.init($('#sbox-iframe')[0], function () { }).nThen(function (waitFor) {
console.log('\n\ndone\n\n'); SFrameChannel.init($('#sbox-iframe')[0].contentWindow, waitFor(function () {
console.log('sframe initialized');
}));
Cryptpad.ready(waitFor());
}).nThen(function (waitFor) {
Cryptpad.onError(function (info) {
console.log('error');
console.log(info);
if (info && info.type === "store") {
//onConnectError();
}
});
}).nThen(function (waitFor) {
var secret = Cryptpad.getSecrets();
var readOnly = secret.keys && !secret.keys.editKeyStr;
if (!secret.keys) { secret.keys = secret.key; }
var outer = CpNfOuter.start({
channel: secret.channel,
network: Cryptpad.getNetwork(),
validateKey: secret.keys.validateKey || undefined,
readOnly: readOnly,
crypto: Crypto.createEncryptor(secret.keys),
}); });
}); });
}); });
Loading…
Cancel
Save