mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	feat: Reconnect logic (#9453)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
		| @@ -27,6 +27,7 @@ | |||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { mapGetters } from 'vuex'; | import { mapGetters } from 'vuex'; | ||||||
|  | import router from '../dashboard/routes'; | ||||||
| import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue'; | import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue'; | ||||||
| import LoadingState from './components/widgets/LoadingState.vue'; | import LoadingState from './components/widgets/LoadingState.vue'; | ||||||
| import NetworkNotification from './components/NetworkNotification.vue'; | import NetworkNotification from './components/NetworkNotification.vue'; | ||||||
| @@ -43,6 +44,7 @@ import { | |||||||
|   registerSubscription, |   registerSubscription, | ||||||
|   verifyServiceWorkerExistence, |   verifyServiceWorkerExistence, | ||||||
| } from './helper/pushHelper'; | } from './helper/pushHelper'; | ||||||
|  | import ReconnectService from 'dashboard/helper/ReconnectService'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'App', |   name: 'App', | ||||||
| @@ -64,6 +66,7 @@ export default { | |||||||
|     return { |     return { | ||||||
|       showAddAccountModal: false, |       showAddAccountModal: false, | ||||||
|       latestChatwootVersion: null, |       latestChatwootVersion: null, | ||||||
|  |       reconnectService: null, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
| @@ -102,6 +105,11 @@ export default { | |||||||
|     this.listenToThemeChanges(); |     this.listenToThemeChanges(); | ||||||
|     this.setLocale(window.chatwootConfig.selectedLocale); |     this.setLocale(window.chatwootConfig.selectedLocale); | ||||||
|   }, |   }, | ||||||
|  |   beforeDestroy() { | ||||||
|  |     if (this.reconnectService) { | ||||||
|  |       this.reconnectService.disconnect(); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     initializeColorTheme() { |     initializeColorTheme() { | ||||||
|       setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches); |       setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches); | ||||||
| @@ -125,6 +133,7 @@ export default { | |||||||
|       this.updateRTLDirectionView(locale); |       this.updateRTLDirectionView(locale); | ||||||
|       this.latestChatwootVersion = latestChatwootVersion; |       this.latestChatwootVersion = latestChatwootVersion; | ||||||
|       vueActionCable.init(pubsubToken); |       vueActionCable.init(pubsubToken); | ||||||
|  |       this.reconnectService = new ReconnectService(this.$store, router); | ||||||
|  |  | ||||||
|       verifyServiceWorkerExistence(registration => |       verifyServiceWorkerExistence(registration => | ||||||
|         registration.pushManager.getSubscription().then(subscription => { |         registration.pushManager.getSubscription().then(subscription => { | ||||||
|   | |||||||
| @@ -9,6 +9,13 @@ class AccountAPI extends ApiClient { | |||||||
|   createAccount(data) { |   createAccount(data) { | ||||||
|     return axios.post(`${this.apiVersion}/accounts`, data); |     return axios.post(`${this.apiVersion}/accounts`, data); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   async getCacheKeys() { | ||||||
|  |     const response = await axios.get( | ||||||
|  |       `/api/v1/accounts/${this.accountIdFromRoute}/cache_keys` | ||||||
|  |     ); | ||||||
|  |     return response.data.cache_keys; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| export default new AccountAPI(); | export default new AccountAPI(); | ||||||
|   | |||||||
| @@ -1,26 +1,133 @@ | |||||||
|  | <script setup> | ||||||
|  | import { ref, computed, onBeforeUnmount } from 'vue'; | ||||||
|  | import { useI18n } from 'dashboard/composables/useI18n'; | ||||||
|  | import { useRoute } from 'dashboard/composables/route'; | ||||||
|  | import { useEmitter } from 'dashboard/composables/emitter'; | ||||||
|  | import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||||
|  | import { | ||||||
|  |   isAConversationRoute, | ||||||
|  |   isAInboxViewRoute, | ||||||
|  |   isNotificationRoute, | ||||||
|  | } from 'dashboard/helper/routeHelpers'; | ||||||
|  | import { useEventListener } from '@vueuse/core'; | ||||||
|  |  | ||||||
|  | const { t } = useI18n(); | ||||||
|  | const route = useRoute(); | ||||||
|  |  | ||||||
|  | const RECONNECTED_BANNER_TIMEOUT = 2000; | ||||||
|  |  | ||||||
|  | const showNotification = ref(!navigator.onLine); | ||||||
|  | const isDisconnected = ref(false); | ||||||
|  | const isReconnecting = ref(false); | ||||||
|  | const isReconnected = ref(false); | ||||||
|  | let reconnectTimeout = null; | ||||||
|  |  | ||||||
|  | const bannerText = computed(() => { | ||||||
|  |   if (isReconnecting.value) return t('NETWORK.NOTIFICATION.RECONNECTING'); | ||||||
|  |   if (isReconnected.value) return t('NETWORK.NOTIFICATION.RECONNECT_SUCCESS'); | ||||||
|  |   return t('NETWORK.NOTIFICATION.OFFLINE'); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | const iconName = computed(() => (isReconnected.value ? 'wifi' : 'wifi-off')); | ||||||
|  | const canRefresh = computed( | ||||||
|  |   () => !isReconnecting.value && !isReconnected.value | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | const refreshPage = () => { | ||||||
|  |   window.location.reload(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const closeNotification = () => { | ||||||
|  |   showNotification.value = false; | ||||||
|  |   isReconnected.value = false; | ||||||
|  |   clearTimeout(reconnectTimeout); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const isInAnyOfTheRoutes = routeName => { | ||||||
|  |   return ( | ||||||
|  |     isAConversationRoute(routeName, true) || | ||||||
|  |     isAInboxViewRoute(routeName, true) || | ||||||
|  |     isNotificationRoute(routeName, true) | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateWebsocketStatus = () => { | ||||||
|  |   isDisconnected.value = true; | ||||||
|  |   showNotification.value = true; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const handleReconnectionCompleted = () => { | ||||||
|  |   isDisconnected.value = false; | ||||||
|  |   isReconnecting.value = false; | ||||||
|  |   isReconnected.value = true; | ||||||
|  |   showNotification.value = true; | ||||||
|  |   reconnectTimeout = setTimeout(closeNotification, RECONNECTED_BANNER_TIMEOUT); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const handleReconnecting = () => { | ||||||
|  |   if (isInAnyOfTheRoutes(route.name)) { | ||||||
|  |     isReconnecting.value = true; | ||||||
|  |     isReconnected.value = false; | ||||||
|  |     showNotification.value = true; | ||||||
|  |   } else { | ||||||
|  |     handleReconnectionCompleted(); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const updateOnlineStatus = event => { | ||||||
|  |   // Case: Websocket is not disconnected | ||||||
|  |   // If the app goes offline, show the notification | ||||||
|  |   // If the app goes online, close the notification | ||||||
|  |  | ||||||
|  |   // Case: Websocket is disconnected | ||||||
|  |   // If the app goes offline, show the notification | ||||||
|  |   // If the app goes online but the websocket is disconnected, don't close the notification | ||||||
|  |   // If the app goes online and the websocket is not disconnected, close the notification | ||||||
|  |  | ||||||
|  |   if (event.type === 'offline') { | ||||||
|  |     showNotification.value = true; | ||||||
|  |   } else if (event.type === 'online' && !isDisconnected.value) { | ||||||
|  |     handleReconnectionCompleted(); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | useEventListener('online', updateOnlineStatus); | ||||||
|  | useEventListener('offline', updateOnlineStatus); | ||||||
|  | useEmitter(BUS_EVENTS.WEBSOCKET_DISCONNECT, updateWebsocketStatus); | ||||||
|  | useEmitter( | ||||||
|  |   BUS_EVENTS.WEBSOCKET_RECONNECT_COMPLETED, | ||||||
|  |   handleReconnectionCompleted | ||||||
|  | ); | ||||||
|  | useEmitter(BUS_EVENTS.WEBSOCKET_RECONNECT, handleReconnecting); | ||||||
|  |  | ||||||
|  | onBeforeUnmount(() => { | ||||||
|  |   clearTimeout(reconnectTimeout); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
| <template> | <template> | ||||||
|   <transition name="network-notification-fade" tag="div"> |   <transition name="network-notification-fade" tag="div"> | ||||||
|     <div v-show="showNotification" class="fixed top-4 left-2 z-50 group"> |     <div v-show="showNotification" class="fixed z-50 top-4 left-2 group"> | ||||||
|       <div |       <div | ||||||
|         class="flex items-center justify-between py-1 px-2 w-full rounded-lg shadow-lg bg-yellow-200 dark:bg-yellow-700 relative" |         class="relative flex items-center justify-between w-full px-2 py-1 bg-yellow-200 rounded-lg shadow-lg dark:bg-yellow-700" | ||||||
|       > |       > | ||||||
|         <fluent-icon |         <fluent-icon | ||||||
|           icon="wifi-off" |           :icon="iconName" | ||||||
|           class="text-yellow-700/50 dark:text-yellow-50" |           class="text-yellow-700/50 dark:text-yellow-50" | ||||||
|           size="18" |           size="18" | ||||||
|         /> |         /> | ||||||
|         <span |         <span | ||||||
|           class="text-xs tracking-wide font-medium px-2 text-yellow-700/70 dark:text-yellow-50" |           class="px-2 text-xs font-medium tracking-wide text-yellow-700/70 dark:text-yellow-50" | ||||||
|         > |         > | ||||||
|           {{ $t('NETWORK.NOTIFICATION.OFFLINE') }} |           {{ bannerText }} | ||||||
|         </span> |         </span> | ||||||
|         <woot-button |         <woot-button | ||||||
|  |           v-if="canRefresh" | ||||||
|           :title="$t('NETWORK.BUTTON.REFRESH')" |           :title="$t('NETWORK.BUTTON.REFRESH')" | ||||||
|           variant="clear" |           variant="clear" | ||||||
|           size="small" |           size="small" | ||||||
|           color-scheme="warning" |           color-scheme="warning" | ||||||
|           icon="arrow-clockwise" |           icon="arrow-clockwise" | ||||||
|           class="visible transition-all duration-500 ease-in-out ml-1" |  | ||||||
|           @click="refreshPage" |           @click="refreshPage" | ||||||
|         /> |         /> | ||||||
|         <woot-button |         <woot-button | ||||||
| @@ -34,55 +141,3 @@ | |||||||
|     </div> |     </div> | ||||||
|   </transition> |   </transition> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> |  | ||||||
| import globalConfigMixin from 'shared/mixins/globalConfigMixin'; |  | ||||||
| import { mapGetters } from 'vuex'; |  | ||||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; |  | ||||||
|  |  | ||||||
| export default { |  | ||||||
|   mixins: [globalConfigMixin], |  | ||||||
|  |  | ||||||
|   data() { |  | ||||||
|     return { |  | ||||||
|       showNotification: !navigator.onLine, |  | ||||||
|     }; |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   computed: { |  | ||||||
|     ...mapGetters({ globalConfig: 'globalConfig/get' }), |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   mounted() { |  | ||||||
|     window.addEventListener('offline', this.updateOnlineStatus); |  | ||||||
|     this.$emitter.on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => { |  | ||||||
|       // TODO: Remove this after completing the conversation list refetching |  | ||||||
|       // TODO: DIRTY FIX : CLEAN UP THIS WITH PROPER FIX, DELAYING THE RECONNECT FOR NOW |  | ||||||
|       // THE CABLE IS FIRING IS VERY COMMON AND THUS INTERFERING USER EXPERIENCE |  | ||||||
|       setTimeout(() => { |  | ||||||
|         this.updateOnlineStatus({ type: 'offline' }); |  | ||||||
|       }, 4000); |  | ||||||
|     }); |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   beforeDestroy() { |  | ||||||
|     window.removeEventListener('offline', this.updateOnlineStatus); |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   methods: { |  | ||||||
|     refreshPage() { |  | ||||||
|       window.location.reload(); |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     closeNotification() { |  | ||||||
|       this.showNotification = false; |  | ||||||
|     }, |  | ||||||
|  |  | ||||||
|     updateOnlineStatus(event) { |  | ||||||
|       if (event.type === 'offline') { |  | ||||||
|         this.showNotification = true; |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
| }; |  | ||||||
| </script> |  | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								app/javascript/dashboard/composables/emitter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/javascript/dashboard/composables/emitter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | import { emitter } from 'shared/helpers/mitt'; | ||||||
|  | import { onMounted, onBeforeUnmount } from 'vue'; | ||||||
|  |  | ||||||
|  | // this will automatically add event listeners to the emitter | ||||||
|  | // and remove them when the component is destroyed | ||||||
|  | const useEmitter = (eventName, callback) => { | ||||||
|  |   const cleanup = () => { | ||||||
|  |     emitter.off(eventName, callback); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   onMounted(() => { | ||||||
|  |     emitter.on(eventName, callback); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   onBeforeUnmount(cleanup); | ||||||
|  |  | ||||||
|  |   return cleanup; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export { useEmitter }; | ||||||
							
								
								
									
										51
									
								
								app/javascript/dashboard/composables/spec/emitter.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/javascript/dashboard/composables/spec/emitter.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | import { shallowMount } from '@vue/test-utils'; | ||||||
|  | import { emitter } from 'shared/helpers/mitt'; | ||||||
|  | import { useEmitter } from '../emitter'; | ||||||
|  |  | ||||||
|  | jest.mock('shared/helpers/mitt', () => ({ | ||||||
|  |   emitter: { | ||||||
|  |     on: jest.fn(), | ||||||
|  |     off: jest.fn(), | ||||||
|  |   }, | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | describe('useEmitter', () => { | ||||||
|  |   let wrapper; | ||||||
|  |   const eventName = 'my-event'; | ||||||
|  |   const callback = jest.fn(); | ||||||
|  |  | ||||||
|  |   beforeEach(() => { | ||||||
|  |     wrapper = shallowMount({ | ||||||
|  |       template: ` | ||||||
|  |         <div> | ||||||
|  |           Hello world | ||||||
|  |         </div> | ||||||
|  |       `, | ||||||
|  |       setup() { | ||||||
|  |         return { | ||||||
|  |           cleanup: useEmitter(eventName, callback), | ||||||
|  |         }; | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   afterEach(() => { | ||||||
|  |     jest.resetAllMocks(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should add an event listener on mount', () => { | ||||||
|  |     expect(emitter.on).toHaveBeenCalledWith(eventName, callback); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should remove the event listener when the component is unmounted', () => { | ||||||
|  |     wrapper.destroy(); | ||||||
|  |     expect(emitter.off).toHaveBeenCalledWith(eventName, callback); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should return the cleanup function', () => { | ||||||
|  |     const cleanup = wrapper.vm.cleanup; | ||||||
|  |     expect(typeof cleanup).toBe('function'); | ||||||
|  |     cleanup(); | ||||||
|  |     expect(emitter.off).toHaveBeenCalledWith(eventName, callback); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										140
									
								
								app/javascript/dashboard/helper/ReconnectService.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								app/javascript/dashboard/helper/ReconnectService.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | import { emitter } from 'shared/helpers/mitt'; | ||||||
|  | import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||||
|  | import { differenceInSeconds } from 'date-fns'; | ||||||
|  | import { | ||||||
|  |   isAConversationRoute, | ||||||
|  |   isAInboxViewRoute, | ||||||
|  |   isNotificationRoute, | ||||||
|  | } from 'dashboard/helper/routeHelpers'; | ||||||
|  |  | ||||||
|  | const MAX_DISCONNECT_SECONDS = 10800; | ||||||
|  |  | ||||||
|  | class ReconnectService { | ||||||
|  |   constructor(store, router) { | ||||||
|  |     this.store = store; | ||||||
|  |     this.router = router; | ||||||
|  |     this.disconnectTime = null; | ||||||
|  |  | ||||||
|  |     this.setupEventListeners(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   disconnect = () => this.removeEventListeners(); | ||||||
|  |  | ||||||
|  |   setupEventListeners = () => { | ||||||
|  |     window.addEventListener('online', this.handleOnlineEvent); | ||||||
|  |     emitter.on(BUS_EVENTS.WEBSOCKET_RECONNECT, this.onReconnect); | ||||||
|  |     emitter.on(BUS_EVENTS.WEBSOCKET_DISCONNECT, this.onDisconnect); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   removeEventListeners = () => { | ||||||
|  |     window.removeEventListener('online', this.handleOnlineEvent); | ||||||
|  |     emitter.off(BUS_EVENTS.WEBSOCKET_RECONNECT, this.onReconnect); | ||||||
|  |     emitter.off(BUS_EVENTS.WEBSOCKET_DISCONNECT, this.onDisconnect); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   getSecondsSinceDisconnect = () => | ||||||
|  |     this.disconnectTime | ||||||
|  |       ? Math.max(differenceInSeconds(new Date(), this.disconnectTime), 0) | ||||||
|  |       : 0; | ||||||
|  |  | ||||||
|  |   // Force reload if the user is disconnected for more than 3 hours | ||||||
|  |   handleOnlineEvent = () => { | ||||||
|  |     if (this.getSecondsSinceDisconnect() >= MAX_DISCONNECT_SECONDS) { | ||||||
|  |       window.location.reload(); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   fetchConversations = async () => { | ||||||
|  |     await this.store.dispatch('updateChatListFilters', { | ||||||
|  |       page: null, | ||||||
|  |       updatedWithin: this.getSecondsSinceDisconnect(), | ||||||
|  |     }); | ||||||
|  |     await this.store.dispatch('fetchAllConversations'); | ||||||
|  |     // Reset the updatedWithin in the store chat list filter after fetching conversations when the user is reconnected | ||||||
|  |     await this.store.dispatch('updateChatListFilters', { | ||||||
|  |       updatedWithin: null, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   fetchFilteredOrSavedConversations = async queryData => { | ||||||
|  |     await this.store.dispatch('fetchFilteredConversations', { | ||||||
|  |       queryData, | ||||||
|  |       page: 1, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   fetchConversationsOnReconnect = async () => { | ||||||
|  |     const { | ||||||
|  |       getAppliedConversationFiltersQuery, | ||||||
|  |       'customViews/getActiveConversationFolder': activeFolder, | ||||||
|  |     } = this.store.getters; | ||||||
|  |     const query = getAppliedConversationFiltersQuery?.payload?.length | ||||||
|  |       ? getAppliedConversationFiltersQuery | ||||||
|  |       : activeFolder?.query; | ||||||
|  |     if (query) { | ||||||
|  |       await this.fetchFilteredOrSavedConversations(query); | ||||||
|  |     } else { | ||||||
|  |       await this.fetchConversations(); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   fetchConversationMessagesOnReconnect = async () => { | ||||||
|  |     const { conversation_id: conversationId } = this.router.currentRoute.params; | ||||||
|  |     if (conversationId) { | ||||||
|  |       await this.store.dispatch('syncActiveConversationMessages', { | ||||||
|  |         conversationId: Number(conversationId), | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   fetchNotificationsOnReconnect = async filter => { | ||||||
|  |     await this.store.dispatch('notifications/index', { ...filter, page: 1 }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   revalidateCaches = async () => { | ||||||
|  |     const { label, inbox, team } = await this.store.dispatch( | ||||||
|  |       'accounts/getCacheKeys' | ||||||
|  |     ); | ||||||
|  |     await Promise.all([ | ||||||
|  |       this.store.dispatch('labels/revalidate', { newKey: label }), | ||||||
|  |       this.store.dispatch('inboxes/revalidate', { newKey: inbox }), | ||||||
|  |       this.store.dispatch('teams/revalidate', { newKey: team }), | ||||||
|  |     ]); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   handleRouteSpecificFetch = async () => { | ||||||
|  |     const currentRoute = this.router.currentRoute.name; | ||||||
|  |     if (isAConversationRoute(currentRoute, true)) { | ||||||
|  |       await this.fetchConversationsOnReconnect(); | ||||||
|  |       await this.fetchConversationMessagesOnReconnect(); | ||||||
|  |     } else if (isAInboxViewRoute(currentRoute, true)) { | ||||||
|  |       await this.fetchNotificationsOnReconnect( | ||||||
|  |         this.store.getters['notifications/getNotificationFilters'] | ||||||
|  |       ); | ||||||
|  |     } else if (isNotificationRoute(currentRoute)) { | ||||||
|  |       await this.fetchNotificationsOnReconnect(); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   setConversationLastMessageId = async () => { | ||||||
|  |     const { conversation_id: conversationId } = this.router.currentRoute.params; | ||||||
|  |     if (conversationId) { | ||||||
|  |       await this.store.dispatch('setConversationLastMessageId', { | ||||||
|  |         conversationId: Number(conversationId), | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   onDisconnect = () => { | ||||||
|  |     this.disconnectTime = new Date(); | ||||||
|  |     this.setConversationLastMessageId(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   onReconnect = async () => { | ||||||
|  |     await this.handleRouteSpecificFetch(); | ||||||
|  |     await this.revalidateCaches(); | ||||||
|  |     emitter.emit(BUS_EVENTS.WEBSOCKET_RECONNECT_COMPLETED); | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default ReconnectService; | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| import AuthAPI from '../api/auth'; | import AuthAPI from '../api/auth'; | ||||||
| import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector'; | import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector'; | ||||||
| import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper'; | import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper'; | ||||||
|  | import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||||
| import { emitter } from 'shared/helpers/mitt'; | import { emitter } from 'shared/helpers/mitt'; | ||||||
|  |  | ||||||
| class ActionCableConnector extends BaseActionCableConnector { | class ActionCableConnector extends BaseActionCableConnector { | ||||||
| @@ -33,34 +34,14 @@ class ActionCableConnector extends BaseActionCableConnector { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   // eslint-disable-next-line class-methods-use-this | ||||||
|   onReconnect = () => { |   onReconnect = () => { | ||||||
|     this.syncActiveConversationMessages(); |     emitter.emit(BUS_EVENTS.WEBSOCKET_RECONNECT); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   // eslint-disable-next-line class-methods-use-this | ||||||
|   onDisconnected = () => { |   onDisconnected = () => { | ||||||
|     this.setActiveConversationLastMessageId(); |     emitter.emit(BUS_EVENTS.WEBSOCKET_DISCONNECT); | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   setActiveConversationLastMessageId = () => { |  | ||||||
|     const { |  | ||||||
|       params: { conversation_id }, |  | ||||||
|     } = this.app.$route; |  | ||||||
|     if (conversation_id) { |  | ||||||
|       this.app.$store.dispatch('setConversationLastMessageId', { |  | ||||||
|         conversationId: Number(conversation_id), |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   syncActiveConversationMessages = () => { |  | ||||||
|     const { |  | ||||||
|       params: { conversation_id }, |  | ||||||
|     } = this.app.$route; |  | ||||||
|     if (conversation_id) { |  | ||||||
|       this.app.$store.dispatch('syncActiveConversationMessages', { |  | ||||||
|         conversationId: Number(conversation_id), |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   isAValidEvent = data => { |   isAValidEvent = data => { | ||||||
|   | |||||||
| @@ -109,5 +109,14 @@ export const getConversationDashboardRoute = routeName => { | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const isAInboxViewRoute = routeName => | export const isAInboxViewRoute = (routeName, includeBase = false) => { | ||||||
|   ['inbox_view_conversation'].includes(routeName); |   const baseRoutes = ['inbox_view']; | ||||||
|  |   const extendedRoutes = ['inbox_view_conversation']; | ||||||
|  |   const routeNames = includeBase | ||||||
|  |     ? [...baseRoutes, ...extendedRoutes] | ||||||
|  |     : extendedRoutes; | ||||||
|  |   return routeNames.includes(routeName); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const isNotificationRoute = routeName => | ||||||
|  |   routeName === 'notifications_index'; | ||||||
|   | |||||||
							
								
								
									
										342
									
								
								app/javascript/dashboard/helper/specs/ReconnectService.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										342
									
								
								app/javascript/dashboard/helper/specs/ReconnectService.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,342 @@ | |||||||
|  | import { emitter } from 'shared/helpers/mitt'; | ||||||
|  | import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||||
|  | import { differenceInSeconds } from 'date-fns'; | ||||||
|  | import { | ||||||
|  |   isAConversationRoute, | ||||||
|  |   isAInboxViewRoute, | ||||||
|  |   isNotificationRoute, | ||||||
|  | } from 'dashboard/helper/routeHelpers'; | ||||||
|  | import ReconnectService from 'dashboard/helper/ReconnectService'; | ||||||
|  |  | ||||||
|  | jest.mock('shared/helpers/mitt', () => ({ | ||||||
|  |   emitter: { | ||||||
|  |     on: jest.fn(), | ||||||
|  |     off: jest.fn(), | ||||||
|  |     emit: jest.fn(), | ||||||
|  |   }, | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | jest.mock('date-fns', () => ({ | ||||||
|  |   differenceInSeconds: jest.fn(), | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | jest.mock('dashboard/helper/routeHelpers', () => ({ | ||||||
|  |   isAConversationRoute: jest.fn(), | ||||||
|  |   isAInboxViewRoute: jest.fn(), | ||||||
|  |   isNotificationRoute: jest.fn(), | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | const storeMock = { | ||||||
|  |   dispatch: jest.fn(), | ||||||
|  |   getters: { | ||||||
|  |     getAppliedConversationFiltersQuery: [], | ||||||
|  |     'customViews/getActiveConversationFolder': { query: {} }, | ||||||
|  |     'notifications/getNotificationFilters': {}, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const routerMock = { | ||||||
|  |   currentRoute: { | ||||||
|  |     name: '', | ||||||
|  |     params: { conversation_id: null }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | describe('ReconnectService', () => { | ||||||
|  |   let reconnectService; | ||||||
|  |  | ||||||
|  |   beforeEach(() => { | ||||||
|  |     window.addEventListener = jest.fn(); | ||||||
|  |     window.removeEventListener = jest.fn(); | ||||||
|  |     Object.defineProperty(window, 'location', { | ||||||
|  |       configurable: true, | ||||||
|  |       value: { reload: jest.fn() }, | ||||||
|  |     }); | ||||||
|  |     reconnectService = new ReconnectService(storeMock, routerMock); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   afterEach(() => { | ||||||
|  |     jest.clearAllMocks(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('constructor', () => { | ||||||
|  |     it('should initialize with store, router, and setup event listeners', () => { | ||||||
|  |       expect(reconnectService.store).toBe(storeMock); | ||||||
|  |       expect(reconnectService.router).toBe(routerMock); | ||||||
|  |       expect(window.addEventListener).toHaveBeenCalledWith( | ||||||
|  |         'online', | ||||||
|  |         reconnectService.handleOnlineEvent | ||||||
|  |       ); | ||||||
|  |       expect(emitter.on).toHaveBeenCalledWith( | ||||||
|  |         BUS_EVENTS.WEBSOCKET_RECONNECT, | ||||||
|  |         reconnectService.onReconnect | ||||||
|  |       ); | ||||||
|  |       expect(emitter.on).toHaveBeenCalledWith( | ||||||
|  |         BUS_EVENTS.WEBSOCKET_DISCONNECT, | ||||||
|  |         reconnectService.onDisconnect | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('disconnect', () => { | ||||||
|  |     it('should remove event listeners', () => { | ||||||
|  |       reconnectService.disconnect(); | ||||||
|  |       expect(window.removeEventListener).toHaveBeenCalledWith( | ||||||
|  |         'online', | ||||||
|  |         reconnectService.handleOnlineEvent | ||||||
|  |       ); | ||||||
|  |       expect(emitter.off).toHaveBeenCalledWith( | ||||||
|  |         BUS_EVENTS.WEBSOCKET_RECONNECT, | ||||||
|  |         reconnectService.onReconnect | ||||||
|  |       ); | ||||||
|  |       expect(emitter.off).toHaveBeenCalledWith( | ||||||
|  |         BUS_EVENTS.WEBSOCKET_DISCONNECT, | ||||||
|  |         reconnectService.onDisconnect | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('getSecondsSinceDisconnect', () => { | ||||||
|  |     it('should return 0 if disconnectTime is null', () => { | ||||||
|  |       reconnectService.disconnectTime = null; | ||||||
|  |       expect(reconnectService.getSecondsSinceDisconnect()).toBe(0); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should return the number of seconds since disconnect', () => { | ||||||
|  |       reconnectService.disconnectTime = new Date(); | ||||||
|  |       differenceInSeconds.mockReturnValue(100); | ||||||
|  |       expect(reconnectService.getSecondsSinceDisconnect()).toBe(100); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('handleOnlineEvent', () => { | ||||||
|  |     it('should reload the page if disconnected for more than 3 hours', () => { | ||||||
|  |       reconnectService.getSecondsSinceDisconnect = jest | ||||||
|  |         .fn() | ||||||
|  |         .mockReturnValue(10801); | ||||||
|  |       reconnectService.handleOnlineEvent(); | ||||||
|  |       expect(window.location.reload).toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should not reload the page if disconnected for less than 3 hours', () => { | ||||||
|  |       reconnectService.getSecondsSinceDisconnect = jest | ||||||
|  |         .fn() | ||||||
|  |         .mockReturnValue(10799); | ||||||
|  |       reconnectService.handleOnlineEvent(); | ||||||
|  |       expect(window.location.reload).not.toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('fetchConversations', () => { | ||||||
|  |     it('should dispatch updateChatListFilters and fetchAllConversations', async () => { | ||||||
|  |       reconnectService.getSecondsSinceDisconnect = jest | ||||||
|  |         .fn() | ||||||
|  |         .mockReturnValue(100); | ||||||
|  |       await reconnectService.fetchConversations(); | ||||||
|  |       expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', { | ||||||
|  |         page: null, | ||||||
|  |         updatedWithin: 100, | ||||||
|  |       }); | ||||||
|  |       expect(storeMock.dispatch).toHaveBeenCalledWith('fetchAllConversations'); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should dispatch updateChatListFilters and reset updatedWithin', async () => { | ||||||
|  |       reconnectService.getSecondsSinceDisconnect = jest | ||||||
|  |         .fn() | ||||||
|  |         .mockReturnValue(100); | ||||||
|  |       await reconnectService.fetchConversations(); | ||||||
|  |       expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', { | ||||||
|  |         updatedWithin: null, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('fetchFilteredOrSavedConversations', () => { | ||||||
|  |     it('should dispatch fetchFilteredConversations', async () => { | ||||||
|  |       const payload = { test: 'data' }; | ||||||
|  |       await reconnectService.fetchFilteredOrSavedConversations(payload); | ||||||
|  |       expect(storeMock.dispatch).toHaveBeenCalledWith( | ||||||
|  |         'fetchFilteredConversations', | ||||||
|  |         { queryData: payload, page: 1 } | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('fetchConversationsOnReconnect', () => { | ||||||
|  |     it('should fetch filtered or saved conversations if query exists', async () => { | ||||||
|  |       storeMock.getters.getAppliedConversationFiltersQuery = { | ||||||
|  |         payload: [ | ||||||
|  |           { | ||||||
|  |             attribute_key: 'status', | ||||||
|  |             filter_operator: 'equal_to', | ||||||
|  |             values: ['open'], | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }; | ||||||
|  |       const spy = jest.spyOn( | ||||||
|  |         reconnectService, | ||||||
|  |         'fetchFilteredOrSavedConversations' | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       await reconnectService.fetchConversationsOnReconnect(); | ||||||
|  |  | ||||||
|  |       expect(spy).toHaveBeenCalledWith( | ||||||
|  |         storeMock.getters.getAppliedConversationFiltersQuery | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should fetch all conversations if no query exists', async () => { | ||||||
|  |       storeMock.getters.getAppliedConversationFiltersQuery = []; | ||||||
|  |       storeMock.getters['customViews/getActiveConversationFolder'] = { | ||||||
|  |         query: null, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const spy = jest.spyOn(reconnectService, 'fetchConversations'); | ||||||
|  |  | ||||||
|  |       await reconnectService.fetchConversationsOnReconnect(); | ||||||
|  |  | ||||||
|  |       expect(spy).toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should fetch filtered or saved conversations if active folder query exists and no applied query', async () => { | ||||||
|  |       storeMock.getters.getAppliedConversationFiltersQuery = []; | ||||||
|  |       storeMock.getters['customViews/getActiveConversationFolder'] = { | ||||||
|  |         query: { test: 'activeFolderQuery' }, | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       const spy = jest.spyOn( | ||||||
|  |         reconnectService, | ||||||
|  |         'fetchFilteredOrSavedConversations' | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       await reconnectService.fetchConversationsOnReconnect(); | ||||||
|  |  | ||||||
|  |       expect(spy).toHaveBeenCalledWith({ test: 'activeFolderQuery' }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('fetchConversationMessagesOnReconnect', () => { | ||||||
|  |     it('should dispatch syncActiveConversationMessages if conversationId exists', async () => { | ||||||
|  |       routerMock.currentRoute.params.conversation_id = 1; | ||||||
|  |       await reconnectService.fetchConversationMessagesOnReconnect(); | ||||||
|  |       expect(storeMock.dispatch).toHaveBeenCalledWith( | ||||||
|  |         'syncActiveConversationMessages', | ||||||
|  |         { conversationId: 1 } | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should not dispatch syncActiveConversationMessages if conversationId does not exist', async () => { | ||||||
|  |       routerMock.currentRoute.params.conversation_id = null; | ||||||
|  |       await reconnectService.fetchConversationMessagesOnReconnect(); | ||||||
|  |       expect(storeMock.dispatch).not.toHaveBeenCalledWith( | ||||||
|  |         'syncActiveConversationMessages', | ||||||
|  |         expect.anything() | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('fetchNotificationsOnReconnect', () => { | ||||||
|  |     it('should dispatch notifications/index', async () => { | ||||||
|  |       const filter = { test: 'filter' }; | ||||||
|  |       await reconnectService.fetchNotificationsOnReconnect(filter); | ||||||
|  |       expect(storeMock.dispatch).toHaveBeenCalledWith('notifications/index', { | ||||||
|  |         ...filter, | ||||||
|  |         page: 1, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('revalidateCaches', () => { | ||||||
|  |     it('should dispatch revalidate actions for labels, inboxes, and teams', async () => { | ||||||
|  |       storeMock.dispatch.mockResolvedValueOnce({ | ||||||
|  |         label: 'labelKey', | ||||||
|  |         inbox: 'inboxKey', | ||||||
|  |         team: 'teamKey', | ||||||
|  |       }); | ||||||
|  |       await reconnectService.revalidateCaches(); | ||||||
|  |       expect(storeMock.dispatch).toHaveBeenCalledWith('accounts/getCacheKeys'); | ||||||
|  |       expect(storeMock.dispatch).toHaveBeenCalledWith('labels/revalidate', { | ||||||
|  |         newKey: 'labelKey', | ||||||
|  |       }); | ||||||
|  |       expect(storeMock.dispatch).toHaveBeenCalledWith('inboxes/revalidate', { | ||||||
|  |         newKey: 'inboxKey', | ||||||
|  |       }); | ||||||
|  |       expect(storeMock.dispatch).toHaveBeenCalledWith('teams/revalidate', { | ||||||
|  |         newKey: 'teamKey', | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('handleRouteSpecificFetch', () => { | ||||||
|  |     it('should fetch conversations and messages if current route is a conversation route', async () => { | ||||||
|  |       isAConversationRoute.mockReturnValue(true); | ||||||
|  |       const spyConversations = jest.spyOn( | ||||||
|  |         reconnectService, | ||||||
|  |         'fetchConversationsOnReconnect' | ||||||
|  |       ); | ||||||
|  |       const spyMessages = jest.spyOn( | ||||||
|  |         reconnectService, | ||||||
|  |         'fetchConversationMessagesOnReconnect' | ||||||
|  |       ); | ||||||
|  |       await reconnectService.handleRouteSpecificFetch(); | ||||||
|  |       expect(spyConversations).toHaveBeenCalled(); | ||||||
|  |       expect(spyMessages).toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should fetch notifications if current route is an inbox view route', async () => { | ||||||
|  |       isAInboxViewRoute.mockReturnValue(true); | ||||||
|  |       const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect'); | ||||||
|  |       await reconnectService.handleRouteSpecificFetch(); | ||||||
|  |       expect(spy).toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should fetch notifications if current route is a notification route', async () => { | ||||||
|  |       isNotificationRoute.mockReturnValue(true); | ||||||
|  |       const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect'); | ||||||
|  |       await reconnectService.handleRouteSpecificFetch(); | ||||||
|  |       expect(spy).toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('setConversationLastMessageId', () => { | ||||||
|  |     it('should dispatch setConversationLastMessageId if conversationId exists', async () => { | ||||||
|  |       routerMock.currentRoute.params.conversation_id = 1; | ||||||
|  |       await reconnectService.setConversationLastMessageId(); | ||||||
|  |       expect(storeMock.dispatch).toHaveBeenCalledWith( | ||||||
|  |         'setConversationLastMessageId', | ||||||
|  |         { conversationId: 1 } | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should not dispatch setConversationLastMessageId if conversationId does not exist', async () => { | ||||||
|  |       routerMock.currentRoute.params.conversation_id = null; | ||||||
|  |       await reconnectService.setConversationLastMessageId(); | ||||||
|  |       expect(storeMock.dispatch).not.toHaveBeenCalledWith( | ||||||
|  |         'setConversationLastMessageId', | ||||||
|  |         expect.anything() | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('onDisconnect', () => { | ||||||
|  |     it('should set disconnectTime and call setConversationLastMessageId', () => { | ||||||
|  |       reconnectService.setConversationLastMessageId = jest.fn(); | ||||||
|  |       reconnectService.onDisconnect(); | ||||||
|  |       expect(reconnectService.disconnectTime).toBeInstanceOf(Date); | ||||||
|  |       expect(reconnectService.setConversationLastMessageId).toHaveBeenCalled(); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('onReconnect', () => { | ||||||
|  |     it('should handle route-specific fetch, revalidate caches, and emit WEBSOCKET_RECONNECT_COMPLETED event', async () => { | ||||||
|  |       reconnectService.handleRouteSpecificFetch = jest.fn(); | ||||||
|  |       reconnectService.revalidateCaches = jest.fn(); | ||||||
|  |       await reconnectService.onReconnect(); | ||||||
|  |       expect(reconnectService.handleRouteSpecificFetch).toHaveBeenCalled(); | ||||||
|  |       expect(reconnectService.revalidateCaches).toHaveBeenCalled(); | ||||||
|  |       expect(emitter.emit).toHaveBeenCalledWith( | ||||||
|  |         BUS_EVENTS.WEBSOCKET_RECONNECT_COMPLETED | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -186,4 +186,12 @@ describe('isAInboxViewRoute', () => { | |||||||
|     expect(isAInboxViewRoute('inbox_view_conversation')).toBe(true); |     expect(isAInboxViewRoute('inbox_view_conversation')).toBe(true); | ||||||
|     expect(isAInboxViewRoute('inbox_conversation')).toBe(false); |     expect(isAInboxViewRoute('inbox_conversation')).toBe(false); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   it('returns true if base inbox view route name is provided and includeBase is true', () => { | ||||||
|  |     expect(isAInboxViewRoute('inbox_view', true)).toBe(true); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('returns false if base inbox view route name is provided and includeBase is false', () => { | ||||||
|  |     expect(isAInboxViewRoute('inbox_view')).toBe(false); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -95,7 +95,9 @@ | |||||||
|   }, |   }, | ||||||
|   "NETWORK": { |   "NETWORK": { | ||||||
|     "NOTIFICATION": { |     "NOTIFICATION": { | ||||||
|       "OFFLINE": "Offline" |       "OFFLINE": "Offline", | ||||||
|  |       "RECONNECTING": "Reconnecting...", | ||||||
|  |       "RECONNECT_SUCCESS": "Reconnected" | ||||||
|     }, |     }, | ||||||
|     "BUTTON": { |     "BUTTON": { | ||||||
|       "REFRESH": "Refresh" |       "REFRESH": "Refresh" | ||||||
|   | |||||||
| @@ -109,6 +109,10 @@ export const actions = { | |||||||
|       // silent error |       // silent error | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  |   getCacheKeys: async () => { | ||||||
|  |     return AccountAPI.getCacheKeys(); | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const mutations = { | export const mutations = { | ||||||
|   | |||||||
| @@ -41,11 +41,14 @@ export const mutations = { | |||||||
|         newAllConversations[indexInCurrentList] = conversation; |         newAllConversations[indexInCurrentList] = conversation; | ||||||
|       } else { |       } else { | ||||||
|         // If the conversation is already in the list and selectedChatId is the same, |         // If the conversation is already in the list and selectedChatId is the same, | ||||||
|         // replace all data except the messages array |         // replace all data except the messages array, attachments, dataFetched, allMessagesLoaded | ||||||
|         const existingConversation = newAllConversations[indexInCurrentList]; |         const existingConversation = newAllConversations[indexInCurrentList]; | ||||||
|         newAllConversations[indexInCurrentList] = { |         newAllConversations[indexInCurrentList] = { | ||||||
|           ...conversation, |           ...conversation, | ||||||
|  |           allMessagesLoaded: existingConversation.allMessagesLoaded, | ||||||
|           messages: existingConversation.messages, |           messages: existingConversation.messages, | ||||||
|  |           dataFetched: existingConversation.dataFetched, | ||||||
|  |           attachments: existingConversation.attachments, | ||||||
|         }; |         }; | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -307,9 +307,17 @@ describe('#mutations', () => { | |||||||
|       expect(state.allConversations).toEqual(data); |       expect(state.allConversations).toEqual(data); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('set all conversation in reconnect if selected chat id and conversation id is the same then do not update messages', () => { |     it('set all conversation in reconnect if selected chat id and conversation id is the same then do not update messages, attachments, dataFetched, allMessagesLoaded', () => { | ||||||
|       const state = { |       const state = { | ||||||
|         allConversations: [{ id: 1, messages: [{ id: 1, content: 'test' }] }], |         allConversations: [ | ||||||
|  |           { | ||||||
|  |             id: 1, | ||||||
|  |             messages: [{ id: 1, content: 'test' }], | ||||||
|  |             attachments: [{ id: 1, name: 'test1.png' }], | ||||||
|  |             dataFetched: true, | ||||||
|  |             allMessagesLoaded: true, | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|         selectedChatId: 1, |         selectedChatId: 1, | ||||||
|       }; |       }; | ||||||
|       const data = [ |       const data = [ | ||||||
| @@ -317,10 +325,20 @@ describe('#mutations', () => { | |||||||
|           id: 1, |           id: 1, | ||||||
|           name: 'test', |           name: 'test', | ||||||
|           messages: [{ id: 1, content: 'updated message' }], |           messages: [{ id: 1, content: 'updated message' }], | ||||||
|  |           attachments: [{ id: 1, name: 'test.png' }], | ||||||
|  |           dataFetched: true, | ||||||
|  |           allMessagesLoaded: true, | ||||||
|         }, |         }, | ||||||
|       ]; |       ]; | ||||||
|       const expected = [ |       const expected = [ | ||||||
|         { id: 1, name: 'test', messages: [{ id: 1, content: 'test' }] }, |         { | ||||||
|  |           id: 1, | ||||||
|  |           name: 'test', | ||||||
|  |           messages: [{ id: 1, content: 'test' }], | ||||||
|  |           attachments: [{ id: 1, name: 'test1.png' }], | ||||||
|  |           dataFetched: true, | ||||||
|  |           allMessagesLoaded: true, | ||||||
|  |         }, | ||||||
|       ]; |       ]; | ||||||
|       mutations[types.SET_ALL_CONVERSATION](state, data); |       mutations[types.SET_ALL_CONVERSATION](state, data); | ||||||
|       expect(state.allConversations).toEqual(expected); |       expect(state.allConversations).toEqual(expected); | ||||||
|   | |||||||
| @@ -199,6 +199,7 @@ | |||||||
|   "video-add-outline": "M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-1.063c.154-.478.255-.98.294-1.5h.769a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6h-8.5A1.75 1.75 0 0 0 3.5 7.75v3.982A6.517 6.517 0 0 0 2 12.81V7.75A3.25 3.25 0 0 1 5.25 4.5h8.5Zm6.75 3.073L17 9.674v4.651l3.5 2.1V7.573ZM12 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0ZM7 18l.001 2.503a.5.5 0 1 1-1 0V18H3.496a.5.5 0 0 1 0-1H6v-2.5a.5.5 0 1 1 1 0V17h2.497a.5.5 0 0 1 0 1H7Z", |   "video-add-outline": "M13.75 4.5A3.25 3.25 0 0 1 17 7.75v.173l3.864-2.318A.75.75 0 0 1 22 6.248V17.75a.75.75 0 0 1-1.136.643L17 16.075v.175a3.25 3.25 0 0 1-3.25 3.25h-1.063c.154-.478.255-.98.294-1.5h.769a1.75 1.75 0 0 0 1.75-1.75v-8.5A1.75 1.75 0 0 0 13.75 6h-8.5A1.75 1.75 0 0 0 3.5 7.75v3.982A6.517 6.517 0 0 0 2 12.81V7.75A3.25 3.25 0 0 1 5.25 4.5h8.5Zm6.75 3.073L17 9.674v4.651l3.5 2.1V7.573ZM12 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0ZM7 18l.001 2.503a.5.5 0 1 1-1 0V18H3.496a.5.5 0 0 1 0-1H6v-2.5a.5.5 0 1 1 1 0V17h2.497a.5.5 0 0 1 0 1H7Z", | ||||||
|   "warning-outline": "M10.91 2.782a2.25 2.25 0 0 1 2.975.74l.083.138 7.759 14.009a2.25 2.25 0 0 1-1.814 3.334l-.154.006H4.243a2.25 2.25 0 0 1-2.041-3.197l.072-.143L10.031 3.66a2.25 2.25 0 0 1 .878-.878Zm9.505 15.613-7.76-14.008a.75.75 0 0 0-1.254-.088l-.057.088-7.757 14.008a.75.75 0 0 0 .561 1.108l.095.006h15.516a.75.75 0 0 0 .696-1.028l-.04-.086-7.76-14.008 7.76 14.008ZM12 16.002a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997ZM11.995 8.5a.75.75 0 0 1 .744.647l.007.102.004 4.502a.75.75 0 0 1-1.494.103l-.006-.102-.004-4.502a.75.75 0 0 1 .75-.75Z", |   "warning-outline": "M10.91 2.782a2.25 2.25 0 0 1 2.975.74l.083.138 7.759 14.009a2.25 2.25 0 0 1-1.814 3.334l-.154.006H4.243a2.25 2.25 0 0 1-2.041-3.197l.072-.143L10.031 3.66a2.25 2.25 0 0 1 .878-.878Zm9.505 15.613-7.76-14.008a.75.75 0 0 0-1.254-.088l-.057.088-7.757 14.008a.75.75 0 0 0 .561 1.108l.095.006h15.516a.75.75 0 0 0 .696-1.028l-.04-.086-7.76-14.008 7.76 14.008ZM12 16.002a.999.999 0 1 1 0 1.997.999.999 0 0 1 0-1.997ZM11.995 8.5a.75.75 0 0 1 .744.647l.007.102.004 4.502a.75.75 0 0 1-1.494.103l-.006-.102-.004-4.502a.75.75 0 0 1 .75-.75Z", | ||||||
|   "wifi-off-outline": "m12.858 14.273 7.434 7.434a1 1 0 0 0 1.414-1.414l-17.999-18a1 1 0 1 0-1.414 1.414L5.39 6.804c-.643.429-1.254.927-1.821 1.495a12.382 12.382 0 0 0-1.39 1.683 1 1 0 0 0 1.644 1.14c.363-.524.761-1.01 1.16-1.41a9.94 9.94 0 0 1 1.855-1.46L7.99 9.405a8.14 8.14 0 0 0-3.203 3.377 1 1 0 0 0 1.784.903 6.08 6.08 0 0 1 1.133-1.563 6.116 6.116 0 0 1 1.77-1.234l1.407 1.407A5.208 5.208 0 0 0 8.336 13.7a5.25 5.25 0 0 0-1.09 1.612 1 1 0 0 0 1.832.802c.167-.381.394-.722.672-1a3.23 3.23 0 0 1 3.108-.841Zm-1.332-5.93 2.228 2.229a6.1 6.1 0 0 1 2.616 1.55c.444.444.837.995 1.137 1.582a1 1 0 1 0 1.78-.911 8.353 8.353 0 0 0-1.503-2.085 8.108 8.108 0 0 0-6.258-2.365ZM8.51 5.327l1.651 1.651a9.904 9.904 0 0 1 10.016 4.148 1 1 0 1 0 1.646-1.136A11.912 11.912 0 0 0 8.51 5.327Zm4.552 11.114a1.501 1.501 0 1 1-2.123 2.123 1.501 1.501 0 0 1 2.123-2.123Z", |   "wifi-off-outline": "m12.858 14.273 7.434 7.434a1 1 0 0 0 1.414-1.414l-17.999-18a1 1 0 1 0-1.414 1.414L5.39 6.804c-.643.429-1.254.927-1.821 1.495a12.382 12.382 0 0 0-1.39 1.683 1 1 0 0 0 1.644 1.14c.363-.524.761-1.01 1.16-1.41a9.94 9.94 0 0 1 1.855-1.46L7.99 9.405a8.14 8.14 0 0 0-3.203 3.377 1 1 0 0 0 1.784.903 6.08 6.08 0 0 1 1.133-1.563 6.116 6.116 0 0 1 1.77-1.234l1.407 1.407A5.208 5.208 0 0 0 8.336 13.7a5.25 5.25 0 0 0-1.09 1.612 1 1 0 0 0 1.832.802c.167-.381.394-.722.672-1a3.23 3.23 0 0 1 3.108-.841Zm-1.332-5.93 2.228 2.229a6.1 6.1 0 0 1 2.616 1.55c.444.444.837.995 1.137 1.582a1 1 0 1 0 1.78-.911 8.353 8.353 0 0 0-1.503-2.085 8.108 8.108 0 0 0-6.258-2.365ZM8.51 5.327l1.651 1.651a9.904 9.904 0 0 1 10.016 4.148 1 1 0 1 0 1.646-1.136A11.912 11.912 0 0 0 8.51 5.327Zm4.552 11.114a1.501 1.501 0 1 1-2.123 2.123 1.501 1.501 0 0 1 2.123-2.123Z", | ||||||
|  |   "wifi-outline": "M17.745 10.75a8.292 8.292 0 0 1 1.492 2.07a.75.75 0 1 1-1.336.683a6.797 6.797 0 0 0-1.217-1.692A6.562 6.562 0 0 0 6.19 13.484a.75.75 0 1 1-1.338-.677a8.062 8.062 0 0 1 12.893-2.057Zm-2.102 3.07c.448.447.816.997 1.072 1.582a.75.75 0 1 1-1.374.602a3.719 3.719 0 0 0-.759-1.124a3.592 3.592 0 0 0-5.08 0c-.31.31-.562.689-.747 1.11a.75.75 0 1 1-1.374-.6a5.11 5.11 0 0 1 1.061-1.57a5.092 5.092 0 0 1 7.201 0Zm4.805-5.541c.51.509.99 1.09 1.408 1.697a.75.75 0 1 1-1.234.852a10.822 10.822 0 0 0-1.235-1.489c-4.08-4.08-10.695-4.08-14.775 0c-.422.422-.84.934-1.222 1.484a.75.75 0 0 1-1.232-.855c.43-.62.904-1.2 1.394-1.69c4.665-4.665 12.23-4.665 16.896 0Zm-7.387 8.16a1.5 1.5 0 1 1-2.122 2.122a1.5 1.5 0 0 1 2.122-2.122Z", | ||||||
|   "whatsapp-outline": "M19.05 4.91A9.816 9.816 0 0 0 12.04 2c-5.46 0-9.91 4.45-9.91 9.91c0 1.75.46 3.45 1.32 4.95L2.05 22l5.25-1.38c1.45.79 3.08 1.21 4.74 1.21c5.46 0 9.91-4.45 9.91-9.91c0-2.65-1.03-5.14-2.9-7.01zm-7.01 15.24c-1.48 0-2.93-.4-4.2-1.15l-.3-.18l-3.12.82l.83-3.04l-.2-.31a8.264 8.264 0 0 1-1.26-4.38c0-4.54 3.7-8.24 8.24-8.24c2.2 0 4.27.86 5.82 2.42a8.183 8.183 0 0 1 2.41 5.83c.02 4.54-3.68 8.23-8.22 8.23zm4.52-6.16c-.25-.12-1.47-.72-1.69-.81c-.23-.08-.39-.12-.56.12c-.17.25-.64.81-.78.97c-.14.17-.29.19-.54.06c-.25-.12-1.05-.39-1.99-1.23c-.74-.66-1.23-1.47-1.38-1.72c-.14-.25-.02-.38.11-.51c.11-.11.25-.29.37-.43s.17-.25.25-.41c.08-.17.04-.31-.02-.43s-.56-1.34-.76-1.84c-.2-.48-.41-.42-.56-.43h-.48c-.17 0-.43.06-.66.31c-.22.25-.86.85-.86 2.07c0 1.22.89 2.4 1.01 2.56c.12.17 1.75 2.67 4.23 3.74c.59.26 1.05.41 1.41.52c.59.19 1.13.16 1.56.1c.48-.07 1.47-.6 1.67-1.18c.21-.58.21-1.07.14-1.18s-.22-.16-.47-.28z", |   "whatsapp-outline": "M19.05 4.91A9.816 9.816 0 0 0 12.04 2c-5.46 0-9.91 4.45-9.91 9.91c0 1.75.46 3.45 1.32 4.95L2.05 22l5.25-1.38c1.45.79 3.08 1.21 4.74 1.21c5.46 0 9.91-4.45 9.91-9.91c0-2.65-1.03-5.14-2.9-7.01zm-7.01 15.24c-1.48 0-2.93-.4-4.2-1.15l-.3-.18l-3.12.82l.83-3.04l-.2-.31a8.264 8.264 0 0 1-1.26-4.38c0-4.54 3.7-8.24 8.24-8.24c2.2 0 4.27.86 5.82 2.42a8.183 8.183 0 0 1 2.41 5.83c.02 4.54-3.68 8.23-8.22 8.23zm4.52-6.16c-.25-.12-1.47-.72-1.69-.81c-.23-.08-.39-.12-.56.12c-.17.25-.64.81-.78.97c-.14.17-.29.19-.54.06c-.25-.12-1.05-.39-1.99-1.23c-.74-.66-1.23-1.47-1.38-1.72c-.14-.25-.02-.38.11-.51c.11-.11.25-.29.37-.43s.17-.25.25-.41c.08-.17.04-.31-.02-.43s-.56-1.34-.76-1.84c-.2-.48-.41-.42-.56-.43h-.48c-.17 0-.43.06-.66.31c-.22.25-.86.85-.86 2.07c0 1.22.89 2.4 1.01 2.56c.12.17 1.75 2.67 4.23 3.74c.59.26 1.05.41 1.41.52c.59.19 1.13.16 1.56.1c.48-.07 1.47-.6 1.67-1.18c.21-.58.21-1.07.14-1.18s-.22-.16-.47-.28z", | ||||||
|   "subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z", |   "subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z", | ||||||
|   "table-switch-outline": [ |   "table-switch-outline": [ | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ export const BUS_EVENTS = { | |||||||
|   TOGGLE_SIDEMENU: 'TOGGLE_SIDEMENU', |   TOGGLE_SIDEMENU: 'TOGGLE_SIDEMENU', | ||||||
|   ON_MESSAGE_LIST_SCROLL: 'ON_MESSAGE_LIST_SCROLL', |   ON_MESSAGE_LIST_SCROLL: 'ON_MESSAGE_LIST_SCROLL', | ||||||
|   WEBSOCKET_DISCONNECT: 'WEBSOCKET_DISCONNECT', |   WEBSOCKET_DISCONNECT: 'WEBSOCKET_DISCONNECT', | ||||||
|  |   WEBSOCKET_RECONNECT: 'WEBSOCKET_RECONNECT', | ||||||
|  |   WEBSOCKET_RECONNECT_COMPLETED: 'WEBSOCKET_RECONNECT_COMPLETED', | ||||||
|   TOGGLE_REPLY_TO_MESSAGE: 'TOGGLE_REPLY_TO_MESSAGE', |   TOGGLE_REPLY_TO_MESSAGE: 'TOGGLE_REPLY_TO_MESSAGE', | ||||||
|   SHOW_TOAST: 'newToastMessage', |   SHOW_TOAST: 'newToastMessage', | ||||||
|   NEW_CONVERSATION_MODAL: 'newConversationModal', |   NEW_CONVERSATION_MODAL: 'newConversationModal', | ||||||
|   | |||||||
| @@ -1,6 +1,4 @@ | |||||||
| import { createConsumer } from '@rails/actioncable'; | import { createConsumer } from '@rails/actioncable'; | ||||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; |  | ||||||
| import { emitter } from 'shared/helpers/mitt'; |  | ||||||
|  |  | ||||||
| const PRESENCE_INTERVAL = 20000; | const PRESENCE_INTERVAL = 20000; | ||||||
| const RECONNECT_INTERVAL = 1000; | const RECONNECT_INTERVAL = 1000; | ||||||
| @@ -28,8 +26,6 @@ class BaseActionCableConnector { | |||||||
|           BaseActionCableConnector.isDisconnected = true; |           BaseActionCableConnector.isDisconnected = true; | ||||||
|           this.onDisconnected(); |           this.onDisconnected(); | ||||||
|           this.initReconnectTimer(); |           this.initReconnectTimer(); | ||||||
|           // TODO: Remove this after completing the conversation list refetching |  | ||||||
|           emitter.emit(BUS_EVENTS.WEBSOCKET_DISCONNECT); |  | ||||||
|         }, |         }, | ||||||
|       } |       } | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -47,6 +47,7 @@ | |||||||
|     "@tailwindcss/typography": "^0.5.9", |     "@tailwindcss/typography": "^0.5.9", | ||||||
|     "@vuelidate/core": "^2.0.3", |     "@vuelidate/core": "^2.0.3", | ||||||
|     "@vuelidate/validators": "^2.0.4", |     "@vuelidate/validators": "^2.0.4", | ||||||
|  |     "@vueuse/core": "^10.10.0", | ||||||
|     "activestorage": "^5.2.6", |     "activestorage": "^5.2.6", | ||||||
|     "autoprefixer": "^10.4.14", |     "autoprefixer": "^10.4.14", | ||||||
|     "axios": "^1.6.0", |     "axios": "^1.6.0", | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -6550,6 +6550,11 @@ | |||||||
|   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" |   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" | ||||||
|   integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== |   integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== | ||||||
|  |  | ||||||
|  | "@types/web-bluetooth@^0.0.20": | ||||||
|  |   version "0.0.20" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597" | ||||||
|  |   integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow== | ||||||
|  |  | ||||||
| "@types/webpack-env@^1.16.0": | "@types/webpack-env@^1.16.0": | ||||||
|   version "1.18.1" |   version "1.18.1" | ||||||
|   resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.1.tgz#49699bb508961e14a3bfb68c78cd87b296889d1d" |   resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.1.tgz#49699bb508961e14a3bfb68c78cd87b296889d1d" | ||||||
| @@ -6818,6 +6823,28 @@ | |||||||
|   dependencies: |   dependencies: | ||||||
|     vue-demi "^0.13.11" |     vue-demi "^0.13.11" | ||||||
|  |  | ||||||
|  | "@vueuse/core@^10.10.0": | ||||||
|  |   version "10.10.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.10.0.tgz#05a98d3c5674762455a2c552c915d461d83e6490" | ||||||
|  |   integrity sha512-vexJ/YXYs2S42B783rI95lMt3GzEwkxzC8Hb0Ndpd8rD+p+Lk/Za4bd797Ym7yq4jXqdSyj3JLChunF/vyYjUw== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/web-bluetooth" "^0.0.20" | ||||||
|  |     "@vueuse/metadata" "10.10.0" | ||||||
|  |     "@vueuse/shared" "10.10.0" | ||||||
|  |     vue-demi ">=0.14.7" | ||||||
|  |  | ||||||
|  | "@vueuse/metadata@10.10.0": | ||||||
|  |   version "10.10.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.10.0.tgz#53e61e9380670e342cbe6e03d852f3319308cb5b" | ||||||
|  |   integrity sha512-UNAo2sTCAW5ge6OErPEHb5z7NEAg3XcO9Cj7OK45aZXfLLH1QkexDcZD77HBi5zvEiLOm1An+p/4b5K3Worpug== | ||||||
|  |  | ||||||
|  | "@vueuse/shared@10.10.0": | ||||||
|  |   version "10.10.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.10.0.tgz#93f7c2210151ff43c2c7677963f7aa3aef5d9896" | ||||||
|  |   integrity sha512-2aW33Ac0Uk0U+9yo3Ypg9s5KcR42cuehRWl7vnUHadQyFvCktseyxxEPBi1Eiq4D2yBGACOnqLZpx1eMc7g5Og== | ||||||
|  |   dependencies: | ||||||
|  |     vue-demi ">=0.14.7" | ||||||
|  |  | ||||||
| "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": | "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": | ||||||
|   version "1.11.6" |   version "1.11.6" | ||||||
|   resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" |   resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" | ||||||
| @@ -20597,6 +20624,11 @@ vue-color@2.8.1: | |||||||
|     material-colors "^1.0.0" |     material-colors "^1.0.0" | ||||||
|     tinycolor2 "^1.1.2" |     tinycolor2 "^1.1.2" | ||||||
|  |  | ||||||
|  | vue-demi@>=0.14.7: | ||||||
|  |   version "0.14.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.8.tgz#00335e9317b45e4a68d3528aaf58e0cec3d5640a" | ||||||
|  |   integrity sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q== | ||||||
|  |  | ||||||
| vue-demi@^0.13.11: | vue-demi@^0.13.11: | ||||||
|   version "0.13.11" |   version "0.13.11" | ||||||
|   resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99" |   resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Sivin Varghese
					Sivin Varghese