" +
+ "Pour vous assurer que CryptPad soit activement développé, nous vous invitons à supporter le projet via la " +
+ 'page OpenCollective, où vous pouvez trouver notre Roadmap et nos objectifs de financement.';
+ out.crowdfunding_popup_yes = "Voir la page";
+ out.crowdfunding_popup_no = "Pas maintenant";
+ out.crowdfunding_popup_never = "Ne plus demander";
+
return out;
});
diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js
index efe898558..0a6ec0e5c 100644
--- a/customize.dist/translations/messages.js
+++ b/customize.dist/translations/messages.js
@@ -136,6 +136,8 @@ define(function () {
out.userListButton = "User list";
+ out.chatButton = "Chat";
+
out.userAccountButton = "Your account";
out.newButton = 'New';
@@ -365,6 +367,8 @@ define(function () {
out.contacts_remove = 'Remove this contact';
out.contacts_confirmRemove = 'Are you sure you want to remove {0} from your contacts?';
out.contacts_typeHere = "Type a message here...";
+ out.contacts_warning = "Everything you type here is persistent and available to all the existing and future users of this pad. Be careful with sensitive information!";
+ out.contacts_padTitle = "Chat";
out.contacts_info1 = "These are your contacts. From here, you can:";
out.contacts_info2 = "Click your contact's icon to chat with them";
@@ -376,6 +380,12 @@ define(function () {
out.contacts_removeHistoryServerError = 'There was an error while removing your chat history. Try again later';
out.contacts_fetchHistory = "Retrieve older history";
+ out.contacts_friends = "Friends";
+ out.contacts_rooms = "Rooms";
+ out.contacts_leaveRoom = "Leave this room";
+
+ out.contacts_online = "Another user from this room is online";
+
// File manager
out.fm_rootName = "Documents";
@@ -407,12 +417,13 @@ define(function () {
out.fm_openParent = "Show in folder";
out.fm_noname = "Untitled Document";
out.fm_emptyTrashDialog = "Are you sure you want to empty the trash?";
- out.fm_removeSeveralPermanentlyDialog = "Are you sure you want to remove these {0} elements from your CryptDrive permanently?";
- out.fm_removePermanentlyDialog = "Are you sure you want to remove that element from your CryptDrive permanently?";
+ out.fm_removeSeveralPermanentlyDialog = "Are you sure you want to permanently remove these {0} elements from your CryptDrive?";
+ out.fm_removePermanentlyNote = "Owned pads will be removed from the server if you continue.";
+ out.fm_removePermanentlyDialog = "Are you sure you want to permanently remove that element from your CryptDrive?";
out.fm_removeSeveralDialog = "Are you sure you want to move these {0} elements to the trash?";
out.fm_removeDialog = "Are you sure you want to move {0} to the trash?";
- out.fm_deleteOwnedPad = "Are you sure you want to remove permanently this pad from the server?";
- out.fm_deleteOwnedPads = "Are you sure you want to remove permanently these pads from the server?";
+ out.fm_deleteOwnedPad = "Are you sure you want to permanently remove this pad from the server?";
+ out.fm_deleteOwnedPads = "Are you sure you want to permanently remove these pads from the server?";
out.fm_restoreDialog = "Are you sure you want to restore {0} to its previous location?";
out.fm_unknownFolderError = "The selected or last visited directory no longer exist. Opening the parent folder...";
out.fm_contextMenuError = "Unable to open the context menu for that element. If the problem persist, try to reload the page.";
@@ -1253,5 +1264,16 @@ define(function () {
out.autostore_forceSave = "Store the file in your CryptDrive"; // File upload modal
out.autostore_notAvailable = "You must store this pad in your CryptDrive before being able to use this feature."; // Properties/tags/move to trash
+ // Crowdfunding messages
+ out.crowdfunding_home1 = "CryptPad needs your help!";
+ out.crowdfunding_home2 = "Click to learn about our crowdfunding campaign.";
+
+ out.crowdfunding_popup_text = "
We need your help!
" +
+ "To ensure that CryptPad is actively developed, consider supporting the project via the " +
+ 'OpenCollective page, where you can see our Roadmap and Funding goals.';
+ out.crowdfunding_popup_yes = "Go to OpenCollective";
+ out.crowdfunding_popup_no = "Not now";
+ out.crowdfunding_popup_never = "Don't ask me again";
+
return out;
});
diff --git a/package.json b/package.json
index 2dca65ee5..8706b37e1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
- "version": "2.7.0",
+ "version": "2.8.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",
diff --git a/www/code/app-code.less b/www/code/app-code.less
index 37a18b4ee..b2b3a1a64 100644
--- a/www/code/app-code.less
+++ b/www/code/app-code.less
@@ -19,17 +19,16 @@
flex-flow: column;
height: 100%;
min-height: 100%;
- width: 50%;
min-width: 20%;
max-width: 80%;
resize: horizontal;
overflow: hidden;
+ width: 50%;
&.cp-app-code-fullpage {
max-width: 100%;
resize: none;
flex: 1;
}
-
}
.CodeMirror {
flex: 1;
@@ -51,9 +50,13 @@
#cp-app-code-container { display: none; }
#cp-app-code-preview { border: 0; }
}
+ &.cp-chat-visible {
+ #cp-app-code-container {
+ width: 35%;
+ }
+ }
}
#cp-app-code-preview {
- flex: 1;
padding: 5px 20px;
overflow: auto;
display: inline-block;
@@ -63,6 +66,7 @@
font-family: Calibri,Ubuntu,sans-serif;
word-wrap: break-word;
position: relative;
+ flex: 1;
media-tag {
* {
max-width:100%;
diff --git a/www/common/common-constants.js b/www/common/common-constants.js
index 908134bec..c3eb447a2 100644
--- a/www/common/common-constants.js
+++ b/www/common/common-constants.js
@@ -13,6 +13,8 @@ define(function () {
storageKey: 'filesData',
tokenKey: 'loginToken',
displayPadCreationScreen: 'displayPadCreationScreen',
- deprecatedKey: 'deprecated'
+ deprecatedKey: 'deprecated',
+ // Sub
+ plan: 'CryptPad_plan'
};
});
diff --git a/www/common/common-interface.js b/www/common/common-interface.js
index b33767f48..2929b17d5 100644
--- a/www/common/common-interface.js
+++ b/www/common/common-interface.js
@@ -892,7 +892,7 @@ define([
h('div.cp-corner-filler', { style: "width:60px;" }),
h('div.cp-corner-filler', { style: "width:40px;" }),
h('div.cp-corner-filler', { style: "width:20px;" }),
- h('div.cp-corner-text', text),
+ Pages.setHTML(h('div.cp-corner-text'), text),
h('div.cp-corner-actions', actions),
Pages.setHTML(h('div.cp-corner-footer'), footer)
]);
diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js
index 1c75927fc..b0d13ec81 100644
--- a/www/common/common-messaging.js
+++ b/www/common/common-messaging.js
@@ -150,7 +150,8 @@ define([
}
cfg.friendComplete({
logText: Messages.contacts_added,
- netfluxId: sender
+ netfluxId: sender,
+ friend: msgData
});
var msg = ["FRIEND_REQ_ACK", chan];
var msgStr = Crypto.encrypt(JSON.stringify(msg), key);
@@ -163,7 +164,7 @@ define([
if (i !== -1) { pendingRequests.splice(i, 1); }
cfg.friendComplete({
logText: Messages.contacts_rejected,
- netfluxId: sender
+ netfluxId: sender,
});
cfg.updateMetadata();
return;
@@ -180,7 +181,8 @@ define([
}
cfg.friendComplete({
logText: Messages.contacts_added,
- netfluxId: sender
+ netfluxId: sender,
+ friend: data
});
});
return;
diff --git a/www/common/common-messenger.js b/www/common/common-messenger.js
index cd0a5626d..9b2b339c0 100644
--- a/www/common/common-messenger.js
+++ b/www/common/common-messenger.js
@@ -5,7 +5,9 @@ define([
'/common/common-util.js',
'/common/common-realtime.js',
'/common/common-constants.js',
-], function (Crypto, Curve, Hash, Util, Realtime, Constants) {
+
+ '/bower_components/nthen/index.js',
+], function (Crypto, Curve, Hash, Util, Realtime, Constants, nThen) {
'use strict';
var Msg = {
inputs: [],
@@ -52,7 +54,7 @@ define([
var msgAlreadyKnown = function (channel, sig) {
return channel.messages.some(function (message) {
- return message[0] === sig;
+ return message.sig === sig;
});
};
@@ -65,6 +67,7 @@ define([
update: [],
friend: [],
unfriend: [],
+ event: []
},
range_requests: {},
};
@@ -73,6 +76,12 @@ define([
messenger.handlers[type].forEach(g);
};
+ var emit = function (ev, data) {
+ eachHandler('event', function (f) {
+ f(ev, data);
+ });
+ };
+
messenger.on = function (type, f) {
var stack = messenger.handlers[type];
if (!Array.isArray(stack)) {
@@ -95,20 +104,26 @@ define([
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; }
+ var getChannel = function (chanId) {
return channels[chanId];
};
- var initRangeRequest = function (txid, curvePublic, sig, cb) {
+ var getFriendFromChannel = function (id) {
+ var friend;
+ for (var k in friends) {
+ if (friends[k].channel === id) {
+ friend = friends[k];
+ break;
+ }
+ }
+ return friend;
+ };
+
+ var initRangeRequest = function (txid, chanId, cb) {
messenger.range_requests[txid] = {
messages: [],
cb: cb,
- curvePublic: curvePublic,
- sig: sig,
+ chanId: chanId,
};
};
@@ -120,24 +135,22 @@ define([
delete messenger.range_requests[txid];
};
- messenger.getMoreHistory = function (curvePublic, hash, count, cb) {
+ messenger.getMoreHistory = function (chanId, 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;
+ // Channel is empty!
+ return void cb(void 0, []);
}
- var chan = getChannel(curvePublic);
+ var chan = getChannel(chanId);
if (typeof(chan) === 'undefined') {
console.error("chan is undefined. we're going to have a problem here");
return;
}
var txid = Util.uid();
- initRangeRequest(txid, curvePublic, hash, cb);
+ initRangeRequest(txid, chanId, cb);
var msg = [ 'GET_HISTORY_RANGE', chan.id, {
from: hash,
count: count,
@@ -151,38 +164,80 @@ define([
});
};
- var getCurveForChannel = function (id) {
+ /*var getCurveForChannel = function (id) {
var channel = channels[id];
if (!channel) { return; }
return channel.curve;
- };
+ };*/
+
+ /*messenger.getChannelHead = function (id, cb) {
+ var channel = getChannel(id);
+ if (channel.isFriendChat) {
+ var friend;
+ for (var k in friends) {
+ if (friends[k].channel === id) {
+ friend = friends[k];
+ break;
+ }
+ }
+ if (!friend) { return void cb('NO_SUCH_FRIEND'); }
+ cb(void 0, friend.lastKnownHash);
+ } else {
+ // TODO room
+ cb('NOT_IMPLEMENTED');
+ }
+ };*/
- 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 (id, hash, cb) {
+ var channel = getChannel(id);
+ if (channel.isFriendChat) {
+ var friend = getFriendFromChannel(id);
+ if (!friend) { return void cb('NO_SUCH_FRIEND'); }
+ friend.lastKnownHash = hash;
+ } else if (channel.isPadChat) {
+ // Nothing to do
+ } else {
+ // TODO room
+ return void cb('NOT_IMPLEMENTED');
+ }
+ cb();
};
- messenger.setChannelHead = function (curvePublic, hash, cb) {
- var friend = friends[curvePublic];
- if (!friend) { return void cb('NO_SUCH_FRIEND'); }
- friend.lastKnownHash = hash;
- cb();
+ // Make sure the data we have about our friends are up-to-date when we see them online
+ var checkFriendData = function (curve, data, channel) {
+ if (curve === proxy.curvePublic) { return; }
+ var friend = getFriend(proxy, curve);
+ if (!friend) { return; }
+ var types = [];
+ Object.keys(data).forEach(function (k) {
+ if (friend[k] !== data[k]) {
+ types.push(k);
+ friend[k] = data[k];
+ }
+ });
+
+ eachHandler('update', function (f) {
+ f(clone(data), types, channel);
+ });
};
// 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;
- }
- });
+ var channel, parsed0;
- if (!isId) { return; }
+ try {
+ parsed0 = JSON.parse(msg);
+ channel = channels[parsed0.channel];
+ if (!channel) { return; }
+ if (channel.userList.indexOf(sender) === -1) { return; }
+ } catch (e) {
+ console.log(msg);
+ console.error(e);
+ // Not an ID message
+ return;
+ }
- var decryptedMsg = channel.encryptor.decrypt(msg);
+ var decryptedMsg = channel.encryptor.decrypt(parsed0.msg);
if (decryptedMsg === null) {
return void console.error("Failed to decrypt message");
@@ -206,20 +261,26 @@ define([
// the sender field. This is to prevent replay attacks.
if (parsed[2] !== sender || !parsed[1]) { return; }
channel.mapId[sender] = parsed[1];
+ checkFriendData(parsed[1].curvePublic, parsed[1], channel.id);
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 myData = createData(proxy);
+ delete myData.channel;
+ var rMsg = [Types.mapIdAck, myData, channel.wc.myID];
var rMsgStr = JSON.stringify(rMsg);
var cryptMsg = channel.encryptor.encrypt(rMsgStr);
- network.sendto(sender, cryptMsg);
+ var data = {
+ channel: channel.id,
+ msg: cryptMsg
+ };
+ network.sendto(sender, JSON.stringify(data));
};
- var orderMessages = function (curvePublic, new_messages /*, sig */) {
- var channel = getChannel(curvePublic);
+ var orderMessages = function (channel, new_messages) {
var messages = channel.messages;
// TODO improve performance, guarantee correct ordering
@@ -236,9 +297,9 @@ define([
};
var pushMsg = function (channel, cryptMsg) {
- var msg = channel.encryptor.decrypt(cryptMsg);
var sig = cryptMsg.slice(0, 64);
if (msgAlreadyKnown(channel, sig)) { return; }
+ var msg = channel.encryptor.decrypt(cryptMsg);
var parsedMsg = JSON.parse(msg);
var curvePublic;
@@ -250,43 +311,38 @@ define([
author: parsedMsg[1],
time: parsedMsg[2],
text: parsedMsg[3],
+ channel: channel.id,
+ name: parsedMsg[4] // Display name for multi-user rooms
// this makes debugging a whole lot easier
- curve: getCurveForChannel(channel.id),
+ //curve: getCurveForChannel(channel.id),
};
channel.messages.push(res);
- eachHandler('message', function (f) {
- f(res);
- });
+ if (!joining[channel.id]) {
+ // Channel is ready
+ 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);
- });
+ checkFriendData(parsedMsg[1], parsedMsg[3], channel.id);
return;
}
if (parsedMsg[0] === Types.unfriend) {
curvePublic = parsedMsg[1];
- delete friends[curvePublic];
- removeFromFriendList(parsedMsg[1], function () {
+ // If this a removal from our part by in another tab, do nothing.
+ // The channel is already closed in the proxy.on('remove') part
+ if (curvePublic === proxy.curvePublic) { return; }
+
+ removeFromFriendList(curvePublic, function () {
channel.wc.leave(Types.unfriend);
+ delete channels[channel.id];
eachHandler('unfriend', function (f) {
- f(curvePublic);
+ f(curvePublic, false);
});
});
return;
@@ -324,7 +380,7 @@ define([
});
});
eachHandler('update', function (f) {
- f(myData, myData.curvePublic);
+ f(myData, ['displayName', 'profile', 'avatar']);
});
friends.me = myData;
}
@@ -352,12 +408,26 @@ define([
return void console.error("received response to unknown request");
}
+ if (!req.cb) {
+ // This is the initial history for a pad chat
+ if (type === 'HISTORY_RANGE') {
+ if (!getChannel(req.chanId)) { return; }
+ if (!Array.isArray(parsed[2])) { return; }
+ pushMsg(getChannel(req.chanId), parsed[2][4]);
+ } else if (type === 'HISTORY_RANGE_END') {
+ if (!getChannel(req.chanId)) { return; }
+ getChannel(req.chanId).ready = true;
+ onChannelReady(req.chanId);
+ return;
+ }
+ return;
+ }
+
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 channel = getChannel(req.chanId);
var decrypted = req.messages.map(function (msg) {
if (msg[2] !== 'MSG') { return; }
@@ -371,6 +441,8 @@ define([
return null;
}
}).filter(function (decrypted) {
+ if (!decrypted.d || decrypted.d[0] !== Types.message) { return; }
+ if (msgAlreadyKnown(channel, decrypted.sig)) { return; }
return decrypted;
}).map(function (O) {
return {
@@ -379,11 +451,12 @@ define([
author: O.d[1],
time: O.d[2],
text: O.d[3],
- curve: curvePublic,
+ channel: req.chanId,
+ name: O.d[4]
};
});
- orderMessages(curvePublic, decrypted, req.sig);
+ orderMessages(channel, decrypted);
req.cb(void 0, decrypted);
return deleteRangeRequest(txid);
} else {
@@ -395,20 +468,17 @@ define([
if ((parsed.validateKey || parsed.owners) && parsed.channel) {
return;
}
+ // End of initial history
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;
}
+ // Initial history message
var chan = parsed[3];
if (!chan || !channels[chan]) { return; }
pushMsg(channels[chan], parsed[4]);
@@ -440,7 +510,7 @@ define([
if (!data) {
// friend is not valid
console.error('friend is not valid');
- return;
+ return void cb('INVALID_FRIEND');
}
var channel = channels[data.channel];
@@ -458,12 +528,13 @@ define([
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];
- Realtime.whenRealtimeSyncs(realtime, function () {
+ removeFromFriendList(curvePublic, function () {
+ delete channels[channel.id];
+ eachHandler('unfriend', function (f) {
+ f(curvePublic, true);
+ });
cb();
});
}, function (err) {
@@ -476,9 +547,27 @@ define([
};
var getChannelMessagesSince = function (chan, data, keys) {
- console.log('Fetching [%s] messages since [%s]', data.curvePublic, data.lastKnownHash || '');
+ console.log('Fetching [%s] messages since [%s]', chan.id, data.lastKnownHash || '');
+
+ if (chan.isPadChat) {
+ // We need to use GET_HISTORY_RANGE to make sure we won't get the full history
+ var txid = Util.uid();
+ initRangeRequest(txid, chan.id, undefined);
+ var msg0 = ['GET_HISTORY_RANGE', chan.id, {
+ //from: hash,
+ count: 10,
+ txid: txid,
+ }
+ ];
+ network.sendto(network.historyKeeper, JSON.stringify(msg0)).then(function () {
+ }, function (err) {
+ throw new Error(err);
+ });
+ return;
+ }
+
var cfg = {
- validateKey: keys.validateKey,
+ validateKey: keys ? keys.validateKey : undefined,
owners: [proxy.edPublic, data.edPublic],
lastKnownHash: data.lastKnownHash
};
@@ -489,79 +578,88 @@ define([
});
};
- 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 openChannel = function (data) {
+ var keys = data.keys;
+ var encryptor = data.encryptor || Curve.createEncryptor(keys);
+ var channel = {
+ id: data.channel,
+ isFriendChat: data.isFriendChat,
+ isPadChat: data.isPadChat,
+ sending: false,
+ encryptor: encryptor,
+ messages: [],
+ userList: [],
+ mapId: {},
+ };
- var msg = [Types.message, proxy.curvePublic, +new Date(), payload];
- var msgStr = JSON.stringify(msg);
- var cryptMsg = channel.encryptor.encrypt(msgStr);
+ var onJoining = function (peer) {
+ if (peer === Msg.hk) { return; }
+ if (channel.userList.indexOf(peer) !== -1) { return; }
+ channel.userList.push(peer);
- channel.wc.bcast(cryptMsg).then(function () {
- pushMsg(channel, cryptMsg);
- cb();
- }, function (err) {
- cb(err);
- });
- }
+ // Join event will be sent once we are able to ID this peer
+ var myData = createData(proxy);
+ delete myData.channel;
+ var msg = [Types.mapId, myData, channel.wc.myID];
+ var msgStr = JSON.stringify(msg);
+ var cryptMsg = channel.encryptor.encrypt(msgStr);
+ var data = {
+ channel: channel.id,
+ msg: cryptMsg
};
+ network.sendto(peer, JSON.stringify(data));
+ };
+
+ var onLeaving = function (peer) {
+ var i = channel.userList.indexOf(peer);
+ while (i !== -1) {
+ channel.userList.splice(i, 1);
+ i = channel.userList.indexOf(peer);
+ }
+ // update status
+ var otherData = channel.mapId[peer];
+ if (!otherData) { return; }
+
+ // Make sure the leaving user is not connected with another netflux id
+ if (channel.userList.some(function (nId) {
+ return channel.mapId[nId]
+ && channel.mapId[nId].curvePublic === otherData.curvePublic;
+ })) { return; }
+
+ // Send the notification
+ eachHandler('leave', function (f) {
+ f(otherData, channel.id);
+ });
+ };
+
+ var onOpen = function (chan) {
+ channel.wc = chan;
+ channels[data.channel] = channel;
+
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);
- });
- });
+ chan.on('leave', onLeaving);
// FIXME don't subscribe to the channel implicitly
- getChannelMessagesSince(chan, data, keys);
- }, function (err) {
+ getChannelMessagesSince(channel, data, keys);
+ };
+ network.join(data.channel).then(onOpen, function (err) {
console.error(err);
});
+ network.on('reconnect', function () {
+ if (!channels[data.channel]) { return; }
+ network.join(data.channel).then(onOpen, function (err) {
+ console.error(err);
+ });
+ });
};
messenger.getFriendList = function (cb) {
@@ -573,7 +671,7 @@ define([
}));
};
- messenger.openFriendChannel = function (curvePublic, cb) {
+ /*messenger.openFriendChannel = function (curvePublic, cb) {
if (typeof(curvePublic) !== 'string') { return void cb('INVALID_ID'); }
if (typeof(cb) !== 'function') { throw new Error('expected callback'); }
@@ -585,10 +683,10 @@ define([
if (!channel) { return void cb('E_NO_CHANNEL'); }
joining[channel] = cb;
openFriendChannel(friend, curvePublic);
- };
+ };*/
- messenger.sendMessage = function (curvePublic, payload, cb) {
- var channel = getChannel(curvePublic);
+ messenger.sendMessage = function (id, payload, cb) {
+ var channel = getChannel(id);
if (!channel) { return void cb('NO_CHANNEL'); }
if (!network.webChannels.some(function (wc) {
if (wc.id === channel.wc.id) { return true; }
@@ -597,6 +695,9 @@ define([
}
var msg = [Types.message, proxy.curvePublic, +new Date(), payload];
+ if (!channel.isFriendChat) {
+ msg.push(proxy[Constants.displayNameKey]);
+ }
var msgStr = JSON.stringify(msg);
var cryptMsg = channel.encryptor.encrypt(msgStr);
@@ -608,18 +709,27 @@ define([
});
};
- messenger.getStatus = function (curvePublic, cb) {
- var channel = getChannel(curvePublic);
+ messenger.getStatus = function (chanId, cb) {
+ // Display green status if one member is not me
+ var channel = getChannel(chanId);
if (!channel) { return void cb('NO_SUCH_CHANNEL'); }
var online = channel.userList.some(function (nId) {
- return channel.mapId[nId] === curvePublic;
+ var data = channel.mapId[nId] || undefined;
+ if (!data) { return false; }
+ return data.curvePublic !== proxy.curvePublic;
});
cb(void 0, online);
};
- messenger.getFriendInfo = function (curvePublic, cb) {
+ messenger.getFriendInfo = function (channel, cb) {
setTimeout(function () {
- var friend = friends[curvePublic];
+ var friend;
+ for (var k in friends) {
+ if (friends[k].channel === channel) {
+ friend = friends[k];
+ break;
+ }
+ }
if (!friend) { return void cb('NO_SUCH_FRIEND'); }
// this clone will be redundant when ui uses postmessage
cb(void 0, clone(friend));
@@ -633,28 +743,215 @@ define([
});
};
- // TODO listen for changes to your friend list
- // emit 'update' events for clients
+ var loadFriend = function (friend, cb) {
+ var channel = friend.channel;
+ if (getChannel(channel)) { return void cb(); }
- //var update = function (curvePublic
+ joining[channel] = cb;
+ var keys = Curve.deriveKeys(friend.curvePublic, proxy.curvePrivate);
+ var data = {
+ keys: keys,
+ channel: friend.channel,
+ lastKnownHash: friend.lastKnownHash,
+ owners: [proxy.edPublic, friend.edPublic],
+ isFriendChat: true
+ };
+ openChannel(data);
+ };
+
+ // Detect friends changes made in another worker
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));
+
+ // Load channel
+ var friend = friends[curvePublic];
+ if (typeof(friend) !== 'object') { return; }
+ var channel = friend.channel;
+ if (!channel) { return; }
+ loadFriend(friend, function () {
+ eachHandler('friend', function (f) {
+ f(curvePublic);
+ });
});
return;
}
- console.error(o, n, p);
+ if (typeof(n) === 'undefined') {
+ // Handled by .on('remove')
+ return;
+ }
}).on('remove', ['friends'], function (o, p) {
+ var curvePublic = p[1];
+ if (!curvePublic) { return; }
+ if (p[2] !== 'channel') { return; }
+ var channel = channels[o];
+ channel.wc.leave(Types.unfriend);
+ delete channels[channel.id];
eachHandler('unfriend', function (f) {
- f(p[1]); // TODO
+ f(curvePublic, true);
});
});
+ // Friend added in our contacts in the current worker
+ messenger.onFriendAdded = function (friendData) {
+ var friend = friends[friendData.curvePublic];
+ if (typeof(friend) !== 'object') { return; }
+ var channel = friend.channel;
+ if (!channel) { return; }
+ loadFriend(friend, function () {
+ eachHandler('friend', function (f) {
+ f(friend.curvePublic);
+ });
+ });
+ };
+
+ var ready = false;
+ var initialized = false;
+ var init = function () {
+ if (initialized) { return; }
+ initialized = true;
+ var friends = getFriendList(proxy);
+
+ nThen(function (waitFor) {
+ Object.keys(friends).forEach(function (key) {
+ if (key === 'me') { return; }
+ var friend = clone(friends[key]);
+ if (typeof(friend) !== 'object') { return; }
+ var channel = friend.channel;
+ if (!channel) { return; }
+ loadFriend(friend, waitFor());
+ });
+ // TODO load rooms
+ }).nThen(function () {
+ ready = true;
+ emit('READY');
+ });
+ };
+ //init();
+
+ var getRooms = function (data, cb) {
+ if (data && data.curvePublic) {
+ var curvePublic = data.curvePublic;
+ // We need to get data about a new friend's room
+ var friend = getFriend(proxy, curvePublic);
+ if (!friend) { return void cb({error: 'NO_SUCH_FRIEND'}); }
+ var channel = getChannel(friend.channel);
+ if (!channel) { return void cb({error: 'NO_SUCH_CHANNEL'}); }
+ return void cb([{
+ id: channel.id,
+ isFriendChat: true,
+ name: friend.displayName,
+ lastKnownHash: friend.lastKnownHash,
+ curvePublic: friend.curvePublic,
+ messages: channel.messages
+ }]);
+ }
+
+ if (data && data.padChat) {
+ var pCChannel = getChannel(data.padChat);
+ if (!pCChannel) { return void cb({error: 'NO_SUCH_CHANNEL'}); }
+ return void cb([{
+ id: pCChannel.id,
+ isPadChat: true,
+ messages: pCChannel.messages
+ }]);
+ }
+
+ var rooms = Object.keys(channels).map(function (id) {
+ var r = getChannel(id);
+ var name, lastKnownHash, curvePublic;
+ if (r.isFriendChat) {
+ var friend = getFriendFromChannel(id);
+ if (!friend) { return null; }
+ name = friend.displayName;
+ lastKnownHash = friend.lastKnownHash;
+ curvePublic = friend.curvePublic;
+ } else if (r.isPadChat) {
+ return;
+ } else {
+ // TODO room get metadata (name) && lastKnownHash
+ }
+ return {
+ id: r.id,
+ isFriendChat: r.isFriendChat,
+ name: name,
+ lastKnownHash: lastKnownHash,
+ curvePublic: curvePublic,
+ messages: r.messages
+ };
+ }).filter(function (x) { return x; });
+ cb(rooms);
+ };
+
+ var getUserList = function (data, cb) {
+ var room = getChannel(data.id);
+ if (!room) { return void cb({error: 'NO_SUCH_CHANNEL'}); }
+ if (room.isFriendChat) {
+ var friend = getFriendFromChannel(data.id);
+ if (!friend) { return void cb({error: 'NO_SUCH_FRIEND'}); }
+ cb([friend]);
+ } else {
+ // TODO room userlist in rooms...
+ // (this is the static userlist, not the netflux one)
+ cb([]);
+ }
+ };
+
+ var openPadChat = function (data, cb) {
+ var channel = data.channel;
+ if (getChannel(channel)) {
+ emit('PADCHAT_READY', channel);
+ return void cb();
+ }
+ var keys = data.secret && data.secret.keys;
+ var cryptKey = keys.viewKeyStr ? Crypto.b64AddSlashes(keys.viewKeyStr) : data.secret.key;
+ var encryptor = Crypto.createEncryptor(cryptKey);
+ var chanData = {
+ encryptor: encryptor,
+ channel: data.channel,
+ isPadChat: true,
+ //lastKnownHash: friend.lastKnownHash,
+ //owners: [proxy.edPublic, friend.edPublic],
+ //isFriendChat: true
+ };
+ openChannel(chanData);
+ joining[channel] = function () {
+ emit('PADCHAT_READY', channel);
+ };
+ cb();
+ };
+
+ network.on('disconnect', function () {
+ emit('DISCONNECT');
+ });
+ network.on('reconnect', function () {
+ emit('RECONNECT');
+ });
+
+ messenger.execCommand = function (obj, cb) {
+ var cmd = obj.cmd;
+ var data = obj.data;
+ if (cmd === 'INIT_FRIENDS') {
+ init();
+ return void cb();
+ }
+ if (cmd === 'IS_READY') {
+ return void cb(ready);
+ }
+ if (cmd === 'GET_ROOMS') {
+ return void getRooms(data, cb);
+ }
+ if (cmd === 'GET_USERLIST') {
+ return void getUserList(data, cb);
+ }
+ if (cmd === 'OPEN_PAD_CHAT') {
+ return void openPadChat(data, cb);
+ }
+ };
+
Object.freeze(messenger);
return messenger;
diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js
index 3f75d3369..3422ef6c8 100644
--- a/www/common/common-ui-elements.js
+++ b/www/common/common-ui-elements.js
@@ -12,10 +12,11 @@ define([
'/common/clipboard.js',
'/customize/messages.js',
'/customize/application_config.js',
+ '/customize/pages.js',
'/bower_components/nthen/index.js',
'css!/customize/fonts/cptools/style.css'
], function ($, Config, Util, Hash, Language, UI, Constants, Feedback, h, MediaTag, Clipboard,
- Messages, AppConfig, NThen) {
+ Messages, AppConfig, Pages, NThen) {
var UIElements = {};
// Configure MediaTags to use our local viewer
@@ -630,23 +631,25 @@ define([
if (!data.FM) { return; }
var $input = $('', {
'type': 'file',
- 'style': 'display: none;'
+ 'style': 'display: none;',
+ 'multiple': 'multiple'
}).on('change', function (e) {
- var file = e.target.files[0];
- var ev = {
- target: data.target
- };
- if (data.filter && !data.filter(file)) {
- return;
- }
- if (data.transformer) {
- data.transformer(file, function (newFile) {
- data.FM.handleFile(newFile, ev);
- if (callback) { callback(); }
- });
- return;
- }
- data.FM.handleFile(file, ev);
+ var files = Util.slice(e.target.files);
+ files.forEach(function (file) {
+ var ev = {
+ target: data.target
+ };
+ if (data.filter && !data.filter(file)) {
+ return;
+ }
+ if (data.transformer) {
+ data.transformer(file, function (newFile) {
+ data.FM.handleFile(newFile, ev);
+ });
+ return;
+ }
+ data.FM.handleFile(file, ev);
+ });
if (callback) { callback(); }
});
if (data.accept) { $input.attr('accept', data.accept); }
@@ -1779,13 +1782,16 @@ define([
var $container = $('
');
var i = 0;
- AppConfig.availablePadTypes.forEach(function (p) {
+ var types = AppConfig.availablePadTypes.filter(function (p) {
if (p === 'drive') { return; }
if (p === 'contacts') { return; }
if (p === 'todo') { return; }
if (p === 'file') { return; }
if (!common.isLoggedIn() && AppConfig.registeredOnlyTypes &&
AppConfig.registeredOnlyTypes.indexOf(p) !== -1) { return; }
+ return true;
+ });
+ types.forEach(function (p) {
var $element = $('