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> | ||||
| import { mapGetters } from 'vuex'; | ||||
| import router from '../dashboard/routes'; | ||||
| import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal.vue'; | ||||
| import LoadingState from './components/widgets/LoadingState.vue'; | ||||
| import NetworkNotification from './components/NetworkNotification.vue'; | ||||
| @@ -43,6 +44,7 @@ import { | ||||
|   registerSubscription, | ||||
|   verifyServiceWorkerExistence, | ||||
| } from './helper/pushHelper'; | ||||
| import ReconnectService from 'dashboard/helper/ReconnectService'; | ||||
|  | ||||
| export default { | ||||
|   name: 'App', | ||||
| @@ -64,6 +66,7 @@ export default { | ||||
|     return { | ||||
|       showAddAccountModal: false, | ||||
|       latestChatwootVersion: null, | ||||
|       reconnectService: null, | ||||
|     }; | ||||
|   }, | ||||
|  | ||||
| @@ -102,6 +105,11 @@ export default { | ||||
|     this.listenToThemeChanges(); | ||||
|     this.setLocale(window.chatwootConfig.selectedLocale); | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     if (this.reconnectService) { | ||||
|       this.reconnectService.disconnect(); | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     initializeColorTheme() { | ||||
|       setColorTheme(window.matchMedia('(prefers-color-scheme: dark)').matches); | ||||
| @@ -125,6 +133,7 @@ export default { | ||||
|       this.updateRTLDirectionView(locale); | ||||
|       this.latestChatwootVersion = latestChatwootVersion; | ||||
|       vueActionCable.init(pubsubToken); | ||||
|       this.reconnectService = new ReconnectService(this.$store, router); | ||||
|  | ||||
|       verifyServiceWorkerExistence(registration => | ||||
|         registration.pushManager.getSubscription().then(subscription => { | ||||
|   | ||||
| @@ -9,6 +9,13 @@ class AccountAPI extends ApiClient { | ||||
|   createAccount(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(); | ||||
|   | ||||
| @@ -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> | ||||
|   <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 | ||||
|         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 | ||||
|           icon="wifi-off" | ||||
|           :icon="iconName" | ||||
|           class="text-yellow-700/50 dark:text-yellow-50" | ||||
|           size="18" | ||||
|         /> | ||||
|         <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> | ||||
|         <woot-button | ||||
|           v-if="canRefresh" | ||||
|           :title="$t('NETWORK.BUTTON.REFRESH')" | ||||
|           variant="clear" | ||||
|           size="small" | ||||
|           color-scheme="warning" | ||||
|           icon="arrow-clockwise" | ||||
|           class="visible transition-all duration-500 ease-in-out ml-1" | ||||
|           @click="refreshPage" | ||||
|         /> | ||||
|         <woot-button | ||||
| @@ -34,55 +141,3 @@ | ||||
|     </div> | ||||
|   </transition> | ||||
| </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 BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector'; | ||||
| import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper'; | ||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||
| import { emitter } from 'shared/helpers/mitt'; | ||||
|  | ||||
| class ActionCableConnector extends BaseActionCableConnector { | ||||
| @@ -33,34 +34,14 @@ class ActionCableConnector extends BaseActionCableConnector { | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   // eslint-disable-next-line class-methods-use-this | ||||
|   onReconnect = () => { | ||||
|     this.syncActiveConversationMessages(); | ||||
|     emitter.emit(BUS_EVENTS.WEBSOCKET_RECONNECT); | ||||
|   }; | ||||
|  | ||||
|   // eslint-disable-next-line class-methods-use-this | ||||
|   onDisconnected = () => { | ||||
|     this.setActiveConversationLastMessageId(); | ||||
|   }; | ||||
|  | ||||
|   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), | ||||
|       }); | ||||
|     } | ||||
|     emitter.emit(BUS_EVENTS.WEBSOCKET_DISCONNECT); | ||||
|   }; | ||||
|  | ||||
|   isAValidEvent = data => { | ||||
|   | ||||
| @@ -109,5 +109,14 @@ export const getConversationDashboardRoute = routeName => { | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const isAInboxViewRoute = routeName => | ||||
|   ['inbox_view_conversation'].includes(routeName); | ||||
| export const isAInboxViewRoute = (routeName, includeBase = false) => { | ||||
|   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_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": { | ||||
|     "NOTIFICATION": { | ||||
|       "OFFLINE": "Offline" | ||||
|       "OFFLINE": "Offline", | ||||
|       "RECONNECTING": "Reconnecting...", | ||||
|       "RECONNECT_SUCCESS": "Reconnected" | ||||
|     }, | ||||
|     "BUTTON": { | ||||
|       "REFRESH": "Refresh" | ||||
|   | ||||
| @@ -109,6 +109,10 @@ export const actions = { | ||||
|       // silent error | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   getCacheKeys: async () => { | ||||
|     return AccountAPI.getCacheKeys(); | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| export const mutations = { | ||||
|   | ||||
| @@ -41,11 +41,14 @@ export const mutations = { | ||||
|         newAllConversations[indexInCurrentList] = conversation; | ||||
|       } else { | ||||
|         // 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]; | ||||
|         newAllConversations[indexInCurrentList] = { | ||||
|           ...conversation, | ||||
|           allMessagesLoaded: existingConversation.allMessagesLoaded, | ||||
|           messages: existingConversation.messages, | ||||
|           dataFetched: existingConversation.dataFetched, | ||||
|           attachments: existingConversation.attachments, | ||||
|         }; | ||||
|       } | ||||
|     }); | ||||
|   | ||||
| @@ -307,9 +307,17 @@ describe('#mutations', () => { | ||||
|       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 = { | ||||
|         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, | ||||
|       }; | ||||
|       const data = [ | ||||
| @@ -317,10 +325,20 @@ describe('#mutations', () => { | ||||
|           id: 1, | ||||
|           name: 'test', | ||||
|           messages: [{ id: 1, content: 'updated message' }], | ||||
|           attachments: [{ id: 1, name: 'test.png' }], | ||||
|           dataFetched: true, | ||||
|           allMessagesLoaded: true, | ||||
|         }, | ||||
|       ]; | ||||
|       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); | ||||
|       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", | ||||
|   "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-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", | ||||
|   "subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z", | ||||
|   "table-switch-outline": [ | ||||
|   | ||||
| @@ -8,6 +8,8 @@ export const BUS_EVENTS = { | ||||
|   TOGGLE_SIDEMENU: 'TOGGLE_SIDEMENU', | ||||
|   ON_MESSAGE_LIST_SCROLL: 'ON_MESSAGE_LIST_SCROLL', | ||||
|   WEBSOCKET_DISCONNECT: 'WEBSOCKET_DISCONNECT', | ||||
|   WEBSOCKET_RECONNECT: 'WEBSOCKET_RECONNECT', | ||||
|   WEBSOCKET_RECONNECT_COMPLETED: 'WEBSOCKET_RECONNECT_COMPLETED', | ||||
|   TOGGLE_REPLY_TO_MESSAGE: 'TOGGLE_REPLY_TO_MESSAGE', | ||||
|   SHOW_TOAST: 'newToastMessage', | ||||
|   NEW_CONVERSATION_MODAL: 'newConversationModal', | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| import { createConsumer } from '@rails/actioncable'; | ||||
| import { BUS_EVENTS } from 'shared/constants/busEvents'; | ||||
| import { emitter } from 'shared/helpers/mitt'; | ||||
|  | ||||
| const PRESENCE_INTERVAL = 20000; | ||||
| const RECONNECT_INTERVAL = 1000; | ||||
| @@ -28,8 +26,6 @@ class BaseActionCableConnector { | ||||
|           BaseActionCableConnector.isDisconnected = true; | ||||
|           this.onDisconnected(); | ||||
|           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", | ||||
|     "@vuelidate/core": "^2.0.3", | ||||
|     "@vuelidate/validators": "^2.0.4", | ||||
|     "@vueuse/core": "^10.10.0", | ||||
|     "activestorage": "^5.2.6", | ||||
|     "autoprefixer": "^10.4.14", | ||||
|     "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" | ||||
|   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": | ||||
|   version "1.18.1" | ||||
|   resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.1.tgz#49699bb508961e14a3bfb68c78cd87b296889d1d" | ||||
| @@ -6818,6 +6823,28 @@ | ||||
|   dependencies: | ||||
|     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": | ||||
|   version "1.11.6" | ||||
|   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" | ||||
|     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: | ||||
|   version "0.13.11" | ||||
|   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