diff --git a/customize.dist/src/less2/include/framework.less b/customize.dist/src/less2/include/framework.less index 4853f663b..a273e8f77 100644 --- a/customize.dist/src/less2/include/framework.less +++ b/customize.dist/src/less2/include/framework.less @@ -12,6 +12,7 @@ @import (reference) './font.less'; @import (reference) "./app-print.less"; @import (reference) "./app-noscroll.less"; +@import (reference) "./messenger.less"; .framework_main(@bg-color, @warn-color, @color) { --LessLoader_require: LessLoader_currentFile(); @@ -36,6 +37,7 @@ .tippy_main(); .checkmark_main(20px); .password_main(); + .messenger_main(); .creation_main( @bg-color: @bg-color, @color: @color diff --git a/customize.dist/src/less2/include/messenger.less b/customize.dist/src/less2/include/messenger.less new file mode 100644 index 000000000..cdac34752 --- /dev/null +++ b/customize.dist/src/less2/include/messenger.less @@ -0,0 +1,252 @@ +@import (reference) './avatar.less'; +@import (reference) "./colortheme-all.less"; + +.messenger_main() { + --LessLoader_require: LessLoader_currentFile(); +}; +& { + @keyframes example { + 0% { + background: rgba(0,0,0,0.1); + } + 50% { + background: rgba(0,0,0,0.3); + } + 100% { + background: rgba(0,0,0,0.1); + } + } + + @button-border: 2px; + @bg-color: @colortheme_friends-bg; + @color: @colortheme_friends-color; + + #cp-app-contacts-container { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + min-height: 0; + &.ready { + background-size: cover; + background-position: center; + } + } + + .cp-app-contacts-spinner { + display: none; + } + + .cp-app-contacts-initializing { + .cp-app-contacts-spinner { + color: white; + display: block; + } + .cp-app-contacts-info { + display: none; + } + #cp-app-contacts-friendlist, + #cp-app-contacts-messaging { + display: flex; + justify-content: center; + align-items: center; + } + } + + #cp-app-contacts-friendlist { + width: 350px; + max-width: 30%; + height: 100%; + background-color: lighten(@bg-color, 10%); + overflow-y: auto; + .cp-app-contacts-friend { + background: rgba(0,0,0,0.1); + padding: 5px; + margin: 10px; + cursor: pointer; + position: relative; + .cp-app-contacts-right-col { + margin-left: 5px; + display: flex; + flex-flow: column; + } + &:hover { + background-color: rgba(0,0,0,0.3); + } + &.cp-app-contacts-notify { + animation: example 2s ease-in-out infinite; + } + } + .cp-app-contacts-remove { + cursor: pointer; + width: 20px; + &:hover { + color: darken(@color, 20%); + } + } + } + + #cp-app-contacts-friendlist .cp-app-contacts-friend, #cp-app-contacts-messaging .cp-avatar { + .avatar_main(30px); + &.cp-avatar { + display: flex; + } + cursor: pointer; + color: @color; + media-tag { + img { + color: #000; + } + } + media-tag, .cp-avatar-default { + margin-right: 5px; + } + .cp-app-contacts-status { + width: 5px; + display: inline-block; + position: absolute; + right: 0; + top: 0; + bottom: 0; + opacity: 0.7; + background-color: #777; + &.cp-app-contacts-online { + background-color: green; + } + &.cp-app-contacts-offline { + background-color: red; + } + } + } + + .placeholder (@color: #bbb) { + &::-webkit-input-placeholder { /* WebKit, Blink, Edge */ + color: @color; + } + &:-moz-placeholder { /* Mozilla Firefox 4 to 18 */ + color: @color; + opacity: 1; + } + &::-moz-placeholder { /* Mozilla Firefox 19+ */ + color: @color; + opacity: 1; + } + &:-ms-input-placeholder { /* Internet Explorer 10-11 */ + color: @color; + } + &::-ms-input-placeholder { /* Microsoft Edge */ + color: @color; + } + } + + #cp-app-contacts-messaging { + flex: 1; + height: 100%; + background-color: lighten(@bg-color, 20%); + min-width: 0; + + .cp-app-contacts-info { + padding: 20px; + } + .cp-app-contacts-header { + background-color: lighten(@bg-color, 15%); + padding: 0; + display: flex; + justify-content: space-between; + align-items: center; + height: 50px; + + .hover () { + height: 100%; + line-height: 30px; + padding: 10px; + &:hover { + background-color: rgba(50,50,50,0.3); + } + } + + .cp-avatar, + .cp-app-contacts-right-col { + flex:1 1 auto; + } + .cp-app-contacts-remove-history { + .hover; + } + .cp-avatar { + margin: 10px; + } + .cp-app-contacts-more-history { + //display: none; + .hover; + &.cp-app-contacts-faded { + color: darken(@bg-color, 5%); + } + } + } + .cp-app-contacts-chat { + height: 100%; + display: flex; + flex-flow: column; + .cp-app-contacts-messages { + padding: 0 20px; + margin: 10px 0; + flex: 1; + overflow-x: auto; + .cp-app-contacts-message { + & > div { + padding: 0 10px; + } + .cp-app-contacts-content { + overflow: hidden; + word-wrap: break-word; + &> * { + margin: 0; + } + } + .cp-app-contacts-date { + display: none; + font-style: italic; + } + .cp-app-contacts-sender { + margin-top: 10px; + font-weight: bold; + background-color: rgba(0,0,0,0.1); + } + } + } + } + .cp-app-contacts-input { + background-color: lighten(@bg-color, 15%); + height: auto; + min-height: 50px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 5%; + textarea { + margin: 5px 0; + padding: 5px 10px; + border: none; + height: 50px; + flex: 1; + background-color: darken(@bg-color, 10%); + color: @color; + resize: none; + overflow-y: auto; + .placeholder(#bbb); + &[disabled=true] { + .placeholder(#999); + } + } + button { + height: 50px; + border-radius: 0; + border: none; + background-color: darken(@bg-color, 15%); + &:hover { + background-color: darken(@bg-color, 20%); + } + } + } + } +} diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 858c0a8d9..5a17cc291 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -134,9 +134,37 @@ } } - .cp-toolbar-userlist-drawer { + .cp-toolbar-chat-drawer { background-color: @toolbar-bg-color; background-color: var(--toolbar-bg-color); + font: @colortheme_app-font-size @colortheme_font; + width: 400px; + display: block; + overflow-y: auto; + overflow-x: hidden; + padding: 0; + box-sizing: border-box; + position: relative; + order: -2; + #cp-app-contacts-container { + height: 100%; + } + .cp-toolbar-chat-drawer-close { + color: @toolbar-color; + color: var(--toolbar-color); + position: absolute; + top: 0; + right: 1px; + font-size: 15px; + opacity: 0.5; + cursor: pointer; + text-shadow: unset; + &:hover { + opacity: 1; + } + } + } + .cp-toolbar-userlist-drawer { font: @colortheme_app-font-size @colortheme_font; min-width: 175px; width: 175px; @@ -145,6 +173,7 @@ overflow-x: hidden; padding: 10px; box-sizing: border-box; + order: -1; .cp-toolbar-userlist-drawer-close { position: absolute; margin-top: -10px; 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..73275a2cc 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: [], @@ -65,6 +67,7 @@ define([ update: [], friend: [], unfriend: [], + ready: [] }, range_requests: {}, }; @@ -95,19 +98,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, sig, cb) { messenger.range_requests[txid] = { messages: [], cb: cb, - curvePublic: curvePublic, + chanId: chanId, sig: sig, }; }; @@ -120,24 +130,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, hash, cb); var msg = [ 'GET_HISTORY_RANGE', chan.id, { from: hash, count: count, @@ -151,23 +159,58 @@ 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 { + // 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) { + if (curve === proxy.curvePublic) { return; } + var friend = getFriend(proxy, curve); + 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); + }); }; // Id message allows us to map a netfluxId with a public curve key @@ -206,20 +249,22 @@ 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]); 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 orderMessages = function (curvePublic, new_messages /*, sig */) { - var channel = getChannel(curvePublic); + var orderMessages = function (channel, new_messages /*, sig */) { var messages = channel.messages; // TODO improve performance, guarantee correct ordering @@ -250,8 +295,9 @@ define([ author: parsedMsg[1], time: parsedMsg[2], text: parsedMsg[3], + channel: channel.id // this makes debugging a whole lot easier - curve: getCurveForChannel(channel.id), + //curve: getCurveForChannel(channel.id), }; channel.messages.push(res); @@ -262,31 +308,21 @@ define([ 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]); 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 +360,7 @@ define([ }); }); eachHandler('update', function (f) { - f(myData, myData.curvePublic); + f(myData, ['displayName', 'profile', 'avatar']); }); friends.me = myData; } @@ -356,8 +392,7 @@ define([ 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; } @@ -379,11 +414,11 @@ define([ author: O.d[1], time: O.d[2], text: O.d[3], - curve: curvePublic, + channel: req.chanId }; }); - orderMessages(curvePublic, decrypted, req.sig); + orderMessages(channel, decrypted, req.sig); req.cb(void 0, decrypted); return deleteRangeRequest(txid); } else { @@ -395,6 +430,7 @@ 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 @@ -409,6 +445,7 @@ define([ } return; } + // Initial history message var chan = parsed[3]; if (!chan || !channels[chan]) { return; } pushMsg(channels[chan], parsed[4]); @@ -440,7 +477,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 +495,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,7 +514,7 @@ 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 || ''); var cfg = { validateKey: keys.validateKey, owners: [proxy.edPublic, data.edPublic], @@ -489,39 +527,19 @@ define([ }); }; - var openFriendChannel = function (data, f) { - var keys = Curve.deriveKeys(data.curvePublic, proxy.curvePrivate); + var openChannel = function (data) { + var keys = data.keys; var encryptor = Curve.createEncryptor(keys); network.join(data.channel).then(function (chan) { var channel = channels[data.channel] = { id: data.channel, + isFriendChat: data.isFriendChat, 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); @@ -530,9 +548,12 @@ define([ 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]; + + // 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, chan.myID]; var msgStr = JSON.stringify(msg); var cryptMsg = channel.encryptor.encrypt(msgStr); network.sendto(peer, cryptMsg); @@ -544,16 +565,24 @@ define([ }); 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; } + 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(curvePublic, channel.id); + f(otherData, channel.id); }); }); @@ -573,7 +602,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 +614,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 +626,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 +640,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 +674,163 @@ 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; } + if (typeof(n) === 'undefined') { + // Handled by .on('remove') + return; + } console.error(o, n, p); }).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 init = function () { + 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 () { + // TODO send event chat ready + // Remove spinner in chatbox + ready = true; + eachHandler('ready', function (f) { + f(); + }); + }); + }; + init(); + + var getRooms = function (curvePublic, cb) { + if (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 + }]); + } + + 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 { + // TODO room get metadata (name) && lastKnownHash + } + return { + id: r.id, + isFriendChat: r.isFriendChat, + name: name, + lastKnownHash: lastKnownHash, + curvePublic: curvePublic + }; + }).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) + } + }; + + messenger.execCommand = function (obj, cb) { + var cmd = obj.cmd; + var data = obj.data; + 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); + } + }; + Object.freeze(messenger); return messenger; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 2b29ff8ec..75bc92129 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -622,6 +622,12 @@ define([ messenger.setChannelHead = function (data, cb) { postMessage("CONTACTS_SET_CHANNEL_HEAD", data, cb); }; + + messenger.execCommand = function (data, cb) { + postMessage("CHAT_COMMAND", data, cb); + }; + + messenger.onEvent = Util.mkEvent(); messenger.onMessageEvent = Util.mkEvent(); messenger.onJoinEvent = Util.mkEvent(); messenger.onLeaveEvent = Util.mkEvent(); @@ -1059,6 +1065,8 @@ define([ CONTACTS_UPDATE: common.messenger.onUpdateEvent.fire, CONTACTS_FRIEND: common.messenger.onFriendEvent.fire, CONTACTS_UNFRIEND: common.messenger.onUnfriendEvent.fire, + // Chat + CHAT_EVENT: common.messenger.onEvent.fire, // Pad PAD_READY: common.padRpc.onReadyEvent.fire, PAD_MESSAGE: common.padRpc.onMessageEvent.fire, diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 2be4ded4f..23594db6e 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -59,6 +59,9 @@ define([ obj[key] = data.value; } broadcast([clientId], "UPDATE_METADATA"); + if (Array.isArray(path) && path[0] === 'profile' && store.messenger) { + store.messenger.updateMyData(); + } onSync(cb); }; @@ -597,6 +600,7 @@ define([ Store.setDisplayName = function (clientId, value, cb) { store.proxy[Constants.displayNameKey] = value; broadcast([clientId], "UPDATE_METADATA"); + if (store.messenger) { store.messenger.updateMyData(); } onSync(cb); }; @@ -859,6 +863,9 @@ define([ }, pinPads: function (data, cb) { Store.pinPads(null, data, cb); }, friendComplete: function (data) { + if (data.friend && store.messenger && store.messenger.onFriendAdded) { + store.messenger.onFriendAdded(data.friend); + } postMessage(clientId, "EV_FRIEND_COMPLETE", data); }, friendRequest: function (data, cb) { @@ -957,6 +964,10 @@ define([ error: e }); }); + }, + + execCommand: function (clientId, data, cb) { + store.messenger.execCommand(data, cb); } }; @@ -1317,7 +1328,6 @@ define([ } }; - var messengerEventInit = false; var sendMessengerEvent = function (q, data) { messengerEventClients.forEach(function (cId) { postMessage(cId, q, data); @@ -1327,41 +1337,47 @@ define([ if (messengerEventClients.indexOf(clientId) === -1) { messengerEventClients.push(clientId); } - if (!messengerEventInit) { - var messenger = store.messenger = Messenger.messenger(store); - messenger.on('message', function (message) { - sendMessengerEvent('CONTACTS_MESSAGE', message); + }; + var loadMessenger = function () { + var messenger = store.messenger = Messenger.messenger(store); + messenger.on('message', function (message) { + sendMessengerEvent('CONTACTS_MESSAGE', message); + }); + messenger.on('join', function (curvePublic, channel) { + sendMessengerEvent('CONTACTS_JOIN', { + curvePublic: curvePublic, + channel: channel, }); - messenger.on('join', function (curvePublic, channel) { - sendMessengerEvent('CONTACTS_JOIN', { - curvePublic: curvePublic, - channel: channel, - }); + }); + messenger.on('leave', function (curvePublic, channel) { + sendMessengerEvent('CONTACTS_LEAVE', { + curvePublic: curvePublic, + channel: channel, }); - messenger.on('leave', function (curvePublic, channel) { - sendMessengerEvent('CONTACTS_LEAVE', { - curvePublic: curvePublic, - channel: channel, - }); + }); + messenger.on('update', function (info, types) { + sendMessengerEvent('CONTACTS_UPDATE', { + types: types, + info: info, }); - messenger.on('update', function (info, curvePublic) { - sendMessengerEvent('CONTACTS_UPDATE', { - curvePublic: curvePublic, - info: info, - }); + }); + messenger.on('friend', function (curvePublic) { + sendMessengerEvent('CONTACTS_FRIEND', { + curvePublic: curvePublic, }); - messenger.on('friend', function (curvePublic) { - sendMessengerEvent('CONTACTS_FRIEND', { - curvePublic: curvePublic, - }); + }); + messenger.on('unfriend', function (curvePublic, removedByMe) { + sendMessengerEvent('CONTACTS_UNFRIEND', { + curvePublic: curvePublic, + removedByMe: removedByMe }); - messenger.on('unfriend', function (curvePublic) { - sendMessengerEvent('CONTACTS_UNFRIEND', { - curvePublic: curvePublic, - }); + }); + messenger.on('ready', function () { + console.log('here'); + sendMessengerEvent('CHAT_EVENT', { + ev: 'READY' }); - messengerEventInit = true; - } + }); }; @@ -1450,6 +1466,7 @@ define([ }); userObject.fixFiles(); loadSharedFolders(waitFor); + loadMessenger(); }).nThen(function () { var requestLogin = function () { broadcast([], "REQUEST_LOGIN"); diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 532b4c10e..a724fbd06 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -69,6 +69,8 @@ define([ CONTACTS_GET_MORE_HISTORY: Store.messenger.getMoreHistory, CONTACTS_SEND_MESSAGE: Store.messenger.sendMessage, CONTACTS_SET_CHANNEL_HEAD: Store.messenger.setChannelHead, + // Chat + CHAT_COMMAND: Store.messenger.execCommand, // Pad SEND_PAD_MSG: Store.sendPadMsg, JOIN_PAD: Store.joinPad, diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 7266ac0c9..cb6cb4acd 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -559,6 +559,7 @@ define([ }, onLocal); var configTb = { displayed: [ + 'chat', 'userlist', 'title', 'useradmin', diff --git a/www/common/sframe-app-outer.js b/www/common/sframe-app-outer.js index b65f3f98b..cc4d5fcb3 100644 --- a/www/common/sframe-app-outer.js +++ b/www/common/sframe-app-outer.js @@ -36,7 +36,8 @@ define([ window.addEventListener('message', onMsg); }).nThen(function (/*waitFor*/) { SFCommonO.start({ - useCreationScreen: true + useCreationScreen: true, + messaging: true }); }); }); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 43728c55c..0bdabf981 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -776,6 +776,14 @@ define([ Cryptpad.messenger.setChannelHead(opt, cb); }); + sframeChan.on('Q_CHAT_COMMAND', function (data, cb) { + Cryptpad.messenger.execCommand(data, cb); + }); + Cryptpad.messenger.onEvent.reg(function (data) { + console.log(data); + sframeChan.event('EV_CHAT_EVENT', data); + }); + Cryptpad.messenger.onMessageEvent.reg(function (data) { sframeChan.event('EV_CONTACTS_MESSAGE', data); }); diff --git a/www/common/sframe-messenger-inner.js b/www/common/sframe-messenger-inner.js index 67532b015..b277a1e58 100644 --- a/www/common/sframe-messenger-inner.js +++ b/www/common/sframe-messenger-inner.js @@ -35,7 +35,7 @@ define([], function () { }); sFrameChan.on('EV_CONTACTS_UPDATE', function (data) { _handlers.update.forEach(function (f) { - f(data.info, data.curvePublic); + f(data.info, data.types); }); }); sFrameChan.on('EV_CONTACTS_FRIEND', function (data) { @@ -45,7 +45,7 @@ define([], function () { }); sFrameChan.on('EV_CONTACTS_UNFRIEND', function (data) { _handlers.unfriend.forEach(function (f) { - f(data.curvePublic); + f(data.curvePublic, data.removedByMe); }); }); diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index 02390b290..a61562887 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -171,6 +171,10 @@ define({ 'Q_CONTACTS_SET_CHANNEL_HEAD': true, 'Q_CONTACTS_CLEAR_OWNED_CHANNEL': true, + // Chat + 'EV_CHAT_EVENT': true, + 'Q_CHAT_COMMAND': true, + // Put one or more entries to the localStore which will go in localStorage. 'EV_LOCALSTORE_PUT': true, // Put one entry in the parent sessionStorage diff --git a/www/common/toolbar3.js b/www/common/toolbar3.js index 35fce658d..840afd017 100644 --- a/www/common/toolbar3.js +++ b/www/common/toolbar3.js @@ -6,8 +6,11 @@ define([ '/common/common-interface.js', '/common/common-hash.js', '/common/common-feedback.js', + '/common/sframe-messenger-inner.js', + '/contacts/messenger-ui.js', '/customize/messages.js', -], function ($, Config, ApiConfig, UIElements, UI, Hash, Feedback, Messages) { +], function ($, Config, ApiConfig, UIElements, UI, Hash, Feedback, +Messenger, MessengerUI, Messages) { var Common; var Bar = { @@ -410,6 +413,72 @@ define([ return $container; }; + var initChat = function (toolbar, config) { + var $container = $('
', {'class': USERLIST_CLS}).appendTo($content); + + toolbar.chatContent = $content; + + var $container = $('', {id: 'cp-toolbar-chat-drawer-open', title: Messages.chatButton || 'CHAT'}); //XXX + + var $button = $('