diff --git a/app/api/server/lib/messages.js b/app/api/server/lib/messages.js index 9fdd48b33e5..18b0b71ca17 100644 --- a/app/api/server/lib/messages.js +++ b/app/api/server/lib/messages.js @@ -115,3 +115,28 @@ export async function findSnippetedMessages({ uid, roomId, pagination: { offset, total, }; } + +export async function findDiscussionsFromRoom({ uid, roomId, pagination: { offset, count, sort } }) { + const room = await Rooms.findOneById(roomId); + + if (!await canAccessRoomAsync(room, { _id: uid })) { + throw new Error('error-not-allowed'); + } + + const cursor = Messages.findDiscussionsByRoom(roomId, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }); + + const total = await cursor.count(); + + const messages = await cursor.toArray(); + + return { + messages, + count: messages.length, + offset, + total, + }; +} diff --git a/app/api/server/v1/chat.js b/app/api/server/v1/chat.js index 4d1610a79d9..8e5ac669d22 100644 --- a/app/api/server/v1/chat.js +++ b/app/api/server/v1/chat.js @@ -9,7 +9,7 @@ import { API } from '../api'; import Rooms from '../../../models/server/models/Rooms'; import Users from '../../../models/server/models/Users'; import { settings } from '../../../settings'; -import { findMentionedMessages, findStarredMessages, findSnippetedMessageById, findSnippetedMessages } from '../lib/messages'; +import { findMentionedMessages, findStarredMessages, findSnippetedMessageById, findSnippetedMessages, findDiscussionsFromRoom } from '../lib/messages'; API.v1.addRoute('chat.delete', { authRequired: true }, { post() { @@ -680,3 +680,25 @@ API.v1.addRoute('chat.getSnippetedMessages', { authRequired: true }, { return API.v1.success(messages); }, }); + +API.v1.addRoute('chat.getDiscussions', { authRequired: true }, { + get() { + const { roomId } = this.queryParams; + const { sort } = this.parseJsonQuery(); + const { offset, count } = this.getPaginationItems(); + + if (!roomId) { + throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.'); + } + const messages = Promise.await(findDiscussionsFromRoom({ + uid: this.userId, + roomId, + pagination: { + offset, + count, + sort, + }, + })); + return API.v1.success(messages); + }, +}); diff --git a/app/discussion/client/index.js b/app/discussion/client/index.js index 9a7bdc13cd5..e8895fbe0f2 100644 --- a/app/discussion/client/index.js +++ b/app/discussion/client/index.js @@ -5,7 +5,6 @@ import './views/DiscussionTabbar'; // Other UI extensions import './lib/messageTypes/discussionMessage'; -import './lib/discussionsOfRoom'; import './createDiscussionMessageAction'; import './discussionFromMessageBox'; import './tabBar'; diff --git a/app/discussion/client/lib/discussionsOfRoom.js b/app/discussion/client/lib/discussionsOfRoom.js deleted file mode 100644 index a620af48a42..00000000000 --- a/app/discussion/client/lib/discussionsOfRoom.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Mongo } from 'meteor/mongo'; - -export const DiscussionOfRoom = new Mongo.Collection('rocketchat_discussions_of_room'); diff --git a/app/discussion/client/views/DiscussionTabbar.js b/app/discussion/client/views/DiscussionTabbar.js index ff73ab8ff64..4fe0175898c 100644 --- a/app/discussion/client/views/DiscussionTabbar.js +++ b/app/discussion/client/views/DiscussionTabbar.js @@ -1,18 +1,24 @@ import _ from 'underscore'; import { ReactiveVar } from 'meteor/reactive-var'; +import { Mongo } from 'meteor/mongo'; import { Template } from 'meteor/templating'; import { messageContext } from '../../../ui-utils/client/lib/messageContext'; -import { DiscussionOfRoom } from '../lib/discussionsOfRoom'; +import { Messages } from '../../../models/client'; +import { APIClient } from '../../../utils/client'; +import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager'; import './DiscussionTabbar.html'; +const LIMIT_DEFAULT = 50; + Template.discussionsTabbar.helpers({ hasMessages() { - return Template.instance().cursor.count() > 0; + return Template.instance().messages.find().count(); }, messages() { - return Template.instance().cursor; + const instance = Template.instance(); + return instance.messages.find({}, { limit: instance.limit.get(), sort: { ts: -1 } }); }, message() { return _.extend(this, { customClass: 'pinned', actionContext: 'pinned' }); @@ -25,31 +31,47 @@ Template.discussionsTabbar.helpers({ Template.discussionsTabbar.onCreated(function() { this.rid = this.data.rid; - this.cursor = DiscussionOfRoom.find({ - rid: this.rid, - }, { - sort: { - ts: -1, - }, - }); + this.messages = new Mongo.Collection(null); this.hasMore = new ReactiveVar(true); - this.limit = new ReactiveVar(50); + this.limit = new ReactiveVar(LIMIT_DEFAULT); this.autorun(() => { - const { rid } = Template.currentData(); - this.subscribe('discussionsOfRoom', rid, this.limit.get(), () => { - const discussionCount = this.cursor.count(); - if (discussionCount < this.limit.get()) { - this.hasMore.set(false); - } + const query = { + rid: this.rid, + drid: { $exists: true }, + }; + + this.cursor && this.cursor.stop(); + + this.limit.set(LIMIT_DEFAULT); + + this.cursor = Messages.find(query).observe({ + added: ({ _id, ...message }) => { + this.messages.upsert({ _id }, message); + }, + changed: ({ _id, ...message }) => { + this.messages.upsert({ _id }, message); + }, + removed: ({ _id }) => { + this.messages.remove({ _id }); + }, }); }); + + this.autorun(async () => { + const limit = this.limit.get(); + const { messages, total } = await APIClient.v1.get(`chat.getDiscussions?roomId=${ this.rid }&count=${ limit }`); + + upsertMessageBulk({ msgs: messages }, this.messages); + + this.hasMore.set(total > limit); + }); }); Template.discussionsTabbar.events({ 'scroll .js-list': _.throttle(function(e, instance) { if (e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight - 10 && instance.hasMore.get()) { - instance.limit.set(instance.limit.get() + 50); + instance.limit.set(instance.limit.get() + LIMIT_DEFAULT); } }, 200), }); diff --git a/app/discussion/server/publications/discussionsOfRoom.js b/app/discussion/server/publications/discussionsOfRoom.js index eebd89a5c7c..23f70605b53 100644 --- a/app/discussion/server/publications/discussionsOfRoom.js +++ b/app/discussion/server/publications/discussionsOfRoom.js @@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Messages } from '../../../models/server'; Meteor.publish('discussionsOfRoom', function(rid, limit = 50) { + console.warn('The publication "discussionsOfRoom" is deprecated and will be removed after version v3.0.0'); if (!this.userId) { return this.ready(); } diff --git a/app/models/server/raw/Messages.js b/app/models/server/raw/Messages.js index 17c7ca07b81..c1d4d7e7a57 100644 --- a/app/models/server/raw/Messages.js +++ b/app/models/server/raw/Messages.js @@ -41,4 +41,10 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } + + findDiscussionsByRoom(rid, options) { + const query = { rid, drid: { $exists: true } }; + + return this.find(query, options); + } } diff --git a/tests/end-to-end/api/05-chat.js b/tests/end-to-end/api/05-chat.js index 38e16220433..42085bbcf56 100644 --- a/tests/end-to-end/api/05-chat.js +++ b/tests/end-to-end/api/05-chat.js @@ -1442,6 +1442,240 @@ describe('[Chat]', function() { }); }); }); + + + describe('[/chat.getMentionedMessages]', () => { + it('should return an error when the required "roomId" parameter is not sent', (done) => { + request.get(api('chat.getMentionedMessages')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.errorType).to.be.equal('error-invalid-params'); + }) + .end(done); + }); + + it('should return an error when the roomId is invalid', (done) => { + request.get(api('chat.getMentionedMessages?roomId=invalid-room')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-allowed'); + }) + .end(done); + }); + + it('should return the mentioned messages', (done) => { + request.get(api('chat.getMentionedMessages?roomId=GENERAL')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.messages).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }) + .end(done); + }); + }); + + describe('[/chat.getStarredMessages]', () => { + it('should return an error when the required "roomId" parameter is not sent', (done) => { + request.get(api('chat.getStarredMessages')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.errorType).to.be.equal('error-invalid-params'); + }) + .end(done); + }); + + it('should return an error when the roomId is invalid', (done) => { + request.get(api('chat.getStarredMessages?roomId=invalid-room')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-allowed'); + }) + .end(done); + }); + + it('should return the starred messages', (done) => { + request.get(api('chat.getStarredMessages?roomId=GENERAL')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.messages).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }) + .end(done); + }); + }); + + describe('[/chat.getSnippetedMessageById]', () => { + it('should return an error when the snippeted messages is disabled', (done) => { + updateSetting('Message_AllowSnippeting', false).then(() => { + request.get(api('chat.getSnippetedMessageById?messageId=invalid-id')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-allowed'); + }) + .end(done); + }); + }); + it('should return an error when the required "messageId" parameter is not sent', (done) => { + updateSetting('Message_AllowSnippeting', true).then(() => { + request.get(api('chat.getSnippetedMessageById')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.errorType).to.be.equal('error-invalid-params'); + }) + .end(done); + }); + }); + }); + + describe('[/chat.getSnippetedMessages]', () => { + it('should return an error when the required "roomId" parameter is not sent', (done) => { + request.get(api('chat.getSnippetedMessages')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.errorType).to.be.equal('error-invalid-params'); + }) + .end(done); + }); + + it('should return an error when the roomId is invalid', (done) => { + request.get(api('chat.getSnippetedMessages?roomId=invalid-room')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-allowed'); + }) + .end(done); + }); + + it('should return an error when the snippeted messages is disabled', (done) => { + updateSetting('Message_AllowSnippeting', false).then(() => { + request.get(api('chat.getSnippetedMessages?roomId=invalid-room')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-allowed'); + }) + .end(done); + }); + }); + + it('should return the snippeted messages', (done) => { + updateSetting('Message_AllowSnippeting', true).then(() => { + request.get(api('chat.getSnippetedMessages?roomId=GENERAL')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.messages).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }) + .end(done); + }); + }); + + it('should return an error when the messageId is invalid', (done) => { + request.get(api('chat.getSnippetedMessageById?messageId=invalid-id')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('invalid-message'); + }) + .end(done); + }); + }); + + describe('[/chat.getDiscussions]', () => { + it('should return an error when the required "roomId" parameter is not sent', (done) => { + request.get(api('chat.getDiscussions')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.errorType).to.be.equal('error-invalid-params'); + }) + .end(done); + }); + + it('should return an error when the roomId is invalid', (done) => { + request.get(api('chat.getDiscussions?roomId=invalid-room')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('error-not-allowed'); + }) + .end(done); + }); + + it('should return the discussions of a room', (done) => { + request.get(api('chat.getDiscussions?roomId=GENERAL')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.messages).to.be.an('array'); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }) + .end(done); + }); + + it('should return an error when the messageId is invalid', (done) => { + request.get(api('chat.getSnippetedMessageById?messageId=invalid-id')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('invalid-message'); + }) + .end(done); + }); + }); }); describe('Threads', () => { @@ -2110,184 +2344,4 @@ describe('Threads', () => { }); }); }); - - describe('[/chat.getMentionedMessages]', () => { - it('should return an error when the required "roomId" parameter is not sent', (done) => { - request.get(api('chat.getMentionedMessages')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('error-invalid-params'); - }) - .end(done); - }); - - it('should return an error when the roomId is invalid', (done) => { - request.get(api('chat.getMentionedMessages?roomId=invalid-room')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('error-not-allowed'); - }) - .end(done); - }); - - it('should return the mentioned messages', (done) => { - request.get(api('chat.getMentionedMessages?roomId=GENERAL')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body.messages).to.be.an('array'); - expect(res.body).to.have.property('offset'); - expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('count'); - }) - .end(done); - }); - }); - - describe('[/chat.getStarredMessages]', () => { - it('should return an error when the required "roomId" parameter is not sent', (done) => { - request.get(api('chat.getStarredMessages')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('error-invalid-params'); - }) - .end(done); - }); - - it('should return an error when the roomId is invalid', (done) => { - request.get(api('chat.getStarredMessages?roomId=invalid-room')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('error-not-allowed'); - }) - .end(done); - }); - - it('should return the starred messages', (done) => { - request.get(api('chat.getStarredMessages?roomId=GENERAL')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body.messages).to.be.an('array'); - expect(res.body).to.have.property('offset'); - expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('count'); - }) - .end(done); - }); - }); - - describe('[/chat.getSnippetedMessageById]', () => { - it('should return an error when the snippeted messages is disabled', (done) => { - updateSetting('Message_AllowSnippeting', false).then(() => { - request.get(api('chat.getSnippetedMessageById?messageId=invalid-id')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('error-not-allowed'); - }) - .end(done); - }); - }); - it('should return an error when the required "messageId" parameter is not sent', (done) => { - updateSetting('Message_AllowSnippeting', true).then(() => { - request.get(api('chat.getSnippetedMessageById')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('error-invalid-params'); - }) - .end(done); - }); - }); - }); - - describe('[/chat.getSnippetedMessages]', () => { - it('should return an error when the required "roomId" parameter is not sent', (done) => { - request.get(api('chat.getSnippetedMessages')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('error-invalid-params'); - }) - .end(done); - }); - - it('should return an error when the roomId is invalid', (done) => { - request.get(api('chat.getSnippetedMessages?roomId=invalid-room')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('error-not-allowed'); - }) - .end(done); - }); - - it('should return an error when the snippeted messages is disabled', (done) => { - updateSetting('Message_AllowSnippeting', false).then(() => { - request.get(api('chat.getSnippetedMessages?roomId=invalid-room')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('error-not-allowed'); - }) - .end(done); - }); - }); - - it('should return the snippeted messages', (done) => { - updateSetting('Message_AllowSnippeting', true).then(() => { - request.get(api('chat.getSnippetedMessages?roomId=GENERAL')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body.messages).to.be.an('array'); - expect(res.body).to.have.property('offset'); - expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('count'); - }) - .end(done); - }); - }); - - it('should return an error when the messageId is invalid', (done) => { - request.get(api('chat.getSnippetedMessageById?messageId=invalid-id')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('invalid-message'); - }) - .end(done); - }); - }); });