diff --git a/app/livechat/client/views/app/tabbar/visitorHistory.html b/app/livechat/client/views/app/tabbar/visitorHistory.html index 5d31eb9af9f..25b744f51cc 100644 --- a/app/livechat/client/views/app/tabbar/visitorHistory.html +++ b/app/livechat/client/views/app/tabbar/visitorHistory.html @@ -5,13 +5,17 @@

{{_ "Past_Chats"}}

-
- -
+ {{#if isLoading}} + {{_ "Loading..."}} + {{else}} +
+ +
+ {{/if}}
diff --git a/app/livechat/client/views/app/tabbar/visitorHistory.js b/app/livechat/client/views/app/tabbar/visitorHistory.js index b475d85102e..84d3c00b447 100644 --- a/app/livechat/client/views/app/tabbar/visitorHistory.js +++ b/app/livechat/client/views/app/tabbar/visitorHistory.js @@ -1,27 +1,20 @@ import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; -import { Mongo } from 'meteor/mongo'; import moment from 'moment'; +import _ from 'underscore'; -import { ChatRoom } from '../../../../../models'; import './visitorHistory.html'; +import { APIClient } from '../../../../../utils/client'; -const visitorHistory = new Mongo.Collection('visitor_history'); +const ITEMS_COUNT = 50; Template.visitorHistory.helpers({ - historyLoaded() { - return !Template.instance().loadHistory.ready(); + isLoading() { + return Template.instance().isLoading.get(); }, previousChats() { - return visitorHistory.find({ - _id: { $ne: this.rid }, - 'v._id': Template.instance().visitorId.get(), - }, { - sort: { - ts: -1, - }, - }); + return Template.instance().history.get(); }, title() { @@ -38,15 +31,40 @@ Template.visitorHistory.helpers({ Template.visitorHistory.onCreated(function() { const currentData = Template.currentData(); this.visitorId = new ReactiveVar(); + this.isLoading = new ReactiveVar(false); + this.history = new ReactiveVar([]); + this.offset = new ReactiveVar(0); + this.total = new ReactiveVar(0); - this.autorun(() => { - const room = ChatRoom.findOne({ _id: Template.currentData().rid }); - if (room) { + this.autorun(async () => { + const { room } = await APIClient.v1.get(`rooms.info?roomId=${ currentData.rid }`); + if (room && room.v) { this.visitorId.set(room.v._id); } }); - if (currentData && currentData.rid) { - this.loadHistory = this.subscribe('livechat:visitorHistory', { rid: currentData.rid }); - } + this.autorun(async () => { + if (!this.visitorId.get() || !currentData || !currentData.rid) { + return; + } + + const offset = this.offset.get(); + this.isLoading.set(true); + const { history, total } = await APIClient.v1.get(`livechat/visitors.chatHistory/room/${ currentData.rid }/visitor/${ this.visitorId.get() }?count=${ ITEMS_COUNT }&offset=${ offset }`); + this.isLoading.set(false); + this.total.set(total); + this.history.set(this.history.get().concat(history)); + }); +}); + +Template.visitorHistory.events({ + 'scroll .visitor-scroll': _.throttle(function(e, instance) { + if (e.target.scrollTop >= (e.target.scrollHeight - e.target.clientHeight)) { + const history = instance.history.get(); + if (instance.total.get() <= history.length) { + return; + } + return instance.offset.set(instance.offset.get() + ITEMS_COUNT); + } + }, 200), }); diff --git a/app/livechat/imports/server/rest/visitors.js b/app/livechat/imports/server/rest/visitors.js index 28229b334eb..42b5a20b25d 100644 --- a/app/livechat/imports/server/rest/visitors.js +++ b/app/livechat/imports/server/rest/visitors.js @@ -2,7 +2,7 @@ import { check } from 'meteor/check'; import { API } from '../../../../api'; -import { findVisitorInfo, findVisitedPages } from '../../../server/api/lib/visitors'; +import { findVisitorInfo, findVisitedPages, findChatHistory } from '../../../server/api/lib/visitors'; API.v1.addRoute('livechat/visitors.info', { authRequired: true }, { get() { @@ -38,3 +38,27 @@ API.v1.addRoute('livechat/visitors.pagesVisited/:roomId', { authRequired: true } return API.v1.success(pages); }, }); + +API.v1.addRoute('livechat/visitors.chatHistory/room/:roomId/visitor/:visitorId', { authRequired: true }, { + get() { + check(this.urlParams, { + visitorId: String, + roomId: String, + }); + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + + const history = Promise.await(findChatHistory({ + userId: this.userId, + roomId: this.urlParams.roomId, + visitorId: this.urlParams.visitorId, + pagination: { + offset, + count, + sort, + }, + })); + + return API.v1.success(history); + }, +}); diff --git a/app/livechat/server/api/lib/visitors.js b/app/livechat/server/api/lib/visitors.js index 48e946d801e..ad7cb3da93e 100644 --- a/app/livechat/server/api/lib/visitors.js +++ b/app/livechat/server/api/lib/visitors.js @@ -1,5 +1,6 @@ import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { LivechatVisitors, Messages, LivechatRooms } from '../../../../models/server/raw'; +import { canAccessRoomAsync } from '../../../../authorization/server/functions/canAccessRoom'; export async function findVisitorInfo({ userId, visitorId }) { if (!await hasPermissionAsync(userId, 'view-l-room')) { @@ -24,7 +25,7 @@ export async function findVisitedPages({ userId, roomId, pagination: { offset, c if (!room) { throw new Error('invalid-room'); } - const cursor = await Messages.findByRoomIdAndType(room._id, 'livechat_navigation_history', { + const cursor = Messages.findByRoomIdAndType(room._id, 'livechat_navigation_history', { sort: sort || { ts: -1 }, skip: offset, limit: count, @@ -41,3 +42,33 @@ export async function findVisitedPages({ userId, roomId, pagination: { offset, c total, }; } + +export async function findChatHistory({ userId, roomId, visitorId, pagination: { offset, count, sort } }) { + if (!await hasPermissionAsync(userId, 'view-l-room')) { + throw new Error('error-not-authorized'); + } + const room = await LivechatRooms.findOneById(roomId); + if (!room) { + throw new Error('invalid-room'); + } + if (!await canAccessRoomAsync(room, { _id: userId })) { + throw new Error('error-not-allowed'); + } + + const cursor = LivechatRooms.findByVisitorId(visitorId, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }); + + const total = await cursor.count(); + + const history = await cursor.toArray(); + + return { + history, + count: history.length, + offset, + total, + }; +} diff --git a/app/livechat/server/publications/visitorHistory.js b/app/livechat/server/publications/visitorHistory.js index 3447983113b..9f2e9296a15 100644 --- a/app/livechat/server/publications/visitorHistory.js +++ b/app/livechat/server/publications/visitorHistory.js @@ -4,6 +4,7 @@ import { hasPermission } from '../../../authorization'; import { LivechatRooms, Subscriptions } from '../../../models'; Meteor.publish('livechat:visitorHistory', function({ rid: roomId }) { + console.warn('The publication "livechat:visitorHistory" is deprecated and will be removed after version v3.0.0'); if (!this.userId) { return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:visitorHistory' })); } diff --git a/app/models/server/raw/LivechatRooms.js b/app/models/server/raw/LivechatRooms.js index 896ff0ed241..b29b854a650 100644 --- a/app/models/server/raw/LivechatRooms.js +++ b/app/models/server/raw/LivechatRooms.js @@ -94,4 +94,13 @@ export class LivechatRoomsRaw extends BaseRaw { } return this.col.aggregate(params).toArray(); } + + findByVisitorId(visitorId, options) { + const query = { + t: 'l', + 'v._id': visitorId, + }; + + return this.find(query, options); + } } diff --git a/tests/end-to-end/api/livechat/visitors.js b/tests/end-to-end/api/livechat/visitors.js index 1e3dc5e8997..e348f849e98 100644 --- a/tests/end-to-end/api/livechat/visitors.js +++ b/tests/end-to-end/api/livechat/visitors.js @@ -101,4 +101,49 @@ describe('LIVECHAT - visitors', function() { }); }); }); + + describe('livechat/visitors.chatHistory/room/room-id/visitor/visitor-id', () => { + it('should return an "unauthorized error" when the user does not have the necessary permission', (done) => { + updatePermission('view-l-room', []).then(() => { + request.get(api('livechat/visitors.chatHistory/room/room-id/visitor/visitor-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-authorized'); + }) + .end(done); + }); + }); + it('should return an "error" when the roomId param is invalid', (done) => { + updatePermission('view-l-room', ['admin']).then(() => { + request.get(api('livechat/visitors.chatHistory/room/room-id/visitor/visitor-id')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + }) + .end(done); + }); + }); + it('should return an array of chat history', (done) => { + updatePermission('view-l-room', ['admin']) + .then(() => { + request.get(api(`livechat/visitors.chatHistory/room/GENERAL/visitor/${ visitor._id }`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.history).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); + }); + }); + }); });