From 7380c44c751eff9ee624d80bf26370411ffed78b Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Tue, 16 Jun 2026 07:50:58 -0300 Subject: [PATCH 01/11] feat: presence sync engine (#40274) --- .changeset/full-results-share.md | 11 + .../api/server/helpers/getUserFromParams.ts | 10 +- apps/meteor/app/api/server/v1/users.ts | 75 ++-- apps/meteor/app/apps/server/bridges/users.ts | 23 +- .../lib/server/functions/getFullUserData.ts | 2 + .../app/slashcommands-status/server/status.ts | 11 +- .../server/methods/setUserStatus.ts | 33 +- .../server/functions/getBaseUserFields.ts | 2 + apps/meteor/server/methods/saveUserProfile.ts | 8 +- .../modules/listeners/listeners.module.ts | 19 +- .../notifications/notifications.module.ts | 7 +- .../tests/data/apps/app-packages/index.ts | 2 + .../presence-state-test_0.0.1.zip | Bin 0 -> 16565 bytes apps/meteor/tests/end-to-end/api/users.ts | 86 ++++- .../tests/end-to-end/apps/presence-state.ts | 74 ++++ .../end-to-end/apps/update-status-text.ts | 11 +- ee/packages/presence/src/Presence.spec.ts | 250 +++++++++++++ ee/packages/presence/src/Presence.ts | 191 +++++++--- .../presence/src/lib/normalizeStatusText.ts | 5 + .../presence/src/lib/presenceEngine.spec.ts | 334 ++++++++++++++++++ .../presence/src/lib/presenceEngine.ts | 208 +++++++++++ .../src/lib/processConnectionStatus.ts | 47 --- .../tests/lib/processConnectionStatus.test.ts | 240 ------------- .../src/definition/accessors/IUserUpdater.ts | 5 + .../apps-engine/src/definition/users/IUser.ts | 3 + .../apps/src/server/accessors/UserUpdater.ts | 11 + .../apps/src/server/bridges/UserBridge.ts | 24 ++ .../tests/test-data/bridges/userBridge.ts | 12 + packages/core-services/src/events/Events.ts | 2 +- packages/core-services/src/types/IPresence.ts | 11 +- packages/core-typings/src/IMeApiUser.ts | 2 + packages/core-typings/src/IUser.ts | 10 +- packages/core-typings/src/UserStatus.ts | 2 + packages/ddp-client/src/types/streams.ts | 18 +- .../model-typings/src/models/IUsersModel.ts | 25 ++ packages/models/src/models/Users.ts | 69 +++- packages/rest-typings/src/v1/users.ts | 14 +- 37 files changed, 1414 insertions(+), 443 deletions(-) create mode 100644 .changeset/full-results-share.md create mode 100644 apps/meteor/tests/data/apps/app-packages/presence-state-test_0.0.1.zip create mode 100644 apps/meteor/tests/end-to-end/apps/presence-state.ts create mode 100644 ee/packages/presence/src/Presence.spec.ts create mode 100644 ee/packages/presence/src/lib/normalizeStatusText.ts create mode 100644 ee/packages/presence/src/lib/presenceEngine.spec.ts create mode 100644 ee/packages/presence/src/lib/presenceEngine.ts delete mode 100644 ee/packages/presence/src/lib/processConnectionStatus.ts delete mode 100644 ee/packages/presence/tests/lib/processConnectionStatus.test.ts diff --git a/.changeset/full-results-share.md b/.changeset/full-results-share.md new file mode 100644 index 0000000000000..2b681e3cecebe --- /dev/null +++ b/.changeset/full-results-share.md @@ -0,0 +1,11 @@ +--- +'@rocket.chat/presence-service': minor +'@rocket.chat/core-services': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/presence': minor +'@rocket.chat/models': patch +'@rocket.chat/meteor': minor +--- + +Adds the backend foundation for a unified presence engine with a priority-based claim system (internal > manual > external), status expiration, and previous state restore. diff --git a/apps/meteor/app/api/server/helpers/getUserFromParams.ts b/apps/meteor/app/api/server/helpers/getUserFromParams.ts index 984658371da2c..2ac656c1ecb76 100644 --- a/apps/meteor/app/api/server/helpers/getUserFromParams.ts +++ b/apps/meteor/app/api/server/helpers/getUserFromParams.ts @@ -10,10 +10,16 @@ export async function getUserFromParams( user?: string; }, full?: T, -): Promise> { +): Promise< + T extends true + ? IUser + : Pick +> { let user; - const projection = full ? {} : { username: 1, name: 1, status: 1, statusText: 1, roles: 1 }; + const projection = full + ? {} + : { username: 1, name: 1, status: 1, statusDefault: 1, statusText: 1, statusSource: 1, statusExpiresAt: 1, roles: 1 }; if (params.userId?.trim()) { user = await Users.findOneById(params.userId, { projection }); } else if (params.username?.trim()) { diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index fbc46b9579a67..e1d56b97cb738 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -1,4 +1,4 @@ -import { MeteorError, Team, api, Calendar } from '@rocket.chat/core-services'; +import { MeteorError, Presence, Team, Calendar } from '@rocket.chat/core-services'; import type { IExportOperation, ILoginToken, IPersonalAccessToken, IUser, UserStatus } from '@rocket.chat/core-typings'; import { Users, Subscriptions, Sessions, OAuthAccessTokens, OAuthRefreshTokens, OAuthAuthCodes } from '@rocket.chat/models'; import { @@ -67,7 +67,6 @@ import { saveCustomFieldsWithoutValidation } from '../../../lib/server/functions import { saveUser } from '../../../lib/server/functions/saveUser'; import { sendWelcomeEmail } from '../../../lib/server/functions/saveUser/sendUserEmail'; import { canEditExtension } from '../../../lib/server/functions/saveUser/validateUserEditing'; -import { setStatusText } from '../../../lib/server/functions/setStatusText'; import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername'; import { validateCustomFields } from '../../../lib/server/functions/validateCustomFields'; @@ -1539,6 +1538,8 @@ API.v1.get( status: 1, utcOffset: 1, statusText: 1, + statusSource: 1, + statusExpiresAt: 1, avatarETag: 1, }, }; @@ -1943,6 +1944,7 @@ API.v1 body: ajv.compile<{ status?: UserStatus; message?: string; + expiresAt?: string; userId?: string; username?: string; user?: string; @@ -1951,6 +1953,7 @@ API.v1 properties: { status: { type: 'string', enum: ['online', 'away', 'offline', 'busy'] }, message: { type: 'string', nullable: true }, + expiresAt: { type: 'string', nullable: true }, userId: { type: 'string' }, username: { type: 'string' }, user: { type: 'string' }, @@ -1998,47 +2001,37 @@ API.v1 return API.v1.forbidden(); } - const { _id, username, roles, name } = user; - let { statusText, status } = user; - - if (this.bodyParams.message || this.bodyParams.message === '') { - await setStatusText(user, this.bodyParams.message, { emit: false }); - statusText = this.bodyParams.message; + const validStatus = ['online', 'away', 'offline', 'busy']; + if (this.bodyParams.status && !validStatus.includes(this.bodyParams.status)) { + throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { + method: 'users.setStatus', + }); } - if (this.bodyParams.status) { - const validStatus = ['online', 'away', 'offline', 'busy']; - if (validStatus.includes(this.bodyParams.status)) { - status = this.bodyParams.status; + const { status, message, expiresAt } = this.bodyParams; - if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { - throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { - method: 'users.setStatus', - }); - } + const statusExpiresAt = expiresAt ? new Date(expiresAt) : undefined; + if (statusExpiresAt && Number.isNaN(statusExpiresAt.getTime())) { + throw new Meteor.Error('error-invalid-date', 'Invalid expiresAt date string', { + method: 'users.setStatus', + }); + } - await Users.updateOne( - { _id: user._id }, - { - $set: { - status, - statusDefault: status, - }, - }, - ); + // If status is missing (message-only update), keep the user's chosen status (statusDefault), + // not the computed status — otherwise a transient auto-away/offline gets pinned as a manual claim. + const effectiveStatus = status || user.statusDefault || ('online' as UserStatus); - void wrapExceptions(() => Calendar.cancelUpcomingStatusChanges(user._id)).suppress(); - } else { - throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { - method: 'users.setStatus', - }); - } + if (effectiveStatus === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { + throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { + method: 'users.setStatus', + }); } - void api.broadcast('presence.status', { - user: { status, _id, username, statusText, roles, name }, - previousStatus: user.status, - }); + await Presence.setStatus(user._id, effectiveStatus, message, statusExpiresAt); + + if (status) { + void wrapExceptions(() => Calendar.cancelUpcomingStatusChanges(user._id)).suppress(); + } return API.v1.success(); }, @@ -2049,12 +2042,14 @@ API.v1 authRequired: true, query: isUsersGetStatusParamsGET, response: { - 200: ajv.compile<{ _id: string; status: string; connectionStatus?: string }>({ + 200: ajv.compile<{ _id: string; status: string; connectionStatus?: string; statusSource?: string; statusExpiresAt?: string }>({ type: 'object', properties: { _id: { type: 'string' }, status: statusType, connectionStatus: { type: 'string', nullable: true }, + statusSource: { type: 'string', nullable: true }, + statusExpiresAt: { type: 'string', nullable: true }, success: { type: 'boolean', enum: [true] }, }, required: ['_id', 'status', 'success'], @@ -2068,9 +2063,10 @@ API.v1 if (isUserFromParams(this.queryParams, this.userId, this.user)) { return API.v1.success({ _id: this.userId, - // message: user.statusText, connectionStatus: (this.user.statusConnection || 'offline') as 'online' | 'offline' | 'away' | 'busy', status: (this.user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy', + ...(this.user.statusSource && { statusSource: this.user.statusSource }), + ...(this.user.statusExpiresAt && { statusExpiresAt: this.user.statusExpiresAt.toISOString() }), }); } @@ -2078,8 +2074,9 @@ API.v1 return API.v1.success({ _id: user._id, - // message: user.statusText, status: (user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy', + ...(user.statusSource && { statusSource: user.statusSource }), + ...(user.statusExpiresAt && { statusExpiresAt: user.statusExpiresAt.toISOString() }), }); }, ); diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts index 816416fa0002c..1caa744316e7b 100644 --- a/apps/meteor/app/apps/server/bridges/users.ts +++ b/apps/meteor/app/apps/server/bridges/users.ts @@ -2,7 +2,7 @@ import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { UserBridge } from '@rocket.chat/apps/dist/server/bridges/UserBridge'; import type { IUserCreationOptions, IUser, UserType } from '@rocket.chat/apps-engine/definition/users'; import { Presence } from '@rocket.chat/core-services'; -import type { UserStatus } from '@rocket.chat/core-typings'; +import type { PresenceSource, UserStatus } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; @@ -179,6 +179,27 @@ export class AppUserBridge extends UserBridge { return true; } + protected async setActiveState( + userId: IUser['id'], + state: Pick, + appId: string, + ): Promise { + this.orch.debugLog(`The App ${appId} is setting active state for user ${userId}`); + + await Presence.setActiveState(userId, { + statusDefault: state.statusDefault as UserStatus, + statusText: state.statusText, + statusSource: state.statusSource as PresenceSource, + ...(state.statusExpiresAt && { statusExpiresAt: state.statusExpiresAt }), + }); + } + + protected async endActiveState(userId: IUser['id'], appId: string): Promise { + this.orch.debugLog(`The App ${appId} is ending active state for user ${userId}`); + + await Presence.endActiveState(userId); + } + protected async getActiveUserCount(): Promise { return Users.getActiveLocalUserCount(); } diff --git a/apps/meteor/app/lib/server/functions/getFullUserData.ts b/apps/meteor/app/lib/server/functions/getFullUserData.ts index 6dcbe771e9bb3..08f9df9f83716 100644 --- a/apps/meteor/app/lib/server/functions/getFullUserData.ts +++ b/apps/meteor/app/lib/server/functions/getFullUserData.ts @@ -18,6 +18,8 @@ export const defaultFields = { bio: 1, reason: 1, statusText: 1, + statusSource: 1, + statusExpiresAt: 1, avatarETag: 1, federated: 1, statusLivechat: 1, diff --git a/apps/meteor/app/slashcommands-status/server/status.ts b/apps/meteor/app/slashcommands-status/server/status.ts index 0a1f02b182571..89bad86a4a373 100644 --- a/apps/meteor/app/slashcommands-status/server/status.ts +++ b/apps/meteor/app/slashcommands-status/server/status.ts @@ -14,12 +14,11 @@ slashCommands.add({ return; } - const user = await Users.findOneById>( - userId, - { - projection: { language: 1, username: 1, name: 1, status: 1, roles: 1, statusText: 1 }, - }, - ); + const user = await Users.findOneById< + Pick + >(userId, { + projection: { language: 1, username: 1, name: 1, status: 1, statusDefault: 1, roles: 1, statusText: 1 }, + }); const lng = user?.language || settings.get('Language') || 'en'; if (!user) { diff --git a/apps/meteor/app/user-status/server/methods/setUserStatus.ts b/apps/meteor/app/user-status/server/methods/setUserStatus.ts index 0235c4b681853..5ad743df9fb7b 100644 --- a/apps/meteor/app/user-status/server/methods/setUserStatus.ts +++ b/apps/meteor/app/user-status/server/methods/setUserStatus.ts @@ -1,11 +1,9 @@ import { Presence } from '@rocket.chat/core-services'; -import type { IUser } from '@rocket.chat/core-typings'; +import { UserStatus, type IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { RateLimiter } from '../../../lib/server'; -import { setStatusText } from '../../../lib/server/functions/setStatusText'; import { settings } from '../../../settings/server'; declare module '@rocket.chat/ddp-client' { @@ -16,30 +14,25 @@ declare module '@rocket.chat/ddp-client' { } export const setUserStatusMethod = async ( - user: Pick, + user: Pick, statusType: IUser['status'], statusText: IUser['statusText'], ): Promise => { - if (statusType) { - if (statusType === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { - throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { - method: 'setUserStatus', - }); - } - await Presence.setStatus(user._id, statusType); + if (statusText != null && !settings.get('Accounts_AllowUserStatusMessageChange')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'setUserStatus', + }); } - if (statusText || statusText === '') { - check(statusText, String); - - if (!settings.get('Accounts_AllowUserStatusMessageChange')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'setUserStatus', - }); - } + const effectiveStatus = statusType || user.statusDefault || UserStatus.ONLINE; - await setStatusText(user, statusText); + if (effectiveStatus === UserStatus.OFFLINE && !settings.get('Accounts_AllowInvisibleStatusOption')) { + throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { + method: 'setUserStatus', + }); } + + await Presence.setStatus(user._id, effectiveStatus, statusText); }; Meteor.methods({ diff --git a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts index 6ee9992826727..acd5233047ddc 100644 --- a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts +++ b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts @@ -10,6 +10,8 @@ export const getBaseUserFields = (allowServiceKeys = false): UserFields => ({ 'status': 1, 'statusDefault': 1, 'statusText': 1, + 'statusSource': 1, + 'statusExpiresAt': 1, 'statusConnection': 1, 'bio': 1, 'avatarOrigin': 1, diff --git a/apps/meteor/server/methods/saveUserProfile.ts b/apps/meteor/server/methods/saveUserProfile.ts index 5f17d4a98f7fa..b124ce003d118 100644 --- a/apps/meteor/server/methods/saveUserProfile.ts +++ b/apps/meteor/server/methods/saveUserProfile.ts @@ -82,12 +82,8 @@ async function saveUserProfile( } } - if (settings.statusText || settings.statusText === '') { - await setUserStatusMethod(user, undefined, settings.statusText); - } - - if (settings.statusType) { - await setUserStatusMethod(user, settings.statusType as UserStatus, undefined); + if (settings.statusType || settings.statusText != null) { + await setUserStatusMethod(user, settings.statusType as UserStatus, settings.statusText); } if (user && (settings.bio || settings.bio === '')) { diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index af62fa8d7ba13..7b221dd7781b8 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -157,7 +157,7 @@ export class ListenersModule { }); service.onEvent('presence.status', ({ user }) => { - const { _id, username, name, status, statusText, roles } = user; + const { _id, username, name, status, statusText, statusSource, statusExpiresAt, roles } = user; if (!status || !username) { return; } @@ -172,16 +172,29 @@ export class ListenersModule { diff: { status, ...(statusText && { statusText }), + ...(statusSource && { statusSource }), + ...(statusExpiresAt && { statusExpiresAt }), }, unset: { ...(!statusText && { statusText: 1 }), + ...(!statusSource && { statusSource: 1 }), + ...(!statusExpiresAt && { statusExpiresAt: 1 }), }, }); - notifications.notifyLoggedInThisInstance('user-status', [_id, username, STATUS_MAP[status], statusText, name, roles]); + notifications.notifyLoggedInThisInstance('user-status', [ + _id, + username, + STATUS_MAP[status], + statusText, + name, + roles, + statusSource, + statusExpiresAt, + ]); if (_id) { - notifications.sendPresence(_id, username, STATUS_MAP[status], statusText); + notifications.sendPresence(_id, username, STATUS_MAP[status], statusText, statusSource, statusExpiresAt); } }); diff --git a/apps/meteor/server/modules/notifications/notifications.module.ts b/apps/meteor/server/modules/notifications/notifications.module.ts index 44c7e7196dac2..fe1cb5a45107c 100644 --- a/apps/meteor/server/modules/notifications/notifications.module.ts +++ b/apps/meteor/server/modules/notifications/notifications.module.ts @@ -1,5 +1,5 @@ import { Authorization, MediaCall, VideoConf, Settings } from '@rocket.chat/core-services'; -import type { ISubscription, IOmnichannelRoom, IUser, IUserDataEvent, PresenceStatusCode } from '@rocket.chat/core-typings'; +import type { ISubscription, IOmnichannelRoom, IUser, IUserDataEvent, PresenceSource, PresenceStatusCode } from '@rocket.chat/core-typings'; import type { StreamerCallbackArgs, StreamKeys, StreamNames } from '@rocket.chat/ddp-client'; import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; @@ -512,7 +512,10 @@ export class NotificationsModule { return this.streamUser.emitWithoutBroadcast(`${userId}/${eventName}`, ...args); } - sendPresence(uid: string, ...args: [username: string, status?: PresenceStatusCode, statusText?: string]): void { + sendPresence( + uid: string, + ...args: [username: string, status?: PresenceStatusCode, statusText?: string, statusSource?: PresenceSource, statusExpiresAt?: Date] + ): void { emit(uid, [args]); return this.streamPresence.emitWithoutBroadcast(uid, args); } diff --git a/apps/meteor/tests/data/apps/app-packages/index.ts b/apps/meteor/tests/data/apps/app-packages/index.ts index 73442172e121b..2e454d6013d25 100644 --- a/apps/meteor/tests/data/apps/app-packages/index.ts +++ b/apps/meteor/tests/data/apps/app-packages/index.ts @@ -13,3 +13,5 @@ export const appUpdateStatusTest = path.resolve(__dirname, './update-status-test export const appExternalIdTest = path.resolve(__dirname, './external-id-test_0.0.1.zip'); export const messageReactionTest = path.resolve(__dirname, './message-updater-test_0.0.1.zip'); + +export const appPresenceStateTest = path.resolve(__dirname, './presence-state-test_0.0.1.zip'); diff --git a/apps/meteor/tests/data/apps/app-packages/presence-state-test_0.0.1.zip b/apps/meteor/tests/data/apps/app-packages/presence-state-test_0.0.1.zip new file mode 100644 index 0000000000000000000000000000000000000000..69d5896e7734d8561ba128e8ca01f3bb321bd532 GIT binary patch literal 16565 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 EA1Ms*`~Uy| literal 0 HcmV?d00001 diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index 453107581e13f..bd5003563d084 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -132,10 +132,11 @@ const preferences = { const getUserStatus = (userId: IUser['_id']) => new Promise<{ + _id: string; status: 'online' | 'offline' | 'away' | 'busy'; - message?: string; - _id?: string; connectionStatus?: 'online' | 'offline' | 'away' | 'busy'; + statusSource?: string; + statusExpiresAt?: string; }>((resolve) => { void request .get(api('users.getStatus')) @@ -5611,8 +5612,8 @@ describe('[Users]', () => { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('_id', credentials['X-User-Id']); expect(res.body).to.have.property('status'); - expect(res.body._id).to.be.equal(credentials['X-User-Id']); }) .end(done); }); @@ -5625,8 +5626,8 @@ describe('[Users]', () => { .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('_id', 'rocket.cat'); expect(res.body).to.have.property('status'); - expect(res.body._id).to.be.equal('rocket.cat'); }) .end(done); }); @@ -5722,7 +5723,6 @@ describe('[Users]', () => { expect(res.body).to.have.property('success', true); void getUserStatus(credentials['X-User-Id']).then((status) => { expect(status.status).to.be.equal('busy'); - expect(status.message).to.be.equal('test'); }); }) .end(done); @@ -5763,6 +5763,23 @@ describe('[Users]', () => { await updateSetting('Accounts_AllowInvisibleStatusOption', true); }); + it('should reject a message-only update when status resolves to offline via statusDefault and "Accounts_AllowInvisibleStatusOption" is disabled', async () => { + await updateSetting('Accounts_AllowInvisibleStatusOption', true); + await request.post(api('users.setStatus')).set(credentials).send({ status: 'offline' }).expect(200); + await updateSetting('Accounts_AllowInvisibleStatusOption', false); + + await request + .post(api('users.setStatus')) + .set(credentials) + .send({ message: 'still trying to stay invisible' }) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.errorType).to.be.equal('error-status-not-allowed'); + }); + + await updateSetting('Accounts_AllowInvisibleStatusOption', true); + }); it('should return an error when the payload is missing all supported fields', (done) => { void request .post(api('users.setStatus')) @@ -5786,6 +5803,65 @@ describe('[Users]', () => { expect(res.body).to.have.property('status', 'error'); }); }); + + it('should set status with expiresAt and return statusSource and statusExpiresAt in getStatus', async () => { + const expiresAt = new Date(Date.now() + 3600_000).toISOString(); + + await request + .post(api('users.setStatus')) + .set(credentials) + .send({ + status: 'busy', + message: 'focus time', + expiresAt, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + + const status = await getUserStatus(credentials['X-User-Id']); + // display status is offline because the test user has no active DDP session; + // the busy claim is persisted in statusDefault and takes effect on reconnect + expect(status.status).to.be.equal('offline'); + expect(status).to.have.property('statusSource', 'manual'); + expect(status).to.have.property('statusExpiresAt'); + expect(new Date(status.statusExpiresAt!).getTime()).to.be.closeTo(new Date(expiresAt).getTime(), 2000); + }); + + it('should not return statusExpiresAt when expiresAt is not set', async () => { + await request + .post(api('users.setStatus')) + .set(credentials) + .send({ + status: 'online', + }) + .expect(200); + + const status = await getUserStatus(credentials['X-User-Id']); + expect(status.status).to.be.equal('online'); + expect(status).to.not.have.property('statusExpiresAt'); + }); + + it('should update only the status message when no status is provided', async () => { + await updateSetting('Accounts_AllowUserStatusMessageChange', true); + + await request + .post(api('users.setStatus')) + .set(credentials) + .send({ + message: 'message only, no status', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + + const infoResponse = await request.get(api('users.info')).query({ userId: credentials['X-User-Id'] }).set(credentials).expect(200); + expect(infoResponse.body.user).to.have.property('statusText', 'message only, no status'); + }); }); describe('[/users.removeOtherTokens]', () => { diff --git a/apps/meteor/tests/end-to-end/apps/presence-state.ts b/apps/meteor/tests/end-to-end/apps/presence-state.ts new file mode 100644 index 0000000000000..41fec5458a575 --- /dev/null +++ b/apps/meteor/tests/end-to-end/apps/presence-state.ts @@ -0,0 +1,74 @@ +import type { App } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { after, before, describe, it } from 'mocha'; + +import { getCredentials, request, credentials } from '../../data/api-data'; +import { appPresenceStateTest } from '../../data/apps/app-packages'; +import { apps } from '../../data/apps/apps-data'; +import { cleanupApps, installLocalTestPackage } from '../../data/apps/helper'; +import { adminUsername } from '../../data/user'; +import { getUserByUsername } from '../../data/users.helper'; +import { IS_EE } from '../../e2e/config/constants'; + +(IS_EE ? describe : describe.skip)('Apps - Presence State Bridge', () => { + let app: App; + + before((done) => getCredentials(done)); + + before(async () => { + await cleanupApps(); + app = await installLocalTestPackage(appPresenceStateTest); + }); + + after(() => cleanupApps()); + + describe('[setActiveState]', () => { + it('should set the user presence with status text and source', async () => { + const user = await getUserByUsername(adminUsername); + + await request + .post(apps(`/public/${app.id}/set-active-state`)) + .set(credentials) + .send({ + userId: user._id, + statusDefault: 'busy', + statusText: 'In a meeting', + statusSource: 'internal', + }) + .expect(200); + + const updatedUser = await getUserByUsername(adminUsername); + expect(updatedUser.statusText).to.equal('In a meeting'); + expect(updatedUser.statusSource).to.equal('internal'); + }); + }); + + describe('[endActiveState]', () => { + it('should restore the user presence to previous state', async () => { + const user = await getUserByUsername(adminUsername); + + // First set an active state + await request + .post(apps(`/public/${app.id}/set-active-state`)) + .set(credentials) + .send({ + userId: user._id, + statusDefault: 'busy', + statusText: 'On a call', + statusSource: 'internal', + }) + .expect(200); + + // Then end it + await request + .post(apps(`/public/${app.id}/end-active-state`)) + .set(credentials) + .send({ userId: user._id }) + .expect(200); + + const updatedUser = await getUserByUsername(adminUsername); + expect(updatedUser.statusText).to.not.equal('On a call'); + expect(updatedUser.statusSource).to.not.equal('internal'); + }); + }); +}); diff --git a/apps/meteor/tests/end-to-end/apps/update-status-text.ts b/apps/meteor/tests/end-to-end/apps/update-status-text.ts index 8b3deece0692b..74096434c616d 100644 --- a/apps/meteor/tests/end-to-end/apps/update-status-text.ts +++ b/apps/meteor/tests/end-to-end/apps/update-status-text.ts @@ -67,9 +67,7 @@ const APP_USERNAME = 'update-status-test.bot'; expect(appUser.statusText).to.be.equal(statusText); }); - it('should update status without changing statusText', async () => { - const userBefore = await getUserByUsername(APP_USERNAME); - + it('should clear statusText when status is updated without providing statusText', async () => { await request .post(apps(`/public/${app.id}/update-status`)) .set(credentials) @@ -78,10 +76,9 @@ const APP_USERNAME = 'update-status-test.bot'; const appUser = await getUserByUsername(APP_USERNAME); - // We can't test the status value because the Presence service will override it with OFFLINE - // when the user doesn't have an active session/connection - // expect(appUser.status).to.equal(status); - expect(appUser.statusText).to.be.equal(userBefore.statusText); + // The test app defaults statusText to '' when not provided, + // so the presence engine correctly clears it + expect(appUser.statusText).to.be.equal(''); }); }); }); diff --git a/ee/packages/presence/src/Presence.spec.ts b/ee/packages/presence/src/Presence.spec.ts new file mode 100644 index 0000000000000..f749ea1738a17 --- /dev/null +++ b/ee/packages/presence/src/Presence.spec.ts @@ -0,0 +1,250 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; +import { registerModel } from '@rocket.chat/models'; + +import { Presence } from './Presence'; + +const findUserMock = jest.fn(); +const updatePresenceMock = jest.fn(); +const findSessionMock = jest.fn(); + +registerModel('IUsersModel', { + findOneById: findUserMock, + updatePresenceAndStatus: updatePresenceMock, + findExpiredStatuses: jest.fn(), + findNextStatusExpiration: jest.fn().mockResolvedValue(null), +} as any); + +registerModel('IUsersSessionsModel', { + findOneById: findSessionMock, + addConnectionById: jest.fn(), + removeConnectionByConnectionId: jest.fn(), + updateConnectionStatusById: jest.fn(), +} as any); + +const user = (o: Partial = {}): IUser => + ({ + _id: 'u1', + username: 'test', + roles: ['user'], + status: UserStatus.ONLINE, + statusDefault: UserStatus.ONLINE, + statusConnection: UserStatus.ONLINE, + statusText: '', + ...o, + }) as IUser; + +const withOnlineSession = () => + findSessionMock.mockResolvedValue({ connections: [{ id: 's1', instanceId: 'i1', status: UserStatus.ONLINE }] }); + +const withNoSessions = () => findSessionMock.mockResolvedValue(null); + +describe('Presence class', () => { + let presence: Presence; + + beforeEach(() => { + jest.clearAllMocks(); + presence = new Presence(); + (presence as any).broadcastEnabled = true; + (presence as any).api = { broadcast: jest.fn(), nodeList: jest.fn().mockResolvedValue([]) }; + updatePresenceMock.mockResolvedValue(user()); + }); + + describe('setActiveState', () => { + it('should apply claim and write combined result when user is online', async () => { + findUserMock.mockResolvedValue(user()); + withOnlineSession(); + + await presence.setActiveState('u1', { + statusDefault: UserStatus.BUSY, + statusSource: 'manual', + statusText: 'Focus', + }); + + expect(updatePresenceMock).toHaveBeenCalledWith( + 'u1', + expect.objectContaining({ statusDefault: UserStatus.BUSY, statusSource: 'manual', status: UserStatus.BUSY }), + expect.any(Array), + undefined, + ); + }); + + it('should not write when claim is rejected (offline + external)', async () => { + findUserMock.mockResolvedValue(user({ statusDefault: UserStatus.OFFLINE })); + withNoSessions(); + + await presence.setActiveState('u1', { + statusDefault: UserStatus.BUSY, + statusSource: 'external', + }); + + expect(updatePresenceMock).not.toHaveBeenCalled(); + }); + + it('should store claim but show offline status when user has no sessions', async () => { + findUserMock.mockResolvedValue(user({ statusDefault: UserStatus.ONLINE })); + withNoSessions(); + + await presence.setActiveState('u1', { + statusDefault: UserStatus.BUSY, + statusSource: 'manual', + statusText: 'Working', + }); + + expect(updatePresenceMock).toHaveBeenCalledWith( + 'u1', + expect.objectContaining({ status: UserStatus.OFFLINE, statusConnection: UserStatus.OFFLINE, statusDefault: UserStatus.BUSY }), + expect.any(Array), + undefined, + ); + }); + + it('should pass expiresAt when provided', async () => { + const expiresAt = new Date(Date.now() + 3600_000); + findUserMock.mockResolvedValue(user()); + withOnlineSession(); + + await presence.setActiveState('u1', { + statusDefault: UserStatus.BUSY, + statusSource: 'manual', + statusText: 'Focus', + statusExpiresAt: expiresAt, + }); + + expect(updatePresenceMock).toHaveBeenCalledWith( + 'u1', + expect.objectContaining({ statusExpiresAt: expiresAt }), + expect.arrayContaining(['previousState']), + undefined, + ); + }); + }); + + describe('endActiveState', () => { + it('should restore previous state and write', async () => { + findUserMock.mockResolvedValue( + user({ + statusSource: 'manual', + statusDefault: UserStatus.BUSY, + previousState: { statusDefault: UserStatus.BUSY, statusText: 'Meeting', statusSource: 'external' }, + }), + ); + withOnlineSession(); + + await presence.endActiveState('u1'); + + expect(updatePresenceMock).toHaveBeenCalledWith( + 'u1', + expect.objectContaining({ statusSource: 'external', statusText: 'Meeting' }), + expect.arrayContaining(['previousState']), + undefined, + ); + }); + }); + + describe('clearActiveState', () => { + it('should reset to online and write', async () => { + findUserMock.mockResolvedValue(user({ statusDefault: UserStatus.BUSY, statusSource: 'manual' })); + withOnlineSession(); + + await presence.clearActiveState('u1'); + + expect(updatePresenceMock).toHaveBeenCalledWith( + 'u1', + expect.objectContaining({ statusDefault: UserStatus.ONLINE, statusText: '', status: UserStatus.ONLINE }), + expect.arrayContaining(['statusSource', 'previousState']), + undefined, + ); + }); + }); + + describe('setStatus', () => { + it('should apply a manual claim when status changes', async () => { + findUserMock.mockResolvedValue(user()); + withOnlineSession(); + + await presence.setStatus('u1', UserStatus.BUSY, 'Working'); + + expect(updatePresenceMock).toHaveBeenCalledWith( + 'u1', + expect.objectContaining({ statusDefault: UserStatus.BUSY, statusSource: 'manual' }), + expect.any(Array), + undefined, + ); + }); + + it('should trigger clearActive when status is ONLINE with no text', async () => { + findUserMock.mockResolvedValue(user({ statusDefault: UserStatus.BUSY, statusSource: 'manual' })); + withOnlineSession(); + + await presence.setStatus('u1', UserStatus.ONLINE); + + expect(updatePresenceMock).toHaveBeenCalledWith( + 'u1', + expect.objectContaining({ statusDefault: UserStatus.ONLINE }), + expect.arrayContaining(['statusSource', 'previousState']), + undefined, + ); + }); + + it('should trigger clearActive when status is ONLINE with empty string text', async () => { + findUserMock.mockResolvedValue(user({ statusDefault: UserStatus.BUSY, statusSource: 'manual' })); + withOnlineSession(); + + await presence.setStatus('u1', UserStatus.ONLINE, ''); + + expect(updatePresenceMock).toHaveBeenCalledWith( + 'u1', + expect.objectContaining({ statusDefault: UserStatus.ONLINE }), + expect.arrayContaining(['statusSource', 'previousState']), + undefined, + ); + }); + + it('should trigger setActive when status is ONLINE with text', async () => { + findUserMock.mockResolvedValue(user()); + withOnlineSession(); + + await presence.setStatus('u1', UserStatus.ONLINE, 'brb'); + + expect(updatePresenceMock).toHaveBeenCalledWith( + 'u1', + expect.objectContaining({ statusDefault: UserStatus.ONLINE, statusSource: 'manual', statusText: 'brb' }), + expect.any(Array), + undefined, + ); + }); + + it('should write empty string statusText when explicitly provided', async () => { + findUserMock.mockResolvedValue(user({ statusText: 'Old text' })); + withOnlineSession(); + + await presence.setStatus('u1', UserStatus.BUSY, ''); + + expect(updatePresenceMock).toHaveBeenCalledWith( + 'u1', + expect.objectContaining({ statusDefault: UserStatus.BUSY, statusText: '' }), + expect.any(Array), + undefined, + ); + }); + + it('should not include statusText when undefined', async () => { + findUserMock.mockResolvedValue(user({ statusText: 'Old text' })); + withOnlineSession(); + + await presence.setStatus('u1', UserStatus.BUSY); + + const updateArg = updatePresenceMock.mock.calls[0][1]; + expect(updateArg).not.toHaveProperty('statusText'); + }); + + it('should not write when user is not found', async () => { + findUserMock.mockResolvedValue(null); + + await presence.setStatus('u1', UserStatus.BUSY); + + expect(updatePresenceMock).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/ee/packages/presence/src/Presence.ts b/ee/packages/presence/src/Presence.ts index e8e76d755f19a..70d4ae0168f0b 100755 --- a/ee/packages/presence/src/Presence.ts +++ b/ee/packages/presence/src/Presence.ts @@ -1,3 +1,5 @@ +import { setTimeout, clearTimeout } from 'node:timers'; + import type { IPresence, IBrokerNode } from '@rocket.chat/core-services'; import { License, ServiceClass, Settings } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; @@ -5,9 +7,25 @@ import { UserStatus } from '@rocket.chat/core-typings'; import { Users, UsersSessions } from '@rocket.chat/models'; import { PresenceReaper } from './lib/PresenceReaper'; -import { processPresenceAndStatus } from './lib/processConnectionStatus'; +import { normalizeStatusText } from './lib/normalizeStatusText'; +import { type ClaimUpdate, processPresence } from './lib/presenceEngine'; const MAX_CONNECTIONS = 200; +const MAX_TIMEOUT_DELAY_MS = 2 ** 31 - 1; + +type PresenceUser = Pick< + IUser, + | '_id' + | 'username' + | 'roles' + | 'status' + | 'statusDefault' + | 'statusSource' + | 'statusText' + | 'statusExpiresAt' + | 'statusConnection' + | 'previousState' +>; export class Presence extends ServiceClass implements IPresence { protected name = 'presence'; @@ -28,6 +46,8 @@ export class Presence extends ServiceClass implements IPresence { private reaper: PresenceReaper; + private expirationTimeout?: NodeJS.Timeout; + constructor() { super(); @@ -97,6 +117,42 @@ export class Presence extends ServiceClass implements IPresence { } catch (e: unknown) { // ignore } + + await this.handleExpirationJob(); + } + + private async processExpiredStatuses(): Promise { + // TODO: in MS mode every instance runs this independently. + // Add a job-level lock to avoid redundant cross-instance reads. + const expiredCursor = Users.findExpiredStatuses(); + for await (const user of expiredCursor) { + await this.updateUserPresence(user, { type: 'endActive' }); + } + } + + private async setupNextExpiration(): Promise { + clearTimeout(this.expirationTimeout); + this.expirationTimeout = undefined; + + const next = await Users.findNextStatusExpiration(); + if (!next?.statusExpiresAt) { + return; + } + + // Node coerces any setTimeout delay > 2^31-1 ms (~24.8 days) to 1ms, firing on the next tick. + // See https://nodejs.org/api/timers.html#settimeoutcallback-delay-args + // "When delay is larger than 2147483647 [...] the delay will be set to 1". + // Cap at the limit so far-future expirations reschedule on each wake instead of misfiring immediately. + const delay = Math.min(Math.max(next.statusExpiresAt.getTime() - Date.now(), 0), MAX_TIMEOUT_DELAY_MS); + this.expirationTimeout = setTimeout(() => { + this.expirationTimeout = undefined; + this.handleExpirationJob().catch((err) => console.error('[Presence] Error handling status expiration:', err)); + }, delay); + } + + private async handleExpirationJob(): Promise { + await this.processExpiredStatuses(); + await this.setupNextExpiration(); } private async handleReaperUpdates(userIds: string[]): Promise { @@ -118,9 +174,7 @@ export class Presence extends ServiceClass implements IPresence { override async stopped(): Promise { this.reaper.stop(); - if (!this.lostConTimeout) { - return; - } + clearTimeout(this.expirationTimeout); clearTimeout(this.lostConTimeout); } @@ -230,27 +284,60 @@ export class Presence extends ServiceClass implements IPresence { return affectedUsers.map(({ _id }) => _id); } - async setStatus(uid: string, statusDefault: UserStatus, statusText?: string): Promise { - const userSessions = (await UsersSessions.findOneById(uid)) || { connections: [] }; - - const user = await Users.findOneById>(uid, { - projection: { username: 1, roles: 1, status: 1 }, - }); + /** + * Updates presence and reschedules the expiration job. + * All public methods should use this instead of calling updateUserPresence directly. + */ + private async updatePresenceAndReschedule(uid: string, claimUpdate: ClaimUpdate): Promise { + const result = await this.updateUserPresence(uid, claimUpdate); + await this.setupNextExpiration(); + return result; + } - const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault); + async setStatus(userId: string, statusDefault: UserStatus, statusText?: string, statusExpiresAt?: Date): Promise { + // Selecting 'online' without a status message clears any manual claim + // and reverts to connection-driven presence. + if (statusDefault === UserStatus.ONLINE && !statusText) { + return this.clearActiveState(userId); + } - const result = await Users.updateStatusById(uid, { + return this.setActiveState(userId, { statusDefault, - status, - statusConnection, - statusText, + statusSource: 'manual', + ...(statusText != null && { statusText: normalizeStatusText(statusText) }), + ...(statusExpiresAt && { statusExpiresAt }), }); + } - if (result.modifiedCount > 0) { - this.broadcast({ _id: uid, username: user?.username, status, statusText, roles: user?.roles || [] }, user?.status); - } + /** + * Applies a presence claim from a source (manual, external, internal). + */ + async setActiveState( + userId: string, + newState: Pick, + ): Promise { + return this.updatePresenceAndReschedule(userId, { + type: 'setActive', + newState: { + ...newState, + ...(newState.statusText != null && { statusText: normalizeStatusText(newState.statusText) }), + }, + }); + } - return !!result.modifiedCount; + /** + * Ends the current active claim. Restores previous if valid, otherwise + * falls back to system-managed. + */ + async endActiveState(userId: string): Promise { + return this.updatePresenceAndReschedule(userId, { type: 'endActive' }); + } + + /** + * Removes all presence claims and resets to "Online" with no text. + */ + async clearActiveState(userId: string): Promise { + return this.updatePresenceAndReschedule(userId, { type: 'clearActive' }); } async setConnectionStatus(uid: string, status: UserStatus, session: string): Promise { @@ -261,38 +348,58 @@ export class Presence extends ServiceClass implements IPresence { return !!result.modifiedCount; } - async updateUserPresence(uid: string): Promise { - const user = await Users.findOneById>(uid, { - projection: { - username: 1, - statusDefault: 1, - statusText: 1, - roles: 1, - status: 1, - }, - }); + /** + * Low-level presence update. Does not reschedule the expiration job. + * Prefer {@link updatePresenceAndReschedule} for public-facing methods. + */ + private async updateUserPresence(uidOrUser: string | PresenceUser, claimUpdate?: ClaimUpdate): Promise { + const user = + typeof uidOrUser === 'string' + ? await Users.findOneById(uidOrUser, { + projection: { + username: 1, + roles: 1, + status: 1, + statusDefault: 1, + statusSource: 1, + statusText: 1, + statusExpiresAt: 1, + statusConnection: 1, + previousState: 1, + }, + }) + : uidOrUser; if (!user) { - return; + return false; } - const userSessions = (await UsersSessions.findOneById(uid)) || { connections: [] }; + const userSessions = await UsersSessions.findOneById(user._id); + const sessions = userSessions?.connections ?? []; - const { statusDefault } = user; - - const { status, statusConnection } = processPresenceAndStatus(userSessions.connections, statusDefault); - - const result = await Users.updateStatusById(uid, { - status, - statusConnection, - }); + const result = processPresence(user, sessions, claimUpdate); + if (Object.keys(result.values).length === 0) { + return false; + } - if (result.modifiedCount > 0) { - this.broadcast({ _id: uid, username: user.username, status, statusText: user.statusText, roles: user.roles }, user.status); + // Only apply this update if statusExpiresAt and previousState haven't changed since we read them. + // Prevents two presence instances from processing the same expiration and overwriting each other. + const guard = + claimUpdate?.type === 'endActive' && user.statusExpiresAt + ? { + statusExpiresAt: user.statusExpiresAt, + ...(user.previousState ? { previousState: user.previousState } : { previousState: { $exists: false } }), + } + : undefined; + + const updatedUser = await Users.updatePresenceAndStatus(user._id, result.values, result.clear, guard); + if (updatedUser) { + this.broadcast(updatedUser, user.status); } + return !!updatedUser; } private broadcast( - user: Pick, + user: Pick, previousStatus: UserStatus | undefined, ): void { if (!this.broadcastEnabled) { diff --git a/ee/packages/presence/src/lib/normalizeStatusText.ts b/ee/packages/presence/src/lib/normalizeStatusText.ts new file mode 100644 index 0000000000000..9f41679ec212f --- /dev/null +++ b/ee/packages/presence/src/lib/normalizeStatusText.ts @@ -0,0 +1,5 @@ +export const STATUS_TEXT_MAX_LENGTH = 120; + +export function normalizeStatusText(text: string): string { + return text.trim().substring(0, STATUS_TEXT_MAX_LENGTH); +} diff --git a/ee/packages/presence/src/lib/presenceEngine.spec.ts b/ee/packages/presence/src/lib/presenceEngine.spec.ts new file mode 100644 index 0000000000000..72f8eb80fec48 --- /dev/null +++ b/ee/packages/presence/src/lib/presenceEngine.spec.ts @@ -0,0 +1,334 @@ +import type { IUser, IUserSessionConnection } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; + +import { processPresence } from './presenceEngine'; + +const ONE_HOUR = 3600_000; + +type PresenceUser = Pick; + +const user = (data: Partial = {}): PresenceUser => ({ + statusDefault: UserStatus.ONLINE, + statusText: '', + ...data, +}); + +const session = (status: UserStatus = UserStatus.ONLINE): IUserSessionConnection => ({ + id: 'random', + instanceId: 'random', + status, + _createdAt: new Date(), + _updatedAt: new Date(), +}); + +describe('processPresence', () => { + describe('baseline (no claim)', () => { + test('should resolve to ONLINE when user has an active session', () => { + const result = processPresence(user(), [session(UserStatus.ONLINE)]); + expect(result.values).toMatchObject({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE }); + }); + + test('should display BUSY when statusDefault is BUSY even if connection is ONLINE', () => { + const result = processPresence(user({ statusDefault: UserStatus.BUSY }), [session(UserStatus.ONLINE)]); + expect(result.values).toMatchObject({ status: UserStatus.BUSY, statusConnection: UserStatus.ONLINE }); + }); + + test('should resolve to AWAY when user is idle', () => { + const result = processPresence(user(), [session(UserStatus.AWAY)]); + expect(result.values).toMatchObject({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY }); + }); + + test('should resolve to OFFLINE when user has no sessions and no active claim', () => { + const result = processPresence(user({ statusDefault: UserStatus.BUSY, statusText: 'Focusing' }), []); + expect(result.values).toMatchObject({ status: UserStatus.OFFLINE, statusConnection: UserStatus.OFFLINE }); + }); + + test('should stay OFFLINE when user is invisible (OFFLINE statusDefault with sessions)', () => { + const result = processPresence(user({ statusDefault: UserStatus.OFFLINE }), [session(UserStatus.ONLINE)]); + expect(result.values).toMatchObject({ status: UserStatus.OFFLINE, statusConnection: UserStatus.ONLINE }); + }); + }); + + describe('session reduction', () => { + test('should resolve to ONLINE when at least one session is active', () => { + const result = processPresence(user(), [session(UserStatus.ONLINE), session(UserStatus.AWAY)]); + expect(result.values).toMatchObject({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE }); + }); + + test('should resolve to AWAY when all sessions are idle', () => { + const result = processPresence(user(), [session(UserStatus.AWAY), session(UserStatus.AWAY)]); + expect(result.values).toMatchObject({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY }); + }); + }); + + describe('setActive', () => { + test('should apply manual claim when user is online', () => { + const result = processPresence(user(), [session()], { + type: 'setActive', + newState: { statusDefault: UserStatus.BUSY, statusSource: 'manual' }, + }); + expect(result.values.status).toBe(UserStatus.BUSY); + expect(result.values.statusSource).toBe('manual'); + }); + + test('should pass expiresAt when provided', () => { + const exp = new Date(Date.now() + ONE_HOUR); + const result = processPresence(user(), [session()], { + type: 'setActive', + newState: { + statusDefault: UserStatus.BUSY, + statusText: 'Out of office', + statusSource: 'manual', + statusExpiresAt: exp, + }, + }); + expect(result.values.statusExpiresAt).toEqual(exp); + }); + + test('should save current as previousState when higher priority claim arrives', () => { + const result = processPresence( + user({ statusSource: 'manual', statusDefault: UserStatus.BUSY, statusText: 'Focusing' }), + [session()], + { type: 'setActive', newState: { statusDefault: UserStatus.BUSY, statusText: 'On a call', statusSource: 'internal' } }, + ); + expect(result.values.statusSource).toBe('internal'); + expect(result.values.previousState).toMatchObject({ statusSource: 'manual', statusText: 'Focusing' }); + }); + + test('should not save previousState when no existing claim', () => { + const result = processPresence(user(), [session()], { + type: 'setActive', + newState: { statusDefault: UserStatus.BUSY, statusSource: 'manual' }, + }); + expect(result.values.previousState).toBeUndefined(); + }); + + test('should overwrite when same priority claim arrives', () => { + const result = processPresence( + user({ statusSource: 'internal', statusDefault: UserStatus.BUSY, statusText: 'On a call' }), + [session()], + { type: 'setActive', newState: { statusDefault: UserStatus.BUSY, statusText: 'In a meeting', statusSource: 'internal' } }, + ); + expect(result.values.statusText).toBe('In a meeting'); + expect(result.values.statusSource).toBe('internal'); + }); + + test('should queue lower priority claim as previousState', () => { + const result = processPresence( + user({ statusSource: 'internal', statusDefault: UserStatus.BUSY, statusText: 'On a call' }), + [session()], + { type: 'setActive', newState: { statusDefault: UserStatus.AWAY, statusText: 'Lunch', statusSource: 'manual' } }, + ); + expect(result.values.previousState).toMatchObject({ statusDefault: UserStatus.AWAY, statusText: 'Lunch', statusSource: 'manual' }); + expect(result.values.statusSource).toBeUndefined(); + }); + + test('should replace expired previousState with lower priority claim', () => { + const result = processPresence( + user({ + statusSource: 'internal', + statusDefault: UserStatus.BUSY, + previousState: { + statusDefault: UserStatus.BUSY, + statusText: 'Old', + statusSource: 'external', + statusExpiresAt: new Date(Date.now() - ONE_HOUR), + }, + }), + [session()], + { type: 'setActive', newState: { statusDefault: UserStatus.BUSY, statusText: 'Deep work', statusSource: 'manual' } }, + ); + expect(result.values.previousState).toMatchObject({ + statusDefault: UserStatus.BUSY, + statusText: 'Deep work', + statusSource: 'manual', + }); + }); + + test('should discard lower priority claim when previousState has higher priority', () => { + const result = processPresence( + user({ + statusSource: 'internal', + statusDefault: UserStatus.BUSY, + previousState: { + statusDefault: UserStatus.BUSY, + statusText: 'Deep work', + statusSource: 'manual', + statusExpiresAt: new Date(Date.now() + ONE_HOUR), + }, + }), + [session()], + { type: 'setActive', newState: { statusDefault: UserStatus.BUSY, statusText: 'Standup', statusSource: 'external' } }, + ); + expect(result.values).toStrictEqual({}); + }); + + test('should apply manual over external without stashing the displaced claim', () => { + const result = processPresence( + user({ statusSource: 'external', statusDefault: UserStatus.BUSY, statusText: 'In a meeting' }), + [session()], + { type: 'setActive', newState: { statusDefault: UserStatus.BUSY, statusText: 'Focusing', statusSource: 'manual' } }, + ); + expect(result.values.statusSource).toBe('manual'); + expect(result.values.statusText).toBe('Focusing'); + expect(result.values.previousState).toBeUndefined(); + expect(result.clear).toContain('previousState'); + }); + + test('should drop a queued previousState when a manual claim wins', () => { + const result = processPresence( + user({ + statusSource: 'external', + statusDefault: UserStatus.BUSY, + statusText: 'In a meeting', + previousState: { statusDefault: UserStatus.AWAY, statusText: 'Lunch', statusSource: 'manual' }, + }), + [session()], + { type: 'setActive', newState: { statusDefault: UserStatus.BUSY, statusText: 'Focusing', statusSource: 'manual' } }, + ); + expect(result.values.statusSource).toBe('manual'); + expect(result.values.previousState).toBeUndefined(); + expect(result.clear).toContain('previousState'); + }); + + test('should clear a queued previousState when overwriting a same-source manual claim', () => { + const result = processPresence( + user({ + statusSource: 'manual', + statusDefault: UserStatus.BUSY, + statusText: 'Focusing', + previousState: { statusDefault: UserStatus.AWAY, statusText: 'Lunch', statusSource: 'external' }, + }), + [session()], + { type: 'setActive', newState: { statusDefault: UserStatus.AWAY, statusText: 'Heads down', statusSource: 'manual' } }, + ); + expect(result.values.statusText).toBe('Heads down'); + expect(result.clear).toContain('previousState'); + }); + }); + + describe('endActive', () => { + test('should reset to ONLINE when no previousState exists', () => { + const result = processPresence( + user({ statusSource: 'manual', statusDefault: UserStatus.BUSY, statusText: 'Design work' }), + [session()], + { type: 'endActive' }, + ); + expect(result.values).toMatchObject({ statusDefault: UserStatus.ONLINE }); + expect(result.clear).toEqual(expect.arrayContaining(['statusSource', 'statusExpiresAt', 'previousState'])); + }); + + test('should restore previousState when valid', () => { + const result = processPresence( + user({ + statusSource: 'internal', + statusDefault: UserStatus.BUSY, + previousState: { statusDefault: UserStatus.BUSY, statusText: 'Focusing', statusSource: 'manual' }, + }), + [session()], + { type: 'endActive' }, + ); + expect(result.values).toMatchObject({ statusSource: 'manual', statusText: 'Focusing' }); + expect(result.clear).toContain('previousState'); + }); + + test('should reset to ONLINE when previousState is expired', () => { + const result = processPresence( + user({ + statusSource: 'internal', + statusDefault: UserStatus.BUSY, + previousState: { + statusDefault: UserStatus.BUSY, + statusText: 'Focus', + statusSource: 'manual', + statusExpiresAt: new Date(Date.now() - 600_000), + }, + }), + [session()], + { type: 'endActive' }, + ); + expect(result.values).toMatchObject({ statusDefault: UserStatus.ONLINE }); + expect(result.clear).toContain('previousState'); + }); + + test('should restore previousState with expiresAt', () => { + const exp = new Date(Date.now() + ONE_HOUR); + const result = processPresence( + user({ + statusSource: 'internal', + statusDefault: UserStatus.BUSY, + previousState: { + statusDefault: UserStatus.BUSY, + statusText: 'Standup', + statusSource: 'external', + statusExpiresAt: exp, + }, + }), + [session()], + { type: 'endActive' }, + ); + expect(result.values).toMatchObject({ statusSource: 'external', statusExpiresAt: exp }); + expect(result.clear).toContain('previousState'); + }); + }); + + describe('clearActive', () => { + test('should reset to ONLINE and clear all claim fields', () => { + const result = processPresence(user({ statusSource: 'manual', statusDefault: UserStatus.BUSY }), [session()], { + type: 'clearActive', + }); + expect(result.values).toMatchObject({ statusDefault: UserStatus.ONLINE, statusText: '', status: UserStatus.ONLINE }); + expect(result.clear).toEqual(expect.arrayContaining(['statusSource', 'statusExpiresAt', 'previousState'])); + }); + + test('should defer to connection when statusDefault resets to ONLINE', () => { + const result = processPresence(user({ statusDefault: UserStatus.BUSY, statusSource: 'manual' }), [session(UserStatus.AWAY)], { + type: 'clearActive', + }); + expect(result.values.status).toBe(UserStatus.AWAY); + expect(result.values.statusDefault).toBe(UserStatus.ONLINE); + }); + }); + + describe('offline users', () => { + test('should reject non-manual claim when user is offline', () => { + const result = processPresence(user({ statusDefault: UserStatus.OFFLINE }), [], { + type: 'setActive', + newState: { statusDefault: UserStatus.BUSY, statusSource: 'external' }, + }); + expect(result.values).toStrictEqual({}); + }); + + test('should accept manual claim when user is offline', () => { + const result = processPresence(user({ statusDefault: UserStatus.OFFLINE }), [], { + type: 'setActive', + newState: { statusDefault: UserStatus.BUSY, statusSource: 'manual', statusText: 'Working' }, + }); + expect(result.values.statusSource).toBe('manual'); + // display status is OFFLINE because there is no active connection; + // statusDefault (BUSY) is persisted and will take effect on reconnect + expect(result.values.status).toBe(UserStatus.OFFLINE); + expect(result.values.statusConnection).toBe(UserStatus.OFFLINE); + }); + }); + + describe('fieldsToUnset', () => { + test('should unset expiresAt when absent from new claim', () => { + const result = processPresence(user({ statusExpiresAt: new Date() }), [session()], { + type: 'setActive', + newState: { statusDefault: UserStatus.BUSY, statusText: 'Focus', statusSource: 'manual' }, + }); + expect(result.clear).toContain('statusExpiresAt'); + }); + }); + + describe('computeStatus', () => { + test('should resolve to OFFLINE when session is OFFLINE regardless of claim', () => { + const result = processPresence(user(), [session(UserStatus.OFFLINE)], { + type: 'setActive', + newState: { statusDefault: UserStatus.BUSY, statusSource: 'manual' }, + }); + expect(result.values.status).toBe(UserStatus.OFFLINE); + }); + }); +}); diff --git a/ee/packages/presence/src/lib/presenceEngine.ts b/ee/packages/presence/src/lib/presenceEngine.ts new file mode 100644 index 0000000000000..48e0b0839b993 --- /dev/null +++ b/ee/packages/presence/src/lib/presenceEngine.ts @@ -0,0 +1,208 @@ +import type { IUser, IUserSessionConnection } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; + +export type ClaimUpdate = + | { type: 'setActive'; newState: Pick } + | { type: 'endActive' } + | { type: 'clearActive' }; + +// Priority: internal > manual > external (lower number = higher priority). +// System states (like auto-away) are not listed here and have the lowest priority. +const PRIORITY = { internal: 1, manual: 2, external: 3 }; +const NO_PRIORITY = 4; + +const RESET_TO_ONLINE = { + set: { statusDefault: UserStatus.ONLINE, statusText: '' }, + unset: ['statusSource', 'statusExpiresAt', 'previousState'], +}; + +function isExpired(expiresAt?: Date): boolean { + return expiresAt != null && expiresAt.getTime() < Date.now(); +} + +function fieldsToUnset(state: Pick, extra?: string[]): string[] { + const fields = new Set(extra); + if (!state.statusExpiresAt) { + fields.add('statusExpiresAt'); + } + return [...fields]; +} + +/** Reduces multiple DDP sessions into one status: ONLINE wins, + * then first non-OFFLINE. */ +function reduceConnections(current: UserStatus, status: UserStatus): UserStatus { + if (current === UserStatus.ONLINE) { + return UserStatus.ONLINE; + } + if (status !== UserStatus.OFFLINE) { + return status; + } + return current; +} + +/** + * Resolves final display status: OFFLINE connection always wins, + * explicit claims (busy/away) override connection, ONLINE defers to connection. + */ +function computeStatus(statusConnection: UserStatus, statusDefault: UserStatus): UserStatus { + if (statusConnection === UserStatus.OFFLINE) { + return UserStatus.OFFLINE; + } + if (statusDefault === UserStatus.ONLINE) { + return statusConnection; + } + return statusDefault; +} + +/** + * Resolves a claim update against the user's current state using the priority system. + * Returns the DB fields to set/unset, or null if the claim is rejected. + */ +function resolveIntent( + user: Pick, + claimUpdate: ClaimUpdate, +): { set: Record & { statusDefault?: UserStatus }; unset: string[] } | null { + const currentStatusDefault = user.statusDefault ?? UserStatus.ONLINE; + + if (claimUpdate.type === 'clearActive') { + return RESET_TO_ONLINE; + } + + if (claimUpdate.type === 'endActive') { + if (user.previousState && !isExpired(user.previousState.statusExpiresAt)) { + const prev = user.previousState; + return { + set: { + statusDefault: prev.statusDefault, + statusSource: prev.statusSource, + ...(prev.statusText != null && { statusText: prev.statusText }), + ...(prev.statusExpiresAt && { statusExpiresAt: prev.statusExpiresAt }), + }, + unset: fieldsToUnset(prev, ['previousState']), + }; + } + return RESET_TO_ONLINE; + } + + // type === 'setActive' + const { newState } = claimUpdate; + + // offline users can only have their status changed by manual sources + if (currentStatusDefault === UserStatus.OFFLINE && newState.statusSource !== 'manual') { + return null; + } + + const currentPriority = user.statusSource ? PRIORITY[user.statusSource] : NO_PRIORITY; + const newPriority = newState.statusSource ? PRIORITY[newState.statusSource] : NO_PRIORITY; + + // a manual claim is the user's explicit intent: when it wins it doesn't stash the + // displaced claim and clears any queued one, so it never gets auto-reverted. + const isManual = newState.statusSource === 'manual'; + + // higher priority -> apply new; stash displaced claim unless the new claim is manual + if (newPriority < currentPriority) { + const previousState = + !isManual && user.statusSource + ? { + statusDefault: currentStatusDefault, + statusText: user.statusText ?? '', + statusSource: user.statusSource, + statusExpiresAt: user.statusExpiresAt, + } + : undefined; + + return { + set: { + statusDefault: newState.statusDefault, + statusSource: newState.statusSource, + ...(newState.statusText != null && { statusText: newState.statusText }), + ...(newState.statusExpiresAt && { statusExpiresAt: newState.statusExpiresAt }), + ...(previousState && { previousState }), + }, + unset: fieldsToUnset(newState, isManual ? ['previousState'] : []), + }; + } + + // same priority -> overwrite; manual also drops any queued previous + if (newPriority === currentPriority) { + return { + set: { + statusDefault: newState.statusDefault, + statusSource: newState.statusSource, + ...(newState.statusText != null && { statusText: newState.statusText }), + ...(newState.statusExpiresAt && { statusExpiresAt: newState.statusExpiresAt }), + }, + unset: fieldsToUnset(newState, isManual ? ['previousState'] : []), + }; + } + + const previousState = { + statusDefault: newState.statusDefault, + statusSource: newState.statusSource, + ...(newState.statusText != null && { statusText: newState.statusText }), + ...(newState.statusExpiresAt && { statusExpiresAt: newState.statusExpiresAt }), + }; + + // lower priority -> save as previous if slot available + if (!user.previousState || isExpired(user.previousState.statusExpiresAt)) { + return { + set: { previousState }, + unset: [], + }; + } + + if ( + newState.statusSource && + user.previousState.statusSource && + PRIORITY[newState.statusSource] <= PRIORITY[user.previousState.statusSource] + ) { + return { + set: { previousState }, + unset: [], + }; + } + + // rejected - can't store + return null; +} + +/** + * Computes the final presence state for a user by combining claim intent with connection reality. + * Returns the DB fields to $set and optionally $unset. + */ +export function processPresence( + user: Pick, + sessions: IUserSessionConnection[], + claimUpdate?: ClaimUpdate, +): { values: Record; clear?: string[] } { + // no claim - status is determined entirely by DDP sessions + if (!claimUpdate) { + if (!sessions.length) { + return { values: { status: UserStatus.OFFLINE, statusConnection: UserStatus.OFFLINE } }; + } + const statusDefault = user.statusDefault ?? UserStatus.ONLINE; + const statusConnection = sessions.map((s) => s.status).reduce(reduceConnections, UserStatus.OFFLINE); + return { values: { status: computeStatus(statusConnection, statusDefault), statusConnection } }; + } + + const intent = resolveIntent(user, claimUpdate); + if (!intent) { + return { values: {} }; + } + + const { set, unset } = intent; + const statusDefault = set.statusDefault ?? user.statusDefault ?? UserStatus.ONLINE; + const clear = unset.length ? unset : undefined; + + // setActive with no DDP sessions: user is disconnected but holding a claim — persist + // it for reconnect but display OFFLINE. Other types (clearActive/endActive) use + // statusDefault so REST-only callers and bots can appear online. + if (!sessions.length) { + const status = claimUpdate.type === 'setActive' ? UserStatus.OFFLINE : statusDefault; + return { values: { ...set, status, statusConnection: UserStatus.OFFLINE }, clear }; + } + + const statusConnection = sessions.map((s) => s.status).reduce(reduceConnections, UserStatus.OFFLINE); + const status = computeStatus(statusConnection, statusDefault); + return { values: { ...set, status, statusConnection }, clear }; +} diff --git a/ee/packages/presence/src/lib/processConnectionStatus.ts b/ee/packages/presence/src/lib/processConnectionStatus.ts deleted file mode 100644 index a14aa45219e8a..0000000000000 --- a/ee/packages/presence/src/lib/processConnectionStatus.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { IUserSessionConnection } from '@rocket.chat/core-typings'; -import { UserStatus } from '@rocket.chat/core-typings'; - -/** - * Defines new connection status compared to a previous connection status - */ -export const processConnectionStatus = (current: UserStatus, status: UserStatus): UserStatus => { - if (current === UserStatus.ONLINE) { - return UserStatus.ONLINE; - } - if (status !== UserStatus.OFFLINE) { - return status; - } - return current; -}; - -/** - * Defines user's status based on presence and connection status - */ -export const processStatus = (statusConnection: UserStatus, statusDefault: UserStatus): UserStatus => { - if (statusConnection === UserStatus.OFFLINE) { - return statusConnection; - } - - if (statusDefault === UserStatus.ONLINE) { - return statusConnection; - } - - return statusDefault; -}; - -/** - * Defines user's status and connection status based on user's connections and default status - */ -export const processPresenceAndStatus = ( - userSessions: IUserSessionConnection[] = [], - statusDefault = UserStatus.ONLINE, -): { status: UserStatus; statusConnection: UserStatus } => { - const statusConnection = userSessions.map((s) => s.status).reduce(processConnectionStatus, UserStatus.OFFLINE); - - const status = processStatus(statusConnection, statusDefault); - - return { - status, - statusConnection, - }; -}; diff --git a/ee/packages/presence/tests/lib/processConnectionStatus.test.ts b/ee/packages/presence/tests/lib/processConnectionStatus.test.ts deleted file mode 100644 index 1646636d92b2e..0000000000000 --- a/ee/packages/presence/tests/lib/processConnectionStatus.test.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { describe, expect, test } from '@jest/globals'; -import { UserStatus } from '@rocket.chat/core-typings'; - -import { processConnectionStatus, processStatus, processPresenceAndStatus } from '../../src/lib/processConnectionStatus'; - -describe('Presence micro service', () => { - test('should return connection as online when there is a connection online', () => { - expect(processConnectionStatus(UserStatus.OFFLINE, UserStatus.ONLINE)).toBe(UserStatus.ONLINE); - expect(processConnectionStatus(UserStatus.ONLINE, UserStatus.ONLINE)).toBe(UserStatus.ONLINE); - expect(processConnectionStatus(UserStatus.BUSY, UserStatus.ONLINE)).toBe(UserStatus.ONLINE); - expect(processConnectionStatus(UserStatus.AWAY, UserStatus.ONLINE)).toBe(UserStatus.ONLINE); - }); - - test('should return the connections status if the other connection is offline', () => { - expect(processConnectionStatus(UserStatus.OFFLINE, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE); - expect(processConnectionStatus(UserStatus.ONLINE, UserStatus.OFFLINE)).toBe(UserStatus.ONLINE); - expect(processConnectionStatus(UserStatus.AWAY, UserStatus.OFFLINE)).toBe(UserStatus.AWAY); - }); - - test('should return the connection status when the default status is online', () => { - expect(processStatus(UserStatus.ONLINE, UserStatus.ONLINE)).toBe(UserStatus.ONLINE); - expect(processStatus(UserStatus.AWAY, UserStatus.ONLINE)).toBe(UserStatus.AWAY); - expect(processStatus(UserStatus.OFFLINE, UserStatus.ONLINE)).toBe(UserStatus.OFFLINE); - }); - - test('should return status busy when the default status is busy', () => { - expect(processStatus(UserStatus.ONLINE, UserStatus.BUSY)).toBe(UserStatus.BUSY); - expect(processStatus(UserStatus.AWAY, UserStatus.BUSY)).toBe(UserStatus.BUSY); - expect(processStatus(UserStatus.OFFLINE, UserStatus.BUSY)).toBe(UserStatus.OFFLINE); - }); - - test('should return status away when the default status is away', () => { - expect(processStatus(UserStatus.ONLINE, UserStatus.AWAY)).toBe(UserStatus.AWAY); - expect(processStatus(UserStatus.AWAY, UserStatus.AWAY)).toBe(UserStatus.AWAY); - expect(processStatus(UserStatus.OFFLINE, UserStatus.AWAY)).toBe(UserStatus.OFFLINE); - }); - - test('should return status offline when the default status is offline', () => { - expect(processStatus(UserStatus.ONLINE, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE); - expect(processStatus(UserStatus.AWAY, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE); - expect(processStatus(UserStatus.OFFLINE, UserStatus.OFFLINE)).toBe(UserStatus.OFFLINE); - }); - - test('should return correct status and statusConnection when connected once', () => { - expect( - processPresenceAndStatus( - [ - { - id: 'random', - instanceId: 'random', - status: UserStatus.ONLINE, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - ], - UserStatus.ONLINE, - ), - ).toStrictEqual({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE }); - - expect( - processPresenceAndStatus( - [ - { - id: 'random', - instanceId: 'random', - status: UserStatus.AWAY, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - ], - UserStatus.ONLINE, - ), - ).toStrictEqual({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY }); - - expect( - processPresenceAndStatus( - [ - { - id: 'random', - instanceId: 'random', - status: UserStatus.ONLINE, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - ], - UserStatus.BUSY, - ), - ).toStrictEqual({ status: UserStatus.BUSY, statusConnection: UserStatus.ONLINE }); - - expect( - processPresenceAndStatus( - [ - { - id: 'random', - instanceId: 'random', - status: UserStatus.ONLINE, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - ], - UserStatus.AWAY, - ), - ).toStrictEqual({ status: UserStatus.AWAY, statusConnection: UserStatus.ONLINE }); - - expect( - processPresenceAndStatus( - [ - { - id: 'random', - instanceId: 'random', - status: UserStatus.AWAY, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - ], - UserStatus.BUSY, - ), - ).toStrictEqual({ status: UserStatus.BUSY, statusConnection: UserStatus.AWAY }); - - expect( - processPresenceAndStatus( - [ - { - id: 'random', - instanceId: 'random', - status: UserStatus.ONLINE, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - ], - UserStatus.OFFLINE, - ), - ).toStrictEqual({ status: UserStatus.OFFLINE, statusConnection: UserStatus.ONLINE }); - - expect( - processPresenceAndStatus( - [ - { - id: 'random', - instanceId: 'random', - status: UserStatus.AWAY, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - ], - UserStatus.OFFLINE, - ), - ).toStrictEqual({ status: UserStatus.OFFLINE, statusConnection: UserStatus.AWAY }); - }); - - test('should return correct status and statusConnection when connected twice', () => { - expect( - processPresenceAndStatus( - [ - { - id: 'random', - instanceId: 'random', - status: UserStatus.ONLINE, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - { - id: 'random', - instanceId: 'random', - status: UserStatus.AWAY, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - ], - UserStatus.ONLINE, - ), - ).toStrictEqual({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE }); - - expect( - processPresenceAndStatus( - [ - { - id: 'random', - instanceId: 'random', - status: UserStatus.AWAY, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - { - id: 'random', - instanceId: 'random', - status: UserStatus.ONLINE, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - ], - UserStatus.ONLINE, - ), - ).toStrictEqual({ status: UserStatus.ONLINE, statusConnection: UserStatus.ONLINE }); - - expect( - processPresenceAndStatus( - [ - { - id: 'random', - instanceId: 'random', - status: UserStatus.AWAY, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - { - id: 'random', - instanceId: 'random', - status: UserStatus.AWAY, - _createdAt: new Date(), - _updatedAt: new Date(), - }, - ], - UserStatus.ONLINE, - ), - ).toStrictEqual({ status: UserStatus.AWAY, statusConnection: UserStatus.AWAY }); - }); - - test('should return correct status and statusConnection when not connected', () => { - expect(processPresenceAndStatus([], UserStatus.ONLINE)).toStrictEqual({ - status: UserStatus.OFFLINE, - statusConnection: UserStatus.OFFLINE, - }); - - expect(processPresenceAndStatus([], UserStatus.BUSY)).toStrictEqual({ - status: UserStatus.OFFLINE, - statusConnection: UserStatus.OFFLINE, - }); - - expect(processPresenceAndStatus([], UserStatus.AWAY)).toStrictEqual({ - status: UserStatus.OFFLINE, - statusConnection: UserStatus.OFFLINE, - }); - - expect(processPresenceAndStatus([], UserStatus.OFFLINE)).toStrictEqual({ - status: UserStatus.OFFLINE, - statusConnection: UserStatus.OFFLINE, - }); - }); -}); diff --git a/packages/apps-engine/src/definition/accessors/IUserUpdater.ts b/packages/apps-engine/src/definition/accessors/IUserUpdater.ts index 2a869a5b99968..f73394003ca79 100644 --- a/packages/apps-engine/src/definition/accessors/IUserUpdater.ts +++ b/packages/apps-engine/src/definition/accessors/IUserUpdater.ts @@ -15,4 +15,9 @@ export interface IUserUpdater { updateBio(user: IUser, bio: IUser['bio']): Promise; updateCustomFields(user: IUser, customFields: IUser['customFields']): Promise; deactivate(userId: IUser['id'], confirmRelinquish: boolean): Promise; + setActiveState( + userId: IUser['id'], + state: Pick, + ): Promise; + endActiveState(userId: IUser['id']): Promise; } diff --git a/packages/apps-engine/src/definition/users/IUser.ts b/packages/apps-engine/src/definition/users/IUser.ts index a4e0daac7dbe3..73f3a41f0a68d 100644 --- a/packages/apps-engine/src/definition/users/IUser.ts +++ b/packages/apps-engine/src/definition/users/IUser.ts @@ -15,6 +15,9 @@ export interface IUser { status: string; statusConnection: UserStatusConnection; statusText?: string; + statusDefault?: string; + statusSource?: 'internal' | 'external' | 'manual'; + statusExpiresAt?: Date; utcOffset: number; createdAt: Date; updatedAt: Date; diff --git a/packages/apps/src/server/accessors/UserUpdater.ts b/packages/apps/src/server/accessors/UserUpdater.ts index bbb0252f33f8e..086e7fa357a01 100644 --- a/packages/apps/src/server/accessors/UserUpdater.ts +++ b/packages/apps/src/server/accessors/UserUpdater.ts @@ -29,4 +29,15 @@ export class UserUpdater implements IUserUpdater { public async deactivate(userId: IUser['id'], confirmRelinquish: boolean) { return this.bridges.getUserBridge().doDeactivate(userId, confirmRelinquish, this.appId); } + + public async setActiveState( + userId: IUser['id'], + state: Pick, + ) { + return this.bridges.getUserBridge().doSetActiveState(userId, state, this.appId); + } + + public async endActiveState(userId: IUser['id']) { + return this.bridges.getUserBridge().doEndActiveState(userId, this.appId); + } } diff --git a/packages/apps/src/server/bridges/UserBridge.ts b/packages/apps/src/server/bridges/UserBridge.ts index 92deb6ee88c39..a8bd300ee3094 100644 --- a/packages/apps/src/server/bridges/UserBridge.ts +++ b/packages/apps/src/server/bridges/UserBridge.ts @@ -70,6 +70,22 @@ export abstract class UserBridge extends BaseBridge { } } + public async doSetActiveState( + userId: IUser['id'], + state: Pick, + appId: string, + ): Promise { + if (this.hasWritePermission(appId)) { + await this.setActiveState(userId, state, appId); + } + } + + public async doEndActiveState(userId: IUser['id'], appId: string): Promise { + if (this.hasWritePermission(appId)) { + await this.endActiveState(userId, appId); + } + } + protected abstract getById(id: string, appId: string): Promise; protected abstract getByUsername(username: string, appId: string): Promise; @@ -134,6 +150,14 @@ export abstract class UserBridge extends BaseBridge { */ protected abstract deactivate(userId: IUser['id'], confirmRelinquish: boolean, appId: string): Promise; + protected abstract setActiveState( + userId: IUser['id'], + state: Pick, + appId: string, + ): Promise; + + protected abstract endActiveState(userId: IUser['id'], appId: string): Promise; + private hasReadPermission(appId: string): boolean { if (AppPermissionManager.hasPermission(appId, AppPermissions.user.read)) { return true; diff --git a/packages/apps/tests/test-data/bridges/userBridge.ts b/packages/apps/tests/test-data/bridges/userBridge.ts index 071b80f488e1f..b8634a0e07e79 100644 --- a/packages/apps/tests/test-data/bridges/userBridge.ts +++ b/packages/apps/tests/test-data/bridges/userBridge.ts @@ -46,4 +46,16 @@ export class TestsUserBridge extends UserBridge { protected deactivate(userId: IUser['id'], confirmRelinquish: boolean, appId: string): Promise { throw new Error('Method not implemented.'); } + + protected setActiveState( + userId: IUser['id'], + state: Pick, + appId: string, + ): Promise { + throw new Error('Method not implemented.'); + } + + protected endActiveState(userId: IUser['id'], appId: string): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/packages/core-services/src/events/Events.ts b/packages/core-services/src/events/Events.ts index aae9118f565ae..397553b37caf3 100644 --- a/packages/core-services/src/events/Events.ts +++ b/packages/core-services/src/events/Events.ts @@ -158,7 +158,7 @@ export type EventSignatures = { }; }): void; 'presence.status'(data: { - user: Pick; + user: Pick; previousStatus: UserStatus | undefined; }): void; 'watch.messages'(data: { message: IMessage }): void; diff --git a/packages/core-services/src/types/IPresence.ts b/packages/core-services/src/types/IPresence.ts index 268fed8cd1e66..bc414a956801a 100644 --- a/packages/core-services/src/types/IPresence.ts +++ b/packages/core-services/src/types/IPresence.ts @@ -1,4 +1,4 @@ -import type { UserStatus } from '@rocket.chat/core-typings'; +import type { IUser, UserStatus } from '@rocket.chat/core-typings'; import type { IServiceClass } from './ServiceClass'; @@ -15,9 +15,14 @@ export interface IPresence extends IServiceClass { ): Promise<{ uid: string; session: string } | undefined>; updateConnection(uid: string, connectionId: string): Promise<{ uid: string; connectionId: string } | undefined>; removeLostConnections(nodeID: string): Promise; - setStatus(uid: string, status: UserStatus, statusText?: string): Promise; + setStatus(userId: string, status: UserStatus, statusText?: string, statusExpiresAt?: Date): Promise; + setActiveState( + userId: string, + newState: Pick, + ): Promise; + endActiveState(userId: string): Promise; + clearActiveState(userId: string): Promise; setConnectionStatus(uid: string, status: UserStatus, session: string): Promise; - updateUserPresence(uid: string): Promise; toggleBroadcast(enabled: boolean): void; getConnectionCount(): { current: number; max: number }; getPeakConnections(reset?: boolean): number; diff --git a/packages/core-typings/src/IMeApiUser.ts b/packages/core-typings/src/IMeApiUser.ts index e51b1863312f4..809f5f32d28f8 100644 --- a/packages/core-typings/src/IMeApiUser.ts +++ b/packages/core-typings/src/IMeApiUser.ts @@ -28,6 +28,8 @@ type MeProjectedUserFields = Pick< | 'status' | 'statusDefault' | 'statusText' + | 'statusSource' + | 'statusExpiresAt' | 'statusConnection' | 'bio' | 'avatarOrigin' diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index 857d4a07fd91b..aedcdfec0d759 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -1,7 +1,7 @@ import type { IAbacAttributeDefinition } from './IAbacAttribute'; import type { IRocketChatRecord } from './IRocketChatRecord'; import type { IRole } from './IRole'; -import type { UserStatus } from './UserStatus'; +import type { PresenceSource, UserStatus } from './UserStatus'; import type { Serialized } from './utils'; export interface ILoginToken { @@ -187,6 +187,14 @@ export interface IUser extends IRocketChatRecord { language?: string; statusDefault?: UserStatus; statusText?: string; + statusSource?: PresenceSource; + statusExpiresAt?: Date; + previousState?: { + statusDefault: UserStatus; + statusText: string; + statusSource: PresenceSource; + statusExpiresAt?: Date; + }; oauth?: { authorizedClients: string[]; }; diff --git a/packages/core-typings/src/UserStatus.ts b/packages/core-typings/src/UserStatus.ts index 97f9b94e3897b..eb3b56f4af1f4 100644 --- a/packages/core-typings/src/UserStatus.ts +++ b/packages/core-typings/src/UserStatus.ts @@ -5,3 +5,5 @@ export enum UserStatus { BUSY = 'busy', DISABLED = 'disabled', } + +export type PresenceSource = 'internal' | 'external' | 'manual'; diff --git a/packages/ddp-client/src/types/streams.ts b/packages/ddp-client/src/types/streams.ts index 2c89e027c776f..665368b13f654 100644 --- a/packages/ddp-client/src/types/streams.ts +++ b/packages/ddp-client/src/types/streams.ts @@ -32,7 +32,6 @@ import type * as UiKit from '@rocket.chat/ui-kit'; type ClientAction = 'inserted' | 'updated' | 'removed' | 'changed'; -// eslint-disable-next-line @typescript-eslint/naming-convention export interface StreamerEvents { 'roles': [ { @@ -234,6 +233,8 @@ export interface StreamerEvents { statusText: IUser['statusText'], name: IUser['name'], roles: IUser['roles'], + statusSource?: IUser['statusSource'], + statusExpiresAt?: IUser['statusExpiresAt'], ], ]; }, @@ -298,7 +299,20 @@ export interface StreamerEvents { }, ]; - 'user-presence': [{ key: string; args: [[username: string, statusChanged?: PresenceStatusCode, statusText?: string]] }]; + 'user-presence': [ + { + key: string; + args: [ + [ + username: string, + statusChanged?: PresenceStatusCode, + statusText?: string, + statusSource?: IUser['statusSource'], + statusExpiresAt?: IUser['statusExpiresAt'], + ], + ]; + }, + ]; // TODO: rename to 'integration-history' 'integrationHistory': [ diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index fcb82c2c1ae0d..43282b597762a 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -172,6 +172,31 @@ export interface IUsersModel extends IBaseModel { updateStatusText(_id: IUser['_id'], statusText: string, options?: UpdateOptions): Promise; + findExpiredStatuses(): FindCursor< + Pick< + IUser, + | '_id' + | 'username' + | 'roles' + | 'status' + | 'statusDefault' + | 'statusSource' + | 'statusText' + | 'statusExpiresAt' + | 'statusConnection' + | 'previousState' + > + >; + + findNextStatusExpiration(): Promise | null>; + + updatePresenceAndStatus( + userId: IUser['_id'], + values: Record, + clear?: string[], + extraFilter?: Filter, + ): Promise; + updateStatusByAppId(appId: string, status: UserStatus): Promise; openAgentsBusinessHoursByBusinessHourId(businessHourIds: string[]): Promise; diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index 27a2fd3461152..666fb419847b2 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -59,6 +59,7 @@ export class UsersRaw extends BaseRaw> implements IU { key: { status: 1 } }, { key: { statusText: 1 } }, { key: { statusConnection: 1 }, sparse: true }, + { key: { statusExpiresAt: 1 }, partialFilterExpression: { statusExpiresAt: { $exists: true } } }, { key: { appId: 1 }, sparse: true }, { key: { type: 1 } }, { key: { federated: 1 }, sparse: true }, @@ -1096,25 +1097,67 @@ export class UsersRaw extends BaseRaw> implements IU return this.updateOne({ _id }, update, { session: options?.session }); } - updateStatus(_id: IUser['_id'], status: UserStatus) { - const update = { - $set: { - status, + findExpiredStatuses() { + return this.find< + Pick< + IUser, + | '_id' + | 'username' + | 'roles' + | 'status' + | 'statusDefault' + | 'statusSource' + | 'statusText' + | 'statusExpiresAt' + | 'statusConnection' + | 'previousState' + > + >( + { statusExpiresAt: { $lt: new Date() } }, + { + projection: { + username: 1, + roles: 1, + status: 1, + statusDefault: 1, + statusSource: 1, + statusText: 1, + statusExpiresAt: 1, + statusConnection: 1, + previousState: 1, + }, + sort: { statusExpiresAt: 1 }, }, - }; + ); + } - return this.updateOne({ _id }, update); + findNextStatusExpiration() { + return this.findOne>( + { statusExpiresAt: { $gte: new Date() } }, + { projection: { _id: 1, statusExpiresAt: 1 }, sort: { statusExpiresAt: 1 } }, + ); } - updateStatusAndStatusDefault(_id: IUser['_id'], status: UserStatus, statusDefault: UserStatus) { - const update = { - $set: { - status, - statusDefault, + updatePresenceAndStatus(userId: IUser['_id'], values: Record, clear?: string[], extraFilter?: Filter) { + const $unset = clear?.length ? Object.fromEntries(clear.map((field) => [field, '' as const])) : undefined; + + return this.findOneAndUpdate( + { + ...extraFilter, + _id: userId, }, - }; + { + $set: values, + ...($unset && { $unset }), + }, + { + returnDocument: 'after', + }, + ); + } - return this.updateOne({ _id }, update); + updateStatusAndStatusDefault(_id: IUser['_id'], status: UserStatus, statusDefault: UserStatus) { + return this.updateOne({ _id }, { $set: { status, statusDefault } }); } updateStatusByAppId(appId: string, status: UserStatus) { diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index dda7d95c75330..884fe2eeb7735 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -318,15 +318,23 @@ export type UsersEndpoints = { }; '/v1/users.setStatus': { - POST: (params: { message?: string; status?: UserStatus; userId?: string; username?: string; user?: string }) => void; + POST: (params: { + message?: string; + status?: UserStatus; + expiresAt?: string; + userId?: string; + username?: string; + user?: string; + }) => void; }; '/v1/users.getStatus': { GET: () => { + _id: string; status: 'online' | 'offline' | 'away' | 'busy'; - message?: string; - _id?: string; connectionStatus?: 'online' | 'offline' | 'away' | 'busy'; + statusSource?: IUser['statusSource']; + statusExpiresAt?: IUser['statusExpiresAt']; }; }; From e141edab88aab365f7e8371091b17df20d90ff72 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Tue, 16 Jun 2026 09:35:37 -0300 Subject: [PATCH 02/11] fix: S3 file upload failing on empty region or scheme-less endpoint (#40759) Co-authored-by: Abbie - Yu Luo <149909888+abbiekuma@users.noreply.github.com> --- .changeset/crisp-corners-itch.md | 5 +++++ .../app/file-upload/server/config/AmazonS3.ts | 18 +++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .changeset/crisp-corners-itch.md diff --git a/.changeset/crisp-corners-itch.md b/.changeset/crisp-corners-itch.md new file mode 100644 index 0000000000000..1a02cccba6f9a --- /dev/null +++ b/.changeset/crisp-corners-itch.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes S3 file upload failing when the region setting is empty or the endpoint is configured without a URL scheme diff --git a/apps/meteor/app/file-upload/server/config/AmazonS3.ts b/apps/meteor/app/file-upload/server/config/AmazonS3.ts index 77a554e46b7af..f5129b1a8daca 100644 --- a/apps/meteor/app/file-upload/server/config/AmazonS3.ts +++ b/apps/meteor/app/file-upload/server/config/AmazonS3.ts @@ -4,11 +4,14 @@ import https from 'node:https'; import _ from 'underscore'; import { forceDownload } from './helper'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import type { S3Options } from '../../ufs/AmazonS3/server'; import { FileUploadClass, FileUpload } from '../lib/FileUpload'; import '../../ufs/AmazonS3/server'; +const hasScheme = (value: string) => /^[a-z][a-z0-9+.-]*:\/\//i.test(value); + const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, req, res) { const forcedDownload = forceDownload(req); @@ -94,6 +97,14 @@ const configure = _.debounce(() => { config.connection.region = Region; } + // Back-compat: AWS SDK v2 defaulted unset region to us-east-1; v3 throws. + if (!Region && !process.env.AWS_REGION) { + config.connection.region = 'us-east-1'; + SystemLogger.warn( + 'FileUpload_S3_Region is empty and AWS_REGION is not set; defaulting to us-east-1. Set FileUpload_S3_Region or AWS_REGION to silence this warning.', + ); + } + if (AWSAccessKeyId && AWSSecretAccessKey) { config.connection.credentials = { accessKeyId: AWSAccessKeyId, @@ -101,8 +112,13 @@ const configure = _.debounce(() => { }; } + // Back-compat: AWS SDK v2 accepted scheme-less endpoints; v3 throws. if (BucketURL) { - config.connection.endpoint = BucketURL; + const isValidScheme = hasScheme(BucketURL); + config.connection.endpoint = isValidScheme ? BucketURL : `https://${BucketURL}`; + if (!isValidScheme) { + SystemLogger.warn(`FileUpload_S3_BucketURL "${BucketURL}" has no scheme; defaulting to "https://${BucketURL}".`); + } } AmazonS3Uploads.store = FileUpload.configureUploadsStore('AmazonS3', AmazonS3Uploads.name, config); From 2a58e30d840f0cde2572c81b05dbc974d91219c5 Mon Sep 17 00:00:00 2001 From: Yasmim Nagat <117310290+yasnagat@users.noreply.github.com> Date: Tue, 16 Jun 2026 10:02:43 -0300 Subject: [PATCH 03/11] chore(deps): bump esbuild (#40962) Co-authored-by: Julio Araujo Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- package.json | 1 + packages/apps/package.json | 2 +- packages/release-action/package.json | 2 +- yarn.lock | 355 +++++++++++++-------------- 4 files changed, 180 insertions(+), 180 deletions(-) diff --git a/package.json b/package.json index 3d5f77fc8723e..4f940944ab8db 100644 --- a/package.json +++ b/package.json @@ -165,6 +165,7 @@ "@opentelemetry/exporter-metrics-otlp-grpc/@grpc/grpc-js": "^1.14.4", "@opentelemetry/exporter-trace-otlp-grpc/@grpc/grpc-js": "^1.14.4", "@opentelemetry/otlp-grpc-exporter-base/@grpc/grpc-js": "^1.14.4", + "storybook/esbuild": "~0.28.1", "qs": "6.15.2", "uuid@npm:13": "13.0.1" }, diff --git a/packages/apps/package.json b/packages/apps/package.json index 6770316b6486b..960c6ad7e7a85 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -28,7 +28,7 @@ "@rocket.chat/model-typings": "workspace:^", "adm-zip": "^0.5.16", "debug": "^4.3.7", - "esbuild": "~0.27.3", + "esbuild": "~0.28.1", "jose": "^4.15.9", "jsonrpc-lite": "^2.2.0", "lodash.clonedeep": "^4.5.0", diff --git a/packages/release-action/package.json b/packages/release-action/package.json index 50da999513f3c..5090490d13dce 100644 --- a/packages/release-action/package.json +++ b/packages/release-action/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@types/node": "~22.19.17", - "esbuild": "~0.25.10", + "esbuild": "~0.28.1", "eslint": "~9.39.4", "typescript": "~5.9.3" }, diff --git a/yarn.lock b/yarn.lock index ef99a9b8992f2..5b82c70e5cbe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3480,13 +3480,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/aix-ppc64@npm:0.25.12" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.27.7": version: 0.27.7 resolution: "@esbuild/aix-ppc64@npm:0.27.7" @@ -3494,10 +3487,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/android-arm64@npm:0.25.12" - conditions: os=android & cpu=arm64 +"@esbuild/aix-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/aix-ppc64@npm:0.28.1" + conditions: os=aix & cpu=ppc64 languageName: node linkType: hard @@ -3508,10 +3501,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/android-arm@npm:0.25.12" - conditions: os=android & cpu=arm +"@esbuild/android-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm64@npm:0.28.1" + conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -3522,10 +3515,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/android-x64@npm:0.25.12" - conditions: os=android & cpu=x64 +"@esbuild/android-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm@npm:0.28.1" + conditions: os=android & cpu=arm languageName: node linkType: hard @@ -3536,10 +3529,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/darwin-arm64@npm:0.25.12" - conditions: os=darwin & cpu=arm64 +"@esbuild/android-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-x64@npm:0.28.1" + conditions: os=android & cpu=x64 languageName: node linkType: hard @@ -3550,10 +3543,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/darwin-x64@npm:0.25.12" - conditions: os=darwin & cpu=x64 +"@esbuild/darwin-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-arm64@npm:0.28.1" + conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -3564,10 +3557,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/freebsd-arm64@npm:0.25.12" - conditions: os=freebsd & cpu=arm64 +"@esbuild/darwin-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-x64@npm:0.28.1" + conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -3578,10 +3571,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/freebsd-x64@npm:0.25.12" - conditions: os=freebsd & cpu=x64 +"@esbuild/freebsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-arm64@npm:0.28.1" + conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -3592,10 +3585,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-arm64@npm:0.25.12" - conditions: os=linux & cpu=arm64 +"@esbuild/freebsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-x64@npm:0.28.1" + conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -3606,10 +3599,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-arm@npm:0.25.12" - conditions: os=linux & cpu=arm +"@esbuild/linux-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm64@npm:0.28.1" + conditions: os=linux & cpu=arm64 languageName: node linkType: hard @@ -3620,10 +3613,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-ia32@npm:0.25.12" - conditions: os=linux & cpu=ia32 +"@esbuild/linux-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm@npm:0.28.1" + conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -3634,10 +3627,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-loong64@npm:0.25.12" - conditions: os=linux & cpu=loong64 +"@esbuild/linux-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ia32@npm:0.28.1" + conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -3648,10 +3641,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-mips64el@npm:0.25.12" - conditions: os=linux & cpu=mips64el +"@esbuild/linux-loong64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-loong64@npm:0.28.1" + conditions: os=linux & cpu=loong64 languageName: node linkType: hard @@ -3662,10 +3655,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-ppc64@npm:0.25.12" - conditions: os=linux & cpu=ppc64 +"@esbuild/linux-mips64el@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-mips64el@npm:0.28.1" + conditions: os=linux & cpu=mips64el languageName: node linkType: hard @@ -3676,10 +3669,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-riscv64@npm:0.25.12" - conditions: os=linux & cpu=riscv64 +"@esbuild/linux-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ppc64@npm:0.28.1" + conditions: os=linux & cpu=ppc64 languageName: node linkType: hard @@ -3690,10 +3683,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-s390x@npm:0.25.12" - conditions: os=linux & cpu=s390x +"@esbuild/linux-riscv64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-riscv64@npm:0.28.1" + conditions: os=linux & cpu=riscv64 languageName: node linkType: hard @@ -3704,10 +3697,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-x64@npm:0.25.12" - conditions: os=linux & cpu=x64 +"@esbuild/linux-s390x@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-s390x@npm:0.28.1" + conditions: os=linux & cpu=s390x languageName: node linkType: hard @@ -3718,10 +3711,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/netbsd-arm64@npm:0.25.12" - conditions: os=netbsd & cpu=arm64 +"@esbuild/linux-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-x64@npm:0.28.1" + conditions: os=linux & cpu=x64 languageName: node linkType: hard @@ -3732,10 +3725,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/netbsd-x64@npm:0.25.12" - conditions: os=netbsd & cpu=x64 +"@esbuild/netbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-arm64@npm:0.28.1" + conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard @@ -3746,10 +3739,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/openbsd-arm64@npm:0.25.12" - conditions: os=openbsd & cpu=arm64 +"@esbuild/netbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-x64@npm:0.28.1" + conditions: os=netbsd & cpu=x64 languageName: node linkType: hard @@ -3760,10 +3753,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/openbsd-x64@npm:0.25.12" - conditions: os=openbsd & cpu=x64 +"@esbuild/openbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-arm64@npm:0.28.1" + conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard @@ -3774,10 +3767,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/openharmony-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/openharmony-arm64@npm:0.25.12" - conditions: os=openharmony & cpu=arm64 +"@esbuild/openbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-x64@npm:0.28.1" + conditions: os=openbsd & cpu=x64 languageName: node linkType: hard @@ -3788,10 +3781,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/sunos-x64@npm:0.25.12" - conditions: os=sunos & cpu=x64 +"@esbuild/openharmony-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openharmony-arm64@npm:0.28.1" + conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard @@ -3802,10 +3795,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/win32-arm64@npm:0.25.12" - conditions: os=win32 & cpu=arm64 +"@esbuild/sunos-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/sunos-x64@npm:0.28.1" + conditions: os=sunos & cpu=x64 languageName: node linkType: hard @@ -3816,10 +3809,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/win32-ia32@npm:0.25.12" - conditions: os=win32 & cpu=ia32 +"@esbuild/win32-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-arm64@npm:0.28.1" + conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -3830,10 +3823,10 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/win32-x64@npm:0.25.12" - conditions: os=win32 & cpu=x64 +"@esbuild/win32-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-ia32@npm:0.28.1" + conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -3844,6 +3837,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-x64@npm:0.28.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": version: 4.9.1 resolution: "@eslint-community/eslint-utils@npm:4.9.1" @@ -7292,7 +7292,7 @@ __metadata: "@react-aria/overlays@patch:@react-aria/overlays@npm%3A3.25.0#~/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch": version: 3.25.0 - resolution: "@react-aria/overlays@patch:@react-aria/overlays@npm%3A3.25.0#~/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch::version=3.25.0&hash=5cb6f6" + resolution: "@react-aria/overlays@patch:@react-aria/overlays@npm%3A3.25.0#~/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch::version=3.25.0&hash=8f84db" dependencies: "@react-aria/focus": "npm:^3.19.1" "@react-aria/i18n": "npm:^3.12.5" @@ -7308,7 +7308,7 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 - checksum: 10/dd5b420d0cda39dbd9c60a5257e2783d22196b77ec98b3982a2176abb25d117f3593f49da7ee2e6df8799e0a9dc43b7d88b503d419f3d57310b041bb3f781f7b + checksum: 10/0a13ca74216d854e8fb7d3e055398eabfe8cb972f8ccc12fbd5dc59716a2dac8eb16897cecb03b936a60f2b79c64f27e23fa563059ef181f753f751890c63230 languageName: node linkType: hard @@ -9047,7 +9047,7 @@ __metadata: "@types/uuid": "npm:~10.0.0" adm-zip: "npm:^0.5.16" debug: "npm:^4.3.7" - esbuild: "npm:~0.27.3" + esbuild: "npm:~0.28.1" eslint: "npm:~9.39.4" jose: "npm:^4.15.9" jsonrpc-lite: "npm:^2.2.0" @@ -10763,7 +10763,7 @@ __metadata: "@octokit/core": "npm:^7.0.6" "@octokit/plugin-throttling": "npm:^11.0.3" "@types/node": "npm:~22.19.17" - esbuild: "npm:~0.25.10" + esbuild: "npm:~0.28.1" eslint: "npm:~9.39.4" mdast-util-to-string: "npm:2.0.0" remark-parse: "npm:9.0.0" @@ -21115,36 +21115,36 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0, esbuild@npm:~0.25.10": - version: 0.25.12 - resolution: "esbuild@npm:0.25.12" - dependencies: - "@esbuild/aix-ppc64": "npm:0.25.12" - "@esbuild/android-arm": "npm:0.25.12" - "@esbuild/android-arm64": "npm:0.25.12" - "@esbuild/android-x64": "npm:0.25.12" - "@esbuild/darwin-arm64": "npm:0.25.12" - "@esbuild/darwin-x64": "npm:0.25.12" - "@esbuild/freebsd-arm64": "npm:0.25.12" - "@esbuild/freebsd-x64": "npm:0.25.12" - "@esbuild/linux-arm": "npm:0.25.12" - "@esbuild/linux-arm64": "npm:0.25.12" - "@esbuild/linux-ia32": "npm:0.25.12" - "@esbuild/linux-loong64": "npm:0.25.12" - "@esbuild/linux-mips64el": "npm:0.25.12" - "@esbuild/linux-ppc64": "npm:0.25.12" - "@esbuild/linux-riscv64": "npm:0.25.12" - "@esbuild/linux-s390x": "npm:0.25.12" - "@esbuild/linux-x64": "npm:0.25.12" - "@esbuild/netbsd-arm64": "npm:0.25.12" - "@esbuild/netbsd-x64": "npm:0.25.12" - "@esbuild/openbsd-arm64": "npm:0.25.12" - "@esbuild/openbsd-x64": "npm:0.25.12" - "@esbuild/openharmony-arm64": "npm:0.25.12" - "@esbuild/sunos-x64": "npm:0.25.12" - "@esbuild/win32-arm64": "npm:0.25.12" - "@esbuild/win32-ia32": "npm:0.25.12" - "@esbuild/win32-x64": "npm:0.25.12" +"esbuild@npm:~0.27.0": + version: 0.27.7 + resolution: "esbuild@npm:0.27.7" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.7" + "@esbuild/android-arm": "npm:0.27.7" + "@esbuild/android-arm64": "npm:0.27.7" + "@esbuild/android-x64": "npm:0.27.7" + "@esbuild/darwin-arm64": "npm:0.27.7" + "@esbuild/darwin-x64": "npm:0.27.7" + "@esbuild/freebsd-arm64": "npm:0.27.7" + "@esbuild/freebsd-x64": "npm:0.27.7" + "@esbuild/linux-arm": "npm:0.27.7" + "@esbuild/linux-arm64": "npm:0.27.7" + "@esbuild/linux-ia32": "npm:0.27.7" + "@esbuild/linux-loong64": "npm:0.27.7" + "@esbuild/linux-mips64el": "npm:0.27.7" + "@esbuild/linux-ppc64": "npm:0.27.7" + "@esbuild/linux-riscv64": "npm:0.27.7" + "@esbuild/linux-s390x": "npm:0.27.7" + "@esbuild/linux-x64": "npm:0.27.7" + "@esbuild/netbsd-arm64": "npm:0.27.7" + "@esbuild/netbsd-x64": "npm:0.27.7" + "@esbuild/openbsd-arm64": "npm:0.27.7" + "@esbuild/openbsd-x64": "npm:0.27.7" + "@esbuild/openharmony-arm64": "npm:0.27.7" + "@esbuild/sunos-x64": "npm:0.27.7" + "@esbuild/win32-arm64": "npm:0.27.7" + "@esbuild/win32-ia32": "npm:0.27.7" + "@esbuild/win32-x64": "npm:0.27.7" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -21200,40 +21200,40 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10/bc9c03d64e96a0632a926662c9d29decafb13a40e5c91790f632f02939bc568edc9abe0ee5d8055085a2819a00139eb12e223cfb8126dbf89bbc569f125d91fd + checksum: 10/262b16c4a33cb70e9f054759a7ce420541649315eef7b064172c795021ccce322e56c3f5fd52e8842873f1c23745f3ab62311a24860950bd5406ba77b36b8529 languageName: node linkType: hard -"esbuild@npm:~0.27.0, esbuild@npm:~0.27.3": - version: 0.27.7 - resolution: "esbuild@npm:0.27.7" - dependencies: - "@esbuild/aix-ppc64": "npm:0.27.7" - "@esbuild/android-arm": "npm:0.27.7" - "@esbuild/android-arm64": "npm:0.27.7" - "@esbuild/android-x64": "npm:0.27.7" - "@esbuild/darwin-arm64": "npm:0.27.7" - "@esbuild/darwin-x64": "npm:0.27.7" - "@esbuild/freebsd-arm64": "npm:0.27.7" - "@esbuild/freebsd-x64": "npm:0.27.7" - "@esbuild/linux-arm": "npm:0.27.7" - "@esbuild/linux-arm64": "npm:0.27.7" - "@esbuild/linux-ia32": "npm:0.27.7" - "@esbuild/linux-loong64": "npm:0.27.7" - "@esbuild/linux-mips64el": "npm:0.27.7" - "@esbuild/linux-ppc64": "npm:0.27.7" - "@esbuild/linux-riscv64": "npm:0.27.7" - "@esbuild/linux-s390x": "npm:0.27.7" - "@esbuild/linux-x64": "npm:0.27.7" - "@esbuild/netbsd-arm64": "npm:0.27.7" - "@esbuild/netbsd-x64": "npm:0.27.7" - "@esbuild/openbsd-arm64": "npm:0.27.7" - "@esbuild/openbsd-x64": "npm:0.27.7" - "@esbuild/openharmony-arm64": "npm:0.27.7" - "@esbuild/sunos-x64": "npm:0.27.7" - "@esbuild/win32-arm64": "npm:0.27.7" - "@esbuild/win32-ia32": "npm:0.27.7" - "@esbuild/win32-x64": "npm:0.27.7" +"esbuild@npm:~0.28.1": + version: 0.28.1 + resolution: "esbuild@npm:0.28.1" + dependencies: + "@esbuild/aix-ppc64": "npm:0.28.1" + "@esbuild/android-arm": "npm:0.28.1" + "@esbuild/android-arm64": "npm:0.28.1" + "@esbuild/android-x64": "npm:0.28.1" + "@esbuild/darwin-arm64": "npm:0.28.1" + "@esbuild/darwin-x64": "npm:0.28.1" + "@esbuild/freebsd-arm64": "npm:0.28.1" + "@esbuild/freebsd-x64": "npm:0.28.1" + "@esbuild/linux-arm": "npm:0.28.1" + "@esbuild/linux-arm64": "npm:0.28.1" + "@esbuild/linux-ia32": "npm:0.28.1" + "@esbuild/linux-loong64": "npm:0.28.1" + "@esbuild/linux-mips64el": "npm:0.28.1" + "@esbuild/linux-ppc64": "npm:0.28.1" + "@esbuild/linux-riscv64": "npm:0.28.1" + "@esbuild/linux-s390x": "npm:0.28.1" + "@esbuild/linux-x64": "npm:0.28.1" + "@esbuild/netbsd-arm64": "npm:0.28.1" + "@esbuild/netbsd-x64": "npm:0.28.1" + "@esbuild/openbsd-arm64": "npm:0.28.1" + "@esbuild/openbsd-x64": "npm:0.28.1" + "@esbuild/openharmony-arm64": "npm:0.28.1" + "@esbuild/sunos-x64": "npm:0.28.1" + "@esbuild/win32-arm64": "npm:0.28.1" + "@esbuild/win32-ia32": "npm:0.28.1" + "@esbuild/win32-x64": "npm:0.28.1" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -21289,7 +21289,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10/262b16c4a33cb70e9f054759a7ce420541649315eef7b064172c795021ccce322e56c3f5fd52e8842873f1c23745f3ab62311a24860950bd5406ba77b36b8529 + checksum: 10/aaa4a922644afffac45e735c99caf343f881e2d36abcc6b6fb53c230bd69940504a5bb6b0041bdd1a690e748ebc681d3308a7d178987c523d74c63c2c280bac8 languageName: node linkType: hard @@ -23086,7 +23086,7 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.10.1, get-tsconfig@npm:^4.7.5": +"get-tsconfig@npm:^4.10.1": version: 4.10.1 resolution: "get-tsconfig@npm:4.10.1" dependencies: @@ -35914,18 +35914,17 @@ __metadata: linkType: hard "tsx@npm:~4.21.0": - version: 4.21.0 - resolution: "tsx@npm:4.21.0" + version: 4.21.1 + resolution: "tsx@npm:4.21.1" dependencies: esbuild: "npm:~0.27.0" fsevents: "npm:~2.3.3" - get-tsconfig: "npm:^4.7.5" dependenciesMeta: fsevents: optional: true bin: tsx: dist/cli.mjs - checksum: 10/7afedeff855ba98c47dc28b33d7e8e253c4dc1f791938db402d79c174bdf806b897c1a5f91e5b1259c112520c816f826b4c5d98f0bad7e95b02dec66fedb64d2 + checksum: 10/e7ee59e3912b05f994714a8c4b4d39040cb54559db60e262b58269f6c099d0ee128c2ccc53379f2ba25e10f398928f86b86d0fdfe3eb6d02c6342fc0597aeed8 languageName: node linkType: hard From ebc9c17b6ba63ee754320eadfba20c024c53c18f Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:01:43 -0300 Subject: [PATCH 04/11] fix: (Mobile) Voice calls failing when accepted from the lock screen (#40422) --- .changeset/fluffy-poets-check.md | 5 + .../src/definition/call/IClientMediaCall.ts | 1 + .../media-signaling/src/definition/client.ts | 1 + .../src/definition/services/negotiation.ts | 1 + packages/media-signaling/src/lib/Call.ts | 13 +++ .../src/lib/NegotiationManager.ts | 4 +- packages/media-signaling/src/lib/Session.ts | 98 +++++++++++++------ 7 files changed, 91 insertions(+), 32 deletions(-) create mode 100644 .changeset/fluffy-poets-check.md diff --git a/.changeset/fluffy-poets-check.md b/.changeset/fluffy-poets-check.md new file mode 100644 index 0000000000000..4ecdb9e7788ac --- /dev/null +++ b/.changeset/fluffy-poets-check.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/media-signaling': minor +--- + +Fixes a soft lock issue where the call negotiation doesn't proceed if the call is accepted while still being initialized diff --git a/packages/media-signaling/src/definition/call/IClientMediaCall.ts b/packages/media-signaling/src/definition/call/IClientMediaCall.ts index 32fb06a6a7882..599015597e439 100644 --- a/packages/media-signaling/src/definition/call/IClientMediaCall.ts +++ b/packages/media-signaling/src/definition/call/IClientMediaCall.ts @@ -30,6 +30,7 @@ export type CallHangupReason = | 'unavailable' // The actor is not available | 'transfer' // one of the users requested the other be transferred to someone else | 'not-answered' // max ringing duration was reached with no answer from the other user + | 'timeout-local-track' // Timeout waiting for the local audio track | 'timeout-remote-sdp' // Timeout waiting for the remote SDP | 'timeout-local-sdp' // Timeout while generating the local SDP + waiting for ICE Gathering | 'timeout-activation' // Timeout connecting to the negotiated session diff --git a/packages/media-signaling/src/definition/client.ts b/packages/media-signaling/src/definition/client.ts index c389adc4ee8c6..4ce3d11ca167e 100644 --- a/packages/media-signaling/src/definition/client.ts +++ b/packages/media-signaling/src/definition/client.ts @@ -2,6 +2,7 @@ export type ClientState = | 'none' // The client doesn't recognize a specific call id at all | 'pending' // The call is ringing | 'accepting' // The client tried to accept the call and is wating for confirmation from the server + | 'waiting-for-track' // The call was accepted, but the client doesn't have a local audio stream yet | 'waiting-for-offer' // The call was accepted, but the client doesn't have a webrtc offer yet | 'waiting-for-answer' // The call was accepted and an offer was already sent, but the client doesn't have an answer yet | 'generating-local-sdp' // The client is generating its first local sdp (offer/answer) diff --git a/packages/media-signaling/src/definition/services/negotiation.ts b/packages/media-signaling/src/definition/services/negotiation.ts index f61684f7a490a..d56eeb2b2d512 100644 --- a/packages/media-signaling/src/definition/services/negotiation.ts +++ b/packages/media-signaling/src/definition/services/negotiation.ts @@ -4,6 +4,7 @@ export type NegotiationManagerEvents = { 'error': { errorCode: string; negotiationId: string }; 'local-sdp': { negotiationId: string; sdp: RTCSessionDescriptionInit }; 'negotiation-needed': { oldNegotiationId: string }; + 'negotiation-started': void; }; export type NegotiationManagerConfig = { diff --git a/packages/media-signaling/src/lib/Call.ts b/packages/media-signaling/src/lib/Call.ts index 4ae5d3765eb68..1faad62b6216d 100644 --- a/packages/media-signaling/src/lib/Call.ts +++ b/packages/media-signaling/src/lib/Call.ts @@ -514,6 +514,10 @@ export class ClientMediaCall implements IClientMediaCall { } return 'pending'; case 'accepted': + if (!this.negotiationManager.isConfigured()) { + return 'waiting-for-track'; + } + if (!this.negotiationManager.currentNegotiationId) { return 'waiting-for-offer'; } @@ -554,6 +558,7 @@ export class ClientMediaCall implements IClientMediaCall { } if (newInputTrack && !hadInputTrack) { + this.updateClientState(); await this.negotiationManager.processNegotiations(); } } @@ -1272,6 +1277,8 @@ export class ClientMediaCall implements IClientMediaCall { switch (state) { case 'pending': return 'not-answered'; + case 'waiting-for-track': + return 'timeout-local-track'; case 'waiting-for-offer': case 'waiting-for-answer': return 'timeout-remote-sdp'; @@ -1372,6 +1379,11 @@ export class ClientMediaCall implements IClientMediaCall { this.config.transporter.requestRenegotiation(this.callId, oldNegotiationId); } + private onNegotiationStarted(): void { + this.config.logger?.debug('ClientMediaCall.onNegotiationStarted'); + this.updateClientState(); + } + private onNegotiationError(negotiationId: string, errorCode: string): void { this.config.logger?.debug('ClientMediaCall.onNegotiationError', negotiationId, errorCode); this.sendError({ @@ -1578,6 +1590,7 @@ export class ClientMediaCall implements IClientMediaCall { this.negotiationManager.emitter.on('local-sdp', ({ sdp, negotiationId }) => this.deliverSdp({ sdp, negotiationId })); this.negotiationManager.emitter.on('negotiation-needed', ({ oldNegotiationId }) => this.onNegotiationNeeded(oldNegotiationId)); + this.negotiationManager.emitter.on('negotiation-started', () => this.onNegotiationStarted()); this.negotiationManager.emitter.on('error', ({ errorCode, negotiationId }) => this.onNegotiationError(negotiationId, errorCode)); this.negotiationManager.setWebRTCProcessor(this.webrtcProcessor); } diff --git a/packages/media-signaling/src/lib/NegotiationManager.ts b/packages/media-signaling/src/lib/NegotiationManager.ts index 514b5ea7ab53f..02e1fb2fab7c9 100644 --- a/packages/media-signaling/src/lib/NegotiationManager.ts +++ b/packages/media-signaling/src/lib/NegotiationManager.ts @@ -233,9 +233,11 @@ export class NegotiationManager { .process(this.webrtcProcessor) // No need to handle errors here as they are already handled by the 'error' event .catch(() => null); + + this.emitter.emit('negotiation-started'); } - protected isConfigured(): this is WebRTCNegotiationManager { + public isConfigured(): this is WebRTCNegotiationManager { if (this.call.state === 'hangup' || this.call.hidden) { this.config.logger?.debug('Ignoring WebRTC negotiations due to call state.'); return false; diff --git a/packages/media-signaling/src/lib/Session.ts b/packages/media-signaling/src/lib/Session.ts index 039e0a08ec209..20c4eecda73bc 100644 --- a/packages/media-signaling/src/lib/Session.ts +++ b/packages/media-signaling/src/lib/Session.ts @@ -60,7 +60,7 @@ export class MediaSignalingSession extends Emitter { private inputTrack: MediaStreamTrack | null; - private updatingInputTrack: boolean; + private switchingInputTrack: boolean; private deviceId: ConstrainDOMString | null; @@ -96,7 +96,7 @@ export class MediaSignalingSession extends Emitter { this.knownCalls = new Map(); this.ignoredCalls = new Set(); this.inputTrack = null; - this.updatingInputTrack = false; + this.switchingInputTrack = false; this.deviceId = null; this.currentDeviceId = null; this.callsToGetUserMedia = 0; @@ -240,6 +240,11 @@ export class MediaSignalingSession extends Emitter { public async setDeviceId(deviceId: ConstrainDOMString | null): Promise { this.deviceId = deviceId; + + if (this.switchingInputTrack) { + this.config.logger?.warn('Audio Device was changed while the input track was being actively switched.'); + } + // do nothing if: // 1. doesn't have any input track yet // 2. it's the same device id @@ -430,40 +435,81 @@ export class MediaSignalingSession extends Emitter { } } - private requestInputTrackUpdate(): void { - if (this.updatingInputTrack || this.callsToGetUserMedia > 0) { + public requestInputTrackUpdate(): void { + // Don't do anything if we don't need to switch the track now + // This extra check here ensures that requestInputTrackUpdate can be called multiple times even though switchInputTrack can't + if (!this.shouldSwitchInputTrack()) { return; } - this.updateInputTrack().catch(() => null); - } + this.switchInputTrack().catch(() => null); + } + + /** + * Switch ON/OFF the use of an audio input track + * If there's one already in use, remove it; Otherwise, request and use a new one. + * This function assumes the current state needs to change and doesn't check anything before starting the switch process + * Switching OFF is straightforward: the current track is removed and stopped + * Switching ON is a multi-step process: + * 1. We request a new track from the media stream factory + * 2. Once the media stream factory returns a valid track, we double check that we still need it + * 2.1. If the track is still needed, we set it to all active calls + * 2.2. If the track is no longer needed by then, we stop it and keep no reference to it + * + * The track state only changes by the end of the whole process, so there's no point in calling this function twice and we guard against it -- + * but we don't guard against external changes to the track (for example, calling setDeviceId will also change the track state) + * */ + private async switchInputTrack(): Promise { + this.config.logger?.debug('MediaSignalingSession.switchInputTrack', this.callsToGetUserMedia); + + if (this.switchingInputTrack) { + this.config.logger?.warn('MediaSignalingSession.switchInputTrack', 'Input Track Switcher was called twice'); + return; + } + if (!this.shouldSwitchInputTrack()) { + this.config.logger?.warn('MediaSignalingSession.switchInputTrack', 'Input Track Switcher was called but is no longer needed'); + return; + } - private async updateInputTrack(): Promise { - this.config.logger?.debug('MediaSignalingSession.updatingInputTrack', this.callsToGetUserMedia); - this.updatingInputTrack = true; + this.switchingInputTrack = true; try { if (this.inputTrack) { - await this.maybeStopInputTrack(); + await this.setInputTrack(null); return; } - await this.maybeStartInputTrack(); + await this.startInputTrack(); } finally { - this.updatingInputTrack = false; - this.config.logger?.debug('MediaSignalingSession.updatingInputTrack.finally', this.callsToGetUserMedia); + this.switchingInputTrack = false; + this.config.logger?.debug('MediaSignalingSession.switchInputTrack.finally', this.callsToGetUserMedia); } } - private async maybeStartInputTrack(): Promise { - this.config.logger?.debug('MediaSignalingSession.maybeStartInputTrack'); + private shouldStartInputTrack(): boolean { + if (this.inputTrack) { + return false; + } + + if (this.callsToGetUserMedia > 0) { + return false; + } + for (const call of this.knownCalls.values()) { - if (!call.needsInputTrack()) { - continue; + if (call.needsInputTrack()) { + return true; } + } + + return false; + } - return this.startInputTrack(); + private shouldSwitchInputTrack(): boolean { + if (this.inputTrack) { + return !this.mayNeedInputTrack(); } + + return this.shouldStartInputTrack(); } private getAudioConstraints(): boolean | MediaTrackConstraints { @@ -476,10 +522,9 @@ export class MediaSignalingSession extends Emitter { private async startInputTrack(): Promise { this.config.logger?.debug('MediaSignalingSession.startInputTrack', this.callsToGetUserMedia); - this.currentDeviceId = this.deviceId; - let userMedia: MediaStream | null = null; + let userMedia: MediaStream | null; this.callsToGetUserMedia++; try { userMedia = await this.config.mediaStreamFactory({ audio: this.getAudioConstraints() }).catch(() => null); @@ -487,12 +532,12 @@ export class MediaSignalingSession extends Emitter { this.callsToGetUserMedia--; } - this.config.logger?.debug('MediaSignalingSession.startInputTrack.done', this.callsToGetUserMedia); - // If there's multiple simultaneous attempts to get the track, only process the output of the last one if (this.callsToGetUserMedia > 0) { + this.config.logger?.debug('MediaSignalingSession.startInputTrack.skipped', this.callsToGetUserMedia); return; } + this.config.logger?.debug('MediaSignalingSession.startInputTrack.done', this.callsToGetUserMedia); if (!userMedia) { return this.hangupCallsThatNeedInput(); @@ -545,15 +590,6 @@ export class MediaSignalingSession extends Emitter { return false; } - private async maybeStopInputTrack(): Promise { - this.config.logger?.debug('MediaSignalingSession.maybeStopInputTrack'); - if (this.mayNeedInputTrack()) { - return; - } - - await this.setInputTrack(null); - } - private async setScreenVideoTrack(newVideoTrack: MediaStreamTrack | null, call: ClientMediaCall): Promise { this.config.logger?.debug('MediaSignalingSession.setScreenVideoTrack', Boolean(newVideoTrack)); From 228486f07c38ea9cd3ea987c3c46bde300b6716f Mon Sep 17 00:00:00 2001 From: Julio Araujo Date: Tue, 16 Jun 2026 17:25:48 +0200 Subject: [PATCH 05/11] chore(deps): bump several dependencies to solve CVEs (#40966) --- apps/meteor/package.json | 8 +- apps/uikit-playground/package.json | 2 +- package.json | 23 +- packages/eslint-config/package.json | 2 +- packages/gazzodown/package.json | 2 +- packages/livechat/package.json | 6 +- packages/ui-client/package.json | 2 +- packages/web-ui-registration/package.json | 2 +- yarn.lock | 801 +++++++++++++--------- 9 files changed, 518 insertions(+), 330 deletions(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index b5e20190c936e..0c57e8e52c7c9 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -68,7 +68,7 @@ "@aws-sdk/client-s3": "^3.862.0", "@aws-sdk/lib-storage": "^3.862.0", "@aws-sdk/s3-request-presigner": "^3.862.0", - "@babel/core": "~7.29.0", + "@babel/core": "~7.29.6", "@babel/preset-env": "~7.29.5", "@babel/runtime": "~7.29.2", "@bugsnag/js": "~7.20.2", @@ -193,7 +193,7 @@ "date-fns": "~4.1.0", "date.js": "~0.3.3", "debug": "~4.3.7", - "dompurify": "~3.4.1", + "dompurify": "~3.4.7", "drachtio-srf": "patch:drachtio-srf@npm%3A5.0.12#~/.yarn/patches/drachtio-srf-npm-5.0.12-b0b1afaad6.patch", "ejson": "^2.2.3", "emailreplyparser": "^0.0.5", @@ -241,7 +241,7 @@ "lodash.debounce": "^4.0.8", "lodash.escape": "^4.0.1", "lodash.get": "^4.4.2", - "mailparser": "~3.7.5", + "mailparser": "~3.9.9", "marked": "^4.3.0", "mem": "^8.1.1", "meteor-node-stubs": "~1.2.27", @@ -257,7 +257,7 @@ "node-dogstatsd": "^0.0.7", "node-fetch": "2.7.0", "node-rsa": "^1.1.1", - "nodemailer": "^8.0.5", + "nodemailer": "^8.0.8", "object-path": "^0.11.8", "overlayscrollbars": "~2.11.5", "overlayscrollbars-react": "^0.5.6", diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index e542f9314115d..0637be2891b94 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -48,7 +48,7 @@ "@vitejs/plugin-react": "~6.0.1", "eslint": "~9.39.4", "typescript": "~5.9.3", - "vite": "^8.0.9" + "vite": "^8.0.16" }, "volta": { "extends": "../../package.json" diff --git a/package.json b/package.json index 4f940944ab8db..ca5407c82da3c 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "testunit": "turbo run testunit" }, "resolutions": { - "external-editor/tmp": "0.2.6", + "external-editor/tmp": "0.2.7", "@sematext/gc-stats@npm:^1.5.9": "patch:@sematext/gc-stats@npm%3A1.5.9#~/.yarn/patches/@sematext-gc-stats-npm-1.5.9-01e77be4d0.patch", "adm-zip": "0.5.9", "axios@npm:^1.6.1": "1.16.0", @@ -68,7 +68,7 @@ "markdown-it": "^14.1.1", "minimist": "1.2.6", "mongodb": "6.10.0", - "protobufjs": "7.5.8", + "protobufjs": "7.6.1", "webdav/axios": "0.32.0", "flatted@npm:^3.1.0": "3.4.2", "flatted@npm:^3.2.9": "3.4.2", @@ -91,6 +91,7 @@ "socks-proxy-agent/socks": "^2.8.9", "mongodb/socks": "^2.8.9", "underscore": "1.13.8", + "ws@npm:^7.5.3": "7.5.11", "ws@npm:^8": "^8.20.1", "ws@npm:^8.2.3": "^8.20.1", "ws@npm:^8.18.0": "^8.20.1", @@ -112,10 +113,10 @@ "xml-crypto/@xmldom/xmldom": "0.8.13", "@react-aria/overlays@npm:^3.25.0": "patch:@react-aria/overlays@npm%3A3.25.0#~/.yarn/patches/@react-aria-overlays-npm-3.25.0-2628866e6e.patch", "xml-encryption/@xmldom/xmldom": "0.8.13", - "form-data@npm:^2.5.0": "2.5.5", - "form-data@npm:^4.0.0": "4.0.5", - "form-data@npm:^4.0.4": "4.0.5", - "form-data@npm:^4.0.5": "4.0.5", + "form-data@npm:^2.5.0": "2.5.6", + "form-data@npm:^4.0.0": "4.0.6", + "form-data@npm:^4.0.4": "4.0.6", + "form-data@npm:^4.0.5": "4.0.6", "browserify-aes/cipher-base": "1.0.7", "browserify-des/cipher-base": "1.0.7", "create-hash/cipher-base": "1.0.7", @@ -140,7 +141,7 @@ "minimatch@npm:^3.1.1": "3.1.5", "minimatch@npm:^9.0.4": "9.0.9", "minimatch@npm:^10.0.0": "10.2.5", - "nodemailer@npm:^8.0.5": "8.0.7", + "nodemailer@npm:^8.0.8": "8.0.8", "mailparser/nodemailer": "7.0.13", "normalize-package-data@npm:2.5.0/semver": "5.7.2", "make-dir@npm:2.1.0/semver": "5.7.2", @@ -158,9 +159,9 @@ "semver@npm:^7.7.2": "7.7.4", "semver@npm:^7.7.3": "7.7.4", "@peggyjs/from-mem/semver": "7.7.4", - "node-gyp/tar": "7.5.13", - "cacache/tar": "7.5.13", - "@mapbox/node-pre-gyp/tar": "7.5.13", + "node-gyp/tar": "7.5.16", + "cacache/tar": "7.5.16", + "@mapbox/node-pre-gyp/tar": "7.5.16", "@opentelemetry/exporter-logs-otlp-grpc/@grpc/grpc-js": "^1.14.4", "@opentelemetry/exporter-metrics-otlp-grpc/@grpc/grpc-js": "^1.14.4", "@opentelemetry/exporter-trace-otlp-grpc/@grpc/grpc-js": "^1.14.4", @@ -182,7 +183,7 @@ "eslint": "~9.39.4", "eslint-plugin-you-dont-need-lodash-underscore": "~6.14.0", "ts-node": "^10.9.2", - "turbo": "2.8.21", + "turbo": "2.9.14", "typescript": "~5.9.3" }, "packageManager": "yarn@4.12.0", diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 077e66f45472d..0722b40946eee 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -15,7 +15,7 @@ "lint:fix": "eslint --fix ." }, "dependencies": { - "@babel/core": "~7.28.6", + "@babel/core": "~7.29.6", "@babel/eslint-parser": "~7.28.6", "@eslint/js": "~9.39.4", "@types/eslint": "~8.44.9", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index f183d9e44abaf..deba5edac2f74 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "date-fns": "~4.1.0", - "dompurify": "~3.4.1", + "dompurify": "~3.4.7", "highlight.js": "11.8.0", "react-error-boundary": "^3.1.4", "react-stately": "~3.17.0" diff --git a/packages/livechat/package.json b/packages/livechat/package.json index e8919628115d4..cef71a405040e 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -37,7 +37,7 @@ "ajv-formats": "^3.0.1", "css-vars-ponyfill": "^2.4.9", "date-fns": "~4.1.0", - "dompurify": "~3.4.1", + "dompurify": "~3.4.7", "emoji-mart": "^3.0.1", "history": "~5.3.0", "i18next": "~23.4.9", @@ -51,7 +51,7 @@ "whatwg-fetch": "^3.6.20" }, "devDependencies": { - "@babel/core": "~7.29.0", + "@babel/core": "~7.29.6", "@babel/preset-env": "~7.29.5", "@babel/preset-react": "~7.28.5", "@babel/preset-typescript": "~7.28.5", @@ -109,7 +109,7 @@ "url-loader": "^4.1.1", "webpack": "~5.104.1", "webpack-cli": "~5.1.4", - "webpack-dev-server": "~5.2.3" + "webpack-dev-server": "~5.2.5" }, "peerDependencies": { "@rocket.chat/fuselage-tokens": "*", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 82a3d15eebb0f..36d0aa6d0e1bb 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@rocket.chat/onboarding-ui": "~0.36.2", - "dompurify": "~3.4.1" + "dompurify": "~3.4.7" }, "devDependencies": { "@react-aria/toolbar": "^3.0.0-nightly.5042", diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 6a10fdde434d6..ca9a1a07eb65e 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -17,7 +17,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "dompurify": "~3.4.1" + "dompurify": "~3.4.7" }, "devDependencies": { "@rocket.chat/core-typings": "workspace:~", diff --git a/yarn.lock b/yarn.lock index 5b82c70e5cbe1..29311dfbc6a42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -830,6 +830,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/code-frame@npm:7.29.7" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.29.7" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.1.1" + checksum: 10/84da552e51a55795a50b3589116edb2f9e368a647d266380683775f18effd9acd4521b0246bebd0b049a7f32af1f87b1e8475d3bcb665f876bd04ade8da99697 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.27.2": version: 7.27.5 resolution: "@babel/compat-data@npm:7.27.5" @@ -851,6 +862,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/compat-data@npm:7.29.7" + checksum: 10/ad2272714087f68970977f6e2b53597a8503fc9c3028c4a91686474bd77a707dd00903cdde4b73788972016d1bad4dc3fa4e5ff38e1ed8f1c3bde1095352973a + languageName: node + linkType: hard + "@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.18.9, @babel/core@npm:^7.22.5, @babel/core@npm:^7.23.9, @babel/core@npm:^7.27.4, @babel/core@npm:^7.7.5": version: 7.27.4 resolution: "@babel/core@npm:7.27.4" @@ -874,7 +892,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.24.4, @babel/core@npm:~7.29.0": +"@babel/core@npm:^7.24.4": version: 7.29.0 resolution: "@babel/core@npm:7.29.0" dependencies: @@ -897,26 +915,26 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:~7.28.6": - version: 7.28.6 - resolution: "@babel/core@npm:7.28.6" +"@babel/core@npm:~7.29.6": + version: 7.29.7 + resolution: "@babel/core@npm:7.29.7" dependencies: - "@babel/code-frame": "npm:^7.28.6" - "@babel/generator": "npm:^7.28.6" - "@babel/helper-compilation-targets": "npm:^7.28.6" - "@babel/helper-module-transforms": "npm:^7.28.6" - "@babel/helpers": "npm:^7.28.6" - "@babel/parser": "npm:^7.28.6" - "@babel/template": "npm:^7.28.6" - "@babel/traverse": "npm:^7.28.6" - "@babel/types": "npm:^7.28.6" + "@babel/code-frame": "npm:^7.29.7" + "@babel/generator": "npm:^7.29.7" + "@babel/helper-compilation-targets": "npm:^7.29.7" + "@babel/helper-module-transforms": "npm:^7.29.7" + "@babel/helpers": "npm:^7.29.7" + "@babel/parser": "npm:^7.29.7" + "@babel/template": "npm:^7.29.7" + "@babel/traverse": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" "@jridgewell/remapping": "npm:^2.3.5" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10/1a150a69c547daf13c457be1fdaf1a0935d02b94605e777e049537ec2f279b4bb442ffbe1c2d8ff62c688878b1d5530a5784daf72ece950d1917fb78717f51d2 + checksum: 10/38e71cf81db790b0bb2a3a0c8140c2b1c87576b61dc6be676de4fab8c3be871af590a739e8c489fe8e8f9a8e5899fa11e35e59e9e09d40b259c6a675f2f98928 languageName: node linkType: hard @@ -1012,6 +1030,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/generator@npm:7.29.7" + dependencies: + "@babel/parser": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" + jsesc: "npm:^3.0.2" + checksum: 10/60fb0432ebeab791b2d68e5fc49da6f8e8b68bcc4751211ccf08ac0101e9dcaddefd0cbbbd488afb1c1517515c7c3e76f63d9b05d06deaeb008afd499488db9c + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.25.9, @babel/helper-annotate-as-pure@npm:^7.27.1, @babel/helper-annotate-as-pure@npm:^7.27.3": version: 7.27.3 resolution: "@babel/helper-annotate-as-pure@npm:7.27.3" @@ -1047,6 +1078,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-compilation-targets@npm:7.29.7" + dependencies: + "@babel/compat-data": "npm:^7.29.7" + "@babel/helper-validator-option": "npm:^7.29.7" + browserslist: "npm:^4.24.0" + lru-cache: "npm:^5.1.1" + semver: "npm:^6.3.1" + checksum: 10/af9ed4299ad5cfbe48432a964f37cbbfc200bbeb0f8ba9cbc86448503fa929382d5161d32096274752230c9feb919c9ef595559498833da656fc6a8e24a62383 + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helper-create-class-features-plugin@npm:7.28.6" @@ -1125,6 +1169,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-globals@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-globals@npm:7.29.7" + checksum: 10/e53203e87ae24a45f59639edea0c429bc3c63c6d74f1862fe60a35032d89478e7511d2f34855da0fcb65782668d72e59e93d1de5bc00121ba9bc1aa38f1f0ad3 + languageName: node + linkType: hard + "@babel/helper-member-expression-to-functions@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-member-expression-to-functions@npm:7.27.1" @@ -1165,6 +1216,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-module-imports@npm:7.29.7" + dependencies: + "@babel/traverse": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" + checksum: 10/28ec6f7efd99588d6eebfb25c9f1ccc34cb0cdb0839c4c0f08b3ec0105ccaefbe7e8b4f651f3f55a4f5c4fcb1d979bd32a9b8ee23e3e62163ea22aaa7ee0dfa1 + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.27.3": version: 7.27.3 resolution: "@babel/helper-module-transforms@npm:7.27.3" @@ -1191,6 +1252,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-module-transforms@npm:7.29.7" + dependencies: + "@babel/helper-module-imports": "npm:^7.29.7" + "@babel/helper-validator-identifier": "npm:^7.29.7" + "@babel/traverse": "npm:^7.29.7" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/33251b1fb44d726194a974a0078b1269511d130a2609357ff829b479e9e4dca96ecd5384c534a477095f665ffb01503d3e680699c2002e5b62e6ca1a272f1892 + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-optimise-call-expression@npm:7.27.1" @@ -1270,6 +1344,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-string-parser@npm:7.29.7" + checksum: 10/4d8ef0ef7105f3d9fe4361137c8f42e5b4c7a52b5380b962762f2a528a1ba89064e2c6236090716ce34b63707b886ae0ebf36b2c2fcc2851f27e652febfc3648 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.22.20, @babel/helper-validator-identifier@npm:^7.25.7, @babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-validator-identifier@npm:7.27.1" @@ -1284,6 +1365,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-validator-identifier@npm:7.29.7" + checksum: 10/2efa42701eb05babf26dff3332109c9e5e1a3400a71fb9e68ee27af28235036a2a72c2494c04bdab3f909075f42a58b2e8271074372bc7f8e79ec02bd364d7a7 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-validator-option@npm:7.27.1" @@ -1291,6 +1379,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helper-validator-option@npm:7.29.7" + checksum: 10/aeb6aa966f59300d3cc2fea7c68e1dfd7ad011fc10e535c8e2b2de3094b27c859428dc7220f16420350f8b1cde99da120b673be04bcb0c2f37b56258c96bed58 + languageName: node + linkType: hard + "@babel/helper-wrap-function@npm:^7.27.1": version: 7.27.1 resolution: "@babel/helper-wrap-function@npm:7.27.1" @@ -1322,6 +1417,16 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/helpers@npm:7.29.7" + dependencies: + "@babel/template": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" + checksum: 10/b4d1ef12c19e896585c009ba29677839097ff04f8b11a2430d335c3fb6bd667b4f9e96a3b185a083fdde6b1137eabbbf2600c32425cb69cefc81d81d5cfe425d + languageName: node + linkType: hard + "@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.16.2, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.27.4, @babel/parser@npm:^7.27.5": version: 7.27.5 resolution: "@babel/parser@npm:7.27.5" @@ -1388,6 +1493,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/parser@npm:7.29.7" + dependencies: + "@babel/types": "npm:^7.29.7" + bin: + parser: ./bin/babel-parser.js + checksum: 10/da40c5928c95997b01aabe84fc3440881b8f20b866714fefa142961d37e82ffc03fbb9afed706f15f8a688278f95286ca0cea0d87ad6c77600f8c6c45d9824ee + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.28.5" @@ -2614,6 +2730,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/template@npm:7.29.7" + dependencies: + "@babel/code-frame": "npm:^7.29.7" + "@babel/parser": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" + checksum: 10/da92f7a5b61e05d2fb3934a44f18cec6006ee3c595116c17a3b44cb9756ecd43205c7360dbfa326fa8f4d00aaeb9e777342a881070d11c2305e9c694bc3ca6ff + languageName: node + linkType: hard + "@babel/traverse@npm:^7.16.0": version: 7.25.9 resolution: "@babel/traverse@npm:7.25.9" @@ -2734,6 +2861,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/traverse@npm:7.29.7" + dependencies: + "@babel/code-frame": "npm:^7.29.7" + "@babel/generator": "npm:^7.29.7" + "@babel/helper-globals": "npm:^7.29.7" + "@babel/parser": "npm:^7.29.7" + "@babel/template": "npm:^7.29.7" + "@babel/types": "npm:^7.29.7" + debug: "npm:^4.3.1" + checksum: 10/ce24086a7dd8c408cbdb159437d3c8e02464a6d32b320d884fa742e2c5a3344b9342a923c7a371fc1789b4d82a59972a7008b5d8bbc1bc0c5ae42a39b28dc7f6 + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": version: 7.23.5 resolution: "@babel/types@npm:7.23.5" @@ -2836,6 +2978,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.29.7": + version: 7.29.7 + resolution: "@babel/types@npm:7.29.7" + dependencies: + "@babel/helper-string-parser": "npm:^7.29.7" + "@babel/helper-validator-identifier": "npm:^7.29.7" + checksum: 10/bd4f5635db1057bd0abeebf93eb3ae38399e152271cea8dce8288350f0afa13ed3e2db2e16e22bd3303068890eec18965a83420539afbe0dde31432b4cf9636d + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -3417,13 +3569,13 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:1.9.2": - version: 1.9.2 - resolution: "@emnapi/core@npm:1.9.2" +"@emnapi/core@npm:1.10.0": + version: 1.10.0 + resolution: "@emnapi/core@npm:1.10.0" dependencies: "@emnapi/wasi-threads": "npm:1.2.1" tslib: "npm:^2.4.0" - checksum: 10/32084861f306b405f10f3ae13d1a49fa75650bdaaa40704892c397856815fe5d3781670d2662806d39c2d8a19bb62826dd7b870a79858f7be77500d9d0d3d91a + checksum: 10/d32f386084e64deaf2609aabb8295d1ad5af6144d0f46d2060b76cc53f1f3b486df54bec9b0f33c37d85a3822e1193ebcd4e3deb4a5f0e4cd650aa2ffc631715 languageName: node linkType: hard @@ -3437,12 +3589,12 @@ __metadata: languageName: node linkType: hard -"@emnapi/runtime@npm:1.9.2": - version: 1.9.2 - resolution: "@emnapi/runtime@npm:1.9.2" +"@emnapi/runtime@npm:1.10.0": + version: 1.10.0 + resolution: "@emnapi/runtime@npm:1.10.0" dependencies: tslib: "npm:^2.4.0" - checksum: 10/de123d6b7acdbe34bf997523be761e5ae6d8f9b3967b72e8e50ff7dd1791a2a0d2b9fb0d7d92230b0738502980ea6f947189b7c1f47814ff666515a55c6fff48 + checksum: 10/d21083d07fa0c2da171c142e78ef986b66b07d45b06accc0bcaf49fcc61bb4dbc10e1c1760813070165b9f49b054376a931045347f21c0f42ff1eb2d2040faac languageName: node linkType: hard @@ -6349,10 +6501,10 @@ __metadata: languageName: node linkType: hard -"@oxc-project/types@npm:=0.126.0": - version: 0.126.0 - resolution: "@oxc-project/types@npm:0.126.0" - checksum: 10/b4d7d350728381916acfd81549021d075c2456e4de64229b0402ae8f9531a2d44f928c835b99da3a589ff1e32c133da1f0c0e2229cc32f62a62e5752b6bd728b +"@oxc-project/types@npm:=0.133.0": + version: 0.133.0 + resolution: "@oxc-project/types@npm:0.133.0" + checksum: 10/de44f653a9e0c0267309122f1f184120c6869af4382218a6bf4a320c5150743eb00b5e8641b04917666281995ed0fe6381561922a48a28082a75bb122acf3ac6 languageName: node linkType: hard @@ -6732,20 +6884,19 @@ __metadata: languageName: node linkType: hard -"@protobufjs/eventemitter@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/eventemitter@npm:1.1.0" - checksum: 10/03af3e99f17ad421283d054c88a06a30a615922a817741b43ca1b13e7c6b37820a37f6eba9980fb5150c54dba6e26cb6f7b64a6f7d8afa83596fafb3afa218c3 +"@protobufjs/eventemitter@npm:^1.1.1": + version: 1.1.1 + resolution: "@protobufjs/eventemitter@npm:1.1.1" + checksum: 10/a54dc1aff4475ffad4fdf3235c71a553f5e40e3b4cf6a2e217151895a61cb4eb0be20d63791db22441ca25e594671f1021977133f9939540750231ff7d8e9dd6 languageName: node linkType: hard -"@protobufjs/fetch@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/fetch@npm:1.1.0" +"@protobufjs/fetch@npm:^1.1.1": + version: 1.1.1 + resolution: "@protobufjs/fetch@npm:1.1.1" dependencies: "@protobufjs/aspromise": "npm:^1.1.1" - "@protobufjs/inquire": "npm:^1.1.0" - checksum: 10/67ae40572ad536e4ef94269199f252c024b66e3059850906bdaee161ca1d75c73d04d35cd56f147a8a5a079f5808e342b99e61942c1dae15604ff0600b09a958 + checksum: 10/427cf2da8c69b494b0df3b2fb1f43c97f0f71ca2c8ef8232dac7e44f2527ad0cc9cecb243eda14a918e86018bfa6d54d92252240d2b37ed205b13adb5506fa1d languageName: node linkType: hard @@ -6756,17 +6907,10 @@ __metadata: languageName: node linkType: hard -"@protobufjs/inquire@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/inquire@npm:1.1.0" - checksum: 10/c09efa34a5465cb120775e1a482136f2340a58b4abce7e93d72b8b5a9324a0e879275016ef9fcd73d72a4731639c54f2bb755bb82f916e4a78892d1d840bb3d2 - languageName: node - linkType: hard - -"@protobufjs/inquire@npm:^1.1.1": - version: 1.1.1 - resolution: "@protobufjs/inquire@npm:1.1.1" - checksum: 10/504740e8ac348f70b33bcf6a20c83d5b9679901654c1a96b18c0491ec2f2f7ac580e74019b6d1bce16113bfb9746bc6e7dfd4e12a717deed699675b7f230ce9e +"@protobufjs/inquire@npm:^1.1.2": + version: 1.1.2 + resolution: "@protobufjs/inquire@npm:1.1.2" + checksum: 10/259756489c75a751552df60d18f82503d2534855646397b96b91cf15807fa852e99bd9eb73dabb64da37aec7913844032ecb031a4326d82aae622f5e4c2f8a17 languageName: node linkType: hard @@ -9297,7 +9441,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/eslint-config@workspace:packages/eslint-config" dependencies: - "@babel/core": "npm:~7.28.6" + "@babel/core": "npm:~7.29.6" "@babel/eslint-parser": "npm:~7.28.6" "@eslint/js": "npm:~9.39.4" "@types/eslint": "npm:~8.44.9" @@ -9546,7 +9690,7 @@ __metadata: "@types/react": "npm:~18.3.28" "@types/react-dom": "npm:~18.3.7" date-fns: "npm:~4.1.0" - dompurify: "npm:~3.4.1" + dompurify: "npm:~3.4.7" eslint: "npm:~9.39.4" highlight.js: "npm:11.8.0" i18next: "npm:~23.4.9" @@ -9712,7 +9856,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/livechat@workspace:packages/livechat" dependencies: - "@babel/core": "npm:~7.29.0" + "@babel/core": "npm:~7.29.6" "@babel/preset-env": "npm:~7.29.5" "@babel/preset-react": "npm:~7.28.5" "@babel/preset-typescript": "npm:~7.28.5" @@ -9747,7 +9891,7 @@ __metadata: cssnano: "npm:^7.1.7" date-fns: "npm:~4.1.0" desvg-loader: "npm:^0.1.0" - dompurify: "npm:~3.4.1" + dompurify: "npm:~3.4.7" emoji-mart: "npm:^3.0.1" eslint: "npm:~9.39.4" file-loader: "npm:^6.2.0" @@ -9790,7 +9934,7 @@ __metadata: url-loader: "npm:^4.1.1" webpack: "npm:~5.104.1" webpack-cli: "npm:~5.1.4" - webpack-dev-server: "npm:~5.2.3" + webpack-dev-server: "npm:~5.2.5" whatwg-fetch: "npm:^3.6.20" peerDependencies: "@rocket.chat/fuselage-tokens": "*" @@ -9925,7 +10069,7 @@ __metadata: "@aws-sdk/lib-storage": "npm:^3.862.0" "@aws-sdk/s3-request-presigner": "npm:^3.862.0" "@axe-core/playwright": "npm:^4.10.2" - "@babel/core": "npm:~7.29.0" + "@babel/core": "npm:~7.29.6" "@babel/preset-env": "npm:~7.29.5" "@babel/preset-react": "npm:~7.27.1" "@babel/register": "npm:~7.28.6" @@ -10147,7 +10291,7 @@ __metadata: date.js: "npm:~0.3.3" debug: "npm:~4.3.7" docker-compose: "npm:^0.24.8" - dompurify: "npm:~3.4.1" + dompurify: "npm:~3.4.7" drachtio-srf: "patch:drachtio-srf@npm%3A5.0.12#~/.yarn/patches/drachtio-srf-npm-5.0.12-b0b1afaad6.patch" ejson: "npm:^2.2.3" emailreplyparser: "npm:^0.0.5" @@ -10201,7 +10345,7 @@ __metadata: lodash.debounce: "npm:^4.0.8" lodash.escape: "npm:^4.0.1" lodash.get: "npm:^4.4.2" - mailparser: "npm:~3.7.5" + mailparser: "npm:~3.9.9" marked: "npm:^4.3.0" mem: "npm:^8.1.1" meteor-node-stubs: "npm:~1.2.27" @@ -10218,7 +10362,7 @@ __metadata: node-dogstatsd: "npm:^0.0.7" node-fetch: "npm:2.7.0" node-rsa: "npm:^1.1.1" - nodemailer: "npm:^8.0.5" + nodemailer: "npm:^8.0.8" nyc: "npm:^17.1.0" object-path: "npm:^0.11.8" outdent: "npm:~0.8.0" @@ -10995,7 +11139,7 @@ __metadata: "@types/jest": "npm:~30.0.0" "@types/react": "npm:~18.3.28" "@types/react-dom": "npm:~18.3.7" - dompurify: "npm:~3.4.1" + dompurify: "npm:~3.4.7" eslint: "npm:~9.39.4" i18next: "npm:~23.4.9" jest: "npm:~30.2.0" @@ -11268,7 +11412,7 @@ __metadata: react-virtuoso: "npm:~4.12.8" reactflow: "npm:^11.11.4" typescript: "npm:~5.9.3" - vite: "npm:^8.0.9" + vite: "npm:^8.0.16" languageName: unknown linkType: soft @@ -11297,7 +11441,7 @@ __metadata: "@testing-library/react": "npm:~16.3.2" "@types/react": "npm:~18.3.28" "@types/react-dom": "npm:~18.3.7" - dompurify: "npm:~3.4.1" + dompurify: "npm:~3.4.7" eslint: "npm:~9.39.4" i18next: "npm:~23.4.9" react: "npm:~18.3.1" @@ -11318,122 +11462,115 @@ __metadata: languageName: unknown linkType: soft -"@rolldown/binding-android-arm64@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-android-arm64@npm:1.0.0-rc.16" +"@rolldown/binding-android-arm64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-android-arm64@npm:1.0.3" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-darwin-arm64@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.0-rc.16" +"@rolldown/binding-darwin-arm64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-darwin-arm64@npm:1.0.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-darwin-x64@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.0-rc.16" +"@rolldown/binding-darwin-x64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-darwin-x64@npm:1.0.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rolldown/binding-freebsd-x64@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.0-rc.16" +"@rolldown/binding-freebsd-x64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-freebsd-x64@npm:1.0.3" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.0-rc.16" +"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.3" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.0-rc.16" +"@rolldown/binding-linux-arm64-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.3" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.0-rc.16" +"@rolldown/binding-linux-arm64-musl@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.3" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.0-rc.16" +"@rolldown/binding-linux-ppc64-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.3" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.0-rc.16" +"@rolldown/binding-linux-s390x-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.3" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.0-rc.16" +"@rolldown/binding-linux-x64-gnu@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.3" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.0-rc.16" +"@rolldown/binding-linux-x64-musl@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.3" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.0-rc.16" +"@rolldown/binding-openharmony-arm64@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.3" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.0-rc.16" +"@rolldown/binding-wasm32-wasi@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.3" dependencies: - "@emnapi/core": "npm:1.9.2" - "@emnapi/runtime": "npm:1.9.2" + "@emnapi/core": "npm:1.10.0" + "@emnapi/runtime": "npm:1.10.0" "@napi-rs/wasm-runtime": "npm:^1.1.4" conditions: cpu=wasm32 languageName: node linkType: hard -"@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.0-rc.16" +"@rolldown/binding-win32-arm64-msvc@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.0-rc.16" +"@rolldown/binding-win32-x64-msvc@npm:1.0.3": + version: 1.0.3 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "@rolldown/pluginutils@npm:1.0.0-rc.16" - checksum: 10/81d2b15b548375efbd63824c817def7ffe5e8f9949ccc5b5e0a15b8b01bf236a1e3f89d614e19aceb0fd8ee2796bc912fd8ba7652d21b9a29cd60e2741b1180e - languageName: node - linkType: hard - "@rolldown/pluginutils@npm:1.0.0-rc.7": version: 1.0.0-rc.7 resolution: "@rolldown/pluginutils@npm:1.0.0-rc.7" @@ -11441,6 +11578,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:^1.0.0": + version: 1.0.1 + resolution: "@rolldown/pluginutils@npm:1.0.1" + checksum: 10/4e95cf9ce23d75e5aa03ea0249cd86f7d1e21f83fbf6f8520e4edd8a251ba1b82c4ba9bc13cd24b6c4661daec6225b06e6d35c64c604e731b230b2a49af47d05 + languageName: node + linkType: hard + "@rtsao/scc@npm:^1.1.0": version: 1.1.0 resolution: "@rtsao/scc@npm:1.1.0" @@ -11480,13 +11624,15 @@ __metadata: languageName: node linkType: hard -"@selderee/plugin-htmlparser2@npm:^0.11.0": - version: 0.11.0 - resolution: "@selderee/plugin-htmlparser2@npm:0.11.0" +"@selderee/plugin-htmlparser2@npm:~0.12.0": + version: 0.12.0 + resolution: "@selderee/plugin-htmlparser2@npm:0.12.0" dependencies: - domhandler: "npm:^5.0.3" - selderee: "npm:^0.11.0" - checksum: 10/7550108d270e6ea2be4850d55cbf4d58d5a90c109a15b874c3c7c622a1399bd8015359ef3672983a86118432ca8325a6aca1fe79d961b01278fdaeaea8895c5f + domelementtype: "npm:~2.3.0" + domhandler: "npm:~5.0.3" + peerDependencies: + selderee: ~0.12.0 + checksum: 10/11399112888e3745f3066f02b8bc52344b6f4011192f5dacec63bed5d83e97801b418e16ec95531715c53d199f2c96f1357a218bd9fef4ebe18e638eb1b19c48 languageName: node linkType: hard @@ -13365,44 +13511,44 @@ __metadata: languageName: node linkType: hard -"@turbo/darwin-64@npm:2.8.21": - version: 2.8.21 - resolution: "@turbo/darwin-64@npm:2.8.21" +"@turbo/darwin-64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/darwin-64@npm:2.9.14" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@turbo/darwin-arm64@npm:2.8.21": - version: 2.8.21 - resolution: "@turbo/darwin-arm64@npm:2.8.21" +"@turbo/darwin-arm64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/darwin-arm64@npm:2.9.14" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@turbo/linux-64@npm:2.8.21": - version: 2.8.21 - resolution: "@turbo/linux-64@npm:2.8.21" +"@turbo/linux-64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/linux-64@npm:2.9.14" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@turbo/linux-arm64@npm:2.8.21": - version: 2.8.21 - resolution: "@turbo/linux-arm64@npm:2.8.21" +"@turbo/linux-arm64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/linux-arm64@npm:2.9.14" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@turbo/windows-64@npm:2.8.21": - version: 2.8.21 - resolution: "@turbo/windows-64@npm:2.8.21" +"@turbo/windows-64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/windows-64@npm:2.9.14" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@turbo/windows-arm64@npm:2.8.21": - version: 2.8.21 - resolution: "@turbo/windows-arm64@npm:2.8.21" +"@turbo/windows-arm64@npm:2.9.14": + version: 2.9.14 + resolution: "@turbo/windows-arm64@npm:2.9.14" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -15829,6 +15975,17 @@ __metadata: languageName: node linkType: hard +"@zone-eu/mailsplit@npm:5.4.12": + version: 5.4.12 + resolution: "@zone-eu/mailsplit@npm:5.4.12" + dependencies: + libbase64: "npm:1.3.0" + libmime: "npm:5.3.8" + libqp: "npm:2.1.1" + checksum: 10/ceb4ca547a6adff1dce1d2e8e6dea1b7fa631a99863ac1a10a53a805283e29e310307dc898354eec6680982daf9968979e981f3b4308ea4a4a1292487ae32999 + languageName: node + linkType: hard + "abbrev@npm:1": version: 1.1.1 resolution: "abbrev@npm:1.1.1" @@ -19875,6 +20032,13 @@ __metadata: languageName: node linkType: hard +"deepmerge-ts@npm:^7.1.5": + version: 7.1.5 + resolution: "deepmerge-ts@npm:7.1.5" + checksum: 10/f86d1a0b3b803cbae749ebaabf2c6db388abec57d77a4003829ecf987504f3c93b01d1ff86ef89a5d56e2b06d7402ea2a7502f0d044f19e036ada1f543e78d3d + languageName: node + linkType: hard + "deepmerge@npm:^4.2.2, deepmerge@npm:^4.3.1": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" @@ -20318,7 +20482,7 @@ __metadata: languageName: node linkType: hard -"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0, domelementtype@npm:^2.3.0": +"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0, domelementtype@npm:^2.3.0, domelementtype@npm:~2.3.0": version: 2.3.0 resolution: "domelementtype@npm:2.3.0" checksum: 10/ee837a318ff702622f383409d1f5b25dd1024b692ef64d3096ff702e26339f8e345820f29a68bcdcea8cfee3531776b3382651232fbeae95612d6f0a75efb4f6 @@ -20352,7 +20516,7 @@ __metadata: languageName: node linkType: hard -"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": +"domhandler@npm:^5.0.2, domhandler@npm:^5.0.3, domhandler@npm:~5.0.3": version: 5.0.3 resolution: "domhandler@npm:5.0.3" dependencies: @@ -20361,15 +20525,15 @@ __metadata: languageName: node linkType: hard -"dompurify@npm:~3.4.1": - version: 3.4.1 - resolution: "dompurify@npm:3.4.1" +"dompurify@npm:~3.4.7": + version: 3.4.10 + resolution: "dompurify@npm:3.4.10" dependencies: "@types/trusted-types": "npm:^2.0.7" dependenciesMeta: "@types/trusted-types": optional: true - checksum: 10/dcaf945376eff2a61841b205501b163b2c8ae9afe7251e68276b561d9fcf943cefc67e2631fdeae080b52a8b37c96e6beb7e6ae80ad8a83692ff67965dd6b4db + checksum: 10/e051c70e7a0729d8cc04d40d7863dc8977495140857e81dc668fb548b7a43c7f80cfc973e3c9c0a6cd18b8eaa94ea7de9603daa02d38c8b15a9b32cc25a6f440 languageName: node linkType: hard @@ -22756,30 +22920,30 @@ __metadata: languageName: node linkType: hard -"form-data@npm:2.5.5": - version: 2.5.5 - resolution: "form-data@npm:2.5.5" +"form-data@npm:2.5.6": + version: 2.5.6 + resolution: "form-data@npm:2.5.6" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.2" + hasown: "npm:^2.0.4" mime-types: "npm:^2.1.35" safe-buffer: "npm:^5.2.1" - checksum: 10/4b6a8d07bb67089da41048e734215f68317a8e29dd5385a972bf5c458a023313c69d3b5d6b8baafbb7f808fa9881e0e2e030ffe61e096b3ddc894c516401271d + checksum: 10/7b2afddf638125b6d22936a1f642e88887b279504510e3f56be6dc90bea35cb8124fe6ecc75032e92264b5ebfbd183d0992307588c782eaa88d1bed76381ad6b languageName: node linkType: hard -"form-data@npm:4.0.5": - version: 4.0.5 - resolution: "form-data@npm:4.0.5" +"form-data@npm:4.0.6": + version: 4.0.6 + resolution: "form-data@npm:4.0.6" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.2" - mime-types: "npm:^2.1.12" - checksum: 10/52ecd6e927c8c4e215e68a7ad5e0f7c1031397439672fd9741654b4a94722c4182e74cc815b225dcb5be3f4180f36428f67c6dd39eaa98af0dcfdd26c00c19cd + hasown: "npm:^2.0.4" + mime-types: "npm:^2.1.35" + checksum: 10/de6614c8537c92fa5fa3ee7e827758f98f5a9c033f348b7de81855ef36e5cb867e75d9f405d9483ab8d724a4a20d4e79926a299fa8dbba38f530eb659f0884e4 languageName: node linkType: hard @@ -23576,6 +23740,15 @@ __metadata: languageName: node linkType: hard +"hasown@npm:^2.0.4": + version: 2.0.4 + resolution: "hasown@npm:2.0.4" + dependencies: + function-bind: "npm:^1.1.2" + checksum: 10/13823863ae48161068b4c51606a3128451c66f14545a5169d667fe9fca168dcd38c27570c7a299e32ef844b8da3d55def7fe88602f8970d4311fb543ee88001a + languageName: node + linkType: hard + "he@npm:1.2.0, he@npm:^1.2.0": version: 1.2.0 resolution: "he@npm:1.2.0" @@ -23791,16 +23964,16 @@ __metadata: languageName: node linkType: hard -"html-to-text@npm:9.0.5": - version: 9.0.5 - resolution: "html-to-text@npm:9.0.5" +"html-to-text@npm:10.0.0": + version: 10.0.0 + resolution: "html-to-text@npm:10.0.0" dependencies: - "@selderee/plugin-htmlparser2": "npm:^0.11.0" - deepmerge: "npm:^4.3.1" + "@selderee/plugin-htmlparser2": "npm:~0.12.0" + deepmerge-ts: "npm:^7.1.5" dom-serializer: "npm:^2.0.0" - htmlparser2: "npm:^8.0.2" - selderee: "npm:^0.11.0" - checksum: 10/e5991f9946dd0e5c91c4ed863c71a4feaef3d5ce85cd8684fb0f2fc175b1ccee323bb97a1773b6bebc47ac7963dbbfd1fc81b024adff705ae7c0e08992d1dba5 + htmlparser2: "npm:^10.1.0" + selderee: "npm:~0.12.0" + checksum: 10/f941412800fbd9b149079ac67af2371bc8124931e38ea212fd52d7f5efec37f793958555519fa12cbd95d9e9b14d6b296bf2343e8b7ee51fdac6e4039aff98c7 languageName: node linkType: hard @@ -23896,18 +24069,6 @@ __metadata: languageName: node linkType: hard -"htmlparser2@npm:^8.0.2": - version: 8.0.2 - resolution: "htmlparser2@npm:8.0.2" - dependencies: - domelementtype: "npm:^2.3.0" - domhandler: "npm:^5.0.3" - domutils: "npm:^3.0.1" - entities: "npm:^4.4.0" - checksum: 10/ea5512956eee06f5835add68b4291d313c745e8407efa63848f4b8a90a2dee45f498a698bca8614e436f1ee0cfdd609938b71d67c693794545982b76e53e6f11 - languageName: node - linkType: hard - "htmlparser2@npm:^9.1.0": version: 9.1.0 resolution: "htmlparser2@npm:9.1.0" @@ -24147,16 +24308,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:0.7.0": - version: 0.7.0 - resolution: "iconv-lite@npm:0.7.0" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 10/5bfc897fedfb7e29991ae5ef1c061ed4f864005f8c6d61ef34aba6a3885c04bd207b278c0642b041383aeac2d11645b4319d0ca7b863b0be4be0cde1c9238ca7 - languageName: node - linkType: hard - -"iconv-lite@npm:^0.7.0": +"iconv-lite@npm:0.7.2, iconv-lite@npm:^0.7.0": version: 0.7.2 resolution: "iconv-lite@npm:0.7.2" dependencies: @@ -26897,10 +27049,10 @@ __metadata: languageName: node linkType: hard -"leac@npm:^0.6.0": - version: 0.6.0 - resolution: "leac@npm:0.6.0" - checksum: 10/bfe6aa128ca98664f124096f65584778194a8e1ddebf77d315fd9681be849c1619b0a9d9f4743e67aea0298808bd69ef25bd74320cad50a80be6852714627870 +"leac@npm:^0.7.0": + version: 0.7.0 + resolution: "leac@npm:0.7.0" + checksum: 10/33bcfae037d626c3bbb3fafd1f0571e525f795fc03ad9383c532fa95fe9c188e79189940e530ea7c73bba573834fc41688654721592edec5c193b93c28f386e6 languageName: node linkType: hard @@ -26935,15 +27087,15 @@ __metadata: languageName: node linkType: hard -"libmime@npm:5.3.7": - version: 5.3.7 - resolution: "libmime@npm:5.3.7" +"libmime@npm:5.3.8": + version: 5.3.8 + resolution: "libmime@npm:5.3.8" dependencies: encoding-japanese: "npm:2.2.0" - iconv-lite: "npm:0.6.3" + iconv-lite: "npm:0.7.2" libbase64: "npm:1.3.0" libqp: "npm:2.1.1" - checksum: 10/9f148cd94256f604e71af60c159ad08b8914029727220b2a048e1c95957b099a41fb0c8a225dd1af87d5dfd4fa7c26afd7cf83d395894b7214e6827d7e25511b + checksum: 10/6a3550470d0003e530dd5fa8c5195fd70f00f51690899dd82effbd33def92df002921b909c256f410987f39d16974e9516f04b48f2f864eedff18187c8aead0a languageName: node linkType: hard @@ -27141,7 +27293,16 @@ __metadata: languageName: node linkType: hard -"linkify-it@npm:5.0.0, linkify-it@npm:^5.0.0": +"linkify-it@npm:5.0.1": + version: 5.0.1 + resolution: "linkify-it@npm:5.0.1" + dependencies: + uc.micro: "npm:^2.0.0" + checksum: 10/47933d11cda495fd31e3bb3c086358305362f4413ffa289500d1e48164acb221b3be6bde6404b86dba93b7fe701c95f318bee285f7613092cd7f28a9e97261e7 + languageName: node + linkType: hard + +"linkify-it@npm:^5.0.0": version: 5.0.0 resolution: "linkify-it@npm:5.0.0" dependencies: @@ -27411,6 +27572,13 @@ __metadata: languageName: node linkType: hard +"long@npm:^5.3.2": + version: 5.3.2 + resolution: "long@npm:5.3.2" + checksum: 10/b6b55ddae56fcce2864d37119d6b02fe28f6dd6d9e44fd22705f86a9254b9321bd69e9ffe35263b4846d54aba197c64882adcb8c543f2383c1e41284b321ea64 + languageName: node + linkType: hard + "longest-streak@npm:^2.0.0": version: 2.0.4 resolution: "longest-streak@npm:2.0.4" @@ -27538,32 +27706,21 @@ __metadata: languageName: node linkType: hard -"mailparser@npm:~3.7.5": - version: 3.7.5 - resolution: "mailparser@npm:3.7.5" +"mailparser@npm:~3.9.9": + version: 3.9.9 + resolution: "mailparser@npm:3.9.9" dependencies: + "@zone-eu/mailsplit": "npm:5.4.12" encoding-japanese: "npm:2.2.0" he: "npm:1.2.0" - html-to-text: "npm:9.0.5" - iconv-lite: "npm:0.7.0" - libmime: "npm:5.3.7" - linkify-it: "npm:5.0.0" - mailsplit: "npm:5.4.6" - nodemailer: "npm:7.0.9" + html-to-text: "npm:10.0.0" + iconv-lite: "npm:0.7.2" + libmime: "npm:5.3.8" + linkify-it: "npm:5.0.1" + nodemailer: "npm:8.0.10" punycode.js: "npm:2.3.1" - tlds: "npm:1.260.0" - checksum: 10/6c8ac6d76d82a76e1338bfaecd36cfb04ef49e94dccad7263ea62efbea8abf3d5102c2a5c786706c1d4fd2fa1e951f1df3f7161b93341a55793908769b271c2d - languageName: node - linkType: hard - -"mailsplit@npm:5.4.6": - version: 5.4.6 - resolution: "mailsplit@npm:5.4.6" - dependencies: - libbase64: "npm:1.3.0" - libmime: "npm:5.3.7" - libqp: "npm:2.1.1" - checksum: 10/c7455aee74313455d6e2ee29327434be9dc55fe2a7688865fcacdac2b7ddbf416ffeaae72489391e6a9522479264867c8e8eb851010b19f20d209bb9c0a7be6b + tlds: "npm:1.261.0" + checksum: 10/e0cccacded20cc38d812f702d3c603f866a438b21fd90853e7456e2938389bdbcbd093152eb229616bc9f2631b6c8b6f657258a3c686b090c009a6287e2a6780 languageName: node linkType: hard @@ -28087,7 +28244,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -28739,6 +28896,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.12": + version: 3.3.12 + resolution: "nanoid@npm:3.3.12" + bin: + nanoid: bin/nanoid.cjs + checksum: 10/6eec280694e2088d18fb802b1e3bfc4578e27b665b7ecfbe36c7356612fea2f814277056e671e2a1529dff551588a652efdc0bfa39f8a3185bc2247be311872e + languageName: node + linkType: hard + "nanoid@npm:^3.3.8": version: 3.3.8 resolution: "nanoid@npm:3.3.8" @@ -29034,10 +29200,10 @@ __metadata: languageName: node linkType: hard -"nodemailer@npm:8.0.7": - version: 8.0.7 - resolution: "nodemailer@npm:8.0.7" - checksum: 10/f0afea43af7f7deb55913811362b5d255e9d70c87024eb8b5161acd8a3a3c765454a52eefbfe40a5fe79ff0475b5445acafd20cb187826222b895dee15470c8f +"nodemailer@npm:8.0.8": + version: 8.0.8 + resolution: "nodemailer@npm:8.0.8" + checksum: 10/b4d2d095355751ab4baa7047c35845a05a42eec734218e69fb36d16e9efaa0804d41741e39bbe0e22dfd416a9be835b09b82df624fd5da2b5d7aa8ca5a449e55 languageName: node linkType: hard @@ -29927,13 +30093,13 @@ __metadata: languageName: node linkType: hard -"parseley@npm:^0.12.0": - version: 0.12.1 - resolution: "parseley@npm:0.12.1" +"parseley@npm:~0.13.1": + version: 0.13.1 + resolution: "parseley@npm:0.13.1" dependencies: - leac: "npm:^0.6.0" - peberminta: "npm:^0.9.0" - checksum: 10/64788dbe1fbbc231e0fef235357823e03ca3d915693b2109ad862293aad5d091e902fd7cf6f54763728e758a228d06497c787a9af0dfdacd6a941bc2dbf2019e + leac: "npm:^0.7.0" + peberminta: "npm:^0.10.0" + checksum: 10/47dec80e2fb017c2f184e7261755edb8f0e1406a16123076d72b4b769bb6edab7933388c33826e79c12e54dfbe0013470fdf4536a7f35c0e97a67592a04fff92 languageName: node linkType: hard @@ -30149,10 +30315,10 @@ __metadata: languageName: node linkType: hard -"peberminta@npm:^0.9.0": - version: 0.9.0 - resolution: "peberminta@npm:0.9.0" - checksum: 10/b396cf8bac836b3cfe9315e6c94747fa02bb68b252a4b176c0f7d6a3fcbdc5e9f23c3523480c91244b42f87be63f200d775587b039abe4b4731915480da2528d +"peberminta@npm:^0.10.0": + version: 0.10.0 + resolution: "peberminta@npm:0.10.0" + checksum: 10/a25e424a1e99d64b08a4fa3fffd7d257cc77568df2bfe35cab55a531851cd9246749d8075d50280f0e6910d877687cbe477260e80cad86d7d64c0e7a4c16b40c languageName: node linkType: hard @@ -31103,7 +31269,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.5.10, postcss@npm:^8.5.10": +"postcss@npm:8.5.10": version: 8.5.10 resolution: "postcss@npm:8.5.10" dependencies: @@ -31135,6 +31301,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.5.15": + version: 8.5.15 + resolution: "postcss@npm:8.5.15" + dependencies: + nanoid: "npm:^3.3.12" + picocolors: "npm:^1.1.1" + source-map-js: "npm:^1.2.1" + checksum: 10/d02ad19eb1e0fa53a1229ee6d53807eb88f903f2b9a8cac66993367f3ac7dd3b97238c783a54ccbf4145f82f6ca9a5cbd58f089846285d759c8a3259fbea8318 + languageName: node + linkType: hard + "postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" @@ -31422,23 +31599,23 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:7.5.8": - version: 7.5.8 - resolution: "protobufjs@npm:7.5.8" +"protobufjs@npm:7.6.1": + version: 7.6.1 + resolution: "protobufjs@npm:7.6.1" dependencies: "@protobufjs/aspromise": "npm:^1.1.2" "@protobufjs/base64": "npm:^1.1.2" "@protobufjs/codegen": "npm:^2.0.5" - "@protobufjs/eventemitter": "npm:^1.1.0" - "@protobufjs/fetch": "npm:^1.1.0" + "@protobufjs/eventemitter": "npm:^1.1.1" + "@protobufjs/fetch": "npm:^1.1.1" "@protobufjs/float": "npm:^1.0.2" - "@protobufjs/inquire": "npm:^1.1.1" + "@protobufjs/inquire": "npm:^1.1.2" "@protobufjs/path": "npm:^1.1.2" "@protobufjs/pool": "npm:^1.1.0" "@protobufjs/utf8": "npm:^1.1.1" "@types/node": "npm:>=13.7.0" - long: "npm:^5.0.0" - checksum: 10/2beb69b88a5c0a8420d0d5934f2358d417ecd6446f4c0d7c8a9ba5fc0e62ec1631938c0ab083258c14f15b87d39d559b49b398779638df57b2f3a3238cc0be1c + long: "npm:^5.3.2" + checksum: 10/010dcf77ab435da50d62887d57cc4b68384768e4d1cbe85948ec747996222ea659501b7c6b6d3426f96fdbaf59f577213788ce461c0938f89b8126569d8be7e2 languageName: node linkType: hard @@ -33121,7 +33298,7 @@ __metadata: eslint-plugin-you-dont-need-lodash-underscore: "npm:~6.14.0" node-gyp: "npm:^11.0.0" ts-node: "npm:^10.9.2" - turbo: "npm:2.8.21" + turbo: "npm:2.9.14" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -33174,27 +33351,27 @@ __metadata: languageName: unknown linkType: soft -"rolldown@npm:1.0.0-rc.16": - version: 1.0.0-rc.16 - resolution: "rolldown@npm:1.0.0-rc.16" - dependencies: - "@oxc-project/types": "npm:=0.126.0" - "@rolldown/binding-android-arm64": "npm:1.0.0-rc.16" - "@rolldown/binding-darwin-arm64": "npm:1.0.0-rc.16" - "@rolldown/binding-darwin-x64": "npm:1.0.0-rc.16" - "@rolldown/binding-freebsd-x64": "npm:1.0.0-rc.16" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.0-rc.16" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.0-rc.16" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.0-rc.16" - "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.0-rc.16" - "@rolldown/binding-linux-s390x-gnu": "npm:1.0.0-rc.16" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.0-rc.16" - "@rolldown/binding-linux-x64-musl": "npm:1.0.0-rc.16" - "@rolldown/binding-openharmony-arm64": "npm:1.0.0-rc.16" - "@rolldown/binding-wasm32-wasi": "npm:1.0.0-rc.16" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.0-rc.16" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.0-rc.16" - "@rolldown/pluginutils": "npm:1.0.0-rc.16" +"rolldown@npm:1.0.3": + version: 1.0.3 + resolution: "rolldown@npm:1.0.3" + dependencies: + "@oxc-project/types": "npm:=0.133.0" + "@rolldown/binding-android-arm64": "npm:1.0.3" + "@rolldown/binding-darwin-arm64": "npm:1.0.3" + "@rolldown/binding-darwin-x64": "npm:1.0.3" + "@rolldown/binding-freebsd-x64": "npm:1.0.3" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.3" + "@rolldown/binding-linux-arm64-gnu": "npm:1.0.3" + "@rolldown/binding-linux-arm64-musl": "npm:1.0.3" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.3" + "@rolldown/binding-linux-s390x-gnu": "npm:1.0.3" + "@rolldown/binding-linux-x64-gnu": "npm:1.0.3" + "@rolldown/binding-linux-x64-musl": "npm:1.0.3" + "@rolldown/binding-openharmony-arm64": "npm:1.0.3" + "@rolldown/binding-wasm32-wasi": "npm:1.0.3" + "@rolldown/binding-win32-arm64-msvc": "npm:1.0.3" + "@rolldown/binding-win32-x64-msvc": "npm:1.0.3" + "@rolldown/pluginutils": "npm:^1.0.0" dependenciesMeta: "@rolldown/binding-android-arm64": optional: true @@ -33227,8 +33404,8 @@ __metadata: "@rolldown/binding-win32-x64-msvc": optional: true bin: - rolldown: bin/cli.mjs - checksum: 10/064ef08cbd171842da7595db7519bb5b7ca2a620b4481338716f2d494353c01511efbb01e93805de649e8cba2898b8ed7814193381ca1edd5b9883f51caf0865 + rolldown: ./bin/cli.mjs + checksum: 10/4dbe2c055104c47c15c051b713068cf4660acd473841904d3f7118f730922b2e498176610a45826cbc1ffe36842a29a076385d3bfcd5acb0f7ef8ad06b8feefb languageName: node linkType: hard @@ -33516,12 +33693,12 @@ __metadata: languageName: node linkType: hard -"selderee@npm:^0.11.0": - version: 0.11.0 - resolution: "selderee@npm:0.11.0" +"selderee@npm:~0.12.0": + version: 0.12.0 + resolution: "selderee@npm:0.12.0" dependencies: - parseley: "npm:^0.12.0" - checksum: 10/9f697a00b8270354777a8423e555fd3168abead1304b8d267412877a4b007830624d8aa562eb29a3ec2d9d2f7f977808d17b790b9c210a7d828c12ed9ef0f1f0 + parseley: "npm:~0.13.1" + checksum: 10/758dba9583ce0c238002d25adca2a730492873eb1458d4e11889cc5c78db3d2add14c09c1805bf7c14949657f068fa55b27ebdf7b09219093464446d5653a2ee languageName: node linkType: hard @@ -35219,16 +35396,16 @@ __metadata: languageName: node linkType: hard -"tar@npm:7.5.13": - version: 7.5.13 - resolution: "tar@npm:7.5.13" +"tar@npm:7.5.16": + version: 7.5.16 + resolution: "tar@npm:7.5.16" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10/2bc2b6f0349038a6621dbba1c4522d45752d5071b2994692257113c2050cd23fafc30308f820e5f8ad6fda3f7d7f92adc9a432aa733daa04c42af2061c021c3f + checksum: 10/fafa22efceb9f056bf29ddc47d9bd90bb82fe3ce57b8d1242fc45771251741964cebba69d4e14a24fd1643f3c7f68478e945a19def534703cf370c2d9dca2e09 languageName: node linkType: hard @@ -35464,7 +35641,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.16": +"tinyglobby@npm:^0.2.12": version: 0.2.16 resolution: "tinyglobby@npm:0.2.16" dependencies: @@ -35484,6 +35661,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.17": + version: 0.2.17 + resolution: "tinyglobby@npm:0.2.17" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.4" + checksum: 10/f85e8a217d675c3f78d5f0ad25ea4557e7e023ed13ddc2b014da10bd0312eea53a34cd52356af07ccdff777f1243012547656282a4ca70936f68bf5065fbaa71 + languageName: node + linkType: hard + "tinykeys@npm:^1.4.0": version: 1.4.0 resolution: "tinykeys@npm:1.4.0" @@ -35505,12 +35692,12 @@ __metadata: languageName: node linkType: hard -"tlds@npm:1.260.0": - version: 1.260.0 - resolution: "tlds@npm:1.260.0" +"tlds@npm:1.261.0": + version: 1.261.0 + resolution: "tlds@npm:1.261.0" bin: tlds: bin.js - checksum: 10/769c6c5d88ee5e8de9bdb70a3d609e0f43e6af776b3959cc226454c1b3b2074c769731f280b3c3e43cc7f92d544f51c8525b676034de37755cf1b5d51109fd37 + checksum: 10/2dfb8c7e0c0c1fe8fd9966ead43f71c36c974aa498b4e290fb383ceb3be3b20b9517041560b966993fb30abd451a83bf37119fa8d99809ce01bbf5b21dd6c6d3 languageName: node linkType: hard @@ -35532,10 +35719,10 @@ __metadata: languageName: node linkType: hard -"tmp@npm:0.2.6": - version: 0.2.6 - resolution: "tmp@npm:0.2.6" - checksum: 10/4ba072821d65f6ec0ae680dd49261bcc66c96a5a463c80ca040747256238aaad68ad5db7aa8367dd1554d22aa77c2988bdb1c5556ecfc4df105f5b73206b7d9b +"tmp@npm:0.2.7": + version: 0.2.7 + resolution: "tmp@npm:0.2.7" + checksum: 10/0a3bc90beb0c6275273c3475fb57e466eaab1c9c4a101d029ff62b18146ce136e7f75d09de34863d9f2c2a492751402508f9e028bc98eb34a1416195d4b15619 languageName: node linkType: hard @@ -35967,16 +36154,16 @@ __metadata: languageName: node linkType: hard -"turbo@npm:2.8.21": - version: 2.8.21 - resolution: "turbo@npm:2.8.21" +"turbo@npm:2.9.14": + version: 2.9.14 + resolution: "turbo@npm:2.9.14" dependencies: - "@turbo/darwin-64": "npm:2.8.21" - "@turbo/darwin-arm64": "npm:2.8.21" - "@turbo/linux-64": "npm:2.8.21" - "@turbo/linux-arm64": "npm:2.8.21" - "@turbo/windows-64": "npm:2.8.21" - "@turbo/windows-arm64": "npm:2.8.21" + "@turbo/darwin-64": "npm:2.9.14" + "@turbo/darwin-arm64": "npm:2.9.14" + "@turbo/linux-64": "npm:2.9.14" + "@turbo/linux-arm64": "npm:2.9.14" + "@turbo/windows-64": "npm:2.9.14" + "@turbo/windows-arm64": "npm:2.9.14" dependenciesMeta: "@turbo/darwin-64": optional: true @@ -35992,7 +36179,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 10/f7f6e391b9babae5da3275e7c63cad481c4362b0559b8769739f43d6cec216458924482b8635744cdda5ed4916e06db2f4fd847a6203de3c8095ecac0d8a6d5d + checksum: 10/99ce1f10c79ec840b542cc628d44f6febc18fcff8fed08f7f966a983a71b03c7febcf3e16066667a01f9d8d7faca133ebd541c57555bd509364f669ceb46807f languageName: node linkType: hard @@ -36916,19 +37103,19 @@ __metadata: languageName: node linkType: hard -"vite@npm:^8.0.9": - version: 8.0.9 - resolution: "vite@npm:8.0.9" +"vite@npm:^8.0.16": + version: 8.0.16 + resolution: "vite@npm:8.0.16" dependencies: fsevents: "npm:~2.3.3" lightningcss: "npm:^1.32.0" picomatch: "npm:^4.0.4" - postcss: "npm:^8.5.10" - rolldown: "npm:1.0.0-rc.16" - tinyglobby: "npm:^0.2.16" + postcss: "npm:^8.5.15" + rolldown: "npm:1.0.3" + tinyglobby: "npm:^0.2.17" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 - "@vitejs/devtools": ^0.1.0 + "@vitejs/devtools": ^0.1.18 esbuild: ^0.27.0 || ^0.28.0 jiti: ">=1.21.0" less: ^4.0.0 @@ -36969,7 +37156,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/745ff66e6d6379d93172cf03c2788db73b83b3f6ad7980456be5d523daf38ee2397d15d266e1acad5afbab950709081e640b447e3ce8a32f0e06856f37253a08 + checksum: 10/a5d91d26f6110672a292a06ca161af9a58279fe9d27106c8c0afb725a942b0b47091c440c3b1e7ebc8e0fe901f64ac6a2ffee3cdae2f899339686dbecd0c0266 languageName: node linkType: hard @@ -37186,9 +37373,9 @@ __metadata: languageName: node linkType: hard -"webpack-dev-server@npm:~5.2.3": - version: 5.2.3 - resolution: "webpack-dev-server@npm:5.2.3" +"webpack-dev-server@npm:~5.2.5": + version: 5.2.5 + resolution: "webpack-dev-server@npm:5.2.5" dependencies: "@types/bonjour": "npm:^3.5.13" "@types/connect-history-api-fallback": "npm:^1.5.4" @@ -37227,7 +37414,7 @@ __metadata: optional: true bin: webpack-dev-server: bin/webpack-dev-server.js - checksum: 10/6a3d55c5d84d10b5e23a0638e0031ef85b262947fdacc86d96b7392538ad5894ac14aebef1e2b877f8d27302c71247215b7aa6713e01b1c37d31342415b1a150 + checksum: 10/2bd8e03615c32ecac53e783e2be5670ccac051d051ef4ed7fe944df8fd8745b508c8ebc63bfecfdccd23c2200ca4c373b22ea2da76ce4e1c0a6f58d900c11ecb languageName: node linkType: hard @@ -37604,9 +37791,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.5.3": - version: 7.5.10 - resolution: "ws@npm:7.5.10" +"ws@npm:7.5.11": + version: 7.5.11 + resolution: "ws@npm:7.5.11" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ^5.0.2 @@ -37615,7 +37802,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 10/9c796b84ba80ffc2c2adcdfc9c8e9a219ba99caa435c9a8d45f9ac593bba325563b3f83edc5eb067cc6d21b9a6bf2c930adf76dd40af5f58a5ca6859e81858f0 + checksum: 10/486141e4a01bb75883f9ba39317309c2427e24db1cb75e340fad6e5886b65c03d994a34209f0e4ba06dd6cb9ec95dd1b6a09c52c05eed9a34d6376f4fbbf617c languageName: node linkType: hard From 5e40de79f0ec3ac833ef3b888dc2a1601e6413ca Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 16 Jun 2026 12:12:37 -0300 Subject: [PATCH 06/11] fix: Use a error boundary fallback on room's contextual bar (#40970) --- apps/meteor/client/views/room/Room.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/Room.tsx b/apps/meteor/client/views/room/Room.tsx index 1ded7fa3bae84..b6c713db3f0ba 100644 --- a/apps/meteor/client/views/room/Room.tsx +++ b/apps/meteor/client/views/room/Room.tsx @@ -19,6 +19,7 @@ import RoomLayout from './layout/RoomLayout'; import ChatProvider from './providers/ChatProvider'; import { DateListProvider } from './providers/DateListProvider'; import { SelectedMessagesProvider } from './providers/SelectedMessagesProvider'; +import GenericError from '../../components/GenericError'; const UiKitContextualBar = lazy(() => import('./contextualBar/uikit/UiKitContextualBar')); @@ -65,7 +66,7 @@ const Room = () => { } aside={ (toolbox.tab?.tabComponent && ( - + }> }>{createElement(toolbox.tab.tabComponent)} @@ -73,7 +74,7 @@ const Room = () => { )) || (contextualBarView && ( // TODO: improve fallback handling - + }> }> From fbf367269c4fb27146e2c4b9616ff372c37bcb7a Mon Sep 17 00:00:00 2001 From: Yasmim Nagat <117310290+yasnagat@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:47:03 -0300 Subject: [PATCH 07/11] chore(deps): bump tsx (#40979) Co-authored-by: Julio Araujo --- apps/meteor/package.json | 2 +- package.json | 1 + yarn.lock | 285 +-------------------------------------- 3 files changed, 9 insertions(+), 279 deletions(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 0c57e8e52c7c9..8694ea1286a29 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -449,7 +449,7 @@ "supports-color": "~7.2.0", "template-file": "^6.0.1", "ts-node": "^10.9.2", - "tsx": "~4.21.0", + "tsx": "~4.22.4", "typescript": "~5.9.3", "webpack": "~5.104.1" }, diff --git a/package.json b/package.json index ca5407c82da3c..785b8f9b7e770 100644 --- a/package.json +++ b/package.json @@ -167,6 +167,7 @@ "@opentelemetry/exporter-trace-otlp-grpc/@grpc/grpc-js": "^1.14.4", "@opentelemetry/otlp-grpc-exporter-base/@grpc/grpc-js": "^1.14.4", "storybook/esbuild": "~0.28.1", + "tsx/esbuild": "0.28.1", "qs": "6.15.2", "uuid@npm:13": "13.0.1" }, diff --git a/yarn.lock b/yarn.lock index 29311dfbc6a42..8436bd65636ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3632,13 +3632,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/aix-ppc64@npm:0.27.7" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/aix-ppc64@npm:0.28.1" @@ -3646,13 +3639,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/android-arm64@npm:0.27.7" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/android-arm64@npm:0.28.1" @@ -3660,13 +3646,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/android-arm@npm:0.27.7" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/android-arm@npm:0.28.1" @@ -3674,13 +3653,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/android-x64@npm:0.27.7" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/android-x64@npm:0.28.1" @@ -3688,13 +3660,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/darwin-arm64@npm:0.27.7" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/darwin-arm64@npm:0.28.1" @@ -3702,13 +3667,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/darwin-x64@npm:0.27.7" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/darwin-x64@npm:0.28.1" @@ -3716,13 +3674,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/freebsd-arm64@npm:0.27.7" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/freebsd-arm64@npm:0.28.1" @@ -3730,13 +3681,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/freebsd-x64@npm:0.27.7" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/freebsd-x64@npm:0.28.1" @@ -3744,13 +3688,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/linux-arm64@npm:0.27.7" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/linux-arm64@npm:0.28.1" @@ -3758,13 +3695,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/linux-arm@npm:0.27.7" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/linux-arm@npm:0.28.1" @@ -3772,13 +3702,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/linux-ia32@npm:0.27.7" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/linux-ia32@npm:0.28.1" @@ -3786,13 +3709,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/linux-loong64@npm:0.27.7" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/linux-loong64@npm:0.28.1" @@ -3800,13 +3716,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/linux-mips64el@npm:0.27.7" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/linux-mips64el@npm:0.28.1" @@ -3814,13 +3723,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/linux-ppc64@npm:0.27.7" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/linux-ppc64@npm:0.28.1" @@ -3828,13 +3730,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/linux-riscv64@npm:0.27.7" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/linux-riscv64@npm:0.28.1" @@ -3842,13 +3737,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/linux-s390x@npm:0.27.7" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/linux-s390x@npm:0.28.1" @@ -3856,13 +3744,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/linux-x64@npm:0.27.7" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/linux-x64@npm:0.28.1" @@ -3870,13 +3751,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/netbsd-arm64@npm:0.27.7" - conditions: os=netbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/netbsd-arm64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/netbsd-arm64@npm:0.28.1" @@ -3884,13 +3758,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/netbsd-x64@npm:0.27.7" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/netbsd-x64@npm:0.28.1" @@ -3898,13 +3765,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/openbsd-arm64@npm:0.27.7" - conditions: os=openbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/openbsd-arm64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/openbsd-arm64@npm:0.28.1" @@ -3912,13 +3772,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/openbsd-x64@npm:0.27.7" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/openbsd-x64@npm:0.28.1" @@ -3926,13 +3779,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openharmony-arm64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/openharmony-arm64@npm:0.27.7" - conditions: os=openharmony & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/openharmony-arm64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/openharmony-arm64@npm:0.28.1" @@ -3940,13 +3786,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/sunos-x64@npm:0.27.7" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/sunos-x64@npm:0.28.1" @@ -3954,13 +3793,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/win32-arm64@npm:0.27.7" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/win32-arm64@npm:0.28.1" @@ -3968,13 +3800,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/win32-ia32@npm:0.27.7" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/win32-ia32@npm:0.28.1" @@ -3982,13 +3807,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.27.7": - version: 0.27.7 - resolution: "@esbuild/win32-x64@npm:0.27.7" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.28.1": version: 0.28.1 resolution: "@esbuild/win32-x64@npm:0.28.1" @@ -10430,7 +10248,7 @@ __metadata: textarea-caret: "npm:^3.1.0" tinykeys: "npm:^1.4.0" ts-node: "npm:^10.9.2" - tsx: "npm:~4.21.0" + tsx: "npm:~4.22.4" tsyringe: "npm:^4.10.0" tweetnacl: "npm:^1.0.3" twilio: "npm:~5.4.5" @@ -21279,96 +21097,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:~0.27.0": - version: 0.27.7 - resolution: "esbuild@npm:0.27.7" - dependencies: - "@esbuild/aix-ppc64": "npm:0.27.7" - "@esbuild/android-arm": "npm:0.27.7" - "@esbuild/android-arm64": "npm:0.27.7" - "@esbuild/android-x64": "npm:0.27.7" - "@esbuild/darwin-arm64": "npm:0.27.7" - "@esbuild/darwin-x64": "npm:0.27.7" - "@esbuild/freebsd-arm64": "npm:0.27.7" - "@esbuild/freebsd-x64": "npm:0.27.7" - "@esbuild/linux-arm": "npm:0.27.7" - "@esbuild/linux-arm64": "npm:0.27.7" - "@esbuild/linux-ia32": "npm:0.27.7" - "@esbuild/linux-loong64": "npm:0.27.7" - "@esbuild/linux-mips64el": "npm:0.27.7" - "@esbuild/linux-ppc64": "npm:0.27.7" - "@esbuild/linux-riscv64": "npm:0.27.7" - "@esbuild/linux-s390x": "npm:0.27.7" - "@esbuild/linux-x64": "npm:0.27.7" - "@esbuild/netbsd-arm64": "npm:0.27.7" - "@esbuild/netbsd-x64": "npm:0.27.7" - "@esbuild/openbsd-arm64": "npm:0.27.7" - "@esbuild/openbsd-x64": "npm:0.27.7" - "@esbuild/openharmony-arm64": "npm:0.27.7" - "@esbuild/sunos-x64": "npm:0.27.7" - "@esbuild/win32-arm64": "npm:0.27.7" - "@esbuild/win32-ia32": "npm:0.27.7" - "@esbuild/win32-x64": "npm:0.27.7" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-arm64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-arm64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/openharmony-arm64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10/262b16c4a33cb70e9f054759a7ce420541649315eef7b064172c795021ccce322e56c3f5fd52e8842873f1c23745f3ab62311a24860950bd5406ba77b36b8529 - languageName: node - linkType: hard - -"esbuild@npm:~0.28.1": +"esbuild@npm:0.28.1, esbuild@npm:~0.28.1": version: 0.28.1 resolution: "esbuild@npm:0.28.1" dependencies: @@ -36100,18 +35829,18 @@ __metadata: languageName: node linkType: hard -"tsx@npm:~4.21.0": - version: 4.21.1 - resolution: "tsx@npm:4.21.1" +"tsx@npm:~4.22.4": + version: 4.22.4 + resolution: "tsx@npm:4.22.4" dependencies: - esbuild: "npm:~0.27.0" + esbuild: "npm:~0.28.0" fsevents: "npm:~2.3.3" dependenciesMeta: fsevents: optional: true bin: tsx: dist/cli.mjs - checksum: 10/e7ee59e3912b05f994714a8c4b4d39040cb54559db60e262b58269f6c099d0ee128c2ccc53379f2ba25e10f398928f86b86d0fdfe3eb6d02c6342fc0597aeed8 + checksum: 10/9d6e7e9d1158eb84327d822720a2053bcfd972f8988e4789041bac5a767fc394ef302b1ff7e1d3dfbee391a0b03bd3dbb02cd302387729baf1dc7b9dce93b3ba languageName: node linkType: hard From 33b79234150b73e93ba37d0e814da7bf4e428b36 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Tue, 16 Jun 2026 14:35:32 -0300 Subject: [PATCH 08/11] fix: thread panel not scrolling to bottom after sending a reply (#40956) Co-authored-by: Claude Sonnet 4.6 --- .changeset/old-cities-enjoy.md | 5 +++++ .../Threads/components/ThreadMessageList.tsx | 20 ++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .changeset/old-cities-enjoy.md diff --git a/.changeset/old-cities-enjoy.md b/.changeset/old-cities-enjoy.md new file mode 100644 index 0000000000000..581cafa174ceb --- /dev/null +++ b/.changeset/old-cities-enjoy.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixes issue that caused threads to sometimes not scroll when sending messages diff --git a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx index 00636af5c35db..65b0c51234d8c 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx @@ -75,6 +75,7 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot const virtualizerRef = useRef(null); const isAtBottom = useRef(null); + const prevItemsLengthRef = useRef(0); const { keepAtBottomRef, setKeepAtBottom } = useKeepAtBottom(isAtBottom); const messagesLength = messages.length; @@ -109,6 +110,7 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot useEffect(() => { lastThreadJumpKeyRef.current = undefined; + prevItemsLengthRef.current = 0; }, [mainMessage._id]); useEffect(() => { @@ -120,15 +122,31 @@ const ThreadMessageList = ({ mainMessage, shouldJumpToBottom, setShouldJumpToBot setShouldJumpToBottom(false); return; } + + // Scroll to bottom when current user's optimistic (temp) message is appended. + // Fires before server confirmation, giving immediate scroll feedback. + const prev = prevItemsLengthRef.current; + prevItemsLengthRef.current = items.length; + if (items.length > prev && uid) { + const lastItem = items.at(-1); + if (lastItem?.temp && lastItem.u._id === uid) { + setShouldJumpToBottom(true); + } + } + if (isAtBottom.current === true && lastScrollSizeRef.current !== handle?.scrollSize) { lastScrollSizeRef.current = handle?.scrollSize ?? 0; setShouldJumpToBottom(true); } if (shouldJumpToBottom) { + // Optimistically mark as at-bottom before the scroll executes (rAF). + // This ensures the ResizeObserver in useKeepAtBottom re-scrolls if + // quote/attachment content grows between now and when Virtua fires the scroll. + isAtBottom.current = true; handle.scrollToIndex(items.length, { align: 'end' }); setShouldJumpToBottom(false); } - }, [loading, items.length, msgJumpParam, threadMsgTargetIndex, shouldJumpToBottom, setShouldJumpToBottom]); + }, [items, loading, msgJumpParam, threadMsgTargetIndex, shouldJumpToBottom, setShouldJumpToBottom, uid]); useEffect(() => { if (threadMsgTargetIndex < 0 || !msgJumpParam) { From 02b80089e0f65df5e01d7d3c9422cf56c3771d86 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 16 Jun 2026 14:52:58 -0300 Subject: [PATCH 09/11] chore: Upgrade fuselage packages (#40974) --- .../CreateDiscussion.spec.tsx.snap | 1 + .../TimestampPicker.spec.tsx.snap | 386 +++++++++--------- .../RoomFormAttributeField.spec.tsx.snap | 1 + .../AppLogsFilterContextualBar.spec.tsx.snap | 166 ++++---- .../AppLogsFilterExpanded.spec.tsx.snap | 176 ++++---- .../CannedResponseList.spec.tsx.snap | 56 +-- .../__snapshots__/RoomFiles.spec.tsx.snap | 258 ++++++------ .../__snapshots__/InviteUsers.spec.tsx.snap | 166 ++++---- .../__snapshots__/RoomMembers.spec.tsx.snap | 240 ++++++----- apps/meteor/package.json | 4 +- apps/uikit-playground/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/package.json | 2 +- packages/storybook-config/package.json | 2 +- packages/ui-avatar/package.json | 2 +- packages/ui-client/package.json | 4 +- packages/ui-composer/package.json | 2 +- packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/package.json | 2 +- packages/ui-voip/package.json | 2 +- packages/web-ui-registration/package.json | 4 +- yarn.lock | 47 +-- 22 files changed, 799 insertions(+), 728 deletions(-) diff --git a/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap b/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap index f79ddc71ebcc7..915ce0fc0ed33 100644 --- a/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap +++ b/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap @@ -215,6 +215,7 @@ exports[`CreateDiscussion renders Default without crashing 1`] = ` aria-haspopup="listbox" aria-invalid="false" aria-labelledby="react-aria-:r4:-label" + autocomplete="off" class="rcx-box rcx-box--full rcx-box--animated rcx-input-box--undecorated rcx-input-box rcx-css-trljwa rcx-css-pqpp8q" id="react-aria-:r4:" name="usernames" diff --git a/apps/meteor/client/components/message/toolbar/items/actions/Timestamp/TimestampPicker/__snapshots__/TimestampPicker.spec.tsx.snap b/apps/meteor/client/components/message/toolbar/items/actions/Timestamp/TimestampPicker/__snapshots__/TimestampPicker.spec.tsx.snap index a504f39d3e63c..433706e37a582 100644 --- a/apps/meteor/client/components/message/toolbar/items/actions/Timestamp/TimestampPicker/__snapshots__/TimestampPicker.spec.tsx.snap +++ b/apps/meteor/client/components/message/toolbar/items/actions/Timestamp/TimestampPicker/__snapshots__/TimestampPicker.spec.tsx.snap @@ -213,54 +213,58 @@ exports[`Story Tests renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-field__row" >