diff --git a/customize.dist/src/less2/include/notifications.less b/customize.dist/src/less2/include/notifications.less new file mode 100644 index 000000000..b0d6b22ed --- /dev/null +++ b/customize.dist/src/less2/include/notifications.less @@ -0,0 +1,43 @@ +@import (reference) "./colortheme-all.less"; + +.notifications_main() { + --LessLoader_require: LessLoader_currentFile(); +} +& { + @notif-height: 50px; + .cp-notifications-container { + max-width: 300px; + display: flex; + flex-flow: column; + .cp-notification { + height: @notif-height; + display: flex; + .cp-notification-content { + flex: 1; + min-width: 0; + p { + word-break: break-all; + } + &.cp-clickable { + cursor: pointer; + &:hover { + background-color: rgba(0,0,0,0.1); + } + } + } + .cp-notification-dismiss { + color: black; + width: 25px; + height: 100%; + display: none; + align-items: center; + justify-content: center; + span { + cursor: pointer; + } + } + } + } +} + + diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 15ef59d62..aa0f9c6f5 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -9,6 +9,7 @@ @import (reference) "./icons.less"; @import (reference) "./modal.less"; @import (reference) "./help.less"; +@import (reference) "./notifications.less"; .toolbar_vars ( @color: @colortheme_default-color, // Color of the text for the toolbar @@ -66,6 +67,7 @@ } .help_main(@color, @bg-color); + .notifications_main(); .dropdown_main(); .history_main(); .iconColors_main(); @@ -567,6 +569,22 @@ } .cp-toolbar-user { height: @toolbar_line-height; + .cp-toolbar-notifications { + height: @toolbar_line-height; + width: @toolbar_line-height; + margin-left: 0; + button { + height: @toolbar_line-height; + width: @toolbar_line-height; + font-size: 15px; + margin-top: -1px; + .cp-dropdown-button-title { + transform: scale(0.5); + bottom: -5px; + right: -5px; + } + } + } .cp-toolbar-new { height: @toolbar_line-height; width: @toolbar_line-height; @@ -834,7 +852,7 @@ line-height: 28px; // padding + border } } - .cp-toolbar-link, .cp-toolbar-new { + .cp-toolbar-link, .cp-toolbar-new, .cp-toolbar-notifications { font-size: 48px; line-height: 64px; width: @toolbar_top-height; @@ -849,14 +867,13 @@ } transition: all 0.15s; } - .cp-toolbar-new { + .cp-toolbar-notifications, .cp-toolbar-new { background-color: rgba(0,0,0,0.2); &:hover { background-color: rgba(0,0,0,0.3); } text-align: center; font-size: 32px; - margin-left: 10px; &> button { display: flex; align-items: center; @@ -884,6 +901,34 @@ } } } + .cp-toolbar-notifications { + margin-left: 10px; + .cp-notifications-empty { + color: black; + padding: 5px; + } + button { + position: relative; + &.fa-bell-o { + cursor: default; + } + .cp-dropdown-button-title { + position: absolute; + bottom: 0; + right: 0; + font-size: 14px; + border: 1px solid; + border-radius: 50%; + width: 20px; + height: 20px; + line-height: 16px; + &.cp-notifications-small { + font-size: 10px; + line-height: 17px; + } + } + } + } .cp-toolbar-link { display: inline-flex; align-items: center; @@ -917,6 +962,7 @@ order: 6; line-height: @toolbar_top-height; color: white; + .cp-toolbar-notifications { order: 1; } .cp-toolbar-new { order: 2; } .cp-toolbar-user-dropdown { order: 3; } .cp-toolbar-backup { order: 4; } // TODO drive migration to secure iframe diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js index b0d13ec81..d8cf76c43 100644 --- a/www/common/common-messaging.js +++ b/www/common/common-messaging.js @@ -7,14 +7,7 @@ define([ '/common/common-realtime.js', ], function (Crypto, Hash, Util, Constants, Messages, Realtime) { - var Msg = { - inputs: [], - }; - - // TODO - // - mute a channel (hide notifications or don't open it?) - var pending = {}; - var pendingRequests = []; + var Msg = {}; var createData = Msg.createData = function (proxy, hash) { return { @@ -23,11 +16,13 @@ define([ profile: proxy.profile && proxy.profile.view, edPublic: proxy.edPublic, curvePublic: proxy.curvePublic, + notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']), avatar: proxy.profile && proxy.profile.avatar }; }; - var getFriend = function (proxy, pubkey) { + var getFriend = Msg.getFriend = function (proxy, pubkey) { + if (!pubkey) { return; } if (pubkey === proxy.curvePublic) { var data = createData(proxy); delete data.channel; @@ -56,21 +51,17 @@ define([ return list; }; - // TODO make this internal to the messenger - var channels = Msg.channels = {}; - - Msg.getLatestMessages = function () { - Object.keys(channels).forEach(function (id) { - if (id === 'me') { return; } - var friend = channels[id]; - friend.getMessagesSinceDisconnect(); - friend.refresh(); + Msg.acceptFriendRequest = function (store, data, cb) { + var friend = getFriend(store.proxy, data.curvePublic) || {}; + var myData = createData(store.proxy, friend.channel || data.channel); + store.mailbox.sendTo('ACCEPT_FRIEND_REQUEST', myData, { + channel: data.notifications, + curvePublic: data.curvePublic + }, function (obj) { + cb(obj); }); }; - - // Invitation - // FIXME there are too many functions with this name - var addToFriendList = Msg.addToFriendList = function (cfg, data, cb) { + Msg.addToFriendList = function (cfg, data, cb) { var proxy = cfg.proxy; var friends = getFriendList(proxy); var pubKey = data.curvePublic; // todo validata data @@ -85,135 +76,6 @@ define([ if (res.error) { console.error(res.error); } }); }); - cfg.updateMetadata(); - }; - - /* Used to accept friend requests within apps other than /contacts/ */ - Msg.addDirectMessageHandler = function (cfg, href) { - var network = cfg.network; - var proxy = cfg.proxy; - if (!network) { return void console.error('Network not ready'); } - network.on('message', function (message, sender) { - var msg; - if (sender === network.historyKeeper) { return; } - try { - var parsed = Hash.parsePadUrl(href); - var secret = Hash.getSecrets(parsed.type, parsed.hash); - if (!parsed.hashData) { return; } - var chan = secret.channel; - // Decrypt - var key = secret.keys ? secret.keys.cryptKey : Hash.decodeBase64(secret.key); - var decryptMsg; - try { - decryptMsg = Crypto.decrypt(message, key); - } catch (e) { - // If we can't decrypt, it means it is not a friend request message - } - if (!decryptMsg) { return; } - // Parse - msg = JSON.parse(decryptMsg); - if (msg[1] !== chan) { return; } - var msgData = msg[2]; - var msgStr; - if (msg[0] === "FRIEND_REQ") { - msg = ["FRIEND_REQ_NOK", chan]; - var todo = function (yes) { - if (yes) { - pending[sender] = msgData; - msg = ["FRIEND_REQ_OK", chan, createData(proxy, msgData.channel)]; - } - msgStr = Crypto.encrypt(JSON.stringify(msg), key); - network.sendto(sender, msgStr); - }; - var existing = getFriend(proxy, msgData.curvePublic); - if (existing) { - todo(true); - return; - } - var confirmMsg = Messages._getKey('contacts_request', [ - Util.fixHTML(msgData.displayName) - ]); - cfg.friendRequest(confirmMsg, todo); - return; - } - if (msg[0] === "FRIEND_REQ_OK") { - var idx = pendingRequests.indexOf(sender); - if (idx !== -1) { pendingRequests.splice(idx, 1); } - - // FIXME clarify this function's name - addToFriendList(cfg, msgData, function (err) { - if (err) { - return void cfg.friendComplete({ - logText: Messages.contacts_addError, - netfluxId: sender - }); - } - cfg.friendComplete({ - logText: Messages.contacts_added, - netfluxId: sender, - friend: msgData - }); - var msg = ["FRIEND_REQ_ACK", chan]; - var msgStr = Crypto.encrypt(JSON.stringify(msg), key); - network.sendto(sender, msgStr); - }); - return; - } - if (msg[0] === "FRIEND_REQ_NOK") { - var i = pendingRequests.indexOf(sender); - if (i !== -1) { pendingRequests.splice(i, 1); } - cfg.friendComplete({ - logText: Messages.contacts_rejected, - netfluxId: sender, - }); - cfg.updateMetadata(); - return; - } - if (msg[0] === "FRIEND_REQ_ACK") { - var data = pending[sender]; - if (!data) { return; } - addToFriendList(cfg, data, function (err) { - if (err) { - return void cfg.friendComplete({ - logText: Messages.contacts_addError, - netfluxId: sender - }); - } - cfg.friendComplete({ - logText: Messages.contacts_added, - netfluxId: sender, - friend: data - }); - }); - return; - } - // TODO: timeout ACK: warn the user - } catch (e) { - console.error("Cannot parse direct message", msg || message, "from", sender, e); - } - }); - }; - - Msg.inviteFromUserlist = function (cfg, data, cb) { - var network = cfg.network; - var netfluxId = data.netfluxId; - var parsed = Hash.parsePadUrl(data.href); - var secret = Hash.getSecrets(parsed.type, parsed.hash); - if (!parsed.hashData) { return; } - // Message - var chan = secret.channel; - var myData = createData(cfg.proxy); - var msg = ["FRIEND_REQ", chan, myData]; - // Encryption - var key = secret.keys ? secret.keys.cryptKey : Hash.decodeBase64(secret.key); - var msgStr = Crypto.encrypt(JSON.stringify(msg), key); - // Send encrypted message - if (pendingRequests.indexOf(netfluxId) === -1) { - pendingRequests.push(netfluxId); - cfg.updateMetadata(); // redraws the userlist in pad - } - network.sendto(netfluxId, msgStr); - cb(); }; return Msg; diff --git a/www/common/common-messenger.js b/www/common/common-messenger.js index c333112d9..f34c177ca 100644 --- a/www/common/common-messenger.js +++ b/www/common/common-messenger.js @@ -68,7 +68,7 @@ define([ }); }; - Msg.messenger = function (store) { + Msg.messenger = function (store, updateMetadata) { var messenger = { handlers: { event: [] @@ -97,6 +97,7 @@ define([ stack.push(f); }; + var allowFriendsChannels = false; var channels = messenger.channels = {}; var joining = {}; @@ -301,7 +302,10 @@ define([ if (!proxy.friends) { return; } var friends = proxy.friends; delete friends[curvePublic]; - Realtime.whenRealtimeSyncs(realtime, cb); + Realtime.whenRealtimeSyncs(realtime, function () { + updateMetadata(); + cb(); + }); }; var pushMsg = function (channel, cryptMsg) { @@ -771,45 +775,9 @@ define([ 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]; - - // Load channel - var friend = friends[curvePublic]; - if (typeof(friend) !== 'object') { return; } - var channel = friend.channel; - if (!channel) { return; } - loadFriend(friend, function () { - emit('FRIEND', { - curvePublic: curvePublic, - }); - }); - return; - } - - 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]; - emit('UNFRIEND', { - curvePublic: curvePublic, - fromMe: true - }); - }); - // Friend added in our contacts in the current worker messenger.onFriendAdded = function (friendData) { + if (!allowFriendsChannels) { return; } var friend = friends[friendData.curvePublic]; if (typeof(friend) !== 'object') { return; } var channel = friend.channel; @@ -820,10 +788,23 @@ define([ }); }); }; + messenger.onFriendRemoved = function (curvePublic, chanId) { + var channel = channels[chanId]; + if (!channel) { return; } + if (channel.wc) { + channel.wc.leave(Types.unfriend); + } + delete channels[channel.id]; + emit('UNFRIEND', { + curvePublic: curvePublic, + fromMe: true + }); + }; var ready = false; var initialized = false; var init = function () { + allowFriendsChannels = true; if (initialized) { return; } initialized = true; var friends = getFriendList(proxy); diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 9c1d2690c..cffaa8a45 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1426,7 +1426,7 @@ define([ return /HTML/.test(Object.prototype.toString.call(o)) && typeof(o.tagName) === 'string'; }; - var allowedTags = ['a', 'p', 'hr']; + var allowedTags = ['a', 'p', 'hr', 'div']; var isValidOption = function (o) { if (typeof o !== "object") { return false; } if (isElement(o)) { return true; } @@ -2608,5 +2608,48 @@ define([ return m; }; + UIElements.displayFriendRequestModal = function (common, data) { + var msg = data.content.msg; + var text = Messages._getKey('contacts_request', [msg.content.displayName]); + + var todo = function (yes) { + common.getSframeChannel().query("Q_ANSWER_FRIEND_REQUEST", { + data: data, + value: yes + }, function (err, obj) { + var error = err || (obj && obj.error); + if (error) { + return void UI.warn(error); + } + UI.log(Messages.contacts_added); + }); + }; + + var content = h('div.cp-share-modal', [ + setHTML(h('p'), text) + ]); + var buttons = [{ + name: Messages.friendRequest_later, + onClick: function () {}, + keys: [27] + }, { + className: 'primary', + name: Messages.friendRequest_accept, + onClick: function () { + todo(true); + }, + keys: [13] + }, { + className: 'primary', + name: Messages.friendRequest_decline, + onClick: function () { + todo(false); + }, + keys: [[13, 'ctrl']] + }]; + var modal = UI.dialog.customModal(content, {buttons: buttons}); + UI.openCustomModal(modal); + }; + return UIElements; }); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 324e6cab9..21cd34472 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -611,16 +611,6 @@ define([ }); }; - // Messaging (manage friends from the userlist) - common.inviteFromUserlist = function (netfluxId, cb) { - postMessage("INVITE_FROM_USERLIST", { - netfluxId: netfluxId, - href: window.location.href - }, function (obj) { - if (obj && obj.error) { return void cb(obj.error); } - cb(); - }); - }; // Admin common.adminRpc = function (data, cb) { @@ -632,14 +622,13 @@ define([ common.onNetworkReconnect = Util.mkEvent(); common.onNewVersionReconnect = Util.mkEvent(); - // Messaging + // Messaging (friend requests) var messaging = common.messaging = {}; - messaging.onFriendRequest = Util.mkEvent(); - messaging.onFriendComplete = Util.mkEvent(); - messaging.addHandlers = function (href) { - postMessage("ADD_DIRECT_MESSAGE_HANDLERS", { - href: href - }); + messaging.answerFriendRequest = function (data, cb) { + postMessage("ANSWER_FRIEND_REQUEST", data, cb); + }; + messaging.sendFriendRequest = function (data, cb) { + postMessage("SEND_FRIEND_REQUEST", data, cb); }; // Onlyoffice @@ -1088,9 +1077,6 @@ define([ var localToken = tryParsing(localStorage.getItem(Constants.tokenKey)); if (localToken !== data.token) { requestLogin(); } }, - // Messaging - Q_FRIEND_REQUEST: common.messaging.onFriendRequest.fire, - EV_FRIEND_COMPLETE: common.messaging.onFriendComplete.fire, // Network NETWORK_DISCONNECT: common.onNetworkDisconnect.fire, NETWORK_RECONNECT: function (data) { diff --git a/www/common/notifications.js b/www/common/notifications.js new file mode 100644 index 000000000..96e31f752 --- /dev/null +++ b/www/common/notifications.js @@ -0,0 +1,58 @@ +define([ + 'jquery', + '/common/hyperscript.js', + '/common/common-ui-elements.js', + '/customize/messages.js', +], function ($, h, UIElements, Messages) { + + var handlers = {}; + + handlers['FRIEND_REQUEST'] = function (common, data, el) { + var content = data.content; + var msg = content.msg; + + // Check authenticity + if (msg.author !== msg.content.curvePublic) { return; } + + common.addFriendRequest(data); + + // Display the notification + $(el).find('.cp-notification-content').addClass("cp-clickable"); + $(el).find('.cp-notification-content p') + .html(Messages._getKey('friendRequest_notification', [msg.content.displayName]) + .click(function () { + UIElements.displayFriendRequestModal(common, data); + }); + }; + + handlers['ACCEPT_FRIEND_REQUEST'] = function (common, data, el) { + var content = data.content; + var msg = content.msg; + $(el).find('.cp-notification-content p') + .html(Messages._getKey('friendRequest_accepted', [msg.content.displayName]) + $(el).find('.cp-notification-dismiss').css('display', 'flex'); + }; + + handlers['DECLINE_FRIEND_REQUEST'] = function (common, data, el) { + var content = data.content; + var msg = content.msg; + $(el).find('.cp-notification-content p') + .html(Messages._getKey('friendRequest_declined', [msg.content.displayName]) + $(el).find('.cp-notification-dismiss').css('display', 'flex'); + }; + + return { + add: function (common, data, el) { + var type = data.content.msg.type; + + if (handlers[type]) { + handlers[type](common, data, el); + } else { + $(el).find('.cp-notification-dismiss').css('display', 'flex'); + } + }, + remove: function (common, data) { + common.removeFriendRequest(data.hash); + }, + }; +}); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index e174ecad3..d26f5ff42 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -123,6 +123,13 @@ define([ list = list.concat(fList); } + if (store.proxy.mailboxes) { + var mList = Object.keys(store.proxy.mailboxes).map(function (m) { + return store.proxy.mailboxes[m].channel; + }); + list = list.concat(mList); + } + list.push(userChannel); list.sort(); @@ -451,6 +458,7 @@ define([ avatar: Util.find(store.proxy, ['profile', 'avatar']), profile: Util.find(store.proxy, ['profile', 'view']), color: getUserColor(), + notifications: Util.find(store.proxy, ['mailboxes', 'notifications', 'channel']), curvePublic: store.proxy.curvePublic, }, // "priv" is not shared with other users but is needed by the apps @@ -460,7 +468,8 @@ define([ friends: store.proxy.friends || {}, settings: store.proxy.settings, thumbnails: disableThumbnails === false, - isDriveOwned: Boolean(Util.find(store, ['driveMetadata', 'owners'])) + isDriveOwned: Boolean(Util.find(store, ['driveMetadata', 'owners'])), + pendingFriends: store.proxy.friends_pending || {} } }; cb(JSON.parse(JSON.stringify(metadata))); @@ -626,6 +635,27 @@ define([ // Set the display name (username) in the proxy Store.setDisplayName = function (clientId, value, cb) { + if (store.mailbox && store.proxy.friends) { + // XXX test mailbox, should be removed in prod + /*store.mailbox.post('notifications', 'NAME_CHANGED', { + old: store.proxy[Constants.displayNameKey], + new: value + }); + Object.keys(store.proxy.friends).forEach(function (curve) { + var f = store.proxy.friends[curve]; + if (!f.notifications) { return; } + store.mailbox.sendTo('NAME_CHANGED', { + old: store.proxy[Constants.displayNameKey], + new: value + }, { + channel: f.notifications, + curvePublic: curve + }, function (obj) { + if (obj && obj.error) { return void console.error(obj.error); } + console.log('notif sent to '+f); + }); + });*/ + } store.proxy[Constants.displayNameKey] = value; broadcast([clientId], "UPDATE_METADATA"); if (store.messenger) { store.messenger.updateMyData(); } @@ -881,36 +911,73 @@ define([ // Messaging (manage friends from the userlist) - var getMessagingCfg = function (clientId) { - return { - proxy: store.proxy, - realtime: store.realtime, - network: store.network, - updateMetadata: function () { - postMessage(clientId, "UPDATE_METADATA"); - }, - 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) { - postMessage(clientId, "Q_FRIEND_REQUEST", data, cb); - }, + Store.answerFriendRequest = function (clientId, obj, cb) { + var value = obj.value; + var data = obj.data; + if (data.type !== 'notifications') { return void cb ({error: 'EINVAL'}); } + var hash = data.content.hash; + var msg = data.content.msg; + + var dismiss = function (cb) { + cb = cb || function () {}; + store.mailbox.dismiss({ + hash: hash, + type: 'notifications' + }, cb); }; + + // If we accept the request, add the friend to the list + if (value) { + Messaging.acceptFriendRequest(store, msg.content, function (obj) { + if (obj && obj.error) { return void cb(obj); } + Messaging.addToFriendList({ + proxy: store.proxy, + realtime: store.realtime, + pinPads: function (data, cb) { Store.pinPads(null, data, cb); }, + }, msg.content, function (err) { + if (store.messenger) { + store.messenger.onFriendAdded(msg.content); + } + broadcast([], "UPDATE_METADATA"); + if (err) { return void cb({error: err}); } + dismiss(cb); + }); + }); + return; + } + // Otherwise, just remove the notification + store.mailbox.sendTo('DECLINE_FRIEND_REQUEST', {}, { + channel: msg.content.notifications, + curvePublic: msg.content.curvePublic + }, function (obj) { + cb(obj); + }); + dismiss(); }; - Store.inviteFromUserlist = function (clientId, data, cb) { - var messagingCfg = getMessagingCfg(clientId); - Messaging.inviteFromUserlist(messagingCfg, data, cb); - }; - Store.addDirectMessageHandlers = function (clientId, data) { - var messagingCfg = getMessagingCfg(clientId); - Messaging.addDirectMessageHandler(messagingCfg, data.href); - }; + Store.sendFriendRequest = function (clientId, data, cb) { + var friend = Messaging.getFriend(store.proxy, data.curvePublic); + if (friend) { return void cb({error: 'ALREADY_FRIEND'}); } + if (!data.notifications || !data.curvePublic) { return void cb({error: 'INVALID_USER'}); } - // Messenger + store.proxy.friends_pending = store.proxy.friends_pending || {}; + + var twoDaysAgo = +new Date() - (2 * 24 * 3600 * 1000); + if (store.proxy.friends_pending[data.curvePublic] && + store.proxy.friends_pending[data.curvePublic] > twoDaysAgo) { + return void cb({error: 'TIMEOUT'}); + } + + store.proxy.friends_pending[data.curvePublic] = +new Date(); + broadcast([], "UPDATE_METADATA"); + + var myData = Messaging.createData(store.proxy); + store.mailbox.sendTo('FRIEND_REQUEST', myData, { + channel: data.notifications, + curvePublic: data.curvePublic + }, function (obj) { + cb(obj); + }); + }; // Get hashes for the share button Store.getStrongerHash = function (clientId, data, cb) { @@ -925,6 +992,7 @@ define([ cb(); }; + // Messenger Store.messenger = { execCommand: function (clientId, data, cb) { if (!store.messenger) { return void cb({error: 'Messenger is disabled'}); } @@ -951,6 +1019,7 @@ define([ // Mailbox Store.mailbox = { execCommand: function (clientId, data, cb) { + if (!store.loggedIn) { return void cb(); } if (!store.mailbox) { return void cb ({error: 'Mailbox is disabled'}); } store.mailbox.execCommand(clientId, data, cb); } @@ -1314,13 +1383,15 @@ define([ if (messengerIdx !== -1) { messengerEventClients.splice(messengerIdx, 1); } - // TODO mailbox events try { store.cursor.removeClient(clientId); } catch (e) { console.error(e); } try { store.onlyoffice.removeClient(clientId); } catch (e) { console.error(e); } + try { + store.mailbox.removeClient(clientId); + } catch (e) { console.error(e); } Object.keys(Store.channels).forEach(function (chanId) { var chanIdx = Store.channels[chanId].clients.indexOf(clientId); @@ -1385,7 +1456,9 @@ define([ }; var loadMessenger = function () { if (AppConfig.availablePadTypes.indexOf('contacts') === -1) { return; } - var messenger = store.messenger = Messenger.messenger(store); + var messenger = store.messenger = Messenger.messenger(store, function () { + broadcast([], "UPDATE_METADATA"); + }); messenger.on('event', function (ev, data) { sendMessengerEvent('CHAT_EVENT', { ev: ev, @@ -1417,7 +1490,16 @@ define([ }; var loadMailbox = function (waitFor) { - store.mailbox = Mailbox.init(store, waitFor, function (ev, data, clients) { + if (!store.loggedIn || !store.proxy.edPublic) { + return; + } + store.mailbox = Mailbox.init({ + store: store, + updateMetadata: function () { + broadcast([], "UPDATE_METADATA"); + }, + pinPads: function (data, cb) { Store.pinPads(null, data, cb); }, + }, waitFor, function (ev, data, clients) { clients.forEach(function (cId) { postMessage(cId, 'MAILBOX_EVENT', { ev: ev, @@ -1427,6 +1509,18 @@ define([ }); }; + var cleanFriendRequests = function () { + try { + if (!store.proxy.friends_pending) { return; } + var twoDaysAgo = +new Date() - (2 * 24 * 3600 * 1000); + Object.keys(store.proxy.friends_pending).forEach(function (curve) { + if (store.proxy.friends_pending[curve] < twoDaysAgo) { + delete store.proxy.friends_pending[curve]; + } + }); + } catch (e) {} + }; + ////////////////////////////////////////////////////////////////// /////////////////////// Init ///////////////////////////////////// ////////////////////////////////////////////////////////////////// @@ -1522,6 +1616,7 @@ define([ loadCursor(); loadOnlyOffice(); loadMailbox(waitFor); + cleanFriendRequests(); }).nThen(function () { var requestLogin = function () { broadcast([], "REQUEST_LOGIN"); @@ -1575,9 +1670,31 @@ define([ // Trigger userlist update when the avatar has changed broadcast([], "UPDATE_METADATA"); }); - proxy.on('change', ['friends'], function () { + proxy.on('change', ['friends'], function (o, n, p) { // Trigger userlist update when the friendlist has changed broadcast([], "UPDATE_METADATA"); + + if (!store.messenger) { return; } + if (o !== undefined) { return; } + var curvePublic = p.slice(-1)[0]; + var friend = proxy.friends && proxy.friends[curvePublic]; + store.messenger.onFriendAdded(friend); + }); + proxy.on('remove', ['friends'], function (o, p) { + broadcast([], "UPDATE_METADATA"); + + if (!store.messenger) { return; } + var curvePublic = p[1]; + if (!curvePublic) { return; } + if (p[2] !== 'channel') { return; } + store.messenger.onFriendRemoved(curvePublic, o); + }); + proxy.on('change', ['friends_pending'], function () { + // Trigger userlist update when the friendlist has changed + broadcast([], "UPDATE_METADATA"); + }); + proxy.on('remove', ['friends_pending'], function () { + broadcast([], "UPDATE_METADATA"); }); proxy.on('change', ['settings'], function () { broadcast([], "UPDATE_METADATA"); diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js new file mode 100644 index 000000000..72a444a6d --- /dev/null +++ b/www/common/outer/mailbox-handlers.js @@ -0,0 +1,59 @@ +define([ + '/common/common-messaging.js', +], function (Messaging) { + + var handlers = {}; + + handlers['FRIEND_REQUEST'] = function (ctx, data, cb) { + if (data.msg.author === data.msg.content.curvePublic && + Messaging.getFriend(ctx.store.proxy, data.msg.author)) { + Messaging.acceptFriendRequest(ctx.store, data.msg.content, function (obj) { + if (obj && obj.error) { return void cb(); } + cb(true); + }); + return; + } + cb(); + }; + handlers['DECLINE_FRIEND_REQUEST'] = function (ctx, box, data, cb) { + // Our friend request was declined. + if (!ctx.store.proxy.friends_pending[data.msg.author]) { return void cb(true); } + delete ctx.store.proxy.friends_pending[data.msg.author]; + ctx.updateMetadata(); + cb(); + }; + handlers['ACCEPT_FRIEND_REQUEST'] = function (ctx, box, data, cb) { + // Our friend request was accepted. + // Make sure we really sent it + if (!ctx.store.proxy.friends_pending[data.msg.author]) { return void cb(); } + // And add the friend + Messaging.addToFriendList({ + proxy: ctx.store.proxy, + realtime: ctx.store.realtime, + pinPads: ctx.pinPads + }, data.msg.content, function (err) { + if (err) { console.error(err); } + delete ctx.store.proxy.friends_pending[data.msg.author]; + if (ctx.store.messenger) { + ctx.store.messenger.onFriendAdded(data.msg.content); + } + ctx.updateMetadata(); + }); + cb(); + }; + + return function (ctx, box, data, cb) { + var type = data.msg.type; + + if (handlers[type]) { + try { + handlers[type](ctx, box, data, cb); + } catch (e) { + cb(); + } + } else { + cb(); + } + }; +}); + diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js index 313690a29..f273ca5a5 100644 --- a/www/common/outer/mailbox.js +++ b/www/common/outer/mailbox.js @@ -1,31 +1,395 @@ -// jshint ignore: start define([ '/common/common-util.js', - '/common/common-constants.js', - '/customize/messages.js', + '/common/common-hash.js', + '/common/common-realtime.js', + '/common/outer/mailbox-handlers.js', '/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad-crypto/crypto.js', -], function (Util, Constants, Messages, CpNetflux, Crypto) { +], function (Util, Hash, Realtime, Handlers, CpNetflux, Crypto) { var Mailbox = {}; - Mailbox.init = function (store, waitFor, emit) { + var TYPES = [ + 'notifications', + 'test' + ]; + var BLOCKING_TYPES = [ + ]; + + var initializeMailboxes = function (ctx, mailboxes) { + if (!mailboxes['notifications']) { + mailboxes.notifications = { + channel: Hash.createChannelId(), + lastKnownHash: '', + viewed: [] + }; + ctx.pinPads([mailboxes.notifications.channel], function (res) { + if (res.error) { console.error(res); } + }); + } + }; + +/* +proxy.mailboxes = { + friends: { + channel: '', + lastKnownHash: '', + viewed: [] + } +}; + +*/ + + var isMessageNew = function (hash, m) { + return (m.viewed || []).indexOf(hash) === -1 && hash !== m.lastKnownHash; + }; + + var showMessage = function (ctx, type, msg, cId) { + ctx.emit('MESSAGE', { + type: type, + content: msg + }, cId ? [cId] : ctx.clients); + }; + + var getMyKeys = function (ctx) { + var proxy = ctx.store && ctx.store.proxy; + if (!proxy.curvePrivate || !proxy.curvePublic) { return; } + return { + curvePrivate: proxy.curvePrivate, + curvePublic: proxy.curvePublic + }; + }; + + // Send a message to someone else + var sendTo = function (ctx, type, msg, user, cb) { + if (!Crypto.Mailbox) { + return void cb({error: "chainpad-crypto is outdated and doesn't support mailboxes."}); + } + var keys = getMyKeys(ctx); + if (!keys) { return void cb({error: "missing asymmetric encryption keys"}); } + if (!user || !user.channel || !user.curvePublic) { return void cb({error: "no notification channel"}); } + + var crypto = Crypto.Mailbox.createEncryptor(keys); + var network = ctx.store.network; + + var ciphertext = crypto.encrypt(JSON.stringify({ + type: type, + content: msg + }), user.curvePublic); + + network.join(user.channel).then(function (wc) { + wc.bcast(ciphertext).then(function () { + cb(); + wc.leave(); + }); + }, function (err) { + cb({error: err}); + }); + }; + + var updateLastKnownHash = function (ctx, type) { + var m = Util.find(ctx, ['store', 'proxy', 'mailboxes', type]); + if (!m) { return; } + var box = ctx.boxes[type]; + if (!box) { return; } + + }; + + // Mark a message as read + var dismiss = function (ctx, data, cId, cb) { + var type = data.type; + var hash = data.hash; + var m = Util.find(ctx, ['store', 'proxy', 'mailboxes', type]); + if (!m) { return void cb({error: 'NOT_FOUND'}); } + var box = ctx.boxes[type]; + if (!box) { return void cb({error: 'NOT_LOADED'}); } + + // If the hash in in our history, get the index from the history: + // - if the index is 0, we can change our lastKnownHash + // - otherwise, just push to view + var idx; + if (box.history.some(function (el, i) { + if (hash === el) { + idx = i; + return true; + } + })) { + if (idx === 0) { + m.lastKnownHash = hash; + box.history.shift(); + delete box.content[hash]; + } else if (m.viewed.indexOf(hash) === -1) { + m.viewed.push(hash); + } + } + + // Clear data in memory if needed + // Check the "viewed" array to see if we're able to bump lastKnownhash more + var sliceIdx; + var lastKnownHash; + box.history.some(function (hash, i) { + var isViewed = m.viewed.indexOf(hash); + if (isViewed !== -1) { + sliceIdx = i + 1; + m.viewed.splice(isViewed, 1); + lastKnownHash = hash; + return false; + } + return true; + }); + + if (sliceIdx) { + box.history = box.history.slice(sliceIdx); + m.lastKnownHash = lastKnownHash; + } + + // Make sure we remove data about dismissed messages + Object.keys(box.content).forEach(function (h) { + if (box.history.indexOf(h) === -1 || m.viewed.indexOf(h) !== -1) { + delete box.content[h]; + } + }); + + Realtime.whenRealtimeSyncs(ctx.store.realtime, function () { + cb(); + ctx.emit('VIEWED', { + type: type, + hash: hash + }, ctx.clients.filter(function (clientId) { + return clientId !== cId; + })); + }); + }; + + + var openChannel = function (ctx, type, m, onReady) { + var box = ctx.boxes[type] = { + queue: [], // Store the messages to send when the channel is ready + history: [], // All the hashes loaded from the server in corretc order + content: {}, // Content of the messages that should be displayed + sendMessage: function (msg) { // To send a message to our box + try { + msg = JSON.stringify(msg); + } catch (e) { + console.error(e); + } + box.queue.push(msg); + } + }; + Crypto = Crypto; + if (!Crypto.Mailbox) { + return void console.error("chainpad-crypto is outdated and doesn't support mailboxes."); + } + var keys = getMyKeys(ctx); + if (!keys) { return void console.error("missing asymmetric encryption keys"); } + var crypto = Crypto.Mailbox.createEncryptor(keys); + // XXX remove 'test' + if (type === 'test') { + crypto = { + encrypt: function (x) { return x; }, + decrypt: function (x) { return x; } + }; + } + var cfg = { + network: ctx.store.network, + channel: m.channel, + noChainPad: true, + crypto: crypto, + owners: [ctx.store.proxy.edPublic], + lastKnownHash: m.lastKnownHash + }; + cfg.onConnect = function (wc, sendMessage) { + // Send a message to our box? + // NOTE: we use our own curvePublic so that we can decrypt our own message :) + box.sendMessage = function (msg) { + try { + msg = JSON.stringify(msg); + } catch (e) { + console.error(e); + } + sendMessage(msg, function (err, hash) { + if (m.viewed.indexOf(hash) === -1) { + m.viewed.push(hash); + } + }, keys.curvePublic); + }; + box.queue.forEach(function (msg) { + box.sendMessage(msg); + }); + box.queue = []; + }; + cfg.onMessage = function (msg, user, vKey, isCp, hash, author) { + if (hash === m.lastKnownHash) { return; } + try { + msg = JSON.parse(msg); + } catch (e) { + console.error(e); + } + if (author) { msg.author = author; } + box.history.push(hash); + if (isMessageNew(hash, m)) { + // Message should be displayed + var message = { + msg: msg, + hash: hash + }; + Handlers(ctx, box, message, function (toDismiss) { + if (toDismiss) { + dismiss(ctx, { + type: type, + hash: hash + }, '', function () { + console.log('Notification handled automatically'); + }); + return; + } + box.content[hash] = msg; + showMessage(ctx, type, message); + }); + } else { + // Message has already been viewed by the user + if (Object.keys(box.content).length === 0) { + // If nothing is displayed yet, we can bump our lastKnownHash and remove this hash + // from our "viewed" array + m.lastKnownHash = hash; + box.history = []; + var idxViewed = m.viewed.indexOf(hash); + if (idxViewed !== -1) { m.viewed.splice(idxViewed, 1); } + } + } + }; + cfg.onReady = function () { + // Clean the "viewed" array: make sure all the "viewed" hashes are + // in history + var toClean = []; + m.viewed.forEach(function (h, i) { + if (box.history.indexOf(h) === -1) { + toClean.push(i); + } + }); + for (var i = toClean.length-1; i>=0; i--) { + m.viewed.splice(toClean[i], 1); + } + // Listen for changes in the "viewed" and lastKnownHash values + var view = function (h) { + delete box.content[h]; + ctx.emit('VIEWED', { + type: type, + hash: h + }, ctx.clients); + }; + ctx.store.proxy.on('change', ['mailboxes', type], function (o, n, p) { + if (p[2] === 'lastKnownHash') { + // Hide everything up to this hash + var sliceIdx; + box.history.some(function (h, i) { + sliceIdx = i + 1; + view(h); + if (h === n) { return true; } + }); + box.history = box.history.slice(sliceIdx); + } + if (p[2] === 'viewed') { + // Hide this message + view(n); + } + }); + // Continue + onReady(); + }; + CpNetflux.start(cfg); + }; + + + var subscribe = function (ctx, data, cId, cb) { + // Get existing notifications + Object.keys(ctx.boxes).forEach(function (type) { + Object.keys(ctx.boxes[type].content).forEach(function (h) { + var message = { + msg: ctx.boxes[type].content[h], + hash: h + }; + showMessage(ctx, type, message, cId); + }); + }); + // Subscribe to new notifications + var idx = ctx.clients.indexOf(cId); + if (idx === -1) { + ctx.clients.push(cId); + } + cb(); + }; + + var removeClient = function (ctx, cId) { + var idx = ctx.clients.indexOf(cId); + ctx.clients.splice(idx, 1); + }; + + Mailbox.init = function (cfg, waitFor, emit) { var mailbox = {}; + var store = cfg.store; var ctx = { store: store, + pinPads: cfg.pinPads, + updateMetadata: cfg.updateMetadata, emit: emit, + clients: [], + boxes: {} }; - mailbox.removeClient = function (clientId) { - // TODO - //removeClient(ctx, clientId); + var mailboxes = store.proxy.mailboxes = store.proxy.mailboxes || {}; + + initializeMailboxes(ctx, mailboxes); + + Object.keys(mailboxes).forEach(function (key) { + if (TYPES.indexOf(key) === -1) { return; } + var m = mailboxes[key]; + + if (BLOCKING_TYPES.indexOf(key) === -1) { + openChannel(ctx, key, m, function () { + updateLastKnownHash(ctx, key); + //console.log(key + ' mailbox is ready'); + }); + } else { + openChannel(ctx, key, m, waitFor(function () { + //console.log(key + ' mailbox is ready'); + })); + } + }); + + // XXX test function used to populate a mailbox, should be removed in prod + mailbox.post = function (box, type, content) { + var b = ctx.boxes[box]; + if (!b) { return; } + b.sendMessage({ + type: type, + content: content, + sender: store.proxy.curvePublic + }); }; - mailbox.leavePad = function (padChan) { - // TODO - //leaveChannel(ctx, padChan); + + mailbox.dismiss = function (data, cb) { + dismiss(ctx, data, '', cb); + }; + + mailbox.sendTo = function (type, msg, user, cb) { + sendTo(ctx, type, msg, user, cb); + }; + + mailbox.removeClient = function (clientId) { + removeClient(ctx, clientId); }; mailbox.execCommand = function (clientId, obj, cb) { var cmd = obj.cmd; var data = obj.data; + if (cmd === 'SUBSCRIBE') { + return void subscribe(ctx, data, clientId, cb); + } + if (cmd === 'DISMISS') { + return void dismiss(ctx, data, clientId, cb); + } + if (cmd === 'SENDTO') { + return void sendTo(ctx, data.type, data.msg, data.user, cb); + } }; return mailbox; diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index aed0d5ab4..40c446c48 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -58,8 +58,8 @@ define([ ADD_SHARED_FOLDER: Store.addSharedFolder, LOAD_SHARED_FOLDER: Store.loadSharedFolderAnon, // Messaging - INVITE_FROM_USERLIST: Store.inviteFromUserlist, - ADD_DIRECT_MESSAGE_HANDLERS: Store.addDirectMessageHandlers, + ANSWER_FRIEND_REQUEST: Store.answerFriendRequest, + SEND_FRIEND_REQUEST: Store.sendFriendRequest, // Chat CHAT_COMMAND: Store.messenger.execCommand, // OnlyOffice diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index b905c9ffd..00125e1d5 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -605,7 +605,8 @@ define([ 'newpad', 'share', 'limit', - 'unpinnedWarning' + 'unpinnedWarning', + 'notifications' ], title: title.getTitleConfig(), metadataMgr: cpNfInner.metadataMgr, diff --git a/www/common/sframe-common-mailbox.js b/www/common/sframe-common-mailbox.js new file mode 100644 index 000000000..ad8524c21 --- /dev/null +++ b/www/common/sframe-common-mailbox.js @@ -0,0 +1,179 @@ +define([ + 'jquery', + '/common/common-util.js', + '/common/common-interface.js', + '/common/common-ui-elements.js', + '/common/notifications.js', + '/common/hyperscript.js', + '/customize/messages.js' +], function ($, Util, UI, UIElements, Notifications, h, Messages) { + var Mailbox = {}; + + Mailbox.create = function (Common) { + var mailbox = Common.mailbox; + var sframeChan = Common.getSframeChannel(); + + var execCommand = function (cmd, data, cb) { + sframeChan.query('Q_MAILBOX_COMMAND', { + cmd: cmd, + data: data + }, function (err, obj) { + if (err) { return void cb({error: err}); } + cb(obj); + }); + }; + + var history = {}; + + var removeFromHistory = function (type, hash) { + history[type] = history[type].filter(function (obj) { + return obj.hash !== hash; + }); + }; + + mailbox.sendTo = function (type, content, user) { + execCommand('SENDTO', { + type: type, + msg: content, + user: user + }, function (err, obj) { + if (err || (obj && obj.error)) { return void console.error(err || obj.error); } + }); + }; + + // UI + + var formatData = function (data) { + return JSON.stringify(data.content.msg.content); + }; + var createElement = function (data) { + var notif; + var dismiss = h('span.fa.fa-times'); + dismiss.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + mailbox.dismiss(data, function (err) { + if (err) { return void console.error(err); } + /*if (notif && notif.parentNode) { + try { + notif.parentNode.removeChild(notif); + } catch (e) { console.error(e); } + }*/ + }); + }); + notif = h('div.cp-notification', { + 'data-hash': data.content.hash + }, [ + h('div.cp-notification-content', h('p', formatData(data))), + h('div.cp-notification-dismiss', dismiss) + ]); + return notif; + }; + + + var onViewedHandlers = []; + var onMessageHandlers = []; + + onViewedHandlers.push(function (data) { + var hash = data.hash.replace(/"/g, '\\\"'); + var $notif = $('.cp-notification[data-hash="'+hash+'"]'); + if ($notif.length) { + $notif.remove(); + } + }); + + // Call the onMessage handlers + var pushMessage = function (data, handler) { + var todo = function (f) { + try { + var el = createElement(data); + Notifications.add(Common, data, el); + f(data, el); + } catch (e) { + console.error(e); + } + }; + if (typeof (handler) === "function") { + return void todo(handler); + } + onMessageHandlers.forEach(todo); + }; + + var onViewed = function (data) { + // data = { type: 'type', hash: 'hash' } + onViewedHandlers.forEach(function (f) { + try { + f(data); + Notifications.remove(Common, data); + } catch (e) { + console.error(e); + } + }); + removeFromHistory(data.type, data.hash); + }; + + var onMessage = function (data) { + // data = { type: 'type', content: {msg: 'msg', hash: 'hash'} } + console.log(data.content); + pushMessage(data); + if (!history[data.type]) { history[data.type] = []; } + history[data.type].push(data.content); + }; + + mailbox.dismiss = function (data, cb) { + var dataObj = { + hash: data.content.hash, + type: data.type + }; + execCommand('DISMISS', dataObj, function (obj) { + if (obj && obj.error) { return void cb(obj.error); } + onViewed(dataObj); + cb(); + }); + }; + + + // Get all existing notifications + the new ones when they come + mailbox.subscribe = function (cfg) { + if (typeof(cfg.onViewed) === "function") { + onViewedHandlers.push(cfg.onViewed); + } + if (typeof(cfg.onMessage) === "function") { + onMessageHandlers.push(cfg.onMessage); + } + Object.keys(history).forEach(function (type) { + history[type].forEach(function (data) { + pushMessage({ + type: type, + content: data + }, cfg.onMessage); + }); + }); + }; + + + // CHANNEL WITH WORKER + + sframeChan.on('EV_MAILBOX_EVENT', function (obj) { + // obj = { ev: 'type', data: obj } + var ev = obj.ev; + var data = obj.data; + if (ev === 'MESSAGE') { + return void onMessage(data); + } + if (ev === 'VIEWED') { + return void onViewed(data); + } + }); + + execCommand('SUBSCRIBE', null, function () { + //console.log('subscribed'); + }); + + return mailbox; + }; + + return Mailbox; +}); + + diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index dc1031a83..9a5ba5730 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -502,16 +502,11 @@ define([ }); // Messaging - sframeChan.on('Q_SEND_FRIEND_REQUEST', function (netfluxId, cb) { - Cryptpad.inviteFromUserlist(netfluxId, cb); + sframeChan.on('Q_SEND_FRIEND_REQUEST', function (data, cb) { + Cryptpad.messaging.sendFriendRequest(data, cb); }); - Cryptpad.messaging.onFriendRequest.reg(function (confirmText, cb) { - sframeChan.query('Q_INCOMING_FRIEND_REQUEST', confirmText, function (err, data) { - cb(data); - }); - }); - Cryptpad.messaging.onFriendComplete.reg(function (data) { - sframeChan.event('EV_FRIEND_REQUEST', data); + sframeChan.on('Q_ANSWER_FRIEND_REQUEST', function (data, cb) { + Cryptpad.messaging.answerFriendRequest(data, cb); }); // History @@ -877,6 +872,13 @@ define([ Cryptpad.cursor.execCommand(data, cb); }); + Cryptpad.mailbox.onEvent.reg(function (data) { + sframeChan.event('EV_MAILBOX_EVENT', data); + }); + sframeChan.on('Q_MAILBOX_COMMAND', function (data, cb) { + Cryptpad.mailbox.execCommand(data, cb); + }); + Cryptpad.onTimeoutEvent.reg(function () { sframeChan.event('EV_WORKER_TIMEOUT'); }); @@ -947,9 +949,6 @@ define([ readOnly: readOnly, crypto: Crypto.createEncryptor(secret.keys), onConnect: function () { - var href = parsed.getUrl(); - // Add friends requests handlers when we have the final href - Cryptpad.messaging.addHandlers(href); if (window.location.hash && window.location.hash !== '#') { /*window.location = parsed.getUrl({ present: parsed.hashData.present, diff --git a/www/common/sframe-common-title.js b/www/common/sframe-common-title.js index 3e98b853e..d204f5268 100644 --- a/www/common/sframe-common-title.js +++ b/www/common/sframe-common-title.js @@ -69,7 +69,23 @@ define([ return void UI.alert(Messages.pinLimitNotPinned, null, true); } else if (err) { return; } evTitleChange.fire(title); - if (titleUpdated) { titleUpdated(undefined, title); } + if (titleUpdated) { + titleUpdated(undefined, title); + // XXX Test notifications from inner + var users = metadataMgr.getMetadata().users; + var me = metadataMgr.getNetfluxId(); + Object.keys(users).forEach(function (netfluxId) { + if (netfluxId === me) { return; } + var user = users[netfluxId]; + if (!user.curvePublic || !user.notifications) { return; } + Common.mailbox.sendTo("TEST_NOTIF_TITLE", { + new_title: title + }, { + channel: user.notifications, + curvePublic: user.curvePublic + }); + }); + } }); }); diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 8a59e3d41..38ea38f27 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -10,6 +10,7 @@ define([ '/common/sframe-common-file.js', '/common/sframe-common-codemirror.js', '/common/sframe-common-cursor.js', + '/common/sframe-common-mailbox.js', '/common/metadata-manager.js', '/customize/application_config.js', @@ -33,6 +34,7 @@ define([ File, CodeMirror, Cursor, + Mailbox, MetadataMgr, AppConfig, CommonRealtime, @@ -378,14 +380,31 @@ define([ funcs.mergeAnonDrive = function (cb) { ctx.sframeChan.query('Q_MERGE_ANON_DRIVE', null, cb); }; - // Friends - var pendingFriends = []; + + // Create friend request funcs.getPendingFriends = function () { - return pendingFriends.slice(); + return ctx.metadataMgr.getPrivateData().pendingFriends; + }; + funcs.sendFriendRequest = function (data, cb) { + ctx.sframeChan.query('Q_SEND_FRIEND_REQUEST', data, cb); + }; + // Friend requests received + var friendRequests = {}; + funcs.addFriendRequest = function (data) { + var curve = Util.find(data, ['content', 'msg', 'author']); + friendRequests[curve] = data; + }; + funcs.removeFriendRequest = function (hash) { + Object.keys(friendRequests).some(function (curve) { + var h = Util.find(friendRequests[curve], ['content', 'hash']); + if (h === hash) { + delete friendRequests[curve]; + return true; + } + }); }; - funcs.sendFriendRequest = function (netfluxId) { - ctx.sframeChan.query('Q_SEND_FRIEND_REQUEST', netfluxId, $.noop); - pendingFriends.push(netfluxId); + funcs.getFriendRequests = function () { + return JSON.parse(JSON.stringify(friendRequests)); }; // Feedback @@ -468,6 +487,8 @@ define([ }); }; + funcs.mailbox = {}; + Object.freeze(funcs); return { create: function (cb) { @@ -520,15 +541,6 @@ define([ UI.addTooltips(); - ctx.sframeChan.on('Q_INCOMING_FRIEND_REQUEST', function (confirmMsg, cb) { - UI.confirm(confirmMsg, cb, null, true); - }); - ctx.sframeChan.on('EV_FRIEND_REQUEST', function (data) { - var i = pendingFriends.indexOf(data.netfluxId); - if (i !== -1) { pendingFriends.splice(i, 1); } - UI.log(data.logText); - }); - ctx.sframeChan.on("EV_PAD_PASSWORD", function () { UIElements.displayPasswordPrompt(funcs); }); @@ -630,6 +642,8 @@ define([ ctx.sframeChan.ready(); cb(funcs); + + Mailbox.create(funcs); }); } }; }); diff --git a/www/common/toolbar3.js b/www/common/toolbar3.js index 41a4f0731..385e35135 100644 --- a/www/common/toolbar3.js +++ b/www/common/toolbar3.js @@ -7,9 +7,10 @@ define([ '/common/common-hash.js', '/common/common-util.js', '/common/common-feedback.js', + '/common/hyperscript.js', '/common/messenger-ui.js', '/customize/messages.js', -], function ($, Config, ApiConfig, UIElements, UI, Hash, Util, Feedback, +], function ($, Config, ApiConfig, UIElements, UI, Hash, Util, Feedback, h, MessengerUI, Messages) { var Common; @@ -37,6 +38,7 @@ MessengerUI, Messages) { var TITLE_CLS = Bar.constants.title = "cp-toolbar-title"; var NEWPAD_CLS = Bar.constants.newpad = "cp-toolbar-new"; var LINK_CLS = Bar.constants.link = "cp-toolbar-link"; + var NOTIFICATIONS_CLS = Bar.constants.user = 'cp-toolbar-notifications'; // User admin menu var USERADMIN_CLS = Bar.constants.user = 'cp-toolbar-user-dropdown'; @@ -70,6 +72,7 @@ MessengerUI, Messages) { 'class': USER_CLS }).appendTo($topContainer); $('', {'class': LIMIT_CLS}).hide().appendTo($userContainer); + $('', {'class': NOTIFICATIONS_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer); $('', {'class': NEWPAD_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer); $('', {'class': USERADMIN_CLS + ' cp-dropdown-container'}).hide().appendTo($userContainer); @@ -229,7 +232,9 @@ MessengerUI, Messages) { // Display the userlist // Editors - var pendingFriends = Common.getPendingFriends(); + var pendingFriends = Common.getPendingFriends(); // Friend requests sent + var friendRequests = Common.getFriendRequests(); // Friend requests received + var friendTo = +new Date() - (2 * 24 * 3600 * 1000); editUsersNames.forEach(function (data) { var name = data.name || Messages.anonymous; var $span = $('', {'class': 'cp-avatar'}); @@ -297,9 +302,18 @@ MessengerUI, Messages) { } } else if (Common.isLoggedIn() && data.curvePublic && !friends[data.curvePublic] && !priv.readOnly) { - if (pendingFriends.indexOf(data.netfluxId) !== -1) { + if (pendingFriends[data.curvePublic] && pendingFriends[data.curvePublic] > friendTo) { $('', {'class': 'cp-toolbar-userlist-friend'}).text(Messages.userlist_pending) .appendTo($rightCol); + } else if (friendRequests[data.curvePublic]) { + $('