Skip to content

Commit

Permalink
Merge pull request #10297 from nextcloud/backport/10262/stable27
Browse files Browse the repository at this point in the history
[stable27] Virtual scrolling for conversations list
  • Loading branch information
ShGKme committed Aug 22, 2023
2 parents b3c2244 + ccc038a commit e9370b7
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 81 deletions.
60 changes: 60 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -58,6 +58,7 @@
"vue-prevent-unload": "^0.2.3",
"vue-router": "^3.6.5",
"vue-shortkey": "^3.1.7",
"vue-virtual-scroller": "^1.1.2",
"vue2-leaflet": "^2.7.1",
"vuex": "^3.6.2",
"webdav": "^5.2.3",
Expand Down
11 changes: 8 additions & 3 deletions src/components/ConversationIcon.vue
Expand Up @@ -27,9 +27,14 @@
<div v-if="iconClass"
class="avatar icon"
:class="iconClass" />
<NcAvatar v-else-if="!isOneToOne"
:url="avatarUrl"
:size="size" />
<!-- img is used here instead of NcAvatar to explicitly set key required to avoid glitching in virtual scrolling -->
<img v-else-if="!isOneToOne"
:key="avatarUrl"
:src="avatarUrl"
:width="size"
:height="size"
:alt="item.displayName"
class="avatar icon">
<NcAvatar v-else
:size="size"
:user="item.name"
Expand Down
3 changes: 3 additions & 0 deletions src/components/LeftSidebar/ConversationsList/Conversation.vue
Expand Up @@ -180,6 +180,8 @@ export default {
},
},
emits: ['click'],
computed: {
counterType() {
if (this.item.unreadMentionDirect || (this.item.unreadMessages !== 0 && (
Expand Down Expand Up @@ -425,6 +427,7 @@ export default {
if (this.isSearchResult) {
this.$store.dispatch('addConversation', this.item)
}
this.$emit('click')
},
onActionClick() {
Expand Down
156 changes: 156 additions & 0 deletions src/components/LeftSidebar/ConversationsListVirtual.vue
@@ -0,0 +1,156 @@
<!--
- @copyright Copyright (c) 2023 Grigorii Shartsev <me@shgk.me>
-
- @author Grigorii Shartsev <me@shgk.me>
-
- @license AGPL-3.0-or-later
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-->

<template>
<RecycleScroller ref="scroller"
item-tag="ul"
:items="conversations"
:item-size="CONVERSATION_ITEM_SIZE"
key-field="token">
<template #default="{ item }">
<Conversation :item="item" />
</template>
<template #after>
<LoadingPlaceholder v-if="loading" type="conversations" />
</template>
</RecycleScroller>
</template>

<script>
import { RecycleScroller } from 'vue-virtual-scroller'
import LoadingPlaceholder from '../LoadingPlaceholder.vue'
import Conversation from './ConversationsList/Conversation.vue'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const CONVERSATION_ITEM_SIZE = 64
export default {
name: 'ConversationsListVirtual',
components: {
LoadingPlaceholder,
Conversation,
RecycleScroller,
},
props: {
conversations: {
type: Array,
required: true,
},
loading: {
type: Boolean,
default: false,
},
},
setup() {
return {
CONVERSATION_ITEM_SIZE,
}
},
methods: {
/**
* Get an index of the first fully visible conversation in viewport
*
* @public
* @return {number}
*/
getFirstItemInViewportIndex() {
// (ceil to include partially) of (absolute number of items above viewport) + 1 (next item is in viewport) - 1 (index starts from 0)
return Math.ceil(this.$refs.scroller.$el.scrollTop / CONVERSATION_ITEM_SIZE)
},
/**
* Get an index of the last fully visible conversation in viewport
*
* @public
* @return {number}
*/
getLastItemInViewportIndex() {
// (floor to include only fully visible) of (absolute number of items below and in viewport) - 1 (index starts from 0)
return Math.floor((this.$refs.scroller.$el.scrollTop + this.$refs.scroller.$el.clientHeight) / CONVERSATION_ITEM_SIZE) - 1
},
/**
* Scroll to conversation by index
*
* @public
* @param {number} index - index of conversation to scroll to
* @return {Promise<void>}
*/
async scrollToItem(index) {
const firstItemIndex = this.getFirstItemInViewportIndex()
const lastItemIndex = this.getLastItemInViewportIndex()
const viewportHeight = this.$refs.scroller.$el.clientHeight
/**
* Scroll to a position with smooth scroll imitation
*
* @param {number} to - target position
* @return {void}
*/
const doScroll = (to) => {
const ITEMS_TO_BORDER_AFTER_SCROLL = 1
const padding = ITEMS_TO_BORDER_AFTER_SCROLL * CONVERSATION_ITEM_SIZE
const from = this.$refs.scroller.$el.scrollTop
const direction = from < to ? 1 : -1
// If we are far from the target - instantly scroll to a close position
if (Math.abs(from - to) > viewportHeight) {
this.$refs.scroller.scrollToPosition(to - direction * viewportHeight)
}
// Scroll to the target with smooth scroll
this.$refs.scroller.$el.scrollTo({
top: to + padding * direction,
behavior: 'smooth',
})
}
if (index < firstItemIndex) { // Item is above
await doScroll(index * CONVERSATION_ITEM_SIZE)
} else if (index > lastItemIndex) { // Item is below
// Position of item + item's height and move to bottom
await doScroll((index + 1) * CONVERSATION_ITEM_SIZE - viewportHeight)
}
},
/**
* Scroll to conversation by token
*
* @param {string} token - token of conversation to scroll to
* @return {void}
*/
scrollToConversation(token) {
const index = this.conversations.findIndex(conversation => conversation.token === token)
if (index !== -1) {
this.scrollToItem(index)
}
},
},
}
</script>

0 comments on commit e9370b7

Please sign in to comment.