mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 03:27:52 +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