diff --git a/public/src/app.js b/public/src/app.js
index a66f0c4151..c0c5fb6332 100644
--- a/public/src/app.js
+++ b/public/src/app.js
@@ -1,5 +1,5 @@
"use strict";
-/*global io, templates, ajaxify, utils, bootbox, overrides, socket, config, Visibility*/
+/*global templates, translator, ajaxify, utils, bootbox, overrides, socket, config, Visibility*/
var app = app || {};
@@ -159,7 +159,7 @@ app.cacheBuster = null;
}
app.currentRoom = '';
});
- }
+ };
function highlightNavigationLink() {
var path = window.location.pathname;
@@ -239,11 +239,7 @@ app.cacheBuster = null;
}
};
- app.openChat = function (username, touid) {
- if (username === app.user.username) {
- return app.alertError('[[error:cant-chat-with-yourself]]');
- }
-
+ app.openChat = function (roomId) {
if (!app.user.uid) {
return app.alertError('[[error:not-logged-in]]');
}
@@ -255,14 +251,35 @@ app.cacheBuster = null;
chat.focusInput(chatModal);
}
- if (!chat.modalExists(touid)) {
- chat.createModal({
- username: username,
- touid: touid
- }, loadAndCenter);
+ if (chat.modalExists(roomId)) {
+ loadAndCenter(chat.getModal(roomId));
} else {
- loadAndCenter(chat.getModal(touid));
+ socket.emit('modules.chats.getUsersInRoom', {roomId: roomId}, function(err, users) {
+ if (err) {
+ return app.alertError(err.message);
+ }
+ users = users.filter(function(user) {
+ return user && parseInt(user.uid, 10) !== parseInt(app.user.uid, 10);
+ });
+ chat.createModal({
+ roomId: roomId,
+ users: users
+ }, loadAndCenter);
+ });
+ }
+ });
+ };
+
+ app.newChat = function (touid) {
+ if (!app.user.uid) {
+ return app.alertError('[[error:not-logged-in]]');
+ }
+
+ socket.emit('modules.chats.newRoom', {touid: touid}, function(err, roomId) {
+ if (err) {
+ return app.alertError(err.message);
}
+ app.openChat(roomId);
});
};
@@ -408,7 +425,7 @@ app.cacheBuster = null;
function handleStatusChange() {
$('[component="header/usercontrol"] [data-status]').off('click').on('click', function(e) {
var status = $(this).attr('data-status');
- socket.emit('user.setStatus', status, function(err, data) {
+ socket.emit('user.setStatus', status, function(err) {
if(err) {
return app.alertError(err.message);
}
diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js
index b67f9dd4a9..9c04d4a84d 100644
--- a/public/src/client/account/header.js
+++ b/public/src/client/account/header.js
@@ -30,7 +30,7 @@ define('forum/account/header', [
});
components.get('account/chat').on('click', function() {
- app.openChat($('.account-username').html(), theirid);
+ app.newChat(theirid);
});
components.get('account/ban').on('click', banAccount);
diff --git a/public/src/client/chats.js b/public/src/client/chats.js
index 90b5e912ed..be9b76a050 100644
--- a/public/src/client/chats.js
+++ b/public/src/client/chats.js
@@ -1,6 +1,6 @@
'use strict';
-/* globals define, config, app, ajaxify, utils, socket, templates */
+/* globals define, config, app, ajaxify, utils, socket, templates, Mousetrap, bootbox */
define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', 'translator'], function(components, S, sounds, infinitescroll, translator) {
var Chats = {
@@ -18,6 +18,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
}
Chats.addEventListeners();
+ Chats.createTagsInput(ajaxify.data.roomId, ajaxify.data.users);
if (env === 'md' || env === 'lg') {
Chats.resizeMainWindow();
@@ -28,39 +29,30 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.initialised = true;
- if (ajaxify.data.hasOwnProperty('meta') && ajaxify.data.meta.hasOwnProperty('uid')) {
- // This is an active chat, focus on the input box
+ if (ajaxify.data.hasOwnProperty('roomId')) {
components.get('chat/input').focus();
}
};
- Chats.getRecipientUid = function() {
- return parseInt($('.expanded-chat').attr('data-uid'), 10);
- };
-
- Chats.isCurrentChat = function(uid) {
- return Chats.getRecipientUid() === parseInt(uid, 10);
- };
-
Chats.addEventListeners = function() {
- components.get('chat/recent').on('click', 'li', function(e) {
- Chats.switchChat(parseInt($(this).attr('data-uid'), 10), $(this).attr('data-username'));
+ $('[component="chat/recent"]').on('click', '[component="chat/recent/room"]', function() {
+ Chats.switchChat($(this).attr('data-roomid'));
});
- Chats.addSendHandlers(Chats.getRecipientUid(), $('.chat-input'), $('.expanded-chat button[data-action="send"]'));
+ Chats.addSendHandlers(ajaxify.data.roomId, $('.chat-input'), $('.expanded-chat button[data-action="send"]'));
$('[data-action="pop-out"]').on('click', function() {
- var username = $('.expanded-chat').attr('data-username'),
- uid = Chats.getRecipientUid(),
- text = components.get('chat/input').val();
+
+ var text = components.get('chat/input').val();
+ var roomId = ajaxify.data.roomId;
if (app.previousUrl && app.previousUrl.match(/chats/)) {
ajaxify.go('chats', function() {
- app.openChat(username, uid);
+ app.openChat(roomId);
}, true);
} else {
window.history.go(-1);
- app.openChat(username, uid);
+ app.openChat(roomId);
}
$(window).one('action:chat.loaded', function() {
@@ -72,14 +64,14 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
.on('click', '[data-action="edit"]', function() {
var messageId = $(this).parents('[data-mid]').attr('data-mid');
var inputEl = components.get('chat/input');
- Chats.prepEdit(inputEl, messageId);
+ Chats.prepEdit(inputEl, messageId, ajaxify.data.roomId);
})
.on('click', '[data-action="delete"]', function() {
var messageId = $(this).parents('[data-mid]').attr('data-mid');
- Chats.delete(messageId);
+ Chats.delete(messageId, ajaxify.data.roomId);
});
- $('.recent-chats').on('scroll', function() {
+ $('[component="chat/recent"]').on('scroll', function() {
var $this = $(this);
var bottom = ($this[0].scrollHeight - $this.height()) * 0.9;
if ($this.scrollTop() > bottom) {
@@ -87,7 +79,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
}
});
- Chats.addSinceHandler(Chats.getRecipientUid(), $('.expanded-chat .chat-content'), $('.expanded-chat [data-since]'));
+ Chats.addSinceHandler(ajaxify.data.roomId, $('.expanded-chat .chat-content'), $('.expanded-chat [data-since]'));
};
Chats.addHotkeys = function() {
@@ -96,7 +88,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
prev = activeContact.prev();
if (prev.length) {
- Chats.switchChat(parseInt(prev.attr('data-uid'), 10), prev.attr('data-username'));
+ Chats.switchChat(prev.attr('data-roomid'));
}
});
Mousetrap.bind('ctrl+down', function() {
@@ -104,7 +96,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
next = activeContact.next();
if (next.length) {
- Chats.switchChat(parseInt(next.attr('data-uid'), 10), next.attr('data-username'));
+ Chats.switchChat(next.attr('data-roomid'));
}
});
Mousetrap.bind('up', function(e) {
@@ -114,13 +106,16 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
var lastMid = message.attr('data-mid');
var inputEl = components.get('chat/input');
- Chats.prepEdit(inputEl, lastMid);
+ Chats.prepEdit(inputEl, lastMid, ajaxify.data.roomId);
}
});
};
- Chats.prepEdit = function(inputEl, messageId) {
- socket.emit('modules.chats.getRaw', { mid: messageId }, function(err, raw) {
+ Chats.prepEdit = function(inputEl, messageId, roomId) {
+ socket.emit('modules.chats.getRaw', { mid: messageId, roomId: roomId }, function(err, raw) {
+ if (err) {
+ return app.alertError(err.message);
+ }
// Populate the input field with the raw message content
if (inputEl.val().length === 0) {
// By setting the `data-mid` attribute, I tell the chat code that I am editing a
@@ -131,42 +126,44 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
});
};
- Chats.delete = function(messageId) {
+ Chats.delete = function(messageId, roomId) {
translator.translate('[[modules:chat.delete_message_confirm]]', function(translated) {
bootbox.confirm(translated, function(ok) {
- if (ok) {
- socket.emit('modules.chats.delete', {
- messageId: messageId
- }, function(err) {
- if (err) {
- return app.alertError(err.message);
- }
-
- // Remove message from list
- components.get('chat/message', messageId).slideUp('slow', function() {
- $(this).remove();
- });
- });
+ if (!ok) {
+ return;
}
+
+ socket.emit('modules.chats.delete', {
+ messageId: messageId,
+ roomId: roomId
+ }, function(err) {
+ if (err) {
+ return app.alertError(err.message);
+ }
+
+ components.get('chat/message', messageId).slideUp('slow', function() {
+ $(this).remove();
+ });
+ });
});
});
};
- Chats.addSinceHandler = function(toUid, chatContentEl, sinceEl) {
+ Chats.addSinceHandler = function(roomId, chatContentEl, sinceEl) {
sinceEl.on('click', function() {
var since = $(this).attr('data-since');
sinceEl.removeClass('selected');
$(this).addClass('selected');
- Chats.loadChatSince(toUid, chatContentEl, since);
+ Chats.loadChatSince(roomId, chatContentEl, since);
return false;
});
};
- Chats.addSendHandlers = function(toUid, inputEl, sendEl) {
+ Chats.addSendHandlers = function(roomId, inputEl, sendEl) {
inputEl.off('keypress').on('keypress', function(e) {
if (e.which === 13 && !e.shiftKey) {
- Chats.sendMessage(toUid, inputEl);
+ Chats.sendMessage(roomId, inputEl);
return false;
}
});
@@ -177,51 +174,66 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
return;
}
- Chats.notifyTyping(toUid, val);
+ Chats.notifyTyping(roomId, val);
$(this).attr('data-typing', val);
});
- sendEl.off('click').on('click', function(e) {
- Chats.sendMessage(toUid, inputEl);
+ sendEl.off('click').on('click', function() {
+ Chats.sendMessage(roomId, inputEl);
inputEl.focus();
return false;
});
};
- Chats.switchChat = function(uid, username) {
- if (!$('#content [component="chat/messages"]').length) {
- return ajaxify.go('chats/' + utils.slugify(username));
+ Chats.createTagsInput = function(roomId, users) {
+ var tagEl = $('.users-tag-input');
+
+ tagEl.tagsinput({
+ confirmKeys: [13, 44],
+ trimValue: true
+ });
+
+ if (users && users.length) {
+ users.forEach(function(user) {
+ tagEl.tagsinput('add', user.username);
+ });
}
- var contactEl = $('.chats-list [data-uid="' + uid + '"]');
+ tagEl.on('itemAdded', function(event) {
+ if (event.item === app.user.username) {
+ return;
+ }
+ socket.emit('modules.chats.addUserToRoom', {roomId: roomId, username: event.item}, function(err) {
+ if (err && err.message === '[[error:no-user]]') {
+ tagEl.tagsinput('remove', event.item);
+ }
+ });
+ });
- Chats.loadChatSince(uid, $('.chat-content'), 'recent');
- Chats.addSendHandlers(uid, components.get('chat/input'), $('[data-action="send"]'));
+ tagEl.on('beforeItemRemove', function(event) {
+ event.cancel = !ajaxify.data.owner;
+ });
- contactEl
- .removeClass('unread')
- .addClass('bg-primary')
- .siblings().removeClass('bg-primary');
+ tagEl.on('itemRemoved', function(event) {
+ socket.emit('modules.chats.removeUserFromRoom', {roomId: roomId, username: event.item});
+ });
- components.get('chat/title').text(username);
- components.get('chat/messages').attr('data-uid', uid).attr('data-username', username);
- components.get('breadcrumb/current').text(username);
- components.get('chat/input').focus();
+ var input = $('.users-tag-container').find('.bootstrap-tagsinput input');
- if (window.history && window.history.pushState) {
- var url = 'chats/' + utils.slugify(username);
+ require(['autocomplete'], function(autocomplete) {
+ autocomplete.user(input);
+ });
+ };
- window.history.pushState({
- url: url
- }, url, RELATIVE_PATH + '/' + url);
- }
+ Chats.switchChat = function(roomid) {
+ ajaxify.go('chats/' + roomid);
};
- Chats.loadChatSince = function(toUid, chatContentEl, since) {
- if (!toUid) {
+ Chats.loadChatSince = function(roomId, chatContentEl, since) {
+ if (!roomId) {
return;
}
- socket.emit('modules.chats.get', {touid: toUid, since: since}, function(err, messages) {
+ socket.emit('modules.chats.get', {roomId: roomId, since: since}, function(err, messages) {
if (err) {
return app.alertError(err.message);
}
@@ -235,12 +247,9 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.addGlobalEventListeners = function() {
$(window).on('resize', Chats.resizeMainWindow);
$(window).on('mousemove keypress click', function() {
- if (newMessage) {
- var recipientUid = Chats.getRecipientUid();
- if (recipientUid) {
- socket.emit('modules.chats.markRead', recipientUid);
- newMessage = false;
- }
+ if (newMessage && ajaxify.data.roomId) {
+ socket.emit('modules.chats.markRead', ajaxify.data.roomId);
+ newMessage = false;
}
});
};
@@ -268,42 +277,11 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.addSocketListeners = function() {
socket.on('event:chats.receive', function(data) {
- if (Chats.isCurrentChat(data.withUid)) {
+ if (parseInt(data.roomId, 10) === parseInt(ajaxify.data.roomId, 10)) {
newMessage = data.self === 0;
data.message.self = data.self;
Chats.appendChatMessage($('.expanded-chat .chat-content'), data.message);
- } else {
- var contactEl = $('[component="chat/recent"] li[data-uid="' + data.withUid + '"]'),
- userKey = parseInt(data.withUid, 10) === parseInt(data.message.fromuid, 10) ? 'fromUser' : 'toUser';
-
- // Spawn a new contact if required
- templates.parse('partials/chat_contacts', {
- contacts: [{
- uid: data.message[userKey].uid,
- username: data.message[userKey].username,
- status: data.message[userKey].status,
- picture: data.message[userKey].picture,
- 'icon:text': data.message[userKey]['icon:text'],
- 'icon:bgColor': data.message[userKey]['icon:bgColor'],
- teaser: {
- content: data.message.cleanedContent,
- timestampISO: new Date(Date.now()).toISOString()
- }
- }]
- }, function(html) {
- translator.translate(html, function(translatedHTML) {
- if (contactEl.length) {
- contactEl.replaceWith(translatedHTML);
- } else {
- $('[component="chat/recent"]').prepend(translatedHTML);
- }
-
- // Mark that contact list entry unread
- $('.chats-list li[data-uid="' + data.withUid + '"]').addClass('unread').find('.timeago').timeago();
- app.alternatingTitle('[[modules:chat.user_has_messaged_you, ' + data.message.fromUser.username + ']]');
- });
- });
}
});
@@ -320,13 +298,12 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
});
socket.on('event:chats.edit', function(data) {
- var message;
data.messages.forEach(function(message) {
templates.parse('partials/chat_message', {
messages: message
}, function(html) {
- body = components.get('chat/message', message.messageId);
+ var body = components.get('chat/message', message.messageId);
if (body.length) {
body.replaceWith(html);
components.get('chat/message', message.messageId).find('.timeago').timeago();
@@ -351,14 +328,14 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.setActive();
};
- Chats.notifyTyping = function(toUid, typing) {
+ Chats.notifyTyping = function(roomId, typing) {
socket.emit('modules.chats.user' + (typing ? 'Start' : 'Stop') + 'Typing', {
- touid: toUid,
+ roomId: roomId,
fromUid: app.user.uid
});
};
- Chats.sendMessage = function(toUid, inputEl) {
+ Chats.sendMessage = function(roomId, inputEl) {
var msg = inputEl.val(),
mid = inputEl.attr('data-mid');
@@ -375,7 +352,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
if (!mid) {
socket.emit('modules.chats.send', {
- touid: toUid,
+ roomId: roomId,
message: msg
}, function(err) {
if (err) {
@@ -386,10 +363,11 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
}
sounds.play('chat-outgoing');
- Chats.notifyTyping(toUid, false);
+ Chats.notifyTyping(roomId, false);
});
} else {
socket.emit('modules.chats.edit', {
+ roomId: roomId,
mid: mid,
message: msg
}, function(err) {
@@ -397,7 +375,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
return app.alertError(err.message);
}
- Chats.notifyTyping(toUid, false);
+ Chats.notifyTyping(roomId, false);
});
}
};
@@ -411,13 +389,12 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
};
Chats.setActive = function() {
- var recipientUid = Chats.getRecipientUid();
- if (recipientUid) {
- socket.emit('modules.chats.markRead', recipientUid);
+ if (ajaxify.data.roomId) {
+ socket.emit('modules.chats.markRead', ajaxify.data.roomId);
$('.expanded-chat input').focus();
}
$('.chats-list li').removeClass('bg-primary');
- $('.chats-list li[data-uid="' + recipientUid + '"]').addClass('bg-primary');
+ $('.chats-list li[data-roomid="' + ajaxify.data.roomId + '"]').addClass('bg-primary');
};
Chats.parseMessage = function(data, callback) {
@@ -429,7 +406,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
};
function loadMoreRecentChats() {
- var recentChats = $('.recent-chats');
+ var recentChats = $('[component="chat/recent"]');
if (recentChats.attr('loading')) {
return;
}
@@ -441,8 +418,8 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
return app.alertError(err.message);
}
- if (data && data.users.length) {
- onRecentChatsLoaded(data.users, function() {
+ if (data && data.rooms.length) {
+ onRecentChatsLoaded(data, function() {
recentChats.removeAttr('loading');
recentChats.attr('data-nextstart', data.nextStart);
});
@@ -452,17 +429,14 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
});
}
- function onRecentChatsLoaded(users, callback) {
- users = users.filter(function(user) {
- return !$('.recent-chats li[data-uid=' + user.uid + ']').length;
- });
-
- if (!users.length) {
+ function onRecentChatsLoaded(data, callback) {
+ if (!data.rooms.length) {
return callback();
}
- app.parseAndTranslate('chats', 'chats', {chats: users}, function(html) {
- $('.recent-chats').append(html);
+ app.parseAndTranslate('chats', 'rooms', data, function(html) {
+ $('[component="chat/recent"]').append(html);
+ html.find('.timeago').timeago();
callback();
});
}
diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js
index c971a7e5fc..0ae05ec7a5 100644
--- a/public/src/client/topic/postTools.js
+++ b/public/src/client/topic/postTools.js
@@ -405,12 +405,10 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
});
}
-
-
function openChat(button) {
var post = button.parents('[data-pid]');
- app.openChat(post.attr('data-username'), post.attr('data-uid'));
+ app.newChat(post.attr('data-uid'));
button.parents('.btn-group').find('.dropdown-toggle').click();
return false;
}
diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js
index a66fff7264..ecf15c2ead 100644
--- a/public/src/modules/autocomplete.js
+++ b/public/src/modules/autocomplete.js
@@ -10,6 +10,9 @@ define('autocomplete', function() {
app.loadJQueryUI(function() {
input.autocomplete({
delay: 100,
+ open: function() {
+ $(this).autocomplete('widget').css('z-index', 20000);
+ },
select: onselect,
source: function(request, response) {
socket.emit('user.search', {query: request.term}, function(err, result) {
diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js
index 14bf5982c5..a3d4620f08 100644
--- a/public/src/modules/chat.js
+++ b/public/src/modules/chat.js
@@ -1,5 +1,5 @@
"use strict";
-/* globals app, config, define, socket, templates, utils, ajaxify */
+/* globals app, define, socket, templates, utils, ajaxify */
define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'translator'], function(components, taskbar, S, sounds, Chats, translator) {
@@ -20,14 +20,12 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
socket.on('event:chats.receive', function(data) {
var username = data.message.fromUser.username;
- var isSelf = parseInt(data.message.fromUser.uid, 10) === parseInt(app.user.uid, 10);
+ var isSelf = data.self === 1;
data.message.self = data.self;
- if (isSelf) {
- username = data.message.toUser.username;
- }
+
newMessage = data.self === 0;
- if (module.modalExists(data.withUid)) {
- var modal = module.getModal(data.withUid);
+ if (module.modalExists(data.roomId)) {
+ var modal = module.getModal(data.roomId);
Chats.appendChatMessage(modal.find('.chat-content'), data.message);
@@ -49,8 +47,7 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
}
} else {
module.createModal({
- username: username,
- touid: data.withUid,
+ roomId: data.roomId,
silent: true
}, function(modal) {
module.toggleNew(modal.attr('UUID'), true, true);
@@ -90,15 +87,16 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
module.loadChatsDropdown = function(chatsListEl) {
var dropdownEl;
- socket.emit('modules.chats.getRecentChats', {after: 0}, function(err, chats) {
+ socket.emit('modules.chats.getRecentChats', {after: 0}, function(err, data) {
if (err) {
return app.alertError(err.message);
}
- chats = chats.users;
+
+ var rooms = data.rooms;
chatsListEl.empty();
- if (!chats.length) {
+ if (!rooms.length) {
translator.translate('[[modules:chat.no_active]]', function(str) {
$('
')
.addClass('no_active')
@@ -108,23 +106,29 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
return;
}
- chats.forEach(function(userObj) {
- dropdownEl = $('')
- .attr('data-uid', userObj.uid)
- .html(''+
+ rooms.forEach(function(roomObj) {
+ function createUserImage(userObj) {
+ return '' +
(userObj.picture ?
'
' :
'' + userObj['icon:text'] + '
') +
' ' +
- userObj.username + '')
+ roomObj.usernames + '';
+ }
+
+ dropdownEl = $('')
+ .attr('data-roomId', roomObj.roomId)
.appendTo(chatsListEl);
+ if (roomObj.lastUser) {
+ dropdownEl.append(createUserImage(roomObj.lastUser));
+ }
dropdownEl.click(function() {
if (!ajaxify.currentPage.match(/^chats\//)) {
- app.openChat(userObj.username, userObj.uid);
+ app.openChat(roomObj.roomId);
} else {
- ajaxify.go('chats/' + utils.slugify(userObj.username));
+ ajaxify.go('chats/' + roomObj.roomId);
}
});
});
@@ -149,12 +153,12 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
chatModal.css('zIndex', topZ + 1);
};
- module.getModal = function(touid) {
- return $('#chat-modal-' + touid);
+ module.getModal = function(roomId) {
+ return $('#chat-modal-' + roomId);
};
- module.modalExists = function(touid) {
- return $('#chat-modal-' + touid).length !== 0;
+ module.modalExists = function(roomId) {
+ return $('#chat-modal-' + roomId).length !== 0;
};
function checkStatus(chatModal) {
@@ -175,8 +179,8 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
uuid = utils.generateUUID(),
dragged = false;
- chatModal.attr('id', 'chat-modal-' + data.touid);
- chatModal.attr('touid', data.touid);
+ chatModal.attr('id', 'chat-modal-' + data.roomId);
+ chatModal.attr('roomId', data.roomId);
chatModal.attr('intervalId', 0);
chatModal.attr('UUID', uuid);
chatModal.css('position', 'fixed');
@@ -210,8 +214,6 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
});
});
- chatModal.find('#chat-with-name').html(data.username);
-
chatModal.find('#chat-close-btn').on('click', function() {
module.close(chatModal);
});
@@ -222,19 +224,17 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
components.get('chat/input').val(text);
});
- ajaxify.go('chats/' + utils.slugify(data.username));
+ ajaxify.go('chats/' + chatModal.attr('roomId'));
module.close(chatModal);
}
chatModal.find('.modal-header').on('dblclick', gotoChats);
chatModal.find('button[data-action="maximize"]').on('click', gotoChats);
- chatModal.on('click', function(e) {
+ chatModal.on('click', function() {
module.bringModalToTop(chatModal);
- if (!dragged) {
- chatModal.find('#chat-message-input').focus();
- } else {
+ if (dragged) {
dragged = false;
}
});
@@ -247,7 +247,7 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
chatModal.on('mousemove keypress click', function() {
if (newMessage) {
- socket.emit('modules.chats.markRead', data.touid);
+ socket.emit('modules.chats.markRead', data.roomId);
newMessage = false;
}
});
@@ -256,22 +256,24 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
.on('click', '[data-action="edit"]', function() {
var messageId = $(this).parents('[data-mid]').attr('data-mid');
var inputEl = chatModal.find('[component="chat/input"]');
- Chats.prepEdit(inputEl, messageId);
+ Chats.prepEdit(inputEl, messageId, data.roomId);
})
.on('click', '[data-action="delete"]', function() {
var messageId = $(this).parents('[data-mid]').attr('data-mid');
- Chats.delete(messageId);
+ Chats.delete(messageId, data.roomId);
});
- Chats.addSinceHandler(chatModal.attr('touid'), chatModal.find('.chat-content'), chatModal.find('[data-since]'));
+ Chats.addSinceHandler(chatModal.attr('roomId'), chatModal.find('.chat-content'), chatModal.find('[data-since]'));
- Chats.addSendHandlers(chatModal.attr('touid'), chatModal.find('#chat-message-input'), chatModal.find('#chat-message-send-btn'));
+ Chats.addSendHandlers(chatModal.attr('roomId'), chatModal.find('#chat-message-input'), chatModal.find('#chat-message-send-btn'));
- Chats.loadChatSince(chatModal.attr('touid'), chatModal.find('.chat-content'), 'recent');
+ Chats.createTagsInput(data.roomId, data.users);
+
+ Chats.loadChatSince(chatModal.attr('roomId'), chatModal.find('.chat-content'), 'recent');
checkStatus(chatModal);
- module.canMessage(data.touid, function(err) {
+ module.canMessage(data.roomId, function(err) {
if (err) {
// Disable the text input
chatModal.find('input[type="text"]').attr('disabled', true);
@@ -279,8 +281,8 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
});
taskbar.push('chat', chatModal.attr('UUID'), {
- title: data.username,
- touid: data.touid,
+ title: data.users.length ? data.users[0].username : '',
+ roomId: data.roomId,
icon: 'fa-comment',
state: ''
});
@@ -304,7 +306,7 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
chatModal.remove();
chatModal.data('modal', null);
taskbar.discard('chat', chatModal.attr('UUID'));
- Chats.notifyTyping(chatModal.attr('touid'), false);
+ Chats.notifyTyping(chatModal.attr('roomId'), false);
if (chatModal.attr('data-mobile')) {
module.disableMobileBehaviour(chatModal);
@@ -327,14 +329,14 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
};
module.load = function(uuid) {
- var chatModal = $('div[UUID="'+uuid+'"]');
+ var chatModal = $('div[UUID="' + uuid + '"]');
chatModal.removeClass('hide');
checkStatus(chatModal);
taskbar.updateActive(uuid);
Chats.scrollToBottom(chatModal.find('.chat-content'));
module.bringModalToTop(chatModal);
module.focusInput(chatModal);
- socket.emit('modules.chats.markRead', chatModal.attr('touid'));
+ socket.emit('modules.chats.markRead', chatModal.attr('roomId'));
var env = utils.findBootstrapEnvironment();
if (env === 'xs' || env === 'sm') {
@@ -373,13 +375,13 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
taskbar.minimize('chat', uuid);
clearInterval(chatModal.attr('intervalId'));
chatModal.attr('intervalId', 0);
- Chats.notifyTyping(chatModal.attr('touid'), false);
+ Chats.notifyTyping(chatModal.attr('roomId'), false);
};
module.toggleNew = taskbar.toggleNew;
- module.canMessage = function(toUid, callback) {
- socket.emit('modules.chats.canMessage', toUid, callback);
+ module.canMessage = function(roomId, callback) {
+ socket.emit('modules.chats.canMessage', roomId, callback);
};
diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js
index 1f95db984b..2c777e7266 100644
--- a/src/controllers/accounts/chats.js
+++ b/src/controllers/accounts/chats.js
@@ -1,13 +1,11 @@
'use strict';
-var async = require('async'),
- nconf = require('nconf'),
+var async = require('async');
+
+var messaging = require('../../messaging');
+var meta = require('../../meta');
+var helpers = require('../helpers');
- user = require('../../user'),
- messaging = require('../../messaging'),
- meta = require('../../meta'),
- helpers = require('../helpers'),
- utils = require('../../../public/src/utils');
var chatsController = {};
@@ -16,35 +14,15 @@ chatsController.get = function(req, res, callback) {
return callback();
}
- // In case a userNAME is passed in instead of a slug, the route should not 404
- var slugified = utils.slugify(req.params.userslug);
- if (req.params.userslug && req.params.userslug !== slugified) {
- return helpers.redirect(res, '/chats/' + slugified);
- }
-
- async.parallel({
- contacts: async.apply(user.getFollowing, req.user.uid, 0, 199),
- recentChats: async.apply(messaging.getRecentChats, req.user.uid, 0, 19)
- }, function(err, results) {
+ messaging.getRecentChats(req.uid, 0, 19, function(err, recentChats) {
if (err) {
return callback(err);
}
- if (results.recentChats.users && results.recentChats.users.length) {
- var contactUids = results.recentChats.users.map(function(chatObj) {
- return parseInt(chatObj.uid, 10);
- });
-
- results.contacts = results.contacts.filter(function(contact) {
- return contactUids.indexOf(parseInt(contact.uid, 10)) === -1;
- });
- }
-
- if (!req.params.userslug) {
+ if (!req.params.roomid) {
return res.render('chats', {
- chats: results.recentChats.users,
- nextStart: results.recentChats.nextStart,
- contacts: results.contacts,
+ rooms: recentChats.rooms,
+ nextStart: recentChats.nextStart,
allowed: true,
title: '[[pages:chats]]',
breadcrumbs: helpers.buildBreadcrumbs([{text: '[[pages:chats]]'}])
@@ -52,21 +30,24 @@ chatsController.get = function(req, res, callback) {
}
async.waterfall([
- async.apply(user.getUidByUserslug, req.params.userslug),
- function(toUid, next) {
- if (!toUid || parseInt(toUid, 10) === parseInt(req.user.uid, 10)) {
+ function (next) {
+ messaging.isUserInRoom(req.uid, req.params.roomid, next);
+ },
+ function (inRoom, next) {
+ if (!inRoom) {
return callback();
}
async.parallel({
- toUser: async.apply(user.getUserFields, toUid, ['uid', 'username']),
+ users: async.apply(messaging.getUsersInRoom, req.params.roomid, 0, -1),
messages: async.apply(messaging.getMessages, {
- fromuid: req.user.uid,
- touid: toUid,
+ uid: req.uid,
+ roomId: req.params.roomid,
since: 'recent',
isNew: false
}),
- allowed: async.apply(messaging.canMessage, req.user.uid, toUid)
+ allowed: async.apply(messaging.canMessageRoom, req.uid, req.params.roomid),
+ owner: async.apply(messaging.isRoomOwner, req.uid, req.params.roomid)
}, next);
}
], function(err, data) {
@@ -74,16 +55,22 @@ chatsController.get = function(req, res, callback) {
return callback(err);
}
- res.render('chats', {
- chats: results.recentChats.users,
- nextStart: results.recentChats.nextStart,
- contacts: results.contacts,
- meta: data.toUser,
- messages: data.messages,
- allowed: data.allowed,
- title: '[[pages:chat, ' + data.toUser.username + ']]',
- breadcrumbs: helpers.buildBreadcrumbs([{text: '[[pages:chats]]', url: '/chats'}, {text: data.toUser.username}])
+ data.users = data.users.filter(function(user) {
+ return user && parseInt(user.uid, 10) !== req.uid;
});
+
+ data.usernames = data.users.map(function(user) {
+ return user && user.username;
+ }).join(', ');
+
+
+ data.roomId = req.params.roomid;
+ data.rooms = recentChats.rooms;
+ data.nextStart = recentChats.nextStart;
+ data.title = '[[pages:chat, ' + data.usernames + ']]';
+ data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[pages:chats]]', url: '/chats'}, {text: data.roomId}]);
+
+ res.render('chats', data);
});
});
};
diff --git a/src/messaging.js b/src/messaging.js
index bff12313ba..c7c8f26978 100644
--- a/src/messaging.js
+++ b/src/messaging.js
@@ -1,22 +1,27 @@
'use strict';
-var db = require('./database'),
- async = require('async'),
- nconf = require('nconf'),
+
+var async = require('async'),
winston = require('winston'),
S = require('string'),
+
+ db = require('./database'),
user = require('./user'),
plugins = require('./plugins'),
meta = require('./meta'),
utils = require('../public/src/utils'),
notifications = require('./notifications'),
- userNotifications = require('./user/notifications'),
- emailer = require('./emailer'),
- sockets = require('./socket.io');
+ userNotifications = require('./user/notifications');
(function(Messaging) {
- Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser
+
+ require('./messaging/create')(Messaging);
+ require('./messaging/delete')(Messaging);
+ require('./messaging/edit')(Messaging);
+ require('./messaging/rooms')(Messaging);
+ require('./messaging/unread')(Messaging);
+ require('./messaging/notifications')(Messaging);
var terms = {
day: 86400000,
@@ -25,147 +30,9 @@ var db = require('./database'),
threemonths: 7776000000
};
- function sortUids(fromuid, touid) {
- return [fromuid, touid].sort();
- }
-
- Messaging.addMessage = function(fromuid, touid, content, timestamp, callback) {
- if (typeof timestamp === 'function') {
- callback = timestamp;
- timestamp = Date.now();
- } else {
- timestamp = timestamp || Date.now();
- }
-
- if (!content) {
- return callback(new Error('[[error:invalid-chat-message]]'));
- }
-
- if (content.length > (meta.config.maximumChatMessageLength || 1000)) {
- return callback(new Error('[[error:chat-message-too-long]]'));
- }
-
- var uids = sortUids(fromuid, touid);
-
- db.incrObjectField('global', 'nextMid', function(err, mid) {
- if (err) {
- return callback(err);
- }
- var message = {
- content: content,
- timestamp: timestamp,
- fromuid: fromuid,
- touid: touid
- };
-
- async.waterfall([
- function(next) {
- plugins.fireHook('filter:messaging.save', message, next);
- },
- function(message, next) {
- db.setObject('message:' + mid, message, next);
- }
- ], function(err) {
- if (err) {
- return callback(err);
- }
-
- async.parallel([
- async.apply(db.sortedSetAdd, 'messages:uid:' + uids[0] + ':to:' + uids[1], timestamp, mid),
- async.apply(Messaging.updateChatTime, fromuid, touid),
- async.apply(Messaging.updateChatTime, touid, fromuid),
- async.apply(Messaging.markRead, fromuid, touid),
- async.apply(Messaging.markUnread, touid, fromuid),
- ], function(err) {
- if (err) {
- return callback(err);
- }
-
- async.waterfall([
- function(next) {
- getMessages([mid], fromuid, touid, true, next);
- },
- function(messages, next) {
- Messaging.isNewSet(fromuid, touid, mid, function(err, isNewSet) {
- if (err) {
- return next(err);
- }
-
- if (!messages || !messages[0]) {
- return next(null, null);
- }
-
- messages[0].newSet = isNewSet;
- messages[0].mid = mid;
- next(null, messages[0]);
- });
- }
- ], callback);
- });
- });
- });
- };
-
- Messaging.editMessage = function(mid, content, callback) {
- async.series([
- function(next) {
- // Verify that the message actually changed
- Messaging.getMessageField(mid, 'content', function(err, raw) {
- if (raw === content) {
- // No dice.
- return callback();
- }
-
- next();
- });
- },
- async.apply(Messaging.setMessageFields, mid, {
- content: content,
- edited: Date.now()
- }),
- function(next) {
- Messaging.getMessageFields(mid, ['fromuid', 'touid'], function(err, data) {
- getMessages([mid], data.fromuid, data.touid, true, function(err, messages) {
- sockets.in('uid_' + data.fromuid).emit('event:chats.edit', {
- messages: messages
- });
- sockets.in('uid_' + data.touid).emit('event:chats.edit', {
- messages: messages
- });
- next();
- });
- });
- }
- ], callback);
- };
-
- Messaging.deleteMessage = function(mid, callback) {
- var uids = [];
- async.series([
- function(next) {
- db.getObject('message:' + mid, function(err, messageObj) {
- messageObj.fromuid = parseInt(messageObj.fromuid, 10);
- messageObj.touid = parseInt(messageObj.touid, 10);
- uids.push(messageObj.fromuid, messageObj.touid);
- uids.sort(function(a, b) {
- return a > b ? 1 : -1;
- });
- next();
- });
- },
- function(next) {
- next();
- },
- function(next) {
- db.sortedSetRemove('messages:uid:' + uids[0] + ':to:' + uids[1], mid, next);
- },
- async.apply(db.delete, 'message:' + mid)
- ], callback);
- };
-
Messaging.getMessageField = function(mid, field, callback) {
Messaging.getMessageFields(mid, [field], function(err, fields) {
- callback(err, fields[field]);
+ callback(err, fields ? fields[field] : null);
});
};
@@ -182,22 +49,21 @@ var db = require('./database'),
};
Messaging.getMessages = function(params, callback) {
- var fromuid = params.fromuid,
- touid = params.touid,
+ var uid = params.uid,
+ roomId = params.roomId,
since = params.since,
isNew = params.isNew,
count = params.count || parseInt(meta.config.chatMessageInboxSize, 10) || 250,
markRead = params.markRead || true;
- var uids = sortUids(fromuid, touid),
- min = params.count ? 0 : Date.now() - (terms[since] || terms.day);
+ var min = params.count ? 0 : Date.now() - (terms[since] || terms.day);
if (since === 'recent') {
count = 49;
min = 0;
}
- db.getSortedSetRevRangeByScore('messages:uid:' + uids[0] + ':to:' + uids[1], 0, count, '+inf', min, function(err, mids) {
+ db.getSortedSetRevRangeByScore('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, count, '+inf', min, function(err, mids) {
if (err) {
return callback(err);
}
@@ -208,115 +74,122 @@ var db = require('./database'),
mids.reverse();
- getMessages(mids, fromuid, touid, isNew, callback);
+ Messaging.getMessagesData(mids, uid, roomId, isNew, callback);
});
if (markRead) {
- notifications.markRead('chat_' + touid + '_' + fromuid, fromuid, function(err) {
+ notifications.markRead('chat_' + roomId + '_' + uid, uid, function(err) {
if (err) {
winston.error('[messaging] Could not mark notifications related to this chat as read: ' + err.message);
}
- userNotifications.pushCount(fromuid);
+ userNotifications.pushCount(uid);
});
}
};
- function getMessages(mids, fromuid, touid, isNew, callback) {
- user.getUsersFields([fromuid, touid], ['uid', 'username', 'userslug', 'picture', 'status'], function(err, userData) {
- if(err) {
- return callback(err);
- }
+ Messaging.getMessagesData = function(mids, uid, roomId, isNew, callback) {
- var keys = mids.map(function(mid) {
- return 'message:' + mid;
- });
+ var keys = mids.map(function(mid) {
+ return 'message:' + mid;
+ });
- async.waterfall([
- async.apply(db.getObjects, keys),
- function(messages, next) {
- messages = messages.map(function(msg, idx) {
- if (msg) {
- msg.messageId = parseInt(mids[idx], 10);
- }
- return msg;
- }).filter(Boolean);
- async.map(messages, function(message, next) {
- var self = parseInt(message.fromuid, 10) === parseInt(fromuid, 10);
- message.fromUser = self ? userData[0] : userData[1];
- message.toUser = self ? userData[1] : userData[0];
- message.timestampISO = utils.toISOString(message.timestamp);
- message.self = self ? 1 : 0;
- message.newSet = false;
-
- if (message.hasOwnProperty('edited')) {
- message.editedISO = new Date(parseInt(message.edited, 10)).toISOString();
- }
+ var messages;
- Messaging.parse(message.content, message.fromuid, fromuid, userData[1], userData[0], isNew, function(result) {
- message.content = result;
- message.cleanedContent = S(result).stripTags().decodeHTMLEntities().s;
- next(null, message);
- });
- }, next);
- },
- function(messages, next) {
- if (messages.length > 1) {
- // Add a spacer in between messages with time gaps between them
- messages = messages.map(function(message, index) {
- // Compare timestamps with the previous message, and check if a spacer needs to be added
- if (index > 0 && parseInt(message.timestamp, 10) > parseInt(messages[index-1].timestamp, 10) + (1000*60*5)) {
- // If it's been 5 minutes, this is a new set of messages
- message.newSet = true;
- } else if (index > 0 && message.fromuid !== messages[index-1].fromuid) {
- // If the previous message was from the other person, this is also a new set
- message.newSet = true;
- }
+ async.waterfall([
+ function (next) {
+ db.getObjects(keys, next);
+ },
+ function (_messages, next) {
+ messages = _messages.map(function(msg, idx) {
+ if (msg) {
+ msg.messageId = parseInt(mids[idx], 10);
+ }
+ return msg;
+ }).filter(Boolean);
- return message;
- });
+ var uids = messages.map(function(msg) {
+ return msg && msg.fromuid;
+ });
- next(undefined, messages);
- } else {
- // For single messages, we don't know the context, so look up the previous message and compare
- var uids = [fromuid, touid].sort(function(a, b) { return a > b ? 1 : -1 });
- var key = 'messages:uid:' + uids[0] + ':to:' + uids[1];
- async.waterfall([
- async.apply(db.sortedSetRank, key, messages[0].messageId),
- function(index, next) {
- // Continue only if this isn't the first message in sorted set
- if (index > 0) {
- db.getSortedSetRange(key, index-1, index-1, next);
- } else {
- messages[0].newSet = true;
- return next(undefined, messages);
- }
- },
- function(mid, next) {
- Messaging.getMessageFields(mid, ['fromuid', 'timestamp'], next);
- }
- ], function(err, fields) {
- if (err) {
- return next(err);
- }
+ user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status'], next);
+ },
+ function (users, next) {
+ messages.forEach(function(message, index) {
+ message.fromUser = users[index];
+ var self = parseInt(message.fromuid, 10) === parseInt(uid, 10);
+ message.self = self ? 1 : 0;
+ message.timestampISO = utils.toISOString(message.timestamp);
+ message.newSet = false;
+ if (message.hasOwnProperty('edited')) {
+ message.editedISO = new Date(parseInt(message.edited, 10)).toISOString();
+ }
+ });
+
+ async.map(messages, function(message, next) {
+ Messaging.parse(message.content, message.fromuid, uid, roomId, isNew, function(result) {
+ message.content = result;
+ message.cleanedContent = S(result).stripTags().decodeHTMLEntities().s;
+ next(null, message);
+ });
+ }, next);
+ },
+ function(messages, next) {
+ if (messages.length > 1) {
+ // Add a spacer in between messages with time gaps between them
+ messages = messages.map(function(message, index) {
+ // Compare timestamps with the previous message, and check if a spacer needs to be added
+ if (index > 0 && parseInt(message.timestamp, 10) > parseInt(messages[index-1].timestamp, 10) + (1000*60*5)) {
+ // If it's been 5 minutes, this is a new set of messages
+ message.newSet = true;
+ } else if (index > 0 && message.fromuid !== messages[index-1].fromuid) {
+ // If the previous message was from the other person, this is also a new set
+ message.newSet = true;
+ }
- if (
- (parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + (1000*60*5)) ||
- (parseInt(messages[0].fromuid, 10) !== parseInt(fields.fromuid, 10))
- ) {
- // If it's been 5 minutes, this is a new set of messages
+ return message;
+ });
+
+ next(undefined, messages);
+ } else {
+ // For single messages, we don't know the context, so look up the previous message and compare
+ var key = 'uid:' + uid + ':chat:room:' + roomId + ':mids';
+ async.waterfall([
+ async.apply(db.sortedSetRank, key, messages[0].messageId),
+ function(index, next) {
+ // Continue only if this isn't the first message in sorted set
+ if (index > 0) {
+ db.getSortedSetRange(key, index-1, index-1, next);
+ } else {
messages[0].newSet = true;
+ return next(undefined, messages);
}
+ },
+ function(mid, next) {
+ Messaging.getMessageFields(mid, ['fromuid', 'timestamp'], next);
+ }
+ ], function(err, fields) {
+ if (err) {
+ return next(err);
+ }
- next(undefined, messages);
- });
- }
+ if (
+ (parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + (1000*60*5)) ||
+ (parseInt(messages[0].fromuid, 10) !== parseInt(fields.fromuid, 10))
+ ) {
+ // If it's been 5 minutes, this is a new set of messages
+ messages[0].newSet = true;
+ }
+
+ next(undefined, messages);
+ });
}
- ], callback);
- });
- }
+ }
+ ], callback);
- Messaging.parse = function (message, fromuid, myuid, toUserData, myUserData, isNew, callback) {
+ };
+
+ Messaging.parse = function (message, fromuid, uid, roomId, isNew, callback) {
plugins.fireHook('filter:parse.raw', message, function(err, parsed) {
if (err) {
return callback(message);
@@ -326,9 +199,8 @@ var db = require('./database'),
message: message,
parsed: parsed,
fromuid: fromuid,
- myuid: myuid,
- toUserData: toUserData,
- myUserData: myUserData,
+ uid: uid,
+ roomId: roomId,
isNew: isNew,
parsedMessage: parsed
};
@@ -339,161 +211,107 @@ var db = require('./database'),
});
};
- Messaging.isNewSet = function(fromuid, touid, mid, callback) {
- var uids = sortUids(fromuid, touid),
- setKey = 'messages:uid:' + uids[0] + ':to:' + uids[1];
+ Messaging.isNewSet = function(uid, roomId, timestamp, callback) {
+ var setKey = 'uid:' + uid + ':chat:room:' + roomId + ':mids';
async.waterfall([
- async.apply(db.sortedSetRank, setKey, mid),
- function(index, next) {
- if (index > 0) {
- db.getSortedSetRange(setKey, index-1, index, next);
- } else {
- next(null, true);
- }
- },
- function(mids, next) {
- if (typeof mids !== 'boolean' && mids && mids.length) {
- db.getObjects(['message:' + mids[0], 'message:' + mids[1]], next);
- } else {
- next(null, mids);
- }
+ function(next) {
+ db.getSortedSetRevRangeWithScores(setKey, 0, 0, next);
},
function(messages, next) {
- if (typeof messages !== 'boolean' && messages && messages.length) {
- next(null, parseInt(messages[1].timestamp, 10) > parseInt(messages[0].timestamp, 10) + (1000*60*5));
+ if (messages && messages.length) {
+ next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + (1000 * 60 * 5));
} else {
- next(null, messages);
+ next(null, true);
}
}
], callback);
};
- Messaging.updateChatTime = function(uid, toUid, callback) {
- callback = callback || function() {};
- db.sortedSetAdd('uid:' + uid + ':chats', Date.now(), toUid, callback);
- };
Messaging.getRecentChats = function(uid, start, stop, callback) {
- db.getSortedSetRevRange('uid:' + uid + ':chats', start, stop, function(err, uids) {
+ db.getSortedSetRevRange('uid:' + uid + ':chat:rooms', start, stop, function(err, roomIds) {
if (err) {
return callback(err);
}
async.parallel({
unread: function(next) {
- db.isSortedSetMembers('uid:' + uid + ':chats:unread', uids, next);
+ db.isSortedSetMembers('uid:' + uid + ':chat:rooms:unread', roomIds, next);
},
users: function(next) {
- user.getUsersFields(uids, ['uid', 'username', 'picture', 'status', 'lastonline'] , next);
+ async.map(roomIds, function(roomId, next) {
+ db.getSortedSetRevRange('chat:room:' + roomId + ':uids', 0, 3, function(err, uids) {
+ if (err) {
+ return next(err);
+ }
+ uids = uids.filter(function(value) {
+ return value && parseInt(value, 10) !== parseInt(uid, 10);
+ });
+ user.getUsersFields(uids, ['uid', 'username', 'picture', 'status', 'lastonline'] , next);
+ });
+ }, next);
},
teasers: function(next) {
- async.map(uids, function(fromuid, next) {
- Messaging.getMessages({
- fromuid: fromuid,
- touid: uid,
- isNew: false,
- count: 1,
- markRead: false
- }, function(err, teaser) {
- teaser = teaser[0];
- teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s;
- next(err, teaser);
- });
+ async.map(roomIds, function(roomId, next) {
+ Messaging.getTeaser(uid, roomId, next);
}, next);
}
}, function(err, results) {
if (err) {
return callback(err);
}
-
- results.users.forEach(function(userData, index) {
- if (userData && parseInt(userData.uid, 10)) {
- userData.unread = results.unread[index];
- userData.status = user.getStatus(userData);
- userData.teaser = results.teasers[index];
- }
- });
-
- results.users = results.users.filter(function(user) {
- return user && parseInt(user.uid, 10);
+ var rooms = results.users.map(function(users, index) {
+ var data = {
+ users: users,
+ unread: results.unread[index],
+ roomId: roomIds[index],
+ teaser: results.teasers[index]
+ };
+ data.users.forEach(function(userData) {
+ if (userData && parseInt(userData.uid, 10)) {
+ userData.status = user.getStatus(userData);
+ }
+ });
+ data.users = data.users.filter(function(user) {
+ return user && parseInt(user.uid, 10);
+ });
+ data.lastUser = data.users[0];
+ data.usernames = data.users.map(function(user) {
+ return user.username;
+ }).join(', ');
+ return data;
});
- callback(null, {users: results.users, nextStart: stop + 1});
+ callback(null, {rooms: rooms, nextStart: stop + 1});
});
});
};
- Messaging.getUnreadCount = function(uid, callback) {
- db.sortedSetCard('uid:' + uid + ':chats:unread', callback);
- };
-
- Messaging.pushUnreadCount = function(uid) {
- Messaging.getUnreadCount(uid, function(err, unreadCount) {
- if (err) {
- return;
- }
- sockets.in('uid_' + uid).emit('event:unread.updateChatCount', unreadCount);
- });
- };
-
- Messaging.markRead = function(uid, toUid, callback) {
- db.sortedSetRemove('uid:' + uid + ':chats:unread', toUid, callback);
- };
-
- Messaging.markUnread = function(uid, toUid, callback) {
+ Messaging.getTeaser = function (uid, roomId, callback) {
async.waterfall([
function (next) {
- user.exists(toUid, next);
+ db.getSortedSetRevRange('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, 0, next);
},
- function (exists, next) {
- if (!exists) {
- return next(new Error('[[error:no-user]]'));
+ function (mids, next) {
+ if (!mids || !mids.length) {
+ return next(null, null);
}
- db.sortedSetAdd('uid:' + uid + ':chats:unread', Date.now(), toUid, next);
+ Messaging.getMessageFields(mids[0], ['content', 'timestamp'], next);
+ },
+ function (teaser, next) {
+ if (teaser && teaser.content) {
+ teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s;
+ teaser.timestampISO = utils.toISOString(teaser.timestamp);
+ }
+
+ next(null, teaser);
}
], callback);
};
- Messaging.notifyUser = function(fromuid, touid, messageObj) {
- // Immediate notifications
- // Recipient
- Messaging.pushUnreadCount(touid);
- sockets.in('uid_' + touid).emit('event:chats.receive', {
- withUid: fromuid,
- message: messageObj,
- self: 0
- });
- // Sender
- Messaging.pushUnreadCount(fromuid);
- sockets.in('uid_' + fromuid).emit('event:chats.receive', {
- withUid: touid,
- message: messageObj,
- self: 1
- });
-
- // Delayed notifications
- var queueObj = Messaging.notifyQueue[fromuid + ':' + touid];
- if (queueObj) {
- queueObj.message.content += '\n' + messageObj.content;
- clearTimeout(queueObj.timeout);
- } else {
- queueObj = Messaging.notifyQueue[fromuid + ':' + touid] = {
- message: messageObj
- };
- }
-
- queueObj.timeout = setTimeout(function() {
- sendNotifications(fromuid, touid, queueObj.message, function(err) {
- if (!err) {
- delete Messaging.notifyQueue[fromuid + ':' + touid];
- }
- });
- }, 1000*60); // wait 60s before sending
- };
-
- Messaging.canMessage = function(fromUid, toUid, callback) {
- if (parseInt(meta.config.disableChat) === 1 || !fromUid || toUid === fromUid) {
+ Messaging.canMessageUser = function(uid, toUid, callback) {
+ if (parseInt(meta.config.disableChat) === 1 || !uid || uid === toUid) {
return callback(null, false);
}
@@ -505,7 +323,7 @@ var db = require('./database'),
if (!exists) {
return callback(null, false);
}
- user.getUserFields(fromUid, ['banned', 'email:confirmed'], next);
+ user.getUserFields(uid, ['banned', 'email:confirmed'], next);
},
function (userData, next) {
if (parseInt(userData.banned, 10) === 1) {
@@ -523,24 +341,31 @@ var db = require('./database'),
return callback(null, true);
}
- user.isAdministrator(fromUid, next);
+ user.isAdministrator(uid, next);
},
function(isAdmin, next) {
if (isAdmin) {
return callback(null, true);
}
- user.isFollowing(toUid, fromUid, next);
+ user.isFollowing(toUid, uid, next);
}
], callback);
+
};
- Messaging.canEdit = function(messageId, uid, callback) {
- if (parseInt(meta.config.disableChat) === 1) {
+ Messaging.canMessageRoom = function(uid, roomId, callback) {
+ if (parseInt(meta.config.disableChat) === 1 || !uid) {
return callback(null, false);
}
async.waterfall([
function (next) {
+ Messaging.isUserInRoom(uid, roomId, next);
+ },
+ function (inRoom, next) {
+ if (!inRoom) {
+ return callback(null, false);
+ }
user.getUserFields(uid, ['banned', 'email:confirmed'], next);
},
function (userData, next) {
@@ -552,54 +377,10 @@ var db = require('./database'),
return callback(null, false);
}
- Messaging.getMessageField(messageId, 'fromuid', next);
- },
- function(fromUid, next) {
- if (parseInt(fromUid, 10) === parseInt(uid, 10)) {
- return callback(null, true);
- }
-
- user.isAdministrator(uid, next);
- },
- function(isAdmin, next) {
- next(null, isAdmin);
+ next(null, true);
}
], callback);
};
- function sendNotifications(fromuid, touid, messageObj, callback) {
- user.isOnline(touid, function(err, isOnline) {
- if (err || isOnline) {
- return callback(err);
- }
-
- notifications.create({
- bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
- bodyLong: messageObj.content,
- nid: 'chat_' + fromuid + '_' + touid,
- from: fromuid,
- path: '/chats/' + messageObj.fromUser.username
- }, function(err, notification) {
- if (!err && notification) {
- notifications.push(notification, [touid], callback);
- }
- });
-
- user.getSettings(messageObj.toUser.uid, function(err, settings) {
- if (settings.sendChatNotifications && !parseInt(meta.config.disableEmailSubscriptions, 10)) {
- emailer.send('notif_chat', touid, {
- subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]',
- username: messageObj.toUser.username,
- userslug: utils.slugify(messageObj.toUser.username),
- summary: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
- message: messageObj,
- site_title: meta.config.title || 'NodeBB',
- url: nconf.get('url'),
- fromUserslug: utils.slugify(messageObj.fromUser.username)
- });
- }
- });
- });
- }
}(exports));
diff --git a/src/messaging/create.js b/src/messaging/create.js
new file mode 100644
index 0000000000..d01eaf6292
--- /dev/null
+++ b/src/messaging/create.js
@@ -0,0 +1,118 @@
+'use strict';
+
+var async = require('async');
+
+var meta = require('../meta');
+var plugins = require('../plugins');
+var db = require('../database');
+
+
+module.exports = function(Messaging) {
+
+ Messaging.sendMessage = function(uid, roomId, content, timestamp, callback) {
+ async.waterfall([
+ function (next) {
+ Messaging.checkContent(content, next);
+ },
+ function (next) {
+ Messaging.isUserInRoom(uid, roomId, next);
+ },
+ function (inRoom, next) {
+ if (!inRoom) {
+ return next(new Error('[[error:not-allowed]]'));
+ }
+
+ Messaging.addMessage(uid, roomId, content, timestamp, next);
+ }
+ ], callback);
+ };
+
+ Messaging.checkContent = function(content, callback) {
+ if (!content) {
+ return callback(new Error('[[error:invalid-chat-message]]'));
+ }
+
+ if (content.length > (meta.config.maximumChatMessageLength || 1000)) {
+ return callback(new Error('[[error:chat-message-too-long]]'));
+ }
+ callback();
+ };
+
+ Messaging.addMessage = function(fromuid, roomId, content, timestamp, callback) {
+ var mid;
+ var message;
+ var isNewSet;
+
+ async.waterfall([
+ function (next) {
+ Messaging.checkContent(content, next);
+ },
+ function (next) {
+ db.incrObjectField('global', 'nextMid', next);
+ },
+ function (_mid, next) {
+ mid = _mid;
+ message = {
+ content: content,
+ timestamp: timestamp,
+ fromuid: fromuid
+ };
+
+ plugins.fireHook('filter:messaging.save', message, next);
+ },
+ function (message, next) {
+ db.setObject('message:' + mid, message, next);
+ },
+ function (next) {
+ Messaging.isNewSet(fromuid, roomId, timestamp, next);
+ },
+ function (_isNewSet, next) {
+ isNewSet = _isNewSet;
+ db.getSortedSetRange('chat:room:' + roomId + ':uids', 0, -1, next);
+ },
+ function (uids, next) {
+ async.parallel([
+ async.apply(Messaging.addRoomToUsers, roomId, uids, timestamp),
+ async.apply(Messaging.addMessageToUsers, roomId, uids, mid, timestamp),
+ async.apply(Messaging.markUnread, uids, roomId)
+ ], next);
+ },
+ function (results, next) {
+ async.parallel({
+ markRead: async.apply(Messaging.markRead, fromuid, roomId),
+ messages: async.apply(Messaging.getMessagesData, [mid], fromuid, roomId, true)
+ }, next);
+ },
+ function (results, next) {
+ if (!results.messages || !results.messages[0]) {
+ return next(null, null);
+ }
+
+ results.messages[0].newSet = isNewSet;
+ results.messages[0].mid = mid;
+ results.messages[0].roomId = roomId;
+ next(null, results.messages[0]);
+ }
+ ], callback);
+ };
+
+ Messaging.addRoomToUsers = function(roomId, uids, timestamp, callback) {
+ if (!uids.length) {
+ return callback();
+ }
+ var keys = uids.map(function(uid) {
+ return 'uid:' + uid + ':chat:rooms';
+ });
+ db.sortedSetsAdd(keys, timestamp, roomId, callback);
+ };
+
+ Messaging.addMessageToUsers = function(roomId, uids, mid, timestamp, callback) {
+ if (!uids.length) {
+ return callback();
+ }
+ var keys = uids.map(function(uid) {
+ return 'uid:' + uid + ':chat:room:' + roomId + ':mids';
+ });
+ db.sortedSetsAdd(keys, timestamp, mid, callback);
+ };
+};
\ No newline at end of file
diff --git a/src/messaging/delete.js b/src/messaging/delete.js
new file mode 100644
index 0000000000..518ab9fafb
--- /dev/null
+++ b/src/messaging/delete.js
@@ -0,0 +1,27 @@
+'use strict';
+
+var async = require('async');
+var db = require('../database');
+
+module.exports = function(Messaging) {
+
+ Messaging.deleteMessage = function(mid, roomId, callback) {
+ async.waterfall([
+ function (next) {
+ Messaging.getUidsInRoom(roomId, 0, -1, next);
+ },
+ function (uids, next) {
+ if (!uids.length) {
+ return next();
+ }
+ var keys = uids.map(function(uid) {
+ return 'uid:' + uid + ':chat:room:' + roomId + 'mids';
+ });
+ db.sortedSetsRemove(keys, roomId, next);
+ },
+ function(next) {
+ db.delete('message:' + mid, next);
+ }
+ ], callback);
+ };
+};
\ No newline at end of file
diff --git a/src/messaging/edit.js b/src/messaging/edit.js
new file mode 100644
index 0000000000..5b2472c4e4
--- /dev/null
+++ b/src/messaging/edit.js
@@ -0,0 +1,80 @@
+'use strict';
+
+var async = require('async');
+
+var meta = require('../meta');
+var user = require('../user');
+
+var sockets = require('../socket.io');
+
+
+module.exports = function(Messaging) {
+
+ Messaging.editMessage = function(uid, mid, roomId, content, callback) {
+ var uids;
+ async.waterfall([
+ function(next) {
+ Messaging.getMessageField(mid, 'content', next);
+ },
+ function (raw, next) {
+ if (raw === content) {
+ return callback();
+ }
+
+ Messaging.setMessageFields(mid, {
+ content: content,
+ edited: Date.now()
+ }, next);
+ },
+ function (next) {
+ Messaging.getUidsInRoom(roomId, 0, -1, next);
+ },
+ function (_uids, next) {
+ uids = _uids;
+ Messaging.getMessagesData([mid], uid, roomId, true, next);
+ },
+ function (messages, next) {
+ uids.forEach(function(uid) {
+ sockets.in('uid_' + uid).emit('event:chats.edit', {
+ messages: messages
+ });
+ });
+ next();
+ }
+ ], callback);
+ };
+
+ Messaging.canEdit = function(messageId, uid, callback) {
+ if (parseInt(meta.config.disableChat) === 1) {
+ return callback(null, false);
+ }
+
+ async.waterfall([
+ function (next) {
+ user.getUserFields(uid, ['banned', 'email:confirmed'], next);
+ },
+ function (userData, next) {
+ if (parseInt(userData.banned, 10) === 1) {
+ return callback(null, false);
+ }
+
+ if (parseInt(meta.config.requireEmailConfirmation, 10) === 1 && parseInt(userData['email:confirmed'], 10) !== 1) {
+ return callback(null, false);
+ }
+
+ Messaging.getMessageField(messageId, 'fromuid', next);
+ },
+ function(fromUid, next) {
+ if (parseInt(fromUid, 10) === parseInt(uid, 10)) {
+ return callback(null, true);
+ }
+
+ user.isAdministrator(uid, next);
+ },
+ function(isAdmin, next) {
+ next(null, isAdmin);
+ }
+ ], callback);
+ };
+
+};
\ No newline at end of file
diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js
new file mode 100644
index 0000000000..9fca3c952b
--- /dev/null
+++ b/src/messaging/notifications.js
@@ -0,0 +1,105 @@
+'use strict';
+
+var async = require('async');
+var nconf = require('nconf');
+
+var user = require('../user');
+var emailer = require('../emailer');
+var notifications = require('../notifications');
+var meta = require('../meta');
+var utils = require('../../public/src/utils');
+var sockets = require('../socket.io');
+
+module.exports = function(Messaging) {
+
+ Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser
+
+ Messaging.notifyUsersInRoom = function(fromUid, roomId, messageObj) {
+ Messaging.getUidsInRoom(roomId, 0, -1, function(err, uids) {
+ if (err) {
+ return;
+ }
+
+ var data = {
+ roomId: roomId,
+ fromUid: fromUid,
+ message: messageObj
+ };
+ uids.forEach(function(uid) {
+ data.self = parseInt(uid, 10) === parseInt(fromUid) ? 1 : 0;
+ Messaging.pushUnreadCount(uid);
+ sockets.in('uid_' + uid).emit('event:chats.receive', data);
+ });
+
+ // Delayed notifications
+ var queueObj = Messaging.notifyQueue[fromUid + ':' + roomId];
+ if (queueObj) {
+ queueObj.message.content += '\n' + messageObj.content;
+ clearTimeout(queueObj.timeout);
+ } else {
+ queueObj = Messaging.notifyQueue[fromUid + ':' + roomId] = {
+ message: messageObj
+ };
+ }
+
+ queueObj.timeout = setTimeout(function() {
+ sendNotifications(fromUid, uids, roomId, queueObj.message, function(err) {
+ if (!err) {
+ delete Messaging.notifyQueue[fromUid + ':' + roomId];
+ }
+ });
+ }, 1000*60); // wait 60s before sending
+ });
+ };
+
+ function sendNotifications(fromuid, uids, roomId, messageObj, callback) {
+ user.isOnline(uids, function(err, isOnline) {
+ if (err) {
+ return callback(err);
+ }
+
+ uids = uids.filter(function(uid, index) {
+ return isOnline[index] && parseInt(fromuid, 10) !== parseInt(uid, 10);
+ });
+
+ if (!uids.length) {
+ return callback();
+ }
+
+ notifications.create({
+ bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
+ bodyLong: messageObj.content,
+ nid: 'chat_' + fromuid + '_' + roomId,
+ from: fromuid,
+ path: '/chats/' + messageObj.roomId
+ }, function(err, notification) {
+ if (!err && notification) {
+ notifications.push(notification, uids, callback);
+ }
+ });
+
+ if (parseInt(meta.config.disableEmailSubscriptions, 10) === 1) {
+ return callback();
+ }
+
+ user.getMultipleUserSettings(uids, function(err, userSettings) {
+ if (err) {
+ return callback(err);
+ }
+ userSettings = userSettings.filter(function(settings) {
+ return settings && settings.sendChatNotifications;
+ });
+ async.each(userSettings, function(settings, next) {
+ emailer.send('notif_chat', settings.uid, {
+ subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]',
+ summary: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
+ message: messageObj,
+ site_title: meta.config.title || 'NodeBB',
+ url: nconf.get('url'),
+ fromUserslug: utils.slugify(messageObj.fromUser.username)
+ }, next);
+ }, callback);
+ });
+ });
+ }
+};
\ No newline at end of file
diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js
new file mode 100644
index 0000000000..5cc3459291
--- /dev/null
+++ b/src/messaging/rooms.js
@@ -0,0 +1,106 @@
+'use strict';
+
+var async = require('async');
+
+var db = require('../database');
+var user = require('../user');
+
+module.exports = function(Messaging) {
+
+ Messaging.newRoom = function(uid, toUids, callback) {
+ var roomId;
+ var now = Date.now();
+ async.waterfall([
+ function (next) {
+ db.incrObjectField('global', 'nextChatRoomId', next);
+ },
+ function (_roomId, next) {
+ roomId = _roomId;
+ db.sortedSetAdd('chat:room:' + roomId + ':uids', now, uid, next);
+ },
+ function (next) {
+ Messaging.addUsersToRoom(uid, toUids, roomId, next);
+ },
+ function (next) {
+ Messaging.addRoomToUsers(roomId, [uid].concat(toUids), now, next);
+ },
+ function (next) {
+ next(null, roomId);
+ }
+ ], callback);
+ };
+
+ Messaging.isUserInRoom = function(uid, roomId, callback) {
+ db.isSortedSetMember('chat:room:' + roomId + ':uids', uid, callback);
+ };
+
+ Messaging.roomExists = function(roomId, callback) {
+ db.exists('chat:room:' + roomId + ':uids', callback);
+ };
+
+ Messaging.isRoomOwner = function(uid, roomId, callback) {
+ db.getSortedSetRange('chat:room:' + roomId + ':uids', 0, 0, function(err, uids) {
+ if (err) {
+ return callback(err);
+ }
+ if (!Array.isArray(uids) || !uids.length) {
+ return callback(null, false);
+ }
+ callback(null, parseInt(uids[0], 10) === parseInt(uid, 10));
+ });
+ };
+
+ Messaging.addUsersToRoom = function(uid, uids, roomId, callback) {
+ async.waterfall([
+ function (next) {
+ Messaging.isUserInRoom(uid, roomId, next);
+ },
+ function (inRoom, next) {
+ if (!inRoom) {
+ return next(new Error('[[error:cant-add-users-to-chat-room]]'));
+ }
+ var now = Date.now();
+ var timestamps = uids.map(function() {
+ return now;
+ });
+ db.sortedSetAdd('chat:room:' + roomId + ':uids', timestamps, uids, next);
+ }
+ ], callback);
+ };
+
+ Messaging.removeUsersFromRoom = function(uid, uids, roomId, callback) {
+ async.waterfall([
+ function (next) {
+ Messaging.isRoomOwner(uid, roomId, next);
+ },
+ function (isOwner, next) {
+ if (!isOwner) {
+ return next(new Error('[[error:cant-add-users-to-chat-room]]'));
+ }
+ db.sortedSetRemove('chat:room:' + roomId + ':uids', uids, next);
+ },
+ function (next) {
+ var keys = uids.map(function(uid) {
+ return 'uid:' + uid + ':chat:rooms';
+ });
+ db.sortedSetsRemove(keys, roomId, next);
+ }
+ ], callback);
+ };
+
+ Messaging.getUidsInRoom = function(roomId, start, stop, callback) {
+ db.getSortedSetRevRange('chat:room:' + roomId + ':uids', start, stop, callback);
+ };
+
+ Messaging.getUsersInRoom = function(roomId, start, stop, callback) {
+ async.waterfall([
+ function (next) {
+ Messaging.getUidsInRoom(roomId, start, stop, next);
+ },
+ function (uids, next) {
+ user.getUsersFields(uids, ['username', 'uid', 'picture', 'status'], next);
+ }
+ ], callback);
+ };
+
+};
\ No newline at end of file
diff --git a/src/messaging/unread.js b/src/messaging/unread.js
new file mode 100644
index 0000000000..4bceaab3ce
--- /dev/null
+++ b/src/messaging/unread.js
@@ -0,0 +1,45 @@
+'use strict';
+
+var async = require('async');
+
+var db = require('../database');
+var sockets = require('../socket.io');
+
+module.exports = function(Messaging) {
+
+ Messaging.getUnreadCount = function(uid, callback) {
+ db.sortedSetCard('uid:' + uid + ':chat:rooms:unread', callback);
+ };
+
+ Messaging.pushUnreadCount = function(uid) {
+ Messaging.getUnreadCount(uid, function(err, unreadCount) {
+ if (err) {
+ return;
+ }
+ sockets.in('uid_' + uid).emit('event:unread.updateChatCount', unreadCount);
+ });
+ };
+
+ Messaging.markRead = function(uid, roomId, callback) {
+ db.sortedSetRemove('uid:' + uid + ':chat:rooms:unread', roomId, callback);
+ };
+
+ Messaging.markUnread = function(uids, roomId, callback) {
+ async.waterfall([
+ function (next) {
+ Messaging.roomExists(roomId, next);
+ },
+ function (exists, next) {
+ if (!exists) {
+ return next(new Error('[[error:chat-room-does-not-exist]]'));
+ }
+ var keys = uids.map(function(uid) {
+ return 'uid:' + uid + ':chat:rooms:unread';
+ });
+
+ db.sortedSetsAdd(keys, Date.now(), roomId, next);
+ }
+ ], callback);
+ };
+
+};
\ No newline at end of file
diff --git a/src/routes/accounts.js b/src/routes/accounts.js
index 2d492c6c78..e3a753c2fc 100644
--- a/src/routes/accounts.js
+++ b/src/routes/accounts.js
@@ -23,5 +23,5 @@ module.exports = function (app, middleware, controllers) {
setupPageRoute(app, '/user/:userslug/settings', middleware, accountMiddlewares, controllers.accounts.settings.get);
setupPageRoute(app, '/notifications', middleware, [middleware.authenticate], controllers.accounts.notifications.get);
- setupPageRoute(app, '/chats/:userslug?', middleware, [middleware.authenticate], controllers.accounts.chats.get);
+ setupPageRoute(app, '/chats/:roomid?', middleware, [middleware.authenticate], controllers.accounts.chats.get);
};
diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js
index 0361560f01..2b4246b625 100644
--- a/src/socket.io/groups.js
+++ b/src/socket.io/groups.js
@@ -22,6 +22,10 @@ SocketGroups.join = function(socket, data, callback) {
return callback(new Error('[[error:invalid-uid]]'));
}
+ if (data.groupName === 'administrators' || groups.isPrivilegeGroup(data.groupName)) {
+ return callback(new Error('[[error:not-allowed]]'));
+ }
+
groups.exists(data.groupName, function(err, exists) {
if (err || !exists) {
return callback(err || new Error('[[error:no-group]]'));
diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js
index 42ac4092d2..3cd7ba3486 100644
--- a/src/socket.io/modules.js
+++ b/src/socket.io/modules.js
@@ -1,14 +1,13 @@
"use strict";
-var meta = require('../meta'),
- Messaging = require('../messaging'),
- utils = require('../../public/src/utils'),
-
- async = require('async'),
-
- server = require('./'),
-
- SocketModules = {
+var async = require('async');
+var meta = require('../meta');
+var Messaging = require('../messaging');
+var utils = require('../../public/src/utils');
+var server = require('./');
+var user = require('../user');
+
+var SocketModules = {
chats: {},
sounds: {},
settings: {}
@@ -17,36 +16,65 @@ var meta = require('../meta'),
/* Chat */
SocketModules.chats.get = function(socket, data, callback) {
- if(!data) {
+ if(!data || !data.roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
Messaging.getMessages({
- fromuid: socket.uid,
- touid: data.touid,
+ uid: socket.uid,
+ roomId: data.roomId,
since: data.since,
isNew: false
}, callback);
- // Mark chat as read
- Messaging.markRead(socket.uid, data.touid);
+ Messaging.markRead(socket.uid, data.roomId);
};
SocketModules.chats.getRaw = function(socket, data, callback) {
- if(!data || !data.hasOwnProperty('mid')) {
+ if (!data || !data.hasOwnProperty('mid')) {
return callback(new Error('[[error:invalid-data]]'));
}
+ async.waterfall([
+ function (next) {
+ Messaging.isUserInRoom(socket.uid, data.roomId, next);
+ },
+ function (inRoom, next) {
+ if (!inRoom) {
+ return next(new Error('[[error:not-allowed]]'));
+ }
+ Messaging.getMessageField(data.mid, 'content', next);
+ }
+ ], callback);
+};
- Messaging.getMessageField(data.mid, 'content', callback);
+SocketModules.chats.newRoom = function(socket, data, callback) {
+ if (!data) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+ var now = Date.now();
+ // Websocket rate limiting
+ socket.lastChatMessageTime = socket.lastChatMessageTime || 0;
+ if (now - socket.lastChatMessageTime < 200) {
+ return callback(new Error('[[error:too-many-messages]]'));
+ } else {
+ socket.lastChatMessageTime = now;
+ }
+
+ Messaging.canMessageUser(socket.uid, data.touid, function(err, allowed) {
+ if (err || !allowed) {
+ return callback(err || new Error('[[error:chat-restricted]]'));
+ }
+
+ Messaging.newRoom(socket.uid, [data.touid], callback);
+ });
};
SocketModules.chats.send = function(socket, data, callback) {
- if (!data) {
+ if (!data || !data.roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
- var now = Date.now(),
- touid = parseInt(data.touid, 10);
+ var now = Date.now();
// Websocket rate limiting
socket.lastChatMessageTime = socket.lastChatMessageTime || 0;
@@ -56,57 +84,112 @@ SocketModules.chats.send = function(socket, data, callback) {
socket.lastChatMessageTime = now;
}
- Messaging.canMessage(socket.uid, touid, function(err, allowed) {
+ Messaging.canMessageRoom(socket.uid, data.roomId, function(err, allowed) {
if (err || !allowed) {
return callback(err || new Error('[[error:chat-restricted]]'));
}
- Messaging.addMessage(socket.uid, touid, data.message, function(err, message) {
+ Messaging.sendMessage(socket.uid, data.roomId, data.message, now, function(err, message) {
if (err) {
return callback(err);
}
- Messaging.notifyUser(socket.uid, touid, message);
+ Messaging.notifyUsersInRoom(socket.uid, data.roomId, message);
callback();
});
});
};
+SocketModules.chats.getUsersInRoom = function(socket, data, callback) {
+ if (!data || !data.roomId) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+
+ async.waterfall([
+ function (next) {
+ Messaging.isUserInRoom(socket.uid, data.roomId, next);
+ },
+ function (inRoom, next) {
+ if (!inRoom) {
+ return next(new Error('[[error:not-allowerd]]'));
+ }
+ Messaging.getUsersInRoom(data.roomId, 0, -1, next);
+ }
+ ], callback);
+};
+
+SocketModules.chats.addUserToRoom = function(socket, data, callback) {
+ if (!data || !data.roomId || !data.username) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+ async.waterfall([
+ function (next) {
+ user.getUidByUsername(data.username, next);
+ },
+ function (uid, next) {
+ if (!uid) {
+ return next(new Error('[[error:no-user]]'));
+ }
+ Messaging.addUsersToRoom(socket.uid, [uid], data.roomId, next);
+ }
+ ], callback);
+};
+
+SocketModules.chats.removeUserFromRoom = function(socket, data, callback) {
+ if (!data || !data.roomId) {
+ return callback(new Error('[[error:invalid-data]]'));
+ }
+ async.waterfall([
+ function (next) {
+ user.getUidByUsername(data.username, next);
+ },
+ function (uid, next) {
+ if (!uid) {
+ return next(new Error('[[error:no-user]]'));
+ }
+
+ Messaging.removeUsersFromRoom(socket.uid, [uid], data.roomId, next);
+ }
+ ], callback);
+};
+
SocketModules.chats.edit = function(socket, data, callback) {
- if (!data) {
+ if (!data || !data.roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
Messaging.canEdit(data.mid, socket.uid, function(err, allowed) {
- if (allowed) {
- Messaging.editMessage(data.mid, data.message, callback);
- } else {
- return callback(new Error('[[error:cant-edit-chat-message]]'));
+ if (err || !allowed) {
+ return callback(err || new Error('[[error:cant-edit-chat-message]]'));
}
+
+ Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message, callback);
});
};
SocketModules.chats.delete = function(socket, data, callback) {
- if (!data) {
+ if (!data || !data.roomId || !data.messageId) {
return callback(new Error('[[error:invalid-data]]'));
}
Messaging.canEdit(data.messageId, socket.uid, function(err, allowed) {
- if (allowed) {
- Messaging.deleteMessage(data.messageId, callback);
+ if (err || !allowed) {
+ return callback(err || new Error('[[error:cant-delete-chat-message]]'));
}
+
+ Messaging.deleteMessage(data.messageId, data.roomId, callback);
});
-}
+};
-SocketModules.chats.canMessage = function(socket, toUid, callback) {
- Messaging.canMessage(socket.uid, toUid, function(err, allowed) {
+SocketModules.chats.canMessage = function(socket, roomId, callback) {
+ Messaging.canMessageRoom(socket.uid, roomId, function(err, allowed) {
callback(!allowed ? new Error('[[error:chat-restricted]]') : undefined);
});
};
-SocketModules.chats.markRead = function(socket, touid, callback) {
- Messaging.markRead(socket.uid, touid, function(err) {
+SocketModules.chats.markRead = function(socket, roomId, callback) {
+ Messaging.markRead(socket.uid, roomId, function(err) {
if (err) {
return callback(err);
}
@@ -125,10 +208,20 @@ SocketModules.chats.userStopTyping = function(socket, data, callback) {
};
function sendTypingNotification(event, socket, data, callback) {
- if (!socket.uid || !data) {
+ if (!socket.uid || !data || !data.roomId) {
return;
}
- server.in('uid_' + data.touid).emit(event, data.fromUid);
+
+ Messaging.getUidsInRoom(data.roomId, 0, -1, function(err, uids) {
+ if (err) {
+ return callback(err);
+ }
+ uids.forEach(function(uid) {
+ if (socket.uid !== parseInt(uid, 10)) {
+ server.in('uid_' + uid).emit(event, data.fromUid);
+ }
+ });
+ });
}
SocketModules.chats.getRecentChats = function(socket, data, callback) {
diff --git a/src/upgrade.js b/src/upgrade.js
index ecbfcfe6dd..4b2f78e56c 100644
--- a/src/upgrade.js
+++ b/src/upgrade.js
@@ -10,7 +10,7 @@ var db = require('./database'),
schemaDate, thisSchemaDate,
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
- latestSchema = Date.UTC(2015, 10, 6);
+ latestSchema = Date.UTC(2015, 11, 15);
Upgrade.check = function(callback) {
db.get('schemaDate', function(err, value) {
@@ -122,7 +122,7 @@ Upgrade.upgrade = function(callback) {
db.setObjectField('user:' + uid, 'picture', '', next);
},
function (next) {
- db.deleteObjectField('user:' + uid, 'gravatarpicture', next);
+ db.deleteObjectField('user:' + uid, 'gravatarpicture', next);
}
], next);
} else {
@@ -143,8 +143,96 @@ Upgrade.upgrade = function(callback) {
winston.info('[2015/11/06] Gravatar removal skipped');
next();
}
- }
+ },
+ function(next) {
+ thisSchemaDate = Date.UTC(2015, 11, 15);
+
+ if (schemaDate < thisSchemaDate) {
+ updatesMade = true;
+ winston.info('[2015/12/15] Upgrading chats');
+
+ db.getObjectFields('global', ['nextMid', 'nextChatRoomId'], function(err, globalData) {
+ if (err) {
+ return next(err);
+ }
+
+ var rooms = {};
+ var roomId = globalData.nextChatRoomId || 1;
+ var currentMid = 1;
+
+ async.whilst(function() {
+ return currentMid < globalData.nextMid;
+ }, function(next) {
+ db.getObject('message:' + currentMid, function(err, message) {
+ function addMessageToUids(roomId, callback) {
+ async.parallel([
+ function(next) {
+ db.sortedSetAdd('uid:' + message.fromuid + ':chat:room:' + roomId + ':mids', msgTime, currentMid, next);
+ },
+ function(next) {
+ db.sortedSetAdd('uid:' + message.touid + ':chat:room:' + roomId + ':mids', msgTime, currentMid, next);
+ }
+ ], callback);
+ }
+
+ if (err || !message) {
+ winston.info('skipping chat message ', currentMid);
+ currentMid ++;
+ return next(err);
+ }
+
+ var pairID = [parseInt(message.fromuid, 10), parseInt(message.touid, 10)].sort().join(':');
+ var msgTime = parseInt(message.timestamp, 10);
+
+ if (rooms[pairID]) {
+ winston.info('adding message ' + currentMid + ' to existing roomID ' + roomId);
+ addMessageToUids(rooms[pairID], function(err) {
+ if (err) {
+ return next(err);
+ }
+ currentMid ++;
+ next();
+ });
+ } else {
+ winston.info('adding message ' + currentMid + ' to new roomID ' + roomId);
+ async.parallel([
+ function(next) {
+ db.sortedSetAdd('uid:' + message.fromuid + ':chat:rooms', msgTime, roomId, next);
+ },
+ function(next) {
+ db.sortedSetAdd('uid:' + message.touid + ':chat:rooms', msgTime, roomId, next);
+ },
+ function(next) {
+ db.sortedSetAdd('chat:room:' + roomId + ':uids', [msgTime, msgTime + 1], [message.fromuid, message.touid], next);
+ },
+ function(next) {
+ addMessageToUids(roomId, next);
+ }
+ ], function(err) {
+ if (err) {
+ return next(err);
+ }
+ rooms[pairID] = roomId;
+ roomId ++;
+ currentMid ++;
+ db.setObjectField('global', 'nextChatRoomId', roomId, next);
+ });
+ }
+ });
+ }, function(err) {
+ if (err) {
+ return next(err);
+ }
+ winston.info('[2015/12/15] Chats upgrade done!');
+ Upgrade.update(thisSchemaDate, next);
+ });
+ });
+ } else {
+ winston.info('[2015/12/15] Chats upgrade skipped!');
+ next();
+ }
+ }
// Add new schema updates here
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!!
], function(err) {
diff --git a/src/user.js b/src/user.js
index 4b64b52dbd..7a9213a963 100644
--- a/src/user.js
+++ b/src/user.js
@@ -132,13 +132,27 @@ var async = require('async'),
};
User.isOnline = function(uid, callback) {
- db.sortedSetScore('users:online', uid, function(err, lastonline) {
- if (err) {
- return callback(err);
- }
- var isOnline = Date.now() - parseInt(lastonline, 10) < 300000;
- callback(null, isOnline);
- });
+ if (Array.isArray(uid)) {
+ db.sortedSetScores('users:online', uid, function(err, lastonline) {
+ if (err) {
+ return callback(err);
+ }
+ var now = Date.now();
+ var isOnline = uid.map(function(uid, index) {
+ return now - lastonline[index] < 300000;
+ });
+ callback(null, isOnline);
+ });
+ } else {
+ db.sortedSetScore('users:online', uid, function(err, lastonline) {
+ if (err) {
+ return callback(err);
+ }
+ var isOnline = Date.now() - parseInt(lastonline, 10) < 300000;
+ callback(null, isOnline);
+ });
+ }
+
};
User.exists = function(uid, callback) {
diff --git a/src/user/delete.js b/src/user/delete.js
index 2fc444da42..5b822fcd7f 100644
--- a/src/user/delete.js
+++ b/src/user/delete.js
@@ -55,6 +55,9 @@ module.exports = function(User) {
function (next) {
deleteVotes(uid, next);
},
+ function (next) {
+ deleteChats(uid, next);
+ },
function (next) {
async.parallel([
function(next) {
@@ -97,6 +100,7 @@ module.exports = function(User) {
'uid:' + uid + ':favourites', 'uid:' + uid + ':followed_tids', 'user:' + uid + ':settings',
'uid:' + uid + ':topics', 'uid:' + uid + ':posts',
'uid:' + uid + ':chats', 'uid:' + uid + ':chats:unread',
+ 'uid:' + uid + ':chat:rooms', 'uid:' + uid + ':chat:rooms:unread',
'uid:' + uid + ':upvote', 'uid:' + uid + ':downvote',
'uid:' + uid + ':ignored:cids', 'uid:' + uid + ':flag:pids'
];
@@ -145,6 +149,29 @@ module.exports = function(User) {
});
}
+ function deleteChats(uid, callback) {
+ async.waterfall([
+ function (next) {
+ db.getSortedSetRange('uid:' + uid + ':chat:rooms', 0, -1, next);
+ },
+ function (roomIds, next) {
+ var userKeys = roomIds.map(function(roomId) {
+ return 'uid:' + uid + ':chat:room:' + roomId + ':mids';
+ });
+ var roomKeys = roomIds.map(function(roomId) {
+ return 'chat:room:' + roomId + ':uids';
+ });
+
+ async.parallel([
+ async.apply(db.sortedSetsRemove, roomKeys, uid),
+ async.apply(db.deleteAll, userKeys)
+ ], next);
+ }
+ ], function(err) {
+ callback(err);
+ });
+ }
+
function deleteUserIps(uid, callback) {
async.waterfall([
function (next) {
diff --git a/src/user/notifications.js b/src/user/notifications.js
index 2d08b83e33..f40143a28d 100644
--- a/src/user/notifications.js
+++ b/src/user/notifications.js
@@ -125,9 +125,7 @@ var async = require('async'),
notification.path = pidToPaths[notification.pid] || notification.path || '';
- if (notification.nid.startsWith('chat')) {
- notification.path = '/chats/' + notification.user.userslug;
- } else if (notification.nid.startsWith('follow')) {
+ if (notification.nid.startsWith('follow')) {
notification.path = '/user/' + notification.user.userslug;
}
diff --git a/tests/messaging.js b/tests/messaging.js
index a709640430..ae1db0952a 100644
--- a/tests/messaging.js
+++ b/tests/messaging.js
@@ -27,14 +27,14 @@ describe('Messaging Library', function() {
describe('.canMessage()', function() {
it('should not error out', function(done) {
- Messaging.canMessage(testUids[1], testUids[2], function(err, allowed) {
+ Messaging.canMessageUser(testUids[1], testUids[2], function(err, allowed) {
assert.ifError(err);
done();
});
});
it('should allow messages to be sent to an unrestricted user', function(done) {
- Messaging.canMessage(testUids[1], testUids[2], function(err, allowed) {
+ Messaging.canMessageUser(testUids[1], testUids[2], function(err, allowed) {
assert.strictEqual(allowed, true, 'should be true, received ' + allowed);
done();
});
@@ -42,7 +42,7 @@ describe('Messaging Library', function() {
it('should NOT allow messages to be sent to a restricted user', function(done) {
User.setSetting(testUids[1], 'restrictChat', '1', function() {
- Messaging.canMessage(testUids[2], testUids[1], function(err, allowed) {
+ Messaging.canMessageUser(testUids[2], testUids[1], function(err, allowed) {
assert.strictEqual(allowed, false, 'should be false, received ' + allowed);
done();
});
@@ -50,7 +50,7 @@ describe('Messaging Library', function() {
});
it('should always allow admins through', function(done) {
- Messaging.canMessage(testUids[0], testUids[1], function(err, allowed) {
+ Messaging.canMessageUser(testUids[0], testUids[1], function(err, allowed) {
assert.strictEqual(allowed, true, 'should be true, received ' + allowed);
done();
});
@@ -58,7 +58,7 @@ describe('Messaging Library', function() {
it('should allow messages to be sent to a restricted user if restricted user follows sender', function(done) {
User.follow(testUids[1], testUids[2], function() {
- Messaging.canMessage(testUids[2], testUids[1], function(err, allowed) {
+ Messaging.canMessageUser(testUids[2], testUids[1], function(err, allowed) {
assert.strictEqual(allowed, true, 'should be true, received ' + allowed);
done();
});