diff options
author | Daniel Calviño Sánchez <danxuliu@gmail.com> | 2018-11-15 06:10:40 +0100 |
---|---|---|
committer | Daniel Calviño Sánchez <danxuliu@gmail.com> | 2018-11-20 12:18:15 +0100 |
commit | e96d9d19d9346854cad24dbc8993dae5f63f200b (patch) | |
tree | 103be67c32ea306197a9dec056c0413f0d8e6c97 | |
parent | a76f6d64d0f192cb8e42ae4c912ee461b3c60ba5 (diff) |
Use VirtualList for the message list in the chat view
The virtual list requires that its internal wrappers use an absolute
position. Due to that absolute position the padding of the container
does not affect the wrappers, so the desired padding must be applied
through its left and right position.
As the virtual list keeps only a subset of its elements in the DOM the
":first-child" pseudo-selector no longer refers to the actual first
child element, but to the first one currently in the DOM; it would be
necessary to apply the CSS rules using a specific CSS class set only in
the desired element. However, as the first comment always includes the
date separator, which already has a top margin, the top padding is not
really needed in the first comment, so it was simply removed.
Moving the message list between the main view and the sidebar changes
its size, and thus it is necessary to reload the virtual list; when the
virtual list is reloaded it is ensured that the last visible element
will still be visible after the reload, so the chat view no longer needs
to explicitly handle that.
In a similar way, the message list also needs to be reloaded when the
window is resized, or when the chat view is in the main view and the
sidebar is opened or closed, as those actions change the size of the
main view.
Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
-rw-r--r-- | css/comments.scss | 8 | ||||
-rw-r--r-- | css/style.scss | 21 | ||||
-rw-r--r-- | js/app.js | 45 | ||||
-rw-r--r-- | js/views/chatview.js | 109 | ||||
-rw-r--r-- | lib/PublicShareAuth/TemplateLoader.php | 1 | ||||
-rw-r--r-- | templates/index-public.php | 1 | ||||
-rw-r--r-- | templates/index.php | 1 |
7 files changed, 103 insertions, 83 deletions
diff --git a/css/comments.scss b/css/comments.scss index 409860437..dd56d3d18 100644 --- a/css/comments.scss +++ b/css/comments.scss @@ -77,6 +77,14 @@ color: grey; } +/* Internal wrappers used by the virtual list. */ +#commentsTabView .comments .wrapper-background, +#commentsTabView .comments .wrapper { + position: absolute; + left: 0; + right: 0; +} + #commentsTabView .comment { position: relative; margin-bottom: 30px; diff --git a/css/style.scss b/css/style.scss index cfdd86cbb..2d7e62dc1 100644 --- a/css/style.scss +++ b/css/style.scss @@ -225,10 +225,6 @@ flex-direction: column; } -#app-content-wrapper #commentsTabView .comment:first-child { - padding-top: 15px; -} - /* The lateral padding is set for each child instead of for the chat view as a whole to prevent showing the scroll bar padded from the border of the chat view (which could be fixed by using a negative margin and a positive padding @@ -251,6 +247,15 @@ padding-right: 44px; } + +#app-content-wrapper #commentsTabView .comments .wrapper-background, +#app-content-wrapper #commentsTabView .comments .wrapper { + /* Padding is not respected in the comment wrapper due to its absolute + * positioning, so it must be set through its position. */ + left: 44px; + right: 44px; +} + /* Hide all siblings of the chat view when shown as the main view */ #app-content-wrapper #commentsTabView ~ *:not(#video-fullscreen):not([id^=tooltip]) { display: none !important; @@ -1223,6 +1228,14 @@ video { padding-right: 15px; } +#app-sidebar #commentsTabView .comments .wrapper-background, +#app-sidebar #commentsTabView .comments .wrapper { + /* Padding is not respected in the comment wrapper due to its absolute + * positioning, so it must be set through its position. */ + left: 15px; + right: 15px; +} + #app-sidebar #commentsTabView .newCommentForm { /* Make room to show the "Add" button when chat is shown in the sidebar. */ margin-right: 44px; @@ -472,18 +472,16 @@ var flags = this.activeRoom.get('participantFlags') || 0; var inCall = flags & OCA.SpreedMe.app.FLAG_IN_CALL !== 0; if (inCall && this._chatViewInMainView === true) { - this._chatView.saveScrollPosition(); this._chatView.$el.detach(); this._sidebarView.addTab('chat', { label: t('spreed', 'Chat'), icon: 'icon-comment', priority: 100 }, this._chatView); this._sidebarView.selectTab('chat'); - this._chatView.restoreScrollPosition(); + this._chatView.reloadMessageList(); this._chatView.setTooltipContainer(this._chatView.$el); this._chatViewInMainView = false; } else if (!inCall && !this._chatViewInMainView) { - this._chatView.saveScrollPosition(); this._sidebarView.removeTab('chat'); this._chatView.$el.prependTo('#app-content-wrapper'); - this._chatView.restoreScrollPosition(); + this._chatView.reloadMessageList(); this._chatView.setTooltipContainer($('#app')); this._chatView.focusChatInput(); this._chatViewInMainView = true; @@ -671,6 +669,45 @@ this._chatView.restoreScrollPosition(); }.bind(this)); + // Opening or closing the sidebar changes the width of the main + // view, so if the chat view is in the main view it needs to be + // reloaded. + var reloadMessageListOnSidebarVisibilityChange = function() { + if (!this._chatViewInMainView) { + return; + } + + this._chatView.reloadMessageList(); + }.bind(this); + this._chatView.listenTo(this._sidebarView, 'opened', reloadMessageListOnSidebarVisibilityChange); + this._chatView.listenTo(this._sidebarView, 'closed', reloadMessageListOnSidebarVisibilityChange); + + // Resizing the window can change the size of the chat view, both + // when it is in the main view and in the sidebar, so the chat view + // needs to be reloaded. The initial reload is not very heavy, so + // the handler is not debounced for a snappier feel and to reduce + // flickering. + // However, resizing the window below certain width causes the + // navigation bar to be hidden; an explicit handling is needed in + // this case because the app navigation (or, more specifically, its + // Snap object) adds a transition to the app content, so the reload + // needs to be delayed to give the transition time to end and thus + // give the app content time to get its final size. + var reloadMessageListOnWindowResize = function() { + var chatView = this._chatView; + + if ($(window).width() >= 768 || !this._chatViewInMainView) { + chatView.reloadMessageList(); + + return; + } + + setTimeout(function() { + chatView.reloadMessageList(); + }, 300); + }.bind(this); + $(window).resize(reloadMessageListOnWindowResize); + this._messageCollection.listenTo(roomChannel, 'leaveCurrentRoom', function() { this.stopReceivingMessages(); }); diff --git a/js/views/chatview.js b/js/views/chatview.js index 5ca240cce..eccf5f858 100644 --- a/js/views/chatview.js +++ b/js/views/chatview.js @@ -241,13 +241,13 @@ onRender: function() { delete this._lastAddedMessageModel; - this._$newestComment = $(); - this.$el.find('.emptycontent').after(this.addCommentTemplate({})); this.$el.find('.has-tooltip').tooltip({container: this._tooltipContainer}); this.$container = this.$el.find('ul.comments'); + this._virtualList = new OCA.SpreedMe.Views.VirtualList(this.$container); + if (OC.getCurrentUser().uid) { this.$el.find('.avatar').avatar(OC.getCurrentUser().uid, 32, undefined, false, undefined, OC.getCurrentUser().displayName); } else { @@ -308,56 +308,50 @@ * be able to restore the scroll position when attached again. */ saveScrollPosition: function() { - var self = this; - if (_.isUndefined(this.$container)) { return; } - var containerHeight = this.$container.outerHeight(); - - this._$lastVisibleComment = this.$container.children('.comment').filter(function() { - return self._getCommentTopPosition($(this)) < containerHeight; - }).last(); + this._savedScrollPosition = this.$container.scrollTop(); }, /** * Restores the scroll position of the message list to the last saved * position. * - * When the scroll position is restored the size of the message list may - * have changed (for example, if the chat view was detached from the - * main view and attached to the sidebar); it is not possible to - * guarantee that exactly the same messages that were visible when the - * scroll position was saved will be visible when the scroll position is - * restored. Due to this, restoring the scroll position just ensures - * that the last message that was partially visible when it was saved - * will be fully visible when it is restored. + * Note that the saved scroll position is valid only if the chat view + * was not resized since it was saved; restoring the scroll position + * after the chat view was resized may or may not work as expected. */ restoreScrollPosition: function() { - if (_.isUndefined(this.$container) || _.isUndefined(this._$lastVisibleComment)) { + if (_.isUndefined(this.$container) || _.isUndefined(this._savedScrollPosition)) { return; } - var scrollBottom = this.$container.scrollTop(); - - // When the last visible comment has a next sibling the scroll - // position is based on the top position of that next sibling. - // Basing it on the last visible comment top position and its height - // could cause the next sibling to be shown due to a negative margin - // "pulling it up" over the last visible comment bottom margin. - var $nextSibling = this._$lastVisibleComment.next(); - if ($nextSibling.length > 0) { - // Substract 1px to ensure that it does not scroll into the next - // element (which would cause the next element to be fully shown - // if saving and restoring the scroll position again) due to - // rounding. - scrollBottom += this._getCommentTopPosition($nextSibling) - 1; - } else if (this._$lastVisibleComment.length > 0) { - scrollBottom += this._getCommentTopPosition(this._$lastVisibleComment) + this._getCommentOuterHeight(this._$lastVisibleComment); + this.$container.scrollTop(this._savedScrollPosition); + }, + + /** + * Reloads the message list. + * + * This needs to be called whenever the size of the chat view has + * changed. + * + * When the message list is reloaded its size may have changed (for + * example, if the chat view was detached from the main view and + * attached to the sidebar); it is not possible to guarantee that + * exactly the same messages that were visible before will be visible + * after the message list is reloaded. Due to this, in those cases + * reloading the message list just ensures that the last message that + * was partially visible before will be fully visible after the message + * list is reloaded. + */ + reloadMessageList: function() { + if (!this._virtualList) { + return; } - this.$container.scrollTop(scrollBottom - this.$container.outerHeight()); + this._virtualList.reload(); }, _formatItem: function(commentModel) { @@ -393,16 +387,14 @@ }, _onAddModelStart: function() { - this._newMessagesBuffer = document.createDocumentFragment(); + this._virtualList.appendElementStart(); - this._scrollToNew = this._$newestComment.length === 0 || this._getCommentTopPosition(this._$newestComment) < this.$container.outerHeight(); + this._scrollToNew = this._virtualList.getLastElement() === this._virtualList.getLastVisibleElement(); }, _onAddModel: function(model) { var $el = $(this.commentTemplate(this._formatItem(model))); - // ParentNode.append() is not compatible with older browsers. - this._newMessagesBuffer.appendChild($el.get(0)); - this._$newestComment = $el; + this._virtualList.appendElement($el); if (this._modelsHaveSameActor(this._lastAddedMessageModel, model) && this._modelsAreTemporaryNear(this._lastAddedMessageModel, model) && @@ -435,44 +427,11 @@ _onAddModelEnd: function() { this.$el.find('.emptycontent').toggleClass('hidden', true); - this.$container.append(this._newMessagesBuffer); - delete this._newMessagesBuffer; + this._virtualList.appendElementEnd(); if (this._scrollToNew) { - var newestCommentHiddenHeight = (this._getCommentTopPosition(this._$newestComment) + this._getCommentOuterHeight(this._$newestComment)) - this.$container.outerHeight(); - this.$container.scrollTop(this.$container.scrollTop() + newestCommentHiddenHeight); - } - }, - - _getCommentTopPosition: function($element) { - // When the margin is positive, jQuery returns the proper top - // position of the element (that is, including the top margin). - // However, when it is negative, jQuery returns where the top - // position of the element would be if there was no margin. Grouped - // messages use a negative top margin to "pull them up" closer to - // the previous message, so in those cases the top position returned - // by jQuery is below the actual top position of the element. - var marginTop = parseInt($element.css('margin-top')); - if (marginTop >= 0) { - return $element.position().top; + this._virtualList.scrollTo(this._virtualList.getLastElement()); } - - return $element.position().top + marginTop; - }, - - _getCommentOuterHeight: function($element) { - // When the margin is positive, jQuery returns the proper outer - // height of the element. However, when it is negative, it - // substracts the negative margin from the overall height of the - // element. Grouped messages use a negative top margin to "pull them - // up" closer to the previous message, so in those cases the outer - // height returned by jQuery is smaller than the actual height. - var marginTop = parseInt($element.css('margin-top')); - if (marginTop >= 0) { - return $element.outerHeight(true); - } - - return $element.outerHeight(true) - marginTop; }, _getDateSeparator: function(timestamp) { diff --git a/lib/PublicShareAuth/TemplateLoader.php b/lib/PublicShareAuth/TemplateLoader.php index 8d187dcb6..52f4d0ddd 100644 --- a/lib/PublicShareAuth/TemplateLoader.php +++ b/lib/PublicShareAuth/TemplateLoader.php @@ -97,6 +97,7 @@ class TemplateLoader { Util::addScript('spreed', 'views/roomlistview'); Util::addScript('spreed', 'views/sidebarview'); Util::addScript('spreed', 'views/tabview'); + Util::addScript('spreed', 'views/virtuallist'); Util::addScript('spreed', 'richobjectstringparser'); Util::addScript('spreed', 'simplewebrtc'); Util::addScript('spreed', 'webrtc'); diff --git a/templates/index-public.php b/templates/index-public.php index 562200b03..f85a7326f 100644 --- a/templates/index-public.php +++ b/templates/index-public.php @@ -32,6 +32,7 @@ script( 'views/roomlistview', 'views/sidebarview', 'views/tabview', + 'views/virtuallist', 'richobjectstringparser', 'simplewebrtc', 'webrtc', diff --git a/templates/index.php b/templates/index.php index d56c20ff2..0263073cd 100644 --- a/templates/index.php +++ b/templates/index.php @@ -31,6 +31,7 @@ script( 'views/roomlistview', 'views/sidebarview', 'views/tabview', + 'views/virtuallist', 'richobjectstringparser', 'simplewebrtc', 'webrtc', |