Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 17 additions & 104 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@
"vue-material-design-icons": "^5.3.1",
"vue-router": "^4.6.3",
"vue-tsc": "^3.1.2",
"vue-virtual-scroller": "^2.0.0-beta.8",
"vuex": "^4.1.0",
"webdav": "^5.8.0",
"webrtc-adapter": "^9.0.3",
Expand Down
256 changes: 118 additions & 138 deletions src/components/LeftSidebar/ConversationsList/ConversationsListVirtual.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,153 +3,133 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<RecycleScroller
ref="scroller"
item-tag="ul"
:items="conversations"
:item-size="itemSize"
key-field="token">
<template #default="{ item }">
<ConversationItem :item="item" :compact="compact" />
</template>
<template #after>
<LoadingPlaceholder v-if="loading" type="conversations" />
</template>
</RecycleScroller>
</template>
<script setup lang="ts">
import type { Conversation } from '../../../types/index.ts'

<script>
import { computed } from 'vue'
import { RecycleScroller } from 'vue-virtual-scroller'
import { useVirtualList } from '@vueuse/core'
import { computed, toRef } from 'vue'
import LoadingPlaceholder from '../../UIShared/LoadingPlaceholder.vue'
import ConversationItem from './ConversationItem.vue'
import { AVATAR } from '../../../constants.ts'

import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

