diff --git a/apps/meteor/app/lib/client/index.ts b/apps/meteor/app/lib/client/index.ts index 35ec26a6f1dfd..2769be7fe5764 100644 --- a/apps/meteor/app/lib/client/index.ts +++ b/apps/meteor/app/lib/client/index.ts @@ -1,5 +1,3 @@ import '../lib/MessageTypes'; import './OAuthProxy'; import './methods/sendMessage'; - -export * from './lib'; diff --git a/apps/meteor/app/lib/client/lib/LoginPresence.ts b/apps/meteor/app/lib/client/lib/LoginPresence.ts deleted file mode 100644 index cea1f982012f9..0000000000000 --- a/apps/meteor/app/lib/client/lib/LoginPresence.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { settings } from '../../../settings/client'; - -class LoginPresence { - private awayTime = 600_000; // 10 minutes - - private started = false; - - private timer: ReturnType; - - private startTimer(): void { - this.stopTimer(); - if (!this.awayTime) { - return; - } - this.timer = setTimeout(() => this.disconnect(), this.awayTime); - } - - private stopTimer(): void { - clearTimeout(this.timer); - } - - private disconnect(): void { - const status = Meteor.status(); - if (status && status.status !== 'offline') { - if (!Meteor.userId() && settings.get('Accounts_AllowAnonymousRead') !== true) { - Meteor.disconnect(); - } - } - this.stopTimer(); - } - - private connect(): void { - const status = Meteor.status(); - if (status && status.status === 'offline') { - Meteor.reconnect(); - } - } - - public start(): void { - if (this.started) { - return; - } - - window.addEventListener('focus', () => { - this.stopTimer(); - this.connect(); - }); - - window.addEventListener('blur', () => { - this.startTimer(); - }); - - if (!window.document.hasFocus()) { - this.startTimer(); - } - - this.started = true; - } -} - -const instance = new LoginPresence(); - -instance.start(); - -export { instance as LoginPresence }; diff --git a/apps/meteor/app/lib/client/lib/index.ts b/apps/meteor/app/lib/client/lib/index.ts deleted file mode 100644 index 847fde4009808..0000000000000 --- a/apps/meteor/app/lib/client/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LoginPresence } from './LoginPresence'; diff --git a/apps/meteor/client/components/AutoupdateToastMessage.tsx b/apps/meteor/client/components/AutoupdateToastMessage.tsx index 84e2fe45d66d2..a3cd43d47cbc4 100644 --- a/apps/meteor/client/components/AutoupdateToastMessage.tsx +++ b/apps/meteor/client/components/AutoupdateToastMessage.tsx @@ -2,16 +2,13 @@ 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'; +import { useIdleActiveEvents } from '../hooks/useIdleActiveEvents'; export const AutoupdateToastMessage = () => { const { t } = useTranslation(); - useIdleDetection( - () => { - window.location.reload(); - }, - { awayOnWindowBlur: true }, - ); + useIdleActiveEvents({ id: 'autoupdate', awayOnWindowBlur: true }, () => { + window.location.reload(); + }); return ( void, + onActiveCallback?: () => void, +) => { + const stableIdleCallback = useEffectEvent(onIdleCallback); + const stableActiveCallback = useEffectEvent(onActiveCallback || (() => undefined)); + + useEffect(() => { + document.addEventListener(`${id}_idle`, stableIdleCallback); + + onActiveCallback && document.addEventListener(`${id}_active`, stableActiveCallback); + + return () => { + document.removeEventListener(`${id}_idle`, stableIdleCallback); + document.removeEventListener(`${id}_active`, stableActiveCallback); + }; + }, [id, onActiveCallback, stableActiveCallback, stableIdleCallback]); + + return useIdleDetection({ id, time, awayOnWindowBlur }); +}; diff --git a/apps/meteor/client/hooks/useIdleConnection.ts b/apps/meteor/client/hooks/useIdleConnection.ts new file mode 100644 index 0000000000000..e6b7336236384 --- /dev/null +++ b/apps/meteor/client/hooks/useIdleConnection.ts @@ -0,0 +1,28 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { ServerContext, useConnectionStatus, useSetting, useUserId } from '@rocket.chat/ui-contexts'; +import { useContext } from 'react'; + +import { useIdleActiveEvents } from './useIdleActiveEvents'; + +export const useIdleConnection = () => { + const uid = useUserId(); + const { status } = useConnectionStatus(); + const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead'); + const { disconnect: disconnectServer, reconnect: reconnectServer } = useContext(ServerContext); + + const disconnect = useEffectEvent(() => { + if (status === 'offline') { + if (!uid && allowAnonymousRead !== true) { + disconnectServer(); + } + } + }); + + const reconnect = useEffectEvent(() => { + if (status === 'offline') { + reconnectServer(); + } + }); + + useIdleActiveEvents({ id: 'useLoginPresence', time: 3000, awayOnWindowBlur: true }, disconnect, reconnect); +}; diff --git a/apps/meteor/client/hooks/useIdleDetection.ts b/apps/meteor/client/hooks/useIdleDetection.ts index 326d0f3d5b85e..16dbed9570658 100644 --- a/apps/meteor/client/hooks/useIdleDetection.ts +++ b/apps/meteor/client/hooks/useIdleDetection.ts @@ -1,28 +1,58 @@ -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useEffect } from 'react'; +import { useEffect, useReducer } from 'react'; const events = ['mousemove', 'mousedown', 'touchend', 'touchstart', 'keypress']; +type UseIdleDetectionOptions = { + id?: string; + time?: number; + awayOnWindowBlur?: boolean; +}; + /** - * 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. + * A hook that detects when the user is idle. * - * @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. + * This hook listens for mousemove, mousedown, touchend, touchstart, and keypress events. + * When any of these events are triggered, the user is considered active. + * If no events are triggered for a specified period of time, the user is considered idle. * + * @param {object} options - An object with the following properties: + * @param {string} options.id - A unique identifier for the idle detection mechanism. Defaults to 'useIdleDetection'. + * @param {number} options.time - The time in milliseconds to consider the user idle. Defaults to 600000 ms (10 minutes). + * @param {boolean} options.awayOnWindowBlur - A boolean flag to trigger the idle state when the window loses focus. Defaults to false. + * + * @returns {boolean} A boolean indicating whether the user is idle or not. */ -export const useIdleDetection = (callback: () => void, { time = 600000, awayOnWindowBlur = false } = {}) => { - const stableCallback = useEffectEvent(callback); +export const useIdleDetection = ({ id = 'useIdleDetection', time = 600000, awayOnWindowBlur = false }: UseIdleDetectionOptions = {}) => { + const [isIdle, dispatch] = useReducer((state: boolean, action: boolean) => { + if (state === action) { + return state; + } + + if (action) { + document.dispatchEvent(new Event(`${id}_idle`)); + } + + if (!action) { + document.dispatchEvent(new Event(`${id}_active`)); + } + + document.dispatchEvent( + new CustomEvent(`${id}_change`, { + detail: { isIdle: action }, + }), + ); + + return action; + }, false); useEffect(() => { let interval: ReturnType; const handleIdle = () => { + dispatch(false); clearTimeout(interval); interval = setTimeout(() => { - document.dispatchEvent(new Event('idle')); + dispatch(true); }, time); }; @@ -33,24 +63,19 @@ export const useIdleDetection = (callback: () => void, { time = 600000, awayOnWi clearTimeout(interval); events.forEach((key) => document.removeEventListener(key, handleIdle)); }; - }, [stableCallback, time]); + }, [time]); useEffect(() => { if (!awayOnWindowBlur) { return; } - window.addEventListener('blur', stableCallback); + const dispatchIdle = () => dispatch(true); + window.addEventListener('blur', dispatchIdle); return () => { - window.removeEventListener('blur', stableCallback); + window.removeEventListener('blur', dispatchIdle); }; - }, [awayOnWindowBlur, stableCallback]); + }, [awayOnWindowBlur]); - useEffect(() => { - document.addEventListener('idle', stableCallback); - - return () => { - document.removeEventListener('idle', stableCallback); - }; - }, [stableCallback]); + return isIdle; }; diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index adbbc07fae54f..619f9d3a14b2b 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -15,6 +15,7 @@ import { Subscriptions, Rooms } from '../../../app/models/client'; import { getUserPreference } from '../../../app/utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback'; +import { useIdleConnection } from '../../hooks/useIdleConnection'; import { useReactiveValue } from '../../hooks/useReactiveValue'; import { createReactiveSubscriptionFactory } from '../../lib/createReactiveSubscriptionFactory'; import { useCreateFontStyleElement } from '../../views/account/accessibility/hooks/useCreateFontStyleElement'; @@ -62,6 +63,7 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => { useDeleteUser(); useUpdateAvatar(); + useIdleConnection(); const contextValue = useMemo( (): ContextType => ({