define([ 'jquery', '/bower_components/chainpad-crypto/crypto.js', '/common/curve.js', '/bower_components/marked/marked.min.js', ], function ($, Crypto, Curve, Marked) { var Msg = { inputs: [], }; var Types = { message: 'MSG', update: 'UPDATE', unfriend: 'UNFRIEND', mapId: 'MAP_ID', mapIdAck: 'MAP_ID_ACK' }; // TODO // - mute a channel (hide notifications or don't open it?) var ready = []; var pending = {}; var pendingRequests = []; var parseMessage = function (content) { return Marked(content); }; var createData = Msg.createData = function (common, hash) { var proxy = common.getProxy(); return { channel: hash || common.createChannelId(), displayName: proxy[common.displayNameKey], profile: proxy.profile && proxy.profile.view, edPublic: proxy.edPublic, curvePublic: proxy.curvePublic, avatar: proxy.profile && proxy.profile.avatar }; }; var getFriend = function (common, pubkey) { var proxy = common.getProxy(); if (pubkey === proxy.curvePublic) { var data = createData(common); delete data.channel; return data; } return proxy.friends ? proxy.friends[pubkey] : undefined; }; var removeFromFriendList = Msg.removeFromFriendList = function (common, curvePublic, cb) { var proxy = common.getProxy(); if (!proxy.friends) { return; } var friends = proxy.friends; delete friends[curvePublic]; common.whenRealtimeSyncs(common.getRealtime(), cb); }; var getFriendList = Msg.getFriendList = function (common) { var proxy = common.getProxy(); if (!proxy.friends) { proxy.friends = {}; } return proxy.friends; }; Msg.getFriendChannelsList = function (common) { var friends = getFriendList(common); var list = []; Object.keys(friends).forEach(function (key) { if (key === "me") { return; } list.push(friends[key].channel); }); return list; }; // Messaging tools var avatars = {}; // TODO make this internal to the messenger var channels = Msg.channels = window.channels = {}; var UI = Msg.UI = {}; UI.init = function (common, $listContainer, $msgContainer) { var ui = { containers: { friendList: $listContainer, messages: $msgContainer, }, }; ui.setFriendList = function (display, remove) { UI.getFriendList(common, display, remove).appendTo($listContainer); }; ui.notify = function (curvePublic) { var $friend = $listContainer.find('.friend').filter(function (idx, el) { return $(el).data('key') === curvePublic; }); $friend.addClass('notify'); }; ui.unnotify = function (curvePublic) { var $friend = $listContainer.find('.friend').filter(function (idx, el) { return $(el).data('key') === curvePublic; }); $friend.removeClass('notify'); }; ui.update = function (curvePublic, types) { var data = getFriend(common, curvePublic); var chan = channels[data.channel]; if (!chan.ready) { chan.updateOnReady = (chan.updateOnReady || []).concat(types); return; } var $friend = $listContainer.find('.friend').filter(function (idx, el) { return $(el).data('key') === curvePublic; }); if (types.indexOf('displayName') >= 0) { $friend.find('.name').text(data.displayName); } if (types.indexOf('avatar') >= 0) { $friend.find('.default').remove(); $friend.find('media-tag').remove(); if (data.avatar && avatars[data.avatar]) { $friend.prepend(avatars[data.avatar]); } else { common.displayAvatar($friend, data.avatar, data.displayName, function ($img) { if (data.avatar && $img) { avatars[data.avatar] = $img[0].outerHTML; } }); } } }; ui.updateStatus = function (curvePublic) { var data = getFriend(common, curvePublic); var chan = channels[data.channel]; // FIXME mixes logic and presentation var $friend = $listContainer.find('.friend').filter(function (idx, el) { return $(el).data('key') === curvePublic; }); var status = chan.userList.some(function (nId) { return chan.mapId[nId] === curvePublic; }); var statusText = status ? 'online' : 'offline'; $friend.find('.status').attr('class', 'status '+statusText); }; ui.getChannel = function (curvePublic) { // TODO extract into UI method var $chat = $msgContainer.find('.chat').filter(function (idx, el) { return $(el).data('key') === curvePublic; }); return $chat.length? $chat: null; }; ui.hideInfo = function () { $msgContainer.find('.info').hide(); }; ui.showInfo = function () { $msgContainer.find('.info').show(); }; ui.createChat = function (curvePublic) { return $('
', {'class':'chat'}) .data('key', curvePublic).appendTo($msgContainer); }; ui.hideChat = function () { $msgContainer.find('.chat').hide(); }; ui.getFriend = function (curvePublic) { return $listContainer.find('.friend').filter(function (idx, el) { return $(el).data('key') === curvePublic; }); }; ui.addToFriendList = function (curvePublic, display, remove) { var $block = $listContainer.find('> div'); UI.addToFriendList(common, $block, display, remove, curvePublic); }; ui.createMessage = function (msg, name) { var $msg = $('
', {'class': 'message'}) .attr('title', msg.time ? new Date(msg.time).toLocaleString(): '?'); if (name) { $('
', {'class':'sender'}).text(name).appendTo($msg); } $('
', {'class':'content'}).html(parseMessage(msg.text)).appendTo($msg); return $msg; }; return ui; }; // internal UI.addToFriendList = function (common, $block, open, remove, f) { var proxy = common.getProxy(); var friends = proxy.friends || {}; if (f === "me") { return; } var data = friends[f]; var $friend = $('
', {'class': 'friend avatar'}).appendTo($block); $friend.data('key', f); var $rightCol = $('', {'class': 'right-col'}); $('', {'class': 'name'}).text(data.displayName).appendTo($rightCol); var $remove = $('', {'class': 'remove fa fa-user-times'}).appendTo($rightCol); $remove.attr('title', common.Messages.contacts_remove); $friend.dblclick(function () { if (data.profile) { window.open('/profile/#' + data.profile); } }); $friend.click(function () { open(data.curvePublic); }); $remove.click(function (e) { e.stopPropagation(); common.confirm(common.Messages._getKey('contacts_confirmRemove', [ common.fixHTML(data.displayName) ]), function (yes) { if (!yes) { return; } remove(data.curvePublic); }, null, 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); }); } $('', {'class': 'status'}).appendTo($friend); }; // iterate over your friends list and return a dom element containing UI UI.getFriendList = function (common, open, remove) { var proxy = common.getProxy(); // throws var $block = $('
'); var friends = proxy.friends || {}; Object.keys(friends).forEach(function (f) { UI.addToFriendList(common, $block, open, remove, f); }); return $block; }; Msg.createOwnedChannel = function (common, channelId, validateKey, owners, cb) { var network = common.getNetwork(); network.join(channelId).then(function (wc) { var cfg = { validateKey: validateKey, owners: owners }; var msg = ['GET_HISTORY', wc.id, cfg]; network.sendto(network.historyKeeper, JSON.stringify(msg)).then(cb, function (err) { throw new Error(err); }); }, function (err) { throw new Error(err); }); }; var msgAlreadyKnown = function (channel, sig) { return channel.messages.some(function (message) { return message[0] === sig; }); }; var pushMsg = function (common, channel, cryptMsg) { var msg = channel.encryptor.decrypt(cryptMsg); var sig = cryptMsg.slice(0, 64); if (msgAlreadyKnown(channel, sig)) { return; } var parsedMsg = JSON.parse(msg); if (parsedMsg[0] === Types.message) { // TODO validate messages here var res = { type: parsedMsg[0], sig: sig, channel: parsedMsg[1], time: parsedMsg[2], text: parsedMsg[3], }; channel.messages.push(res); return true; } var proxy; if (parsedMsg[0] === Types.update) { proxy = common.getProxy(); if (parsedMsg[1] === common.getProxy().curvePublic) { return; } var newdata = parsedMsg[3]; var data = getFriend(common, parsedMsg[1]); var types = []; Object.keys(newdata).forEach(function (k) { if (data[k] !== newdata[k]) { types.push(k); data[k] = newdata[k]; } }); channel.updateUI(types); return; } if (parsedMsg[0] === Types.unfriend) { proxy = common.getProxy(); removeFromFriendList(common, channel.friendEd, function () { channel.wc.leave(Types.unfriend); channel.removeUI(); }); return; } }; /* Broadcast a display name, profile, or avatar change to all contacts */ var updateMyData = function (common) { var friends = getFriendList(common); var mySyncData = friends.me; var myData = createData(common); 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]; 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 () { channel.refresh(); }, function (err) { console.error(err); }); }); friends.me = myData; } }; var onChannelReady = function (common, chanId) { if (ready.indexOf(chanId) !== -1) { return; } ready.push(chanId); channels[chanId].updateStatus(); // c'est quoi? var friends = getFriendList(common); if (ready.length === Object.keys(friends).length) { // All channels are ready updateMyData(common); } return ready.length; }; // Id message allows us to map a netfluxId with a public curve key var onIdMessage = function (common, 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) { // console.error('unable to decrypt message'); // console.error('potentially meant for yourself'); // message failed to parse, meaning somebody sent it to you but // encrypted it with the wrong key, or you're sending a message to // yourself in a different tab. return; } 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; } if (parsed[2] !== sender || !parsed[1]) { return; } channel.mapId[sender] = parsed[1]; channel.updateStatus(); if (parsed[0] !== Types.mapId) { return; } // Don't send your key if it's already an ACK // Answer with your own key var proxy = common.getProxy(); var network = common.getNetwork(); 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 onDirectMessage = function (common, msg, sender) { if (sender !== Msg.hk) { return void onIdMessage(common, msg, sender); } var parsed = JSON.parse(msg); if ((parsed.validateKey || parsed.owners) && parsed.channel) { return; } if (parsed.state && parsed.state === 1 && parsed.channel) { if (channels[parsed.channel]) { // parsed.channel is Ready // TODO: call a function that shows that the channel is ready? (remove a spinner, ...) // channel[parsed.channel].ready(); channels[parsed.channel].ready = true; onChannelReady(common, 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(common, channels[chan], parsed[4]); channels[chan].refresh(); }; var onMessage = function (common, msg, sender, chan) { if (!channels[chan.id]) { return; } var isMessage = pushMsg(common, channels[chan.id], msg); if (isMessage) { // Don't notify for your own messages if (channels[chan.id].wc.myID !== sender) { channels[chan.id].notify(); } channels[chan.id].refresh(); } }; // TODO extract into UI method var createChatBox = function (common, $container, curvePublic, messenger) { var data = getFriend(common, curvePublic); // Input var channel = channels[data.channel]; var $header = $('
', { 'class': 'header', }).appendTo($container); var $avatar = $('
', {'class': 'avatar'}).appendTo($header); // more history... $('', { 'class': 'more-history', }) .text('get more history') .click(function () { console.log("GETTING HISTORY"); channel.getPreviousMessages(); }) .appendTo($header); var $removeHistory = $('', { 'class': 'remove-history fa fa-eraser', title: common.Messages.contacts_removeHistoryTitle }) .click(function () { common.confirm(common.Messages.contacts_confirmRemoveHistory, function (yes) { if (!yes) { return; } common.clearOwnedChannel(data.channel, function (e) { if (e) { console.error(e); common.alert(common.Messages.contacts_removeHistoryServerError); return; } }); }); }); $removeHistory.appendTo($header); $('
', {'class': 'messages'}).appendTo($container); var $inputBlock = $('
', {'class': 'input'}).appendTo($container); var $input = $('