diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 3d58267ed70..7f6f660851c 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -1,9 +1,6 @@ # Meteor packages used by this project, one per line. - # - # 'meteor add' and 'meteor remove' will edit this file for you, - # but you can also edit it by hand. rocketchat:ddp @@ -11,7 +8,6 @@ rocketchat:mongo-config rocketchat:livechat rocketchat:streamer rocketchat:version -rocketchat:user-presence accounts-base@3.1.1 accounts-facebook@1.3.4 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 1c509ae85eb..2c3b80cb7b7 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -74,7 +74,6 @@ rocketchat:ddp@0.0.1 rocketchat:livechat@0.0.1 rocketchat:mongo-config@0.0.1 rocketchat:streamer@1.1.0 -rocketchat:user-presence@2.6.3 rocketchat:version@1.0.0 routepolicy@1.1.2 service-configuration@1.3.5 diff --git a/apps/meteor/client/lib/userPresence.ts b/apps/meteor/client/lib/userPresence.ts new file mode 100644 index 00000000000..31ed7570f32 --- /dev/null +++ b/apps/meteor/client/lib/userPresence.ts @@ -0,0 +1,107 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; +import { useConnectionStatus, useIsLoggingIn, useMethod, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { withDebouncing } from '../../lib/utils/highOrderFunctions'; +import { Users } from '../stores'; + +// TODO: merge this with the current React-based implementation of idle detection + +export class UserPresence { + private user: IUser | undefined; + + private timer: ReturnType | undefined; + + private status: UserStatus | undefined; + + private awayTime: number | undefined = 60_000; + + private connected = true; + + private goOnline: () => Promise = async () => undefined; + + private goAway: () => Promise = async () => undefined; + + private storeUser: (doc: IUser) => void = () => undefined; + + private startTimer() { + this.stopTimer(); + if (!this.awayTime) return; + + this.timer = setTimeout(this.setAway, this.awayTime); + } + + private stopTimer() { + clearTimeout(this.timer); + } + + private readonly setOnline = () => this.setStatus(UserStatus.ONLINE); + + private readonly setAway = () => this.setStatus(UserStatus.AWAY); + + private readonly setStatus = withDebouncing({ wait: 1000 })(async (newStatus: UserStatus.ONLINE | UserStatus.AWAY) => { + if (!this.connected || newStatus === this.status) { + this.startTimer(); + return; + } + + if (this.user?.status !== newStatus && this.user?.statusDefault === newStatus) { + this.storeUser({ ...this.user, status: newStatus }); + } + + switch (newStatus) { + case UserStatus.ONLINE: + await this.goOnline(); + break; + + case UserStatus.AWAY: + await this.goAway(); + this.stopTimer(); + break; + } + + this.status = newStatus; + }); + + readonly use = () => { + const user = useUser() ?? undefined; + const { connected } = useConnectionStatus(); + const isLoggingIn = useIsLoggingIn(); + const enableAutoAway = useUserPreference('enableAutoAway'); + const idleTimeLimit = useUserPreference('idleTimeLimit') ?? 300; + + this.user = user; + this.connected = connected; + this.awayTime = enableAutoAway ? idleTimeLimit * 1000 : undefined; + this.goOnline = useMethod('UserPresence:online'); + this.goAway = useMethod('UserPresence:away'); + this.storeUser = Users.use((state) => state.store); + + useEffect(() => { + const documentEvents = ['mousemove', 'mousedown', 'touchend', 'keydown'] as const; + documentEvents.forEach((key) => document.addEventListener(key, this.setOnline)); + window.addEventListener('focus', this.setOnline); + + return () => { + documentEvents.forEach((key) => document.removeEventListener(key, this.setOnline)); + window.removeEventListener('focus', this.setOnline); + }; + }, []); + + useEffect(() => { + if (!user || !connected || isLoggingIn) return; + this.startTimer(); + }, [connected, isLoggingIn, user]); + + useEffect(() => { + if (connected) { + this.startTimer(); + this.status = UserStatus.ONLINE; + return; + } + this.stopTimer(); + this.status = UserStatus.OFFLINE; + }, [connected]); + }; +} diff --git a/apps/meteor/client/providers/UserPresenceProvider.tsx b/apps/meteor/client/providers/UserPresenceProvider.tsx index 6936b106e38..8cf1322c9ae 100644 --- a/apps/meteor/client/providers/UserPresenceProvider.tsx +++ b/apps/meteor/client/providers/UserPresenceProvider.tsx @@ -1,15 +1,20 @@ import type { UserPresenceContextValue } from '@rocket.chat/ui-contexts'; import { useSetting, UserPresenceContext } from '@rocket.chat/ui-contexts'; -import type { ReactElement, ReactNode } from 'react'; +import type { ReactNode } from 'react'; import { useMemo, useEffect } from 'react'; import { Presence } from '../lib/presence'; +import { UserPresence } from '../lib/userPresence'; + +const userPresence = new UserPresence(); type UserPresenceProviderProps = { children?: ReactNode; }; -const UserPresenceProvider = ({ children }: UserPresenceProviderProps): ReactElement => { +const UserPresenceProvider = ({ children }: UserPresenceProviderProps) => { + userPresence.use(); + const usePresenceDisabled = useSetting('Presence_broadcast_disabled', false); useEffect(() => { diff --git a/apps/meteor/client/startup/startup.ts b/apps/meteor/client/startup/startup.ts index d582ffcba18..21747fc94e6 100644 --- a/apps/meteor/client/startup/startup.ts +++ b/apps/meteor/client/startup/startup.ts @@ -1,10 +1,8 @@ import type { UserStatus } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; -import { UserPresence } from 'meteor/rocketchat:user-presence'; import { Tracker } from 'meteor/tracker'; import moment from 'moment'; -import { getUserPreference } from '../../app/utils/client'; import 'highlight.js/styles/github.css'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { synchronizeUserData, removeLocalUserData } from '../lib/userData'; @@ -13,9 +11,6 @@ import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent'; Meteor.startup(() => { fireGlobalEvent('startup', true); - window.lastMessageWindow = {}; - window.lastMessageWindowHistory = {}; - let status: UserStatus | undefined = undefined; Tracker.autorun(async () => { const uid = Meteor.userId(); @@ -42,16 +37,6 @@ Meteor.startup(() => { sdk.call('userSetUtcOffset', utcOffset); } - if (getUserPreference(user, 'enableAutoAway')) { - const idleTimeLimit = (getUserPreference(user, 'idleTimeLimit') as number | null | undefined) || 300; - UserPresence.awayTime = idleTimeLimit * 1000; - } else { - delete UserPresence.awayTime; - UserPresence.stopTimer(); - } - - UserPresence.start(); - if (user.status !== status) { status = user.status; fireGlobalEvent('status-changed', status); diff --git a/apps/meteor/definition/externals/meteor/konecty-user-presence.d.ts b/apps/meteor/definition/externals/meteor/konecty-user-presence.d.ts deleted file mode 100644 index 57ab90d07e7..00000000000 --- a/apps/meteor/definition/externals/meteor/konecty-user-presence.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -declare module 'meteor/rocketchat:user-presence' { - namespace UserPresenceMonitor { - function processUserSession(userSession: any, event: string): void; - function onSetUserStatus(callback: any): void; - } - - namespace UserPresence { - let awayTime: number | undefined; - function removeConnectionsByInstanceId(id: string): void; - function start(): void; - function stopTimer(): void; - function setDefaultStatus(userId: string, status: string): void; - } -} diff --git a/apps/meteor/packages/meteor-user-presence/.gitignore b/apps/meteor/packages/meteor-user-presence/.gitignore deleted file mode 100644 index 3d0583d5c7a..00000000000 --- a/apps/meteor/packages/meteor-user-presence/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.npm/ \ No newline at end of file diff --git a/apps/meteor/packages/meteor-user-presence/client/client.js b/apps/meteor/packages/meteor-user-presence/client/client.js deleted file mode 100644 index 6d115db2a4a..00000000000 --- a/apps/meteor/packages/meteor-user-presence/client/client.js +++ /dev/null @@ -1,113 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; -import { check } from 'meteor/check'; - -import { debounce } from './utils'; - -let timer; -let status; - -export const UserPresence = { - awayTime: 60000, // 1 minute - awayOnWindowBlur: false, - callbacks: [], - connected: true, - started: false, - userId: null, - - /** - * The callback will receive the following parameters: user, status - */ - onSetUserStatus(callback) { - this.callbacks.push(callback); - }, - - runCallbacks(user, status) { - this.callbacks.forEach(function (callback) { - callback.call(null, user, status); - }); - }, - - startTimer() { - UserPresence.stopTimer(); - if (!UserPresence.awayTime) { - return; - } - timer = setTimeout(UserPresence.setAway, UserPresence.awayTime); - }, - stopTimer() { - clearTimeout(timer); - }, - restartTimer() { - UserPresence.startTimer(); - }, - // eslint-disable-next-line no-use-before-define - setAway: () => setUserPresence('away'), - // eslint-disable-next-line no-use-before-define - setOnline: () => setUserPresence('online'), - start(userId) { - // after first call overwrite start function to only call startTimer - this.start = () => { - this.startTimer(); - }; - this.userId = userId; - - // register a tracker on connection status so we can setup the away timer again (on reconnect) - Tracker.autorun(() => { - const { connected } = Meteor.status(); - this.connected = connected; - if (connected) { - this.startTimer(); - status = 'online'; - return; - } - this.stopTimer(); - status = 'offline'; - }); - - ['mousemove', 'mousedown', 'touchend', 'keydown'].forEach((key) => document.addEventListener(key, this.setOnline)); - - window.addEventListener('focus', this.setOnline); - - if (this.awayOnWindowBlur === true) { - window.addEventListener('blur', this.setAway); - } - }, -}; - -const setUserPresence = debounce(async (newStatus) => { - if (!UserPresence.connected || newStatus === status) { - UserPresence.startTimer(); - return; - } - switch (newStatus) { - case 'online': - await Meteor.callAsync('UserPresence:online', UserPresence.userId); - break; - case 'away': - await Meteor.callAsync('UserPresence:away', UserPresence.userId); - UserPresence.stopTimer(); - break; - default: - return; - } - status = newStatus; -}, 1000); - -Meteor.methods({ - async 'UserPresence:setDefaultStatus'(status) { - check(status, String); - await Meteor.users.updateAsync({ _id: Meteor.userId() }, { $set: { status, statusDefault: status } }); - }, - async 'UserPresence:online'() { - const user = await Meteor.userAsync(); - if (user && user.status !== 'online' && user.statusDefault === 'online') { - await Meteor.users.updateAsync({ _id: Meteor.userId() }, { $set: { status: 'online' } }); - } - UserPresence.runCallbacks(user, 'online'); - }, - async 'UserPresence:away'() { - const user = await Meteor.userAsync(); - UserPresence.runCallbacks(user, 'away'); - }, -}); diff --git a/apps/meteor/packages/meteor-user-presence/client/utils.js b/apps/meteor/packages/meteor-user-presence/client/utils.js deleted file mode 100644 index 2ffcb42fccc..00000000000 --- a/apps/meteor/packages/meteor-user-presence/client/utils.js +++ /dev/null @@ -1,8 +0,0 @@ -export function debounce(func, wait) { - let timeout; - - return (...args) => { - if (timeout) clearTimeout(timeout); - timeout = setTimeout(() => func(...args), wait); - }; -} diff --git a/apps/meteor/packages/meteor-user-presence/package.js b/apps/meteor/packages/meteor-user-presence/package.js deleted file mode 100644 index 700c67cdf73..00000000000 --- a/apps/meteor/packages/meteor-user-presence/package.js +++ /dev/null @@ -1,18 +0,0 @@ -Package.describe({ - name: 'rocketchat:user-presence', - summary: 'Track user status', - version: '2.6.3', - git: 'https://github.com/Konecty/meteor-user-presence', -}); - -Package.onUse(function (api) { - api.use('tracker'); - api.use('check'); - api.use('ecmascript'); - - api.mainModule('client/client.js', 'client'); -}); - -Npm.depends({ - colors: '1.3.2', -});