diff --git a/services/web/app/coffee/Features/Chat/ChatController.coffee b/services/web/app/coffee/Features/Chat/ChatController.coffee index f8a6e9065b..a38c26a9fc 100644 --- a/services/web/app/coffee/Features/Chat/ChatController.coffee +++ b/services/web/app/coffee/Features/Chat/ChatController.coffee @@ -18,7 +18,7 @@ module.exports = getMessages: (req, res)-> project_id = req.params.Project_id - query = req.body + query = req.query logger.log project_id:project_id, query:query, "getting messages" ChatHandler.getMessages project_id, query, (err, messages)-> if err? diff --git a/services/web/app/views/layout/navbar.jade b/services/web/app/views/layout/navbar.jade index 9ec79b03ea..38bd507d64 100644 --- a/services/web/app/views/layout/navbar.jade +++ b/services/web/app/views/layout/navbar.jade @@ -31,33 +31,4 @@ nav.navbar.navbar-default else | !{item.text} - //- li.dropdown - //- a.dropdown-toggle(href='#',data-toggle="dropdown") - //- | Help - //- b.caret - //- ul.dropdown-menu - //- li - //- a(href='/learn') Documentation - //- -if(typeof(tenderUrl) != "undefined") - //- li - //- a.js-tender-widget(href='#') Contact Support - - //- -if(session && session.user) - //- li.dropdown - //- a.dropdown-toggle(href="#",data-toggle="dropdown") - //- | Account - //- b.caret - //- ul.dropdown-menu - //- li - //- a(href='/user/settings').userSettingsLink Account Settings - //- - if (settings.enableSubscriptions) - //- li - //- a(href='/user/subscription').subscriptionLink Subscription - //- li - //- a(href='/logout').logoutLink Logout - //- -else - //- li - //- a(href='/register') Register - //- li - //- a#loginLink(href='/login'+getRedirAsQueryString()) Login diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade index 1ba1a3e367..d1e59753da 100644 --- a/services/web/app/views/project/editor.jade +++ b/services/web/app/views/project/editor.jade @@ -62,7 +62,7 @@ block content include ./editor/publish-template .ui-layout-east - //- include ./editor/chat + include ./editor/chat script(type="text/ng-template", id="genericMessageModalTemplate") .modal-header @@ -120,4 +120,5 @@ block content data-ace-base=jsPath+'ace', src=jsPath+'libs/require.js?fingerprint='+fingerprint(jsPath + 'libs/require.js') ) + diff --git a/services/web/app/views/project/editor/chat.jade b/services/web/app/views/project/editor/chat.jade index 244f15a8f5..754d044392 100644 --- a/services/web/app/views/project/editor/chat.jade +++ b/services/web/app/views/project/editor/chat.jade @@ -1,24 +1,25 @@ aside.chat( ng-controller="ChatController" - infinite-scroll="loadMore()" - infinite-scroll-disabled="chat.loading || chat.atEnd" - infinite-scroll-initialize="ui.chatOpen" ) .messages( - infinite-scroll="loadMore()" + infinite-scroll="loadMoreMessages()" infinite-scroll-disabled="chat.loading || chat.atEnd" infinite-scroll-initialize="ui.chatOpen" - scroll-glue + infinite-scroll-upwards="true" + update-scroll-bottom-on="updateScrollPosition" ) .infinite-scroll-inner + .loading(ng-show="chat.loading") + i.fa.fa-fw.fa-spin.fa-refresh + | Loading... ul.list-unstyled li.message( - ng-repeat="message in chat.groupedMessages | orderBy:'timestamp':false" + ng-repeat="message in chat.messages" ng-controller="ChatMessageController" ng-class="{'self': message.user.id == user.id }" ) - div.date(ng-if="$index == 0 || (message.timestamp - chat.groupedMessages[$index - 1].timestamp) > 5 * 60 * 1000") - {{ message.timestamp | formatDate:'h:MMa' }} {{ message.timestamp | relativeDate }} + div.date(ng-if="$index == 0 || (message.timestamp - chat.messages[$index - 1].timestamp) > 5 * 60 * 1000") + {{ message.timestamp | formatDate:'h:mm a' }} {{ message.timestamp | relativeDate }} span.avatar img(ng-src="{{message.user.gravatar_url}}") div.message-wrapper @@ -30,7 +31,7 @@ aside.chat( }" ) .arrow(ng-style="{'border-color': 'hsl({{ hue(message.user) }}, 60%, 80%)'}") - p(ng-repeat="content in message.contents") {{ content }} + p(ng-repeat="content in message.contents track by $index") {{ content }} .new-message textarea(placeholder="Your message...", on-enter="sendMessage()", ng-model="newMessageContent") diff --git a/services/web/app/views/project/editor/header.jade b/services/web/app/views/project/editor/header.jade index a123d9d14f..833ce393f5 100644 --- a/services/web/app/views/project/editor/header.jade +++ b/services/web/app/views/project/editor/header.jade @@ -6,6 +6,12 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading") tooltip-placement="bottom" ) i.fa.fa-fw.fa-bars + a( + href="/project" + tooltip="Back to projects", + tooltip-placement="bottom" + ) + i.fa.fa-fw.fa-level-up .toolbar-center.project-name(ng-controller="ProjectNameController") span.name( @@ -50,12 +56,12 @@ header.toolbar.toolbar-header(ng-cloak, ng-hide="state.loading") tooltip-placement="bottom" ) i.fa.fa-fw.fa-history - //- a.btn.btn-full-height( - //- href, - //- tooltip="Chat", - //- tooltip-placement="bottom", - //- ng-class="{ active: ui.chatOpen }", - //- ng-click="toggleChat()", - //- ng-controller="ChatButtonController" - //- ) - //- i.fa.fa-fw.fa-comment \ No newline at end of file + a.btn.btn-full-height( + href, + tooltip="Chat", + tooltip-placement="bottom", + ng-class="{ active: ui.chatOpen }", + ng-click="toggleChat()", + ng-controller="ChatButtonController" + ) + i.fa.fa-fw.fa-comment \ No newline at end of file diff --git a/services/web/app/views/project/editor/left-menu.jade b/services/web/app/views/project/editor/left-menu.jade index c24a2622bc..54fd47621f 100644 --- a/services/web/app/views/project/editor/left-menu.jade +++ b/services/web/app/views/project/editor/left-menu.jade @@ -2,14 +2,6 @@ aside#left-menu.full-size( ng-class="{ 'shown': ui.leftMenuShown }" ng-cloak ) - ul.list-unstyled.nav - li - a( - href="/project" - ) - i.fa.fa-list-alt.fa-fw - |   View All Projects - h4 Download ul.list-unstyled.nav.nav-downloads diff --git a/services/web/public/coffee/directives/scroll.coffee b/services/web/public/coffee/directives/scroll.coffee index d3ba6ebe0c..f39f45ff4f 100644 --- a/services/web/public/coffee/directives/scroll.coffee +++ b/services/web/public/coffee/directives/scroll.coffee @@ -1,34 +1,36 @@ define [ "base" ], (App) -> - - fakeNgModel = (initValue) -> - $setViewValue: (value) -> - @$viewValue = value - return - - $viewValue: initValue - - App.directive "scrollGlue", -> + App.directive "updateScrollBottomOn", ($timeout) -> return { - - priority: 1 - require: ["?ngModel"] restrict: "A" - - link: (scope, $el, attrs, ctrls) -> - scrollToBottom = -> - el.scrollTop = el.scrollHeight - - shouldActivateAutoScroll = -> - el.scrollTop + el.clientHeight + 10 >= el.scrollHeight - - el = $el[0] - ngModel = ctrls[0] or fakeNgModel(true) - scope.$watch -> - scrollToBottom() if ngModel.$viewValue - - $el.bind "scroll", -> - activate = shouldActivateAutoScroll() - scope.$apply ngModel.$setViewValue.bind(ngModel, activate) if activate isnt ngModel.$viewValue + link: (scope, element, attrs, ctrls) -> + # We keep the offset from the bottom fixed whenever the event fires + # + # ^ | ^ + # | | | scrollTop + # | | v + # | |----------- + # | | ^ + # | | | + # | | | clientHeight (viewable area) + # | | | + # | | | + # | | v + # | |----------- + # | | ^ + # | | | scrollBottom + # v | v + # \ + # scrollHeight + + scrollBottom = 0 + element.on "scroll", (e) -> + scrollBottom = element[0].scrollHeight - element[0].scrollTop - element[0].clientHeight + + scope.$on attrs.updateScrollBottomOn, () -> + $timeout () -> + element.scrollTop(element[0].scrollHeight - element[0].clientHeight - scrollBottom) + , 0 } + diff --git a/services/web/public/coffee/ide/chat/controllers/ChatController.coffee b/services/web/public/coffee/ide/chat/controllers/ChatController.coffee index ff40b84c6d..8affd3d26a 100644 --- a/services/web/public/coffee/ide/chat/controllers/ChatController.coffee +++ b/services/web/public/coffee/ide/chat/controllers/ChatController.coffee @@ -1,48 +1,25 @@ define [ "base" + "ide/chat/services/chatMessages" ], (App) -> - App.controller "ChatController", ["$scope", "$http", "ide", "$location", ($scope, $http, @ide, $location) -> - MESSAGES_URL = "/project/#{$scope.project_id}/messages" - CONNECTED_USER_URL = "/project/#{$scope.project_id}/connected_users" - - $scope.$on "project:joined", => - @ide.socket.on "new-chat-message", (message) => - $scope.chat.messages.push(message) - - $http.get(MESSAGES_URL).success (data, status, headers, config)-> - $scope.chat.messages = data - - $http.get(CONNECTED_USER_URL).success (data)-> - console.log data - - $scope.$watchCollection "chat.messages", (messages) -> + App.controller "ChatController", ($scope, chatMessages, @ide, $location) -> + $scope.chat = chatMessages.state + + $scope.$watch "chat.messages", (messages) -> if messages? - console.log "grouping messages" - $scope.chat.groupedMessages = groupMessages(messages) - + $scope.$emit "updateScrollPosition" + , true # Deep watch + + $scope.$on "layout:chat:resize", () -> + $scope.$emit "updateScrollPosition" + $scope.sendMessage = -> - body = - content:$scope.newMessageContent - _csrf : window.csrfToken - $http.post(MESSAGES_URL, body).success (data, status, headers, config)-> - $scope.newMessageContent = "" - - TIMESTAMP_GROUP_SIZE = 5 * 60 * 1000 # 5 minutes - groupMessages = (messages) -> - previousMessage = null - groupedMessages = [] - for message in messages - shouldGroup = previousMessage? and - previousMessage.user == message.user and - message.timestamp - previousMessage.timestamp < TIMESTAMP_GROUP_SIZE - if shouldGroup - previousMessage.timestamp = message.timestamp - previousMessage.contents.push message.content - else - groupedMessages.push(previousMessage = { - user: message.user - timestamp: message.timestamp - contents: [message.content] - }) - return groupedMessages - ] \ No newline at end of file + chatMessages + .sendMessage $scope.newMessageContent + .then () -> + $scope.newMessageContent = "" + + $scope.loadMoreMessages = -> + chatMessages.loadMoreMessages() + + \ No newline at end of file diff --git a/services/web/public/coffee/ide/chat/services/chatMessages.coffee b/services/web/public/coffee/ide/chat/services/chatMessages.coffee new file mode 100644 index 0000000000..74d074e3c2 --- /dev/null +++ b/services/web/public/coffee/ide/chat/services/chatMessages.coffee @@ -0,0 +1,80 @@ +define [ + "base" +], (App) -> + App.factory "chatMessages", ($http, ide) -> + MESSAGES_URL = "/project/#{ide.project_id}/messages" + MESSAGE_LIMIT = 50 + CONNECTED_USER_URL = "/project/#{ide.project_id}/connected_users" + + chat = { + state: + messages: [] + loading: false + atEnd: false + nextBeforeTimestamp: null + } + + ide.socket.on "new-chat-message", (message) => + appendMessage(message) + + chat.loadMoreMessages = () -> + return if chat.state.atEnd + + url = MESSAGES_URL + "?limit=#{MESSAGE_LIMIT}" + if chat.state.nextBeforeTimestamp? + url += "&before=#{chat.state.nextBeforeTimestamp}" + + chat.state.loading = true + return $http + .get(url) + .success (messages = [])-> + chat.state.loading = false + if messages.length < MESSAGE_LIMIT + chat.state.atEnd = true + messages.reverse() + prependMessages(messages) + chat.state.nextBeforeTimestamp = chat.state.messages[0]?.timestamp + + TIMESTAMP_GROUP_SIZE = 5 * 60 * 1000 # 5 minutes + + prependMessage = (message) -> + firstMessage = chat.state.messages[0] + shouldGroup = firstMessage? and + firstMessage.user.id == message.user.id and + firstMessage.timestamp - message.timestamp < TIMESTAMP_GROUP_SIZE + if shouldGroup + firstMessage.timestamp = message.timestamp + firstMessage.contents.unshift message.content + else + chat.state.messages.unshift({ + user: message.user + timestamp: message.timestamp + contents: [message.content] + }) + + prependMessages = (messages) -> + for message in messages.slice(0).reverse() + prependMessage(message) + + appendMessage = (message) -> + lastMessage = chat.state.messages[chat.state.messages.length - 1] + shouldGroup = lastMessage? and + lastMessage.user.id == message.user.id and + message.timestamp - lastMessage.timestamp < TIMESTAMP_GROUP_SIZE + if shouldGroup + lastMessage.timestamp = message.timestamp + lastMessage.contents.push message.content + else + chat.state.messages.push({ + user: message.user + timestamp: message.timestamp + contents: [message.content] + }) + + chat.sendMessage = (message) -> + body = + content: message + _csrf : window.csrfToken + return $http.post(MESSAGES_URL, body) + + return chat \ No newline at end of file diff --git a/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee b/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee index d902f2de54..7817cb8725 100644 --- a/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee +++ b/services/web/public/coffee/ide/track-changes/TrackChangesManager.coffee @@ -63,7 +63,7 @@ define [ @$scope.trackChanges.updates[indexOfLastUpdateNotByMe].selectedFrom = true - BATCH_SIZE: 4 + BATCH_SIZE: 10 fetchNextBatchOfUpdates: () -> url = "/project/#{@ide.project_id}/updates?min_count=#{@BATCH_SIZE}" if @$scope.trackChanges.nextBeforeTimestamp? diff --git a/services/web/public/coffee/ide/track-changes/directives/infiniteScroll.coffee b/services/web/public/coffee/ide/track-changes/directives/infiniteScroll.coffee index dcc087c25a..65b1ac51eb 100644 --- a/services/web/public/coffee/ide/track-changes/directives/infiniteScroll.coffee +++ b/services/web/public/coffee/ide/track-changes/directives/infiniteScroll.coffee @@ -8,29 +8,25 @@ define [ element.css 'overflow-y': 'auto' atEndOfListView = () -> + if attrs.infiniteScrollUpwards? + atTopOfListView() + else + atBottomOfList() + + atTopOfListView = () -> + element.scrollTop() < 30 + + atBottomOfListView = () -> element.scrollTop() + element.height() >= innerElement.height() - 30 listShorterThanContainer = () -> - element.innerHeight() > @$(".change-list").outerHeight() + element.height() > innerElement.height() loadUntilFull = () -> if (listShorterThanContainer() or atEndOfListView()) and not scope.$eval(attrs.infiniteScrollDisabled) promise = scope.$eval(attrs.infiniteScroll) promise.then () -> loadUntilFull() - # @collection.fetchNextBatch - # error: (error) => - # @hideLoading() - # @showEmptyMessageIfCollectionEmpty() - # callback(error) - # success: (collection, response) => - # @hideLoading() - # if @collection.isAtEnd() - # @atEndOfCollection = true - # @showEmptyMessageIfCollectionEmpty() - # callback() - # else - # @loadUntilFull(callback) element.on "scroll", (event) -> loadUntilFull() diff --git a/services/web/public/stylesheets/app/editor/chat.less b/services/web/public/stylesheets/app/editor/chat.less index ebf4e67e9a..717dc37309 100644 --- a/services/web/public/stylesheets/app/editor/chat.less +++ b/services/web/public/stylesheets/app/editor/chat.less @@ -1,6 +1,12 @@ @new-message-height: 80px; .chat { + .loading { + font-family: @font-family-serif; + padding: @line-height-computed / 2; + text-align: center; + } + .messages { position: absolute; top: 0; diff --git a/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee b/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee index 8fa69097c6..6a2df298f5 100644 --- a/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee +++ b/services/web/test/UnitTests/coffee/Chat/ChatControllerTests.coffee @@ -55,7 +55,7 @@ describe "ChatController", -> describe "getMessages", -> beforeEach -> - @req.body = @query + @req.query = @query it "should ask the chat handler about the request", (done)->