diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 5ddbb27c9..af4410010 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -489,6 +489,10 @@ define([ return loadingScreen(); }; + Pages['/friends/'] = Pages['/friends/index.html'] = function () { + return loadingScreen(); + }; + Pages['/pad/'] = Pages['/pad/index.html'] = function () { return loadingScreen(); }; @@ -501,5 +505,9 @@ define([ return loadingScreen(); }; + Pages['/invite/'] = Pages['/invite/index.html'] = function () { + return loadingScreen(); + }; + return Pages; }); diff --git a/customize.dist/src/less/toolbar.less b/customize.dist/src/less/toolbar.less index 80f33fd2a..0c4ae362e 100644 --- a/customize.dist/src/less/toolbar.less +++ b/customize.dist/src/less/toolbar.less @@ -21,6 +21,7 @@ .slideColor { color: @toolbar-slide-bg; } .pollColor { color: @toolbar-poll-bg; } .fileColor { color: @toolbar-file-bg; } +.friendsColor { color: @toolbar-friends-bg; } .whiteboardColor { color: @toolbar-whiteboard-bg; } .driveColor { color: @toolbar-drive-bg; } .defaultColor { color: @toolbar-default-bg; } @@ -255,6 +256,11 @@ body { @color: @toolbar-file-color; .addToolbarColors(@color, @bgcolor); } + &.app-friends { + @bgcolor: @toolbar-friends-bg; + @color: @toolbar-friends-color; + .addToolbarColors(@color, @bgcolor); + } } diff --git a/customize.dist/src/less/variables.less b/customize.dist/src/less/variables.less index 2ccebf170..63c737266 100644 --- a/customize.dist/src/less/variables.less +++ b/customize.dist/src/less/variables.less @@ -97,6 +97,8 @@ @toolbar-drive-color: #fff; @toolbar-file-bg: #cd2532; @toolbar-file-color: #fff; +@toolbar-friends-bg: #607B8D; +@toolbar-friends-color: #fff; @toolbar-default-bg: #ddd; @toolbar-default-color: #000; diff --git a/customize.dist/template.js b/customize.dist/template.js index ad00c5170..f6bc7cd9c 100644 --- a/customize.dist/template.js +++ b/customize.dist/template.js @@ -12,7 +12,7 @@ $(function () { var Messages = Cryptpad.Messages; var $body = $('body'); var isMainApp = function () { - return /^\/(pad|code|slide|poll|whiteboard|file|media|drive)\/$/.test(location.pathname); + return /^\/(pad|code|slide|poll|whiteboard|file|media|friends|drive)\/$/.test(location.pathname); }; var rightLink = function (ref, loc, txt) { @@ -148,6 +148,9 @@ $(function () { } else if (/file/.test(pathname)) { $('body').append(h('body', Pages[pathname]()).innerHTML); require([ '/file/main.js' ], ready); + } else if (/friends/.test(pathname)) { + $('body').append(h('body', Pages[pathname]()).innerHTML); + require([ '/friends/main.js' ], ready); } else if (/pad/.test(pathname)) { $('body').append(h('body', Pages[pathname]()).innerHTML); require([ '/pad/main.js' ], ready); @@ -183,6 +186,8 @@ $(function () { } else if (/^\/($|^\/index\.html$)/.test(pathname)) { // TODO use different top bar require([ '/customize/main.js', ], function () {}); + } else if (/invite/.test(pathname)) { + require([ '/invite/main.js'], function () {}); } else { require([ '/customize/main.js', ], function () {}); } diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 6fc608855..ebc8aa20a 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -256,6 +256,11 @@ define(function () { out.profile_description = "Description"; out.profile_fieldSaved = 'New value saved: {0}'; + out.profile_inviteButton = "Connect"; + out.profile_inviteButtonTitle ='Create a link that will invite this user to connect with you.'; + out.profile_inviteExplanation = "Clicking OK will create a link to a secure messaging session that only {0} will be able to redeem.

