diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue
index 481c671ce..c343fbe4f 100644
--- a/app/javascript/dashboard/App.vue
+++ b/app/javascript/dashboard/App.vue
@@ -27,6 +27,7 @@
+
-
+
- {{ $t('NETWORK.NOTIFICATION.OFFLINE') }}
+ {{ bannerText }}
-
-
diff --git a/app/javascript/dashboard/composables/emitter.js b/app/javascript/dashboard/composables/emitter.js
new file mode 100644
index 000000000..c547099ab
--- /dev/null
+++ b/app/javascript/dashboard/composables/emitter.js
@@ -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 };
diff --git a/app/javascript/dashboard/composables/spec/emitter.spec.js b/app/javascript/dashboard/composables/spec/emitter.spec.js
new file mode 100644
index 000000000..c732b7c58
--- /dev/null
+++ b/app/javascript/dashboard/composables/spec/emitter.spec.js
@@ -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: `
+
+ Hello world
+
+ `,
+ 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);
+ });
+});
diff --git a/app/javascript/dashboard/helper/ReconnectService.js b/app/javascript/dashboard/helper/ReconnectService.js
new file mode 100644
index 000000000..d2058073c
--- /dev/null
+++ b/app/javascript/dashboard/helper/ReconnectService.js
@@ -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;
diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js
index 3b61695db..729f298b7 100644
--- a/app/javascript/dashboard/helper/actionCable.js
+++ b/app/javascript/dashboard/helper/actionCable.js
@@ -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 => {
diff --git a/app/javascript/dashboard/helper/routeHelpers.js b/app/javascript/dashboard/helper/routeHelpers.js
index 86c286368..68cf0a627 100644
--- a/app/javascript/dashboard/helper/routeHelpers.js
+++ b/app/javascript/dashboard/helper/routeHelpers.js
@@ -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';
diff --git a/app/javascript/dashboard/helper/specs/ReconnectService.spec.js b/app/javascript/dashboard/helper/specs/ReconnectService.spec.js
new file mode 100644
index 000000000..1d0878d4d
--- /dev/null
+++ b/app/javascript/dashboard/helper/specs/ReconnectService.spec.js
@@ -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
+ );
+ });
+ });
+});
diff --git a/app/javascript/dashboard/helper/specs/routeHelpers.spec.js b/app/javascript/dashboard/helper/specs/routeHelpers.spec.js
index 40467709e..1e100a678 100644
--- a/app/javascript/dashboard/helper/specs/routeHelpers.spec.js
+++ b/app/javascript/dashboard/helper/specs/routeHelpers.spec.js
@@ -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);
+ });
});
diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
index e7b740f45..bba50316c 100644
--- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json
+++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
@@ -95,7 +95,9 @@
},
"NETWORK": {
"NOTIFICATION": {
- "OFFLINE": "Offline"
+ "OFFLINE": "Offline",
+ "RECONNECTING": "Reconnecting...",
+ "RECONNECT_SUCCESS": "Reconnected"
},
"BUTTON": {
"REFRESH": "Refresh"
diff --git a/app/javascript/dashboard/store/modules/accounts.js b/app/javascript/dashboard/store/modules/accounts.js
index 3b67c69dc..c757b7fcb 100644
--- a/app/javascript/dashboard/store/modules/accounts.js
+++ b/app/javascript/dashboard/store/modules/accounts.js
@@ -109,6 +109,10 @@ export const actions = {
// silent error
}
},
+
+ getCacheKeys: async () => {
+ return AccountAPI.getCacheKeys();
+ },
};
export const mutations = {
diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js
index adfb74202..769354ed2 100644
--- a/app/javascript/dashboard/store/modules/conversations/index.js
+++ b/app/javascript/dashboard/store/modules/conversations/index.js
@@ -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,
};
}
});
diff --git a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js
index 2694e3456..b10a24ea0 100644
--- a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js
@@ -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);
diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json
index f58c2a7dd..561129c00 100644
--- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json
+++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json
@@ -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": [
diff --git a/app/javascript/shared/constants/busEvents.js b/app/javascript/shared/constants/busEvents.js
index dc6892e26..2229a8b4c 100644
--- a/app/javascript/shared/constants/busEvents.js
+++ b/app/javascript/shared/constants/busEvents.js
@@ -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',
diff --git a/app/javascript/shared/helpers/BaseActionCableConnector.js b/app/javascript/shared/helpers/BaseActionCableConnector.js
index 270a2b809..3eb61a80a 100644
--- a/app/javascript/shared/helpers/BaseActionCableConnector.js
+++ b/app/javascript/shared/helpers/BaseActionCableConnector.js
@@ -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);
},
}
);
diff --git a/package.json b/package.json
index bfce2e0be..0163878aa 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/yarn.lock b/yarn.lock
index 288115117..538954eda 100644
--- a/yarn.lock
+++ b/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"