From 075b0292b55894dffaa04140050102d52091c496 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Wed, 13 Dec 2023 17:28:30 +0530 Subject: [PATCH] feat: Conversation list virtualization (#8540) Co-authored-by: Muhsin Keloth Co-authored-by: Pranav Raj S --- .../dashboard/components/ChatList.vue | 118 ++++++++++++------ .../dashboard/components/ConversationItem.vue | 72 +++++++++++ .../widgets/conversation/ConversationCard.vue | 7 +- package.json | 1 + yarn.lock | 5 + 5 files changed, 166 insertions(+), 37 deletions(-) create mode 100644 app/javascript/dashboard/components/ConversationItem.vue diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 7d76b3303..f3e901d37 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -130,37 +130,29 @@ class="conversations-list flex-1" :class="{ 'overflow-hidden': isContextMenuOpen }" > - -
- -
-

- {{ $t('CHAT_LIST.EOF') }} -

- + + + import { mapGetters } from 'vuex'; +import VirtualList from 'vue-virtual-scroll-list'; import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue'; import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue'; import ChatTypeTabs from './widgets/ChatTypeTabs.vue'; -import ConversationCard from './widgets/conversation/ConversationCard.vue'; +import ConversationItem from './ConversationItem.vue'; import timeMixin from '../mixins/time'; import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import conversationMixin from '../mixins/conversations'; @@ -220,12 +213,14 @@ export default { components: { AddCustomViews, ChatTypeTabs, - ConversationCard, + // eslint-disable-next-line vue/no-unused-components + ConversationItem, ConversationAdvancedFilter, DeleteCustomViews, ConversationBulkActions, ConversationBasicFilter, IntersectionObserver, + VirtualList, }, mixins: [ timeMixin, @@ -235,6 +230,20 @@ export default { filterMixin, uiSettingsMixin, ], + provide() { + return { + // Actions to be performed on virtual list item and context menu. + selectConversation: this.selectConversation, + deSelectConversation: this.deSelectConversation, + assignAgent: this.onAssignAgent, + assignTeam: this.onAssignTeam, + assignLabels: this.onAssignLabels, + updateConversationStatus: this.toggleConversationStatus, + toggleContextMenu: this.onContextMenuToggle, + markAsUnread: this.markAsUnread, + assignPriority: this.assignPriority, + }; + }, props: { conversationInbox: { type: [String, Number], @@ -289,6 +298,17 @@ export default { root: this.$refs.conversationList, rootMargin: '100px 0px 100px 0px', }, + + itemComponent: ConversationItem, + // virtualListExtraProps is to pass the props to the conversationItem component. + virtualListExtraProps: { + label: this.label, + teamId: this.teamId, + foldersId: this.foldersId, + conversationType: this.conversationType, + showAssignee: false, + isConversationSelected: this.isConversationSelected, + }, }; }, computed: { @@ -507,16 +527,22 @@ export default { }, label() { this.resetAndFetchData(); + this.updateVirtualListProps('label', this.label); }, conversationType() { this.resetAndFetchData(); + this.updateVirtualListProps('conversationType', this.conversationType); }, activeFolder() { this.resetAndFetchData(); + this.updateVirtualListProps('foldersId', this.foldersId); }, chatLists() { this.chatsOnView = this.conversationList; }, + showAssigneeInConversationCard(newVal) { + this.updateVirtualListProps('showAssignee', newVal); + }, }, mounted() { this.setFiltersFromUISettings(); @@ -533,6 +559,12 @@ export default { }); }, methods: { + updateVirtualListProps(key, value) { + this.virtualListExtraProps = { + ...this.virtualListExtraProps, + [key]: value, + }; + }, onApplyFilter(payload) { this.resetBulkActions(); this.foldersQuery = filterQueryGenerator(payload); @@ -695,7 +727,7 @@ export default { fetchConversations() { this.$store .dispatch('fetchAllConversations', this.conversationFilters) - .then(() => this.$emit('conversation-load')); + .then(this.emitConversationLoaded); }, loadMoreConversations() { if (this.hasCurrentPageEndReached || this.chatListLoading) { @@ -719,7 +751,7 @@ export default { queryData: filterQueryGenerator(payload), page, }) - .then(() => this.$emit('conversation-load')); + .then(this.emitConversationLoaded); this.showAdvancedFilters = false; }, fetchSavedFilteredConversations(payload) { @@ -729,7 +761,7 @@ export default { queryData: payload, page, }) - .then(() => this.$emit('conversation-load')); + .then(this.emitConversationLoaded); }, updateAssigneeTab(selectedTab) { if (this.activeAssigneeTab !== selectedTab) { @@ -741,6 +773,20 @@ export default { } } }, + emitConversationLoaded() { + this.$emit('conversation-load'); + this.$nextTick(() => { + // Addressing a known issue in the virtual list library where dynamically added items + // might not render correctly. This workaround involves a slight manual adjustment + // to the scroll position, triggering the list to refresh its rendering. + const virtualList = this.$refs.conversationVirtualList; + const scrollToOffset = virtualList?.scrollToOffset; + const currentOffset = virtualList?.getOffset() || 0; + if (scrollToOffset) { + scrollToOffset(currentOffset + 1); + } + }); + }, resetBulkActions() { this.selectedConversations = []; this.selectedInboxes = []; diff --git a/app/javascript/dashboard/components/ConversationItem.vue b/app/javascript/dashboard/components/ConversationItem.vue new file mode 100644 index 000000000..b013b0171 --- /dev/null +++ b/app/javascript/dashboard/components/ConversationItem.vue @@ -0,0 +1,72 @@ + + + diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 635600787..a19ac630a 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -31,7 +31,7 @@ size="40px" />
@@ -175,6 +175,10 @@ export default { type: Boolean, default: false, }, + enableContextMenu: { + type: Boolean, + default: false, + }, }, data() { return { @@ -289,6 +293,7 @@ export default { this.$emit(action, this.chat.id, this.inbox.id); }, openContextMenu(e) { + if (!this.enableContextMenu) return; e.preventDefault(); this.$emit('context-menu-toggle', true); this.contextMenu.x = e.pageX || e.clientX; diff --git a/package.json b/package.json index 7dcdaccd0..337f579fa 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "vue-router": "~3.5.2", "vue-template-compiler": "^2.7.0", "vue-upload-component": "2.8.22", + "vue-virtual-scroll-list": "^2.3.5", "vue2-datepicker": "^3.9.1", "vuedraggable": "^2.24.3", "vuelidate": "0.7.7", diff --git a/yarn.lock b/yarn.lock index 8cb66f523..f7a9462e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20649,6 +20649,11 @@ vue-upload-component@2.8.22: resolved "https://registry.yarnpkg.com/vue-upload-component/-/vue-upload-component-2.8.22.tgz#7a1573149a4afa5ca6e8c7e0bc70533925fe26b7" integrity sha512-AJpETqiZrgqs8bwJQpWTFrRg3i6s7cUodRRZVnb1f94Jvpd0YYfzGY4zluBqPmssNSkUaYu7EteXaK8aW17Osw== +vue-virtual-scroll-list@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-2.3.5.tgz#b589ac6245faf857c35090f854e59d653e90626c" + integrity sha512-YFK6u5yltqtAOfTBcij/KGAS2SoZvzbNIAf9qTULauPObEp53xj22tDuohrrM2vNkgoD5kejXICIUBt2Q4ZDqQ== + vue2-datepicker@^3.9.1: version "3.9.1" resolved "https://registry.yarnpkg.com/vue2-datepicker/-/vue2-datepicker-3.9.1.tgz#00d11cf30942e850f8b1a397af3c15c7465f248e"