diff --git a/apps/meteor/client/components/AutoupdateToastMessage.tsx b/apps/meteor/client/components/AutoupdateToastMessage.tsx
new file mode 100644
index 00000000000..84e2fe45d66
--- /dev/null
+++ b/apps/meteor/client/components/AutoupdateToastMessage.tsx
@@ -0,0 +1,30 @@
+import { css } from '@rocket.chat/css-in-js';
+import { Box, Button } from '@rocket.chat/fuselage';
+import { useTranslation } from 'react-i18next';
+
+import { useIdleDetection } from '../hooks/useIdleDetection';
+
+export const AutoupdateToastMessage = () => {
+ const { t } = useTranslation();
+ useIdleDetection(
+ () => {
+ window.location.reload();
+ },
+ { awayOnWindowBlur: true },
+ );
+
+ return (
+
+ {t('An_update_is_available')}
+
+
+ );
+};
diff --git a/apps/meteor/client/hooks/useAutoupdate.spec.ts b/apps/meteor/client/hooks/useAutoupdate.spec.ts
new file mode 100644
index 00000000000..5e4aadee690
--- /dev/null
+++ b/apps/meteor/client/hooks/useAutoupdate.spec.ts
@@ -0,0 +1,54 @@
+import { useToastMessageDispatch } from '@rocket.chat/ui-contexts';
+import { renderHook } from '@testing-library/react';
+
+import { useAutoupdate } from './useAutoupdate';
+
+jest.mock('@rocket.chat/ui-contexts', () => ({
+ useToastMessageDispatch: jest.fn(() => jest.fn()),
+}));
+
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+describe('useAutoupdate', () => {
+ it('should add event listener to document on mount', () => {
+ const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
+ renderHook(() => useAutoupdate());
+ expect(addEventListenerSpy).toHaveBeenCalled();
+ expect(addEventListenerSpy).toHaveBeenCalledWith('client_changed', expect.any(Function));
+ });
+
+ it('should remove event listener on unmount', () => {
+ const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
+ const { unmount } = renderHook(() => useAutoupdate());
+ unmount();
+ expect(removeEventListenerSpy).toHaveBeenCalled();
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('client_changed', expect.any(Function));
+ });
+
+ it('should call toast function when client_changed event is fired', () => {
+ const toastMock = jest.fn();
+ (useToastMessageDispatch as jest.Mock).mockImplementation(() => toastMock);
+ renderHook(() => useAutoupdate());
+
+ const event = new Event('client_changed');
+ document.dispatchEvent(event);
+
+ expect(toastMock).toHaveBeenCalledTimes(1);
+ expect(toastMock).toHaveBeenCalledWith({
+ type: 'info',
+ message: expect.anything(),
+ options: { isPersistent: true },
+ });
+ });
+
+ it('should not call toast function when client_changed event is not fired', () => {
+ const toastMock = jest.fn();
+ (useToastMessageDispatch as jest.Mock).mockImplementation(() => toastMock);
+ renderHook(() => useAutoupdate());
+ expect(toastMock).not.toHaveBeenCalled();
+ });
+});
diff --git a/apps/meteor/client/hooks/useAutoupdate.tsx b/apps/meteor/client/hooks/useAutoupdate.tsx
new file mode 100644
index 00000000000..6d73dee3560
--- /dev/null
+++ b/apps/meteor/client/hooks/useAutoupdate.tsx
@@ -0,0 +1,32 @@
+import { useToastMessageDispatch } from '@rocket.chat/ui-contexts';
+import { useEffect } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { AutoupdateToastMessage } from '../components/AutoupdateToastMessage';
+
+export const useAutoupdate = () => {
+ const toast = useToastMessageDispatch();
+ const { t } = useTranslation();
+ const isDevMode = process.env.NODE_ENV === 'development';
+
+ useEffect(() => {
+ const fn = () => {
+ // To test this feature locally, comment the if statement below
+ if (isDevMode) {
+ window.location.reload();
+ return;
+ }
+ toast({
+ type: 'info',
+ options: { isPersistent: true },
+ message: ,
+ });
+ };
+
+ document.addEventListener('client_changed', fn);
+
+ return () => {
+ document.removeEventListener('client_changed', fn);
+ };
+ }, [isDevMode, t, toast]);
+};
diff --git a/apps/meteor/client/hooks/useIdleDetection.ts b/apps/meteor/client/hooks/useIdleDetection.ts
new file mode 100644
index 00000000000..326d0f3d5b8
--- /dev/null
+++ b/apps/meteor/client/hooks/useIdleDetection.ts
@@ -0,0 +1,56 @@
+import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
+import { useEffect } from 'react';
+
+const events = ['mousemove', 'mousedown', 'touchend', 'touchstart', 'keypress'];
+
+/**
+ * useIdleDetection is a custom hook that triggers a callback function when the user is detected to be idle.
+ * The idle state is determined based on the absence of certain user interactions for a specified time period.
+ *
+ * @param callback - The callback function to be called when the user is detected to be idle.
+ * @param options - An optional configuration object.
+ * @param options.time - The time in milliseconds to consider the user idle. Defaults to 600000 ms (10 minutes).
+ * @param options.awayOnWindowBlur - A boolean flag to trigger the callback when the window loses focus. Defaults to false.
+ *
+ */
+
+export const useIdleDetection = (callback: () => void, { time = 600000, awayOnWindowBlur = false } = {}) => {
+ const stableCallback = useEffectEvent(callback);
+
+ useEffect(() => {
+ let interval: ReturnType;
+ const handleIdle = () => {
+ clearTimeout(interval);
+ interval = setTimeout(() => {
+ document.dispatchEvent(new Event('idle'));
+ }, time);
+ };
+
+ handleIdle();
+
+ events.forEach((key) => document.addEventListener(key, handleIdle));
+ return () => {
+ clearTimeout(interval);
+ events.forEach((key) => document.removeEventListener(key, handleIdle));
+ };
+ }, [stableCallback, time]);
+
+ useEffect(() => {
+ if (!awayOnWindowBlur) {
+ return;
+ }
+
+ window.addEventListener('blur', stableCallback);
+ return () => {
+ window.removeEventListener('blur', stableCallback);
+ };
+ }, [awayOnWindowBlur, stableCallback]);
+
+ useEffect(() => {
+ document.addEventListener('idle', stableCallback);
+
+ return () => {
+ document.removeEventListener('idle', stableCallback);
+ };
+ }, [stableCallback]);
+};
diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx
index 5a3bab6d6b3..23a1373f6cf 100644
--- a/apps/meteor/client/views/root/AppLayout.tsx
+++ b/apps/meteor/client/views/root/AppLayout.tsx
@@ -20,6 +20,7 @@ import { useLivechatEnterprise } from '../../../app/livechat-enterprise/hooks/us
import { useNextcloud } from '../../../app/nextcloud/client/useNextcloud';
import { useTokenPassAuth } from '../../../app/tokenpass/client/hooks/useTokenPassAuth';
import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking';
+import { useAutoupdate } from '../../hooks/useAutoupdate';
import { useLoadRoomForAllowedAnonymousRead } from '../../hooks/useLoadRoomForAllowedAnonymousRead';
import { useNotifyUser } from '../../hooks/useNotifyUser';
import { appLayout } from '../../lib/appLayout';
@@ -57,6 +58,7 @@ const AppLayout = () => {
useOTRMessaging();
useUpdateVideoConfUser();
useStoreCookiesOnLogin();
+ useAutoupdate();
const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot);
diff --git a/apps/meteor/packages/autoupdate/autoupdate_client.js b/apps/meteor/packages/autoupdate/autoupdate_client.js
index dfd924c7f1c..c7b07ca85eb 100644
--- a/apps/meteor/packages/autoupdate/autoupdate_client.js
+++ b/apps/meteor/packages/autoupdate/autoupdate_client.js
@@ -117,9 +117,7 @@ Autoupdate._retrySubscription = () => {
doc.versionNonRefreshable,
`Page will reload in ${reloadDelayInSeconds} seconds`,
);
- setTimeout(() => {
- Package.reload.Reload._reload();
- }, reloadDelayInSeconds * 1000);
+ document.dispatchEvent(new Event('client_changed'));
}
return;
}
diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json
index b47f90547c2..3fce23220d0 100644
--- a/packages/i18n/src/locales/en.i18n.json
+++ b/packages/i18n/src/locales/en.i18n.json
@@ -6787,5 +6787,7 @@
"Recent": "Recent",
"On_All_Contacts": "On All Contacts",
"Once": "Once",
- "__unreadTitle__from__roomTitle__": "{{unreadTitle}} from {{roomTitle}}"
-}
\ No newline at end of file
+ "__unreadTitle__from__roomTitle__": "{{unreadTitle}} from {{roomTitle}}",
+ "An_update_is_available": "An update is available",
+ "Reload_to_update": "Reload to update"
+}