diff --git a/app/livechat/client/views/app/tabbar/visitorNavigation.js b/app/livechat/client/views/app/tabbar/visitorNavigation.js index 9c8360c4be20d..95fd911abc86f 100644 --- a/app/livechat/client/views/app/tabbar/visitorNavigation.js +++ b/app/livechat/client/views/app/tabbar/visitorNavigation.js @@ -1,26 +1,40 @@ -import { Mongo } from 'meteor/mongo'; +import { ReactiveVar } from 'meteor/reactive-var'; import { Template } from 'meteor/templating'; import moment from 'moment'; +import _ from 'underscore'; import { ChatRoom } from '../../../../../models'; import { t } from '../../../../../utils'; import './visitorNavigation.html'; +import { APIClient } from '../../../../../utils/client'; -const visitorNavigationHistory = new Mongo.Collection('visitor_navigation_history'); +const ITEMS_COUNT = 50; Template.visitorNavigation.helpers({ loadingNavigation() { - return !Template.instance().pageVisited.ready(); + return Template.instance().isLoading.get(); }, pages() { const room = ChatRoom.findOne({ _id: this.rid }, { fields: { 'v.token': 1 } }); if (room) { - return visitorNavigationHistory.find({ rid: room._id }, { sort: { ts: -1 } }); + return Template.instance().pages.get(); } }, + onTableScroll() { + const instance = Template.instance(); + return function(currentTarget) { + if ( + currentTarget.offsetHeight + currentTarget.scrollTop + >= currentTarget.scrollHeight - 100 + ) { + return instance.limit.set(instance.limit.get() + 50); + } + }; + }, + pageTitle() { return this.navigation.page.title || t('Empty_title'); }, @@ -28,12 +42,38 @@ Template.visitorNavigation.helpers({ accessDateTime() { return moment(this.ts).format('L LTS'); }, + }); -Template.visitorNavigation.onCreated(function() { +Template.visitorNavigation.events({ + 'scroll .visitor-scroll': _.throttle(function(e, instance) { + if (e.target.scrollTop >= (e.target.scrollHeight - e.target.clientHeight)) { + const pages = instance.pages.get(); + if (instance.total.get() <= pages.length) { + return; + } + return instance.offset.set(instance.offset.get() + ITEMS_COUNT); + } + }, 200), +}); + +Template.visitorNavigation.onCreated(async function() { const currentData = Template.currentData(); + this.isLoading = new ReactiveVar(true); + this.pages = new ReactiveVar([]); + this.offset = new ReactiveVar(0); + this.ready = new ReactiveVar(true); + this.total = new ReactiveVar(0); - if (currentData && currentData.rid) { - this.pageVisited = this.subscribe('livechat:visitorPageVisited', { rid: currentData.rid }); - } + this.autorun(async () => { + this.isLoading.set(true); + const offset = this.offset.get(); + if (currentData && currentData.rid) { + const { pages, total } = await APIClient.v1.get(`livechat/visitors.pagesVisited/${ currentData.rid }?count=${ ITEMS_COUNT }&offset=${ offset }`); + this.isLoading.set(false); + this.total.set(total); + this.pages.set(this.pages.get().concat(pages)); + } + this.isLoading.set(false); + }); }); diff --git a/app/livechat/imports/server/rest/visitors.js b/app/livechat/imports/server/rest/visitors.js index 6df472bb20c5a..28229b334eb3a 100644 --- a/app/livechat/imports/server/rest/visitors.js +++ b/app/livechat/imports/server/rest/visitors.js @@ -1,7 +1,8 @@ + import { check } from 'meteor/check'; import { API } from '../../../../api'; -import { findVisitorInfo } from '../../../server/api/lib/visitors'; +import { findVisitorInfo, findVisitedPages } from '../../../server/api/lib/visitors'; API.v1.addRoute('livechat/visitors.info', { authRequired: true }, { get() { @@ -14,3 +15,26 @@ API.v1.addRoute('livechat/visitors.info', { authRequired: true }, { return API.v1.success(visitor); }, }); + +API.v1.addRoute('livechat/visitors.pagesVisited/:roomId', { authRequired: true }, { + get() { + check(this.urlParams, { + roomId: String, + }); + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + + + const pages = Promise.await(findVisitedPages({ + userId: this.userId, + roomId: this.urlParams.roomId, + pagination: { + offset, + count, + sort, + }, + })); + + return API.v1.success(pages); + }, +}); diff --git a/app/livechat/server/api/lib/visitors.js b/app/livechat/server/api/lib/visitors.js index f72351f99934c..48e946d801eb3 100644 --- a/app/livechat/server/api/lib/visitors.js +++ b/app/livechat/server/api/lib/visitors.js @@ -1,5 +1,5 @@ import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { LivechatVisitors } from '../../../../models/server/raw'; +import { LivechatVisitors, Messages, LivechatRooms } from '../../../../models/server/raw'; export async function findVisitorInfo({ userId, visitorId }) { if (!await hasPermissionAsync(userId, 'view-l-room')) { @@ -15,3 +15,29 @@ export async function findVisitorInfo({ userId, visitorId }) { visitor, }; } + +export async function findVisitedPages({ userId, roomId, 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'); + } + const cursor = await Messages.findByRoomIdAndType(room._id, 'livechat_navigation_history', { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + }); + + const total = await cursor.count(); + + const pages = await cursor.toArray(); + + return { + pages, + count: pages.length, + offset, + total, + }; +} diff --git a/app/livechat/server/publications/visitorPageVisited.js b/app/livechat/server/publications/visitorPageVisited.js index e5a0283e7457d..fa2a09ce768d0 100644 --- a/app/livechat/server/publications/visitorPageVisited.js +++ b/app/livechat/server/publications/visitorPageVisited.js @@ -4,6 +4,7 @@ import { hasPermission } from '../../../authorization'; import { LivechatRooms, Messages } from '../../../models'; Meteor.publish('livechat:visitorPageVisited', function({ rid: roomId }) { + console.warn('The publication "livechat:visitorPageVisited" 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:visitorPageVisited' })); } diff --git a/app/models/server/raw/Messages.js b/app/models/server/raw/Messages.js index 1323e0f39c425..17c7ca07b81e9 100644 --- a/app/models/server/raw/Messages.js +++ b/app/models/server/raw/Messages.js @@ -21,6 +21,17 @@ export class MessagesRaw extends BaseRaw { return this.find(query, options); } + findByRoomIdAndType(roomId, type, options) { + const query = { + rid: roomId, + t: type, + }; + + if (options == null) { options = {}; } + + return this.find(query, options); + } + findSnippetedByRoom(roomId, options) { const query = { _hidden: { $ne: true }, diff --git a/tests/end-to-end/api/livechat/visitors.js b/tests/end-to-end/api/livechat/visitors.js index e0af003223d7b..1e3dc5e899753 100644 --- a/tests/end-to-end/api/livechat/visitors.js +++ b/tests/end-to-end/api/livechat/visitors.js @@ -56,4 +56,49 @@ describe('LIVECHAT - visitors', function() { .end(done); }); }); + + describe('livechat/visitors.pagesVisited', () => { + 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.pagesVisited/room-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 not provided', (done) => { + updatePermission('view-l-room', ['admin']).then(() => { + request.get(api('livechat/visitors.pagesVisited/room-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 pages', (done) => { + updatePermission('view-l-room', ['admin']) + .then(() => { + request.get(api('livechat/visitors.pagesVisited/GENERAL')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body.pages).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); + }); + }); + }); });