88 changed files with 2233 additions and 8640 deletions
Split View
Diff Options
-
40check-accounts.js
-
54customize.dist/header.js
-
78customize.dist/main.js
-
4customize.dist/messages.js
-
47customize.dist/pages.js
-
27customize.dist/src/less2/include/dropdown.less
-
1customize.dist/src/less2/include/toolbar-history.less
-
103customize.dist/src/less2/include/toolbar.less
-
1customize.dist/src/less2/main.less
-
3customize.dist/template.js
-
16customize.dist/translations/messages.fr.js
-
4customize.dist/translations/messages.js
-
2package.json
-
33www/assert/main.js
-
18www/code/inner.js
-
14www/common/common-constants.js
-
366www/common/common-file.js
-
25www/common/common-hash.js
-
266www/common/common-history.js
-
125www/common/common-interface.js
-
33www/common/common-messaging.js
-
36www/common/common-messenger.js
-
59www/common/common-metadata.js
-
29www/common/common-notifier.js
-
9www/common/common-realtime.js
-
15www/common/common-thumbnail.js
-
87www/common/common-title.js
-
404www/common/common-ui-elements.js
-
117www/common/common-userlist.js
-
112www/common/common-userlist2.js
-
23www/common/common-util.js
-
22www/common/cryptget.js
-
1360www/common/cryptpad-common.js
-
9www/common/diffMarked.js
-
1179www/common/fileObject.js
-
59www/common/fsStore.js
-
7www/common/login.js
-
11www/common/mergeDrive.js
-
22www/common/migrate-user-object.js
-
93www/common/outer/upload.js
-
38www/common/sframe-app-framework.js
-
18www/common/sframe-common-codemirror.js
-
94www/common/sframe-common-file.js
-
28www/common/sframe-common-history.js
-
195www/common/sframe-common-outer.js
-
10www/common/sframe-common-title.js
-
66www/common/sframe-common.js
-
6www/common/sframe-messenger-inner.js
-
677www/common/sframe-messenger-outer.js
-
3www/common/sframe-protocol.js
-
865www/common/toolbar.js
-
1069www/common/toolbar2.js
-
331www/common/toolbar3.js
-
66www/common/userObject.js
-
32www/contacts/inner.js
-
36www/contacts/messenger-ui.js
-
31www/debug/app-debug.less
-
52www/debug/colors.js
-
38www/debug/index.html
-
19www/debug/inner.html
-
179www/debug/inner.js
-
58www/debug/main.js
-
180www/drive/inner.js
-
4www/drive/main.js
-
7www/drive/tests.js
-
46www/file/inner.js
-
1www/filepicker/app-filepicker.less
-
43www/filepicker/inner.js
-
42www/filepicker/main.js
-
33www/invite/main.js
-
40www/login/main.js
-
16www/oldsettings/index.html
-
569www/oldsettings/main.js
-
54www/oldsettings/main.less
-
4www/pad/app-pad.less
-
33www/poll/inner.js
-
67www/poll/render.js
-
72www/profile/inner.js
-
27www/profile/main.js
-
68www/register/main.js
-
55www/settings/inner.js
-
31www/settings/main.js
-
528www/settings/messenger-ui.js
-
23www/slide/inner.js
-
31www/todo/inner.js
-
6www/todo/main.js
-
17www/user/main.js
-
52www/whiteboard/inner.js
@ -0,0 +1,40 @@ |
|||
/* globals Buffer */ |
|||
var Https = require('https'); |
|||
var Config = require("./config.js"); |
|||
var Package = require("./package.json"); |
|||
|
|||
var body = JSON.stringify({ |
|||
domain: Config.myDomain, |
|||
adminEmail: Config.adminEmail, |
|||
version: Package.version, |
|||
}); |
|||
|
|||
var options = { |
|||
host: 'accounts.cryptpad.fr', |
|||
path: '/api/getauthorized', |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/json', |
|||
'Content-Length': Buffer.byteLength(body) |
|||
} |
|||
}; |
|||
|
|||
Https.request(options, function (response) { |
|||
if (!('' + response.statusCode).match(/^2\d\d$/)) { |
|||
throw new Error('SERVER ERROR ' + response.statusCode); |
|||
} |
|||
var str = ''; |
|||
response.on('data', function (chunk) { |
|||
str += chunk; |
|||
}); |
|||
response.on('end', function () { |
|||
try { |
|||
var json = JSON.parse(str); |
|||
console.log(json); |
|||
} catch (e) { |
|||
throw new Error(e); |
|||
} |
|||
}); |
|||
}).on('error', function (e) { |
|||
console.error(e); |
|||
}).end(body); |
|||
@ -1,54 +0,0 @@ |
|||
define([ |
|||
'jquery', |
|||
'/customize/application_config.js', |
|||
'/common/cryptpad-common.js', |
|||
'/api/config', |
|||
], function ($, Config, Cryptpad, ApiConfig) { |
|||
|
|||
window.APP = { |
|||
Cryptpad: Cryptpad, |
|||
}; |
|||
|
|||
var Messages = Cryptpad.Messages; |
|||
|
|||
$(function () { |
|||
// Language selector
|
|||
var $sel = $('#language-selector'); |
|||
Cryptpad.createLanguageSelector(undefined, $sel); |
|||
$sel.find('button').addClass('btn').addClass('btn-secondary'); |
|||
$sel.show(); |
|||
|
|||
var $upgrade = $('#upgrade'); |
|||
|
|||
var showUpgrade = function (text, feedback, url) { |
|||
if (ApiConfig.removeDonateButton) { return; } |
|||
if (localStorage.plan) { return; } |
|||
if (!text) { return; } |
|||
$upgrade.text(text).show(); |
|||
$upgrade.click(function () { |
|||
Cryptpad.feedback(feedback); |
|||
window.open(url,'_blank'); |
|||
}); |
|||
}; |
|||
|
|||
// User admin menu
|
|||
var $userMenu = $('#user-menu'); |
|||
var userMenuCfg = { |
|||
$initBlock: $userMenu, |
|||
'static': true |
|||
}; |
|||
var $userAdmin = Cryptpad.createUserAdminMenu(userMenuCfg); |
|||
$userAdmin.find('button').addClass('btn').addClass('btn-secondary'); |
|||
|
|||
$(window).click(function () { |
|||
$('.cp-dropdown-content').hide(); |
|||
}); |
|||
|
|||
if (Cryptpad.isLoggedIn() && ApiConfig.allowSubscriptions) { |
|||
showUpgrade(Messages.upgradeAccount, "HOME_UPGRADE_ACCOUNT", Cryptpad.upgradeURL); |
|||
} else { |
|||
showUpgrade(Messages.supportCryptpad, "HOME_SUPPORT_CRYPTPAD", Cryptpad.donateURL); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
@ -0,0 +1,14 @@ |
|||
define(function () { |
|||
return { |
|||
// localStorage
|
|||
userHashKey: 'User_hash', |
|||
userNameKey: 'User_name', |
|||
fileHashKey: 'FS_hash', |
|||
// sessionStorage
|
|||
newPadPathKey: "newPadPath", |
|||
// Store
|
|||
displayNameKey: 'cryptpad.username', |
|||
oldStorageKey: 'CryptPad_RECENTPADS', |
|||
storageKey: 'filesData', |
|||
}; |
|||
}); |
|||
@ -1,366 +0,0 @@ |
|||
define([ |
|||
'jquery', |
|||
'/file/file-crypto.js', |
|||
'/common/common-thumbnail.js', |
|||
'/bower_components/tweetnacl/nacl-fast.min.js', |
|||
], function ($, FileCrypto, Thumb) { |
|||
var Nacl = window.nacl; |
|||
var module = {}; |
|||
|
|||
var blobToArrayBuffer = module.blobToArrayBuffer = function (blob, cb) { |
|||
var reader = new FileReader(); |
|||
reader.onloadend = function () { |
|||
cb(void 0, this.result); |
|||
}; |
|||
reader.readAsArrayBuffer(blob); |
|||
}; |
|||
|
|||
var arrayBufferToString = function (AB) { |
|||
try { |
|||
return Nacl.util.encodeBase64(new Uint8Array(AB)); |
|||
} catch (e) { |
|||
console.error(e); |
|||
return null; |
|||
} |
|||
}; |
|||
|
|||
module.upload = function (file, noStore, common, updateProgress, onComplete, onError, onPending) { |
|||
var u8 = file.blob; // This is not a blob but a uint8array
|
|||
var metadata = file.metadata; |
|||
|
|||
// if it exists, path contains the new pad location in the drive
|
|||
var path = file.path; |
|||
|
|||
var key = Nacl.randomBytes(32); |
|||
var next = FileCrypto.encrypt(u8, metadata, key); |
|||
|
|||
var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata); |
|||
|
|||
var sendChunk = function (box, cb) { |
|||
var enc = Nacl.util.encodeBase64(box); |
|||
common.rpc.send.unauthenticated('UPLOAD', enc, function (e, msg) { |
|||
cb(e, msg); |
|||
}); |
|||
}; |
|||
|
|||
var actual = 0; |
|||
var again = function (err, box) { |
|||
if (err) { throw new Error(err); } |
|||
if (box) { |
|||
actual += box.length; |
|||
var progressValue = (actual / estimate * 100); |
|||
updateProgress(progressValue); |
|||
|
|||
return void sendChunk(box, function (e) { |
|||
if (e) { return console.error(e); } |
|||
next(again); |
|||
}); |
|||
} |
|||
|
|||
if (actual !== estimate) { |
|||
console.error('Estimated size does not match actual size'); |
|||
} |
|||
|
|||
// if not box then done
|
|||
common.uploadComplete(function (e, id) { |
|||
if (e) { return void console.error(e); } |
|||
var uri = ['', 'blob', id.slice(0,2), id].join('/'); |
|||
console.log("encrypted blob is now available as %s", uri); |
|||
|
|||
var b64Key = Nacl.util.encodeBase64(key); |
|||
|
|||
var hash = common.getFileHashFromKeys(id, b64Key); |
|||
var href = '/file/#' + hash; |
|||
|
|||
var title = metadata.name; |
|||
|
|||
if (noStore) { return void onComplete(href); } |
|||
|
|||
common.initialPath = path; |
|||
common.renamePad(title || "", href, function (err) { |
|||
if (err) { return void console.error(err); } |
|||
onComplete(href); |
|||
common.setPadAttribute('fileType', metadata.type, null, href); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
common.uploadStatus(estimate, function (e, pending) { |
|||
if (e) { |
|||
console.error(e); |
|||
onError(e); |
|||
return; |
|||
} |
|||
|
|||
if (pending) { |
|||
return void onPending(function () { |
|||
// if the user wants to cancel the pending upload to execute that one
|
|||
common.uploadCancel(function (e, res) { |
|||
if (e) { |
|||
return void console.error(e); |
|||
} |
|||
console.log(res); |
|||
next(again); |
|||
}); |
|||
}); |
|||
} |
|||
next(again); |
|||
}); |
|||
}; |
|||
|
|||
module.create = function (common, config) { |
|||
var File = {}; |
|||
|
|||
var Messages = common.Messages; |
|||
|
|||
var queue = File.queue = { |
|||
queue: [], |
|||
inProgress: false |
|||
}; |
|||
|
|||
var uid = function () { |
|||
return 'file-' + String(Math.random()).substring(2); |
|||
}; |
|||
|
|||
var $table = File.$table = $('<table>', { id: 'uploadStatus' }); |
|||
var $thead = $('<tr>').appendTo($table); |
|||
$('<td>').text(Messages.upload_name).appendTo($thead); |
|||
$('<td>').text(Messages.upload_size).appendTo($thead); |
|||
$('<td>').text(Messages.upload_progress).appendTo($thead); |
|||
$('<td>').text(Messages.cancel).appendTo($thead); |
|||
|
|||
var createTableContainer = function ($body) { |
|||
File.$container = $('<div>', { id: 'uploadStatusContainer' }).append($table).appendTo($body); |
|||
return File.$container; |
|||
}; |
|||
|
|||
var getData = function (file, href) { |
|||
var data = {}; |
|||
|
|||
data.name = file.metadata.name; |
|||
data.url = href; |
|||
if (file.metadata.type.slice(0,6) === 'image/') { |
|||
data.mediatag = true; |
|||
} |
|||
|
|||
return data; |
|||
}; |
|||
|
|||
var upload = function (file) { |
|||
var blob = file.blob; // This is not a blob but an array buffer
|
|||
var u8 = new Uint8Array(blob); |
|||
var metadata = file.metadata; |
|||
var id = file.id; |
|||
if (queue.inProgress) { return; } |
|||
queue.inProgress = true; |
|||
|
|||
var $row = $table.find('tr[id="'+id+'"]'); |
|||
|
|||
$row.find('.upCancel').html('-'); |
|||
var $pv = $row.find('.progressValue'); |
|||
var $pb = $row.find('.progressContainer'); |
|||
var $pc = $row.find('.upProgress'); |
|||
var $link = $row.find('.upLink'); |
|||
|
|||
var updateProgress = function (progressValue) { |
|||
$pv.text(Math.round(progressValue*100)/100 + '%'); |
|||
$pb.css({ |
|||
width: (progressValue/100)*$pc.width()+'px' |
|||
}); |
|||
}; |
|||
|
|||
var onComplete = function (href) { |
|||
$link.attr('href', href) |
|||
.click(function (e) { |
|||
e.preventDefault(); |
|||
window.open($link.attr('href'), '_blank'); |
|||
}); |
|||
var title = metadata.name; |
|||
common.log(Messages._getKey('upload_success', [title])); |
|||
common.prepareFeedback('upload')(); |
|||
|
|||
if (config.onUploaded) { |
|||
var data = getData(file, href); |
|||
config.onUploaded(file.dropEvent, data); |
|||
} |
|||
|
|||
queue.inProgress = false; |
|||
queue.next(); |
|||
}; |
|||
|
|||
var onError = function (e) { |
|||
queue.inProgress = false; |
|||
queue.next(); |
|||
if (e === 'TOO_LARGE') { |
|||
// TODO update table to say too big?
|
|||
return void common.alert(Messages.upload_tooLarge); |
|||
} |
|||
if (e === 'NOT_ENOUGH_SPACE') { |
|||
// TODO update table to say not enough space?
|
|||
return void common.alert(Messages.upload_notEnoughSpace); |
|||
} |
|||
console.error(e); |
|||
return void common.alert(Messages.upload_serverError); |
|||
}; |
|||
|
|||
var onPending = function (cb) { |
|||
common.confirm(Messages.upload_uploadPending, function (yes) { |
|||
if (!yes) { return; } |
|||
cb(); |
|||
}); |
|||
}; |
|||
|
|||
file.blob = u8; |
|||
module.upload(file, config.noStore, common, updateProgress, onComplete, onError, onPending); |
|||
}; |
|||
|
|||
var prettySize = function (bytes) { |
|||
var kB = common.bytesToKilobytes(bytes); |
|||
if (kB < 1024) { return kB + Messages.KB; } |
|||
var mB = common.bytesToMegabytes(bytes); |
|||
return mB + Messages.MB; |
|||
}; |
|||
|
|||
queue.next = function () { |
|||
if (queue.queue.length === 0) { |
|||
queue.to = window.setTimeout(function () { |
|||
if (config.keepTable) { return; } |
|||
File.$container.fadeOut(); |
|||
}, 3000); |
|||
return; |
|||
} |
|||
if (queue.inProgress) { return; } |
|||
File.$container.show(); |
|||
var file = queue.queue.shift(); |
|||
upload(file); |
|||
}; |
|||
queue.push = function (obj) { |
|||
var id = uid(); |
|||
obj.id = id; |
|||
queue.queue.push(obj); |
|||
|
|||
$table.show(); |
|||
var estimate = FileCrypto.computeEncryptedSize(obj.blob.byteLength, obj.metadata); |
|||
|
|||
var $progressBar = $('<div>', {'class':'progressContainer'}); |
|||
var $progressValue = $('<span>', {'class':'progressValue'}).text(Messages.upload_pending); |
|||
|
|||
var $tr = $('<tr>', {id: id}).appendTo($table); |
|||
|
|||
var $cancel = $('<span>', {'class': 'cancel fa fa-times'}).click(function () { |
|||
queue.queue = queue.queue.filter(function (el) { return el.id !== id; }); |
|||
$cancel.remove(); |
|||
$tr.find('.upCancel').text('-'); |
|||
$tr.find('.progressValue').text(Messages.upload_cancelled); |
|||
}); |
|||
|
|||
var $link = $('<a>', { |
|||
'class': 'upLink', |
|||
'rel': 'noopener noreferrer' |
|||
}).text(obj.metadata.name); |
|||
|
|||
$('<td>').append($link).appendTo($tr); |
|||
$('<td>').text(prettySize(estimate)).appendTo($tr); |
|||
$('<td>', {'class': 'upProgress'}).append($progressBar).append($progressValue).appendTo($tr); |
|||
$('<td>', {'class': 'upCancel'}).append($cancel).appendTo($tr); |
|||
|
|||
queue.next(); |
|||
}; |
|||
|
|||
var handleFile = File.handleFile = function (file, e, thumbnail) { |
|||
var thumb; |
|||
var file_arraybuffer; |
|||
var finish = function () { |
|||
var metadata = { |
|||
name: file.name, |
|||
type: file.type, |
|||
}; |
|||
if (thumb) { metadata.thumbnail = thumb; } |
|||
queue.push({ |
|||
blob: file_arraybuffer, |
|||
metadata: metadata, |
|||
dropEvent: e |
|||
}); |
|||
}; |
|||
|
|||
blobToArrayBuffer(file, function (e, buffer) { |
|||
if (e) { console.error(e); } |
|||
file_arraybuffer = buffer; |
|||
if (thumbnail) { // there is already a thumbnail
|
|||
return blobToArrayBuffer(thumbnail, function (e, buffer) { |
|||
if (e) { console.error(e); } |
|||
thumb = arrayBufferToString(buffer); |
|||
finish(); |
|||
}); |
|||
} |
|||
|
|||
if (!Thumb.isSupportedType(file.type)) { return finish(); } |
|||
// make a resized thumbnail from the image..
|
|||
Thumb.fromBlob(file, function (e, thumb64) { |
|||
if (e) { console.error(e); } |
|||
if (!thumb64) { return finish(); } |
|||
thumb = thumb64; |
|||
finish(); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
var onFileDrop = File.onFileDrop = function (file, e) { |
|||
if (!common.isLoggedIn()) { |
|||
return common.alert(common.Messages.upload_mustLogin); |
|||
} |
|||
|
|||
Array.prototype.slice.call(file).forEach(function (d) { |
|||
handleFile(d, e); |
|||
}); |
|||
}; |
|||
|
|||
var createAreaHandlers = File.createDropArea = function ($area, $hoverArea) { |
|||
var counter = 0; |
|||
if (!$hoverArea) { $hoverArea = $area; } |
|||
if (!$area) { return; } |
|||
$hoverArea |
|||
.on('dragenter', function (e) { |
|||
e.preventDefault(); |
|||
e.stopPropagation(); |
|||
counter++; |
|||
$hoverArea.addClass('hovering'); |
|||
}) |
|||
.on('dragleave', function (e) { |
|||
e.preventDefault(); |
|||
e.stopPropagation(); |
|||
counter--; |
|||
if (counter <= 0) { |
|||
$hoverArea.removeClass('hovering'); |
|||
} |
|||
}); |
|||
|
|||
$area |
|||
.on('drag dragstart dragend dragover drop dragenter dragleave', function (e) { |
|||
e.preventDefault(); |
|||
e.stopPropagation(); |
|||
}) |
|||
.on('drop', function (e) { |
|||
e.stopPropagation(); |
|||
|
|||
var dropped = e.originalEvent.dataTransfer.files; |
|||
counter = 0; |
|||
$hoverArea.removeClass('hovering'); |
|||
onFileDrop(dropped, e); |
|||
}); |
|||
}; |
|||
|
|||
var createUploader = function ($area, $hover, $body) { |
|||
if (!config.noHandlers) { |
|||
createAreaHandlers($area, null); |
|||
} |
|||
createTableContainer($body); |
|||
}; |
|||
|
|||
createUploader(config.dropArea, config.hoverArea, config.body); |
|||
|
|||
return File; |
|||
}; |
|||
|
|||
return module; |
|||
}); |
|||
@ -1,266 +0,0 @@ |
|||
define([ |
|||
'jquery', |
|||
'/bower_components/chainpad-crypto/crypto.js', |
|||
'/bower_components/chainpad/chainpad.dist.js', |
|||
], function ($, Crypto, 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 network = common.getNetwork(); |
|||
var hkn = network.historyKeeper; |
|||
|
|||
var wcId = common.hrefToHexChannelId(config.href || window.location.href); |
|||
|
|||
var createRealtime = function () { |
|||
return ChainPad.create({ |
|||
userName: 'history', |
|||
initialState: '', |
|||
patchTransformer: ChainPad.NaiveJSONStransformer, |
|||
logLevel: 0, |
|||
noPrune: true |
|||
}); |
|||
}; |
|||
var realtime = createRealtime(); |
|||
|
|||
var parsed = config.href ? common.parsePadUrl(config.href) : {}; |
|||
var secret = common.getSecrets(parsed.type, parsed.hash); |
|||
|
|||
History.readOnly = 0; |
|||
if (!secret.keys) { |
|||
secret.keys = secret.key; |
|||
History.readOnly = 0; |
|||
} |
|||
else if (!secret.keys.validateKey) { |
|||
History.readOnly = 1; |
|||
} |
|||
|
|||
var crypto = Crypto.createEncryptor(secret.keys); |
|||
|
|||
var to = window.setTimeout(function () { |
|||
cb('[GET_FULL_HISTORY_TIMEOUT]'); |
|||
}, 30000); |
|||
|
|||
var parse = function (msg) { |
|||
try { |
|||
return JSON.parse(msg); |
|||
} catch (e) { |
|||
return null; |
|||
} |
|||
}; |
|||
var onMsg = function (msg) { |
|||
var parsed = parse(msg); |
|||
if (parsed[0] === 'FULL_HISTORY_END') { |
|||
console.log('END'); |
|||
window.clearTimeout(to); |
|||
cb(null, realtime); |
|||
return; |
|||
} |
|||
if (parsed[0] !== 'FULL_HISTORY') { return; } |
|||
if (parsed[1] && parsed[1].validateKey) { // First message
|
|||
secret.keys.validateKey = parsed[1].validateKey; |
|||
return; |
|||
} |
|||
msg = parsed[1][4]; |
|||
if (msg) { |
|||
msg = msg.replace(/^cp\|/, ''); |
|||
var decryptedMsg = crypto.decrypt(msg, secret.keys.validateKey); |
|||
realtime.message(decryptedMsg); |
|||
} |
|||
}; |
|||
|
|||
network.on('message', function (msg) { |
|||
onMsg(msg); |
|||
}); |
|||
|
|||
network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', wcId, secret.keys.validateKey])); |
|||
}; |
|||
|
|||
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 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(); |
|||
|
|||
common.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 () { |
|||
common.confirm(Messages.history_restorePrompt, function (yes) { |
|||
if (!yes) { return; } |
|||
close(); |
|||
onRevert(); |
|||
common.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; |
|||
}); |
|||
|
|||
@ -1,59 +0,0 @@ |
|||
define(function () { |
|||
var module = {}; |
|||
|
|||
module.create = function (UserList, Title, cfg, Cryptpad) { |
|||
var exp = {}; |
|||
|
|||
exp.update = function (shjson) { |
|||
// Extract the user list (metadata) from the hyperjson
|
|||
var json = (!shjson || typeof shjson !== "string") ? "" : JSON.parse(shjson); |
|||
var titleUpdated = false; |
|||
var metadata; |
|||
if (Array.isArray(json)) { |
|||
metadata = json[3] && json[3].metadata; |
|||
} else { |
|||
metadata = json.metadata; |
|||
} |
|||
if (typeof metadata === "object") { |
|||
if (Cryptpad) { |
|||
if (typeof(metadata.type) === 'undefined') { |
|||
// initialize pad type by location.pathname
|
|||
metadata.type = Cryptpad.getAppType(); |
|||
} |
|||
} else { |
|||
console.log("Cryptpad should exist but it does not"); |
|||
} |
|||
if (metadata.users) { |
|||
var userData = metadata.users; |
|||
// Update the local user data
|
|||
UserList.addToUserData(userData); |
|||
} |
|||
if (metadata.defaultTitle) { |
|||
Title.updateDefaultTitle(metadata.defaultTitle); |
|||
} |
|||
if (typeof metadata.title !== "undefined") { |
|||
Title.updateTitle(metadata.title || Title.defaultTitle); |
|||
titleUpdated = true; |
|||
} |
|||
if (metadata.slideOptions && cfg.slideOptions) { |
|||
cfg.slideOptions(metadata.slideOptions); |
|||
} |
|||
if (metadata.color && cfg.slideColors) { |
|||
cfg.slideColors(metadata.color, metadata.backColor); |
|||
} |
|||
if (typeof(metadata.palette) !== 'undefined' && cfg.updatePalette) { |
|||
cfg.updatePalette(metadata.palette); |
|||
} |
|||
} |
|||
if (!titleUpdated) { |
|||
Title.updateTitle(Title.defaultTitle); |
|||
} |
|||
}; |
|||
|
|||
return exp; |
|||
}; |
|||
|
|||
return module; |
|||
}); |
|||
|
|||
|
|||
@ -0,0 +1,29 @@ |
|||
define([ |
|||
'/common/visible.js', |
|||
'/common/notify.js' |
|||
], function (Visible, Notify) { |
|||
var Notifier = {}; |
|||
|
|||
var notify = {}; |
|||
Notifier.unnotify = function () { |
|||
if (notify.tabNotification && |
|||
typeof(notify.tabNotification.cancel) === 'function') { |
|||
notify.tabNotification.cancel(); |
|||
} |
|||
}; |
|||
|
|||
Notifier.notify = function () { |
|||
if (Visible.isSupported() && !Visible.currently()) { |
|||
Notifier.unnotify(); |
|||
notify.tabNotification = Notify.tab(1000, 10); |
|||
} |
|||
}; |
|||
|
|||
if (Visible.isSupported()) { |
|||
Visible.onChange(function (yes) { |
|||
if (yes) { Notifier.unnotify(); } |
|||
}); |
|||
} |
|||
|
|||
return Notifier; |
|||
}); |
|||
@ -1,87 +0,0 @@ |
|||
define(['jquery'], function ($) { |
|||
var module = {}; |
|||
|
|||
module.create = function (cfg, onLocal, Cryptpad) { |
|||
var exp = {}; |
|||
|
|||
var parsed = exp.parsedHref = Cryptpad.parsePadUrl(window.location.href); |
|||
exp.defaultTitle = Cryptpad.getDefaultName(parsed); |
|||
|
|||
exp.title = document.title; // TOOD slides
|
|||
|
|||
cfg = cfg || {}; |
|||
|
|||
var getHeadingText = cfg.getHeadingText || function () { return; }; |
|||
var updateLocalTitle = function (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 { |
|||
return exp.title || getHeadingText() || exp.defaultTitle; |
|||
} |
|||
}; |
|||
|
|||
var renameCb = function (err, newTitle) { |
|||
if (err) { return; } |
|||
updateLocalTitle(newTitle); |
|||
onLocal(); |
|||
}; |
|||
|
|||
// update title: href is optional; if not specified, we use window.location.href
|
|||
exp.updateTitle = function (newTitle, href, cb) { |
|||
cb = cb || $.noop; |
|||
if (newTitle === exp.title) { return; } |
|||
// Change the title now, and set it back to the old value if there is an error
|
|||
var oldTitle = exp.title; |
|||
Cryptpad.renamePad(newTitle, href, function (err, data) { |
|||
if (err) { |
|||
console.log("Couldn't set pad title"); |
|||
console.error(err); |
|||
updateLocalTitle(oldTitle); |
|||
return void cb(err); |
|||
} |
|||
updateLocalTitle(data); |
|||
cb(null, data); |
|||
if (!$title) { return; } |
|||
$title.find('span.title').text(data); |
|||
$title.find('input').val(data); |
|||
}); |
|||
}; |
|||
|
|||
exp.updateDefaultTitle = function (newDefaultTitle) { |
|||
exp.defaultTitle = newDefaultTitle; |
|||
if (!$title) { return; } |
|||
$title.find('input').attr("placeholder", exp.defaultTitle); |
|||
}; |
|||
|
|||
exp.getTitleConfig = function () { |
|||
return { |
|||
onRename: renameCb, |
|||
suggestName: suggestTitle, |
|||
defaultName: exp.defaultTitle |
|||
}; |
|||
}; |
|||
|
|||
return exp; |
|||
}; |
|||
|
|||
return module; |
|||
}); |
|||
|
|||
@ -1,117 +0,0 @@ |
|||
define(['json.sortify'], function (Sortify) { |
|||
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 oldUserData = {}; |
|||
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") { |
|||
// Make sure we don't update the userlist everytime someone makes a change to the pad
|
|||
if (Sortify(oldUserData) === Sortify(userData)) { return; } |
|||
oldUserData = JSON.parse(JSON.stringify(userData)); |
|||
|
|||
userList.onChange(userData); |
|||
} |
|||
}; |
|||
|
|||
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; |
|||
}); |
|||
@ -1,112 +0,0 @@ |
|||
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; |
|||
}); |
|||
1360
www/common/cryptpad-common.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1179
www/common/fileObject.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,93 @@ |
|||
define([ |
|||
'/file/file-crypto.js', |
|||
'/common/common-hash.js', |
|||
'/bower_components/tweetnacl/nacl-fast.min.js', |
|||
], function (FileCrypto, Hash) { |
|||
var Nacl = window.nacl; |
|||
var module = {}; |
|||
|
|||
module.upload = function (file, noStore, common, updateProgress, onComplete, onError, onPending) { |
|||
var u8 = file.blob; // This is not a blob but a uint8array
|
|||
var metadata = file.metadata; |
|||
|
|||
// if it exists, path contains the new pad location in the drive
|
|||
var path = file.path; |
|||
|
|||
var key = Nacl.randomBytes(32); |
|||
var next = FileCrypto.encrypt(u8, metadata, key); |
|||
|
|||
var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata); |
|||
|
|||
var sendChunk = function (box, cb) { |
|||
var enc = Nacl.util.encodeBase64(box); |
|||
common.rpc.send.unauthenticated('UPLOAD', enc, function (e, msg) { |
|||
cb(e, msg); |
|||
}); |
|||
}; |
|||
|
|||
var actual = 0; |
|||
var again = function (err, box) { |
|||
if (err) { throw new Error(err); } |
|||
if (box) { |
|||
actual += box.length; |
|||
var progressValue = (actual / estimate * 100); |
|||
updateProgress(progressValue); |
|||
|
|||
return void sendChunk(box, function (e) { |
|||
if (e) { return console.error(e); } |
|||
next(again); |
|||
}); |
|||
} |
|||
|
|||
if (actual !== estimate) { |
|||
console.error('Estimated size does not match actual size'); |
|||
} |
|||
|
|||
// if not box then done
|
|||
common.uploadComplete(function (e, id) { |
|||
if (e) { return void console.error(e); } |
|||
var uri = ['', 'blob', id.slice(0,2), id].join('/'); |
|||
console.log("encrypted blob is now available as %s", uri); |
|||
|
|||
var b64Key = Nacl.util.encodeBase64(key); |
|||
|
|||
var hash = Hash.getFileHashFromKeys(id, b64Key); |
|||
var href = '/file/#' + hash; |
|||
|
|||
var title = metadata.name; |
|||
|
|||
if (noStore) { return void onComplete(href); } |
|||
|
|||
common.initialPath = path; |
|||
common.renamePad(title || "", href, function (err) { |
|||
if (err) { return void console.error(err); } |
|||
onComplete(href); |
|||
common.setPadAttribute('fileType', metadata.type, null, href); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
common.uploadStatus(estimate, function (e, pending) { |
|||
if (e) { |
|||
console.error(e); |
|||
onError(e); |
|||
return; |
|||
} |
|||
|
|||
if (pending) { |
|||
return void onPending(function () { |
|||
// if the user wants to cancel the pending upload to execute that one
|
|||
common.uploadCancel(function (e, res) { |
|||
if (e) { |
|||
return void console.error(e); |
|||
} |
|||
console.log(res); |
|||
next(again); |
|||
}); |
|||
}); |
|||
} |
|||
next(again); |
|||
}); |
|||
}; |
|||
return module; |
|||
}); |
|||
@ -1,677 +0,0 @@ |
|||
define([ |
|||
'jquery', |
|||
'/bower_components/chainpad-crypto/crypto.js', |
|||
'/common/curve.js', |
|||
'/common/common-hash.js', |
|||
], function ($, Crypto, Curve, Hash) { |
|||
'use strict'; |
|||
var Msg = { |
|||
inputs: [], |
|||
}; |
|||
|
|||
var Types = { |
|||
message: 'MSG', |
|||
update: 'UPDATE', |
|||
unfriend: 'UNFRIEND', |
|||
mapId: 'MAP_ID', |
|||
mapIdAck: 'MAP_ID_ACK' |
|||
}; |
|||
|
|||
var clone = function (o) { |
|||
return JSON.parse(JSON.stringify(o)); |
|||
}; |
|||
|
|||
// TODO
|
|||
// - mute a channel (hide notifications or don't open it?)
|
|||
var createData = Msg.createData = function (proxy, hash) { |
|||
return { |
|||
channel: hash || Hash.createChannelId(), |
|||
displayName: proxy['cryptpad.username'], |
|||
profile: proxy.profile && proxy.profile.view, |
|||
edPublic: proxy.edPublic, |
|||
curvePublic: proxy.curvePublic, |
|||
avatar: proxy.profile && proxy.profile.avatar |
|||
}; |
|||
}; |
|||
|
|||
var getFriend = function (proxy, pubkey) { |
|||
if (pubkey === proxy.curvePublic) { |
|||
var data = createData(proxy); |
|||
delete data.channel; |
|||
return data; |
|||
} |
|||
return proxy.friends ? proxy.friends[pubkey] : undefined; |
|||
}; |
|||
|
|||
var getFriendList = Msg.getFriendList = function (proxy) { |
|||
if (!proxy.friends) { proxy.friends = {}; } |
|||
return proxy.friends; |
|||
}; |
|||
|
|||
var eachFriend = function (friends, cb) { |
|||
Object.keys(friends).forEach(function (id) { |
|||
if (id === 'me') { return; } |
|||
cb(friends[id], id, friends); |
|||
}); |
|||
}; |
|||
|
|||
Msg.getFriendChannelsList = function (proxy) { |
|||
var list = []; |
|||
eachFriend(proxy, function (friend) { |
|||
list.push(friend.channel); |
|||
}); |
|||
return list; |
|||
}; |
|||
|
|||
var msgAlreadyKnown = function (channel, sig) { |
|||
return channel.messages.some(function (message) { |
|||
return message[0] === sig; |
|||
}); |
|||
}; |
|||
|
|||
Msg.messenger = function (common) { |
|||
var messenger = { |
|||
handlers: { |
|||
message: [], |
|||
join: [], |
|||
leave: [], |
|||
update: [], |
|||
friend: [], |
|||
unfriend: [], |
|||
}, |
|||
range_requests: {}, |
|||
}; |
|||
|
|||
var eachHandler = function (type, g) { |
|||
messenger.handlers[type].forEach(g); |
|||
}; |
|||
|
|||
messenger.on = function (type, f) { |
|||
var stack = messenger.handlers[type]; |
|||
if (!Array.isArray(stack)) { |
|||
return void console.error('unsupported message type'); |
|||
} |
|||
if (typeof(f) !== 'function') { |
|||
return void console.error('expected function'); |
|||
} |
|||
stack.push(f); |
|||
}; |
|||
|
|||
var channels = messenger.channels = {}; |
|||
|
|||
var joining = {}; |
|||
|
|||
// declare common variables
|
|||
var network = common.getNetwork(); |
|||
var proxy = common.getProxy(); |
|||
var realtime = common.getRealtime(); |
|||
Msg.hk = network.historyKeeper; |
|||
var friends = getFriendList(proxy); |
|||
|
|||
var getChannel = function (curvePublic) { |
|||
var friend = friends[curvePublic]; |
|||
if (!friend) { return; } |
|||
var chanId = friend.channel; |
|||
if (!chanId) { return; } |
|||
return channels[chanId]; |
|||
}; |
|||
|
|||
var initRangeRequest = function (txid, curvePublic, sig, cb) { |
|||
messenger.range_requests[txid] = { |
|||
messages: [], |
|||
cb: cb, |
|||
curvePublic: curvePublic, |
|||
sig: sig, |
|||
}; |
|||
}; |
|||
|
|||
var getRangeRequest = function (txid) { |
|||
return messenger.range_requests[txid]; |
|||
}; |
|||
|
|||
var deleteRangeRequest = function (txid) { |
|||
delete messenger.range_requests[txid]; |
|||
}; |
|||
|
|||
messenger.getMoreHistory = function (curvePublic, hash, count, cb) { |
|||
if (typeof(cb) !== 'function') { return; } |
|||
|
|||
if (typeof(hash) !== 'string') { |
|||
// FIXME hash is not necessarily defined.
|
|||
// What does this mean?
|
|||
console.error("not sure what to do here"); |
|||
return; |
|||
} |
|||
|
|||
var chan = getChannel(curvePublic); |
|||
if (typeof(chan) === 'undefined') { |
|||
console.error("chan is undefined. we're going to have a problem here"); |
|||
return; |
|||
} |
|||
|
|||
var txid = common.uid(); |
|||
initRangeRequest(txid, curvePublic, hash, cb); |
|||
var msg = [ 'GET_HISTORY_RANGE', chan.id, { |
|||
from: hash, |
|||
count: count, |
|||
txid: txid, |
|||
} |
|||
]; |
|||
|
|||
network.sendto(network.historyKeeper, JSON.stringify(msg)).then(function () { |
|||
}, function (err) { |
|||
throw new Error(err); |
|||
}); |
|||
}; |
|||
|
|||
var getCurveForChannel = function (id) { |
|||
var channel = channels[id]; |
|||
if (!channel) { return; } |
|||
return channel.curve; |
|||
}; |
|||
|
|||
messenger.getChannelHead = function (curvePublic, cb) { |
|||
var friend = friends[curvePublic]; |
|||
if (!friend) { return void cb('NO_SUCH_FRIEND'); } |
|||
cb(void 0, friend.lastKnownHash); |
|||
}; |
|||
|
|||
messenger.setChannelHead = function (curvePublic, hash, cb) { |
|||
var friend = friends[curvePublic]; |
|||
if (!friend) { return void cb('NO_SUCH_FRIEND'); } |
|||
friend.lastKnownHash = hash; |
|||
cb(); |
|||
}; |
|||
|
|||
// Id message allows us to map a netfluxId with a public curve key
|
|||
var onIdMessage = function (msg, sender) { |
|||
var channel; |
|||
var isId = Object.keys(channels).some(function (chanId) { |
|||
if (channels[chanId].userList.indexOf(sender) !== -1) { |
|||
channel = channels[chanId]; |
|||
return true; |
|||
} |
|||
}); |
|||
|
|||
if (!isId) { return; } |
|||
|
|||
var decryptedMsg = channel.encryptor.decrypt(msg); |
|||
|
|||
if (decryptedMsg === null) { |
|||
return void console.error("Failed to decrypt message"); |
|||
} |
|||
|
|||
if (!decryptedMsg) { |
|||
console.error('decrypted message was falsey but not null'); |
|||
return; |
|||
} |
|||
|
|||
var parsed; |
|||
try { |
|||
parsed = JSON.parse(decryptedMsg); |
|||
} catch (e) { |
|||
console.error(decryptedMsg); |
|||
return; |
|||
} |
|||
if (parsed[0] !== Types.mapId && parsed[0] !== Types.mapIdAck) { return; } |
|||
|
|||
// check that the responding peer's encrypted netflux id matches
|
|||
// the sender field. This is to prevent replay attacks.
|
|||
if (parsed[2] !== sender || !parsed[1]) { return; } |
|||
channel.mapId[sender] = parsed[1]; |
|||
eachHandler('join', function (f) { |
|||
f(parsed[1], channel.id); |
|||
}); |
|||
|
|||
if (parsed[0] !== Types.mapId) { return; } // Don't send your key if it's already an ACK
|
|||
// Answer with your own key
|
|||
var rMsg = [Types.mapIdAck, proxy.curvePublic, channel.wc.myID]; |
|||
var rMsgStr = JSON.stringify(rMsg); |
|||
var cryptMsg = channel.encryptor.encrypt(rMsgStr); |
|||
network.sendto(sender, cryptMsg); |
|||
}; |
|||
|
|||
var orderMessages = function (curvePublic, new_messages /*, sig */) { |
|||
var channel = getChannel(curvePublic); |
|||
var messages = channel.messages; |
|||
|
|||
// TODO improve performance, guarantee correct ordering
|
|||
new_messages.reverse().forEach(function (msg) { |
|||
messages.unshift(msg); |
|||
}); |
|||
}; |
|||
|
|||
var removeFromFriendList = function (curvePublic, cb) { |
|||
if (!proxy.friends) { return; } |
|||
var friends = proxy.friends; |
|||
delete friends[curvePublic]; |
|||
common.whenRealtimeSyncs(realtime, cb); |
|||
}; |
|||
|
|||
var pushMsg = function (channel, cryptMsg) { |
|||
var msg = channel.encryptor.decrypt(cryptMsg); |
|||
var sig = cryptMsg.slice(0, 64); |
|||
if (msgAlreadyKnown(channel, sig)) { return; } |
|||
|
|||
var parsedMsg = JSON.parse(msg); |
|||
var curvePublic; |
|||
if (parsedMsg[0] === Types.message) { |
|||
// TODO validate messages here
|
|||
var res = { |
|||
type: parsedMsg[0], |
|||
sig: sig, |
|||
author: parsedMsg[1], |
|||
time: parsedMsg[2], |
|||
text: parsedMsg[3], |
|||
// this makes debugging a whole lot easier
|
|||
curve: getCurveForChannel(channel.id), |
|||
}; |
|||
|
|||
channel.messages.push(res); |
|||
eachHandler('message', function (f) { |
|||
f(res); |
|||
}); |
|||
|
|||
return true; |
|||
} |
|||
if (parsedMsg[0] === Types.update) { |
|||
if (parsedMsg[1] === proxy.curvePublic) { return; } |
|||
curvePublic = parsedMsg[1]; |
|||
var newdata = parsedMsg[3]; |
|||
var data = getFriend(proxy, parsedMsg[1]); |
|||
var types = []; |
|||
Object.keys(newdata).forEach(function (k) { |
|||
if (data[k] !== newdata[k]) { |
|||
types.push(k); |
|||
data[k] = newdata[k]; |
|||
} |
|||
}); |
|||
|
|||
eachHandler('update', function (f) { |
|||
f(clone(newdata), curvePublic); |
|||
}); |
|||
return; |
|||
} |
|||
if (parsedMsg[0] === Types.unfriend) { |
|||
curvePublic = parsedMsg[1]; |
|||
delete friends[curvePublic]; |
|||
|
|||
removeFromFriendList(parsedMsg[1], function () { |
|||
channel.wc.leave(Types.unfriend); |
|||
eachHandler('unfriend', function (f) { |
|||
f(curvePublic); |
|||
}); |
|||
}); |
|||
return; |
|||
} |
|||
}; |
|||
|
|||
/* Broadcast a display name, profile, or avatar change to all contacts |
|||
*/ |
|||
|
|||
// TODO send event...
|
|||
messenger.updateMyData = function () { |
|||
var friends = getFriendList(proxy); |
|||
var mySyncData = friends.me; |
|||
var myData = createData(proxy); |
|||
if (!mySyncData || mySyncData.displayName !== myData.displayName |
|||
|| mySyncData.profile !== myData.profile |
|||
|| mySyncData.avatar !== myData.avatar) { |
|||
delete myData.channel; |
|||
Object.keys(channels).forEach(function (chan) { |
|||
var channel = channels[chan]; |
|||
|
|||
if (!channel) { |
|||
return void console.error('NO_SUCH_CHANNEL'); |
|||
} |
|||
|
|||
|
|||
var msg = [Types.update, myData.curvePublic, +new Date(), myData]; |
|||
var msgStr = JSON.stringify(msg); |
|||
var cryptMsg = channel.encryptor.encrypt(msgStr); |
|||
channel.wc.bcast(cryptMsg).then(function () { |
|||
// TODO send event
|
|||
//channel.refresh();
|
|||
}, function (err) { |
|||
console.error(err); |
|||
}); |
|||
}); |
|||
eachHandler('update', function (f) { |
|||
f(myData, myData.curvePublic); |
|||
}); |
|||
friends.me = myData; |
|||
} |
|||
}; |
|||
|
|||
var onChannelReady = function (chanId) { |
|||
var cb = joining[chanId]; |
|||
if (typeof(cb) !== 'function') { |
|||
return void console.error('channel ready without callback'); |
|||
} |
|||
delete joining[chanId]; |
|||
return cb(); |
|||
}; |
|||
|
|||
var onDirectMessage = function (common, msg, sender) { |
|||
if (sender !== Msg.hk) { return void onIdMessage(msg, sender); } |
|||
var parsed = JSON.parse(msg); |
|||
|
|||
if (/HISTORY_RANGE/.test(parsed[0])) { |
|||
//console.log(parsed);
|
|||
var txid = parsed[1]; |
|||
var req = getRangeRequest(txid); |
|||
var type = parsed[0]; |
|||
if (!req) { |
|||
return void console.error("received response to unknown request"); |
|||
} |
|||
|
|||
if (type === 'HISTORY_RANGE') { |
|||
req.messages.push(parsed[2]); |
|||
} else if (type === 'HISTORY_RANGE_END') { |
|||
// process all the messages (decrypt)
|
|||
var curvePublic = req.curvePublic; |
|||
var channel = getChannel(curvePublic); |
|||
|
|||
var decrypted = req.messages.map(function (msg) { |
|||
if (msg[2] !== 'MSG') { return; } |
|||
try { |
|||
return { |
|||
d: JSON.parse(channel.encryptor.decrypt(msg[4])), |
|||
sig: msg[4].slice(0, 64), |
|||
}; |
|||
} catch (e) { |
|||
console.log('failed to decrypt'); |
|||
return null; |
|||
} |
|||
}).filter(function (decrypted) { |
|||
return decrypted; |
|||
}).map(function (O) { |
|||
return { |
|||
type: O.d[0], |
|||
sig: O.sig, |
|||
author: O.d[1], |
|||
time: O.d[2], |
|||
text: O.d[3], |
|||
curve: curvePublic, |
|||
}; |
|||
}); |
|||
|
|||
orderMessages(curvePublic, decrypted, req.sig); |
|||
req.cb(void 0, decrypted); |
|||
return deleteRangeRequest(txid); |
|||
} else { |
|||
console.log(parsed); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
if ((parsed.validateKey || parsed.owners) && parsed.channel) { |
|||
return; |
|||
} |
|||
if (parsed.state && parsed.state === 1 && parsed.channel) { |
|||
if (channels[parsed.channel]) { |
|||
// parsed.channel is Ready
|
|||
// channel[parsed.channel].ready();
|
|||
channels[parsed.channel].ready = true; |
|||
onChannelReady(parsed.channel); |
|||
var updateTypes = channels[parsed.channel].updateOnReady; |
|||
if (updateTypes) { |
|||
|
|||
//channels[parsed.channel].updateUI(updateTypes);
|
|||
} |
|||
} |
|||
return; |
|||
} |
|||
var chan = parsed[3]; |
|||
if (!chan || !channels[chan]) { return; } |
|||
pushMsg(channels[chan], parsed[4]); |
|||
}; |
|||
|
|||
var onMessage = function (msg, sender, chan) { |
|||
if (!channels[chan.id]) { return; } |
|||
|
|||
var isMessage = pushMsg(channels[chan.id], msg); |
|||
if (isMessage) { |
|||
if (channels[chan.id].wc.myID !== sender) { |
|||
// Don't notify for your own messages
|
|||
//channels[chan.id].notify();
|
|||
} |
|||
//channels[chan.id].refresh();
|
|||
// TODO emit message event
|
|||
} |
|||
}; |
|||
|
|||
// listen for messages...
|
|||
network.on('message', function(msg, sender) { |
|||
onDirectMessage(common, msg, sender); |
|||
}); |
|||
|
|||
messenger.removeFriend = function (curvePublic, cb) { |
|||
if (typeof(cb) !== 'function') { throw new Error('NO_CALLBACK'); } |
|||
var data = getFriend(proxy, curvePublic); |
|||
|
|||
if (!data) { |
|||
// friend is not valid
|
|||
console.error('friend is not valid'); |
|||
return; |
|||
} |
|||
|
|||
var channel = channels[data.channel]; |
|||
if (!channel) { |
|||
return void cb("NO_SUCH_CHANNEL"); |
|||
} |
|||
|
|||
if (!network.webChannels.some(function (wc) { |
|||
return wc.id === channel.id; |
|||
})) { |
|||
console.error('bad channel: ', curvePublic); |
|||
} |
|||
|
|||
var msg = [Types.unfriend, proxy.curvePublic, +new Date()]; |
|||
var msgStr = JSON.stringify(msg); |
|||
var cryptMsg = channel.encryptor.encrypt(msgStr); |
|||
|
|||
// TODO emit remove_friend event?
|
|||
try { |
|||
channel.wc.bcast(cryptMsg).then(function () { |
|||
delete friends[curvePublic]; |
|||
delete channels[curvePublic]; |
|||
common.whenRealtimeSyncs(realtime, function () { |
|||
cb(); |
|||
}); |
|||
}, function (err) { |
|||
console.error(err); |
|||
cb(err); |
|||
}); |
|||
} catch (e) { |
|||
cb(e); |
|||
} |
|||
}; |
|||
|
|||
var getChannelMessagesSince = function (chan, data, keys) { |
|||
console.log('Fetching [%s] messages since [%s]', data.curvePublic, data.lastKnownHash || ''); |
|||
var cfg = { |
|||
validateKey: keys.validateKey, |
|||
owners: [proxy.edPublic, data.edPublic], |
|||
lastKnownHash: data.lastKnownHash |
|||
}; |
|||
var msg = ['GET_HISTORY', chan.id, cfg]; |
|||
network.sendto(network.historyKeeper, JSON.stringify(msg)) |
|||
.then($.noop, function (err) { |
|||
throw new Error(err); |
|||
}); |
|||
}; |
|||
|
|||
var openFriendChannel = function (data, f) { |
|||
var keys = Curve.deriveKeys(data.curvePublic, proxy.curvePrivate); |
|||
var encryptor = Curve.createEncryptor(keys); |
|||
network.join(data.channel).then(function (chan) { |
|||
var channel = channels[data.channel] = { |
|||
id: data.channel, |
|||
sending: false, |
|||
friendEd: f, |
|||
keys: keys, |
|||
curve: data.curvePublic, |
|||
encryptor: encryptor, |
|||
messages: [], |
|||
wc: chan, |
|||
userList: [], |
|||
mapId: {}, |
|||
send: function (payload, cb) { |
|||
if (!network.webChannels.some(function (wc) { |
|||
if (wc.id === channel.wc.id) { return true; } |
|||
})) { |
|||
return void cb('NO_SUCH_CHANNEL'); |
|||
} |
|||
|
|||
var msg = [Types.message, proxy.curvePublic, +new Date(), payload]; |
|||
var msgStr = JSON.stringify(msg); |
|||
var cryptMsg = channel.encryptor.encrypt(msgStr); |
|||
|
|||
channel.wc.bcast(cryptMsg).then(function () { |
|||
pushMsg(channel, cryptMsg); |
|||
cb(); |
|||
}, function (err) { |
|||
cb(err); |
|||
}); |
|||
} |
|||
}; |
|||
chan.on('message', function (msg, sender) { |
|||
onMessage(msg, sender, chan); |
|||
}); |
|||
|
|||
var onJoining = function (peer) { |
|||
if (peer === Msg.hk) { return; } |
|||
if (channel.userList.indexOf(peer) !== -1) { return; } |
|||
|
|||
channel.userList.push(peer); |
|||
var msg = [Types.mapId, proxy.curvePublic, chan.myID]; |
|||
var msgStr = JSON.stringify(msg); |
|||
var cryptMsg = channel.encryptor.encrypt(msgStr); |
|||
network.sendto(peer, cryptMsg); |
|||
}; |
|||
chan.members.forEach(function (peer) { |
|||
if (peer === Msg.hk) { return; } |
|||
if (channel.userList.indexOf(peer) !== -1) { return; } |
|||
channel.userList.push(peer); |
|||
}); |
|||
chan.on('join', onJoining); |
|||
chan.on('leave', function (peer) { |
|||
var curvePublic = channel.mapId[peer]; |
|||
var i = channel.userList.indexOf(peer); |
|||
while (i !== -1) { |
|||
channel.userList.splice(i, 1); |
|||
i = channel.userList.indexOf(peer); |
|||
} |
|||
// update status
|
|||
if (!curvePublic) { return; } |
|||
eachHandler('leave', function (f) { |
|||
f(curvePublic, channel.id); |
|||
}); |
|||
}); |
|||
|
|||
// FIXME don't subscribe to the channel implicitly
|
|||
getChannelMessagesSince(chan, data, keys); |
|||
}, function (err) { |
|||
console.error(err); |
|||
}); |
|||
}; |
|||
|
|||
messenger.getFriendList = function (cb) { |
|||
var friends = proxy.friends; |
|||
if (!friends) { return void cb(void 0, []); } |
|||
|
|||
cb(void 0, Object.keys(proxy.friends).filter(function (k) { |
|||
return k !== 'me'; |
|||
})); |
|||
}; |
|||
|
|||
messenger.openFriendChannel = function (curvePublic, cb) { |
|||
if (typeof(curvePublic) !== 'string') { return void cb('INVALID_ID'); } |
|||
if (typeof(cb) !== 'function') { throw new Error('expected callback'); } |
|||
|
|||
var friend = clone(friends[curvePublic]); |
|||
if (typeof(friend) !== 'object') { |
|||
return void cb('NO_FRIEND_DATA'); |
|||
} |
|||
var channel = friend.channel; |
|||
if (!channel) { return void cb('E_NO_CHANNEL'); } |
|||
joining[channel] = cb; |
|||
openFriendChannel(friend, curvePublic); |
|||
}; |
|||
|
|||
messenger.sendMessage = function (curvePublic, payload, cb) { |
|||
var channel = getChannel(curvePublic); |
|||
if (!channel) { return void cb('NO_CHANNEL'); } |
|||
if (!network.webChannels.some(function (wc) { |
|||
if (wc.id === channel.wc.id) { return true; } |
|||
})) { |
|||
return void cb('NO_SUCH_CHANNEL'); |
|||
} |
|||
|
|||
var msg = [Types.message, proxy.curvePublic, +new Date(), payload]; |
|||
var msgStr = JSON.stringify(msg); |
|||
var cryptMsg = channel.encryptor.encrypt(msgStr); |
|||
|
|||
channel.wc.bcast(cryptMsg).then(function () { |
|||
pushMsg(channel, cryptMsg); |
|||
cb(); |
|||
}, function (err) { |
|||
cb(err); |
|||
}); |
|||
}; |
|||
|
|||
messenger.getStatus = function (curvePublic, cb) { |
|||
var channel = getChannel(curvePublic); |
|||
if (!channel) { return void cb('NO_SUCH_CHANNEL'); } |
|||
var online = channel.userList.some(function (nId) { |
|||
return channel.mapId[nId] === curvePublic; |
|||
}); |
|||
cb(void 0, online); |
|||
}; |
|||
|
|||
messenger.getFriendInfo = function (curvePublic, cb) { |
|||
setTimeout(function () { |
|||
var friend = friends[curvePublic]; |
|||
if (!friend) { return void cb('NO_SUCH_FRIEND'); } |
|||
// this clone will be redundant when ui uses postmessage
|
|||
cb(void 0, clone(friend)); |
|||
}); |
|||
}; |
|||
|
|||
messenger.getMyInfo = function (cb) { |
|||
cb(void 0, { |
|||
curvePublic: proxy.curvePublic, |
|||
displayName: common.getDisplayName(), |
|||
}); |
|||
}; |
|||
|
|||
// TODO listen for changes to your friend list
|
|||
// emit 'update' events for clients
|
|||
|
|||
//var update = function (curvePublic
|
|||
proxy.on('change', ['friends'], function (o, n, p) { |
|||
var curvePublic; |
|||
if (o === undefined) { |
|||
// new friend added
|
|||
curvePublic = p.slice(-1)[0]; |
|||
eachHandler('friend', function (f) { |
|||
f(curvePublic, clone(n)); |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
console.error(o, n, p); |
|||
}).on('remove', ['friends'], function (o, p) { |
|||
eachHandler('unfriend', function (f) { |
|||
f(p[1]); // TODO
|
|||
}); |
|||
}); |
|||
|
|||
Object.freeze(messenger); |
|||
|
|||
return messenger; |
|||
}; |
|||
|
|||
return Msg; |
|||
}); |
|||
@ -1,865 +0,0 @@ |
|||
define([ |
|||
'jquery', |
|||
'/customize/application_config.js', |
|||
'/api/config' |
|||
], function ($, Config, ApiConfig) { |
|||
|
|||
var urlArgs = ApiConfig.requireConf.urlArgs; |
|||
|
|||
var Messages = {}; |
|||
|
|||
var Bar = { |
|||
constants: {}, |
|||
}; |
|||
|
|||
/** Id of the div containing the user list. */ |
|||
var USER_LIST_CLS = Bar.constants.userlist = 'cryptpad-user-list'; |
|||
|
|||
/** Id of the div containing the lag info. */ |
|||
var LAG_ELEM_CLS = Bar.constants.lag = 'cryptpad-lag'; |
|||
|
|||
var LIMIT_ELEM_CLS = Bar.constants.lag = 'cryptpad-limit'; |
|||
|
|||
/** The toolbar class which contains the user list, debug link and lag. */ |
|||
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 HISTORY_CLS = Bar.constants.history = 'cryptpad-toolbar-history'; |
|||
|
|||
var SPINNER_CLS = Bar.constants.spinner = 'cryptpad-spinner'; |
|||
|
|||
var STATE_CLS = Bar.constants.state = 'cryptpad-state'; |
|||
|
|||
var USERNAME_CLS = Bar.constants.username = 'cryptpad-toolbar-username'; |
|||
|
|||
var READONLY_CLS = Bar.constants.readonly = 'cryptpad-readonly'; |
|||
|
|||
var USERBUTTONS_CONTAINER_CLS = Bar.constants.userButtonsContainer = "cryptpad-userbuttons-container"; |
|||
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"; |
|||
var DROPDOWN_CONTAINER_CLS = Bar.constants.dropdownContainer = "cryptpad-dropdown-container"; |
|||
var DROPDOWN_CLS = Bar.constants.dropdown = "cryptpad-dropdown"; |
|||
var TITLE_CLS = Bar.constants.title = "cryptpad-title"; |
|||
var USER_CLS = Bar.constants.userAdmin = "cryptpad-user"; |
|||
var USERBUTTON_CLS = Bar.constants.changeUsername = "cryptpad-change-username"; |
|||
|
|||
var SPINNER_DISAPPEAR_TIME = 3000; |
|||
|
|||
var uid = function () { |
|||
return 'cryptpad-uid-' + String(Math.random()).substring(2); |
|||
}; |
|||
|
|||
var $style; |
|||
|
|||
var connected = false; |
|||
var firstConnection = true; |
|||
var lagErrors = 0; |
|||
|
|||
var styleToolbar = function ($container, href, version) { |
|||
href = href || '/customize/toolbar.css' + (version?('?' + version): ''); |
|||
|
|||
$.ajax({ |
|||
url: href, |
|||
dataType: 'text', |
|||
success: function (data) { |
|||
$container.append($('<style>').text(data)); |
|||
}, |
|||
}); |
|||
}; |
|||
|
|||
var createRealtimeToolbar = function ($container, config) { |
|||
var $toolbar = $('<div>', { |
|||
'class': TOOLBAR_CLS, |
|||
id: uid(), |
|||
}) |
|||
.append($('<div>', {'class': TOP_CLS})) |
|||
.append($('<div>', {'class': LEFTSIDE_CLS})) |
|||
.append($('<div>', {'class': RIGHTSIDE_CLS})) |
|||
.append($('<div>', {'class': HISTORY_CLS})); |
|||
|
|||
// The 'notitle' class removes the line added for the title with a small screen
|
|||
if (!config || typeof config !== "object") { |
|||
$toolbar.addClass('notitle'); |
|||
} |
|||
|
|||
$container.prepend($toolbar); |
|||
|
|||
if (ApiConfig && ApiConfig.requireConf && ApiConfig.requireConf.urlArgs) { |
|||
styleToolbar($container, undefined, ApiConfig.requireConf.urlArgs); |
|||
} else { |
|||
styleToolbar($container); |
|||
} |
|||
return $toolbar; |
|||
}; |
|||
|
|||
var createSpinner = function ($container, config) { |
|||
if (config.displayed.indexOf('spinner') !== -1) { |
|||
var $spin = $('<span>', {'class':SPINNER_CLS}); |
|||
var $spinner = $('<span>', { |
|||
id: uid(), |
|||
'class': 'spin fa fa-spinner fa-pulse', |
|||
}).appendTo($spin).hide(); |
|||
$('<span>', { |
|||
id: uid(), |
|||
'class': 'synced fa fa-check', |
|||
title: Messages.synced |
|||
}).appendTo($spin); |
|||
$container.prepend($spin); |
|||
return $spin[0]; |
|||
} |
|||
}; |
|||
|
|||
var kickSpinner = function (Cryptpad, realtime, local, spinnerElement) { |
|||
if (!spinnerElement) { return; } |
|||
$(spinnerElement).find('.spin').show(); |
|||
$(spinnerElement).find('.synced').hide(); |
|||
var onSynced = function () { |
|||
if (spinnerElement.timeout) { clearTimeout(spinnerElement.timeout); } |
|||
spinnerElement.timeout = setTimeout(function () { |
|||
$(spinnerElement).find('.spin').hide(); |
|||
$(spinnerElement).find('.synced').show(); |
|||
}, local ? 0 : SPINNER_DISAPPEAR_TIME); |
|||
}; |
|||
if (Cryptpad) { |
|||
Cryptpad.whenRealtimeSyncs(realtime, onSynced); |
|||
return; |
|||
} |
|||
onSynced(); |
|||
}; |
|||
|
|||
var createUserButtons = function ($userlistElement, config, readOnly, Cryptpad) { |
|||
// User list button
|
|||
if (config.displayed.indexOf('userlist') !== -1) { |
|||
if (!config.userData) { |
|||
throw new Error("You must provide a `userData` object to display the userlist"); |
|||
} |
|||
var dropdownConfig = { |
|||
options: [{ |
|||
tag: 'p', |
|||
attributes: {'class': USERLIST_CLS}, |
|||
}] // Entries displayed in the menu
|
|||
}; |
|||
var $block = Cryptpad.createDropdown(dropdownConfig); |
|||
$block.attr('id', 'userButtons'); |
|||
$userlistElement.append($block); |
|||
} |
|||
|
|||
// Share button
|
|||
if (config.displayed.indexOf('share') !== -1) { |
|||
var secret = Cryptpad.find(config, ['share', 'secret']); |
|||
var channel = Cryptpad.find(config, ['share', 'channel']); |
|||
if (!secret || !channel) { |
|||
throw new Error("Unable to display the share button: share.secret and share.channel required"); |
|||
} |
|||
Cryptpad.getRecentPads(function (err, recent) { |
|||
var $shareIcon = $('<span>', {'class': 'fa fa-share-alt'}); |
|||
var $span = $('<span>', {'class': 'large'}).append(' ' +Messages.shareButton); |
|||
var hashes = Cryptpad.getHashes(channel, secret); |
|||
var options = []; |
|||
|
|||
// If we have a stronger version in drive, add it and add a redirect button
|
|||
var stronger = recent && Cryptpad.findStronger(null, recent); |
|||
if (stronger) { |
|||
var parsed = Cryptpad.parsePadUrl(stronger); |
|||
hashes.editHash = parsed.hash; |
|||
} |
|||
|
|||
if (hashes.editHash) { |
|||
options.push({ |
|||
tag: 'a', |
|||
attributes: {title: Messages.editShareTitle, 'class': 'editShare'}, |
|||
content: '<span class="fa fa-users"></span> ' + Messages.editShare |
|||
}); |
|||
if (stronger) { |
|||
// We're in view mode, display the "open editing link" button
|
|||
options.push({ |
|||
tag: 'a', |
|||
attributes: { |
|||
title: Messages.editOpenTitle, |
|||
'class': 'editOpen', |
|||
href: window.location.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 (hashes.editHash && !stronger) { |
|||
// We're in edit mode, display the "open readonly" button
|
|||
options.push({ |
|||
tag: 'a', |
|||
attributes: { |
|||
title: Messages.viewOpenTitle, |
|||
'class': 'viewOpen', |
|||
href: window.location.pathname + '#' + hashes.viewHash, |
|||
target: '_blank' |
|||
}, |
|||
content: '<span class="fa fa-eye"></span> ' + Messages.viewOpen |
|||
}); |
|||
} |
|||
} |
|||
if (hashes.fileHash) { |
|||
options.push({ |
|||
tag: 'a', |
|||
attributes: {title: Messages.viewShareTitle, 'class': 'fileShare'}, |
|||
content: '<span class="fa fa-eye"></span> ' + Messages.viewShare |
|||
}); |
|||
} |
|||
var dropdownConfigShare = { |
|||
text: $('<div>').append($shareIcon).append($span).html(), |
|||
options: options |
|||
}; |
|||
var $shareBlock = Cryptpad.createDropdown(dropdownConfigShare); |
|||
$shareBlock.find('button').attr('id', 'shareButton'); |
|||
$shareBlock.find('.dropdown-bar-content').addClass(SHARE_CLS).addClass(EDITSHARE_CLS).addClass(VIEWSHARE_CLS); |
|||
$userlistElement.append($shareBlock); |
|||
|
|||
if (hashes.editHash) { |
|||
$shareBlock.find('a.editShare').click(function () { |
|||
var url = window.location.origin + window.location.pathname + '#' + hashes.editHash; |
|||
var success = Cryptpad.Clipboard.copy(url); |
|||
if (success) { Cryptpad.log(Messages.shareSuccess); } |
|||
}); |
|||
} |
|||
if (hashes.viewHash) { |
|||
$shareBlock.find('a.viewShare').click(function () { |
|||
var url = window.location.origin + window.location.pathname + '#' + hashes.viewHash ; |
|||
var success = Cryptpad.Clipboard.copy(url); |
|||
if (success) { Cryptpad.log(Messages.shareSuccess); } |
|||
}); |
|||
} |
|||
if (hashes.fileHash) { |
|||
$shareBlock.find('a.fileShare').click(function () { |
|||
var url = window.location.origin + window.location.pathname + '#' + hashes.fileHash ; |
|||
var success = Cryptpad.Clipboard.copy(url); |
|||
if (success) { Cryptpad.log(Messages.shareSuccess); } |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
var createUserList = function ($container, config, readOnly, Cryptpad) { |
|||
if (config.displayed.indexOf('userlist') === -1 && config.displayed.indexOf('share') === -1) { return; } |
|||
var $userlist = $('<div>', { |
|||
'class': USER_LIST_CLS, |
|||
id: uid(), |
|||
}); |
|||
createUserButtons($userlist, config, readOnly, Cryptpad); |
|||
$container.append($userlist); |
|||
return $userlist[0]; |
|||
}; |
|||
|
|||
var getOtherUsers = function(myUserName, userList, userData) { |
|||
var i = 0; // duplicates counter
|
|||
var list = []; |
|||
var myUid = userData[myUserName] ? userData[myUserName].uid : undefined; |
|||
var uids = []; |
|||
userList.forEach(function(user) { |
|||
if(user !== myUserName) { |
|||
var data = (userData) ? (userData[user] || null) : null; |
|||
var userName = (data) ? data.name : null; |
|||
var userId = (data) ? data.uid : null; |
|||
if (userName && uids.indexOf(userId) === -1 && (!myUid || userId !== myUid)) { |
|||
uids.push(userId); |
|||
list.push(userName); |
|||
} else { i++; } |
|||
} |
|||
}); |
|||
return { |
|||
list: list, |
|||
duplicates: i |
|||
}; |
|||
}; |
|||
|
|||
var arrayIntersect = function(a, b) { |
|||
return $.grep(a, function(i) { |
|||
return $.inArray(i, b) > -1; |
|||
}); |
|||
}; |
|||
|
|||
var getViewers = function (n) { |
|||
if (!n || !parseInt(n) || n === 0) { return ''; } |
|||
if (n === 1) { return '; + ' + Messages.oneViewer; } |
|||
return '; + ' + Messages._getKey('viewers', [n]); |
|||
}; |
|||
|
|||
var checkSynchronizing = function (userList, myUserName, $stateElement) { |
|||
var meIdx = userList.indexOf(myUserName); |
|||
if (meIdx === -1) { |
|||
$stateElement.text(Messages.synchronizing); |
|||
return; |
|||
} |
|||
$stateElement.text(''); |
|||
}; |
|||
|
|||
var updateUserList = function (config, myUserName, userlistElement, userList, userData, readOnly, $userAdminElement) { |
|||
// Make sure the elements are displayed
|
|||
var $userButtons = $(userlistElement).find("#userButtons"); |
|||
$userButtons.attr('display', 'inline'); |
|||
|
|||
if (config.displayed.indexOf('userlist') !== -1) { |
|||
var numberOfUsers = userList.length; |
|||
|
|||
// 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 = readOnly === -1 ? userList : arrayIntersect(userList, Object.keys(userData)); |
|||
|
|||
// Names of editing users
|
|||
var others = getOtherUsers(myUserName, userList, userData); |
|||
var editUsersNames = others.list; |
|||
var duplicates = others.duplicates; |
|||
|
|||
var numberOfEditUsers = userList.length - duplicates; |
|||
var numberOfViewUsers = numberOfUsers - numberOfEditUsers - duplicates; |
|||
|
|||
// Number of anonymous editing users
|
|||
var anonymous = numberOfEditUsers - editUsersNames.length; |
|||
|
|||
// Update the userlist
|
|||
var $usersTitle = $('<h2>').text(Messages.users); |
|||
var $editUsers = $userButtons.find('.' + USERLIST_CLS); |
|||
$editUsers.html('').append($usersTitle); |
|||
|
|||
var $editUsersList = $('<pre>'); |
|||
if (readOnly !== 1) { // Yourself - edit
|
|||
$editUsers.append('<span class="yourself">' + Messages.yourself + '</span>'); |
|||
anonymous--; |
|||
} |
|||
// Editors
|
|||
$editUsersList.text(editUsersNames.join('\n')); // .text() to avoid XSS
|
|||
$editUsers.append($editUsersList); |
|||
if (anonymous > 0) { // Anonymous editors
|
|||
var text = anonymous === 1 ? Messages.anonymousUser : Messages.anonymousUsers; |
|||
$editUsers.append('<span class="anonymous">' + anonymous + ' ' + text + '</span>'); |
|||
} |
|||
if (numberOfViewUsers > 0) { // Viewers
|
|||
var viewText = '<span class="viewer">'; |
|||
if (numberOfEditUsers > 0) { |
|||
$editUsers.append('<br>'); |
|||
viewText += Messages.and + ' '; |
|||
} |
|||
var viewerText = numberOfViewUsers !== 1 ? Messages.viewers : Messages.viewer; |
|||
viewText += numberOfViewUsers + ' ' + viewerText + '</span>'; |
|||
$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 viewersText = numberOfViewUsers !== 1 ? Messages.viewers : Messages.viewer; |
|||
var editorsText = numberOfEditUsers !== 1 ? Messages.editors : Messages.editor; |
|||
var $span = $('<span>', {'class': 'large'}).html(fa_editusers + ' ' + numberOfEditUsers + ' ' + editorsText + ' ' + fa_viewusers + ' ' + numberOfViewUsers + ' ' + viewersText); |
|||
var $spansmall = $('<span>', {'class': 'narrow'}).html(fa_editusers + ' ' + numberOfEditUsers + ' ' + fa_viewusers + ' ' + numberOfViewUsers); |
|||
$userButtons.find('.buttonTitle').html('').append($span).append($spansmall); |
|||
} |
|||
|
|||
if (config.displayed.indexOf('useradmin') !== -1) { |
|||
// Change username in useradmin dropdown
|
|||
var $userElement = $userAdminElement.find('.' + USERNAME_CLS); |
|||
$userElement.show(); |
|||
if (readOnly === 1) { |
|||
$userElement.addClass(READONLY_CLS).text(Messages.readonly); |
|||
} |
|||
else { |
|||
var name = userData[myUserName] && userData[myUserName].name; |
|||
if (!name) { |
|||
name = Messages.anonymous; |
|||
} |
|||
$userElement.removeClass(READONLY_CLS).text(name); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
var createLagElement = function () { |
|||
var $lag = $('<span>', { |
|||
'class': LAG_ELEM_CLS, |
|||
id: uid(), |
|||
}); |
|||
var $a = $('<span>', {'class': 'cryptpad-lag', id: 'newLag'}); |
|||
$('<span>', {'class': 'bar1'}).appendTo($a); |
|||
$('<span>', {'class': 'bar2'}).appendTo($a); |
|||
$('<span>', {'class': 'bar3'}).appendTo($a); |
|||
$('<span>', {'class': 'bar4'}).appendTo($a); |
|||
return $a[0]; |
|||
}; |
|||
|
|||
var checkLag = function (getLag, lagElement) { |
|||
var lag; |
|||
if(typeof getLag === "function") { |
|||
lag = getLag(); |
|||
} |
|||
var lagLight = $('<div>', { |
|||
'class': 'lag' |
|||
}); |
|||
var title; |
|||
var $lag = $(lagElement); |
|||
if (lag) { |
|||
$lag.attr('class', 'cryptpad-lag'); |
|||
firstConnection = false; |
|||
title = Messages.lag + ' : ' + lag + ' ms\n'; |
|||
if (lag > 30000) { |
|||
$lag.addClass('lag0'); |
|||
title = Messages.redLight; |
|||
} else if (lag > 5000) { |
|||
$lag.addClass('lag1'); |
|||
title += Messages.orangeLight; |
|||
} else if (lag > 1000) { |
|||
$lag.addClass('lag2'); |
|||
title += Messages.orangeLight; |
|||
} else if (lag > 300) { |
|||
$lag.addClass('lag3'); |
|||
title += Messages.greenLight; |
|||
} else { |
|||
$lag.addClass('lag4'); |
|||
title += Messages.greenLight; |
|||
} |
|||
} |
|||
else if (!firstConnection) { |
|||
$lag.attr('class', 'cryptpad-lag'); |
|||
// Display the red light at the 2nd failed attemp to get the lag
|
|||
lagLight.addClass('lag-red'); |
|||
title = Messages.redLight; |
|||
} |
|||
if (title) { |
|||
$lag.attr('title', title); |
|||
} |
|||
}; |
|||
|
|||
var createLinkToMain = function ($topContainer) { |
|||
var $linkContainer = $('<span>', { |
|||
'class': "cryptpad-link" |
|||
}).appendTo($topContainer); |
|||
var $imgTag = $('<img>', { |
|||
src: "/customize/cryptofist_mini.png?" + urlArgs, |
|||
alt: "Cryptpad" |
|||
}); |
|||
|
|||
// We need to override the "a" tag action here because it is inside the iframe!
|
|||
var $aTagSmall = $('<a>', { |
|||
href: "/", |
|||
title: Messages.header_logoTitle, |
|||
'class': "cryptpad-logo" |
|||
}).append($imgTag); |
|||
var $span = $('<span>').text('CryptPad'); |
|||
var $aTagBig = $aTagSmall.clone().addClass('large').append($span); |
|||
$aTagSmall.addClass('narrow'); |
|||
var onClick = function (e) { |
|||
e.preventDefault(); |
|||
if (e.ctrlKey) { |
|||
window.open('/'); |
|||
return; |
|||
} |
|||
window.location = "/"; |
|||
}; |
|||
|
|||
var onContext = function (e) { e.stopPropagation(); }; |
|||
|
|||
$aTagBig.click(onClick).contextmenu(onContext); |
|||
$aTagSmall.click(onClick).contextmenu(onContext); |
|||
|
|||
$linkContainer.append($aTagSmall).append($aTagBig); |
|||
}; |
|||
|
|||
var createUserAdmin = function ($topContainer, config, readOnly, lagElement, Cryptpad) { |
|||
var $lag = $(lagElement); |
|||
|
|||
var $userContainer = $('<span>', { |
|||
'class': USER_CLS |
|||
}).appendTo($topContainer); |
|||
|
|||
if (config.displayed.indexOf('state') !== -1) { |
|||
var $state = $('<span>', { |
|||
'class': STATE_CLS |
|||
}).text(Messages.synchronizing).appendTo($userContainer); |
|||
} |
|||
|
|||
if (config.displayed.indexOf('lag') !== -1) { |
|||
$userContainer.append($lag); |
|||
} |
|||
|
|||
if (config.displayed.indexOf('limit') !== -1 && Config.enablePinning) { |
|||
var usage; |
|||
var $limitIcon = $('<span>', {'class': 'fa fa-exclamation-triangle'}); |
|||
var $limit = $('<span>', { |
|||
'class': LIMIT_ELEM_CLS, |
|||
'title': Messages.pinLimitReached |
|||
}).append($limitIcon).hide().appendTo($userContainer); |
|||
var todo = function (e, overLimit) { |
|||
if (e) { return void console.error("Unable to get the pinned usage"); } |
|||
if (overLimit) { |
|||
var message = Messages.pinLimitReachedAlert; |
|||
if (ApiConfig.noSubscriptionButton === true) { |
|||
message = Messages.pinLimitReachedAlertNoAccounts; |
|||
} |
|||
$limit.show().click(function () { |
|||
Cryptpad.alert(message, null, true); |
|||
}); |
|||
} |
|||
}; |
|||
Cryptpad.isOverPinLimit(todo); |
|||
} |
|||
|
|||
if (config.displayed.indexOf('newpad') !== -1) { |
|||
var pads_options = []; |
|||
Config.availablePadTypes.forEach(function (p) { |
|||
if (p === 'drive') { return; } |
|||
pads_options.push({ |
|||
tag: 'a', |
|||
attributes: { |
|||
'target': '_blank', |
|||
'href': '/' + p + '/', |
|||
}, |
|||
content: Messages.type[p] |
|||
}); |
|||
}); |
|||
var $plusIcon = $('<span>', {'class': 'fa fa-plus'}); |
|||
var $newbig = $('<span>', {'class': 'big'}).append(' ' +Messages.newButton); |
|||
var $newButton = $('<div>').append($plusIcon).append($newbig); |
|||
var dropdownConfig = { |
|||
text: $newButton.html(), // Button initial text
|
|||
options: pads_options, // Entries displayed in the menu
|
|||
left: true, // Open to the left of the button
|
|||
}; |
|||
var $newPadBlock = Cryptpad.createDropdown(dropdownConfig); |
|||
$newPadBlock.find('button').attr('title', Messages.newButtonTitle); |
|||
$newPadBlock.find('button').attr('id', 'newdoc'); |
|||
$newPadBlock.appendTo($userContainer); |
|||
} |
|||
|
|||
// User dropdown
|
|||
if (config.displayed.indexOf('useradmin') !== -1) { |
|||
var userMenuCfg = {}; |
|||
if (!config.hideDisplayName) { |
|||
userMenuCfg = { |
|||
displayNameCls: USERNAME_CLS, |
|||
changeNameButtonCls: USERBUTTON_CLS, |
|||
}; |
|||
} |
|||
if (readOnly !== 1) { |
|||
userMenuCfg.displayName = 1; |
|||
userMenuCfg.displayChangeName = 1; |
|||
} |
|||
var $userAdmin = Cryptpad.createUserAdminMenu(userMenuCfg); |
|||
|
|||
$userAdmin.attr('id', 'userDropdown'); |
|||
$userContainer.append($userAdmin); |
|||
|
|||
var $userButton = $userAdmin.find('a.' + USERBUTTON_CLS); |
|||
var renameAlertOpened; |
|||
$userButton.click(function (e) { |
|||
e.preventDefault(); |
|||
e.stopPropagation(); |
|||
Cryptpad.getLastName(function (err, lastName) { |
|||
if (err) { return void console.error("Cannot get last name", err); } |
|||
Cryptpad.prompt(Messages.changeNamePrompt, lastName || '', function (newName) { |
|||
if (newName === null && typeof(lastName) === "string") { return; } |
|||
if (newName === null) { newName = ''; } |
|||
Cryptpad.changeDisplayName(newName); |
|||
}); |
|||
}); |
|||
}); |
|||
Cryptpad.onDisplayNameChanged(function (newName) { |
|||
Cryptpad.findCancelButton().click(); |
|||
}); |
|||
} |
|||
|
|||
return $userContainer; |
|||
}; |
|||
|
|||
var createTitle = function ($container, readOnly, config, Cryptpad) { |
|||
var $titleContainer = $('<span>', { |
|||
id: 'toolbarTitle', |
|||
'class': TITLE_CLS |
|||
}).appendTo($container); |
|||
|
|||
if (!config || typeof config !== "object") { return; } |
|||
|
|||
var callback = config.onRename; |
|||
var placeholder = config.defaultName; |
|||
var suggestName = config.suggestName; |
|||
|
|||
// Buttons
|
|||
var $text = $('<span>', { |
|||
'class': 'title' |
|||
}).appendTo($titleContainer); |
|||
var $pencilIcon = $('<span>', { |
|||
'class': 'pencilIcon', |
|||
'title': Messages.clickToEdit |
|||
}); |
|||
if (readOnly === 1 || typeof(Cryptpad) === "undefined") { return $titleContainer; } |
|||
var $input = $('<input>', { |
|||
type: 'text', |
|||
placeholder: placeholder |
|||
}).appendTo($titleContainer).hide(); |
|||
if (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($titleContainer); |
|||
} |
|||
|
|||
// Events
|
|||
$input.on('mousedown', function (e) { |
|||
if (!$input.is(":focus")) { |
|||
$input.focus(); |
|||
} |
|||
e.stopPropagation(); |
|||
return true; |
|||
}); |
|||
$input.on('keyup', function (e) { |
|||
if (e.which === 13 && connected === true) { |
|||
var name = $input.val().trim(); |
|||
if (name === "") { |
|||
name = $input.attr('placeholder'); |
|||
} |
|||
Cryptpad.renamePad(name, function (err, newtitle) { |
|||
if (err) { return; } |
|||
$text.text(newtitle); |
|||
callback(null, newtitle); |
|||
$input.hide(); |
|||
$text.show(); |
|||
//$pencilIcon.css('display', '');
|
|||
}); |
|||
} |
|||
else if (e.which === 27) { |
|||
$input.hide(); |
|||
$text.show(); |
|||
//$pencilIcon.css('display', '');
|
|||
} |
|||
}); |
|||
|
|||
var displayInput = function () { |
|||
if (connected === false) { return; } |
|||
$text.hide(); |
|||
//$pencilIcon.css('display', 'none');
|
|||
var inputVal = suggestName() || ""; |
|||
$input.val(inputVal); |
|||
$input.show(); |
|||
$input.focus(); |
|||
}; |
|||
$text.on('click', displayInput); |
|||
$pencilIcon.on('click', displayInput); |
|||
return $titleContainer; |
|||
}; |
|||
|
|||
var create = Bar.create = function ($container, myUserName, realtime, getLag, userList, config) { |
|||
config = config || {}; |
|||
var readOnly = (typeof config.readOnly !== "undefined") ? (config.readOnly ? 1 : 0) : -1; |
|||
var Cryptpad = config.common; |
|||
Messages = Cryptpad.Messages; |
|||
config.displayed = config.displayed || []; |
|||
|
|||
var toolbar = createRealtimeToolbar($container, config.title); |
|||
var userListElement = createUserList(toolbar.find('.' + LEFTSIDE_CLS), config, readOnly, Cryptpad); |
|||
var $titleElement = createTitle(toolbar.find('.' + TOP_CLS), readOnly, config.title, Cryptpad); |
|||
var $linkElement = createLinkToMain(toolbar.find('.' + TOP_CLS)); |
|||
var lagElement = createLagElement(); |
|||
var $userAdminElement = createUserAdmin(toolbar.find('.' + TOP_CLS), config, readOnly, lagElement, Cryptpad); |
|||
var spinner = createSpinner($userAdminElement, config); |
|||
var userData = config.userData; |
|||
// readOnly = 1 (readOnly enabled), 0 (disabled), -1 (old pad without readOnly mode)
|
|||
var saveElement; |
|||
var loadElement; |
|||
var $stateElement = toolbar.find('.' + STATE_CLS); |
|||
|
|||
if (config.ifrw) { |
|||
var removeDropdowns = function (e) { |
|||
$container.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 || !$titleElement) { |
|||
return; |
|||
} |
|||
|
|||
if (!$titleElement.find('input').is(':visible')) { return; } |
|||
|
|||
var ev = $.Event("keyup"); |
|||
ev.which = 13; |
|||
$titleElement.find('input').trigger(ev); |
|||
|
|||
/* |
|||
$titleElement.find('input').hide(); |
|||
$titleElement.find('span.title').show(); |
|||
$titleElement.find('span.pencilIcon').css('display', ''); |
|||
*/ |
|||
}; |
|||
$(config.ifrw).on('click', removeDropdowns); |
|||
$(config.ifrw).on('click', cancelEditTitle); |
|||
|
|||
try { |
|||
if (config.ifrw.$('iframe').length) { |
|||
var innerIfrw = 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
|
|||
} |
|||
} |
|||
|
|||
// Update user list
|
|||
if (userList) { |
|||
if (userData) { |
|||
userList.change.push(function (newUserData) { |
|||
var users = userList.users; |
|||
if (users.indexOf(myUserName) !== -1) { connected = true; } |
|||
if (!connected) { return; } |
|||
checkSynchronizing(users, myUserName, $stateElement); |
|||
updateUserList(config, myUserName, userListElement, users, userData, readOnly, $userAdminElement); |
|||
}); |
|||
} else { |
|||
userList.change.push(function () { |
|||
var users = userList.users; |
|||
if (users.indexOf(myUserName) !== -1) { connected = true; } |
|||
if (!connected) { return; } |
|||
checkSynchronizing(users, myUserName, $stateElement); |
|||
}); |
|||
} |
|||
} |
|||
// Display notifications when users are joining/leaving the session
|
|||
var oldUserData; |
|||
if (typeof Cryptpad !== "undefined" && userList) { |
|||
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 : name; |
|||
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; |
|||
}; |
|||
|
|||
userList.change.push(function (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 (userList.users.indexOf(u) === -1) { |
|||
var temp = JSON.parse(JSON.stringify(oldUserData[u])); |
|||
delete oldUserData[u]; |
|||
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 (readOnly === 0 && !oldUserData[myUserName]) { |
|||
oldUserData = JSON.parse(JSON.stringify(newdata)); |
|||
return; |
|||
} |
|||
for (var k in newdata) { |
|||
if (k !== myUserName && userList.users.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)); |
|||
}); |
|||
} |
|||
|
|||
var ks = function (local) { |
|||
return function () { |
|||
if (connected) { kickSpinner(Cryptpad, realtime, local, spinner); } |
|||
}; |
|||
}; |
|||
|
|||
if (realtime) { |
|||
realtime.onPatch(ks()); |
|||
realtime.onMessage(ks(true)); |
|||
|
|||
checkLag(getLag, lagElement); |
|||
setInterval(function () { |
|||
if (!connected) { return; } |
|||
checkLag(getLag, lagElement); |
|||
}, 3000); |
|||
} else { connected = true; } |
|||
|
|||
var failed = function () { |
|||
connected = false; |
|||
$stateElement.text(Messages.disconnected); |
|||
checkLag(undefined, lagElement); |
|||
}; |
|||
|
|||
// On log out, remove permanently the realtime elements of the toolbar
|
|||
Cryptpad.onLogout(function () { |
|||
failed(); |
|||
$userAdminElement.find('#userDropdown').hide(); |
|||
$(userListElement).hide(); |
|||
}); |
|||
|
|||
return { |
|||
failed: failed, |
|||
reconnecting: function (userId) { |
|||
myUserName = userId; |
|||
connected = false; |
|||
$stateElement.text(Messages.reconnecting); |
|||
checkLag(getLag, lagElement); |
|||
}, |
|||
connected: function () { |
|||
connected = true; |
|||
} |
|||
}; |
|||
}; |
|||
|
|||
return Bar; |
|||
}); |
|||
1069
www/common/toolbar2.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,31 @@ |
|||
@import (once) "../../customize/src/less2/include/browser.less"; |
|||
@import (once) "../../customize/src/less2/include/toolbar.less"; |
|||
@import (once) "../../customize/src/less2/include/markdown.less"; |
|||
@import (once) '../../customize/src/less2/include/fileupload.less'; |
|||
@import (once) '../../customize/src/less2/include/alertify.less'; |
|||
@import (once) '../../customize/src/less2/include/tools.less'; |
|||
@import (once) '../../customize/src/less2/include/tokenfield.less'; |
|||
|
|||
.toolbar_main(); |
|||
.fileupload_main(); |
|||
.alertify_main(); |
|||
.tokenfield_main(); |
|||
|
|||
// body |
|||
&.cp-app-debug { |
|||
display: flex; |
|||
flex-flow: column; |
|||
height: 100%; |
|||
|
|||
#cp-app-debug { |
|||
flex: 1; |
|||
display: flex; |
|||
min-height: 0; |
|||
} |
|||
#cp-app-debug-content { |
|||
flex: 1; |
|||
overflow: auto; |
|||
white-space: pre-wrap; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,52 @@ |
|||
define(function () { |
|||
var padZero = function (str, len) { |
|||
len = len || 2; |
|||
var zeros = new Array(len).join('0'); |
|||
return (zeros + str).slice(-len); |
|||
}; |
|||
var invertColor = function (hex) { |
|||
if (hex.indexOf('#') === 0) { |
|||
hex = hex.slice(1); |
|||
} |
|||
// convert 3-digit hex to 6-digits.
|
|||
if (hex.length === 3) { |
|||
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; |
|||
} |
|||
if (hex.length !== 6) { |
|||
console.error(hex); |
|||
throw new Error('Invalid HEX color.'); |
|||
} |
|||
// invert color components
|
|||
var r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16), |
|||
g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16), |
|||
b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16); |
|||
// pad each with zeros and return
|
|||
return '#' + padZero(r) + padZero(g) + padZero(b); |
|||
}; |
|||
var rgb2hex = function (rgb) { |
|||
if (rgb.indexOf('#') === 0) { return rgb; } |
|||
rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); |
|||
var hex = function (x) { |
|||
return ("0" + parseInt(x).toString(16)).slice(-2); |
|||
}; |
|||
return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]); |
|||
}; |
|||
var hex2rgba = function (hex, opacity) { |
|||
if (hex.indexOf('#') === 0) { |
|||
hex = hex.slice(1); |
|||
} |
|||
if (hex.length === 3) { |
|||
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; |
|||
} |
|||
if (!opacity) { opacity = 1; } |
|||
var r = parseInt(hex.slice(0,2), 16); |
|||
var g = parseInt(hex.slice(2,4), 16); |
|||
var b = parseInt(hex.slice(4,6), 16); |
|||
return 'rgba('+r+', '+g+', '+b+', '+opacity+')'; |
|||
}; |
|||
return { |
|||
invert: invertColor, |
|||
rgb2hex: rgb2hex, |
|||
hex2rgba: hex2rgba |
|||
}; |
|||
}); |
|||
@ -0,0 +1,38 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<title>CryptPad</title> |
|||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<meta name="referrer" content="no-referrer" /> |
|||
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script> |
|||
<style> |
|||
html, body { |
|||
margin: 0px; |
|||
padding: 0px; |
|||
} |
|||
#sbox-iframe { |
|||
position:fixed; |
|||
top:0px; |
|||
left:0px; |
|||
bottom:0px; |
|||
right:0px; |
|||
width:100%; |
|||
height:100%; |
|||
border:none; |
|||
margin:0; |
|||
padding:0; |
|||
overflow:hidden; |
|||
} |
|||
#sbox-filePicker-iframe { |
|||
position: fixed; |
|||
top:0; left:0; |
|||
bottom:0; right:0; |
|||
width:100%; |
|||
height: 100%; |
|||
border: 0; |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<iframe id="sbox-iframe"> |
|||
@ -0,0 +1,19 @@ |
|||
<!DOCTYPE html> |
|||
<html class="cp-app-noscroll"> |
|||
<head> |
|||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> |
|||
<script async data-bootload="/debug/inner.js" data-main="/common/sframe-boot.js?ver=1.6" src="/bower_components/requirejs/require.js?ver=2.3.5"></script> |
|||
<style> |
|||
.loading-hidden { display: none; } |
|||
#editor1 { display: none; } |
|||
.html2canvas-container { width: 3000px !important; height: 3000px !important; } |
|||
</style> |
|||
</head> |
|||
<body class="cp-app-debug"> |
|||
<div id="cp-toolbar" class="cp-toolbar-container"></div> |
|||
<div id="cp-app-debug"> |
|||
<div id="cp-app-debug-content"></div> |
|||
</div> |
|||
</body> |
|||
</html> |
|||
|
|||
@ -0,0 +1,179 @@ |
|||
define([ |
|||
'jquery', |
|||
'/bower_components/chainpad-crypto/crypto.js', |
|||
'/bower_components/textpatcher/TextPatcher.js', |
|||
'/common/toolbar3.js', |
|||
'json.sortify', |
|||
'/bower_components/chainpad-json-validator/json-ot.js', |
|||
'/common/common-util.js', |
|||
'/bower_components/nthen/index.js', |
|||
'/common/sframe-common.js', |
|||
'/common/common-interface.js', |
|||
'/api/config', |
|||
'/common/common-realtime.js', |
|||
'/customize/messages.js', |
|||
'/customize/application_config.js', |
|||
|
|||
'/bower_components/secure-fabric.js/dist/fabric.min.js', |
|||
|
|||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', |
|||
'less!/bower_components/components-font-awesome/css/font-awesome.min.css', |
|||
'less!/customize/src/less2/main.less', |
|||
], function ( |
|||
$, |
|||
Crypto, |
|||
TextPatcher, |
|||
Toolbar, |
|||
JSONSortify, |
|||
JsonOT, |
|||
Util, |
|||
nThen, |
|||
SFCommon, |
|||
UI, |
|||
ApiConfig, |
|||
CommonRealtime, |
|||
Messages, |
|||
AppConfig) |
|||
{ |
|||
var APP = window.APP = { |
|||
$: $, |
|||
AppConfig: AppConfig, |
|||
SFCommon: SFCommon, |
|||
Crypto: Crypto, |
|||
ApiConfig: ApiConfig |
|||
}; |
|||
|
|||
var toolbar; |
|||
var common; |
|||
|
|||
nThen(function (waitFor) { |
|||
$(waitFor(function () { |
|||
UI.addLoadingScreen(); |
|||
})); |
|||
SFCommon.create(waitFor(function (c) { APP.common = common = c; })); |
|||
}).nThen(function (/*waitFor*/) { |
|||
var initializing = true; |
|||
var $bar = $('#cp-toolbar'); |
|||
var Title; |
|||
var cpNfInner; |
|||
var metadataMgr; |
|||
var readOnly = true; |
|||
|
|||
var config = APP.config = { |
|||
readOnly: readOnly, |
|||
transformFunction: JsonOT.validate, |
|||
// cryptpad debug logging (default is 1)
|
|||
// logLevel: 0,
|
|||
validateContent: function (content) { |
|||
try { |
|||
JSON.parse(content); |
|||
return true; |
|||
} catch (e) { |
|||
console.log("Failed to parse, rejecting patch"); |
|||
return false; |
|||
} |
|||
} |
|||
}; |
|||
|
|||
var history = false; |
|||
|
|||
var setHistory = function (bool, update) { |
|||
history = bool; |
|||
if (!bool && update) { config.onRemote(); } |
|||
}; |
|||
|
|||
var displayDoc = function (doc) { |
|||
$('#cp-app-debug-content').text(JSON.stringify(doc, 0, 2)); |
|||
console.log(doc); |
|||
}; |
|||
|
|||
config.onLocal = function () { }; |
|||
|
|||
config.onInit = function (info) { |
|||
Title = common.createTitle({}); |
|||
|
|||
var configTb = { |
|||
displayed: ['title', 'useradmin', 'spinner', 'share', 'userlist', 'newpad', 'limit'], |
|||
title: Title.getTitleConfig(), |
|||
metadataMgr: metadataMgr, |
|||
readOnly: 1, |
|||
realtime: info.realtime, |
|||
sfCommon: common, |
|||
$container: $bar, |
|||
$contentContainer: $('#cp-app-debug') |
|||
}; |
|||
toolbar = APP.toolbar = Toolbar.create(configTb); |
|||
Title.setToolbar(toolbar); |
|||
|
|||
/* add a history button */ |
|||
var histConfig = { |
|||
onLocal: config.onLocal, |
|||
onRemote: config.onRemote, |
|||
setHistory: setHistory, |
|||
applyVal: function (val) { |
|||
displayDoc(JSON.parse(val) || {}); |
|||
}, |
|||
$toolbar: $bar, |
|||
debug: true |
|||
}; |
|||
var $hist = common.createButton('history', true, {histConfig: histConfig}); |
|||
$hist.addClass('cp-hidden-if-readonly'); |
|||
toolbar.$rightside.append($hist); |
|||
}; |
|||
|
|||
config.onReady = function (info) { |
|||
if (APP.realtime !== info.realtime) { |
|||
var realtime = APP.realtime = info.realtime; |
|||
APP.patchText = TextPatcher.create({ |
|||
realtime: realtime, |
|||
//logging: true
|
|||
}); |
|||
} |
|||
|
|||
var userDoc = APP.realtime.getUserDoc(); |
|||
if (userDoc !== "") { |
|||
var hjson = JSON.parse(userDoc); |
|||
|
|||
if (Array.isArray(hjson)) { |
|||
metadataMgr.updateMetadata(hjson[3]); |
|||
} else if (hjson && hjson.metadata) { |
|||
metadataMgr.updateMetadata(hjson.metadata); |
|||
} |
|||
displayDoc(hjson); |
|||
} |
|||
|
|||
initializing = false; |
|||
UI.removeLoadingScreen(); |
|||
}; |
|||
|
|||
config.onRemote = function () { |
|||
if (initializing) { return; } |
|||
if (history) { return; } |
|||
var userDoc = APP.realtime.getUserDoc(); |
|||
|
|||
var json = JSON.parse(userDoc); |
|||
if (Array.isArray(json)) { |
|||
metadataMgr.updateMetadata(json[3]); |
|||
} else if (json && json.metadata) { |
|||
metadataMgr.updateMetadata(json.metadata); |
|||
} |
|||
displayDoc(json); |
|||
}; |
|||
|
|||
config.onAbort = function () { |
|||
console.log('onAbort'); |
|||
}; |
|||
|
|||
config.onConnectionChange = function (info) { |
|||
console.log('onConnectionChange', info.state); |
|||
}; |
|||
|
|||
cpNfInner = APP.cpNfInner = common.startRealtime(config); |
|||
metadataMgr = APP.metadataMgr = cpNfInner.metadataMgr; |
|||
|
|||
cpNfInner.onInfiniteSpinner(function () { |
|||
console.error('infinite spinner'); |
|||
}); |
|||
}); |
|||
|
|||
}); |
|||
@ -0,0 +1,58 @@ |
|||
// Load #1, load as little as possible because we are in a race to get the loading screen up.
|
|||
define([ |
|||
'/bower_components/nthen/index.js', |
|||
'/api/config', |
|||
'jquery', |
|||
'/common/requireconfig.js', |
|||
'/common/sframe-common-outer.js', |
|||
'/common/cryptpad-common.js', |
|||
'/common/common-util.js', |
|||
'/common/common-hash.js', |
|||
'/common/common-realtime.js', |
|||
'/common/common-constants.js', |
|||
'/common/common-interface.js', |
|||
], function (nThen, ApiConfig, $, RequireConfig, SFCommonO, |
|||
Cryptpad, Util, Hash, Realtime, Constants, UI) { |
|||
|
|||
window.Cryptpad = { |
|||
Common: Cryptpad, |
|||
Util: Util, |
|||
Hash: Hash, |
|||
Realtime: Realtime, |
|||
Constants: Constants, |
|||
UI: UI |
|||
}; |
|||
|
|||
var requireConfig = RequireConfig(); |
|||
|
|||
// Loaded in load #2
|
|||
nThen(function (waitFor) { |
|||
$(waitFor()); |
|||
}).nThen(function (waitFor) { |
|||
var req = { |
|||
cfg: requireConfig, |
|||
req: [ '/common/loading.js' ], |
|||
pfx: window.location.origin |
|||
}; |
|||
window.rc = requireConfig; |
|||
window.apiconf = ApiConfig; |
|||
$('#sbox-iframe').attr('src', |
|||
ApiConfig.httpSafeOrigin + '/debug/inner.html?' + requireConfig.urlArgs + |
|||
'#' + encodeURIComponent(JSON.stringify(req))); |
|||
|
|||
// This is a cheap trick to avoid loading sframe-channel in parallel with the
|
|||
// loading screen setup.
|
|||
var done = waitFor(); |
|||
var onMsg = function (msg) { |
|||
var data = JSON.parse(msg.data); |
|||
if (data.q !== 'READY') { return; } |
|||
window.removeEventListener('message', onMsg); |
|||
var _done = done; |
|||
done = function () { }; |
|||
_done(); |
|||
}; |
|||
window.addEventListener('message', onMsg); |
|||
}).nThen(function (/*waitFor*/) { |
|||
SFCommonO.start(); |
|||
}); |
|||
}); |
|||
@ -1,16 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html class="cp"> |
|||
<!-- If this file is not called customize.dist/src/template.html, it is generated --> |
|||
<head> |
|||
<title data-localization="main_title">CryptPad: Zero Knowledge, Collaborative Real Time Editing</title> |
|||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> |
|||
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/> |
|||
<script async data-bootload="/customize/template.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script> |
|||
</head> |
|||
<body class="html"> |
|||
<noscript> |
|||
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p> |
|||
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p> |
|||
</noscript> |
|||
</html> |
|||
@ -1,569 +0,0 @@ |
|||
define([ |
|||
'jquery', |
|||
'/common/cryptpad-common.js', |
|||
'/common/cryptget.js', |
|||
'/common/mergeDrive.js', |
|||
'/common/toolbar2.js', |
|||
'/bower_components/file-saver/FileSaver.min.js', |
|||
|
|||
'less!/customize/src/less/cryptpad.less', |
|||
'less!/bower_components/components-font-awesome/css/font-awesome.min.css', |
|||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', |
|||
'less!/customize/src/less/toolbar.less', |
|||
'less!/settings/main.less', |
|||
], function ($, Cryptpad, Crypt, Merge, Toolbar) { |
|||
var saveAs = window.saveAs; |
|||
|
|||
var USERNAME_KEY = 'cryptpad.username'; |
|||
|
|||
var APP = window.APP = { |
|||
Cryptpad: Cryptpad, |
|||
_onRefresh: [] |
|||
}; |
|||
|
|||
var Messages = Cryptpad.Messages; |
|||
|
|||
// Manage changes in the realtime object made from another page
|
|||
var onRefresh = function (h) { |
|||
if (typeof(h) !== "function") { return; } |
|||
if (APP._onRefresh.indexOf(h) !== -1) { return; } |
|||
APP._onRefresh.push(h); |
|||
}; |
|||
var refresh = APP.refresh = function () { |
|||
APP._onRefresh.forEach(function (h) { |
|||
h(); |
|||
}); |
|||
}; |
|||
|
|||
var categories = { |
|||
'account': [ |
|||
'infoBlock', |
|||
'displayName', |
|||
'languageSelector', |
|||
'logoutEverywhere', |
|||
'resetTips', |
|||
'thumbnails', |
|||
'userFeedback' |
|||
], |
|||
'drive': [ |
|||
'backupDrive', |
|||
'importLocalPads', |
|||
'resetDrive' |
|||
], |
|||
'code': [ |
|||
'indentUnit', |
|||
'indentType' |
|||
] |
|||
}; |
|||
|
|||
var createInfoBlock = function (store) { |
|||
var obj = store.proxy; |
|||
var $div = $('<div>', {'class': 'infoBlock'}); |
|||
|
|||
var $account = $('<div>', {'class': 'element'}).appendTo($div); |
|||
var accountName = obj.login_name || localStorage[Cryptpad.userNameKey]; |
|||
var $label = $('<span>', {'class': 'label'}).text(Messages.user_accountName); |
|||
var $name = $('<span>').text(accountName || ''); |
|||
if (!accountName) { |
|||
$label.text(''); |
|||
$name.text(Messages.settings_anonymous); |
|||
} |
|||
$account.append($label).append($name); |
|||
|
|||
var publicKey = obj.edPublic; |
|||
if (publicKey) { |
|||
var $key = $('<div>', {'class': 'element'}).appendTo($div); |
|||
var userHref = Cryptpad.getUserHrefFromKeys(accountName, publicKey); |
|||
var $pubLabel = $('<span>', {'class': 'label'}) |
|||
.text(Messages.settings_publicSigningKey); |
|||
$key.append($pubLabel).append(Cryptpad.dialog.selectable(userHref)); |
|||
} |
|||
|
|||
return $div; |
|||
}; |
|||
|
|||
// Create the block containing the display name field
|
|||
var createDisplayNameInput = function (store) { |
|||
var obj = store.proxy; |
|||
var $div = $('<div>', {'class': 'displayName element'}); |
|||
$('<label>', {'for' : 'displayName'}).text(Messages.user_displayName).appendTo($div); |
|||
var $inputBlock = $('<div>', {'class': 'inputBlock'}).appendTo($div); |
|||
var $input = $('<input>', { |
|||
'type': 'text', |
|||
'id': 'displayName', |
|||
'placeholder': Messages.anonymous}).appendTo($inputBlock); |
|||
var $save = $('<button>', {'class': 'btn btn-primary'}).text(Messages.settings_save).appendTo($inputBlock); |
|||
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide().appendTo($div); |
|||
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide().appendTo($div); |
|||
|
|||
var displayName = obj[USERNAME_KEY] || ''; |
|||
$input.val(displayName); |
|||
|
|||
// When the display name is changed (enter or button clicked)
|
|||
var todo = function () { |
|||
displayName = $input.val(); |
|||
if (displayName === obj[USERNAME_KEY]) { return; } |
|||
obj[USERNAME_KEY] = displayName; |
|||
Cryptpad.changeDisplayName(displayName); |
|||
$spinner.show(); |
|||
Cryptpad.whenRealtimeSyncs(store.info.realtime, function () { |
|||
$spinner.hide(); |
|||
$ok.show(); |
|||
}); |
|||
}; |
|||
$input.on('keyup', function (e) { |
|||
if ($input.val() !== displayName) { $ok.hide(); } |
|||
if (e.which === 13) { todo(); } |
|||
}); |
|||
$save.click(todo); |
|||
|
|||
// On remote change
|
|||
var onChange = function () { |
|||
if (obj[USERNAME_KEY] !== $input.val()) { |
|||
$input.val(obj[USERNAME_KEY]); |
|||
$input.focusout(); |
|||
} |
|||
}; |
|||
onRefresh(onChange); |
|||
|
|||
return $div; |
|||
}; |
|||
var createIndentUnitSelector = function (obj) { |
|||
var proxy = obj.proxy; |
|||
proxy.settings = proxy.settings || {}; |
|||
|
|||
var $div = $('<div>', { |
|||
'class': 'indentUnit element' |
|||
}); |
|||
$('<label>').text(Messages.settings_codeIndentation).appendTo($div); |
|||
|
|||
var $inputBlock = $('<div>', { |
|||
'class': 'inputBlock', |
|||
}).appendTo($div); |
|||
|
|||
var $input = $('<input>', { |
|||
'min': 1, |
|||
'max': 8, |
|||
type: 'number', |
|||
}).on('change', function () { |
|||
var val = parseInt($input.val()); |
|||
if (typeof(val) !== 'number') { return; } |
|||
Cryptpad.setAttribute(['codemirror', 'indentUnit'], val); |
|||
}).appendTo($inputBlock); |
|||
|
|||
proxy.on('change', [ 'settings', 'codemirror', 'indentUnit', ], function (o, n) { |
|||
$input.val(n); |
|||
}); |
|||
|
|||
Cryptpad.getAttribute(['codemirror', 'indentUnit'], function (e, val) { |
|||
if (e) { return void console.error(e); } |
|||
if (typeof(val) !== 'number') { |
|||
$input.val(2); |
|||
} else { |
|||
$input.val(val); |
|||
} |
|||
}); |
|||
return $div; |
|||
}; |
|||
|
|||
var createIndentTypeSelector = function (obj) { |
|||
var proxy = obj.proxy; |
|||
proxy.settings = proxy.settings || {}; |
|||
|
|||
var key = 'indentWithTabs'; |
|||
|
|||
var $div = $('<div>', { |
|||
'class': 'indentType element' |
|||
}); |
|||
$('<label>').text(Messages.settings_codeUseTabs).appendTo($div); |
|||
|
|||
var $inputBlock = $('<div>', { |
|||
'class': 'inputBlock', |
|||
}).css('flex-flow', 'column') |
|||
.appendTo($div); |
|||
|
|||
var $input = $('<input>', { |
|||
type: 'checkbox', |
|||
}).on('change', function () { |
|||
var val = $input.is(':checked'); |
|||
if (typeof(val) !== 'boolean') { return; } |
|||
Cryptpad.setAttribute(['codemirror', key], val); |
|||
}).appendTo($inputBlock); |
|||
|
|||
$input[0].checked = !!proxy.settings[key]; |
|||
proxy.on('change', ['settings', 'codemirror', key], function (o, n) { |
|||
$input[0].checked = !!n; |
|||
}); |
|||
|
|||
Cryptpad.getAttribute(['codemirror', key], function (e, val) { |
|||
if (e) { return void console.error(e); } |
|||
$input[0].checked = !!val; |
|||
}); |
|||
return $div; |
|||
}; |
|||
|
|||
var createResetTips = function () { |
|||
var $div = $('<div>', {'class': 'resetTips element'}); |
|||
$('<label>', {'for' : 'resetTips'}).text(Messages.settings_resetTips).appendTo($div); |
|||
$('<span>', {'class': 'description'}) |
|||
.text(Messages.settings_resetTipsButton).appendTo($div); |
|||
var $button = $('<button>', {'id': 'resetTips', 'class': 'btn btn-primary'}) |
|||
.text(Messages.settings_resetTipsAction).appendTo($div); |
|||
|
|||
$button.click(function () { |
|||
Object.keys(localStorage).forEach(function (k) { |
|||
if(k.slice(0, 9) === "hide-info") { |
|||
delete localStorage[k]; |
|||
} |
|||
}); |
|||
Cryptpad.alert(Messages.settings_resetTipsDone); |
|||
}); |
|||
|
|||
return $div; |
|||
}; |
|||
|
|||
var createThumbnails = function (obj) { |
|||
var $div = $('<div>', {'class': 'thumbnails element'}); |
|||
$('<label>').text(Messages.settings_thumbnails).appendTo($div); |
|||
|
|||
// Disable
|
|||
$('<span>', {'class': 'description'}) |
|||
.text(Messages.settings_disableThumbnailsDescription).appendTo($div); |
|||
var $label = $('<label>', { 'for': 'disableThumbnails', 'class': 'noTitle' }) |
|||
.text(Messages.settings_disableThumbnailsAction); |
|||
|
|||
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}); |
|||
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}); |
|||
|
|||
var $checkbox = $('<input>', { |
|||
'type': 'checkbox', |
|||
id: 'disableThumbnails' |
|||
}).on('change', function () { |
|||
$spinner.show(); |
|||
$ok.hide(); |
|||
if (!obj.proxy.settings) { obj.proxy.settings = {}; } |
|||
if (!obj.proxy.settings.general) { obj.proxy.settings.general = {}; } |
|||
obj.proxy.settings.general.disableThumbnails = $checkbox.is(':checked') || false; |
|||
Cryptpad.whenRealtimeSyncs(obj.info.realtime, function () { |
|||
$spinner.hide(); |
|||
$ok.show(); |
|||
}); |
|||
}); |
|||
|
|||
$checkbox.appendTo($div); |
|||
$label.appendTo($div); |
|||
|
|||
$ok.hide().appendTo($div); |
|||
$spinner.hide().appendTo($div); |
|||
|
|||
if (obj.proxy.settings && obj.proxy.settings.general |
|||
&& obj.proxy.settings.general.disableThumbnails) { |
|||
$checkbox[0].checked = true; |
|||
} |
|||
|
|||
// Reset
|
|||
/*$('<span>', {'class': 'description'}) |
|||
.text(Messages.settings_resetThumbnailsDescription).appendTo($div); |
|||
var $button = $('<button>', {'id': 'resetThumbnails', 'class': 'btn btn-primary'}) |
|||
.text(Messages.settings_resetThumbnailsAction).appendTo($div); |
|||
|
|||
$button.click(function () { |
|||
// TODO we need to have access to the sandbox localforage first...
|
|||
Cryptpad.alert(Messages.settings_resetThumbnailsDone); |
|||
});*/ |
|||
|
|||
return $div; |
|||
}; |
|||
|
|||
var createBackupDrive = function (store) { |
|||
var obj = store.proxy; |
|||
var $div = $('<div>', {'class': 'backupDrive element'}); |
|||
|
|||
var exportFile = function () { |
|||
var sjson = JSON.stringify(obj); |
|||
var name = obj.login_name || obj[USERNAME_KEY] || Messages.anonymous; |
|||
var suggestion = name + '-' + new Date().toDateString(); |
|||
Cryptpad.prompt(Cryptpad.Messages.exportPrompt, |
|||
Cryptpad.fixFileName(suggestion) + '.json', function (filename) { |
|||
if (!(typeof(filename) === 'string' && filename)) { return; } |
|||
var blob = new Blob([sjson], {type: "application/json;charset=utf-8"}); |
|||
saveAs(blob, filename); |
|||
}); |
|||
}; |
|||
var importFile = function (content) { |
|||
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).appendTo($div); |
|||
Crypt.put(Cryptpad.getUserHash() || localStorage[Cryptpad.fileHashKey], content, function (e) { |
|||
if (e) { console.error(e); } |
|||
$spinner.remove(); |
|||
}); |
|||
}; |
|||
|
|||
$('<label>', {'for' : 'exportDrive'}).text(Messages.settings_backupCategory).appendTo($div); |
|||
$('<span>', {'class': 'description'}) |
|||
.text(Messages.settings_backupTitle).appendTo($div); |
|||
/* add an export button */ |
|||
var $export = Cryptpad.createButton('export', true, {}, exportFile); |
|||
$export.attr('class', 'btn btn-success').text(Messages.settings_backup); |
|||
$div.append($export); |
|||
|
|||
/* add an import button */ |
|||
var $import = Cryptpad.createButton('import', true, {}, importFile); |
|||
$import.attr('class', 'btn btn-success').text(Messages.settings_restore); |
|||
$div.append($import); |
|||
|
|||
return $div; |
|||
}; |
|||
|
|||
var createResetDrive = function (obj) { |
|||
var $div = $('<div>', {'class': 'resetDrive element'}); |
|||
$('<label>', {'for' : 'resetDrive'}).text(Messages.settings_resetNewTitle).appendTo($div); |
|||
$('<span>', {'class': 'description'}) |
|||
.text(Messages.settings_reset).appendTo($div); |
|||
var $button = $('<button>', {'id': 'resetDrive', 'class': 'btn btn-danger'}) |
|||
.text(Messages.settings_resetButton).appendTo($div); |
|||
|
|||
$button.click(function () { |
|||
Cryptpad.prompt(Messages.settings_resetPrompt, "", function (val) { |
|||
if (val !== "I love CryptPad") { |
|||
Cryptpad.alert(Messages.settings_resetError); |
|||
return; |
|||
} |
|||
obj.proxy.drive = Cryptpad.getStore().getEmptyObject(); |
|||
Cryptpad.alert(Messages.settings_resetDone); |
|||
}, undefined, true); |
|||
}); |
|||
|
|||
return $div; |
|||
}; |
|||
|
|||
var createUserFeedbackToggle = function (obj) { |
|||
var $div = $('<div>', { 'class': 'userFeedback element'}); |
|||
|
|||
$('<span>', {'class': 'label'}).text(Messages.settings_userFeedbackTitle).appendTo($div); |
|||
|
|||
var $label = $('<label>', { 'for': 'userFeedback', 'class': 'noTitle' }) |
|||
.text(Messages.settings_userFeedback); |
|||
|
|||
$('<span>', {'class': 'description'}) |
|||
.append(Messages.settings_userFeedbackHint1) |
|||
.append(Messages.settings_userFeedbackHint2).appendTo($div); |
|||
|
|||
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}); |
|||
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}); |
|||
|
|||
var $checkbox = $('<input>', { |
|||
'type': 'checkbox', |
|||
}).on('change', function () { |
|||
$spinner.show(); |
|||
$ok.hide(); |
|||
obj.proxy.allowUserFeedback = $checkbox.is(':checked') || false; |
|||
Cryptpad.whenRealtimeSyncs(obj.info.realtime, function () { |
|||
$spinner.hide(); |
|||
$ok.show(); |
|||
}); |
|||
}); |
|||
|
|||
$checkbox.appendTo($div); |
|||
$label.appendTo($div); |
|||
|
|||
$ok.hide().appendTo($div); |
|||
$spinner.hide().appendTo($div); |
|||
|
|||
if (obj.proxy.allowUserFeedback) { |
|||
$checkbox[0].checked = true; |
|||
} |
|||
return $div; |
|||
}; |
|||
|
|||
var createUsageButton = function () { |
|||
Cryptpad.createUsageBar(function (err, $bar) { |
|||
if (err) { return void console.error(err); } |
|||
APP.$usage.html('').append($bar); |
|||
}, true); |
|||
}; |
|||
|
|||
var createLogoutEverywhere = function (obj) { |
|||
var proxy = obj.proxy; |
|||
var $div = $('<div>', { 'class': 'logoutEverywhere element'}); |
|||
$('<label>', { 'for': 'logoutEverywhere'}) |
|||
.text(Messages.settings_logoutEverywhereTitle).appendTo($div); |
|||
$('<span>', {'class': 'description'}) |
|||
.text(Messages.settings_logoutEverywhere).appendTo($div); |
|||
var $button = $('<button>', { id: 'logoutEverywhere', 'class': 'btn btn-primary' }) |
|||
.text(Messages.settings_logoutEverywhereButton) |
|||
.appendTo($div); |
|||
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide().appendTo($div); |
|||
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide().appendTo($div); |
|||
|
|||
$button.click(function () { |
|||
var realtime = obj.info.realtime; |
|||
console.log(realtime); |
|||
|
|||
Cryptpad.confirm(Messages.settings_logoutEverywhereConfirm, function (yes) { |
|||
if (!yes) { return; } |
|||
$spinner.show(); |
|||
$ok.hide(); |
|||
|
|||
var token = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER); |
|||
localStorage.setItem('loginToken', token); |
|||
proxy.loginToken = token; |
|||
|
|||
Cryptpad.whenRealtimeSyncs(realtime, function () { |
|||
$spinner.hide(); |
|||
$ok.show(); |
|||
window.setTimeout(function () { |
|||
$ok.fadeOut(1500); |
|||
}, 2500); |
|||
}); |
|||
}); |
|||
}); |
|||
return $div; |
|||
}; |
|||
|
|||
var createImportLocalPads = function (obj) { |
|||
if (!Cryptpad.isLoggedIn()) { return; } |
|||
var $div = $('<div>', {'class': 'importLocalPads element'}); |
|||
$('<label>', {'for' : 'importLocalPads'}).text(Messages.settings_import).appendTo($div); |
|||
$('<span>', {'class': 'description'}) |
|||
.text(Messages.settings_importTitle).appendTo($div); |
|||
var $button = $('<button>', {'id': 'importLocalPads', 'class': 'btn btn-primary'}) |
|||
.text(Messages.settings_import).appendTo($div); |
|||
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide().appendTo($div); |
|||
var $spinner = $('<span>', {'class': 'fa fa-spinner fa-pulse'}).hide().appendTo($div); |
|||
|
|||
$button.click(function () { |
|||
Cryptpad.confirm(Messages.settings_importConfirm, function (yes) { |
|||
if (!yes) { return; } |
|||
$spinner.show(); |
|||
$ok.hide(); |
|||
Merge.anonDriveIntoUser(obj, localStorage.FS_hash, function () { |
|||
$spinner.hide(); |
|||
$ok.show(); |
|||
Cryptpad.alert(Messages.settings_importDone); |
|||
}); |
|||
}, undefined, true); |
|||
}); |
|||
|
|||
return $div; |
|||
}; |
|||
|
|||
var createLanguageSelector = function () { |
|||
var $div = $('<div>', {'class': 'languageSelector element'}); |
|||
$('<label>').text(Messages.language).appendTo($div); |
|||
var $b = Cryptpad.createLanguageSelector().appendTo($div); |
|||
$b.find('button').addClass('btn btn-secondary'); |
|||
return $div; |
|||
}; |
|||
|
|||
|
|||
var hideCategories = function () { |
|||
APP.$rightside.find('> div').hide(); |
|||
}; |
|||
var showCategories = function (cat) { |
|||
hideCategories(); |
|||
cat.forEach(function (c) { |
|||
APP.$rightside.find('.'+c).show(); |
|||
}); |
|||
}; |
|||
|
|||
var createLeftside = function () { |
|||
var $categories = $('<div>', {'class': 'categories'}).appendTo(APP.$leftside); |
|||
APP.$usage = $('<div>', {'class': 'usage'}).appendTo(APP.$leftside); |
|||
var active = 'account'; |
|||
Object.keys(categories).forEach(function (key) { |
|||
var $category = $('<div>', {'class': 'category'}).appendTo($categories); |
|||
if (key === 'account') { $category.append($('<span>', {'class': 'fa fa-user-o'})); } |
|||
if (key === 'drive') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); } |
|||
if (key === 'code') { $category.append($('<span>', {'class': 'fa fa-file-code-o' })); } |
|||
|
|||
if (key === active) { |
|||
$category.addClass('active'); |
|||
} |
|||
|
|||
$category.click(function () { |
|||
active = key; |
|||
$categories.find('.active').removeClass('active'); |
|||
$category.addClass('active'); |
|||
showCategories(categories[key]); |
|||
}); |
|||
|
|||
$category.append(Messages['settings_cat_'+key]); |
|||
}); |
|||
showCategories(categories[active]); |
|||
}; |
|||
|
|||
var createToolbar = function () { |
|||
var displayed = ['useradmin', 'newpad', 'limit', 'upgrade', 'pageTitle']; |
|||
var configTb = { |
|||
displayed: displayed, |
|||
ifrw: window, |
|||
common: Cryptpad, |
|||
$container: APP.$toolbar, |
|||
pageTitle: Messages.settings_title |
|||
}; |
|||
var toolbar = APP.toolbar = Toolbar.create(configTb); |
|||
toolbar.$rightside.html(''); // Remove the drawer if we don't use it to hide the toolbar
|
|||
}; |
|||
|
|||
var andThen = function (obj) { |
|||
APP.$leftside = $('<div>', {id: 'leftSide'}).appendTo(APP.$container); |
|||
var $rightside = APP.$rightside = $('<div>', {id: 'rightSide'}).appendTo(APP.$container); |
|||
|
|||
createToolbar(); |
|||
|
|||
//$rightside.append(createTitle());
|
|||
$rightside.append(createInfoBlock(obj)); |
|||
$rightside.append(createDisplayNameInput(obj)); |
|||
$rightside.append(createLanguageSelector()); |
|||
$rightside.append(createIndentUnitSelector(obj)); |
|||
$rightside.append(createIndentTypeSelector(obj)); |
|||
|
|||
if (Cryptpad.isLoggedIn()) { |
|||
$rightside.append(createLogoutEverywhere(obj)); |
|||
} |
|||
$rightside.append(createResetTips()); |
|||
$rightside.append(createThumbnails(obj)); |
|||
$rightside.append(createBackupDrive(obj)); |
|||
$rightside.append(createImportLocalPads(obj)); |
|||
$rightside.append(createResetDrive(obj)); |
|||
$rightside.append(createUserFeedbackToggle(obj)); |
|||
|
|||
obj.proxy.on('change', [], refresh); |
|||
obj.proxy.on('remove', [], refresh); |
|||
Cryptpad.onDisplayNameChanged(refresh); |
|||
|
|||
createLeftside(); |
|||
createUsageButton(); |
|||
|
|||
Cryptpad.removeLoadingScreen(); |
|||
}; |
|||
|
|||
$(function () { |
|||
$(window).click(function () { |
|||
$('.cp-dropdown-content').hide(); |
|||
}); |
|||
|
|||
APP.$container = $('#container'); |
|||
APP.$toolbar = $('#toolbar'); |
|||
|
|||
Cryptpad.ready(function () { |
|||
//if (!Cryptpad.getUserHash()) { return redirectToMain(); }
|
|||
|
|||
var storeObj = Cryptpad.getStore().getProxy && Cryptpad.getStore().getProxy().proxy |
|||
? Cryptpad.getStore().getProxy() : undefined; |
|||
|
|||
andThen(storeObj); |
|||
Cryptpad.reportAppUsage(); |
|||
}); |
|||
}); |
|||
|
|||
window.addEventListener('storage', function (e) { |
|||
if (e.key !== Cryptpad.userHashKey) { return; } |
|||
var o = e.oldValue; |
|||
var n = e.newValue; |
|||
window.location.reload(); |
|||
if (o && !n) { // disconnect
|
|||
//redirectToMain();
|
|||
} |
|||
}); |
|||
}); |
|||
@ -1,54 +0,0 @@ |
|||
@import '/customize/src/less/variables.less'; |
|||
@import '/customize/src/less/mixins.less'; |
|||
@import '/customize/src/less/sidebar-layout.less'; |
|||
|
|||
.cp { |
|||
#container { |
|||
#rightSide { |
|||
.userFeedback, .thumbnails { |
|||
input[type="checkbox"] { |
|||
vertical-align: middle; |
|||
margin-right: 5px; |
|||
} |
|||
} |
|||
.languageSelector { |
|||
button.btn { |
|||
width: @button-width; |
|||
background-color: @button-alt-bg; |
|||
border-color: #adadad; |
|||
color: black; |
|||
&:hover { |
|||
background-color: darken(@button-alt-bg, 15%); |
|||
} |
|||
} |
|||
} |
|||
.inputBlock { |
|||
input { |
|||
border-top-right-radius: 0.25em; |
|||
border-bottom-right-radius: 0.25em; |
|||
padding: 5px; |
|||
padding-left: 15px; |
|||
&[type="number"] { |
|||
border-right: 1px solid #adadad; |
|||
} |
|||
&[type="checkbox"] { |
|||
margin-right: 100%; |
|||
} |
|||
} |
|||
} |
|||
.infoBlock { |
|||
[type="text"] { |
|||
width: @button-width; |
|||
} |
|||
} |
|||
.backupDrive { |
|||
button { |
|||
span.fa { |
|||
margin-right: 5px; |
|||
} |
|||
margin-right: 5px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,528 +0,0 @@ |
|||
define([ |
|||
'jquery', |
|||
'/common/cryptpad-common.js', |
|||
'/common/hyperscript.js', |
|||
'/bower_components/marked/marked.min.js', |
|||
'/common/media-tag.js', |
|||
], function ($, Cryptpad, h, Marked, MediaTag) { |
|||
'use strict'; |
|||
|
|||
var UI = {}; |
|||
var Messages = Cryptpad.Messages; |
|||
|
|||
var m = function (md) { |
|||
var d = h('div.cp-app-contacts-content'); |
|||
try { |
|||
d.innerHTML = Marked(md || ''); |
|||
var $d = $(d); |
|||
// remove potentially malicious elements
|
|||
$d.find('script, iframe, object, applet, video, audio').remove(); |
|||
|
|||
// activate media-tags
|
|||
$d.find('media-tag').each(function (i, e) { MediaTag(e); }); |
|||
} catch (e) { |
|||
console.error(md); |
|||
console.error(e); |
|||
} |
|||
return d; |
|||
}; |
|||
|
|||
var dataQuery = function (curvePublic) { |
|||
return '[data-key="' + curvePublic + '"]'; |
|||
}; |
|||
|
|||
var initChannel = function (state, curvePublic, info) { |
|||
console.log('initializing channel for [%s]', curvePublic); |
|||
state.channels[curvePublic] = { |
|||
messages: [], |
|||
HEAD: info.lastKnownHash, |
|||
TAIL: null, |
|||
}; |
|||
}; |
|||
|
|||
UI.create = function (messenger, $userlist, $messages, common) { |
|||
var origin = common.getMetadataMgr().getPrivateData().origin; |
|||
|
|||
var state = window.state = { |
|||
active: '', |
|||
}; |
|||
|
|||
state.channels = {}; |
|||
var displayNames = state.displayNames = {}; |
|||
|
|||
var avatars = state.avatars = {}; |
|||
var setActive = function (curvePublic) { |
|||
state.active = curvePublic; |
|||
}; |
|||
var isActive = function (curvePublic) { |
|||
return curvePublic === state.active; |
|||
}; |
|||
|
|||
var find = {}; |
|||
find.inList = function (curvePublic) { |
|||
return $userlist.find(dataQuery(curvePublic)); |
|||
}; |
|||
|
|||
var notify = function (curvePublic) { |
|||
find.inList(curvePublic).addClass('cp-app-contacts-notify'); |
|||
}; |
|||
var unnotify = function (curvePublic) { |
|||
find.inList(curvePublic).removeClass('cp-app-contacts-notify'); |
|||
}; |
|||
|
|||
var markup = {}; |
|||
markup.message = function (msg) { |
|||
var curvePublic = msg.author; |
|||
var name = displayNames[msg.author]; |
|||
return h('div.cp-app-contacts-message', { |
|||
title: msg.time? new Date(msg.time).toLocaleString(): '?', |
|||
'data-key': curvePublic, |
|||
}, [ |
|||
name? h('div.cp-app-contacts-sender', name): undefined, |
|||
m(msg.text), |
|||
]); |
|||
}; |
|||
|
|||
var getChat = function (curvePublic) { |
|||
return $messages.find(dataQuery(curvePublic)); |
|||
}; |
|||
|
|||
var normalizeLabels = function ($messagebox) { |
|||
$messagebox.find('div.cp-app-contacts-message').toArray().reduce(function (a, b) { |
|||
var $b = $(b); |
|||
if ($(a).data('key') === $b.data('key')) { |
|||
$b.find('.cp-app-contacts-sender').hide(); |
|||
return a; |
|||
} |
|||
return b; |
|||
}, []); |
|||
}; |
|||
|
|||
markup.chatbox = function (curvePublic, data) { |
|||
var moreHistory = h('span.cp-app-contacts-more-history.fa.fa-history', { |
|||
title: Messages.contacts_fetchHistory, |
|||
}); |
|||
var displayName = data.displayName; |
|||
|
|||
var fetching = false; |
|||
var $moreHistory = $(moreHistory).click(function () { |
|||
if (fetching) { return; } |
|||
|
|||
// get oldest known message...
|
|||
var channel = state.channels[curvePublic]; |
|||
|
|||
if (channel.exhausted) { |
|||
return void $moreHistory.addClass('cp-app-contacts-faded'); |
|||
} |
|||
|
|||
console.log('getting history'); |
|||
var sig = channel.TAIL || channel.HEAD; |
|||
|
|||
fetching = true; |
|||
var $messagebox = $(getChat(curvePublic)).find('.cp-app-contacts-messages'); |
|||
messenger.getMoreHistory(curvePublic, sig, 10, function (e, history) { |
|||
fetching = false; |
|||
if (e) { return void console.error(e); } |
|||
|
|||
if (history.length === 0) { |
|||
channel.exhausted = true; |
|||
return; |
|||
} |
|||
|
|||
history.forEach(function (msg) { |
|||
if (channel.exhausted) { return; } |
|||
if (msg.sig) { |
|||
if (msg.sig === channel.TAIL) { |
|||
console.error('No more messages to fetch'); |
|||
channel.exhausted = true; |
|||
console.log(channel); |
|||
return void $moreHistory.addClass('cp-app-contacts-faded'); |
|||
} else { |
|||
channel.TAIL = msg.sig; |
|||
} |
|||
} else { |
|||
return void console.error('expected signature'); |
|||
} |
|||
if (msg.type !== 'MSG') { return; } |
|||
|
|||
// FIXME Schlameil the painter (performance does not scale well)
|
|||
if (channel.messages.some(function (old) { |
|||
return msg.sig === old.sig; |
|||
})) { return; } |
|||
|
|||
channel.messages.unshift(msg); |
|||
var el_message = markup.message(msg); |
|||
$messagebox.prepend(el_message); |
|||
}); |
|||
normalizeLabels($messagebox); |
|||
}); |
|||
}); |
|||
|
|||
var removeHistory = h('span.cp-app-contacts-remove-history.fa.fa-eraser', { |
|||
title: Messages.contacts_removeHistoryTitle |
|||
}); |
|||
|
|||
$(removeHistory).click(function () { |
|||
Cryptpad.confirm(Messages.contacts_confirmRemoveHistory, function (yes) { |
|||
if (!yes) { return; } |
|||
Cryptpad.clearOwnedChannel(data.channel, function (e) { |
|||
if (e) { |
|||
console.error(e); |
|||
Cryptpad.alert(Messages.contacts_removeHistoryServerError); |
|||
return; |
|||
} |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
var avatar = h('div.cp-avatar'); |
|||
var header = h('div.cp-app-contacts-header', [ |
|||
avatar, |
|||
moreHistory, |
|||
removeHistory, |
|||
]); |
|||
var messages = h('div.cp-app-contacts-messages'); |
|||
var input = h('textarea', { |
|||
placeholder: Messages.contacts_typeHere |
|||
}); |
|||
var sendButton = h('button.btn.btn-primary.fa.fa-paper-plane', { |
|||
title: Messages.contacts_send, |
|||
}); |
|||
|
|||
var rightCol = h('span.cp-app-contacts-right-col', [ |
|||
h('span.cp-app-contacts-name', displayName), |
|||
]); |
|||
|
|||
var $avatar = $(avatar); |
|||
if (data.avatar && avatars[data.avatar]) { |
|||
$avatar.append(avatars[data.avatar]).append(rightCol); |
|||
} else { |
|||
common.displayAvatar($avatar, data.avatar, data.displayName, function ($img) { |
|||
if (data.avatar && $img) { |
|||
avatars[data.avatar] = $img[0].outerHTML; |
|||
} |
|||
$(rightCol).insertAfter($avatar); |
|||
}); |
|||
} |
|||
|
|||
var sending = false; |
|||
var send = function (content) { |
|||
if (typeof(content) !== 'string' || !content.trim()) { return; } |
|||
if (sending) { return false; } |
|||
sending = true; |
|||
messenger.sendMessage(curvePublic, content, function (e) { |
|||
if (e) { |
|||
// failed to send
|
|||
return void console.error('failed to send'); |
|||
} |
|||
input.value = ''; |
|||
sending = false; |
|||
console.log('sent successfully'); |
|||
var $messagebox = $(messages); |
|||
|
|||
var height = $messagebox[0].scrollHeight; |
|||
$messagebox.scrollTop(height); |
|||
}); |
|||
}; |
|||
|
|||
var onKeyDown = function (e) { |
|||
// ignore anything that isn't 'enter'
|
|||
if (e.keyCode !== 13) { return; } |
|||
// send unless they're holding a ctrl-key or shift
|
|||
if (!e.ctrlKey && !e.shiftKey) { |
|||
send(this.value); |
|||
return false; |
|||
} |
|||
|
|||
// insert a newline if they're holding either
|
|||
var val = this.value; |
|||
var start = this.selectionState; |
|||
var end = this.selectionEnd; |
|||
|
|||
if (![start,end].some(function (x) { |
|||
return typeof(x) !== 'number'; |
|||
})) { |
|||
this.value = val.slice(0, start) + '\n' + val.slice(end); |
|||
this.selectionStart = this.selectionEnd = start + 1; |
|||
} else if (document.selection && document.selection.createRange) { |
|||
this.focus(); |
|||
var range = document.selection.createRange(); |
|||
range.text = '\r\n'; |
|||
range.collapse(false); |
|||
range.select(); |
|||
} |
|||
return false; |
|||
}; |
|||
$(input).on('keydown', onKeyDown); |
|||
$(sendButton).click(function () { send(input.value); }); |
|||
|
|||
return h('div.cp-app-contacts-chat', { |
|||
'data-key': curvePublic, |
|||
}, [ |
|||
header, |
|||
messages, |
|||
h('div.cp-app-contacts-input', [ |
|||
input, |
|||
sendButton, |
|||
]), |
|||
]); |
|||
}; |
|||
|
|||
var hideInfo = function () { |
|||
$messages.find('.cp-app-contacts-info').hide(); |
|||
}; |
|||
|
|||
var updateStatus = function (curvePublic) { |
|||
var $status = find.inList(curvePublic).find('.cp-app-contacts-status'); |
|||
// FIXME this stopped working :(
|
|||
messenger.getStatus(curvePublic, function (e, online) { |
|||
// if error maybe you shouldn't display this friend...
|
|||
if (e) { |
|||
find.inList(curvePublic).hide(); |
|||
getChat(curvePublic).hide(); |
|||
|
|||
return void console.error(curvePublic, e); |
|||
} |
|||
if (online) { |
|||
return void $status |
|||
.removeClass('cp-app-contacts-offline').addClass('cp-app-contacts-online'); |
|||
} |
|||
$status.removeClass('cp-app-contacts-online').addClass('cp-app-contacts-offline'); |
|||
}); |
|||
}; |
|||
|
|||
var display = function (curvePublic) { |
|||
var channel = state.channels[curvePublic]; |
|||
var lastMsg = channel.messages.slice(-1)[0]; |
|||
|
|||
if (lastMsg) { |
|||
channel.HEAD = lastMsg.sig; |
|||
messenger.setChannelHead(curvePublic, channel.HEAD, function (e) { |
|||
if (e) { console.error(e); } |
|||
}); |
|||
} |
|||
|
|||
setActive(curvePublic); |
|||
unnotify(curvePublic); |
|||
var $chat = getChat(curvePublic); |
|||
hideInfo(); |
|||
$messages.find('div.cp-app-contacts-chat[data-key]').hide(); |
|||
if ($chat.length) { |
|||
var $chat_messages = $chat.find('div.cp-app-contacts-message'); |
|||
if (!$chat_messages.length) { |
|||
var $more = $chat.find('.cp-app-contacts-more-history'); |
|||
$more.click(); |
|||
} |
|||
return void $chat.show(); |
|||
} |
|||
messenger.getFriendInfo(curvePublic, function (e, info) { |
|||
if (e) { return void console.error(e); } // FIXME
|
|||
var chatbox = markup.chatbox(curvePublic, info); |
|||
$messages.append(chatbox); |
|||
}); |
|||
}; |
|||
|
|||
var removeFriend = function (curvePublic) { |
|||
messenger.removeFriend(curvePublic, function (e /*, removed */) { |
|||
if (e) { return void console.error(e); } |
|||
find.inList(curvePublic).remove(); |
|||
//console.log(removed);
|
|||
}); |
|||
}; |
|||
|
|||
markup.friend = function (data) { |
|||
var curvePublic = data.curvePublic; |
|||
var friend = h('div.cp-app-contacts-friend.cp-avatar', { |
|||
'data-key': curvePublic, |
|||
}); |
|||
|
|||
var remove = h('span.cp-app-contacts-remove.fa.fa-user-times', { |
|||
title: Messages.contacts_remove |
|||
}); |
|||
var status = h('span.cp-app-contacts-status'); |
|||
var rightCol = h('span.cp-app-contacts-right-col', [ |
|||
h('span.cp-app-contacts-name', [data.displayName]), |
|||
remove, |
|||
]); |
|||
|
|||
var $friend = $(friend) |
|||
.click(function () { |
|||
display(curvePublic); |
|||
}) |
|||
.dblclick(function () { |
|||
if (data.profile) { window.open(origin + '/profile/#' + data.profile); } |
|||
}); |
|||
|
|||
$(remove).click(function (e) { |
|||
e.stopPropagation(); |
|||
Cryptpad.confirm(Messages._getKey('contacts_confirmRemove', [ |
|||
Cryptpad.fixHTML(data.displayName) |
|||
]), function (yes) { |
|||
if (!yes) { return; } |
|||
removeFriend(curvePublic, function (e) { |
|||
if (e) { return void console.error(e); } |
|||
}); |
|||
// TODO remove friend from userlist ui
|
|||
// FIXME seems to trigger EJOINED from netflux-websocket (from server);
|
|||
// (tried to join a channel in which you were already present)
|
|||
}, undefined, true); |
|||
}); |
|||
|
|||
if (data.avatar && avatars[data.avatar]) { |
|||
$friend.append(avatars[data.avatar]); |
|||
$friend.append(rightCol); |
|||
} else { |
|||
common.displayAvatar($friend, data.avatar, data.displayName, function ($img) { |
|||
if (data.avatar && $img) { |
|||
avatars[data.avatar] = $img[0].outerHTML; |
|||
} |
|||
$friend.append(rightCol); |
|||
}); |
|||
} |
|||
$friend.append(status); |
|||
return $friend; |
|||
}; |
|||
|
|||
var isBottomedOut = function ($elem) { |
|||
return ($elem[0].scrollHeight - $elem.scrollTop() === $elem.outerHeight()); |
|||
}; |
|||
|
|||
var initializing = true; |
|||
messenger.on('message', function (message) { |
|||
if (!initializing) { Cryptpad.notify(); } |
|||
var curvePublic = message.curve; |
|||
|
|||
var name = displayNames[curvePublic]; |
|||
var chat = getChat(curvePublic, name); |
|||
|
|||
console.log(message); |
|||
|
|||
var el_message = markup.message(message); |
|||
|
|||
state.channels[curvePublic].messages.push(message); |
|||
|
|||
var $chat = $(chat); |
|||
|
|||
if (!$chat.length) { |
|||
console.error("Got a message but the chat isn't open"); |
|||
} |
|||
|
|||
var $messagebox = $chat.find('.cp-app-contacts-messages'); |
|||
var shouldScroll = isBottomedOut($messagebox); |
|||
|
|||
$messagebox.append(el_message); |
|||
|
|||
if (shouldScroll) { |
|||
$messagebox.scrollTop($messagebox.outerHeight()); |
|||
} |
|||
normalizeLabels($messagebox); |
|||
|
|||
var channel = state.channels[curvePublic]; |
|||
if (!channel) { |
|||
console.error('expected channel [%s] to be open', curvePublic); |
|||
return; |
|||
} |
|||
|
|||
if (isActive(curvePublic)) { |
|||
channel.HEAD = message.sig; |
|||
messenger.setChannelHead(curvePublic, message.sig, function (e) { |
|||
if (e) { return void console.error(e); } |
|||
}); |
|||
return; |
|||
} |
|||
var lastMsg = channel.messages.slice(-1)[0]; |
|||
if (lastMsg.sig !== channel.HEAD) { |
|||
return void notify(curvePublic); |
|||
} |
|||
unnotify(curvePublic); |
|||
}); |
|||
|
|||
messenger.on('join', function (curvePublic, channel) { |
|||
channel = channel; |
|||
updateStatus(curvePublic); |
|||
}); |
|||
messenger.on('leave', function (curvePublic, channel) { |
|||
channel = channel; |
|||
updateStatus(curvePublic); |
|||
}); |
|||
|
|||
// change in your friend list
|
|||
messenger.on('update', function (info, curvePublic) { |
|||
var name = displayNames[curvePublic] = info.displayName; |
|||
|
|||
// update label in friend list
|
|||
find.inList(curvePublic).find('.cp-app-contacts-name').text(name); |
|||
|
|||
// update title bar and messages
|
|||
$messages.find(dataQuery(curvePublic) + ' .cp-app-contacts-header ' + |
|||
'.cp-app-contacts-name, div.cp-app-contacts-message'+ |
|||
dataQuery(curvePublic) + ' div.cp-app-contacts-sender').text(name).text(name); |
|||
}); |
|||
|
|||
var connectToFriend = function (curvePublic, cb) { |
|||
messenger.getFriendInfo(curvePublic, function (e, info) { |
|||
if (e) { return void console.error(e); } |
|||
var name = displayNames[curvePublic] = info.displayName; |
|||
initChannel(state, curvePublic, info); |
|||
|
|||
var chatbox = markup.chatbox(curvePublic, info); |
|||
$(chatbox).hide(); |
|||
$messages.append(chatbox); |
|||
|
|||
var friend = markup.friend(info, name); |
|||
$userlist.append(friend); |
|||
messenger.openFriendChannel(curvePublic, function (e) { |
|||
if (e) { return void console.error(e); } |
|||
cb(); |
|||
updateStatus(curvePublic); |
|||
// don't add friends that are already in your userlist
|
|||
//if (friendExistsInUserList(k)) { return; }
|
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
messenger.on('friend', function (curvePublic) { |
|||
console.log('new friend: ', curvePublic); |
|||
//console.error("TODO redraw user list");
|
|||
//console.error("TODO connect to new friend");
|
|||
// FIXME this doesn't work right now because the friend hasn't been fully added?
|
|||
connectToFriend(curvePublic, function () { |
|||
//console.error('connected');
|
|||
}); |
|||
}); |
|||
|
|||
messenger.on('unfriend', function (curvePublic) { |
|||
console.log('unfriend', curvePublic); |
|||
find.inList(curvePublic).remove(); |
|||
console.error('TODO remove chatbox'); |
|||
console.error('TODO show something if that chatbox was active'); |
|||
}); |
|||
|
|||
Cryptpad.onDisplayNameChanged(function () { |
|||
//messenger.checkNewFriends();
|
|||
messenger.updateMyData(); |
|||
}); |
|||
|
|||
// FIXME dirty hack
|
|||
messenger.getMyInfo(function (e, info) { |
|||
displayNames[info.curvePublic] = info.displayName; |
|||
}); |
|||
|
|||
messenger.getFriendList(function (e, keys) { |
|||
var count = keys.length + 1; |
|||
var ready = function () { |
|||
count--; |
|||
if (count === 0) { |
|||
initializing = false; |
|||
Cryptpad.removeLoadingScreen(); |
|||
} |
|||
}; |
|||
ready(); |
|||
keys.forEach(function (curvePublic) { |
|||
connectToFriend(curvePublic, ready); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
return UI; |
|||
}); |
|||
Write
Preview
Loading…
Cancel
Save