Merge branch 'staging' of github.com:xwiki-labs/cryptpad into staging
This commit is contained in:
@@ -1,24 +1,8 @@
|
||||
// This is stage 1, it can be changed but you must bump the version of the project.
|
||||
define([], function () {
|
||||
// fix up locations so that relative urls work.
|
||||
require.config({
|
||||
baseUrl: window.location.pathname,
|
||||
paths: {
|
||||
// jquery declares itself as literally "jquery" so it cannot be pulled by path :(
|
||||
"jquery": "/bower_components/jquery/dist/jquery.min",
|
||||
// json.sortify same
|
||||
"json.sortify": "/bower_components/json.sortify/dist/JSON.sortify",
|
||||
//"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf",
|
||||
//"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker"
|
||||
cm: '/bower_components/codemirror'
|
||||
},
|
||||
map: {
|
||||
'*': {
|
||||
'css': '/bower_components/require-css/css.js',
|
||||
'less': '/common/RequireLess.js',
|
||||
}
|
||||
}
|
||||
});
|
||||
define([
|
||||
'/common/requireconfig.js'
|
||||
], function (RequireConfig) {
|
||||
require.config(RequireConfig());
|
||||
|
||||
// most of CryptPad breaks if you don't support isArray
|
||||
if (!Array.isArray) {
|
||||
|
||||
@@ -38,13 +38,13 @@ define([
|
||||
var parsed = config.href ? common.parsePadUrl(config.href) : {};
|
||||
var secret = common.getSecrets(parsed.type, parsed.hash);
|
||||
|
||||
History.readOnly = 1;
|
||||
History.readOnly = 0;
|
||||
if (!secret.keys) {
|
||||
secret.keys = secret.key;
|
||||
History.readOnly = 2;
|
||||
History.readOnly = 0;
|
||||
}
|
||||
else if (!secret.keys.validateKey) {
|
||||
History.readOnly = 0;
|
||||
History.readOnly = 1;
|
||||
}
|
||||
|
||||
var crypto = Crypto.createEncryptor(secret.keys);
|
||||
@@ -203,7 +203,7 @@ define([
|
||||
'class':'revertHistory buttonSuccess',
|
||||
title: Messages.history_restoreTitle
|
||||
}).text(Messages.history_restore).appendTo($nav);
|
||||
if (!History.readOnly) { $rev.hide(); }
|
||||
if (History.readOnly) { $rev.hide(); }
|
||||
|
||||
onUpdate = function () {
|
||||
$cur.attr('max', states.length);
|
||||
|
||||
@@ -9,38 +9,46 @@ define([
|
||||
AppConfig.badStateTimeout: 30000;
|
||||
|
||||
var connected = false;
|
||||
var intr;
|
||||
var infiniteSpinnerHandlers = [];
|
||||
|
||||
/*
|
||||
TODO make this not blow up when disconnected or lagging...
|
||||
*/
|
||||
common.whenRealtimeSyncs = function (realtime, cb) {
|
||||
realtime.sync();
|
||||
|
||||
common.whenRealtimeSyncs = function (Cryptpad, realtime, cb) {
|
||||
window.setTimeout(function () {
|
||||
if (realtime.getAuthDoc() === realtime.getUserDoc()) {
|
||||
return void cb();
|
||||
} else {
|
||||
realtime.onSettle(cb);
|
||||
}
|
||||
|
||||
var to = setTimeout(function () {
|
||||
if (!connected) { return; }
|
||||
if (intr) { return; }
|
||||
intr = window.setInterval(function () {
|
||||
var l;
|
||||
try {
|
||||
l = realtime.getLag();
|
||||
} catch (e) {
|
||||
throw new Error("ChainPad.getLag() does not exist, please `bower update`");
|
||||
}
|
||||
if (l.lag < BAD_STATE_TIMEOUT || !connected) { return; }
|
||||
realtime.abort();
|
||||
// don't launch more than one popup
|
||||
if (common.infiniteSpinnerDetected) { return; }
|
||||
infiniteSpinnerHandlers.forEach(function (ish) { ish(); });
|
||||
|
||||
// inform the user their session is in a bad state
|
||||
common.confirm(Messages.realtime_unrecoverableError, function (yes) {
|
||||
Cryptpad.confirm(Messages.realtime_unrecoverableError, function (yes) {
|
||||
if (!yes) { return; }
|
||||
window.location.reload();
|
||||
});
|
||||
common.infiniteSpinnerDetected = true;
|
||||
}, BAD_STATE_TIMEOUT);
|
||||
realtime.onSettle(function () {
|
||||
clearTimeout(to);
|
||||
cb();
|
||||
});
|
||||
}, 2000);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
common.onInfiniteSpinner = function (f) { infiniteSpinnerHandlers.push(f); };
|
||||
|
||||
common.setConnectionState = function (bool) {
|
||||
if (typeof(bool) !== 'boolean') { return; }
|
||||
connected = bool;
|
||||
|
||||
112
www/common/common-userlist2.js
Normal file
112
www/common/common-userlist2.js
Normal file
@@ -0,0 +1,112 @@
|
||||
define(function () {
|
||||
var module = {};
|
||||
|
||||
module.create = function (info, onLocal, Cryptget, Cryptpad) {
|
||||
var exp = {};
|
||||
|
||||
var userData = exp.userData = {};
|
||||
var userList = exp.userList = info.userList;
|
||||
var myData = exp.myData = {};
|
||||
exp.myUserName = info.myID;
|
||||
exp.myNetfluxId = info.myID;
|
||||
|
||||
var network = Cryptpad.getNetwork();
|
||||
|
||||
var parsed = Cryptpad.parsePadUrl(window.location.href);
|
||||
var appType = parsed ? parsed.type : undefined;
|
||||
|
||||
var addToUserData = exp.addToUserData = function(data) {
|
||||
var users = userList.users;
|
||||
for (var attrname in data) { userData[attrname] = data[attrname]; }
|
||||
|
||||
if (users && users.length) {
|
||||
for (var userKey in userData) {
|
||||
if (users.indexOf(userKey) === -1) {
|
||||
delete userData[userKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(userList && typeof userList.onChange === "function") {
|
||||
userList.onChange(userData);
|
||||
}
|
||||
};
|
||||
|
||||
exp.getToolbarConfig = function () {
|
||||
return {
|
||||
data: userData,
|
||||
list: userList,
|
||||
userNetfluxId: exp.myNetfluxId
|
||||
};
|
||||
};
|
||||
|
||||
var setName = exp.setName = function (newName, cb) {
|
||||
if (typeof(newName) !== 'string') { return; }
|
||||
var myUserNameTemp = newName.trim();
|
||||
if(myUserNameTemp.length > 32) {
|
||||
myUserNameTemp = myUserNameTemp.substr(0, 32);
|
||||
}
|
||||
exp.myUserName = myUserNameTemp;
|
||||
myData = {};
|
||||
myData[exp.myNetfluxId] = {
|
||||
name: exp.myUserName,
|
||||
uid: Cryptpad.getUid(),
|
||||
avatar: Cryptpad.getAvatarUrl(),
|
||||
profile: Cryptpad.getProfileUrl(),
|
||||
curvePublic: Cryptpad.getProxy().curvePublic
|
||||
};
|
||||
addToUserData(myData);
|
||||
/*Cryptpad.setAttribute('username', exp.myUserName, function (err) {
|
||||
if (err) {
|
||||
console.log("Couldn't set username");
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
if (typeof cb === "function") { cb(); }
|
||||
});*/
|
||||
if (typeof cb === "function") { cb(); }
|
||||
};
|
||||
|
||||
exp.getLastName = function ($changeNameButton, isNew) {
|
||||
Cryptpad.getLastName(function (err, lastName) {
|
||||
if (err) {
|
||||
console.log("Could not get previous name");
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
// Update the toolbar list:
|
||||
// Add the current user in the metadata
|
||||
if (typeof(lastName) === 'string') {
|
||||
setName(lastName, onLocal);
|
||||
} else {
|
||||
myData[exp.myNetfluxId] = {
|
||||
name: "",
|
||||
uid: Cryptpad.getUid(),
|
||||
avatar: Cryptpad.getAvatarUrl(),
|
||||
profile: Cryptpad.getProfileUrl(),
|
||||
curvePublic: Cryptpad.getProxy().curvePublic
|
||||
};
|
||||
addToUserData(myData);
|
||||
onLocal();
|
||||
$changeNameButton.click();
|
||||
}
|
||||
if (isNew && appType) {
|
||||
Cryptpad.selectTemplate(appType, info.realtime, Cryptget);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Cryptpad.onDisplayNameChanged(function (newName) {
|
||||
setName(newName, onLocal);
|
||||
});
|
||||
|
||||
network.on('reconnect', function (uid) {
|
||||
exp.myNetfluxId = uid;
|
||||
exp.setName(exp.myUserName);
|
||||
});
|
||||
|
||||
return exp;
|
||||
};
|
||||
|
||||
return module;
|
||||
});
|
||||
@@ -132,7 +132,9 @@ define([
|
||||
common.initMessagingUI = Messaging.UI.init;
|
||||
|
||||
// Realtime
|
||||
var whenRealtimeSyncs = common.whenRealtimeSyncs = Realtime.whenRealtimeSyncs;
|
||||
var whenRealtimeSyncs = common.whenRealtimeSyncs = function (realtime, cb) {
|
||||
Realtime.whenRealtimeSyncs(common, realtime, cb);
|
||||
};
|
||||
|
||||
// Userlist
|
||||
common.createUserList = UserList.create;
|
||||
@@ -198,10 +200,20 @@ define([
|
||||
}
|
||||
return '';
|
||||
};
|
||||
common.getAccountName = function () {
|
||||
return localStorage[common.userNameKey];
|
||||
};
|
||||
|
||||
var randomToken = function () {
|
||||
return Math.random().toString(16).replace(/0./, '');
|
||||
};
|
||||
|
||||
common.isFeedbackAllowed = function () {
|
||||
try {
|
||||
if (!getStore().getProxy().proxy.allowUserFeedback) { return; }
|
||||
return true;
|
||||
} catch (e) { return void console.error(e); }
|
||||
};
|
||||
var feedback = common.feedback = function (action, force) {
|
||||
if (force !== true) {
|
||||
if (!action) { return; }
|
||||
@@ -827,6 +839,13 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
// SFRAME: talk to anon_rpc from the iframe
|
||||
common.anonRpcMsg = function (msg, data, cb) {
|
||||
if (!msg) { return; }
|
||||
if (!anon_rpc) { return void cb('ANON_RPC_NOT_READY'); }
|
||||
anon_rpc.send(msg, data, cb);
|
||||
};
|
||||
|
||||
common.getFileSize = function (href, cb) {
|
||||
if (!anon_rpc) { return void cb('ANON_RPC_NOT_READY'); }
|
||||
//if (!pinsReady()) { return void cb('RPC_NOT_READY'); }
|
||||
@@ -1030,6 +1049,41 @@ define([
|
||||
};
|
||||
};
|
||||
|
||||
// Forget button
|
||||
var moveToTrash = common.moveToTrash = function (cb) {
|
||||
var href = window.location.href;
|
||||
common.forgetPad(href, function (err) {
|
||||
if (err) {
|
||||
console.log("unable to forget pad");
|
||||
console.error(err);
|
||||
cb(err, null);
|
||||
return;
|
||||
}
|
||||
var n = getNetwork();
|
||||
var r = getRealtime();
|
||||
if (n && r) {
|
||||
whenRealtimeSyncs(r, function () {
|
||||
n.disconnect();
|
||||
cb();
|
||||
});
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
};
|
||||
var saveAsTemplate = common.saveAsTemplate = function (Cryptput, data, cb) {
|
||||
var p = parsePadUrl(window.location.href);
|
||||
if (!p.type) { return; }
|
||||
var hash = createRandomHash();
|
||||
var href = '/' + p.type + '/#' + hash;
|
||||
Cryptput(hash, data.toSave, function (e) {
|
||||
if (e) { throw new Error(e); }
|
||||
common.addTemplate(makePad(href, data.title));
|
||||
whenRealtimeSyncs(getStore().getProxy().info.realtime, function () {
|
||||
cb();
|
||||
});
|
||||
});
|
||||
};
|
||||
common.createButton = function (type, rightside, data, callback) {
|
||||
var button;
|
||||
var size = "17px";
|
||||
@@ -1118,17 +1172,12 @@ define([
|
||||
console.error("Parse error while setting the title", e);
|
||||
}
|
||||
}
|
||||
var p = parsePadUrl(window.location.href);
|
||||
if (!p.type) { return; }
|
||||
var hash = createRandomHash();
|
||||
var href = '/' + p.type + '/#' + hash;
|
||||
data.Crypt.put(hash, toSave, function (e) {
|
||||
if (e) { throw new Error(e); }
|
||||
common.addTemplate(makePad(href, title));
|
||||
whenRealtimeSyncs(getStore().getProxy().info.realtime, function () {
|
||||
common.alert(Messages.templateSaved);
|
||||
common.feedback('TEMPLATE_CREATED');
|
||||
});
|
||||
saveAsTemplate(data.Crypt.put, {
|
||||
title: title,
|
||||
toSave: toSave
|
||||
}, function () {
|
||||
common.alert(Messages.templateSaved);
|
||||
common.feedback('TEMPLATE_CREATED');
|
||||
});
|
||||
};
|
||||
common.prompt(Messages.saveTemplatePrompt, title || document.title, todo);
|
||||
@@ -1151,29 +1200,14 @@ define([
|
||||
button
|
||||
.click(prepareFeedback(type))
|
||||
.click(function() {
|
||||
var href = window.location.href;
|
||||
var msg = isLoggedIn() ? Messages.forgetPrompt : Messages.fm_removePermanentlyDialog;
|
||||
common.confirm(msg, function (yes) {
|
||||
if (!yes) { return; }
|
||||
common.forgetPad(href, function (err) {
|
||||
if (err) {
|
||||
console.log("unable to forget pad");
|
||||
console.error(err);
|
||||
callback(err, null);
|
||||
return;
|
||||
}
|
||||
var n = getNetwork();
|
||||
var r = getRealtime();
|
||||
if (n && r) {
|
||||
whenRealtimeSyncs(r, function () {
|
||||
n.disconnect();
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
moveToTrash(function (err) {
|
||||
if (err) { return void callback(err); }
|
||||
var cMsg = isLoggedIn() ? Messages.movedToTrash : Messages.deleted;
|
||||
common.alert(cMsg, undefined, true);
|
||||
callback();
|
||||
return;
|
||||
});
|
||||
});
|
||||
@@ -1254,7 +1288,7 @@ define([
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
var getFirstEmojiOrCharacter = function (str) {
|
||||
var getFirstEmojiOrCharacter = common.getFirstEmojiOrCharacter = function (str) {
|
||||
if (!str || !str.trim()) { return '?'; }
|
||||
var emojis = emojiStringToArray(str);
|
||||
return isEmoji(emojis[0])? emojis[0]: str[0];
|
||||
@@ -1305,6 +1339,7 @@ define([
|
||||
'image/jpg',
|
||||
'image/gif',
|
||||
];
|
||||
// SFRAME: copied to sframe-common-interface.js
|
||||
common.displayAvatar = function ($container, href, name, cb) {
|
||||
var MutationObserver = window.MutationObserver;
|
||||
var displayDefault = function () {
|
||||
@@ -1643,6 +1678,7 @@ define([
|
||||
return $block;
|
||||
};
|
||||
|
||||
// SFRAME: moved to sframe-common-interface.js
|
||||
common.createUserAdminMenu = function (config) {
|
||||
var $displayedName = $('<span>', {'class': config.displayNameCls || 'displayName'});
|
||||
var accountName = localStorage[common.userNameKey];
|
||||
@@ -1792,6 +1828,27 @@ define([
|
||||
return $userAdmin;
|
||||
};
|
||||
|
||||
common.getShareHashes = function (secret, cb) {
|
||||
if (!window.location.hash) {
|
||||
var hashes = common.getHashes(secret.channel, secret);
|
||||
return void cb(null, hashes);
|
||||
}
|
||||
common.getRecentPads(function (err, recent) {
|
||||
var parsed = parsePadUrl(window.location.href);
|
||||
if (!parsed.type || !parsed.hashData) { return void cb('E_INVALID_HREF'); }
|
||||
var hashes = common.getHashes(secret.channel, secret);
|
||||
|
||||
// If we have a stronger version in drive, add it and add a redirect button
|
||||
var stronger = recent && common.findStronger(null, recent);
|
||||
if (stronger) {
|
||||
var parsed2 = parsePadUrl(stronger);
|
||||
hashes.editHash = parsed2.hash;
|
||||
}
|
||||
|
||||
cb(null, hashes);
|
||||
});
|
||||
};
|
||||
|
||||
var CRYPTPAD_VERSION = 'cryptpad-version';
|
||||
var updateLocalVersion = function () {
|
||||
// Check for CryptPad updates
|
||||
|
||||
31
www/common/loading.js
Normal file
31
www/common/loading.js
Normal file
@@ -0,0 +1,31 @@
|
||||
define([
|
||||
'less!/customize/src/less/loading.less'
|
||||
], function () {
|
||||
var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; });
|
||||
var elem = document.createElement('div');
|
||||
elem.setAttribute('id', 'loading');
|
||||
elem.innerHTML = [
|
||||
'<div class="loadingContainer">',
|
||||
'<img class="cryptofist" src="/customize/cryptpad-new-logo-colors-logoonly.png?' + urlArgs + '">',
|
||||
'<div class="spinnerContainer">',
|
||||
'<span class="fa fa-circle-o-notch fa-spin fa-4x fa-fw"></span>',
|
||||
'</div>',
|
||||
'<p id="cp-loading-message"></p>',
|
||||
'</div>'
|
||||
].join('');
|
||||
var intr;
|
||||
var append = function () {
|
||||
if (!document.body) { return; }
|
||||
clearInterval(intr);
|
||||
document.body.appendChild(elem);
|
||||
require([
|
||||
'/customize/messages.js',
|
||||
|
||||
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||
], function (Messages) {
|
||||
document.getElementById('cp-loading-message').innerText = Messages.loading;
|
||||
});
|
||||
};
|
||||
intr = setInterval(append, 100);
|
||||
append();
|
||||
});
|
||||
139
www/common/metadata-manager.js
Normal file
139
www/common/metadata-manager.js
Normal file
@@ -0,0 +1,139 @@
|
||||
define([], function () {
|
||||
var UNINIT = 'uninitialized';
|
||||
var create = function (sframeChan) {
|
||||
var meta = UNINIT;
|
||||
var members = [];
|
||||
var metadataObj = UNINIT;
|
||||
// This object reflects the metadata which is in the document at this moment.
|
||||
// Normally when a person leaves the pad, everybody sees them leave and updates
|
||||
// their metadata, this causes everyone to fight to change the document and
|
||||
// operational transform doesn't like it. So this is a lazy object which is
|
||||
// only updated either:
|
||||
// 1. On changes to the metadata that come in from someone else
|
||||
// 2. On changes connects, disconnects or changes to your own metadata
|
||||
var metadataLazyObj = UNINIT;
|
||||
var priv = {};
|
||||
var dirty = true;
|
||||
var changeHandlers = [];
|
||||
var lazyChangeHandlers = [];
|
||||
var titleChangeHandlers = [];
|
||||
|
||||
var rememberedTitle;
|
||||
|
||||
var checkUpdate = function (lazy) {
|
||||
if (!dirty) { return; }
|
||||
if (meta === UNINIT) { throw new Error(); }
|
||||
if (metadataObj === UNINIT) {
|
||||
metadataObj = {
|
||||
defaultTitle: meta.doc.defaultTitle,
|
||||
title: meta.doc.defaultTitle,
|
||||
type: meta.doc.type,
|
||||
users: {}
|
||||
};
|
||||
metadataLazyObj = JSON.parse(JSON.stringify(metadataObj));
|
||||
}
|
||||
if (!metadataObj.users) { metadataObj.users = {}; }
|
||||
if (!metadataLazyObj.users) { metadataLazyObj.users = {}; }
|
||||
var mdo = {};
|
||||
// We don't want to add our user data to the object multiple times.
|
||||
//var containsYou = false;
|
||||
//console.log(metadataObj);
|
||||
Object.keys(metadataObj.users).forEach(function (x) {
|
||||
if (members.indexOf(x) === -1) { return; }
|
||||
mdo[x] = metadataObj.users[x];
|
||||
/*if (metadataObj.users[x].uid === meta.user.uid) {
|
||||
//console.log('document already contains you');
|
||||
containsYou = true;
|
||||
}*/
|
||||
});
|
||||
//if (!containsYou) { mdo[meta.user.netfluxId] = meta.user; }
|
||||
mdo[meta.user.netfluxId] = meta.user;
|
||||
metadataObj.users = mdo;
|
||||
var lazyUserStr = JSON.stringify(metadataLazyObj.users[meta.user.netfluxId]);
|
||||
dirty = false;
|
||||
if (lazy || lazyUserStr !== JSON.stringify(meta.user)) {
|
||||
metadataLazyObj = JSON.parse(JSON.stringify(metadataObj));
|
||||
lazyChangeHandlers.forEach(function (f) { f(); });
|
||||
}
|
||||
|
||||
if (metadataObj.title !== rememberedTitle) {
|
||||
console.log("Title update\n" + metadataObj.title + '\n');
|
||||
rememberedTitle = metadataObj.title;
|
||||
titleChangeHandlers.forEach(function (f) { f(metadataObj.title); });
|
||||
}
|
||||
|
||||
changeHandlers.forEach(function (f) { f(); });
|
||||
};
|
||||
var change = function (lazy) {
|
||||
dirty = true;
|
||||
setTimeout(function () {
|
||||
checkUpdate(lazy);
|
||||
});
|
||||
};
|
||||
|
||||
sframeChan.on('EV_METADATA_UPDATE', function (ev) {
|
||||
meta = ev;
|
||||
if (ev.priv) {
|
||||
priv = ev.priv;
|
||||
}
|
||||
change(true);
|
||||
});
|
||||
sframeChan.on('EV_RT_CONNECT', function (ev) {
|
||||
meta.user.netfluxId = ev.myID;
|
||||
members = ev.members;
|
||||
change(true);
|
||||
});
|
||||
sframeChan.on('EV_RT_JOIN', function (ev) {
|
||||
members.push(ev);
|
||||
change(false);
|
||||
});
|
||||
sframeChan.on('EV_RT_LEAVE', function (ev) {
|
||||
var idx = members.indexOf(ev);
|
||||
if (idx === -1) { console.log('Error: ' + ev + ' not in members'); return; }
|
||||
members.splice(idx, 1);
|
||||
change(false);
|
||||
});
|
||||
sframeChan.on('EV_RT_DISCONNECT', function () {
|
||||
members = [];
|
||||
change(true);
|
||||
});
|
||||
|
||||
return Object.freeze({
|
||||
updateMetadata: function (m) {
|
||||
if (JSON.stringify(metadataObj) === JSON.stringify(m)) { return; }
|
||||
metadataObj = JSON.parse(JSON.stringify(m));
|
||||
metadataLazyObj = JSON.parse(JSON.stringify(m));
|
||||
change(false);
|
||||
},
|
||||
updateTitle: function (t) {
|
||||
metadataObj.title = t;
|
||||
change(true);
|
||||
},
|
||||
getMetadata: function () {
|
||||
checkUpdate(false);
|
||||
return Object.freeze(JSON.parse(JSON.stringify(metadataObj)));
|
||||
},
|
||||
getMetadataLazy: function () {
|
||||
return metadataLazyObj;
|
||||
},
|
||||
onTitleChange: function (f) { titleChangeHandlers.push(f); },
|
||||
onChange: function (f) { changeHandlers.push(f); },
|
||||
onChangeLazy: function (f) { lazyChangeHandlers.push(f); },
|
||||
isConnected : function () {
|
||||
return members.indexOf(meta.user.netfluxId) !== -1;
|
||||
},
|
||||
getViewers : function () {
|
||||
checkUpdate(false);
|
||||
var list = members.slice().filter(function (m) { return m.length === 32; });
|
||||
return list.length - Object.keys(metadataObj.users).length;
|
||||
},
|
||||
getPrivateData : function () {
|
||||
return priv;
|
||||
},
|
||||
getNetfluxId : function () {
|
||||
return meta.user.netfluxId;
|
||||
}
|
||||
});
|
||||
};
|
||||
return Object.freeze({ create: create });
|
||||
});
|
||||
@@ -1,11 +1,4 @@
|
||||
(function () {
|
||||
var Mod = function (ApiConfig) {
|
||||
var requireConf;
|
||||
if (ApiConfig && ApiConfig.requireConf) {
|
||||
requireConf = ApiConfig.requireConf;
|
||||
}
|
||||
var urlArgs = typeof(requireConf.urlArgs) === 'string'? '?' + urlArgs: '';
|
||||
|
||||
define(['/api/config'], function (ApiConfig) {
|
||||
var Module = {};
|
||||
|
||||
var isSupported = Module.isSupported = function () {
|
||||
@@ -48,8 +41,8 @@
|
||||
}
|
||||
};
|
||||
|
||||
var DEFAULT_MAIN = '/customize/main-favicon.png' + urlArgs;
|
||||
var DEFAULT_ALT = '/customize/alt-favicon.png' + urlArgs;
|
||||
var DEFAULT_MAIN = '/customize/main-favicon.png?' + ApiConfig.requireConf.urlArgs;
|
||||
var DEFAULT_ALT = '/customize/alt-favicon.png?' + ApiConfig.requireConf.urlArgs;
|
||||
|
||||
var createFavicon = function () {
|
||||
console.log("creating favicon");
|
||||
@@ -117,14 +110,6 @@
|
||||
cancel: cancel,
|
||||
};
|
||||
};
|
||||
return Module;
|
||||
};
|
||||
|
||||
if (typeof(module) !== 'undefined' && module.exports) {
|
||||
module.exports = Mod();
|
||||
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
|
||||
define(['/api/config'], Mod);
|
||||
} else {
|
||||
window.Visible = Mod();
|
||||
}
|
||||
}());
|
||||
return Module;
|
||||
});
|
||||
27
www/common/requireconfig.js
Normal file
27
www/common/requireconfig.js
Normal file
@@ -0,0 +1,27 @@
|
||||
define([
|
||||
'/api/config'
|
||||
], function (ApiConfig) {
|
||||
var out = {
|
||||
// fix up locations so that relative urls work.
|
||||
baseUrl: window.location.pathname,
|
||||
paths: {
|
||||
// jquery declares itself as literally "jquery" so it cannot be pulled by path :(
|
||||
"jquery": "/bower_components/jquery/dist/jquery.min",
|
||||
// json.sortify same
|
||||
"json.sortify": "/bower_components/json.sortify/dist/JSON.sortify",
|
||||
//"pdfjs-dist/build/pdf": "/bower_components/pdfjs-dist/build/pdf",
|
||||
//"pdfjs-dist/build/pdf.worker": "/bower_components/pdfjs-dist/build/pdf.worker"
|
||||
cm: '/bower_components/codemirror'
|
||||
},
|
||||
map: {
|
||||
'*': {
|
||||
'css': '/bower_components/require-css/css.js',
|
||||
'less': '/common/RequireLess.js',
|
||||
}
|
||||
}
|
||||
};
|
||||
Object.keys(ApiConfig.requireConf).forEach(function (k) { out[k] = ApiConfig.requireConf[k]; });
|
||||
return function () {
|
||||
return JSON.parse(JSON.stringify(out));
|
||||
};
|
||||
});
|
||||
30
www/common/sframe-boot.js
Normal file
30
www/common/sframe-boot.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// Stage 0, this gets cached which means we can't change it. boot2-sframe.js is changable.
|
||||
// Note that this file is meant to be executed only inside of a sandbox iframe.
|
||||
;(function () {
|
||||
var req = JSON.parse(decodeURIComponent(window.location.hash.substring(1)));
|
||||
req.cfg = req.cfg || {};
|
||||
if (req.pfx) {
|
||||
req.cfg.onNodeCreated = function (node /*, config, module, path*/) {
|
||||
node.setAttribute('src', req.pfx + node.getAttribute('src'));
|
||||
};
|
||||
}
|
||||
require.config(req.cfg);
|
||||
var txid = Math.random().toString(16).replace('0.', '');
|
||||
var intr;
|
||||
var ready = function () {
|
||||
intr = setInterval(function () {
|
||||
if (typeof(txid) !== 'string') { return; }
|
||||
window.parent.postMessage(JSON.stringify({ q: 'READY', txid: txid }), '*');
|
||||
}, 1);
|
||||
};
|
||||
if (req.req) { require(req.req, ready); } else { ready(); }
|
||||
var onReply = function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (data.txid !== txid) { return; }
|
||||
clearInterval(intr);
|
||||
txid = {};
|
||||
window.removeEventListener('message', onReply);
|
||||
require(['/common/sframe-boot2.js'], function () { });
|
||||
};
|
||||
window.addEventListener('message', onReply);
|
||||
}());
|
||||
35
www/common/sframe-boot2.js
Normal file
35
www/common/sframe-boot2.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// This is stage 1, it can be changed but you must bump the version of the project.
|
||||
// Note: This must only be loaded from inside of a sandbox-iframe.
|
||||
define(['/common/requireconfig.js'], function (RequireConfig) {
|
||||
require.config(RequireConfig());
|
||||
|
||||
// most of CryptPad breaks if you don't support isArray
|
||||
if (!Array.isArray) {
|
||||
Array.isArray = function(arg) { // CRYPTPAD_SHIM
|
||||
return Object.prototype.toString.call(arg) === '[object Array]';
|
||||
};
|
||||
}
|
||||
|
||||
// In the event that someone clicks a link in the iframe, it's going to cause the iframe
|
||||
// to navigate away from the pad which is going to be a mess. Instead we'll just reload
|
||||
// the top level and then it will be simply that a link doesn't work properly.
|
||||
window.onunload = function () {
|
||||
window.parent.location.reload();
|
||||
};
|
||||
|
||||
// Make sure anything which might have leaked to the localstorage is always cleaned up.
|
||||
try { window.localStorage.clear(); } catch (e) { }
|
||||
try { window.sessionStorage.clear(); } catch (e) { }
|
||||
|
||||
var mkFakeStore = function () {
|
||||
var fakeStorage = {
|
||||
getItem: function (k) { return fakeStorage[k]; },
|
||||
setItem: function (k, v) { fakeStorage[k] = v; return v; }
|
||||
};
|
||||
return fakeStorage;
|
||||
};
|
||||
window.__defineGetter__('localStorage', function () { return mkFakeStore(); });
|
||||
window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); });
|
||||
|
||||
require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]);
|
||||
});
|
||||
101
www/common/sframe-chainpad-netflux-inner.js
Normal file
101
www/common/sframe-chainpad-netflux-inner.js
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2014 XWiki SAS
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
define([
|
||||
'/bower_components/chainpad/chainpad.dist.js'
|
||||
], function () {
|
||||
var ChainPad = window.ChainPad;
|
||||
var module = { exports: {} };
|
||||
|
||||
var verbose = function (x) { console.log(x); };
|
||||
verbose = function () {}; // comment out to enable verbose logging
|
||||
|
||||
module.exports.start = function (config) {
|
||||
var onConnectionChange = config.onConnectionChange || function () { };
|
||||
var onRemote = config.onRemote || function () { };
|
||||
var onInit = config.onInit || function () { };
|
||||
var onLocal = config.onLocal || function () { };
|
||||
var setMyID = config.setMyID || function () { };
|
||||
var onReady = config.onReady || function () { };
|
||||
var userName = config.userName;
|
||||
var initialState = config.initialState;
|
||||
var transformFunction = config.transformFunction;
|
||||
var validateContent = config.validateContent;
|
||||
var avgSyncMilliseconds = config.avgSyncMilliseconds;
|
||||
var logLevel = typeof(config.logLevel) !== 'undefined'? config.logLevel : 1;
|
||||
var readOnly = config.readOnly || false;
|
||||
var sframeChan = config.sframeChan;
|
||||
var metadataMgr = config.metadataMgr;
|
||||
config = undefined;
|
||||
|
||||
var chainpad;
|
||||
var myID;
|
||||
var isReady = false;
|
||||
|
||||
sframeChan.on('EV_RT_DISCONNECT', function () {
|
||||
isReady = false;
|
||||
onConnectionChange({ state: false });
|
||||
});
|
||||
sframeChan.on('EV_RT_CONNECT', function (content) {
|
||||
//content.members.forEach(userList.onJoin);
|
||||
myID = content.myID;
|
||||
isReady = false;
|
||||
if (chainpad) {
|
||||
// it's a reconnect
|
||||
onConnectionChange({ state: true, myId: myID });
|
||||
return;
|
||||
}
|
||||
chainpad = ChainPad.create({
|
||||
userName: userName,
|
||||
initialState: initialState,
|
||||
transformFunction: transformFunction,
|
||||
validateContent: validateContent,
|
||||
avgSyncMilliseconds: avgSyncMilliseconds,
|
||||
logLevel: logLevel
|
||||
});
|
||||
chainpad.onMessage(function(message, cb) {
|
||||
sframeChan.query('Q_RT_MESSAGE', message, cb);
|
||||
});
|
||||
chainpad.onPatch(function () {
|
||||
onRemote({ realtime: chainpad });
|
||||
});
|
||||
onInit({
|
||||
myID: myID,
|
||||
realtime: chainpad,
|
||||
readOnly: readOnly
|
||||
});
|
||||
});
|
||||
sframeChan.on('Q_RT_MESSAGE', function (content, cb) {
|
||||
if (isReady) {
|
||||
onLocal(); // should be onBeforeMessage
|
||||
}
|
||||
chainpad.message(content);
|
||||
cb('OK');
|
||||
});
|
||||
sframeChan.on('EV_RT_READY', function () {
|
||||
if (isReady) { return; }
|
||||
isReady = true;
|
||||
chainpad.start();
|
||||
setMyID({ myID: myID });
|
||||
onReady({ realtime: chainpad });
|
||||
});
|
||||
return Object.freeze({
|
||||
getMyID: function () { return myID; },
|
||||
metadataMgr: metadataMgr
|
||||
});
|
||||
};
|
||||
return Object.freeze(module.exports);
|
||||
});
|
||||
246
www/common/sframe-chainpad-netflux-outer.js
Normal file
246
www/common/sframe-chainpad-netflux-outer.js
Normal file
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright 2014 XWiki SAS
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
define([], function () {
|
||||
var USE_HISTORY = true;
|
||||
|
||||
var verbose = function (x) { console.log(x); };
|
||||
verbose = function () {}; // comment out to enable verbose logging
|
||||
|
||||
var unBencode = function (str) { return str.replace(/^\d+:/, ''); };
|
||||
|
||||
var start = function (conf) {
|
||||
var channel = conf.channel;
|
||||
var Crypto = conf.crypto;
|
||||
var validateKey = conf.validateKey;
|
||||
var readOnly = conf.readOnly || false;
|
||||
var network = conf.network;
|
||||
var sframeChan = conf.sframeChan;
|
||||
var onConnect = conf.onConnect || function () { };
|
||||
conf = undefined;
|
||||
|
||||
var initializing = true;
|
||||
var lastKnownHash;
|
||||
|
||||
var queue = [];
|
||||
var messageFromInner = function (m, cb) { queue.push([ m, cb ]); };
|
||||
sframeChan.on('Q_RT_MESSAGE', function (message, cb) {
|
||||
messageFromInner(message, cb);
|
||||
});
|
||||
|
||||
var onReady = function () {
|
||||
// Trigger onReady only if not ready yet. This is important because the history keeper sends a direct
|
||||
// message through "network" when it is synced, and it triggers onReady for each channel joined.
|
||||
if (!initializing) { return; }
|
||||
sframeChan.event('EV_RT_READY', null);
|
||||
// we're fully synced
|
||||
initializing = false;
|
||||
};
|
||||
|
||||
// shim between chainpad and netflux
|
||||
var msgIn = function (peerId, msg) {
|
||||
msg = msg.replace(/^cp\|/, '');
|
||||
try {
|
||||
var decryptedMsg = Crypto.decrypt(msg, validateKey);
|
||||
return decryptedMsg;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
|
||||
var msgOut = function (msg) {
|
||||
if (readOnly) { return; }
|
||||
try {
|
||||
var cmsg = Crypto.encrypt(msg);
|
||||
if (msg.indexOf('[4') === 0) { cmsg = 'cp|' + cmsg; }
|
||||
return cmsg;
|
||||
} catch (err) {
|
||||
console.log(msg);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
var onMessage = function(peer, msg, wc, network, direct) {
|
||||
// unpack the history keeper from the webchannel
|
||||
var hk = network.historyKeeper;
|
||||
|
||||
if (direct && peer !== hk) {
|
||||
return;
|
||||
}
|
||||
if (direct) {
|
||||
var parsed = JSON.parse(msg);
|
||||
if (parsed.validateKey && parsed.channel) {
|
||||
if (parsed.channel === wc.id && !validateKey) {
|
||||
validateKey = parsed.validateKey;
|
||||
}
|
||||
// We have to return even if it is not the current channel:
|
||||
// we don't want to continue with other channels messages here
|
||||
return;
|
||||
}
|
||||
if (parsed.state && parsed.state === 1 && parsed.channel) {
|
||||
if (parsed.channel === wc.id) {
|
||||
onReady(wc);
|
||||
}
|
||||
// We have to return even if it is not the current channel:
|
||||
// we don't want to continue with other channels messages here
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The history keeper is different for each channel :
|
||||
// no need to check if the message is related to the current channel
|
||||
if (peer === hk) {
|
||||
// if the peer is the 'history keeper', extract their message
|
||||
var parsed1 = JSON.parse(msg);
|
||||
msg = parsed1[4];
|
||||
// Check that this is a message for us
|
||||
if (parsed1[3] !== wc.id) { return; }
|
||||
}
|
||||
|
||||
lastKnownHash = msg.slice(0,64);
|
||||
var message = msgIn(peer, msg);
|
||||
|
||||
verbose(message);
|
||||
|
||||
// slice off the bencoded header
|
||||
// Why are we getting bencoded stuff to begin with?
|
||||
// FIXME this shouldn't be necessary
|
||||
message = unBencode(message);//.slice(message.indexOf(':[') + 1);
|
||||
|
||||
// pass the message into Chainpad
|
||||
sframeChan.query('Q_RT_MESSAGE', message, function () { });
|
||||
};
|
||||
|
||||
// We use an object to store the webchannel so that we don't have to push new handlers to chainpad
|
||||
// and remove the old ones when reconnecting and keeping the same 'realtime' object
|
||||
// See realtime.onMessage below: we call wc.bcast(...) but wc may change
|
||||
var wcObject = {};
|
||||
var onOpen = function(wc, network, firstConnection) {
|
||||
wcObject.wc = wc;
|
||||
channel = wc.id;
|
||||
|
||||
onConnect(wc);
|
||||
onConnect = function () { };
|
||||
|
||||
// Add the existing peers in the userList
|
||||
sframeChan.event('EV_RT_CONNECT', { myID: wc.myID, members: wc.members, readOnly: readOnly });
|
||||
|
||||
// Add the handlers to the WebChannel
|
||||
wc.on('message', function (msg, sender) { //Channel msg
|
||||
onMessage(sender, msg, wc, network);
|
||||
});
|
||||
wc.on('join', function (m) { sframeChan.event('EV_RT_JOIN', m); });
|
||||
wc.on('leave', function (m) { sframeChan.event('EV_RT_LEAVE', m); });
|
||||
|
||||
if (firstConnection) {
|
||||
// Sending a message...
|
||||
messageFromInner = function(message, cb) {
|
||||
// Filter messages sent by Chainpad to make it compatible with Netflux
|
||||
message = msgOut(message);
|
||||
if (message) {
|
||||
// Do not remove wcObject, it allows us to use a new 'wc' without changing the handler if we
|
||||
// want to keep the same chainpad (realtime) object
|
||||
try {
|
||||
wcObject.wc.bcast(message).then(function() {
|
||||
cb();
|
||||
}, function(err) {
|
||||
// The message has not been sent, display the error.
|
||||
console.error(err);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
// Just skip calling back and it will fail on the inside.
|
||||
}
|
||||
}
|
||||
};
|
||||
queue.forEach(function (arr) { messageFromInner(arr[0], arr[1]); });
|
||||
}
|
||||
|
||||
// Get the channel history
|
||||
if (USE_HISTORY) {
|
||||
var hk;
|
||||
|
||||
wc.members.forEach(function (p) {
|
||||
if (p.length === 16) { hk = p; }
|
||||
});
|
||||
network.historyKeeper = hk;
|
||||
|
||||
var msg = ['GET_HISTORY', wc.id];
|
||||
// Add the validateKey if we are the channel creator and we have a validateKey
|
||||
msg.push(validateKey);
|
||||
msg.push(lastKnownHash);
|
||||
if (hk) { network.sendto(hk, JSON.stringify(msg)); }
|
||||
} else {
|
||||
onReady(wc);
|
||||
}
|
||||
};
|
||||
|
||||
var isIntentionallyLeaving = false;
|
||||
window.addEventListener("beforeunload", function () {
|
||||
isIntentionallyLeaving = true;
|
||||
});
|
||||
|
||||
var findChannelById = function (webChannels, channelId) {
|
||||
var webChannel;
|
||||
|
||||
// Array.some terminates once a truthy value is returned
|
||||
// best case is faster than forEach, though webchannel arrays seem
|
||||
// to consistently have a length of 1
|
||||
webChannels.some(function(chan) {
|
||||
if(chan.id === channelId) { webChannel = chan; return true;}
|
||||
});
|
||||
return webChannel;
|
||||
};
|
||||
|
||||
var connectTo = function (network, firstConnection) {
|
||||
// join the netflux network, promise to handle opening of the channel
|
||||
network.join(channel || null).then(function(wc) {
|
||||
onOpen(wc, network, firstConnection);
|
||||
}, function(error) {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
network.on('disconnect', function (reason) {
|
||||
console.log('disconnect');
|
||||
if (isIntentionallyLeaving) { return; }
|
||||
if (reason === "network.disconnect() called") { return; }
|
||||
sframeChan.event('EV_RT_DISCONNECT');
|
||||
});
|
||||
|
||||
network.on('reconnect', function () {
|
||||
initializing = true;
|
||||
connectTo(network, false);
|
||||
});
|
||||
|
||||
network.on('message', function (msg, sender) { // Direct message
|
||||
var wchan = findChannelById(network.webChannels, channel);
|
||||
if (wchan) {
|
||||
onMessage(sender, msg, wchan, network, true);
|
||||
}
|
||||
});
|
||||
|
||||
connectTo(network, true);
|
||||
};
|
||||
|
||||
return {
|
||||
start: function (config) {
|
||||
config.sframeChan.whenReg('EV_RT_READY', function () {
|
||||
start(config);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
141
www/common/sframe-channel.js
Normal file
141
www/common/sframe-channel.js
Normal file
@@ -0,0 +1,141 @@
|
||||
// This file provides the API for the channel for talking to and from the sandbox iframe.
|
||||
define([
|
||||
'/common/sframe-protocol.js'
|
||||
], function (SFrameProtocol) {
|
||||
|
||||
var mkTxid = function () {
|
||||
return Math.random().toString(16).replace('0.', '') + Math.random().toString(16).replace('0.', '');
|
||||
};
|
||||
|
||||
var create = function (ow, cb) {
|
||||
var otherWindow;
|
||||
var handlers = {};
|
||||
var queries = {};
|
||||
|
||||
// list of handlers which are registered from the other side...
|
||||
var insideHandlers = [];
|
||||
var callWhenRegistered = {};
|
||||
|
||||
var chan = {};
|
||||
|
||||
// Send a query. channel.query('Q_SOMETHING', { args: "whatever" }, function (reply) { ... });
|
||||
chan.query = function (q, content, cb) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[q]) {
|
||||
throw new Error('please only make queries are defined in sframe-protocol.js');
|
||||
}
|
||||
var txid = mkTxid();
|
||||
var timeout = setTimeout(function () {
|
||||
delete queries[txid];
|
||||
console.log("Timeout making query " + q);
|
||||
}, 30000);
|
||||
queries[txid] = function (data, msg) {
|
||||
clearTimeout(timeout);
|
||||
delete queries[txid];
|
||||
cb(undefined, data.content, msg);
|
||||
};
|
||||
otherWindow.postMessage(JSON.stringify({
|
||||
txid: txid,
|
||||
content: content,
|
||||
q: q
|
||||
}), '*');
|
||||
};
|
||||
|
||||
// Fire an event. channel.event('EV_SOMETHING', { args: "whatever" });
|
||||
var event = chan.event = function (e, content) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[e]) {
|
||||
throw new Error('please only fire events that are defined in sframe-protocol.js');
|
||||
}
|
||||
if (e.indexOf('EV_') !== 0) {
|
||||
throw new Error('please only use events (starting with EV_) for event messages');
|
||||
}
|
||||
otherWindow.postMessage(JSON.stringify({ content: content, q: e }), '*');
|
||||
};
|
||||
|
||||
// Be notified on query or event. channel.on('EV_SOMETHING', function (args, reply) { ... });
|
||||
// If the type is a query, your handler will be invoked with a reply function that takes
|
||||
// one argument (the content to reply with).
|
||||
chan.on = function (queryType, handler, quiet) {
|
||||
if (!otherWindow && !quiet) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[queryType]) {
|
||||
throw new Error('please only register handlers which are defined in sframe-protocol.js');
|
||||
}
|
||||
(handlers[queryType] = handlers[queryType] || []).push(function (data, msg) {
|
||||
handler(data.content, function (replyContent) {
|
||||
if (queryType.indexOf('Q_') !== 0) { throw new Error("replies to events are invalid"); }
|
||||
msg.source.postMessage(JSON.stringify({
|
||||
txid: data.txid,
|
||||
content: replyContent
|
||||
}), '*');
|
||||
}, msg);
|
||||
});
|
||||
if (!quiet) {
|
||||
event('EV_REGISTER_HANDLER', queryType);
|
||||
}
|
||||
};
|
||||
|
||||
// If a particular handler is registered, call the callback immediately, otherwise it will be called
|
||||
// when that handler is first registered.
|
||||
// channel.whenReg('Q_SOMETHING', function () { ...query Q_SOMETHING?... });
|
||||
chan.whenReg = function (queryType, cb, always) {
|
||||
if (!otherWindow) { throw new Error('not yet initialized'); }
|
||||
if (!SFrameProtocol[queryType]) {
|
||||
throw new Error('please only register handlers which are defined in sframe-protocol.js');
|
||||
}
|
||||
var reg = always;
|
||||
if (insideHandlers.indexOf(queryType) > -1) {
|
||||
cb();
|
||||
} else {
|
||||
reg = true;
|
||||
}
|
||||
if (reg) {
|
||||
(callWhenRegistered[queryType] = callWhenRegistered[queryType] || []).push(cb);
|
||||
}
|
||||
};
|
||||
|
||||
// Same as whenReg except it will invoke every time there is another registration, not just once.
|
||||
chan.onReg = function (queryType, cb) { chan.whenReg(queryType, cb, true); };
|
||||
|
||||
chan.on('EV_REGISTER_HANDLER', function (content) {
|
||||
if (callWhenRegistered[content]) {
|
||||
callWhenRegistered[content].forEach(function (f) { f(); });
|
||||
delete callWhenRegistered[content];
|
||||
}
|
||||
insideHandlers.push(content);
|
||||
}, true);
|
||||
|
||||
var txid;
|
||||
window.addEventListener('message', function (msg) {
|
||||
var data = JSON.parse(msg.data);
|
||||
if (ow !== msg.source) {
|
||||
console.log("DROP Message from unexpected source");
|
||||
console.log(msg);
|
||||
} else if (!otherWindow) {
|
||||
otherWindow = ow;
|
||||
ow.postMessage(JSON.stringify({ txid: data.txid }), '*');
|
||||
cb(chan);
|
||||
} else if (typeof(data.q) === 'string' && handlers[data.q]) {
|
||||
handlers[data.q].forEach(function (f) {
|
||||
f(data || JSON.parse(msg.data), msg);
|
||||
data = undefined;
|
||||
});
|
||||
} else if (typeof(data.q) === 'undefined' && queries[data.txid]) {
|
||||
queries[data.txid](data, msg);
|
||||
} else if (data.txid === txid) {
|
||||
// stray message from init
|
||||
return;
|
||||
} else {
|
||||
console.log("DROP Unhandled message");
|
||||
console.log(msg);
|
||||
}
|
||||
});
|
||||
if (window !== window.top) {
|
||||
// we're in the sandbox
|
||||
otherWindow = ow;
|
||||
cb(chan);
|
||||
}
|
||||
};
|
||||
|
||||
return { create: create };
|
||||
});
|
||||
223
www/common/sframe-common-history.js
Normal file
223
www/common/sframe-common-history.js
Normal file
@@ -0,0 +1,223 @@
|
||||
define([
|
||||
'jquery',
|
||||
'/bower_components/chainpad-json-validator/json-ot.js',
|
||||
'/bower_components/chainpad/chainpad.dist.js',
|
||||
], function ($, JsonOT) {
|
||||
var ChainPad = window.ChainPad;
|
||||
var History = {};
|
||||
|
||||
var getStates = function (rt) {
|
||||
var states = [];
|
||||
var b = rt.getAuthBlock();
|
||||
if (b) { states.unshift(b); }
|
||||
while (b.getParent()) {
|
||||
b = b.getParent();
|
||||
states.unshift(b);
|
||||
}
|
||||
return states;
|
||||
};
|
||||
|
||||
var loadHistory = function (config, common, cb) {
|
||||
var createRealtime = function () {
|
||||
return ChainPad.create({
|
||||
userName: 'history',
|
||||
initialState: '',
|
||||
transformFunction: JsonOT.validate,
|
||||
logLevel: 0,
|
||||
noPrune: true
|
||||
});
|
||||
};
|
||||
var realtime = createRealtime();
|
||||
|
||||
History.readOnly = common.getMetadataMgr().getPrivateData().readOnly;
|
||||
|
||||
var to = window.setTimeout(function () {
|
||||
cb('[GET_FULL_HISTORY_TIMEOUT]');
|
||||
}, 30000);
|
||||
|
||||
common.getFullHistory(realtime, function () {
|
||||
window.clearTimeout(to);
|
||||
cb(null, realtime);
|
||||
});
|
||||
};
|
||||
|
||||
History.create = function (common, config) {
|
||||
if (!config.$toolbar) { return void console.error("config.$toolbar is undefined");}
|
||||
if (History.loading) { return void console.error("History is already being loaded..."); }
|
||||
History.loading = true;
|
||||
var $toolbar = config.$toolbar;
|
||||
|
||||
if (!config.applyVal || !config.setHistory || !config.onLocal || !config.onRemote) {
|
||||
throw new Error("Missing config element: applyVal, onLocal, onRemote, setHistory");
|
||||
}
|
||||
|
||||
// config.setHistory(bool, bool)
|
||||
// - bool1: history value
|
||||
// - bool2: reset old content?
|
||||
var render = function (val) {
|
||||
if (typeof val === "undefined") { return; }
|
||||
try {
|
||||
config.applyVal(val);
|
||||
} catch (e) {
|
||||
// Probably a parse error
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
var onClose = function () { config.setHistory(false, true); };
|
||||
var onRevert = function () {
|
||||
config.setHistory(false, false);
|
||||
config.onLocal();
|
||||
config.onRemote();
|
||||
};
|
||||
var onReady = function () {
|
||||
config.setHistory(true);
|
||||
};
|
||||
|
||||
var Messages = common.Messages;
|
||||
var Cryptpad = common.getCryptpadCommon();
|
||||
|
||||
var realtime;
|
||||
|
||||
var states = [];
|
||||
var c = states.length - 1;
|
||||
|
||||
var $hist = $toolbar.find('.cryptpad-toolbar-history');
|
||||
var $left = $toolbar.find('.cryptpad-toolbar-leftside');
|
||||
var $right = $toolbar.find('.cryptpad-toolbar-rightside');
|
||||
var $cke = $toolbar.find('.cke_toolbox_main');
|
||||
|
||||
$hist.html('').show();
|
||||
$left.hide();
|
||||
$right.hide();
|
||||
$cke.hide();
|
||||
|
||||
Cryptpad.spinner($hist).get().show();
|
||||
|
||||
var onUpdate;
|
||||
|
||||
var update = function () {
|
||||
if (!realtime) { return []; }
|
||||
states = getStates(realtime);
|
||||
if (typeof onUpdate === "function") { onUpdate(); }
|
||||
return states;
|
||||
};
|
||||
|
||||
// Get the content of the selected version, and change the version number
|
||||
var get = function (i) {
|
||||
i = parseInt(i);
|
||||
if (isNaN(i)) { return; }
|
||||
if (i < 0) { i = 0; }
|
||||
if (i > states.length - 1) { i = states.length - 1; }
|
||||
var val = states[i].getContent().doc;
|
||||
c = i;
|
||||
if (typeof onUpdate === "function") { onUpdate(); }
|
||||
$hist.find('.next, .previous').css('visibility', '');
|
||||
if (c === states.length - 1) { $hist.find('.next').css('visibility', 'hidden'); }
|
||||
if (c === 0) { $hist.find('.previous').css('visibility', 'hidden'); }
|
||||
return val || '';
|
||||
};
|
||||
|
||||
var getNext = function (step) {
|
||||
return typeof step === "number" ? get(c + step) : get(c + 1);
|
||||
};
|
||||
var getPrevious = function (step) {
|
||||
return typeof step === "number" ? get(c - step) : get(c - 1);
|
||||
};
|
||||
|
||||
// Create the history toolbar
|
||||
var display = function () {
|
||||
$hist.html('');
|
||||
var $prev =$('<button>', {
|
||||
'class': 'previous fa fa-step-backward buttonPrimary',
|
||||
title: Messages.history_prev
|
||||
}).appendTo($hist);
|
||||
var $nav = $('<div>', {'class': 'goto'}).appendTo($hist);
|
||||
var $next = $('<button>', {
|
||||
'class': 'next fa fa-step-forward buttonPrimary',
|
||||
title: Messages.history_next
|
||||
}).appendTo($hist);
|
||||
|
||||
$('<label>').text(Messages.history_version).appendTo($nav);
|
||||
var $cur = $('<input>', {
|
||||
'class' : 'gotoInput',
|
||||
'type' : 'number',
|
||||
'min' : '1',
|
||||
'max' : states.length
|
||||
}).val(c + 1).appendTo($nav).mousedown(function (e) {
|
||||
// stopPropagation because the event would be cancelled by the dropdown menus
|
||||
e.stopPropagation();
|
||||
});
|
||||
var $label2 = $('<label>').text(' / '+ states.length).appendTo($nav);
|
||||
$('<br>').appendTo($nav);
|
||||
var $close = $('<button>', {
|
||||
'class':'closeHistory',
|
||||
title: Messages.history_closeTitle
|
||||
}).text(Messages.history_closeTitle).appendTo($nav);
|
||||
var $rev = $('<button>', {
|
||||
'class':'revertHistory buttonSuccess',
|
||||
title: Messages.history_restoreTitle
|
||||
}).text(Messages.history_restore).appendTo($nav);
|
||||
if (History.readOnly) { $rev.hide(); }
|
||||
|
||||
onUpdate = function () {
|
||||
$cur.attr('max', states.length);
|
||||
$cur.val(c+1);
|
||||
$label2.text(' / ' + states.length);
|
||||
};
|
||||
|
||||
var close = function () {
|
||||
$hist.hide();
|
||||
$left.show();
|
||||
$right.show();
|
||||
$cke.show();
|
||||
};
|
||||
|
||||
// Buttons actions
|
||||
$prev.click(function () { render(getPrevious()); });
|
||||
$next.click(function () { render(getNext()); });
|
||||
$cur.keydown(function (e) {
|
||||
var p = function () { e.preventDefault(); };
|
||||
if (e.which === 13) { p(); return render( get($cur.val() - 1) ); } // Enter
|
||||
if ([37, 40].indexOf(e.which) >= 0) { p(); return render(getPrevious()); } // Left
|
||||
if ([38, 39].indexOf(e.which) >= 0) { p(); return render(getNext()); } // Right
|
||||
if (e.which === 33) { p(); return render(getNext(10)); } // PageUp
|
||||
if (e.which === 34) { p(); return render(getPrevious(10)); } // PageUp
|
||||
if (e.which === 27) { p(); $close.click(); }
|
||||
}).keyup(function (e) { e.stopPropagation(); }).focus();
|
||||
$cur.on('change', function () {
|
||||
render( get($cur.val() - 1) );
|
||||
});
|
||||
$close.click(function () {
|
||||
states = [];
|
||||
close();
|
||||
onClose();
|
||||
});
|
||||
$rev.click(function () {
|
||||
Cryptpad.confirm(Messages.history_restorePrompt, function (yes) {
|
||||
if (!yes) { return; }
|
||||
close();
|
||||
onRevert();
|
||||
Cryptpad.log(Messages.history_restoreDone);
|
||||
});
|
||||
});
|
||||
|
||||
// Display the latest content
|
||||
render(get(c));
|
||||
};
|
||||
|
||||
// Load all the history messages into a new chainpad object
|
||||
loadHistory(config, common, function (err, newRt) {
|
||||
History.loading = false;
|
||||
if (err) { throw new Error(err); }
|
||||
realtime = newRt;
|
||||
update();
|
||||
c = states.length - 1;
|
||||
display();
|
||||
onReady();
|
||||
});
|
||||
};
|
||||
|
||||
return History;
|
||||
});
|
||||
|
||||
|
||||
264
www/common/sframe-common-interface.js
Normal file
264
www/common/sframe-common-interface.js
Normal file
@@ -0,0 +1,264 @@
|
||||
define([
|
||||
'jquery',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/media-tag.js',
|
||||
], function ($, Cryptpad, MediaTag) {
|
||||
var UI = {};
|
||||
var Messages = Cryptpad.Messages;
|
||||
|
||||
/**
|
||||
* Requirements from cryptpad-common.js
|
||||
* getFileSize
|
||||
* - hrefToHexChannelId
|
||||
* displayAvatar
|
||||
* - getFirstEmojiOrCharacter
|
||||
* - parsePadUrl
|
||||
* - getSecrets
|
||||
* - base64ToHex
|
||||
* - getBlobPathFromHex
|
||||
* - bytesToMegabytes
|
||||
* createUserAdminMenu
|
||||
* - fixHTML
|
||||
* - createDropdown
|
||||
*/
|
||||
|
||||
UI.getFileSize = function (Common, href, cb) {
|
||||
var channelId = Cryptpad.hrefToHexChannelId(href);
|
||||
Common.sendAnonRpcMsg("GET_FILE_SIZE", channelId, function (data) {
|
||||
if (!data) { return void cb("No response"); }
|
||||
if (data.error) { return void cb(data.error); }
|
||||
if (data.response && data.response.length && typeof(data.response[0]) === 'number') {
|
||||
return void cb(void 0, data.response[0]);
|
||||
} else {
|
||||
cb('INVALID_RESPONSE');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
UI.displayAvatar = function (Common, $container, href, name, cb) {
|
||||
var MutationObserver = window.MutationObserver;
|
||||
var displayDefault = function () {
|
||||
var text = Cryptpad.getFirstEmojiOrCharacter(name);
|
||||
var $avatar = $('<span>', {'class': 'default'}).text(text);
|
||||
$container.append($avatar);
|
||||
if (cb) { cb(); }
|
||||
};
|
||||
|
||||
if (!href) { return void displayDefault(); }
|
||||
var parsed = Cryptpad.parsePadUrl(href);
|
||||
var secret = Cryptpad.getSecrets('file', parsed.hash);
|
||||
if (secret.keys && secret.channel) {
|
||||
var cryptKey = secret.keys && secret.keys.fileKeyStr;
|
||||
var hexFileName = Cryptpad.base64ToHex(secret.channel);
|
||||
var src = Cryptpad.getBlobPathFromHex(hexFileName);
|
||||
UI.getFileSize(Common, href, function (e, data) {
|
||||
if (e) {
|
||||
displayDefault();
|
||||
return void console.error(e);
|
||||
}
|
||||
if (typeof data !== "number") { return void displayDefault(); }
|
||||
if (Cryptpad.bytesToMegabytes(data) > 0.5) { return void displayDefault(); }
|
||||
var $img = $('<media-tag>').appendTo($container);
|
||||
$img.attr('src', src);
|
||||
$img.attr('data-crypto-key', 'cryptpad:' + cryptKey);
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
||||
if (mutation.addedNodes.length > 1 ||
|
||||
mutation.addedNodes[0].nodeName !== 'IMG') {
|
||||
$img.remove();
|
||||
return void displayDefault();
|
||||
}
|
||||
var $image = $img.find('img');
|
||||
var onLoad = function () {
|
||||
var img = new Image();
|
||||
img.onload = function () {
|
||||
var w = img.width;
|
||||
var h = img.height;
|
||||
if (w>h) {
|
||||
$image.css('max-height', '100%');
|
||||
$img.css('flex-direction', 'column');
|
||||
if (cb) { cb($img); }
|
||||
return;
|
||||
}
|
||||
$image.css('max-width', '100%');
|
||||
$img.css('flex-direction', 'row');
|
||||
if (cb) { cb($img); }
|
||||
};
|
||||
img.src = $image.attr('src');
|
||||
};
|
||||
if ($image[0].complete) { onLoad(); }
|
||||
$image.on('load', onLoad);
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe($img[0], {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
characterData: false
|
||||
});
|
||||
MediaTag($img[0]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
UI.createUserAdminMenu = function (config) {
|
||||
var Common = config.Common;
|
||||
var metadataMgr = config.metadataMgr;
|
||||
|
||||
var displayNameCls = config.displayNameCls || 'displayName';
|
||||
var $displayedName = $('<span>', {'class': displayNameCls});
|
||||
|
||||
var accountName = metadataMgr.getPrivateData().accountName;
|
||||
var origin = metadataMgr.getPrivateData().origin;
|
||||
var padType = metadataMgr.getMetadata().type;
|
||||
|
||||
var $userName = $('<span>', {'class': 'userDisplayName'});
|
||||
var options = [];
|
||||
if (config.displayNameCls) {
|
||||
var $userAdminContent = $('<p>');
|
||||
if (accountName) {
|
||||
var $userAccount = $('<span>', {'class': 'userAccount'}).append(Messages.user_accountName + ': ' + Cryptpad.fixHTML(accountName));
|
||||
$userAdminContent.append($userAccount);
|
||||
$userAdminContent.append($('<br>'));
|
||||
}
|
||||
if (config.displayName) {
|
||||
// Hide "Display name:" in read only mode
|
||||
$userName.append(Messages.user_displayName + ': ');
|
||||
$userName.append($displayedName);
|
||||
}
|
||||
$userAdminContent.append($userName);
|
||||
options.push({
|
||||
tag: 'p',
|
||||
attributes: {'class': 'accountData'},
|
||||
content: $userAdminContent.html()
|
||||
});
|
||||
}
|
||||
if (padType !== 'drive') {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {
|
||||
'target': '_blank',
|
||||
'href': origin+'/drive/'
|
||||
},
|
||||
content: Messages.login_accessDrive
|
||||
});
|
||||
}
|
||||
// Add the change display name button if not in read only mode
|
||||
if (config.changeNameButtonCls && config.displayChangeName) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': config.changeNameButtonCls},
|
||||
content: Messages.user_rename
|
||||
});
|
||||
}
|
||||
if (accountName) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'profile'},
|
||||
content: Messages.profileButton
|
||||
});
|
||||
}
|
||||
if (padType !== 'settings') {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'settings'},
|
||||
content: Messages.settingsButton
|
||||
});
|
||||
}
|
||||
// Add login or logout button depending on the current status
|
||||
if (accountName) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'logout'},
|
||||
content: Messages.logoutButton
|
||||
});
|
||||
} else {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'login'},
|
||||
content: Messages.login_login
|
||||
});
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'register'},
|
||||
content: Messages.login_register
|
||||
});
|
||||
}
|
||||
var $icon = $('<span>', {'class': 'fa fa-user-secret'});
|
||||
//var $userbig = $('<span>', {'class': 'big'}).append($displayedName.clone());
|
||||
var $userButton = $('<div>').append($icon);//.append($userbig);
|
||||
if (accountName) {
|
||||
$userButton = $('<div>').append(accountName);
|
||||
}
|
||||
/*if (account && config.displayNameCls) {
|
||||
$userbig.append($('<span>', {'class': 'account-name'}).text('(' + accountName + ')'));
|
||||
} else if (account) {
|
||||
// If no display name, do not display the parentheses
|
||||
$userbig.append($('<span>', {'class': 'account-name'}).text(accountName));
|
||||
}*/
|
||||
var dropdownConfigUser = {
|
||||
text: $userButton.html(), // Button initial text
|
||||
options: options, // Entries displayed in the menu
|
||||
left: true, // Open to the left of the button
|
||||
container: config.$initBlock, // optional
|
||||
feedback: "USER_ADMIN",
|
||||
};
|
||||
var $userAdmin = Cryptpad.createDropdown(dropdownConfigUser);
|
||||
|
||||
var $displayName = $userAdmin.find('.'+displayNameCls);
|
||||
|
||||
var $avatar = $userAdmin.find('.buttonTitle');
|
||||
var updateButton = function () {
|
||||
var myData = metadataMgr.getMetadata().users[metadataMgr.getNetfluxId()];
|
||||
if (!myData) { return; }
|
||||
var newName = myData.name;
|
||||
var url = myData.avatar;
|
||||
$displayName.text(newName || Messages.anonymous);
|
||||
if (accountName) {
|
||||
$avatar.html('');
|
||||
UI.displayAvatar(Common, $avatar, url, newName, function ($img) {
|
||||
if ($img) {
|
||||
$userAdmin.find('button').addClass('avatar');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
metadataMgr.onChange(updateButton);
|
||||
updateButton();
|
||||
|
||||
$userAdmin.find('a.logout').click(function () {
|
||||
Common.logout(function () {
|
||||
window.top.location = origin+'/';
|
||||
});
|
||||
});
|
||||
$userAdmin.find('a.settings').click(function () {
|
||||
if (padType) {
|
||||
window.open(origin+'/settings/');
|
||||
} else {
|
||||
window.top.location = origin+'/settings/';
|
||||
}
|
||||
});
|
||||
$userAdmin.find('a.profile').click(function () {
|
||||
if (padType) {
|
||||
window.open(origin+'/profile/');
|
||||
} else {
|
||||
window.top.location = origin+'/profile/';
|
||||
}
|
||||
});
|
||||
$userAdmin.find('a.login').click(function () {
|
||||
Common.setLoginRedirect(function () {
|
||||
window.top.location = origin+'/login/';
|
||||
});
|
||||
});
|
||||
$userAdmin.find('a.register').click(function () {
|
||||
Common.setLoginRedirect(function () {
|
||||
window.top.location = origin+'/register/';
|
||||
});
|
||||
});
|
||||
|
||||
return $userAdmin;
|
||||
};
|
||||
|
||||
return UI;
|
||||
});
|
||||
83
www/common/sframe-common-title.js
Normal file
83
www/common/sframe-common-title.js
Normal file
@@ -0,0 +1,83 @@
|
||||
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.updateTitle(md.title || md.defaultTitle);
|
||||
});
|
||||
|
||||
exp.getTitleConfig = function () {
|
||||
return {
|
||||
updateTitle: exp.updateTitle,
|
||||
suggestName: suggestTitle,
|
||||
defaultName: exp.defaultTitle
|
||||
};
|
||||
};
|
||||
|
||||
return exp;
|
||||
};
|
||||
|
||||
return module;
|
||||
});
|
||||
|
||||
303
www/common/sframe-common.js
Normal file
303
www/common/sframe-common.js
Normal file
@@ -0,0 +1,303 @@
|
||||
define([
|
||||
'jquery',
|
||||
'/bower_components/nthen/index.js',
|
||||
'/customize/messages.js',
|
||||
'/common/sframe-chainpad-netflux-inner.js',
|
||||
'/common/sframe-channel.js',
|
||||
'/common/sframe-common-title.js',
|
||||
'/common/sframe-common-interface.js',
|
||||
'/common/sframe-common-history.js',
|
||||
'/common/metadata-manager.js',
|
||||
|
||||
'/customize/application_config.js',
|
||||
'/common/cryptpad-common.js',
|
||||
'/common/common-realtime.js'
|
||||
], function ($, nThen, Messages, CpNfInner, SFrameChannel, Title, UI, History, MetadataMgr,
|
||||
AppConfig, Cryptpad, CommonRealtime) {
|
||||
|
||||
// Chainpad Netflux Inner
|
||||
var funcs = {};
|
||||
var ctx = {};
|
||||
|
||||
funcs.Messages = Messages;
|
||||
|
||||
funcs.startRealtime = function (options) {
|
||||
if (ctx.cpNfInner) { return ctx.cpNfInner; }
|
||||
options.sframeChan = ctx.sframeChan;
|
||||
options.metadataMgr = ctx.metadataMgr;
|
||||
ctx.cpNfInner = CpNfInner.start(options);
|
||||
ctx.cpNfInner.metadataMgr.onChangeLazy(options.onLocal);
|
||||
return ctx.cpNfInner;
|
||||
};
|
||||
|
||||
funcs.getMetadataMgr = function () {
|
||||
return ctx.metadataMgr;
|
||||
};
|
||||
funcs.getCryptpadCommon = function () {
|
||||
return Cryptpad;
|
||||
};
|
||||
|
||||
var isLoggedIn = funcs.isLoggedIn = function () {
|
||||
if (!ctx.cpNfInner) { throw new Error("cpNfInner is not ready!"); }
|
||||
return ctx.cpNfInner.metadataMgr.getPrivateData().accountName;
|
||||
};
|
||||
|
||||
var titleUpdated;
|
||||
funcs.updateTitle = function (title, cb) {
|
||||
ctx.metadataMgr.updateTitle(title);
|
||||
titleUpdated = cb;
|
||||
};
|
||||
|
||||
// UI
|
||||
funcs.createUserAdminMenu = UI.createUserAdminMenu;
|
||||
funcs.displayAvatar = UI.displayAvatar;
|
||||
|
||||
// History
|
||||
funcs.getHistory = function (config) { return History.create(funcs, config); };
|
||||
|
||||
// Title module
|
||||
funcs.createTitle = Title.create;
|
||||
|
||||
funcs.getDefaultTitle = function () {
|
||||
if (!ctx.cpNfInner) { throw new Error("cpNfInner is not ready!"); }
|
||||
return ctx.cpNfInner.metadataMgr.getMetadata().defaultTitle;
|
||||
};
|
||||
|
||||
funcs.setDisplayName = function (name, cb) {
|
||||
ctx.sframeChan.query('Q_SETTINGS_SET_DISPLAY_NAME', name, function (err) {
|
||||
if (cb) { cb(err); }
|
||||
});
|
||||
};
|
||||
|
||||
funcs.logout = function (cb) {
|
||||
ctx.sframeChan.query('Q_LOGOUT', null, function (err) {
|
||||
if (cb) { cb(err); }
|
||||
});
|
||||
};
|
||||
|
||||
funcs.setLoginRedirect = function (cb) {
|
||||
ctx.sframeChan.query('Q_SET_LOGIN_REDIRECT', null, function (err) {
|
||||
if (cb) { cb(err); }
|
||||
});
|
||||
};
|
||||
|
||||
funcs.sendAnonRpcMsg = function (msg, content, cb) {
|
||||
ctx.sframeChan.query('Q_ANON_RPC_MESSAGE', {
|
||||
msg: msg,
|
||||
content: content
|
||||
}, function (err, data) {
|
||||
if (cb) { cb(data); }
|
||||
});
|
||||
};
|
||||
|
||||
funcs.isOverPinLimit = function (cb) {
|
||||
ctx.sframeChan.query('Q_GET_PIN_LIMIT_STATUS', null, function (err, data) {
|
||||
cb(data.error, data.overLimit, data.limits);
|
||||
});
|
||||
};
|
||||
|
||||
funcs.getFullHistory = function (realtime, cb) {
|
||||
ctx.sframeChan.on('EV_RT_HIST_MESSAGE', function (content) {
|
||||
realtime.message(content);
|
||||
});
|
||||
ctx.sframeChan.query('Q_GET_FULL_HISTORY', null, cb);
|
||||
};
|
||||
|
||||
funcs.feedback = function (action, force) {
|
||||
if (force !== true) {
|
||||
if (!action) { return; }
|
||||
try {
|
||||
if (!ctx.metadataMgr.getPrivateData().feedbackAllowed) { return; }
|
||||
} catch (e) { return void console.error(e); }
|
||||
}
|
||||
var randomToken = Math.random().toString(16).replace(/0./, '');
|
||||
//var origin = ctx.metadataMgr.getPrivateData().origin;
|
||||
var href = /*origin +*/ '/common/feedback.html?' + action + '=' + randomToken;
|
||||
$.ajax({
|
||||
type: "HEAD",
|
||||
url: href,
|
||||
});
|
||||
};
|
||||
var prepareFeedback = function (key) {
|
||||
if (typeof(key) !== 'string') { return $.noop; }
|
||||
|
||||
var type = ctx.metadataMgr.getMetadata().type;
|
||||
return function () {
|
||||
funcs.feedback((key + (type? '_' + type: '')).toUpperCase());
|
||||
};
|
||||
};
|
||||
|
||||
// BUTTONS
|
||||
var isStrongestStored = function () {
|
||||
var data = ctx.metadataMgr.getPrivateData();
|
||||
return !data.readOnly || !data.availableHashes.editHash;
|
||||
};
|
||||
funcs.createButton = function (type, rightside, data, callback) {
|
||||
var button;
|
||||
var size = "17px";
|
||||
switch (type) {
|
||||
case 'export':
|
||||
button = $('<button>', {
|
||||
'class': 'fa fa-download',
|
||||
title: Messages.exportButtonTitle,
|
||||
}).append($('<span>', {'class': 'drawer'}).text(Messages.exportButton));
|
||||
|
||||
button.click(prepareFeedback(type));
|
||||
if (callback) {
|
||||
button.click(callback);
|
||||
}
|
||||
break;
|
||||
case 'import':
|
||||
button = $('<button>', {
|
||||
'class': 'fa fa-upload',
|
||||
title: Messages.importButtonTitle,
|
||||
}).append($('<span>', {'class': 'drawer'}).text(Messages.importButton));
|
||||
if (callback) {
|
||||
button
|
||||
.click(prepareFeedback(type))
|
||||
.click(Cryptpad.importContent('text/plain', function (content, file) {
|
||||
callback(content, file);
|
||||
}, {accept: data ? data.accept : undefined}));
|
||||
}
|
||||
break;
|
||||
case 'template':
|
||||
if (!AppConfig.enableTemplates) { return; }
|
||||
button = $('<button>', {
|
||||
title: Messages.saveTemplateButton,
|
||||
}).append($('<span>', {'class':'fa fa-bookmark', style: 'font:'+size+' FontAwesome'}));
|
||||
if (data.rt) {
|
||||
button
|
||||
.click(function () {
|
||||
var title = data.getTitle() || document.title;
|
||||
var todo = function (val) {
|
||||
if (typeof(val) !== "string") { return; }
|
||||
var toSave = data.rt.getUserDoc();
|
||||
if (val.trim()) {
|
||||
val = val.trim();
|
||||
title = val;
|
||||
try {
|
||||
var parsed = JSON.parse(toSave);
|
||||
var meta;
|
||||
if (Array.isArray(parsed) && typeof(parsed[3]) === "object") {
|
||||
meta = parsed[3].metadata; // pad
|
||||
} else if (parsed.info) {
|
||||
meta = parsed.info; // poll
|
||||
} else {
|
||||
meta = parsed.metadata;
|
||||
}
|
||||
if (typeof(meta) === "object") {
|
||||
meta.title = val;
|
||||
meta.defaultTitle = val;
|
||||
delete meta.users;
|
||||
}
|
||||
toSave = JSON.stringify(parsed);
|
||||
} catch(e) {
|
||||
console.error("Parse error while setting the title", e);
|
||||
}
|
||||
}
|
||||
ctx.sframeChan.query('Q_SAVE_AS_TEMPLATE', {
|
||||
title: title,
|
||||
toSave: toSave
|
||||
}, function () {
|
||||
Cryptpad.alert(Messages.templateSaved);
|
||||
funcs.feedback('TEMPLATE_CREATED');
|
||||
});
|
||||
};
|
||||
Cryptpad.prompt(Messages.saveTemplatePrompt, title, todo);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'forget':
|
||||
button = $('<button>', {
|
||||
id: 'cryptpad-forget',
|
||||
title: Messages.forgetButtonTitle,
|
||||
'class': "fa fa-trash cryptpad-forget",
|
||||
style: 'font:'+size+' FontAwesome'
|
||||
});
|
||||
if (!isStrongestStored()) {
|
||||
button.addClass('hidden');
|
||||
}
|
||||
if (callback) {
|
||||
button
|
||||
.click(prepareFeedback(type))
|
||||
.click(function() {
|
||||
var msg = isLoggedIn() ? Messages.forgetPrompt : Messages.fm_removePermanentlyDialog;
|
||||
Cryptpad.confirm(msg, function (yes) {
|
||||
if (!yes) { return; }
|
||||
ctx.sframeChan.query('Q_MOVE_TO_TRASH', null, function (err) {
|
||||
if (err) { return void callback(err); }
|
||||
var cMsg = isLoggedIn() ? Messages.movedToTrash : Messages.deleted;
|
||||
Cryptpad.alert(cMsg, undefined, true);
|
||||
callback();
|
||||
return;
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'history':
|
||||
if (!AppConfig.enableHistory) {
|
||||
button = $('<span>');
|
||||
break;
|
||||
}
|
||||
button = $('<button>', {
|
||||
title: Messages.historyButton,
|
||||
'class': "fa fa-history history",
|
||||
}).append($('<span>', {'class': 'drawer'}).text(Messages.historyText));
|
||||
if (data.histConfig) {
|
||||
button
|
||||
.click(prepareFeedback(type))
|
||||
.on('click', function () {
|
||||
funcs.getHistory(data.histConfig);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'more':
|
||||
button = $('<button>', {
|
||||
title: Messages.moreActions || 'TODO',
|
||||
'class': "drawer-button fa fa-ellipsis-h",
|
||||
style: 'font:'+size+' FontAwesome'
|
||||
});
|
||||
break;
|
||||
default:
|
||||
button = $('<button>', {
|
||||
'class': "fa fa-question",
|
||||
style: 'font:'+size+' FontAwesome'
|
||||
})
|
||||
.click(prepareFeedback(type));
|
||||
}
|
||||
if (rightside) {
|
||||
button.addClass('rightside-button');
|
||||
}
|
||||
return button;
|
||||
|
||||
};
|
||||
/* funcs.storeLinkToClipboard = function (readOnly, cb) {
|
||||
ctx.sframeChan.query('Q_STORE_LINK_TO_CLIPBOARD', readOnly, function (err) {
|
||||
if (cb) { cb(err); }
|
||||
});
|
||||
};
|
||||
*/
|
||||
|
||||
Object.freeze(funcs);
|
||||
return { create: function (cb) {
|
||||
nThen(function (waitFor) {
|
||||
SFrameChannel.create(window.top, waitFor(function (sfc) { ctx.sframeChan = sfc; }));
|
||||
// CpNfInner.start() should be here....
|
||||
}).nThen(function () {
|
||||
ctx.metadataMgr = MetadataMgr.create(ctx.sframeChan);
|
||||
ctx.metadataMgr.onTitleChange(function (title) {
|
||||
ctx.sframeChan.query('Q_SET_PAD_TITLE_IN_DRIVE', title, function (err) {
|
||||
if (err) { return; }
|
||||
if (titleUpdated) { titleUpdated(undefined, title); }
|
||||
});
|
||||
});
|
||||
|
||||
ctx.sframeChan.on('EV_RT_CONNECT', function () { CommonRealtime.setConnectionState(true); });
|
||||
ctx.sframeChan.on('EV_RT_DISCONNECT', function () { CommonRealtime.setConnectionState(false); });
|
||||
|
||||
cb(funcs);
|
||||
});
|
||||
} };
|
||||
});
|
||||
77
www/common/sframe-protocol.js
Normal file
77
www/common/sframe-protocol.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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,
|
||||
|
||||
});
|
||||
999
www/common/toolbar3.js
Normal file
999
www/common/toolbar3.js
Normal file
@@ -0,0 +1,999 @@
|
||||
define([
|
||||
'jquery',
|
||||
'/customize/application_config.js',
|
||||
'/api/config',
|
||||
], function ($, Config, ApiConfig) {
|
||||
var Messages = {};
|
||||
var Cryptpad;
|
||||
var Common;
|
||||
|
||||
var Bar = {
|
||||
constants: {},
|
||||
};
|
||||
|
||||
var SPINNER_DISAPPEAR_TIME = 1000;
|
||||
|
||||
// Toolbar parts
|
||||
var TOOLBAR_CLS = Bar.constants.toolbar = 'cryptpad-toolbar';
|
||||
var TOP_CLS = Bar.constants.top = 'cryptpad-toolbar-top';
|
||||
var LEFTSIDE_CLS = Bar.constants.leftside = 'cryptpad-toolbar-leftside';
|
||||
var RIGHTSIDE_CLS = Bar.constants.rightside = 'cryptpad-toolbar-rightside';
|
||||
var DRAWER_CLS = Bar.constants.drawer = 'drawer-content';
|
||||
var HISTORY_CLS = Bar.constants.history = 'cryptpad-toolbar-history';
|
||||
|
||||
// Userlist
|
||||
var USERLIST_CLS = Bar.constants.userlist = "cryptpad-dropdown-users";
|
||||
var EDITSHARE_CLS = Bar.constants.editShare = "cryptpad-dropdown-editShare";
|
||||
var VIEWSHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-viewShare";
|
||||
var SHARE_CLS = Bar.constants.viewShare = "cryptpad-dropdown-share";
|
||||
|
||||
// Top parts
|
||||
var USER_CLS = Bar.constants.userAdmin = "cryptpad-user";
|
||||
var SPINNER_CLS = Bar.constants.spinner = 'cryptpad-spinner';
|
||||
var LIMIT_CLS = Bar.constants.lag = 'cryptpad-limit';
|
||||
var TITLE_CLS = Bar.constants.title = "cryptpad-title";
|
||||
var NEWPAD_CLS = Bar.constants.newpad = "cryptpad-new";
|
||||
|
||||
// User admin menu
|
||||
var USERADMIN_CLS = Bar.constants.user = 'cryptpad-user-dropdown';
|
||||
var USERNAME_CLS = Bar.constants.username = 'cryptpad-toolbar-username';
|
||||
/*var READONLY_CLS = */Bar.constants.readonly = 'cryptpad-readonly';
|
||||
var USERBUTTON_CLS = Bar.constants.changeUsername = "cryptpad-change-username";
|
||||
|
||||
// Create the toolbar element
|
||||
|
||||
var uid = function () {
|
||||
return 'cryptpad-uid-' + String(Math.random()).substring(2);
|
||||
};
|
||||
|
||||
var createRealtimeToolbar = function (config) {
|
||||
if (!config.$container) { return; }
|
||||
var $container = config.$container;
|
||||
var $toolbar = $('<div>', {
|
||||
'class': TOOLBAR_CLS,
|
||||
id: uid(),
|
||||
});
|
||||
|
||||
// TODO iframe
|
||||
// var parsed = Cryptpad.parsePadUrl(window.location.href);
|
||||
var parsed = { type:'pad' };
|
||||
if (typeof parsed.type === "string") {
|
||||
config.$container.parents('body').addClass('app-' + parsed.type);
|
||||
}
|
||||
|
||||
var $topContainer = $('<div>', {'class': TOP_CLS});
|
||||
$('<span>', {'class': 'filler'}).appendTo($topContainer);
|
||||
var $userContainer = $('<span>', {
|
||||
'class': USER_CLS
|
||||
}).appendTo($topContainer);
|
||||
$('<span>', {'class': LIMIT_CLS}).hide().appendTo($userContainer);
|
||||
$('<span>', {'class': NEWPAD_CLS + ' dropdown-bar'}).hide().appendTo($userContainer);
|
||||
$('<span>', {'class': USERADMIN_CLS + ' dropdown-bar'}).hide().appendTo($userContainer);
|
||||
|
||||
$toolbar.append($topContainer)
|
||||
.append($('<div>', {'class': LEFTSIDE_CLS}))
|
||||
.append($('<div>', {'class': RIGHTSIDE_CLS}))
|
||||
.append($('<div>', {'class': HISTORY_CLS}));
|
||||
|
||||
var $rightside = $toolbar.find('.'+RIGHTSIDE_CLS);
|
||||
if (!config.hideDrawer) {
|
||||
var $drawerContent = $('<div>', {
|
||||
'class': DRAWER_CLS,// + ' dropdown-bar-content cryptpad-dropdown'
|
||||
'tabindex': 1
|
||||
}).appendTo($rightside).hide();
|
||||
var $drawer = Cryptpad.createButton('more', true).appendTo($rightside);
|
||||
$drawer.click(function () {
|
||||
$drawerContent.toggle();
|
||||
$drawer.removeClass('active');
|
||||
if ($drawerContent.is(':visible')) {
|
||||
$drawer.addClass('active');
|
||||
$drawerContent.focus();
|
||||
}
|
||||
});
|
||||
var onBlur = function (e) {
|
||||
if (e.relatedTarget) {
|
||||
if ($(e.relatedTarget).is('.drawer-button')) { return; }
|
||||
if ($(e.relatedTarget).parents('.'+DRAWER_CLS).length) {
|
||||
$(e.relatedTarget).blur(onBlur);
|
||||
return;
|
||||
}
|
||||
}
|
||||
$drawer.removeClass('active');
|
||||
$drawerContent.hide();
|
||||
};
|
||||
$drawerContent.blur(onBlur);
|
||||
}
|
||||
|
||||
// The 'notitle' class removes the line added for the title with a small screen
|
||||
if (!config.title || typeof config.title !== "object") {
|
||||
$toolbar.addClass('notitle');
|
||||
}
|
||||
|
||||
$container.prepend($toolbar);
|
||||
|
||||
$container.on('drop dragover', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
return $toolbar;
|
||||
};
|
||||
|
||||
// Userlist elements
|
||||
|
||||
var getOtherUsers = function(config) {
|
||||
//var userList = config.userList.getUserlist();
|
||||
var userData = config.metadataMgr.getMetadata().users;
|
||||
|
||||
var i = 0; // duplicates counter
|
||||
var list = [];
|
||||
|
||||
// Display only one time each user (if he is connected in multiple tabs)
|
||||
var uids = [];
|
||||
Object.keys(userData).forEach(function(user) {
|
||||
//if (user !== userNetfluxId) {
|
||||
var data = userData[user] || {};
|
||||
var userId = data.uid;
|
||||
if (!userId) { return; }
|
||||
//data.netfluxId = user;
|
||||
if (uids.indexOf(userId) === -1) {// && (!myUid || userId !== myUid)) {
|
||||
uids.push(userId);
|
||||
list.push(data);
|
||||
} else { i++; }
|
||||
//}
|
||||
});
|
||||
return {
|
||||
list: list,
|
||||
duplicates: i
|
||||
};
|
||||
};
|
||||
|
||||
var avatars = {};
|
||||
var updateUserList = function (toolbar, config) {
|
||||
// Make sure the elements are displayed
|
||||
var $userButtons = toolbar.userlist;
|
||||
var $userlistContent = toolbar.userlistContent;
|
||||
|
||||
var metadataMgr = config.metadataMgr;
|
||||
var userData = metadataMgr.getMetadata().users;
|
||||
var viewers = metadataMgr.getViewers();
|
||||
var origin = config.metadataMgr.getPrivateData().origin;
|
||||
|
||||
// If we are using old pads (readonly unavailable), only editing users are in userList.
|
||||
// With new pads, we also have readonly users in userList, so we have to intersect with
|
||||
// the userData to have only the editing users. We can't use userData directly since it
|
||||
// may contain data about users that have already left the channel.
|
||||
//userList = config.readOnly === -1 ? userList : arrayIntersect(userList, Object.keys(userData));
|
||||
|
||||
// Names of editing users
|
||||
var others = getOtherUsers(config);
|
||||
var editUsersNames = others.list;
|
||||
var duplicates = others.duplicates; // Number of duplicates
|
||||
|
||||
editUsersNames.sort(function (a, b) {
|
||||
var na = a.name || Messages.anonymous;
|
||||
var nb = b.name || Messages.anonymous;
|
||||
return na.toLowerCase() > nb.toLowerCase();
|
||||
});
|
||||
|
||||
var numberOfEditUsers = Object.keys(userData).length - duplicates;
|
||||
var numberOfViewUsers = viewers;
|
||||
|
||||
// Update the userlist
|
||||
var $editUsers = $userlistContent.find('.' + USERLIST_CLS).html('');
|
||||
Cryptpad.clearTooltips();
|
||||
|
||||
var $editUsersList = $('<div>', {'class': 'userlist-others'});
|
||||
|
||||
// Editors
|
||||
// TODO iframe enable friends
|
||||
//var pendingFriends = Cryptpad.getPendingInvites();
|
||||
editUsersNames.forEach(function (data) {
|
||||
var name = data.name || Messages.anonymous;
|
||||
var $span = $('<span>', {'class': 'avatar'});
|
||||
var $rightCol = $('<span>', {'class': 'right-col'});
|
||||
$('<span>', {'class': 'name'}).text(name).appendTo($rightCol);
|
||||
//var proxy = Cryptpad.getProxy();
|
||||
//var isMe = data.curvePublic === proxy.curvePublic;
|
||||
|
||||
/*if (Cryptpad.isLoggedIn() && data.curvePublic) {
|
||||
if (isMe) {
|
||||
$span.attr('title', Messages._getKey('userlist_thisIsYou', [
|
||||
name
|
||||
]));
|
||||
$nameSpan.text(name);
|
||||
} else if (!proxy.friends || !proxy.friends[data.curvePublic]) {
|
||||
if (pendingFriends.indexOf(data.netfluxId) !== -1) {
|
||||
$('<span>', {'class': 'friend'}).text(Messages.userlist_pending)
|
||||
.appendTo($rightCol);
|
||||
} else {
|
||||
$('<span>', {
|
||||
'class': 'fa fa-user-plus friend',
|
||||
'title': Messages._getKey('userlist_addAsFriendTitle', [
|
||||
name
|
||||
])
|
||||
}).appendTo($rightCol).click(function (e) {
|
||||
e.stopPropagation();
|
||||
Cryptpad.inviteFromUserlist(Cryptpad, data.netfluxId);
|
||||
});
|
||||
}
|
||||
}
|
||||
}*/
|
||||
if (data.profile) {
|
||||
$span.addClass('clickable');
|
||||
$span.click(function () {
|
||||
window.open(origin+'/profile/#' + data.profile);
|
||||
});
|
||||
}
|
||||
if (data.avatar && avatars[data.avatar]) {
|
||||
$span.append(avatars[data.avatar]);
|
||||
$span.append($rightCol);
|
||||
} else {
|
||||
Common.displayAvatar(Common, $span, data.avatar, name, function ($img) {
|
||||
if (data.avatar && $img) {
|
||||
avatars[data.avatar] = $img[0].outerHTML;
|
||||
}
|
||||
$span.append($rightCol);
|
||||
});
|
||||
}
|
||||
$span.data('uid', data.uid);
|
||||
$editUsersList.append($span);
|
||||
});
|
||||
$editUsers.append($editUsersList);
|
||||
|
||||
// Viewers
|
||||
if (numberOfViewUsers > 0) {
|
||||
var viewText = '<div class="viewer">';
|
||||
var viewerText = numberOfViewUsers !== 1 ? Messages.viewers : Messages.viewer;
|
||||
viewText += numberOfViewUsers + ' ' + viewerText + '</div>';
|
||||
$editUsers.append(viewText);
|
||||
}
|
||||
|
||||
// Update the buttons
|
||||
var fa_editusers = '<span class="fa fa-users"></span>';
|
||||
var fa_viewusers = '<span class="fa fa-eye"></span>';
|
||||
var $spansmall = $('<span>').html(fa_editusers + ' ' + numberOfEditUsers + ' ' + fa_viewusers + ' ' + numberOfViewUsers);
|
||||
$userButtons.find('.buttonTitle').html('').append($spansmall);
|
||||
};
|
||||
|
||||
var initUserList = function (toolbar, config) {
|
||||
// TODO clean comments
|
||||
if (config.metadataMgr) { /* && config.userList.list && config.userList.userNetfluxId) {*/
|
||||
//var userList = config.userList.list;
|
||||
//userList.change.push
|
||||
var metadataMgr = config.metadataMgr;
|
||||
metadataMgr.onChange(function () {
|
||||
if (metadataMgr.isConnected()) {toolbar.connected = true;}
|
||||
if (!toolbar.connected) { return; }
|
||||
//if (config.userList.data) {
|
||||
updateUserList(toolbar, config);
|
||||
//}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create sub-elements
|
||||
|
||||
var createUserList = function (toolbar, config) {
|
||||
if (!config.metadataMgr) {
|
||||
throw new Error("You must provide a `metadataMgr` to display the userlist");
|
||||
}
|
||||
var $content = $('<div>', {'class': 'userlist-drawer'});
|
||||
$content.on('drop dragover', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
var $closeIcon = $('<span>', {"class": "fa fa-window-close close"}).appendTo($content);
|
||||
$('<h2>').text(Messages.users).appendTo($content);
|
||||
$('<p>', {'class': USERLIST_CLS}).appendTo($content);
|
||||
|
||||
toolbar.userlistContent = $content;
|
||||
|
||||
var $container = $('<span>', {id: 'userButtons', title: Messages.userListButton});
|
||||
|
||||
var $button = $('<button>').appendTo($container);
|
||||
$('<span>',{'class': 'buttonTitle'}).appendTo($button);
|
||||
|
||||
toolbar.$leftside.prepend($container);
|
||||
|
||||
if (config.$contentContainer) {
|
||||
config.$contentContainer.prepend($content);
|
||||
}
|
||||
|
||||
var $ck = config.$container.find('.cke_toolbox_main');
|
||||
var mobile = $('body').width() <= 600;
|
||||
var hide = function () {
|
||||
$content.hide();
|
||||
$button.removeClass('active');
|
||||
$ck.css({
|
||||
'padding-left': '',
|
||||
});
|
||||
};
|
||||
var show = function () {
|
||||
$content.show();
|
||||
if (mobile) {
|
||||
$ck.hide();
|
||||
}
|
||||
$button.addClass('active');
|
||||
$ck.css({
|
||||
'padding-left': '175px',
|
||||
});
|
||||
var h = $ck.is(':visible') ? -$ck.height() : 0;
|
||||
$content.css('margin-top', h+'px');
|
||||
};
|
||||
$(window).on('cryptpad-ck-toolbar', function () {
|
||||
if (mobile && $ck.is(':visible')) { return void hide(); }
|
||||
if ($content.is(':visible')) { return void show(); }
|
||||
hide();
|
||||
});
|
||||
$(window).on('resize', function () {
|
||||
mobile = $('body').width() <= 600;
|
||||
var h = $ck.is(':visible') ? -$ck.height() : 0;
|
||||
$content.css('margin-top', h+'px');
|
||||
});
|
||||
$closeIcon.click(function () {
|
||||
//Cryptpad.setAttribute('userlist-drawer', false); TODO iframe
|
||||
hide();
|
||||
});
|
||||
$button.click(function () {
|
||||
var visible = $content.is(':visible');
|
||||
if (visible) { hide(); }
|
||||
else { show(); }
|
||||
visible = !visible;
|
||||
// TODO iframe
|
||||
//Cryptpad.setAttribute('userlist-drawer', visible);
|
||||
Common.feedback(visible?'USERLIST_SHOW': 'USERLIST_HIDE');
|
||||
});
|
||||
show();
|
||||
// TODO iframe
|
||||
/*Cryptpad.getAttribute('userlist-drawer', function (err, val) {
|
||||
if (val === false || mobile) { return void hide(); }
|
||||
show();
|
||||
});*/
|
||||
|
||||
return $container;
|
||||
};
|
||||
|
||||
var createShare = function (toolbar, config) {
|
||||
if (!config.metadataMgr) {
|
||||
throw new Error("You must provide a `metadataMgr` to display the userlist");
|
||||
}
|
||||
var metadataMgr = config.metadataMgr;
|
||||
var origin = config.metadataMgr.getPrivateData().origin;
|
||||
var pathname = config.metadataMgr.getPrivateData().pathname;
|
||||
var hashes = metadataMgr.getPrivateData().availableHashes;
|
||||
var readOnly = metadataMgr.getPrivateData().readOnly;
|
||||
|
||||
var $shareIcon = $('<span>', {'class': 'fa fa-share-alt'});
|
||||
var options = [];
|
||||
|
||||
if (hashes.editHash) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {title: Messages.editShareTitle, 'class': 'editShare'},
|
||||
content: '<span class="fa fa-users"></span> ' + Messages.editShare
|
||||
});
|
||||
if (readOnly) {
|
||||
// We're in view mode, display the "open editing link" button
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {
|
||||
title: Messages.editOpenTitle,
|
||||
'class': 'editOpen',
|
||||
href: origin + pathname + '#' + hashes.editHash,
|
||||
target: '_blank'
|
||||
},
|
||||
content: '<span class="fa fa-users"></span> ' + Messages.editOpen
|
||||
});
|
||||
}
|
||||
options.push({tag: 'hr'});
|
||||
}
|
||||
if (hashes.viewHash) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {title: Messages.viewShareTitle, 'class': 'viewShare'},
|
||||
content: '<span class="fa fa-eye"></span> ' + Messages.viewShare
|
||||
});
|
||||
if (!readOnly) {
|
||||
// We're in edit mode, display the "open readonly" button
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {
|
||||
title: Messages.viewOpenTitle,
|
||||
'class': 'viewOpen',
|
||||
href: origin + pathname + '#' + hashes.viewHash,
|
||||
target: '_blank'
|
||||
},
|
||||
content: '<span class="fa fa-eye"></span> ' + Messages.viewOpen
|
||||
});
|
||||
}
|
||||
}
|
||||
var dropdownConfigShare = {
|
||||
text: $('<div>').append($shareIcon).html(),
|
||||
options: options,
|
||||
feedback: 'SHARE_MENU',
|
||||
};
|
||||
var $shareBlock = Cryptpad.createDropdown(dropdownConfigShare);
|
||||
$shareBlock.find('.dropdown-bar-content').addClass(SHARE_CLS).addClass(EDITSHARE_CLS).addClass(VIEWSHARE_CLS);
|
||||
$shareBlock.addClass('shareButton');
|
||||
$shareBlock.find('button').attr('title', Messages.shareButton);
|
||||
|
||||
if (hashes.editHash) {
|
||||
$shareBlock.find('a.editShare').click(function () {
|
||||
/*Common.storeLinkToClipboard(false, function (err) {
|
||||
if (!err) { Cryptpad.log(Messages.shareSuccess); }
|
||||
});*/
|
||||
var url = origin + pathname + '#' + hashes.editHash;
|
||||
var success = Cryptpad.Clipboard.copy(url);
|
||||
if (success) { Cryptpad.log(Messages.shareSuccess); }
|
||||
});
|
||||
}
|
||||
if (hashes.viewHash) {
|
||||
$shareBlock.find('a.viewShare').click(function () {
|
||||
/*Common.storeLinkToClipboard(true, function (err) {
|
||||
if (!err) { Cryptpad.log(Messages.shareSuccess); }
|
||||
});*/
|
||||
var url = origin + pathname + '#' + hashes.viewHash;
|
||||
var success = Cryptpad.Clipboard.copy(url);
|
||||
if (success) { Cryptpad.log(Messages.shareSuccess); }
|
||||
});
|
||||
}
|
||||
|
||||
toolbar.$leftside.append($shareBlock);
|
||||
toolbar.share = $shareBlock;
|
||||
|
||||
return "Loading share button";
|
||||
};
|
||||
|
||||
var createFileShare = function (toolbar) {
|
||||
if (!window.location.hash) {
|
||||
throw new Error("Unable to display the share button: hash required in the URL");
|
||||
}
|
||||
var $shareIcon = $('<span>', {'class': 'fa fa-share-alt'});
|
||||
var $button = $('<button>', {'title': Messages.shareButton}).append($shareIcon);
|
||||
$button.addClass('shareButton');
|
||||
$button.click(function () {
|
||||
var url = window.location.href;
|
||||
var success = Cryptpad.Clipboard.copy(url);
|
||||
if (success) { Cryptpad.log(Messages.shareSuccess); }
|
||||
});
|
||||
|
||||
toolbar.$leftside.append($button);
|
||||
return $button;
|
||||
};
|
||||
|
||||
var createTitle = function (toolbar, config) {
|
||||
var $titleContainer = $('<span>', {
|
||||
id: 'toolbarTitle',
|
||||
'class': TITLE_CLS
|
||||
}).appendTo(toolbar.$top);
|
||||
|
||||
var $hoverable = $('<span>', {'class': 'hoverable'}).appendTo($titleContainer);
|
||||
|
||||
if (typeof config.title !== "object") {
|
||||
console.error("config.title", config);
|
||||
throw new Error("config.title is not an object");
|
||||
}
|
||||
var updateTitle = config.title.updateTitle;
|
||||
var placeholder = config.title.defaultName;
|
||||
var suggestName = config.title.suggestName;
|
||||
|
||||
// Buttons
|
||||
var $text = $('<span>', {
|
||||
'class': 'title'
|
||||
}).appendTo($hoverable);
|
||||
var $pencilIcon = $('<span>', {
|
||||
'class': 'pencilIcon',
|
||||
'title': Messages.clickToEdit
|
||||
});
|
||||
var $saveIcon = $('<span>', {
|
||||
'class': 'saveIcon',
|
||||
'title': Messages.saveTitle
|
||||
}).hide();
|
||||
if (config.readOnly === 1) {
|
||||
$titleContainer.append($('<span>', {'class': 'readOnly'})
|
||||
.text('('+Messages.readonly+')'));
|
||||
}
|
||||
if (config.readOnly === 1 || typeof(Cryptpad) === "undefined") { return $titleContainer; }
|
||||
var $input = $('<input>', {
|
||||
type: 'text',
|
||||
placeholder: placeholder
|
||||
}).appendTo($hoverable).hide();
|
||||
if (config.readOnly !== 1) {
|
||||
$text.attr("title", Messages.clickToEdit);
|
||||
$text.addClass("editable");
|
||||
var $icon = $('<span>', {
|
||||
'class': 'fa fa-pencil readonly',
|
||||
style: 'font-family: FontAwesome;'
|
||||
});
|
||||
$pencilIcon.append($icon).appendTo($hoverable);
|
||||
var $icon2 = $('<span>', {
|
||||
'class': 'fa fa-check readonly',
|
||||
style: 'font-family: FontAwesome;'
|
||||
});
|
||||
$saveIcon.append($icon2).appendTo($hoverable);
|
||||
}
|
||||
|
||||
// Events
|
||||
$input.on('mousedown', function (e) {
|
||||
if (!$input.is(":focus")) {
|
||||
$input.focus();
|
||||
}
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
});
|
||||
var save = function () {
|
||||
var name = $input.val().trim();
|
||||
if (name === "") {
|
||||
name = $input.attr('placeholder');
|
||||
}
|
||||
updateTitle(name, function (err/*, newtitle*/) {
|
||||
if (err) { return console.error(err); }
|
||||
//$text.text(newtitle);
|
||||
$input.hide();
|
||||
$text.show();
|
||||
$pencilIcon.show();
|
||||
$saveIcon.hide();
|
||||
});
|
||||
};
|
||||
$input.on('keyup', function (e) {
|
||||
if (e.which === 13 && toolbar.connected === true) {
|
||||
save();
|
||||
} else if (e.which === 27) {
|
||||
$input.hide();
|
||||
$text.show();
|
||||
$pencilIcon.show();
|
||||
$saveIcon.hide();
|
||||
//$pencilIcon.css('display', '');
|
||||
} else if (e.which === 32) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
$saveIcon.click(save);
|
||||
|
||||
var displayInput = function () {
|
||||
if (toolbar.connected === false) { return; }
|
||||
$input.width(Math.max($text.width(), 300)+'px');
|
||||
$text.hide();
|
||||
//$pencilIcon.css('display', 'none');
|
||||
var inputVal = suggestName() || "";
|
||||
$input.val(inputVal);
|
||||
$input.show();
|
||||
$input.focus();
|
||||
$pencilIcon.hide();
|
||||
$saveIcon.show();
|
||||
};
|
||||
$text.on('click', displayInput);
|
||||
$pencilIcon.on('click', displayInput);
|
||||
return $titleContainer;
|
||||
};
|
||||
|
||||
var createPageTitle = function (toolbar, config) {
|
||||
if (config.title || !config.pageTitle) { return; }
|
||||
var $titleContainer = $('<span>', {
|
||||
id: 'toolbarTitle',
|
||||
'class': TITLE_CLS
|
||||
}).appendTo(toolbar.$top);
|
||||
|
||||
toolbar.$top.find('.filler').hide();
|
||||
|
||||
var $hoverable = $('<span>', {'class': 'hoverable'}).appendTo($titleContainer);
|
||||
|
||||
// Buttons
|
||||
$('<span>', {
|
||||
'class': 'title pageTitle'
|
||||
}).appendTo($hoverable).text(config.pageTitle);
|
||||
};
|
||||
|
||||
var createLinkToMain = function (toolbar) {
|
||||
var $linkContainer = $('<span>', {
|
||||
'class': "cryptpad-link"
|
||||
}).appendTo(toolbar.$top);
|
||||
|
||||
// We need to override the "a" tag action here because it is inside the iframe!
|
||||
var inDrive = /^\/drive/;
|
||||
|
||||
var href = inDrive ? '/index.html' : '/drive/';
|
||||
var buttonTitle = inDrive ? Messages.header_homeTitle : Messages.header_logoTitle;
|
||||
|
||||
var $aTag = $('<a>', {
|
||||
href: href,
|
||||
title: buttonTitle,
|
||||
'class': "cryptpad-logo"
|
||||
}).append($('<img>', {
|
||||
src: '/customize/images/logo_white.png?' + ApiConfig.requireConf.urlArgs
|
||||
}));
|
||||
var onClick = function (e) {
|
||||
e.preventDefault();
|
||||
if (e.ctrlKey) {
|
||||
window.open(href);
|
||||
return;
|
||||
}
|
||||
window.location = href;
|
||||
};
|
||||
|
||||
var onContext = function (e) { e.stopPropagation(); };
|
||||
|
||||
$aTag.click(onClick).contextmenu(onContext);
|
||||
|
||||
$linkContainer.append($aTag);
|
||||
|
||||
return $linkContainer;
|
||||
};
|
||||
|
||||
var typing = -1;
|
||||
var kickSpinner = function (toolbar, config, local) {
|
||||
if (!toolbar.spinner) { return; }
|
||||
var $spin = toolbar.spinner;
|
||||
|
||||
if (typing === -1) {
|
||||
typing = 1;
|
||||
$spin.text(Messages.typing);
|
||||
$spin.interval = window.setInterval(function () {
|
||||
var dots = Array(typing+1).join('.');
|
||||
$spin.text(Messages.typing + dots);
|
||||
typing++;
|
||||
if (typing > 3) { typing = 0; }
|
||||
}, 500);
|
||||
}
|
||||
var onSynced = function () {
|
||||
if ($spin.timeout) { clearTimeout($spin.timeout); }
|
||||
$spin.timeout = setTimeout(function () {
|
||||
window.clearInterval($spin.interval);
|
||||
typing = -1;
|
||||
$spin.text(Messages.saved);
|
||||
}, local ? 0 : SPINNER_DISAPPEAR_TIME);
|
||||
};
|
||||
if (Cryptpad) {
|
||||
Cryptpad.whenRealtimeSyncs(config.realtime, onSynced);
|
||||
return;
|
||||
}
|
||||
onSynced();
|
||||
};
|
||||
var ks = function (toolbar, config, local) {
|
||||
return function () {
|
||||
if (toolbar.connected) { kickSpinner(toolbar, config, local); }
|
||||
};
|
||||
};
|
||||
var createSpinner = function (toolbar, config) {
|
||||
var $spin = $('<span>', {'class': SPINNER_CLS}).appendTo(toolbar.$leftside);
|
||||
$spin.text(Messages.synchronizing);
|
||||
|
||||
if (config.realtime) {
|
||||
config.realtime.onPatch(ks(toolbar, config));
|
||||
config.realtime.onMessage(ks(toolbar, config, true));
|
||||
}
|
||||
// without this, users in read-only mode say 'synchronizing' until they
|
||||
// receive a patch.
|
||||
if (Cryptpad) {
|
||||
typing = 0;
|
||||
Cryptpad.whenRealtimeSyncs(config.realtime, function () {
|
||||
kickSpinner(toolbar, config);
|
||||
});
|
||||
}
|
||||
return $spin;
|
||||
};
|
||||
|
||||
var createLimit = function (toolbar) {
|
||||
if (!Config.enablePinning) { return; }
|
||||
var $limitIcon = $('<span>', {'class': 'fa fa-exclamation-triangle'});
|
||||
var $limit = toolbar.$userAdmin.find('.'+LIMIT_CLS).attr({
|
||||
'title': Messages.pinLimitReached
|
||||
}).append($limitIcon).hide();
|
||||
var todo = function (e, overLimit) {
|
||||
if (e) { return void console.error("Unable to get the pinned usage"); }
|
||||
if (overLimit) {
|
||||
var key = 'pinLimitReachedAlert';
|
||||
if (ApiConfig.noSubscriptionButton === true) {
|
||||
key = 'pinLimitReachedAlertNoAccounts';
|
||||
}
|
||||
$limit.show().click(function () {
|
||||
Cryptpad.alert(Messages._getKey(key, [encodeURIComponent(window.location.hostname)]), null, true);
|
||||
});
|
||||
}
|
||||
};
|
||||
Common.isOverPinLimit(todo);
|
||||
|
||||
return $limit;
|
||||
};
|
||||
|
||||
var createNewPad = function (toolbar, config) {
|
||||
var $newPad = toolbar.$top.find('.'+NEWPAD_CLS).show();
|
||||
|
||||
var origin = config.metadataMgr.getPrivateData().origin;
|
||||
|
||||
var pads_options = [];
|
||||
Config.availablePadTypes.forEach(function (p) {
|
||||
if (p === 'drive') { return; }
|
||||
if (!Cryptpad.isLoggedIn() && Config.registeredOnlyTypes &&
|
||||
Config.registeredOnlyTypes.indexOf(p) !== -1) { return; }
|
||||
pads_options.push({
|
||||
tag: 'a',
|
||||
attributes: {
|
||||
'target': '_blank',
|
||||
'href': origin + '/' + p + '/',
|
||||
},
|
||||
content: $('<div>').append(Cryptpad.getIcon(p)).html() + Messages.type[p]
|
||||
});
|
||||
});
|
||||
var dropdownConfig = {
|
||||
text: '', // Button initial text
|
||||
options: pads_options, // Entries displayed in the menu
|
||||
container: $newPad,
|
||||
left: true,
|
||||
feedback: /drive/.test(window.location.pathname)?
|
||||
'DRIVE_NEWPAD': 'NEWPAD',
|
||||
};
|
||||
var $newPadBlock = Cryptpad.createDropdown(dropdownConfig);
|
||||
$newPadBlock.find('button').attr('title', Messages.newButtonTitle);
|
||||
$newPadBlock.find('button').addClass('fa fa-th');
|
||||
return $newPadBlock;
|
||||
};
|
||||
|
||||
var createUserAdmin = function (toolbar, config) {
|
||||
if (!config.metadataMgr) {
|
||||
throw new Error("You must provide a `metadataMgr` to display the user menu");
|
||||
}
|
||||
var metadataMgr = config.metadataMgr;
|
||||
var $userAdmin = toolbar.$userAdmin.find('.'+USERADMIN_CLS).show();
|
||||
var userMenuCfg = {
|
||||
$initBlock: $userAdmin,
|
||||
metadataMgr: metadataMgr,
|
||||
Common: Common
|
||||
};
|
||||
if (!config.hideDisplayName) {
|
||||
$.extend(true, userMenuCfg, {
|
||||
displayNameCls: USERNAME_CLS,
|
||||
changeNameButtonCls: USERBUTTON_CLS,
|
||||
});
|
||||
}
|
||||
if (config.readOnly !== 1) {
|
||||
userMenuCfg.displayName = 1;
|
||||
userMenuCfg.displayChangeName = 1;
|
||||
}
|
||||
Common.createUserAdminMenu(userMenuCfg);
|
||||
$userAdmin.find('button').attr('title', Messages.userAccountButton);
|
||||
|
||||
var $userButton = toolbar.$userNameButton = $userAdmin.find('a.' + USERBUTTON_CLS);
|
||||
$userButton.click(function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var myData = metadataMgr.getMetadata().users[metadataMgr.getNetfluxId()];
|
||||
var lastName = myData.name;
|
||||
Cryptpad.prompt(Messages.changeNamePrompt, lastName || '', function (newName) {
|
||||
if (newName === null && typeof(lastName) === "string") { return; }
|
||||
if (newName === null) { newName = ''; }
|
||||
else { Common.feedback('NAME_CHANGED'); }
|
||||
Common.setDisplayName(newName, function (err) {
|
||||
if (err) {
|
||||
console.log("Couldn't set username");
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
//Cryptpad.changeDisplayName(newName, true); Already done?
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return $userAdmin;
|
||||
};
|
||||
|
||||
// Events
|
||||
var initClickEvents = function (toolbar, config) {
|
||||
var removeDropdowns = function () {
|
||||
window.setTimeout(function () {
|
||||
toolbar.$toolbar.find('.cryptpad-dropdown').hide();
|
||||
});
|
||||
};
|
||||
var cancelEditTitle = function (e) {
|
||||
// Now we want to apply the title even if we click somewhere else
|
||||
if ($(e.target).parents('.' + TITLE_CLS).length || !toolbar.title) {
|
||||
return;
|
||||
}
|
||||
var $title = toolbar.title;
|
||||
if (!$title.find('input').is(':visible')) { return; }
|
||||
|
||||
// Press enter
|
||||
var ev = $.Event("keyup");
|
||||
ev.which = 13;
|
||||
$title.find('input').trigger(ev);
|
||||
};
|
||||
// Click in the main window
|
||||
var w = config.ifrw || window;
|
||||
$(w).on('click', removeDropdowns);
|
||||
$(w).on('click', cancelEditTitle);
|
||||
// Click in iframes
|
||||
try {
|
||||
if (w.$ && w.$('iframe').length) {
|
||||
config.ifrw.$('iframe').each(function (i, el) {
|
||||
$(el.contentWindow).on('click', removeDropdowns);
|
||||
$(el.contentWindow).on('click', cancelEditTitle);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// empty try catch in case this iframe is problematic
|
||||
}
|
||||
};
|
||||
|
||||
// Notifications
|
||||
var initNotifications = function (toolbar, config) {
|
||||
// Display notifications when users are joining/leaving the session
|
||||
var oldUserData;
|
||||
if (!config.metadataMgr) { return; }
|
||||
var metadataMgr = config.metadataMgr;
|
||||
var userNetfluxId = metadataMgr.getNetfluxId();
|
||||
if (typeof Cryptpad !== "undefined") {
|
||||
var notify = function(type, name, oldname) {
|
||||
// type : 1 (+1 user), 0 (rename existing user), -1 (-1 user)
|
||||
if (typeof name === "undefined") { return; }
|
||||
name = name || Messages.anonymous;
|
||||
switch(type) {
|
||||
case 1:
|
||||
Cryptpad.log(Messages._getKey("notifyJoined", [name]));
|
||||
break;
|
||||
case 0:
|
||||
oldname = (!oldname) ? Messages.anonymous : oldname;
|
||||
Cryptpad.log(Messages._getKey("notifyRenamed", [oldname, name]));
|
||||
break;
|
||||
case -1:
|
||||
Cryptpad.log(Messages._getKey("notifyLeft", [name]));
|
||||
break;
|
||||
default:
|
||||
console.log("Invalid type of notification");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
var userPresent = function (id, user, data) {
|
||||
if (!(user && user.uid)) {
|
||||
console.log('no uid');
|
||||
return 0;
|
||||
}
|
||||
if (!data) {
|
||||
console.log('no data');
|
||||
return 0;
|
||||
}
|
||||
|
||||
var count = 0;
|
||||
Object.keys(data).forEach(function (k) {
|
||||
if (data[k] && data[k].uid === user.uid) { count++; }
|
||||
});
|
||||
return count;
|
||||
};
|
||||
|
||||
metadataMgr.onChange(function () {
|
||||
var newdata = metadataMgr.getMetadata().users;
|
||||
var netfluxIds = Object.keys(newdata);
|
||||
// Notify for disconnected users
|
||||
if (typeof oldUserData !== "undefined") {
|
||||
for (var u in oldUserData) {
|
||||
// if a user's uid is still present after having left, don't notify
|
||||
if (netfluxIds.indexOf(u) === -1) {
|
||||
var temp = JSON.parse(JSON.stringify(oldUserData[u]));
|
||||
delete oldUserData[u];
|
||||
if (temp && newdata[userNetfluxId] && temp.uid === newdata[userNetfluxId].uid) { return; }
|
||||
if (userPresent(u, temp, newdata || oldUserData) < 1) {
|
||||
notify(-1, temp.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update the "oldUserData" object and notify for new users and names changed
|
||||
if (typeof newdata === "undefined") { return; }
|
||||
if (typeof oldUserData === "undefined") {
|
||||
oldUserData = JSON.parse(JSON.stringify(newdata));
|
||||
return;
|
||||
}
|
||||
if (config.readOnly === 0 && !oldUserData[userNetfluxId]) {
|
||||
oldUserData = JSON.parse(JSON.stringify(newdata));
|
||||
return;
|
||||
}
|
||||
for (var k in newdata) {
|
||||
if (k !== userNetfluxId && netfluxIds.indexOf(k) !== -1) {
|
||||
if (typeof oldUserData[k] === "undefined") {
|
||||
// if the same uid is already present in the userdata, don't notify
|
||||
if (!userPresent(k, newdata[k], oldUserData)) {
|
||||
notify(1, newdata[k].name);
|
||||
}
|
||||
} else if (oldUserData[k].name !== newdata[k].name) {
|
||||
notify(0, newdata[k].name, oldUserData[k].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
oldUserData = JSON.parse(JSON.stringify(newdata));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Main
|
||||
|
||||
Bar.create = function (cfg) {
|
||||
var config = cfg || {};
|
||||
Cryptpad = config.common;
|
||||
Common = config.sfCommon;
|
||||
Messages = Cryptpad.Messages;
|
||||
config.readOnly = (typeof config.readOnly !== "undefined") ? (config.readOnly ? 1 : 0) : -1;
|
||||
config.displayed = config.displayed || [];
|
||||
|
||||
var toolbar = {};
|
||||
|
||||
toolbar.connected = false;
|
||||
toolbar.firstConnection = true;
|
||||
|
||||
var $toolbar = toolbar.$toolbar = createRealtimeToolbar(config);
|
||||
toolbar.$leftside = $toolbar.find('.'+Bar.constants.leftside);
|
||||
toolbar.$rightside = $toolbar.find('.'+Bar.constants.rightside);
|
||||
toolbar.$drawer = $toolbar.find('.'+Bar.constants.drawer);
|
||||
toolbar.$top = $toolbar.find('.'+Bar.constants.top);
|
||||
toolbar.$history = $toolbar.find('.'+Bar.constants.history);
|
||||
|
||||
toolbar.$userAdmin = $toolbar.find('.'+Bar.constants.userAdmin);
|
||||
|
||||
// Create the subelements
|
||||
var tb = {};
|
||||
tb['userlist'] = createUserList;
|
||||
tb['share'] = createShare;
|
||||
tb['fileshare'] = createFileShare;//TODO
|
||||
tb['title'] = createTitle;
|
||||
tb['pageTitle'] = createPageTitle;//TODO
|
||||
tb['lag'] = $.noop;
|
||||
tb['spinner'] = createSpinner;
|
||||
tb['state'] = $.noop;
|
||||
tb['limit'] = createLimit; // TODO
|
||||
tb['upgrade'] = $.noop;
|
||||
tb['newpad'] = createNewPad;
|
||||
tb['useradmin'] = createUserAdmin;
|
||||
|
||||
var addElement = toolbar.addElement = function (arr, additionnalCfg, init) {
|
||||
if (typeof additionnalCfg === "object") { $.extend(true, config, additionnalCfg); }
|
||||
arr.forEach(function (el) {
|
||||
if (typeof el !== "string" || !el.trim()) { return; }
|
||||
if (typeof tb[el] === "function") {
|
||||
if (!init && config.displayed.indexOf(el) !== -1) { return; } // Already done
|
||||
toolbar[el] = tb[el](toolbar, config);
|
||||
if (!init) { config.displayed.push(el); }
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
addElement(config.displayed, {}, true);
|
||||
initUserList(toolbar, config);
|
||||
|
||||
toolbar['linkToMain'] = createLinkToMain(toolbar, config);
|
||||
|
||||
if (!config.realtime) { toolbar.connected = true; }
|
||||
|
||||
initClickEvents(toolbar, config);
|
||||
initNotifications(toolbar, config);
|
||||
|
||||
var failed = toolbar.failed = function () {
|
||||
toolbar.connected = false;
|
||||
|
||||
if (toolbar.spinner) {
|
||||
toolbar.spinner.text(Messages.disconnected);
|
||||
}
|
||||
//checkLag(toolbar, config);
|
||||
};
|
||||
toolbar.reconnecting = function (/*userId*/) {
|
||||
//if (config.metadataMgr) { config.userList.userNetfluxId = userId; } TODO
|
||||
toolbar.connected = false;
|
||||
if (toolbar.spinner) {
|
||||
toolbar.spinner.text(Messages.reconnecting);
|
||||
}
|
||||
//checkLag(toolbar, config);
|
||||
};
|
||||
|
||||
// On log out, remove permanently the realtime elements of the toolbar
|
||||
Cryptpad.onLogout(function () {
|
||||
failed();
|
||||
if (toolbar.useradmin) { toolbar.useradmin.hide(); }
|
||||
if (toolbar.userlist) { toolbar.userlist.hide(); }
|
||||
});
|
||||
|
||||
return toolbar;
|
||||
};
|
||||
|
||||
return Bar;
|
||||
});
|
||||
@@ -413,6 +413,15 @@ define([
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
exp.getRecentPads = function () {
|
||||
var allFiles = files[FILES_DATA];
|
||||
var sorted = Object.keys(allFiles)
|
||||
.sort(function (a,b) {
|
||||
return allFiles[a].atime < allFiles[b].atime;
|
||||
})
|
||||
.map(function (str) { return Number(str); });
|
||||
return sorted;
|
||||
};
|
||||
|
||||
/**
|
||||
* OPERATIONS
|
||||
|
||||
Reference in New Issue
Block a user