The link will be copied to your clipboard and can be shared publicly."; + out.profile_viewMyProfile = "View my profile"; + // File manager out.fm_rootName = "Documents"; diff --git a/www/common/common-hash.js b/www/common/common-hash.js index c4f452dc7..888c0a338 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -54,7 +54,7 @@ Version 1 if (!hash) { return; } var parsed = {}; var hashArr = fixDuplicateSlashes(hash).split('/'); - if (['media', 'file', 'user'].indexOf(type) === -1) { + if (['media', 'file', 'user', 'invite'].indexOf(type) === -1) { parsed.type = 'pad'; if (hash.slice(0,1) !== '/' && hash.length >= 56) { // Old hash @@ -93,6 +93,16 @@ Version 1 } return parsed; } + if (['invite'].indexOf(type) !== -1) { + parsed.type = 'invite'; + if (hashArr[1] && hashArr[1] === '1') { + parsed.version = 1; + parsed.channel = hashArr[2]; + parsed.pubkey = hashArr[3].replace(/-/g, '/'); + return parsed; + } + return parsed; + } return; }; var parsePadUrl = Hash.parsePadUrl = function (href) { @@ -320,5 +330,11 @@ Version 1 return hash; }; + Hash.createInviteUrl = function (curvePublic, channel) { + channel = channel || Hash.createChannelId(); + return window.location.origin + '/invite/#/1/' + channel + + '/' + curvePublic.replace(/\//g, '-') + '/'; + }; + return Hash; }); diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js new file mode 100644 index 000000000..23da2bee6 --- /dev/null +++ b/www/common/common-messaging.js @@ -0,0 +1,444 @@ +define([ + 'jquery', + '/bower_components/chainpad-crypto/crypto.js', + '/common/curve.js' +], function ($, Crypto, Curve) { + var Msg = {}; + + var Types = { + message: 'MSG' + }; + + // TODO: pin the chat channel! + + + // TODO: new Types + // - send a rename message to the chat + // - petnames + // - close a chat / remove a friend + + // TODO + // - mute a channel (hide notifications or don't open it?) + // + + var pending = {}; + + var createData = Msg.createData = function (common, hash) { + var proxy = common.getProxy(); + return { + channel: hash || common.createChannelId(), + displayName: proxy[common.displayNameKey], + profile: proxy.profile.view, + edPublic: proxy.edPublic, + curvePublic: proxy.curvePublic, + avatar: proxy.profile.avatar + }; + }; + + var getFriend = function (common, pubkey) { + var proxy = common.getProxy(); + if (pubkey === proxy.edPublic) { + var data = createData(common); + delete data.channel; + return data; + } + return proxy.friends ? proxy.friends[pubkey] : undefined; + }; + + var getFriendList = Msg.getFriendList = function (common) { + var proxy = common.getProxy(); + return proxy.friends || {}; + }; + + Msg.getFriendChannelsList = function (common) { + var friends = getFriendList(common); + var list = []; + Object.keys(friends).forEach(function (key) { + list.push(friends[key].channel); + }); + return list; + }; + + // Messaging tools + + var avatars = {}; + Msg.getFriendListUI = function (common, open) { + var proxy = common.getProxy(); + var $block = $('
'); + var friends = proxy.friends || {}; + Object.keys(friends).forEach(function (f) { + var data = friends[f]; + var $friend = $('
', {'class': 'friend'}).appendTo($block); + $friend.data('key', f); + var $rightCol = $('', {'class': 'right-col'}); + $('', {'class': 'name'}).text(data.displayName).appendTo($rightCol); + $friend.dblclick(function () { + window.open('/profile/#' + data.profile); + }); + $friend.click(function () { + open(data.edPublic); + }); + 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); + }); + } + }); + 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 channels = Msg.channels = window.channels = {}; + + var pushMsg = function (channel, cryptMsg) { + var msg = channel.encryptor.decrypt(cryptMsg); + var parsedMsg = JSON.parse(msg); + if (parsedMsg[0] !== Types.message) { return; } + parsedMsg.shift(); + channel.messages.push([cryptMsg.slice(0,64), parsedMsg]); + }; + + var onDirectMessage = function (msg, sender) { + if (sender !== Msg.hk) { return; } + 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; + } + 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; } + pushMsg(channels[chan.id], msg); + channels[chan.id].notify(); + channels[chan.id].refresh(); + }; + + var createChatBox = function (common, $container, edPublic) { + var data = getFriend(common, edPublic); + var proxy = common.getProxy(); + + var $header = $('
', {'class': 'header'}).appendTo($container); + $('
', {'class': 'messages'}).appendTo($container); + var $inputBlock = $('
', {'class': 'input'}).appendTo($container); + + // Input + var channel = channels[data.channel]; + var $input = $('', {type: 'text'}).appendTo($inputBlock); + var send = function () { + if (!$input.val()) { return; } + var msg = [Types.message, proxy.edPublic, +new Date(), $input.val()]; + var msgStr = JSON.stringify(msg); + var cryptMsg = channel.encryptor.encrypt(msgStr); + channel.wc.bcast(cryptMsg).then(function () { + $input.val(''); + pushMsg(channel, cryptMsg); + channel.refresh(); + }, function (err) { + console.error(err); + }); + }; + $('