chore: User presence relocation (#36704)

pull/36617/head^2
Tasso Evangelista 5 months ago committed by GitHub
parent 1045010054
commit 83eeed0a8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      apps/meteor/.meteor/packages
  2. 1
      apps/meteor/.meteor/versions
  3. 107
      apps/meteor/client/lib/userPresence.ts
  4. 9
      apps/meteor/client/providers/UserPresenceProvider.tsx
  5. 15
      apps/meteor/client/startup/startup.ts
  6. 14
      apps/meteor/definition/externals/meteor/konecty-user-presence.d.ts
  7. 1
      apps/meteor/packages/meteor-user-presence/.gitignore
  8. 113
      apps/meteor/packages/meteor-user-presence/client/client.js
  9. 8
      apps/meteor/packages/meteor-user-presence/client/utils.js
  10. 18
      apps/meteor/packages/meteor-user-presence/package.js

@ -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

@ -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

@ -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<typeof setTimeout> | undefined;
private status: UserStatus | undefined;
private awayTime: number | undefined = 60_000;
private connected = true;
private goOnline: () => Promise<boolean | undefined> = async () => undefined;
private goAway: () => Promise<boolean | undefined> = 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<boolean>('enableAutoAway');
const idleTimeLimit = useUserPreference<number>('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]);
};
}

@ -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(() => {

@ -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);

@ -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;
}
}

@ -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');
},
});

@ -1,8 +0,0 @@
export function debounce(func, wait) {
let timeout;
return (...args) => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}

@ -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',
});
Loading…
Cancel
Save