export default {
name: 'ConversationsListVirtual',

components: {
LoadingPlaceholder,
ConversationItem,
RecycleScroller,
},

props: {
conversations: {
type: Array,
required: true,
},

loading: {
type: Boolean,
default: false,
},

compact: {
type: Boolean,
default: false,
},
},

setup(props) {
/* Consider:
* avatar size (and two lines of text) or compact mode (28px)
* list-item padding
* list-item__wrapper padding
*/
const itemSize = computed(() => props.compact ? 28 + 2 * 2 + 0 * 2 : AVATAR.SIZE.DEFAULT + 2 * 4 + 2 * 2)
return {
itemSize,
const props = defineProps<{
conversations: Conversation[]
loading?: boolean
compact?: boolean
}>()

/**
* Consider:
* avatar size (and two lines of text) or compact mode (28px)
* list-item padding
* list-item__wrapper padding
*/
const itemHeight = computed(() => props.compact ? 28 + 2 * 2 : AVATAR.SIZE.DEFAULT + 2 * 4 + 2 * 2)

const { list, containerProps, wrapperProps } = useVirtualList<Conversation>(toRef(() => props.conversations), {
itemHeight: () => itemHeight.value,
overscan: 10,
})

/**
* Get an index of the first fully visible conversation in viewport
* Math.ceil to include partially of (absolute number of items above viewport) + 1 (next item is in viewport) - 1 (index starts from 0)
*/
function getFirstItemInViewportIndex(): number {
return Math.ceil(containerProps.ref.value!.scrollTop / itemHeight.value)
}

/**
* Get an index of the last fully visible conversation in viewport
* Math.floor to include only fully visible of (absolute number of items below and in viewport) - 1 (index starts from 0)
*/
function getLastItemInViewportIndex(): number {
return Math.floor((containerProps.ref.value!.scrollTop + containerProps.ref.value!.clientHeight) / itemHeight.value) - 1
}

/**
* Scroll to conversation by index
*
* @param index - index of conversation to scroll to
*/
function scrollToItem(index: number) {
const firstItemIndex = getFirstItemInViewportIndex()
const lastItemIndex = getLastItemInViewportIndex()

const viewportHeight = containerProps.ref.value!.clientHeight

/**
* Scroll to a position with smooth scroll imitation
*
* @param to - target position (in px)
*/
const doScroll = (to: number) => {
const ITEMS_TO_BORDER_AFTER_SCROLL = 1
const padding = ITEMS_TO_BORDER_AFTER_SCROLL * itemHeight.value
const from = containerProps.ref.value!.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) {
containerProps.ref.value!.scrollTo({
top: to - direction * viewportHeight,
behavior: 'instant',
})
}
},

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 / this.itemSize)
},

/**
* 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) / this.itemSize) - 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 * this.itemSize
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 * this.itemSize)
} else if (index > lastItemIndex) { // Item is below
// Position of item + item's height and move to bottom
await doScroll((index + 1) * this.itemSize - 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)
}
},
},

// Scroll to the target with smooth scroll
containerProps.ref.value!.scrollTo({
top: to + padding * direction,
behavior: 'smooth',
})
}

if (index < firstItemIndex) { // Item is above
doScroll(index * itemHeight.value)
} else if (index > lastItemIndex) { // Item is below
// Position of item + item's height and move to bottom
doScroll((index + 1) * itemHeight.value - viewportHeight)
}
}

/**
* Scroll to conversation by token
*
* @param token - token of conversation to scroll to
*/
function scrollToConversation(token: string) {
const index = props.conversations.findIndex((conversation) => conversation.token === token)
if (index !== -1) {
scrollToItem(index)
}
}

defineExpose({
getFirstItemInViewportIndex,
getLastItemInViewportIndex,
scrollToItem,
scrollToConversation,
})
</script>

<template>
<li
:ref="containerProps.ref"
:style="containerProps.style"
@scroll="containerProps.onScroll">
<LoadingPlaceholder v-if="loading" type="conversations" />
<ul
v-else
:style="wrapperProps.style">
<ConversationItem
v-for="item in list"
:key="item.data.id"
:item="item.data"
:compact />
</ul>
</li>
</template>

<style lang="scss" scoped>
// Overwrite NcListItem styles
// TOREMOVE: get rid of it or find better approach
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,55 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<RecycleScroller
ref="scroller"
item-tag="ul"
:items="conversations"
:item-size="CONVERSATION_ITEM_SIZE"
key-field="token">
<template #default="{ item }">
<ConversationSearchResult :item="item" @click="onClick" />
</template>
<template #after>
<LoadingPlaceholder v-if="loading" type="conversations" />
</template>
</RecycleScroller>
</template>
<script setup lang="ts">
import type { Conversation } from '../../../types/index.ts'

<script>
import { RecycleScroller } from 'vue-virtual-scroller'
import { useVirtualList } from '@vueuse/core'
import { toRef } from 'vue'
import LoadingPlaceholder from '../../UIShared/LoadingPlaceholder.vue'
import ConversationSearchResult from './ConversationSearchResult.vue'
import { AVATAR } from '../../../constants.ts'

import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

const CONVERSATION_ITEM_SIZE = AVATAR.SIZE.DEFAULT + 2 * 4 + 2 * 2

export default {
name: 'ConversationsSearchListVirtual',

components: {
LoadingPlaceholder,
ConversationSearchResult,
RecycleScroller,
},

props: {
conversations: {
type: Array,
required: true,
},

loading: {
type: Boolean,
default: false,
},
},

emits: ['select'],

setup() {
return {
CONVERSATION_ITEM_SIZE,
}
},

methods: {
onClick(item) {
this.$emit('select', item)
},
},
const props = defineProps<{
conversations: Conversation[]
loading?: boolean
}>()

const emit = defineEmits<{
(event: 'select', item: Conversation): void
}>()

const itemHeight = AVATAR.SIZE.DEFAULT + 2 * 4 + 2 * 2

const { list, containerProps, wrapperProps } = useVirtualList<Conversation>(toRef(() => props.conversations), {
itemHeight,
overscan: 10,
})

/**
* Pass selected conversation to parent component
*
* @param item - selected conversation
*/
function handleClick(item: Conversation) {
emit('select', item)
}
</script>

<template>
<li
:ref="containerProps.ref"
:style="containerProps.style"
@scroll="containerProps.onScroll">
<LoadingPlaceholder v-if="loading" type="conversations" />
<ul
v-else
:style="wrapperProps.style">
<ConversationSearchResult
v-for="item in list"
:key="item.data.id"
:item="item.data"
@click="handleClick" />
</ul>
</li>
</template>
Loading
Loading