From 58bb172ad313f9df0c1025aaf6e510c6c3c84395 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 8 May 2017 11:57:41 -0300 Subject: [PATCH] finished rocket-chat-ui --- .../client/lib/RoomHistoryManager.coffee | 219 ------------- .../client/lib/RoomHistoryManager.js | 266 +++++++++++++++ .../client/lib/RoomManager.coffee | 253 --------------- .../rocketchat-ui/client/lib/RoomManager.js | 302 ++++++++++++++++++ .../rocketchat-ui/client/lib/readMessages.js | 2 +- .../rocketchat-ui/client/lib/rocket.coffee | 112 ------- packages/rocketchat-ui/client/lib/rocket.js | 121 +++++++ .../rocketchat-ui/client/views/app/room.js | 2 +- packages/rocketchat-ui/package.js | 8 +- 9 files changed, 695 insertions(+), 590 deletions(-) delete mode 100644 packages/rocketchat-ui/client/lib/RoomHistoryManager.coffee create mode 100644 packages/rocketchat-ui/client/lib/RoomHistoryManager.js delete mode 100644 packages/rocketchat-ui/client/lib/RoomManager.coffee create mode 100644 packages/rocketchat-ui/client/lib/RoomManager.js delete mode 100644 packages/rocketchat-ui/client/lib/rocket.coffee create mode 100644 packages/rocketchat-ui/client/lib/rocket.js diff --git a/packages/rocketchat-ui/client/lib/RoomHistoryManager.coffee b/packages/rocketchat-ui/client/lib/RoomHistoryManager.coffee deleted file mode 100644 index 7afe45fdd890..000000000000 --- a/packages/rocketchat-ui/client/lib/RoomHistoryManager.coffee +++ /dev/null @@ -1,219 +0,0 @@ -@RoomHistoryManager = new class - defaultLimit = 50 - - histories = {} - - getRoom = (rid) -> - if not histories[rid]? - histories[rid] = - hasMore: new ReactiveVar true - hasMoreNext: new ReactiveVar false - isLoading: new ReactiveVar false - unreadNotLoaded: new ReactiveVar 0 - firstUnread: new ReactiveVar - loaded: undefined - - return histories[rid] - - getMore = (rid, limit=defaultLimit) -> - room = getRoom rid - if room.hasMore.curValue isnt true - return - - room.isLoading.set true - - # ScrollListener.setLoader true - lastMessage = ChatMessage.findOne({rid: rid}, {sort: {ts: 1}}) - # lastMessage ?= ChatMessage.findOne({rid: rid}, {sort: {ts: 1}}) - - if lastMessage? - ts = lastMessage.ts - else - ts = undefined - - ls = undefined - typeName = undefined - - subscription = ChatSubscription.findOne rid: rid - if subscription? - ls = subscription.ls - typeName = subscription.t + subscription.name - else - curRoomDoc = ChatRoom.findOne(_id: rid) - typeName = curRoomDoc?.t + curRoomDoc?.name - - Meteor.call 'loadHistory', rid, ts, limit, ls, (err, result) -> - room.unreadNotLoaded.set result?.unreadNotLoaded - room.firstUnread.set result?.firstUnread - - wrapper = $('.messages-box .wrapper').get(0) - if wrapper? - previousHeight = wrapper.scrollHeight - - for item in result?.messages or [] when item.t isnt 'command' - item.roles = _.union(UserRoles.findOne(item.u?._id)?.roles, RoomRoles.findOne({rid: item.rid, 'u._id': item.u?._id})?.roles) - ChatMessage.upsert {_id: item._id}, item - - if wrapper? - heightDiff = wrapper.scrollHeight - previousHeight - wrapper.scrollTop += heightDiff - - Meteor.defer -> - readMessage.refreshUnreadMark(rid, true) - RoomManager.updateMentionsMarksOfRoom typeName - - room.isLoading.set false - room.loaded ?= 0 - if result?.messages?.length? - room.loaded += result.messages.length - if result?.messages?.length < limit - room.hasMore.set false - - getMoreNext = (rid, limit=defaultLimit) -> - room = getRoom rid - if room.hasMoreNext.curValue isnt true - return - - instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance() - instance.atBottom = false - - room.isLoading.set true - - lastMessage = ChatMessage.findOne({rid: rid}, {sort: {ts: -1}}) - - typeName = undefined - - subscription = ChatSubscription.findOne rid: rid - if subscription? - ls = subscription.ls - typeName = subscription.t + subscription.name - else - curRoomDoc = ChatRoom.findOne(_id: rid) - typeName = curRoomDoc?.t + curRoomDoc?.name - - ts = lastMessage.ts - - if ts - Meteor.call 'loadNextMessages', rid, ts, limit, (err, result) -> - for item in result?.messages or [] - if item.t isnt 'command' - item.roles = _.union(UserRoles.findOne(item.u?._id)?.roles, RoomRoles.findOne({rid: item.rid, 'u._id': item.u?._id})?.roles) - ChatMessage.upsert {_id: item._id}, item - - Meteor.defer -> - RoomManager.updateMentionsMarksOfRoom typeName - - room.isLoading.set false - room.loaded ?= 0 - if result.messages.length? - room.loaded += result.messages.length - if result.messages.length < limit - room.hasMoreNext.set false - - getSurroundingMessages = (message, limit=defaultLimit) -> - unless message?.rid - return - - instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance() - - if ChatMessage.findOne message._id - wrapper = $('.messages-box .wrapper') - msgElement = $("##{message._id}", wrapper) - pos = wrapper.scrollTop() + msgElement.offset().top - wrapper.height()/2 - wrapper.animate({ - scrollTop: pos - }, 500) - msgElement.addClass('highlight') - - setTimeout -> - messages = wrapper[0] - instance.atBottom = messages.scrollTop >= messages.scrollHeight - messages.clientHeight; - - setTimeout -> - msgElement.removeClass('highlight') - , 500 - else - room = getRoom message.rid - room.isLoading.set true - ChatMessage.remove { rid: message.rid } - - typeName = undefined - - subscription = ChatSubscription.findOne rid: message.rid - if subscription? - ls = subscription.ls - typeName = subscription.t + subscription.name - else - curRoomDoc = ChatRoom.findOne(_id: message.rid) - typeName = curRoomDoc?.t + curRoomDoc?.name - - Meteor.call 'loadSurroundingMessages', message, limit, (err, result) -> - for item in result?.messages or [] - if item.t isnt 'command' - item.roles = _.union(UserRoles.findOne(item.u?._id)?.roles, RoomRoles.findOne({rid: item.rid, 'u._id': item.u?._id})?.roles) - ChatMessage.upsert {_id: item._id}, item - - Meteor.defer -> - readMessage.refreshUnreadMark(message.rid, true) - RoomManager.updateMentionsMarksOfRoom typeName - wrapper = $('.messages-box .wrapper') - msgElement = $("##{message._id}", wrapper) - pos = wrapper.scrollTop() + msgElement.offset().top - wrapper.height()/2 - wrapper.animate({ - scrollTop: pos - }, 500) - - msgElement.addClass('highlight') - - setTimeout -> - room.isLoading.set false - messages = wrapper[0] - instance.atBottom = !result.moreAfter && messages.scrollTop >= messages.scrollHeight - messages.clientHeight; - , 500 - - setTimeout -> - msgElement.removeClass('highlight') - , 500 - room.loaded ?= 0 - if result.messages.length? - room.loaded += result.messages.length - room.hasMore.set result.moreBefore - room.hasMoreNext.set result.moreAfter - - hasMore = (rid) -> - room = getRoom rid - - return room.hasMore.get() - - hasMoreNext = (rid) -> - room = getRoom rid - return room.hasMoreNext.get() - - - getMoreIfIsEmpty = (rid) -> - room = getRoom rid - - if room.loaded is undefined - getMore rid - - - isLoading = (rid) -> - room = getRoom rid - return room.isLoading.get() - - clear = (rid) -> - ChatMessage.remove({ rid: rid }) - if histories[rid]? - histories[rid].hasMore.set true - histories[rid].isLoading.set false - histories[rid].loaded = undefined - - getRoom: getRoom - getMore: getMore - getMoreNext: getMoreNext - getMoreIfIsEmpty: getMoreIfIsEmpty - hasMore: hasMore - hasMoreNext: hasMoreNext - isLoading: isLoading - clear: clear - getSurroundingMessages: getSurroundingMessages diff --git a/packages/rocketchat-ui/client/lib/RoomHistoryManager.js b/packages/rocketchat-ui/client/lib/RoomHistoryManager.js new file mode 100644 index 000000000000..21b9bbc8306a --- /dev/null +++ b/packages/rocketchat-ui/client/lib/RoomHistoryManager.js @@ -0,0 +1,266 @@ +/* globals readMessage UserRoles RoomRoles*/ +export const RoomHistoryManager = new class { + constructor() { + this.defaultLimit = 50; + this.histories = {}; + } + getRoom(rid) { + if ((this.histories[rid] == null)) { + this.histories[rid] = { + hasMore: new ReactiveVar(true), + hasMoreNext: new ReactiveVar(false), + isLoading: new ReactiveVar(false), + unreadNotLoaded: new ReactiveVar(0), + firstUnread: new ReactiveVar, + loaded: undefined + }; + } + + return this.histories[rid]; + } + + getMore(rid, limit) { + let ts; + if (limit == null) { limit = this.defaultLimit; } + const room = this.getRoom(rid); + if (room.hasMore.curValue !== true) { + return; + } + + room.isLoading.set(true); + + // ScrollListener.setLoader true + const lastMessage = ChatMessage.findOne({rid}, {sort: {ts: 1}}); + // lastMessage ?= ChatMessage.findOne({rid: rid}, {sort: {ts: 1}}) + + if (lastMessage != null) { + ({ ts } = lastMessage); + } else { + ts = undefined; + } + + let ls = undefined; + let typeName = undefined; + + const subscription = ChatSubscription.findOne({rid}); + if (subscription != null) { + ({ ls } = subscription); + typeName = subscription.t + subscription.name; + } else { + const curRoomDoc = ChatRoom.findOne({_id: rid}); + typeName = (curRoomDoc != null ? curRoomDoc.t : undefined) + (curRoomDoc != null ? curRoomDoc.name : undefined); + } + + return Meteor.call('loadHistory', rid, ts, limit, ls, function(err, result) { + if (err) { + return; + } + let previousHeight; + room.unreadNotLoaded.set(result.unreadNotLoaded); + room.firstUnread.set(result.firstUnread); + + const wrapper = $('.messages-box .wrapper').get(0); + if (wrapper != null) { + previousHeight = wrapper.scrollHeight; + } + + result.messages.forEach(item => { + if (item.t !== 'command') { + const roles = [ + (item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {}, + (item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {} + ].map(e => e.roles); + item.roles = _.union.apply(_.union, roles); + ChatMessage.upsert({_id: item._id}, item); + } + }); + + if (wrapper) { + const heightDiff = wrapper.scrollHeight - previousHeight; + wrapper.scrollTop += heightDiff; + } + + Meteor.defer(() => { + readMessage.refreshUnreadMark(rid, true); + return RoomManager.updateMentionsMarksOfRoom(typeName); + }); + + room.isLoading.set(false); + if (room.loaded == null) { room.loaded = 0; } + room.loaded += result.messages.length; + if (result.messages.length < limit) { + return room.hasMore.set(false); + } + }); + } + + getMoreNext(rid, limit) { + if (limit == null) { limit = this.defaultLimit; } + const room = this.getRoom(rid); + if (room.hasMoreNext.curValue !== true) { + return; + } + + const instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance(); + instance.atBottom = false; + + room.isLoading.set(true); + + const lastMessage = ChatMessage.findOne({rid}, {sort: {ts: -1}}); + + let typeName = undefined; + + const subscription = ChatSubscription.findOne({rid}); + if (subscription != null) { + // const { ls } = subscription; + typeName = subscription.t + subscription.name; + } else { + const curRoomDoc = ChatRoom.findOne({_id: rid}); + typeName = (curRoomDoc != null ? curRoomDoc.t : undefined) + (curRoomDoc != null ? curRoomDoc.name : undefined); + } + + const { ts } = lastMessage; + + if (ts) { + return Meteor.call('loadNextMessages', rid, ts, limit, function(err, result) { + for (const item of Array.from((result != null ? result.messages : undefined) || [])) { + if (item.t !== 'command') { + const roles = [ + (item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {}, + (item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {} + ].map(e => e.roles); + item.roles = _.union.apply(_.union, roles); + ChatMessage.upsert({_id: item._id}, item); + } + } + + Meteor.defer(() => RoomManager.updateMentionsMarksOfRoom(typeName)); + + room.isLoading.set(false); + if (room.loaded == null) { room.loaded = 0; } + + room.loaded += result.messages.length; + if (result.messages.length < limit) { + room.hasMoreNext.set(false); + } + }); + } + } + + getSurroundingMessages(message, limit) { + if (limit == null) { limit = this.defaultLimit; } + if (!(message != null ? message.rid : undefined)) { + return; + } + + const instance = Blaze.getView($('.messages-box .wrapper')[0]).templateInstance(); + + if (ChatMessage.findOne(message._id)) { + const wrapper = $('.messages-box .wrapper'); + const msgElement = $(`#${ message._id }`, wrapper); + const pos = (wrapper.scrollTop() + msgElement.offset().top) - (wrapper.height()/2); + wrapper.animate({ + scrollTop: pos + }, 500); + msgElement.addClass('highlight'); + + setTimeout(function() { + const messages = wrapper[0]; + return instance.atBottom = messages.scrollTop >= (messages.scrollHeight - messages.clientHeight); + }); + + return setTimeout(() => msgElement.removeClass('highlight') + , 500); + } else { + const room = this.getRoom(message.rid); + room.isLoading.set(true); + ChatMessage.remove({ rid: message.rid }); + + let typeName = undefined; + + const subscription = ChatSubscription.findOne({rid: message.rid}); + if (subscription) { + // const { ls } = subscription; + typeName = subscription.t + subscription.name; + } else { + const curRoomDoc = ChatRoom.findOne({_id: message.rid}); + typeName = (curRoomDoc != null ? curRoomDoc.t : undefined) + (curRoomDoc != null ? curRoomDoc.name : undefined); + } + + return Meteor.call('loadSurroundingMessages', message, limit, function(err, result) { + for (const item of Array.from((result != null ? result.messages : undefined) || [])) { + if (item.t !== 'command') { + const roles = [ + (item.u && item.u._id && UserRoles.findOne(item.u._id, { fields: { roles: 1 }})) || {}, + (item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {} + ].map(e => e.roles); + item.roles = _.union.apply(_.union, roles); + ChatMessage.upsert({_id: item._id}, item); + } + } + + Meteor.defer(function() { + readMessage.refreshUnreadMark(message.rid, true); + RoomManager.updateMentionsMarksOfRoom(typeName); + const wrapper = $('.messages-box .wrapper'); + const msgElement = $(`#${ message._id }`, wrapper); + const pos = (wrapper.scrollTop() + msgElement.offset().top) - (wrapper.height()/2); + wrapper.animate({ + scrollTop: pos + }, 500); + + msgElement.addClass('highlight'); + + setTimeout(function() { + room.isLoading.set(false); + const messages = wrapper[0]; + instance.atBottom = !result.moreAfter && (messages.scrollTop >= (messages.scrollHeight - messages.clientHeight)); + return 500; + }); + + return setTimeout(() => msgElement.removeClass('highlight') + , 500); + }); + if (room.loaded == null) { room.loaded = 0; } + room.loaded += result.messages.length; + room.hasMore.set(result.moreBefore); + return room.hasMoreNext.set(result.moreAfter); + }); + } + } + + hasMore(rid) { + const room = this.getRoom(rid); + return room.hasMore.get(); + } + + hasMoreNext(rid) { + const room = this.getRoom(rid); + return room.hasMoreNext.get(); + } + + + getMoreIfIsEmpty(rid) { + const room = this.getRoom(rid); + + if (room.loaded === undefined) { + return this.getMore(rid); + } + } + + + isLoading(rid) { + const room = this.getRoom(rid); + return room.isLoading.get(); + } + + clear(rid) { + ChatMessage.remove({ rid }); + if (this.histories[rid] != null) { + this.histories[rid].hasMore.set(true); + this.histories[rid].isLoading.set(false); + return this.histories[rid].loaded = undefined; + } + } +}; +this.RoomHistoryManager = RoomHistoryManager; diff --git a/packages/rocketchat-ui/client/lib/RoomManager.coffee b/packages/rocketchat-ui/client/lib/RoomManager.coffee deleted file mode 100644 index 135f5121618c..000000000000 --- a/packages/rocketchat-ui/client/lib/RoomManager.coffee +++ /dev/null @@ -1,253 +0,0 @@ -loadMissedMessages = (rid) -> - lastMessage = ChatMessage.findOne({rid: rid}, {sort: {ts: -1}, limit: 1}) - if not lastMessage? - return - - Meteor.call 'loadMissedMessages', rid, lastMessage.ts, (err, result) -> - for item in result - RocketChat.promises.run('onClientMessageReceived', item).then (item) -> - item.roles = _.union(UserRoles.findOne(item.u?._id)?.roles, RoomRoles.findOne({rid: item.rid, 'u._id': item.u?._id})?.roles) - ChatMessage.upsert {_id: item._id}, item - -connectionWasOnline = true -Tracker.autorun -> - connected = Meteor.connection.status().connected - - if connected is true and connectionWasOnline is false and RoomManager.openedRooms? - for key, value of RoomManager.openedRooms - if value.rid? - loadMissedMessages(value.rid) - - connectionWasOnline = connected - -# Reload rooms after login -currentUsername = undefined -Tracker.autorun (c) -> - user = Meteor.user() - if currentUsername is undefined and user?.username? - currentUsername = user.username - RoomManager.closeAllRooms() - FlowRouter._current.route.callAction(FlowRouter._current) - -Meteor.startup -> - ChatMessage.find().observe - removed: (record) -> - if RoomManager.getOpenedRoomByRid(record.rid)? - recordBefore = ChatMessage.findOne {ts: {$lt: record.ts}}, {sort: {ts: -1}} - if recordBefore? - ChatMessage.update {_id: recordBefore._id}, {$set: {tick: new Date}} - - recordAfter = ChatMessage.findOne {ts: {$gt: record.ts}}, {sort: {ts: 1}} - if recordAfter? - ChatMessage.update {_id: recordAfter._id}, {$set: {tick: new Date}} - - -onDeleteMessageStream = (msg) -> - ChatMessage.remove _id: msg._id - - -Tracker.autorun -> - if Meteor.userId() - RocketChat.Notifications.onUser 'message', (msg) -> - msg.u = - username: 'rocket.cat' - msg.private = true - - ChatMessage.upsert { _id: msg._id }, msg - - -@RoomManager = new class - openedRooms = {} - msgStream = new Meteor.Streamer 'room-messages' - onlineUsers = new ReactiveVar {} - - Dep = new Tracker.Dependency - - close = (typeName) -> - if openedRooms[typeName] - if openedRooms[typeName].rid? - msgStream.removeAllListeners openedRooms[typeName].rid - RocketChat.Notifications.unRoom openedRooms[typeName].rid, 'deleteMessage', onDeleteMessageStream - - openedRooms[typeName].ready = false - openedRooms[typeName].active = false - if openedRooms[typeName].template? - Blaze.remove openedRooms[typeName].template - delete openedRooms[typeName].dom - delete openedRooms[typeName].template - - rid = openedRooms[typeName].rid - delete openedRooms[typeName] - - if rid? - RoomHistoryManager.clear rid - - - computation = Tracker.autorun -> - for typeName, record of openedRooms when record.active is true - do (typeName, record) -> - - user = Meteor.user() - - if record.ready is true - return - - ready = CachedChatRoom.ready.get() and CachedChatSubscription.ready.get() is true - - if ready is true - type = typeName.substr(0, 1) - name = typeName.substr(1) - - room = Tracker.nonreactive => - return RocketChat.roomTypes.findRoom(type, name, user) - - if not room? - record.ready = true - else - openedRooms[typeName].rid = room._id - - RoomHistoryManager.getMoreIfIsEmpty room._id - record.ready = RoomHistoryManager.isLoading(room._id) is false - Dep.changed() - - if openedRooms[typeName].streamActive isnt true - openedRooms[typeName].streamActive = true - msgStream.on openedRooms[typeName].rid, (msg) -> - - RocketChat.promises.run('onClientMessageReceived', msg).then (msg) -> - - # Should not send message to room if room has not loaded all the current messages - if RoomHistoryManager.hasMoreNext(openedRooms[typeName].rid) is false - - # Do not load command messages into channel - if msg.t isnt 'command' - msg.roles = _.union(UserRoles.findOne(msg.u?._id)?.roles, RoomRoles.findOne({rid: msg.rid, 'u._id': msg.u?._id})?.roles) - ChatMessage.upsert { _id: msg._id }, msg - - Meteor.defer -> - RoomManager.updateMentionsMarksOfRoom typeName - - RocketChat.callbacks.run 'streamMessage', msg - - window.fireGlobalEvent('new-message', msg); - - RocketChat.Notifications.onRoom openedRooms[typeName].rid, 'deleteMessage', onDeleteMessageStream - - Dep.changed() - - - closeOlderRooms = -> - maxRoomsOpen = 10 - if Object.keys(openedRooms).length <= maxRoomsOpen - return - - roomsToClose = _.sortBy(_.values(openedRooms), 'lastSeen').reverse().slice(maxRoomsOpen) - for roomToClose in roomsToClose - close roomToClose.typeName - - - closeAllRooms = -> - for key, openedRoom of openedRooms - close openedRoom.typeName - - - open = (typeName) -> - if not openedRooms[typeName]? - openedRooms[typeName] = - typeName: typeName - active: false - ready: false - unreadSince: new ReactiveVar undefined - - openedRooms[typeName].lastSeen = new Date - - if openedRooms[typeName].ready - closeOlderRooms() - - if CachedChatSubscription.ready.get() is true - - if openedRooms[typeName].active isnt true - openedRooms[typeName].active = true - - computation?.invalidate() - - return { - ready: -> - Dep.depend() - return openedRooms[typeName].ready - } - - getOpenedRoomByRid = (rid) -> - for typeName, openedRoom of openedRooms - if openedRoom.rid is rid - return openedRoom - - getDomOfRoom = (typeName, rid) -> - room = openedRooms[typeName] - if not room? - return - - if not room.dom? and rid? - room.dom = document.createElement 'div' - room.dom.classList.add 'room-container' - contentAsFunc = (content) -> - return -> content - - room.template = Blaze._TemplateWith { _id: rid }, contentAsFunc(Template.room) - Blaze.render room.template, room.dom #, nextNode, parentView - - return room.dom - - existsDomOfRoom = (typeName) -> - room = openedRooms[typeName] - return room?.dom? - - updateUserStatus = (user, status, utcOffset) -> - onlineUsersValue = onlineUsers.curValue - - if status is 'offline' - delete onlineUsersValue[user.username] - else - onlineUsersValue[user.username] = - _id: user._id - status: status - utcOffset: utcOffset - - onlineUsers.set onlineUsersValue - - updateMentionsMarksOfRoom = (typeName) -> - dom = getDomOfRoom typeName - if not dom? - return - - ticksBar = $(dom).find('.ticks-bar') - $(dom).find('.ticks-bar > .tick').remove() - - scrollTop = $(dom).find('.messages-box > .wrapper').scrollTop() - 50 - totalHeight = $(dom).find('.messages-box > .wrapper > ul').height() + 40 - - $('.messages-box .mention-link-me').each (index, item) -> - topOffset = $(item).offset().top + scrollTop - percent = 100 / totalHeight * topOffset - if $(item).hasClass('mention-link-all') - ticksBar.append('
') - else - ticksBar.append('
') - - open: open - close: close - closeAllRooms: closeAllRooms - getDomOfRoom: getDomOfRoom - existsDomOfRoom: existsDomOfRoom - msgStream: msgStream - openedRooms: openedRooms - updateUserStatus: updateUserStatus - onlineUsers: onlineUsers - updateMentionsMarksOfRoom: updateMentionsMarksOfRoom - getOpenedRoomByRid: getOpenedRoomByRid - computation: computation - - -RocketChat.callbacks.add 'afterLogoutCleanUp', -> - RoomManager.closeAllRooms() -, RocketChat.callbacks.priority.MEDIUM, 'roommanager-after-logout-cleanup' diff --git a/packages/rocketchat-ui/client/lib/RoomManager.js b/packages/rocketchat-ui/client/lib/RoomManager.js new file mode 100644 index 000000000000..85ac43ee63b0 --- /dev/null +++ b/packages/rocketchat-ui/client/lib/RoomManager.js @@ -0,0 +1,302 @@ +const RoomManager = new function() { + const openedRooms = {}; + const msgStream = new Meteor.Streamer('room-messages'); + const onlineUsers = new ReactiveVar({}); + const Dep = new Tracker.Dependency(); + const Cls = class { + static initClass() { + /* globals CachedChatRoom CachedChatSubscription */ + this.prototype.openedRooms = openedRooms; + this.prototype.onlineUsers = onlineUsers; + this.prototype.computation = Tracker.autorun(() => { + Object.keys(openedRooms).forEach(typeName => { + const record = openedRooms[typeName]; + if (record.active !== true || record.ready === true) { return; } + const ready = CachedChatRoom.ready.get() && CachedChatSubscription.ready.get() === true; + if (ready !== true) { return; } + const user = Meteor.user(); + + const type = typeName.substr(0, 1); + const name = typeName.substr(1); + + const room = Tracker.nonreactive(() => { + return RocketChat.roomTypes.findRoom(type, name, user); + }); + + if (room == null) { + record.ready = true; + } else { + openedRooms[typeName].rid = room._id; + + RoomHistoryManager.getMoreIfIsEmpty(room._id); + record.ready = RoomHistoryManager.isLoading(room._id) === false; + Dep.changed(); + + if (openedRooms[typeName].streamActive !== true) { + openedRooms[typeName].streamActive = true; + msgStream.on(openedRooms[typeName].rid, msg => + + RocketChat.promises.run('onClientMessageReceived', msg).then(function(msg) { + + // Should not send message to room if room has not loaded all the current messages + if (RoomHistoryManager.hasMoreNext(openedRooms[typeName].rid) === false) { + + // Do not load command messages into channel + if (msg.t !== 'command') { + const roles = [ + (msg.u && msg.u._id && UserRoles.findOne(msg.u._id, { fields: { roles: 1 }})) || {}, + (msg.u && msg.u._id && RoomRoles.findOne({rid: msg.rid, 'u._id': msg.u._id})) || {} + ].map(e => e.roles); + msg.roles = _.union.apply(_.union, roles); + ChatMessage.upsert({ _id: msg._id }, msg); + } + + Meteor.defer(() => RoomManager.updateMentionsMarksOfRoom(typeName)); + + RocketChat.callbacks.run('streamMessage', msg); + + return window.fireGlobalEvent('new-message', msg); + } + }) + ); + + RocketChat.Notifications.onRoom(openedRooms[typeName].rid, 'deleteMessage', onDeleteMessageStream); // eslint-disable-line no-use-before-define + } + } + return Dep.changed(); + }); + }); + } + + getOpenedRoomByRid(rid) { + return Object.keys(openedRooms).map(typeName => openedRooms[typeName]).find(openedRoom => openedRoom.rid === rid); + } + + getDomOfRoom(typeName, rid) { + const room = openedRooms[typeName]; + if ((room == null)) { + return; + } + + if ((room.dom == null) && (rid != null)) { + room.dom = document.createElement('div'); + room.dom.classList.add('room-container'); + const contentAsFunc = content => () => content; + + room.template = Blaze._TemplateWith({ _id: rid }, contentAsFunc(Template.room)); + Blaze.render(room.template, room.dom); //, nextNode, parentView + } + + return room.dom; + } + + close(typeName) { + if (openedRooms[typeName]) { + if (openedRooms[typeName].rid != null) { + msgStream.removeAllListeners(openedRooms[typeName].rid); + RocketChat.Notifications.unRoom(openedRooms[typeName].rid, 'deleteMessage', onDeleteMessageStream); // eslint-disable-line no-use-before-define + } + + openedRooms[typeName].ready = false; + openedRooms[typeName].active = false; + if (openedRooms[typeName].template != null) { + Blaze.remove(openedRooms[typeName].template); + } + delete openedRooms[typeName].dom; + delete openedRooms[typeName].template; + + const { rid } = openedRooms[typeName]; + delete openedRooms[typeName]; + + if (rid != null) { + return RoomHistoryManager.clear(rid); + } + } + } + + + closeOlderRooms() { + const maxRoomsOpen = 10; + if (Object.keys(openedRooms).length <= maxRoomsOpen) { + return; + } + + const roomsToClose = _.sortBy(_.values(openedRooms), 'lastSeen').reverse().slice(maxRoomsOpen); + return Array.from(roomsToClose).map((roomToClose) => + this.close(roomToClose.typeName)); + } + + + closeAllRooms() { + Object.keys(openedRooms).forEach(key => { + const openedRoom = openedRooms[key]; + this.close(openedRoom.typeName); + }); + } + + + open(typeName) { + if ((openedRooms[typeName] == null)) { + openedRooms[typeName] = { + typeName, + active: false, + ready: false, + unreadSince: new ReactiveVar(undefined) + }; + } + + openedRooms[typeName].lastSeen = new Date; + + if (openedRooms[typeName].ready) { + this.closeOlderRooms(); + } + + if (CachedChatSubscription.ready.get() === true) { + + if (openedRooms[typeName].active !== true) { + openedRooms[typeName].active = true; + if (this.computation) { + this.computation.invalidate(); + } + } + } + + return { + ready() { + Dep.depend(); + return openedRooms[typeName].ready; + } + }; + } + + existsDomOfRoom(typeName) { + const room = openedRooms[typeName]; + return ((room != null ? room.dom : undefined) != null); + } + + updateUserStatus(user, status, utcOffset) { + const onlineUsersValue = onlineUsers.curValue; + + if (status === 'offline') { + delete onlineUsersValue[user.username]; + } else { + onlineUsersValue[user.username] = { + _id: user._id, + status, + utcOffset + }; + } + + return onlineUsers.set(onlineUsersValue); + } + + updateMentionsMarksOfRoom(typeName) { + const dom = this.getDomOfRoom(typeName); + if ((dom == null)) { + return; + } + + const ticksBar = $(dom).find('.ticks-bar'); + $(dom).find('.ticks-bar > .tick').remove(); + + const scrollTop = $(dom).find('.messages-box > .wrapper').scrollTop() - 50; + const totalHeight = $(dom).find('.messages-box > .wrapper > ul').height() + 40; + + return $('.messages-box .mention-link-me').each(function(index, item) { + const topOffset = $(item).offset().top + scrollTop; + const percent = (100 / totalHeight) * topOffset; + if ($(item).hasClass('mention-link-all')) { + return ticksBar.append(`
`); + } else { + return ticksBar.append(`
`); + } + }); + } + }; + Cls.initClass(); + return new Cls; +}; + +const loadMissedMessages = function(rid) { + const lastMessage = ChatMessage.findOne({rid}, {sort: {ts: -1}, limit: 1}); + if (lastMessage == null) { + return; + } + + return Meteor.call('loadMissedMessages', rid, lastMessage.ts, (err, result) => + Array.from(result).map((item) => + RocketChat.promises.run('onClientMessageReceived', item).then(function(item) { + /* globals UserRoles RoomRoles*/ + const roles = [ + (item.u && item.u._id && UserRoles.findOne(item.u._id)) || {}, + (item.u && item.u._id && RoomRoles.findOne({rid: item.rid, 'u._id': item.u._id})) || {} + ].map(({roles}) => roles); + item.roles = _.union.apply(_, roles); + return ChatMessage.upsert({_id: item._id}, item); + })) +); +}; + +let connectionWasOnline = true; +Tracker.autorun(function() { + const { connected } = Meteor.connection.status(); + + if (connected === true && connectionWasOnline === false && RoomManager.openedRooms != null) { + Object.keys(RoomManager.openedRooms).forEach(key => { + const value = RoomManager.openedRooms[key]; + if (value.rid != null) { + loadMissedMessages(value.rid); + } + }); + } + return connectionWasOnline = connected; +}); + +// Reload rooms after login +let currentUsername = undefined; +Tracker.autorun(() => { + const user = Meteor.user(); + if ((currentUsername === undefined) && ((user != null ? user.username : undefined) != null)) { + currentUsername = user.username; + RoomManager.closeAllRooms(); + return FlowRouter._current.route.callAction(FlowRouter._current); + } +}); + +Meteor.startup(() => + ChatMessage.find().observe({ + removed(record) { + if (RoomManager.getOpenedRoomByRid(record.rid) != null) { + const recordBefore = ChatMessage.findOne({ts: {$lt: record.ts}}, {sort: {ts: -1}}); + if (recordBefore != null) { + ChatMessage.update({_id: recordBefore._id}, {$set: {tick: new Date}}); + } + + const recordAfter = ChatMessage.findOne({ts: {$gt: record.ts}}, {sort: {ts: 1}}); + if (recordAfter != null) { + return ChatMessage.update({_id: recordAfter._id}, {$set: {tick: new Date}}); + } + } + } + }) +); + + +const onDeleteMessageStream = msg => ChatMessage.remove({_id: msg._id}); + + +Tracker.autorun(function() { + if (Meteor.userId()) { + return RocketChat.Notifications.onUser('message', function(msg) { + msg.u = + {username: 'rocket.cat'}; + msg.private = true; + + return ChatMessage.upsert({ _id: msg._id }, msg); + }); + } +}); + +export { RoomManager }; +this.RoomManager = RoomManager; +RocketChat.callbacks.add('afterLogoutCleanUp', () => RoomManager.closeAllRooms(), RocketChat.callbacks.priority.MEDIUM, 'roommanager-after-logout-cleanup'); diff --git a/packages/rocketchat-ui/client/lib/readMessages.js b/packages/rocketchat-ui/client/lib/readMessages.js index c5afd8cc0885..4bd003eb658e 100644 --- a/packages/rocketchat-ui/client/lib/readMessages.js +++ b/packages/rocketchat-ui/client/lib/readMessages.js @@ -11,7 +11,7 @@ // window.addEventListener 'focus', -> // readMessage.refreshUnreadMark(undefined, true) -const readMessage = new class { +const readMessage = new class { constructor() { this.debug = false; this.callbacks = []; diff --git a/packages/rocketchat-ui/client/lib/rocket.coffee b/packages/rocketchat-ui/client/lib/rocket.coffee deleted file mode 100644 index 2ff6190ef8fb..000000000000 --- a/packages/rocketchat-ui/client/lib/rocket.coffee +++ /dev/null @@ -1,112 +0,0 @@ -RocketChat.Login = (-> - onClick = (el) -> - $el = $(el) - if $el.length - $el.addClass "active" - $el.find("input").focus() - onBlur = (input) -> - $input = $(input) - if $input.length - if input.value == "" - $input.parents(".input-text").removeClass "active" - check = (form) -> - $form = $(form) - if $form.length - inputs = $form.find("input") - inputs.each -> - if @.value != "" - console.log @.value - $(@).parents(".input-text").addClass "active" - check: check - onClick: onClick - onBlur: onBlur - )() - -RocketChat.Button = (-> - time = undefined - loading = (el) -> - $el = $(el) - next = el.attr("data-loading-text") - html = el.find("span").html() - el.addClass("-progress").attr("data-def-text",html).find("span").html(next) - time = setTimeout -> - el.addClass("going") - , 1 - done = (el) -> - $el = $(el) - el.addClass("done") - reset = (el) -> - clearTimeout(time) if time - $el = $(el) - html= $el.attr("data-def-text") - $el.find("span").html(html) if html - $el.removeClass("-progress going done") - done: done - loading: loading - reset: reset - )() - -RocketChat.animationSupport = -> - animeEnd = - WebkitAnimation: "webkitAnimationEnd" - OAnimation: "oAnimationEnd" - msAnimation: "MSAnimationEnd" - animation: "animationend" - - transEndEventNames = - WebkitTransition: "webkitTransitionEnd" - MozTransition: "transitionend" - OTransition: "oTransitionEnd otransitionend" - msTransition: "MSTransitionEnd" - transition: "transitionend" - prefixB = transEndEventNames[Modernizr.prefixed("transition")] - prefixA = animeEnd[Modernizr.prefixed("animation")] - support = Modernizr.cssanimations - support: support - animation: prefixA - transition: prefixB - -RocketChat.animeBack = (el, callback, type) -> - el = $(el) - if not el.length > 0 - callback el if callback - return - s = animationSupport() - p = ((if type then s.animation else s.transition)) - el.one p, (e) -> - - #el.off(p); - callback e - return - - return - -RocketChat.preLoadImgs = (urls, callback) -> - L_ = (x) -> - if x.width > 0 - $(x).addClass("loaded").removeClass "loading" - loaded = $(".loaded", preLoader) - if loaded.length is urls.length and not ended - ended = 1 - imgs = preLoader.children() - callback imgs - preLoader.remove() - return - im = new Array() - preLoader = $("
").attr(id: "perverter-preloader") - loaded = undefined - ended = undefined - i = 0 - - while i < urls.length - im[i] = new Image() - im[i].onload = -> - L_ this - return - - $(im[i]).appendTo(preLoader).addClass "loading" - im[i].src = urls[i] - L_ im[i] if im[i].width > 0 - i++ - - return diff --git a/packages/rocketchat-ui/client/lib/rocket.js b/packages/rocketchat-ui/client/lib/rocket.js new file mode 100644 index 000000000000..5970e6c273eb --- /dev/null +++ b/packages/rocketchat-ui/client/lib/rocket.js @@ -0,0 +1,121 @@ +/* globals Modernizr */ +RocketChat.Login = (function() { + function onClick(el) { + const $el = $(el); + if ($el.length) { + $el.addClass('active'); + return $el.find('input').focus(); + } + } + function onBlur(input) { + const $input = $(input); + if ($input.length) { + if (input.value === '') { + return $input.parents('.input-text').removeClass('active'); + } + } + } + function check(form) { + const $form = $(form); + if ($form.length) { + const inputs = $form.find('input'); + return inputs.each(function() { + if (this.value !== '') { + console.log(this.value); + return $(this).parents('.input-text').addClass('active'); + } + }); + } + } + return { check, onClick, onBlur }; +}()); + +RocketChat.Button = (function() { + let time = undefined; + const loading = function(el) { + const next = el.attr('data-loading-text'); + const html = el.find('span').html(); + el.addClass('-progress').attr('data-def-text', html).find('span').html(next); + return time = setTimeout(() => el.addClass('going'), 1); + }; + const done = function(el) { + return el.addClass('done'); + }; + const reset = function(el) { + if (time) { clearTimeout(time); } + const $el = $(el); + const html= $el.attr('data-def-text'); + if (html) { $el.find('span').html(html); } + return $el.removeClass('-progress going done'); + }; + return { done, loading, reset }; +}()); + +RocketChat.animationSupport = function() { + const animeEnd = { + WebkitAnimation: 'webkitAnimationEnd', + OAnimation: 'oAnimationEnd', + msAnimation: 'MSAnimationEnd', + animation: 'animationend' + }; + + const transEndEventNames = { + WebkitTransition: 'webkitTransitionEnd', + MozTransition: 'transitionend', + OTransition: 'oTransitionEnd otransitionend', + msTransition: 'MSTransitionEnd', + transition: 'transitionend' + }; + const prefixB = transEndEventNames[Modernizr.prefixed('transition')]; + const prefixA = animeEnd[Modernizr.prefixed('animation')]; + const support = Modernizr.cssanimations; + return { + support, + animation: prefixA, + transition: prefixB + }; +}; + +RocketChat.animeBack = function(e, callback, type) { + const el = $(e); + if (!el.length > 0) { + if (callback) { callback(el); } + return; + } + const s = RocketChat.animationSupport(); + const p = ((type ? s.animation : s.transition)); + el.one(p, function(e) { + + //el.off(p); + callback(e); + }); + +}; + +RocketChat.preLoadImgs = function(urls, callback) { + const preLoader = $('
').attr({id: 'perverter-preloader'}); + let ended = undefined; + const l_ = function(x) { + if (x.width > 0) { + $(x).addClass('loaded').removeClass('loading'); + const loaded = $('.loaded', preLoader); + if ((loaded.length === urls.length) && !ended) { + ended = 1; + const imgs = preLoader.children(); + callback(imgs); + preLoader.remove(); + } + } + }; + return urls.map(url => { + const im = new Image(); + im.onload = function() { + l_(this); + }; + $(im).appendTo(preLoader).addClass('loading'); + im.src = url; + if (im.width > 0) { l_(im); } + return im; + }); + +}; diff --git a/packages/rocketchat-ui/client/views/app/room.js b/packages/rocketchat-ui/client/views/app/room.js index f6e21d90158b..875b28bbfb7c 100644 --- a/packages/rocketchat-ui/client/views/app/room.js +++ b/packages/rocketchat-ui/client/views/app/room.js @@ -220,7 +220,7 @@ Template.room.helpers({ canPreview() { const room = Session.get(`roomData${ this._id }`); - if (room.t !== 'c') { + if (room && room.t !== 'c') { return true; } diff --git a/packages/rocketchat-ui/package.js b/packages/rocketchat-ui/package.js index f9ad4248d588..44831edb8326 100644 --- a/packages/rocketchat-ui/package.js +++ b/packages/rocketchat-ui/package.js @@ -42,15 +42,15 @@ Package.onUse(function(api) { api.addFiles('client/lib/fireEvent.js', 'client'); api.addFiles('client/lib/iframeCommands.js', 'client'); api.addFiles('client/lib/menu.js', 'client'); - api.addFiles('client/lib/modal.coffee', 'client'); + api.addFiles('client/lib/modal.js', 'client'); api.addFiles('client/lib/Modernizr.js', 'client'); api.addFiles('client/lib/msgTyping.js', 'client'); api.addFiles('client/lib/notification.js', 'client'); api.addFiles('client/lib/parentTemplate.js', 'client'); api.addFiles('client/lib/readMessages.js', 'client'); - api.addFiles('client/lib/rocket.coffee', 'client'); - api.addFiles('client/lib/RoomHistoryManager.coffee', 'client'); - api.addFiles('client/lib/RoomManager.coffee', 'client'); + api.addFiles('client/lib/rocket.js', 'client'); + api.addFiles('client/lib/RoomHistoryManager.js', 'client'); + api.addFiles('client/lib/RoomManager.js', 'client'); api.addFiles('client/lib/sideNav.js', 'client'); api.addFiles('client/lib/tapi18n.js', 'client'); api.addFiles('client/lib/textarea-autogrow.js', 'client');