From 5387d91667904cab443bda8aa82519e995310d80 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 20 Mar 2025 08:05:46 -0600 Subject: [PATCH 001/187] chore: User model to TS (#35056) --- .../api/server/helpers/addUserToFileObj.ts | 4 +- apps/meteor/app/api/server/lib/users.ts | 4 +- apps/meteor/app/api/server/v1/channels.ts | 8 +- apps/meteor/app/api/server/v1/groups.ts | 4 +- apps/meteor/app/api/server/v1/im.ts | 5 +- apps/meteor/app/api/server/v1/users.ts | 10 +- apps/meteor/app/api/server/v1/voip/rooms.ts | 4 +- .../app/apps/server/bridges/activation.ts | 3 +- .../methods/setUserPublicAndPrivateKeys.ts | 6 +- .../classes/converters/ConverterCache.ts | 4 +- .../classes/converters/UserConverter.ts | 6 +- .../server/functions/updateGroupDMsName.ts | 11 +- .../app/livechat/server/api/lib/users.ts | 2 +- .../business-hour/AbstractBusinessHour.ts | 3 +- .../livechat/server/business-hour/Helper.ts | 4 +- .../server/lib/contacts/getContacts.ts | 3 +- .../livechat/server/lib/isMessageFromBot.ts | 4 +- .../server/lib/routing/LoadRotation.ts | 2 +- apps/meteor/ee/server/lib/ldap/Manager.ts | 4 + .../server/api/methods/regenerateToken.ts | 13 +- apps/meteor/server/lib/findUsersOfRoom.ts | 6 +- apps/meteor/server/lib/ldap/Manager.ts | 10 +- apps/meteor/server/lib/ldap/UserConverter.ts | 4 +- apps/meteor/server/methods/browseChannels.ts | 10 +- apps/meteor/server/methods/getUsersOfRoom.ts | 3 +- .../server/methods/removeUserFromRoom.ts | 5 + .../server/services/authorization/service.ts | 6 +- .../rocket-chat/adapters/User.ts | 4 +- .../services/omnichannel-voip/service.ts | 8 +- .../src/OmnichannelTranscript.ts | 5 +- packages/core-typings/src/IUser.ts | 4 + packages/core-typings/src/utils.ts | 2 + .../model-typings/src/models/IBaseModel.ts | 2 +- .../model-typings/src/models/IUsersModel.ts | 278 ++--- packages/models/src/models/BaseRaw.ts | 2 +- .../models/src/models/{Users.js => Users.ts} | 1052 +++++++++-------- packages/models/tsconfig.json | 4 +- 37 files changed, 823 insertions(+), 686 deletions(-) rename packages/models/src/models/{Users.js => Users.ts} (62%) diff --git a/apps/meteor/app/api/server/helpers/addUserToFileObj.ts b/apps/meteor/app/api/server/helpers/addUserToFileObj.ts index 4c46192b53224..1a9471e6d550d 100644 --- a/apps/meteor/app/api/server/helpers/addUserToFileObj.ts +++ b/apps/meteor/app/api/server/helpers/addUserToFileObj.ts @@ -1,8 +1,10 @@ import type { IUpload, IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; +const isString = (value: unknown): value is string => typeof value === 'string'; + export async function addUserToFileObj(files: IUpload[]): Promise<(IUpload & { user?: Pick })[]> { - const uids = files.map(({ userId }) => userId).filter(Boolean); + const uids = files.map(({ userId }) => userId).filter(isString); const users = await Users.findByIds(uids, { projection: { name: 1, username: 1 } }).toArray(); diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index aa99d92166678..c95338d8a4ae3 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -2,7 +2,7 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { Mongo } from 'meteor/mongo'; -import type { Filter, RootFilterOperators } from 'mongodb'; +import type { Filter, FindOptions, RootFilterOperators } from 'mongodb'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; @@ -21,7 +21,7 @@ export async function findUsersToAutocomplete({ const searchFields = settings.get('Accounts_SearchFields').trim().split(','); const exceptions = selector.exceptions || []; const conditions = selector.conditions || {}; - const options = { + const options: FindOptions & { limit: number } = { projection: { name: 1, username: 1, diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 24d0bbac7d033..7824ac0c2070e 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1,5 +1,5 @@ import { Team, Room } from '@rocket.chat/core-services'; -import { TEAM_TYPE, type IRoom, type ISubscription, type IUser, type RoomType } from '@rocket.chat/core-typings'; +import { TEAM_TYPE, type IRoom, type ISubscription, type IUser, type RoomType, type UserStatus } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { isChannelsAddAllProps, @@ -1013,7 +1013,7 @@ API.v1.addRoute( }, ]; - const { cursor, totalCount } = await Rooms.findPaginated(ourQuery, { + const { cursor, totalCount } = Rooms.findPaginated(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, @@ -1052,7 +1052,7 @@ API.v1.addRoute( }); } - const { cursor, totalCount } = await Rooms.findPaginatedByTypeAndIds('c', rids, { + const { cursor, totalCount } = Rooms.findPaginatedByTypeAndIds('c', rids, { sort: sort || { name: 1 }, skip: offset, limit: count, @@ -1103,7 +1103,7 @@ API.v1.addRoute( const { cursor, totalCount } = await findUsersOfRoom({ rid: findResult._id, - ...(status && { status: { $in: status } }), + ...(status && { status: { $in: status as UserStatus[] } }), skip, limit, filter, diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index d2dcc6f34f466..ea2b3ef840ba5 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -1,5 +1,5 @@ import { Team, isMeteorError } from '@rocket.chat/core-services'; -import type { IIntegration, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; +import type { IIntegration, IUser, IRoom, RoomType, UserStatus } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { isGroupsOnlineProps, isGroupsMessagesProps } from '@rocket.chat/rest-typings'; import { check, Match } from 'meteor/check'; @@ -740,7 +740,7 @@ API.v1.addRoute( const { cursor, totalCount } = await findUsersOfRoom({ rid: findResult.rid, - ...(status && { status: { $in: status } }), + ...(status && { status: { $in: status as UserStatus[] } }), skip, limit, filter, diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index f345bad4118c2..5f196263e1dfe 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -1,7 +1,7 @@ /** * Docs: https://github.com/RocketChat/developer-docs/blob/master/reference/api/rest-api/endpoints/team-collaboration-endpoints/im-endpoints */ -import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Uploads, Messages, Rooms, Users } from '@rocket.chat/models'; import { isDmDeleteProps, @@ -13,6 +13,7 @@ import { } from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import type { FindOptions } from 'mongodb'; import { eraseRoom } from '../../../../server/lib/eraseRoom'; import { openRoom } from '../../../../server/lib/openRoom'; @@ -338,7 +339,7 @@ API.v1.addRoute( room._id, ); - const options = { + const options: FindOptions = { projection: { _id: 1, username: 1, diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index d3c05a5589e22..c645535ea40cd 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -233,9 +233,7 @@ API.v1.addRoute( }); } - let user = await (async (): Promise< - Pick | undefined | null - > => { + let user = await (async (): Promise | undefined | null> => { if (isUserFromParams(this.bodyParams, this.userId, this.user)) { return Users.findOneById(this.userId); } @@ -269,9 +267,11 @@ API.v1.addRoute( const sentTheUserByFormData = fields.userId || fields.username; if (sentTheUserByFormData) { if (fields.userId) { - user = await Users.findOneById(fields.userId, { projection: { username: 1 } }); + user = await Users.findOneById>(fields.userId, { projection: { username: 1 } }); } else if (fields.username) { - user = await Users.findOneByUsernameIgnoringCase(fields.username, { projection: { username: 1 } }); + user = await Users.findOneByUsernameIgnoringCase>(fields.username, { + projection: { username: 1 }, + }); } if (!user) { diff --git a/apps/meteor/app/api/server/v1/voip/rooms.ts b/apps/meteor/app/api/server/v1/voip/rooms.ts index 2e4c7cea7c1b0..13d2e2c31a054 100644 --- a/apps/meteor/app/api/server/v1/voip/rooms.ts +++ b/apps/meteor/app/api/server/v1/voip/rooms.ts @@ -1,5 +1,5 @@ import { LivechatVoip } from '@rocket.chat/core-services'; -import type { ILivechatAgent, IVoipRoom } from '@rocket.chat/core-typings'; +import type { IVoipRoom } from '@rocket.chat/core-typings'; import { VoipRoom, LivechatVisitors, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { isVoipRoomProps, isVoipRoomsProps, isVoipRoomCloseProps } from '@rocket.chat/rest-typings'; @@ -128,7 +128,7 @@ API.v1.addRoute( return API.v1.failure('agent-not-found'); } - const agentObj: ILivechatAgent = await Users.findOneAgentById(agentId, { + const agentObj = await Users.findOneAgentById(agentId, { projection: { username: 1 }, }); if (!agentObj?.username) { diff --git a/apps/meteor/app/apps/server/bridges/activation.ts b/apps/meteor/app/apps/server/bridges/activation.ts index dc5a0f57e0035..399dfd285e65e 100644 --- a/apps/meteor/app/apps/server/bridges/activation.ts +++ b/apps/meteor/app/apps/server/bridges/activation.ts @@ -1,6 +1,7 @@ import type { IAppServerOrchestrator, AppStatus } from '@rocket.chat/apps'; import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; import { AppActivationBridge as ActivationBridge } from '@rocket.chat/apps-engine/server/bridges/AppActivationBridge'; +import { UserStatus } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; export class AppActivationBridge extends ActivationBridge { @@ -29,7 +30,7 @@ export class AppActivationBridge extends ActivationBridge { } protected async appStatusChanged(app: ProxiedApp, status: AppStatus): Promise { - const userStatus = ['auto_enabled', 'manually_enabled'].includes(status) ? 'online' : 'offline'; + const userStatus = ['auto_enabled', 'manually_enabled'].includes(status) ? UserStatus.ONLINE : UserStatus.OFFLINE; await Users.updateStatusByAppId(app.getID(), userStatus); diff --git a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts index 45a00886af1e1..2477d1547ca25 100644 --- a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts +++ b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts @@ -11,6 +11,10 @@ declare module '@rocket.chat/ddp-client' { } } +const isKeysResult = (result: any): result is { public_key: string; private_key: string } => { + return result.private_key && result.public_key; +}; + Meteor.methods({ async 'e2e.setUserPublicAndPrivateKeys'(keyPair) { const userId = Meteor.userId(); @@ -24,7 +28,7 @@ Meteor.methods({ if (!keyPair.force) { const keys = await Users.fetchKeysByUserId(userId); - if (keys.private_key && keys.public_key) { + if (isKeysResult(keys)) { throw new Meteor.Error('error-keys-already-set', 'Keys already set', { method: 'e2e.setUserPublicAndPrivateKeys', }); diff --git a/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts b/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts index 284e51dddcd57..3dec0ab78d624 100644 --- a/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts +++ b/apps/meteor/app/importer/server/classes/converters/ConverterCache.ts @@ -215,7 +215,9 @@ export class ConverterCache { } const user = await Users.findOneByUsername>(username, { projection: { _id: 1 } }); - this.addUsernameToId(username, user?._id); + if (user) { + this.addUsernameToId(username, user._id); + } return user?._id; } diff --git a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts index 1b411100e13c1..88ea742de7bc7 100644 --- a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts @@ -127,7 +127,7 @@ export class UserConverter extends RecordConverter { + async findExistingUser(data: IImportUser): Promise { if (data.emails.length) { const emailUser = await Users.findOneByEmailAddress(data.emails[0], {}); @@ -138,7 +138,7 @@ export class UserConverter extends RecordConverter(data.username, {}); } } @@ -221,7 +221,7 @@ export class UserConverter extends RecordConverter { + async insertOrUpdateUser(existingUser: IUser | null | undefined, data: IImportUser): Promise { if (!data.username && !existingUser?.username) { const emails = data.emails.filter(Boolean).map((email) => ({ address: email })); data.username = await generateUsernameSuggestion({ diff --git a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts index 0f83cfe1c0880..6e0eb970b9bc5 100644 --- a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts +++ b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts @@ -1,4 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { isNotUndefined } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Users } from '@rocket.chat/models'; import type { ClientSession } from 'mongodb'; @@ -13,8 +14,8 @@ async function getUsersWhoAreInTheSameGroupDMsAs(user: IUser) { return; } - const userIds = new Set(); - const users = new Map(); + const userIds = new Set(); + const users = new Map(); const rooms = Rooms.findGroupDMsByUids([user._id], { projection: { uids: 1 } }); await rooms.forEach((room) => { @@ -25,9 +26,7 @@ async function getUsersWhoAreInTheSameGroupDMsAs(user: IUser) { room.uids.forEach((uid) => uid !== user._id && userIds.add(uid)); }); - (await Users.findByIds([...userIds], { projection: { username: 1, name: 1 } }).toArray()).forEach((user: IUser) => - users.set(user._id, user), - ); + (await Users.findByIds([...userIds], { projection: { username: 1, name: 1 } }).toArray()).forEach((user) => users.set(user._id, user)); return users; } @@ -59,7 +58,7 @@ export const updateGroupDMsName = async ( const rooms = Rooms.findGroupDMsByUids([userThatChangedName._id], { projection: { uids: 1 }, session }); // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const getMembers = (uids: string[]) => uids.map((uid) => users.get(uid)).filter(Boolean); + const getMembers = (uids: string[]) => uids.map((uid) => users.get(uid)).filter(isNotUndefined); // loop rooms to update the subscriptions from them all for await (const room of rooms) { diff --git a/apps/meteor/app/livechat/server/api/lib/users.ts b/apps/meteor/app/livechat/server/api/lib/users.ts index 49ac5682a6c04..62663603768fe 100644 --- a/apps/meteor/app/livechat/server/api/lib/users.ts +++ b/apps/meteor/app/livechat/server/api/lib/users.ts @@ -51,7 +51,7 @@ async function findUsers({ sortedResults, totalCount: [{ total } = { total: 0 }], }, - ] = await Users.findAgentsWithDepartments(role, query, { + ] = await Users.findAgentsWithDepartments(role, query, { sort: sort || { name: 1 }, skip: offset, limit: count, diff --git a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts index 940954dfa2a79..1a61cca494935 100644 --- a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts @@ -1,4 +1,5 @@ import type { AtLeast, ILivechatAgentStatus, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; import type { ILivechatBusinessHoursModel, IUsersModel } from '@rocket.chat/model-typings'; import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import type { IWorkHoursCronJobsWrapper } from '@rocket.chat/models'; @@ -55,7 +56,7 @@ export abstract class AbstractBusinessHourBehavior { status, // Why this works: statusDefault is the property set when a user manually changes their status // So if it's set to offline, we can be sure the user will be offline after login and we can skip the update - { livechatStatusSystemModified: true, statusDefault: { $ne: 'offline' } }, + { livechatStatusSystemModified: true, statusDefault: { $ne: UserStatus.OFFLINE } }, { livechatStatusSystemModified: true }, ); diff --git a/apps/meteor/app/livechat/server/business-hour/Helper.ts b/apps/meteor/app/livechat/server/business-hour/Helper.ts index d21b51ce0184d..e5b16865d8f91 100644 --- a/apps/meteor/app/livechat/server/business-hour/Helper.ts +++ b/apps/meteor/app/livechat/server/business-hour/Helper.ts @@ -49,7 +49,7 @@ export const createDefaultBusinessHourIfNotExists = async (): Promise => { } }; -export async function makeAgentsUnavailableBasedOnBusinessHour(agentIds: string[] | null = null) { +export async function makeAgentsUnavailableBasedOnBusinessHour(agentIds?: string[]) { const results = await Users.findAgentsAvailableWithoutBusinessHours(agentIds).toArray(); const update = await Users.updateLivechatStatusByAgentIds( @@ -75,7 +75,7 @@ export async function makeAgentsUnavailableBasedOnBusinessHour(agentIds: string[ ); } -export async function makeOnlineAgentsAvailable(agentIds: string[] | null = null) { +export async function makeOnlineAgentsAvailable(agentIds?: string[]) { const results = await Users.findOnlineButNotAvailableAgents(agentIds).toArray(); const update = await Users.updateLivechatStatusByAgentIds( diff --git a/apps/meteor/app/livechat/server/lib/contacts/getContacts.ts b/apps/meteor/app/livechat/server/lib/contacts/getContacts.ts index 09b7a2545a1cc..ad9a4cbadf174 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/getContacts.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/getContacts.ts @@ -1,4 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { isNotUndefined } from '@rocket.chat/core-typings'; import { LivechatContacts, Users } from '@rocket.chat/models'; import type { PaginatedResult, ILivechatContactWithManagerData } from '@rocket.chat/rest-typings'; import type { FindCursor, Sort } from 'mongodb'; @@ -25,7 +26,7 @@ export async function getContacts(params: GetContactsParams): Promise contactManager))]; + const managerIds = [...new Set(rawContacts.map(({ contactManager }) => contactManager))].filter(isNotUndefined); const managersCursor: FindCursor<[string, Pick]> = Users.findByIds(managerIds, { projection: { name: 1, username: 1 }, }).map((manager) => [manager._id, manager]); diff --git a/apps/meteor/app/livechat/server/lib/isMessageFromBot.ts b/apps/meteor/app/livechat/server/lib/isMessageFromBot.ts index 8c53bf7c555d0..e394366cba7ad 100644 --- a/apps/meteor/app/livechat/server/lib/isMessageFromBot.ts +++ b/apps/meteor/app/livechat/server/lib/isMessageFromBot.ts @@ -1,6 +1,6 @@ -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; -export async function isMessageFromBot(message: IMessage): Promise { +export async function isMessageFromBot(message: IMessage): Promise | null> { return Users.isUserInRole(message.u._id, 'bot'); } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts index b520a1b2a2a35..7c915c65b8812 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts @@ -33,7 +33,7 @@ class LoadRotation { ignoreAgentId, settings.get('Livechat_enabled_when_agent_idle'), ); - if (!nextAgent) { + if (!nextAgent?.username) { return; } diff --git a/apps/meteor/ee/server/lib/ldap/Manager.ts b/apps/meteor/ee/server/lib/ldap/Manager.ts index 61e0ba990082b..f74e5e43ab415 100644 --- a/apps/meteor/ee/server/lib/ldap/Manager.ts +++ b/apps/meteor/ee/server/lib/ldap/Manager.ts @@ -291,6 +291,10 @@ export class LDAPEEManager extends LDAPManager { const roomOwner = settings.get('LDAP_Sync_User_Data_Channels_Admin') || ''; const user = await Users.findOneByUsernameIgnoringCase(roomOwner); + if (!user) { + logger.error(`Unable to find user '${roomOwner}' to be the owner of the channel '${channel}'.`); + return; + } const room = await createRoom('c', channel, user, [], false, false, { customFields: { ldap: true }, diff --git a/apps/meteor/imports/personal-access-tokens/server/api/methods/regenerateToken.ts b/apps/meteor/imports/personal-access-tokens/server/api/methods/regenerateToken.ts index 0ee4cf018f38e..281ed55dc013c 100644 --- a/apps/meteor/imports/personal-access-tokens/server/api/methods/regenerateToken.ts +++ b/apps/meteor/imports/personal-access-tokens/server/api/methods/regenerateToken.ts @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Users } from '@rocket.chat/models'; +import { isPersonalAccessToken } from '@rocket.chat/core-typings'; import { hasPermissionAsync } from '../../../../../app/authorization/server/functions/hasPermission'; import { twoFactorRequired } from '../../../../../app/2fa/server/twoFactorRequired'; @@ -33,8 +34,14 @@ export const regeneratePersonalAccessTokenOfUser = async (tokenName: string, use await removePersonalAccessTokenOfUser(tokenName, userId); - return generatePersonalAccessTokenOfUser({ tokenName, userId, bypassTwoFactor: tokenExist.bypassTwoFactor || false }); -} + const tokenObject = tokenExist.services?.resume?.loginTokens?.find((token) => isPersonalAccessToken(token) && token.name === tokenName); + + return generatePersonalAccessTokenOfUser({ + tokenName, + userId, + bypassTwoFactor: (tokenObject && isPersonalAccessToken(tokenObject) && tokenObject.bypassTwoFactor) || false, + }); +}; Meteor.methods({ 'personalAccessTokens:regenerateToken': twoFactorRequired(async function ({ tokenName }) { @@ -44,7 +51,7 @@ Meteor.methods({ method: 'personalAccessTokens:regenerateToken', }); } - + return regeneratePersonalAccessTokenOfUser(tokenName, uid); }), }); diff --git a/apps/meteor/server/lib/findUsersOfRoom.ts b/apps/meteor/server/lib/findUsersOfRoom.ts index 6441d5265d114..25c0a34f0198a 100644 --- a/apps/meteor/server/lib/findUsersOfRoom.ts +++ b/apps/meteor/server/lib/findUsersOfRoom.ts @@ -1,13 +1,13 @@ import type { IUser } from '@rocket.chat/core-typings'; import type { FindPaginated } from '@rocket.chat/model-typings'; import { Users } from '@rocket.chat/models'; -import type { FilterOperators, FindCursor } from 'mongodb'; +import type { FindCursor, FindOptions, Filter } from 'mongodb'; import { settings } from '../../app/settings/server'; type FindUsersParam = { rid: string; - status?: FilterOperators; + status?: Filter['status']; skip?: number; limit?: number; filter?: string; @@ -15,7 +15,7 @@ type FindUsersParam = { }; export function findUsersOfRoom({ rid, status, skip = 0, limit = 0, filter = '', sort }: FindUsersParam): FindPaginated> { - const options = { + const options: FindOptions = { projection: { name: 1, username: 1, diff --git a/apps/meteor/server/lib/ldap/Manager.ts b/apps/meteor/server/lib/ldap/Manager.ts index a0f474cfe5d89..4f34acc61de1c 100644 --- a/apps/meteor/server/lib/ldap/Manager.ts +++ b/apps/meteor/server/lib/ldap/Manager.ts @@ -140,12 +140,12 @@ export class LDAPManager { } // This method will only find existing users that are already linked to LDAP - protected static async findExistingLDAPUser(ldapUser: ILDAPEntry): Promise { + protected static async findExistingLDAPUser(ldapUser: ILDAPEntry): Promise { const uniqueIdentifierField = this.getLdapUserUniqueID(ldapUser); if (uniqueIdentifierField) { logger.debug({ msg: 'Querying user', uniqueId: uniqueIdentifierField.value }); - return UsersRaw.findOneByLDAPId(uniqueIdentifierField.value, uniqueIdentifierField.attribute); + return UsersRaw.findOneByLDAPId(uniqueIdentifierField.value, uniqueIdentifierField.attribute); } } @@ -341,7 +341,7 @@ export class LDAPManager { ldapUser: ILDAPEntry, existingUser?: IUser, usedUsername?: string | undefined, - ): Promise { + ): Promise { logger.debug({ msg: 'Syncing user data', ldapUser: omit(ldapUser, '_raw'), @@ -501,14 +501,14 @@ export class LDAPManager { } // This method will find existing users by LDAP id or by username. - private static async findExistingUser(ldapUser: ILDAPEntry, slugifiedUsername: string): Promise { + private static async findExistingUser(ldapUser: ILDAPEntry, slugifiedUsername: string): Promise { const user = await this.findExistingLDAPUser(ldapUser); if (user) { return user; } // If we don't have that ldap user linked yet, check if there's any non-ldap user with the same username - return UsersRaw.findOneWithoutLDAPByUsernameIgnoringCase(slugifiedUsername); + return UsersRaw.findOneWithoutLDAPByUsernameIgnoringCase(slugifiedUsername); } private static fallbackToDefaultLogin(username: LoginUsername, password: string): LDAPLoginResult { diff --git a/apps/meteor/server/lib/ldap/UserConverter.ts b/apps/meteor/server/lib/ldap/UserConverter.ts index 1d94db88db3c7..4f76c61cefe62 100644 --- a/apps/meteor/server/lib/ldap/UserConverter.ts +++ b/apps/meteor/server/lib/ldap/UserConverter.ts @@ -20,7 +20,7 @@ export class LDAPUserConverter extends UserConverter { this.mergeExistingUsers = settings.get('LDAP_Merge_Existing_Users') ?? true; } - async findExistingUser(data: IImportUser): Promise { + async findExistingUser(data: IImportUser): Promise { if (data.services?.ldap?.id) { const importedUser = await Users.findOneByLDAPId(data.services.ldap.id, data.services.ldap.idAttribute); if (importedUser) { @@ -41,7 +41,7 @@ export class LDAPUserConverter extends UserConverter { } if (data.username) { - return Users.findOneWithoutLDAPByUsernameIgnoringCase(data.username); + return Users.findOneWithoutLDAPByUsernameIgnoringCase(data.username); } } diff --git a/apps/meteor/server/methods/browseChannels.ts b/apps/meteor/server/methods/browseChannels.ts index 5956fa4eaa8c3..bba6c6a173c22 100644 --- a/apps/meteor/server/methods/browseChannels.ts +++ b/apps/meteor/server/methods/browseChannels.ts @@ -6,6 +6,7 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import mem from 'mem'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { Meteor } from 'meteor/meteor'; +import type { FindOptions, SortDirection } from 'mongodb'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { federationSearchUsers } from '../../app/federation/server/handler'; @@ -32,7 +33,7 @@ const sortChannels = (field: string, direction: 'asc' | 'desc'): Record { +const sortUsers = (field: string, direction: 'asc' | 'desc'): Record => { switch (field) { case 'email': return { @@ -183,7 +184,7 @@ const findUsers = async ({ viewFullOtherUserInfo, }: { text: string; - sort: Record; + sort: Record; pagination: { skip: number; limit: number; @@ -194,7 +195,7 @@ const findUsers = async ({ const searchFields = workspace === 'all' ? ['username', 'name', 'emails.address'] : settings.get('Accounts_SearchFields').trim().split(','); - const options = { + const options: FindOptions = { ...pagination, sort, projection: { @@ -264,7 +265,7 @@ const getUsers = async ( user: IUser | undefined, text: string, workspace: string, - sort: Record, + sort: Record, pagination: { skip: number; limit: number; @@ -289,6 +290,7 @@ const getUsers = async ( // Add the federated user to the results results.unshift({ + _id: user._id, username: user.username, name: user.name, bio: user.bio, diff --git a/apps/meteor/server/methods/getUsersOfRoom.ts b/apps/meteor/server/methods/getUsersOfRoom.ts index d3d69ce937542..915cfad4fe01f 100644 --- a/apps/meteor/server/methods/getUsersOfRoom.ts +++ b/apps/meteor/server/methods/getUsersOfRoom.ts @@ -1,4 +1,5 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Subscriptions, Rooms } from '@rocket.chat/models'; import { check } from 'meteor/check'; @@ -54,7 +55,7 @@ Meteor.methods({ const { cursor } = findUsersOfRoom({ rid, - status: !showAll ? { $ne: 'offline' } : undefined, + status: !showAll ? { $ne: UserStatus.OFFLINE } : undefined, limit, skip, filter, diff --git a/apps/meteor/server/methods/removeUserFromRoom.ts b/apps/meteor/server/methods/removeUserFromRoom.ts index 8d1cb2e2ba2be..28e900667204d 100644 --- a/apps/meteor/server/methods/removeUserFromRoom.ts +++ b/apps/meteor/server/methods/removeUserFromRoom.ts @@ -55,6 +55,11 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri } const removedUser = await Users.findOneByUsernameIgnoringCase(data.username); + if (!removedUser) { + throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', { + method: 'removeUserFromRoom', + }); + } await Room.beforeUserRemoved(room); diff --git a/apps/meteor/server/services/authorization/service.ts b/apps/meteor/server/services/authorization/service.ts index d970068e23ba4..af9801c1da08e 100644 --- a/apps/meteor/server/services/authorization/service.ts +++ b/apps/meteor/server/services/authorization/service.ts @@ -126,7 +126,7 @@ export class Authorization extends ServiceClass implements IAuthorization { private getUserFromRoles = mem( async (roleIds: string[]) => { - const options = { + const users = await Users.findUsersInRoles(roleIds, null, { sort: { username: 1, }, @@ -134,9 +134,7 @@ export class Authorization extends ServiceClass implements IAuthorization { username: 1, roles: 1, }, - }; - - const users = await Users.findUsersInRoles(roleIds, null, options).toArray(); + }).toArray(); return users.map((user) => ({ ...user, diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/User.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/User.ts index 73bbdcadc77bf..14f938c91e4c3 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/User.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/adapters/User.ts @@ -93,8 +93,8 @@ export class RocketChatUserAdapter { return user; } - public async getInternalUserByUsername(username: string): Promise { - return Users.findOneByUsername(username); + public async getInternalUserByUsername(username: string): Promise { + return Users.findOneByUsername(username); } public async createFederatedUser(federatedUser: FederatedUser): Promise { diff --git a/apps/meteor/server/services/omnichannel-voip/service.ts b/apps/meteor/server/services/omnichannel-voip/service.ts index 9c30202045cf6..d6d199ca47b38 100644 --- a/apps/meteor/server/services/omnichannel-voip/service.ts +++ b/apps/meteor/server/services/omnichannel-voip/service.ts @@ -15,7 +15,7 @@ import { isILivechatVisitor, OmnichannelSourceType, isVoipRoom, VoipClientEvents import { Logger } from '@rocket.chat/logger'; import { Users, VoipRoom, PbxEvents } from '@rocket.chat/models'; import type { PaginatedResult } from '@rocket.chat/rest-typings'; -import type { FindOptions } from 'mongodb'; +import type { FindOptions, SortDirection } from 'mongodb'; import _ from 'underscore'; import type { IOmniRoomClosingMessage } from './internalTypes'; @@ -183,7 +183,7 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn private async getAllocatedExtesionAllocationData(projection: Partial<{ [P in keyof IUser]: number }>): Promise { const roles: string[] = ['livechat-agent', 'livechat-manager', 'admin']; - const options = { + const options: FindOptions = { sort: { username: 1, }, @@ -458,9 +458,9 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn text?: string, count?: number, offset?: number, - sort?: Record, + sort?: Record, ): Promise<{ agents: ILivechatAgent[]; total: number }> { - const { cursor, totalCount } = Users.getAvailableAgentsIncludingExt(includeExtension, text, { count, skip: offset, sort }); + const { cursor, totalCount } = Users.getAvailableAgentsIncludingExt(includeExtension, text, { limit: count, skip: offset, sort }); const [agents, total] = await Promise.all([cursor.toArray(), totalCount]); diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index 8f4d0e44b5544..20288a4427ca3 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -17,6 +17,7 @@ import type { ILivechatVisitor, ILivechatAgent, IOmnichannelSystemMessage, + AtLeast, } from '@rocket.chat/core-typings'; import { isQuoteAttachment, isFileAttachment, isFileImageAttachment } from '@rocket.chat/core-typings'; import type { Logger } from '@rocket.chat/logger'; @@ -64,7 +65,7 @@ export type MessageData = Pick< type WorkerData = { siteName: string; visitor: Pick | null; - agent: ILivechatAgent | undefined; + agent: ILivechatAgent | undefined | null; closedAt?: Date; messages: MessageData[]; timezone: string; @@ -101,7 +102,7 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT this.log = new loggerClass('OmnichannelTranscript'); } - async getTimezone(user?: { utcOffset?: string | number }): Promise { + async getTimezone(user?: AtLeast | null): Promise { const reportingTimezone = await settingsService.get('Default_Timezone_For_Reporting'); switch (reportingTimezone) { diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index c1ba49b556e13..510d6d4ceed22 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -21,6 +21,9 @@ export interface IPersonalAccessToken extends ILoginToken { bypassTwoFactor?: boolean; } +export const isPersonalAccessToken = (token: LoginToken): token is IPersonalAccessToken => + 'type' in token && token.type === 'personalAccessToken'; + export interface IUserEmailVerificationToken { token: string; address: string; @@ -218,6 +221,7 @@ export interface IUser extends IRocketChatRecord { requirePasswordChangeReason?: string; roomRolePriorities?: Record; isOAuthUser?: boolean; // client only field + __rooms?: string[]; } export interface IRegisterUser extends IUser { diff --git a/packages/core-typings/src/utils.ts b/packages/core-typings/src/utils.ts index 2e20ebc48c842..3e8f4869ce3d2 100644 --- a/packages/core-typings/src/utils.ts +++ b/packages/core-typings/src/utils.ts @@ -48,3 +48,5 @@ export type DeepPartial = { ? DeepPartial : T[P]; }; + +export const isNotUndefined = (value: T | undefined): value is T => value !== undefined; diff --git a/packages/model-typings/src/models/IBaseModel.ts b/packages/model-typings/src/models/IBaseModel.ts index c3a541a1d1a75..e293dfa52d750 100644 --- a/packages/model-typings/src/models/IBaseModel.ts +++ b/packages/model-typings/src/models/IBaseModel.ts @@ -25,7 +25,7 @@ import type { import type { Updater } from '../updater'; -export type DefaultFields = Record | Record | void; +export type DefaultFields = Partial> | Partial> | void; export type ResultFields = Defaults extends void ? Base : Defaults[keyof Defaults] extends 1 diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 75f4cdf67e8bf..73bcbb5c02812 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -1,13 +1,13 @@ import type { IUser, IRole, - IRoom, ILivechatAgent, UserStatus, ILoginToken, IPersonalAccessToken, AtLeast, ILivechatAgentStatus, + IMeteorLoginToken, } from '@rocket.chat/core-typings'; import type { Document, @@ -19,131 +19,136 @@ import type { DeleteResult, WithId, UpdateOptions, - ClientSession, + UpdateFilter, } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; export interface IUsersModel extends IBaseModel { addRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][]): Promise; - findUsersInRoles(roles: IRole['_id'][], scope?: null, options?: any): FindCursor; - findPaginatedUsersInRoles(roles: IRole['_id'][], options?: any): FindPaginated>; + findUsersInRoles(roles: IRole['_id'][] | IRole['_id'], _scope?: null, options?: FindOptions): FindCursor; + findPaginatedUsersInRoles(roles: IRole['_id'][] | IRole['_id'], options?: FindOptions): FindPaginated>; findOneByIdWithEmailAddress(uid: IUser['_id'], options?: FindOptions): Promise; - findOneByUsername(username: string, options?: any): Promise; - findOneAgentById(_id: string, options: any): Promise; - findUsersInRolesWithQuery(roles: IRole['_id'] | IRole['_id'][], query: any, options: any): FindCursor; - findPaginatedUsersInRolesWithQuery( - roles: IRole['_id'] | IRole['_id'][], - query: any, - options: any, - ): FindPaginated>; - findOneByUsernameAndRoomIgnoringCase(username: string, rid: IRoom['_id'], options: any): FindCursor; - findOneByIdAndLoginHashedToken(_id: string, token: any, options?: any): FindCursor; - findByActiveUsersExcept( - searchTerm: any, - exceptions: any, - options: any, - searchFields: any, - extraQuery?: any, - params?: { startsWith?: boolean; endsWith?: boolean }, - ): FindCursor; - findPaginatedByActiveUsersExcept( - searchTerm: any, - exceptions: any, - options: any, - searchFields: any, - extraQuery?: any, - params?: { startsWith?: boolean; endsWith?: boolean }, - ): FindPaginated>; - - findPaginatedByActiveLocalUsersExcept( - searchTerm: any, - exceptions: any, - options: any, - forcedSearchFields: any, - localDomain: any, - ): FindPaginated>; + findOneByUsername(username: string, options?: FindOptions): Promise; + findOneAgentById(_id: IUser['_id'], options?: FindOptions): Promise; + findUsersInRolesWithQuery(roles: IRole['_id'][] | IRole['_id'], query: Filter, options?: FindOptions): FindCursor; + findPaginatedUsersInRolesWithQuery( + roles: IRole['_id'][] | IRole['_id'], + query: Filter, + options?: FindOptions, + ): FindPaginated>>; + findOneByUsernameAndRoomIgnoringCase(username: string | RegExp, rid: string, options?: FindOptions): Promise; + findOneByIdAndLoginHashedToken(_id: IUser['_id'], token: string, options?: FindOptions): Promise; + findByActiveUsersExcept( + searchTerm: string, + exceptions: string[], + options?: FindOptions, + searchFields?: string[], + extraQuery?: Filter[], + extra?: { startsWith: boolean; endsWith: boolean }, + ): FindCursor; + findPaginatedByActiveUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + searchFields?: string[], + extraQuery?: Filter[], + extra?: { startsWith?: boolean; endsWith?: boolean }, + ): FindPaginated>>; + + findPaginatedByActiveLocalUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + forcedSearchFields?: string[], + localDomain?: string, + ): FindPaginated>>; - findPaginatedByActiveExternalUsersExcept( - searchTerm: any, - exceptions: any, - options: any, - forcedSearchFields: any, - localDomain: any, - ): FindPaginated>; + findPaginatedByActiveExternalUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + forcedSearchFields?: string[], + localDomain?: string, + ): FindPaginated>>; - findActive(options?: any): FindCursor; + findActive(query: Filter, options?: FindOptions): FindCursor; - findActiveByIds(userIds: any, options?: any): FindCursor; + findActiveByIds(userIds: IUser['_id'][], options?: FindOptions): FindCursor; - findByIds(userIds: any, options?: any): FindCursor; + findByIds(userIds: IUser['_id'][], options?: FindOptions): FindCursor; - findOneByUsernameIgnoringCase(username: any, options?: any): Promise; + findOneByUsernameIgnoringCase(username: IUser['username'], options?: FindOptions): Promise; - findOneWithoutLDAPByUsernameIgnoringCase(username: string, options?: any): Promise; + findOneWithoutLDAPByUsernameIgnoringCase(username: string, options?: FindOptions): Promise; - findOneByLDAPId(id: any, attribute?: any): Promise; + findOneByLDAPId(id: string, attribute?: string): Promise; - findOneByAppId(appId: string, options?: FindOptions): Promise; + findOneByAppId(appId: string, options?: FindOptions): Promise; - findLDAPUsers(options?: any): FindCursor; + findLDAPUsers(options?: FindOptions): FindCursor; - findLDAPUsersExceptIds(userIds: IUser['_id'][], options?: FindOptions): FindCursor; + findLDAPUsersExceptIds(userIds: IUser['_id'][], options?: FindOptions): FindCursor; - findConnectedLDAPUsers(options?: any): FindCursor; + findConnectedLDAPUsers(options?: FindOptions): FindCursor; - isUserInRole(userId: IUser['_id'], roleId: IRole['_id']): Promise; + isUserInRole(userId: IUser['_id'], roleId: IRole['_id']): Promise | null>; - getDistinctFederationDomains(): any; + getDistinctFederationDomains(): Promise; getNextLeastBusyAgent( - department: any, - ignoreAgentId: any, + department?: string, + ignoreAgentId?: string, isEnabledWhenAgentIdle?: boolean, - ): Promise<{ agentId: string; username: string; lastRoutingTime: Date; departments: any[]; count: number }>; + ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; count: number; departments?: any[] }>; getLastAvailableAgentRouted( - department: any, - ignoreAgentId: any, + department?: string, + ignoreAgentId?: string, isEnabledWhenAgentIdle?: boolean, - ): Promise<{ agentId: string; username: string; lastRoutingTime: Date; departments: any[] }>; + ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; departments?: any[] }>; - setLastRoutingTime(userId: any): Promise; + setLastRoutingTime(userId: IUser['_id']): Promise | null>; - setLivechatStatusIf(userId: string, status: ILivechatAgentStatus, conditions?: any, extraFields?: any): Promise; + setLivechatStatusIf( + userId: IUser['_id'], + status: ILivechatAgentStatus, + conditions?: Filter, + extraFields?: UpdateFilter['$set'], + ): Promise; getAgentAndAmountOngoingChats( - userId: any, - ): Promise<{ agentId: string; username: string; lastAssignTime: Date; lastRoutingTime: Date; queueInfo: { chats: number } }>; + userId: IUser['_id'], + ): Promise<{ agentId: string; username?: string; lastAssignTime?: Date; lastRoutingTime?: Date; queueInfo: { chats: number } }>; - findAllResumeTokensByUserId(userId: any): any; + findAllResumeTokensByUserId(userId: IUser['_id']): Promise<{ tokens: IMeteorLoginToken[] }[]>; - findActiveByUsernameOrNameRegexWithExceptionsAndConditions( - termRegex: any, - exceptions: any, - conditions: any, - options: any, + findActiveByUsernameOrNameRegexWithExceptionsAndConditions( + termRegex: { $regex: string; $options: string } | RegExp, + exceptions?: string[], + conditions?: Filter, + options?: FindOptions, ): FindCursor; - countAllAgentsStatus({ departmentId }: { departmentId?: any }): any; + countAllAgentsStatus({ + departmentId, + }: { + departmentId?: string; + }): Promise<{ offline: number; away: number; busy: number; available: number }[]>; - getTotalOfRegisteredUsersByDate({ start, end, options }: { start: any; end: any; options?: any }): Promise; - // TODO change back to below when convert model to TS - // Promise< - // { - // date: string; - // users: number; - // type: 'users'; - // }[] - // >; + getTotalOfRegisteredUsersByDate(params: { + start: Date; + end: Date; + options?: { count?: number; sort?: Record }; + }): Promise<{ date: string; users: number; type: 'users' }[]>; - getUserLanguages(): any; + getUserLanguages(): Promise<{ _id: string; total: number }[]>; - updateStatusText(_id: any, statusText: any, options?: { session?: ClientSession }): any; + updateStatusText(_id: IUser['_id'], statusText: string, options?: UpdateOptions): Promise; - updateStatusByAppId(appId: any, status: any): any; + updateStatusByAppId(appId: string, status: UserStatus): Promise; - openAgentsBusinessHoursByBusinessHourId(businessHourIds: any): any; + openAgentsBusinessHoursByBusinessHourId(businessHourIds: string[]): Promise; - openAgentBusinessHoursByBusinessHourIdsAndAgentId(businessHourIds: string[], agentId: string): Promise; + openAgentBusinessHoursByBusinessHourIdsAndAgentId(businessHourIds: string[], agentId: IUser['_id']): Promise; addBusinessHourByAgentIds(agentIds: string[], businessHourId: string): any; @@ -189,7 +194,11 @@ export interface IUsersModel extends IBaseModel { unsetExtension(userId: any): any; - getAvailableAgentsIncludingExt(includeExt: any, text: any, options: any): FindPaginated>; + getAvailableAgentsIncludingExt( + includeExt?: string, + text?: string, + options?: FindOptions, + ): FindPaginated>>; findActiveUsersTOTPEnable(options: any): any; @@ -199,7 +208,7 @@ export interface IUsersModel extends IBaseModel { countActiveUsersEmail2faEnable(options: any): Promise; - findActiveByIdsOrUsernames(userIds: string[], options?: any): FindCursor; + findActiveByIdsOrUsernames(userIds: IUser['_id'][], options?: FindOptions): FindCursor; setAsFederated(userId: string): any; @@ -208,26 +217,29 @@ export interface IUsersModel extends IBaseModel { findOneByResetToken(token: string, options: FindOptions): Promise; updateStatusById( - userId: string, + userId: IUser['_id'], { statusDefault, status, statusConnection, statusText, - }: { statusDefault?: string; status: UserStatus; statusConnection: UserStatus; statusText?: string }, + }: { statusDefault?: UserStatus; status: UserStatus; statusConnection: UserStatus; statusText?: string }, ): Promise; - setFederationAvatarUrlById(userId: string, federationAvatarUrl: string): Promise; + setFederationAvatarUrlById(userId: IUser['_id'], federationAvatarUrl: string): Promise; - findSearchedServerNamesByUserId(userId: string): Promise; + findSearchedServerNamesByUserId(userId: IUser['_id']): Promise; addServerNameToSearchedServerNamesList(userId: string, serverName: string): Promise; removeServerNameFromSearchedServerNamesList(userId: string, serverName: string): Promise; countFederatedExternalUsers(): Promise; - findOnlineUserFromList(userList: string[], isLivechatEnabledWhenAgentIdle?: boolean): FindCursor; - countOnlineUserFromList(userList: string[], isLivechatEnabledWhenAgentIdle?: boolean): Promise; + findOnlineUserFromList( + userList: string | string[], + isLivechatEnabledWhenAgentIdle?: boolean, + ): FindCursor; + countOnlineUserFromList(userList: string | string[], isLivechatEnabledWhenAgentIdle?: boolean): Promise; getUnavailableAgents( departmentId?: string, extraQuery?: Document, @@ -247,28 +259,29 @@ export interface IUsersModel extends IBaseModel { isLivechatEnabledWhenAgentIdle?: boolean, ): Promise; - findBotAgents(usernameList?: string[]): FindCursor; - countBotAgents(usernameList?: string[]): Promise; + findBotAgents(usernameList?: string | string[]): FindCursor; + countBotAgents(usernameList?: string | string[]): Promise; removeAllRoomsByUserId(userId: string): Promise; removeRoomByUserId(userId: string, rid: string): Promise; addRoomByUserId(userId: string, rid: string): Promise; - addRoomByUserIds(uids: string[], rid: string): Promise; + addRoomByUserIds(uids: string[], rid: string): Promise; removeRoomByRoomIds(rids: string[]): Promise; addRoomRolePriorityByUserId(userId: string, rid: string, rolePriority: number): Promise; removeRoomRolePriorityByUserId(userId: string, rid: string): Promise; assignRoomRolePrioritiesByUserIdPriorityMap(rolePrioritiesMap: Record, rid: string): Promise; - unassignRoomRolePrioritiesByRoomId(rid: string): Promise; + unassignRoomRolePrioritiesByRoomId(rid: string): Promise; getLoginTokensByUserId(userId: string): FindCursor; addPersonalAccessTokenToUser(data: { userId: string; loginTokenObject: IPersonalAccessToken }): Promise; removePersonalAccessTokenOfUser(data: { userId: string; loginTokenObject: AtLeast; }): Promise; - findPersonalAccessTokenByTokenNameAndUserId(data: { userId: string; tokenName: string }): Promise; + findPersonalAccessTokenByTokenNameAndUserId({ userId, tokenName }: { userId: IUser['_id']; tokenName: string }): Promise; setOperator(userId: string, operator: boolean): Promise; checkOnlineAgents(agentId?: string, isLivechatEnabledWhenIdle?: boolean): Promise; - findOnlineAgents(agentId?: string, isLivechatEnabledWhenIdle?: boolean): FindCursor; - findOneBotAgent(): Promise; + findOnlineAgents(agentId?: IUser['_id'], isLivechatEnabledWhenIdle?: boolean): FindCursor; + countOnlineAgents(agentId: string): Promise; + findOneBotAgent(): Promise; findOneOnlineAgentById( agentId: string, isLivechatEnabledWhenAgentIdle?: boolean, @@ -280,21 +293,21 @@ export interface IUsersModel extends IBaseModel { ignoreAgentId?: string, extraQuery?: Filter, enabledWhenAgentIdle?: boolean, - ): Promise<{ agentId: string; username: string } | null>; - getNextBotAgent(ignoreAgentId?: string): Promise<{ agentId: string; username: string } | null>; + ): Promise<{ agentId: string; username?: string } | null>; + getNextBotAgent(ignoreAgentId?: string): Promise<{ agentId: string; username?: string } | null>; setLivechatStatus(userId: string, status: ILivechatAgentStatus): Promise; makeAgentUnavailableAndUnsetExtension(userId: string): Promise; setLivechatData(userId: string, data?: Record): Promise; closeOffice(): Promise; openOffice(): Promise; getAgentInfo( - agentId: string, + agentId: IUser['_id'], showAgentEmail?: boolean, - ): Promise | null>; + ): Promise | null>; roleBaseQuery(userId: string): { _id: string }; setE2EPublicAndPrivateKeysByUserId(userId: string, e2e: { public_key: string; private_key: string }): Promise; rocketMailUnsubscribe(userId: string, createdAt: string): Promise; - fetchKeysByUserId(userId: string): Promise<{ public_key: string; private_key: string } | Record>; + fetchKeysByUserId(userId: string): Promise<{ public_key: string; private_key: string } | object>; disable2FAAndSetTempSecretByUserId(userId: string, tempSecret: string): Promise; enable2FAAndSetSecretAndCodesByUserId(userId: string, secret: string, codes: string[]): Promise; disable2FAByUserId(userId: string): Promise; @@ -304,8 +317,6 @@ export interface IUsersModel extends IBaseModel { findByIdsWithPublicE2EKey(userIds: string[], options?: FindOptions): FindCursor; resetE2EKey(userId: string): Promise; removeExpiredEmailCodeOfUserId(userId: string): Promise; - removeEmailCodeByUserId(userId: string): Promise; - increaseInvalidEmailCodeAttempt(userId: string): Promise; maxInvalidEmailCodeAttemptsReached(userId: string, maxAttemtps: number): Promise; addEmailCodeByUserId(userId: string, code: string, expire: Date): Promise; findActiveUsersInRoles(roles: string[], options?: FindOptions): FindCursor; @@ -328,12 +339,12 @@ export interface IUsersModel extends IBaseModel { findOneByIdAndLoginToken(userId: string, loginToken: string, options?: FindOptions): Promise; findOneActiveById(userId: string, options?: FindOptions): Promise; findOneByIdOrUsername(userId: string, options?: FindOptions): Promise; - findOneByRolesAndType(roles: string[], type: string, options?: FindOptions): Promise; + findOneByRolesAndType(roles: IRole['_id'][], type: string, options?: FindOptions): Promise; findNotOfflineByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersNotOffline(options?: FindOptions): FindCursor; countUsersNotOffline(options?: FindOptions): Promise; findNotIdUpdatedFrom(userId: string, updatedFrom: Date, options?: FindOptions): FindCursor; - findByRoomId(roomId: string, options?: FindOptions): FindCursor; + findByRoomId(roomId: string, options?: FindOptions): Promise>; findByUsername(username: string, options?: FindOptions): FindCursor; findByUsernames(usernames: string[], options?: FindOptions): FindCursor; findByUsernamesIgnoringCase(usernames: string[], options?: FindOptions): FindCursor; @@ -344,28 +355,27 @@ export interface IUsersModel extends IBaseModel { findByUsernameNameOrEmailAddress(nameOrUsernameOrEmail: string, options?: FindOptions): FindCursor; findCrowdUsers(options?: FindOptions): FindCursor; getLastLogin(options?: FindOptions): Promise; - findUsersByUsernames(usernames: string[], options?: FindOptions): FindCursor; + findUsersByUsernames(usernames: string[], options?: FindOptions): FindCursor; findUsersByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersWithUsernameByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersWithUsernameByIdsNotOffline(userIds: string[], options?: FindOptions): FindCursor; getOldest(options?: FindOptions): Promise; - findActiveRemoteUsers(options?: FindOptions): FindCursor; findActiveFederated(options?: FindOptions): FindCursor; getSAMLByIdAndSAMLProvider(userId: string, samlProvider: string): Promise; - findBySAMLNameIdOrIdpSession(samlNameId: string, idpSession: string): FindCursor; - findBySAMLInResponseTo(inResponseTo: string): FindCursor; + findBySAMLNameIdOrIdpSession(samlNameId: string, idpSession: string, options?: FindOptions): FindCursor; + findBySAMLInResponseTo(inResponseTo: string, options?: FindOptions): FindCursor; addImportIds(userId: string, importIds: string | string[]): Promise; updateInviteToken(userId: string, token: string): Promise; updateLastLoginById(userId: string): Promise; addPasswordToHistory(userId: string, password: string, passwordHistoryAmount: number): Promise; setServiceId(userId: string, serviceName: string, serviceId: string): Promise; - setUsername(userId: string, username: string, options?: { session?: ClientSession }): Promise; - setEmail(userId: string, email: string, verified?: boolean, options?: { session?: ClientSession }): Promise; + setUsername(userId: string, username: string, options?: UpdateOptions): Promise; + setEmail(userId: string, email: string, verified?: boolean, options?: UpdateOptions): Promise; setEmailVerified(userId: string, email: string): Promise; - setName(userId: string, name: string, options?: { session?: ClientSession }): Promise; - unsetName(userId: string, options?: { session?: ClientSession }): Promise; + setName(userId: string, name: string, options?: UpdateOptions): Promise; + unsetName(userId: string, options?: UpdateOptions): Promise; setCustomFields(userId: string, customFields: Record): Promise; - setAvatarData(userId: string, origin: string, etag?: Date | null | string, options?: { session?: ClientSession }): Promise; + setAvatarData(userId: string, origin: string, etag?: Date | null | string, options?: UpdateOptions): Promise; unsetAvatarData(userId: string): Promise; setUserActive(userId: string, active: boolean): Promise; setAllUsersActive(active: boolean): Promise; @@ -384,7 +394,6 @@ export interface IUsersModel extends IBaseModel { setPreferences(userId: string, preferences: Record): Promise; setTwoFactorAuthorizationHashAndUntilForUserIdAndToken(userId: string, token: string, hash: string, until: Date): Promise; setUtcOffset(userId: string, utcOffset: number): Promise; - saveUserById(userId: string, user: Partial): Promise; setReason(userId: string, reason: string): Promise; unsetReason(userId: string): Promise; bannerExistsById(userId: string, bannerId: string): Promise; @@ -405,24 +414,27 @@ export interface IUsersModel extends IBaseModel { updateCustomFieldsById(userId: string, customFields: Record): Promise; countRoomMembers(roomId: string): Promise; countRemote(options?: FindOptions): Promise; - findOneByImportId(importId: string, options?: FindOptions): Promise; + findOneByImportId(_id: IUser['_id'], options?: FindOptions): Promise; removeAgent(_id: string): Promise; - findAgentsWithDepartments( - role: string, + findAgentsWithDepartments( + role: IRole['_id'][] | IRole['_id'], query: Filter, - options: FindOptions, + options?: FindOptions, ): Promise<{ sortedResults: (T & { departments: string[] })[]; totalCount: { total: number }[] }[]>; countByRole(roleName: string): Promise; removeEmailCodeOfUserId(userId: string): Promise; incrementInvalidEmailCodeAttempt(userId: string): Promise | null>; - findOnlineButNotAvailableAgents(userIds: string[] | null): FindCursor>; - findAgentsAvailableWithoutBusinessHours(userIds: string[] | null): FindCursor>; - updateLivechatStatusByAgentIds(userIds: string[], status: ILivechatAgentStatus): Promise; - findOneByFreeSwitchExtension(extension: string, options?: FindOptions): Promise; - findOneByFreeSwitchExtensions(extensions: string[], options?: FindOptions): Promise; + findOnlineButNotAvailableAgents(userIds?: IUser['_id'][]): FindCursor; + findAgentsAvailableWithoutBusinessHours(userIds?: IUser['_id'][]): FindCursor>; + updateLivechatStatusByAgentIds(userIds: string[], status: ILivechatAgentStatus): Promise; + findOneByFreeSwitchExtension(freeSwitchExtension: string, options?: FindOptions): Promise; + findOneByFreeSwitchExtensions( + freeSwitchExtensions: string[], + options?: FindOptions, + ): Promise; setFreeSwitchExtension(userId: string, extension: string | undefined): Promise; - findAssignedFreeSwitchExtensions(): FindCursor; - findUsersWithAssignedFreeSwitchExtensions(options?: FindOptions): FindCursor; + findAssignedFreeSwitchExtensions(): FindCursor; + findUsersWithAssignedFreeSwitchExtensions(options?: FindOptions): FindCursor; countUsersInRoles(roles: IRole['_id'][]): Promise; countAllUsersWithPendingAvatar(): Promise; } diff --git a/packages/models/src/models/BaseRaw.ts b/packages/models/src/models/BaseRaw.ts index 047c889ed1611..0c28c17913e3a 100644 --- a/packages/models/src/models/BaseRaw.ts +++ b/packages/models/src/models/BaseRaw.ts @@ -53,7 +53,7 @@ export abstract class BaseRaw< TDeleted extends RocketChatRecordDeleted = RocketChatRecordDeleted, > implements IBaseModel { - public readonly defaultFields: C | undefined; + protected defaultFields: C | undefined; public readonly col: Collection; diff --git a/packages/models/src/models/Users.js b/packages/models/src/models/Users.ts similarity index 62% rename from packages/models/src/models/Users.js rename to packages/models/src/models/Users.ts index 108963273ee7f..9f9cb54886e32 100644 --- a/packages/models/src/models/Users.js +++ b/packages/models/src/models/Users.ts @@ -1,10 +1,37 @@ -import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import { Subscriptions } from '@rocket.chat/models'; +import type { + AtLeast, + DeepWritable, + ILivechatAgent, + ILoginToken, + IMeteorLoginToken, + IPersonalAccessToken, + IRole, + IRoom, + IUser, + RocketChatRecordDeleted, +} from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus, UserStatus } from '@rocket.chat/core-typings'; +import type { DefaultFields, InsertionModel, IUsersModel } from '@rocket.chat/model-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; - +import type { + Collection, + Db, + Filter, + FindOptions, + IndexDescription, + Document, + UpdateFilter, + UpdateOptions, + FindCursor, + SortDirection, + UpdateResult, + FindOneAndUpdateOptions, +} from 'mongodb'; + +import { Subscriptions } from '../index'; import { BaseRaw } from './BaseRaw'; -const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdle) => ({ +const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdle?: boolean): Filter => ({ statusLivechat: 'available', roles: 'livechat-agent', // ignore deactivated users @@ -14,7 +41,7 @@ const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdl { status: { $exists: true, - $ne: 'offline', + $ne: UserStatus.OFFLINE, }, roles: { $ne: 'bot', @@ -31,8 +58,8 @@ const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdl }), }); -export class UsersRaw extends BaseRaw { - constructor(db, trash) { +export class UsersRaw extends BaseRaw> implements IUsersModel { + constructor(db: Db, trash?: Collection>) { super(db, 'users', trash, { collectionNameResolver(name) { return name; @@ -45,19 +72,19 @@ export class UsersRaw extends BaseRaw { } // Move index from constructor to here - modelIndexes() { + modelIndexes(): IndexDescription[] { return [ - { key: { __rooms: 1 }, sparse: 1 }, - { key: { roles: 1 }, sparse: 1 }, + { key: { __rooms: 1 }, sparse: true }, + { key: { roles: 1 }, sparse: true }, { key: { name: 1 } }, - { key: { bio: 1 }, sparse: 1 }, - { key: { nickname: 1 }, sparse: 1 }, + { key: { bio: 1 }, sparse: true }, + { key: { nickname: 1 }, sparse: true }, { key: { createdAt: 1 } }, { key: { lastLogin: 1 } }, { key: { status: 1 } }, { key: { statusText: 1 } }, - { key: { statusConnection: 1 }, sparse: 1 }, - { key: { appId: 1 }, sparse: 1 }, + { key: { statusConnection: 1 }, sparse: true }, + { key: { appId: 1 }, sparse: true }, { key: { type: 1 } }, { key: { federated: 1 }, sparse: true }, { key: { federation: 1 }, sparse: true }, @@ -106,7 +133,7 @@ export class UsersRaw extends BaseRaw { * @param {string} uid * @param {IRole['_id'][]} roles list of role ids */ - addRolesByUserId(uid, roles) { + addRolesByUserId(uid: string, roles: string | string[]) { if (!Array.isArray(roles)) { roles = [roles]; process.env.NODE_ENV === 'development' && console.warn('[WARN] Users.addRolesByUserId: roles should be an array'); @@ -129,8 +156,8 @@ export class UsersRaw extends BaseRaw { * @param {null} scope the value for the role scope (room id) - not used in the users collection * @param {any} options */ - findUsersInRoles(roles, scope, options) { - roles = [].concat(roles); + findUsersInRoles(roles: IRole['_id'][] | IRole['_id'], _scope?: null, options?: FindOptions) { + roles = ([] as string[]).concat(roles); const query = { roles: { $in: roles }, @@ -139,8 +166,8 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countUsersInRoles(roles) { - roles = [].concat(roles); + countUsersInRoles(roles: IRole['_id'][] | IRole['_id']) { + roles = ([] as string[]).concat(roles); const query = { roles: { $in: roles }, @@ -149,8 +176,8 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query); } - findPaginatedUsersInRoles(roles, options) { - roles = [].concat(roles); + findPaginatedUsersInRoles(roles: IRole['_id'][] | IRole['_id'], options?: FindOptions) { + roles = ([] as string[]).concat(roles); const query = { roles: { $in: roles }, @@ -159,19 +186,19 @@ export class UsersRaw extends BaseRaw { return this.findPaginated(query, options); } - findOneByUsername(username, options = null) { + findOneByUsername(username: string, options?: FindOptions) { const query = { username }; - return this.findOne(query, options); + return this.findOne(query, options); } - findOneAgentById(_id, options) { + findOneAgentById(_id: IUser['_id'], options?: FindOptions) { const query = { _id, roles: 'livechat-agent', }; - return this.findOne(query, options); + return this.findOne(query, options); } /** @@ -179,8 +206,8 @@ export class UsersRaw extends BaseRaw { * @param {any} query * @param {any} options */ - findUsersInRolesWithQuery(roles, query, options) { - roles = [].concat(roles); + findUsersInRolesWithQuery(roles: IRole['_id'][] | IRole['_id'], query: Filter, options?: FindOptions) { + roles = ([] as string[]).concat(roles); Object.assign(query, { roles: { $in: roles } }); @@ -192,16 +219,24 @@ export class UsersRaw extends BaseRaw { * @param {any} query * @param {any} options */ - findPaginatedUsersInRolesWithQuery(roles, query, options) { - roles = [].concat(roles); + findPaginatedUsersInRolesWithQuery( + roles: IRole['_id'][] | IRole['_id'], + query: Filter, + options?: FindOptions, + ) { + roles = ([] as string[]).concat(roles); Object.assign(query, { roles: { $in: roles } }); - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findAgentsWithDepartments(role, query, options) { - const roles = [].concat(role); + findAgentsWithDepartments( + role: IRole['_id'][] | IRole['_id'], + query: Filter, + options?: FindOptions, + ): Promise<{ sortedResults: (T & { departments: string[] })[]; totalCount: { total: number }[] }[]> { + const roles = ([] as string[]).concat(role); Object.assign(query, { roles: { $in: roles } }); @@ -237,16 +272,16 @@ export class UsersRaw extends BaseRaw { }, { $facet: { - sortedResults: [{ $sort: options.sort }, { $skip: options.skip }, options.limit && { $limit: options.limit }], + sortedResults: [{ $sort: options?.sort }, { $skip: options?.skip }, options?.limit && { $limit: options.limit }], totalCount: [{ $group: { _id: null, total: { $sum: 1 } } }], }, }, ]; - return this.col.aggregate(aggregate).toArray(); + return this.col.aggregate<{ sortedResults: (T & { departments: string[] })[]; totalCount: { total: number }[] }>(aggregate).toArray(); } - findOneByUsernameAndRoomIgnoringCase(username, rid, options) { + findOneByUsernameAndRoomIgnoringCase(username: string | RegExp, rid: string, options?: FindOptions) { if (typeof username === 'string') { username = new RegExp(`^${escapeRegExp(username)}$`, 'i'); } @@ -259,7 +294,7 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneByIdAndLoginHashedToken(_id, token, options = {}) { + findOneByIdAndLoginHashedToken(_id: IUser['_id'], token: string, options: FindOptions = {}) { const query = { _id, 'services.resume.loginTokens.hashedToken': token, @@ -268,7 +303,14 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findByActiveUsersExcept(searchTerm, exceptions, options, searchFields, extraQuery = [], { startsWith = false, endsWith = false } = {}) { + findByActiveUsersExcept( + searchTerm: string, + exceptions: string[], + options?: FindOptions, + searchFields?: string[], + extraQuery: Filter[] = [], + { startsWith = false, endsWith = false } = {}, + ) { if (exceptions == null) { exceptions = []; } @@ -281,10 +323,13 @@ export class UsersRaw extends BaseRaw { const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i'); - const orStmt = (searchFields || []).reduce((acc, el) => { - acc.push({ [el.trim()]: termRegex }); - return acc; - }, []); + const orStmt = (searchFields || []).reduce( + (acc, el) => { + acc.push({ [el.trim()]: termRegex }); + return acc; + }, + [] as Record[], + ); const query = { $and: [ @@ -304,12 +349,12 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findPaginatedByActiveUsersExcept( - searchTerm, - exceptions, - options, - searchFields, - extraQuery = [], + findPaginatedByActiveUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + searchFields: string[] = [], + extraQuery: Filter[] = [], { startsWith = false, endsWith = false } = {}, ) { if (exceptions == null) { @@ -324,10 +369,13 @@ export class UsersRaw extends BaseRaw { const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i'); - const orStmt = (searchFields || []).reduce((acc, el) => { - acc.push({ [el.trim()]: termRegex }); - return acc; - }, []); + const orStmt = (searchFields || []).reduce( + (acc, el) => { + acc.push({ [el.trim()]: termRegex }); + return acc; + }, + [] as Record[], + ); const query = { $and: [ @@ -344,30 +392,42 @@ export class UsersRaw extends BaseRaw { ], }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findPaginatedByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { + findPaginatedByActiveLocalUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + forcedSearchFields?: string[], + localDomain?: string, + ) { const extraQuery = [ { $or: [{ federation: { $exists: false } }, { 'federation.origin': localDomain }], }, ]; - return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); + return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); } - findPaginatedByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { + findPaginatedByActiveExternalUsersExcept( + searchTerm: string, + exceptions?: string[], + options?: FindOptions, + forcedSearchFields?: string[], + localDomain?: string, + ) { const extraQuery = [{ federation: { $exists: true } }, { 'federation.origin': { $ne: localDomain } }]; - return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); + return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); } - findActive(query, options = {}) { + findActive(query: Filter, options: FindOptions = {}) { Object.assign(query, { active: true }); return this.find(query, options); } - findActiveByIds(userIds, options = {}) { + findActiveByIds(userIds: IUser['_id'][], options: FindOptions = {}) { const query = { _id: { $in: userIds }, active: true, @@ -376,7 +436,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findActiveByIdsOrUsernames(userIds, options = {}) { + findActiveByIdsOrUsernames(userIds: IUser['_id'][], options: FindOptions = {}) { const query = { $or: [{ _id: { $in: userIds } }, { username: { $in: userIds } }], active: true, @@ -385,32 +445,32 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findByIds(userIds, options = {}) { + findByIds(userIds: IUser['_id'][], options: FindOptions = {}) { const query = { _id: { $in: userIds }, }; - return this.find(query, options); + return this.find(query, options); } - findOneByImportId(_id, options) { - return this.findOne({ importIds: _id }, options); + findOneByImportId(_id: IUser['_id'], options?: FindOptions) { + return this.findOne({ importIds: _id }, options); } - findOneByUsernameIgnoringCase(username, options) { + findOneByUsernameIgnoringCase(username: IUser['username'], options?: FindOptions) { if (!username) { throw new Error('invalid username'); } const query = { username }; - return this.findOne(query, { + return this.findOne(query, { collation: { locale: 'en', strength: 2 }, // Case insensitive ...options, }); } - findOneWithoutLDAPByUsernameIgnoringCase(username, options) { + findOneWithoutLDAPByUsernameIgnoringCase(username: string, options?: FindOptions) { const expression = new RegExp(`^${escapeRegExp(username)}$`, 'i'); const query = { @@ -420,34 +480,31 @@ export class UsersRaw extends BaseRaw { }, }; - return this.findOne(query, options); + return this.findOne(query, options); } - async findOneByLDAPId(id, attribute = undefined) { + async findOneByLDAPId(id: string, attribute?: string) { const query = { 'services.ldap.id': id, + ...(attribute && { 'services.ldap.idAttribute': attribute }), }; - if (attribute) { - query['services.ldap.idAttribute'] = attribute; - } - - return this.findOne(query); + return this.findOne(query); } - async findOneByAppId(appId, options) { + async findOneByAppId(appId: string, options?: FindOptions) { const query = { appId }; - return this.findOne(query, options); + return this.findOne(query, options); } - findLDAPUsers(options) { + findLDAPUsers(options?: FindOptions) { const query = { ldap: true }; - return this.find(query, options); + return this.find(query, options); } - findLDAPUsersExceptIds(userIds, options = {}) { + findLDAPUsersExceptIds(userIds: IUser['_id'][], options: FindOptions = {}) { const query = { ldap: true, _id: { @@ -455,10 +512,10 @@ export class UsersRaw extends BaseRaw { }, }; - return this.find(query, options); + return this.find(query, options); } - findConnectedLDAPUsers(options) { + findConnectedLDAPUsers(options?: FindOptions) { const query = { 'ldap': true, 'services.resume.loginTokens': { @@ -467,25 +524,29 @@ export class UsersRaw extends BaseRaw { }, }; - return this.find(query, options); + return this.find(query, options); } - isUserInRole(userId, roleId) { + isUserInRole(userId: IUser['_id'], roleId: IRole['_id']) { const query = { _id: userId, roles: roleId, }; - return this.findOne(query, { projection: { roles: 1 } }); + return this.findOne>(query, { projection: { roles: 1 } }); } getDistinctFederationDomains() { return this.col.distinct('federation.origin', { federation: { $exists: true } }); } - async getNextLeastBusyAgent(department, ignoreAgentId, isEnabledWhenAgentIdle) { + async getNextLeastBusyAgent( + department?: string, + ignoreAgentId?: string, + isEnabledWhenAgentIdle?: boolean, + ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; count: number; departments?: any[] }> { const match = queryStatusAgentOnline({ ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }) }, isEnabledWhenAgentIdle); - const aggregate = [ + const aggregate: Document[] = [ { $match: match }, { $lookup: { @@ -535,7 +596,9 @@ export class UsersRaw extends BaseRaw { aggregate.push({ $limit: 1 }); - const [agent] = await this.col.aggregate(aggregate).toArray(); + const [agent] = await this.col + .aggregate<{ agentId: string; username?: string; lastRoutingTime?: Date; count: number; departments?: any[] }>(aggregate) + .toArray(); if (agent) { await this.setLastRoutingTime(agent.agentId); } @@ -543,9 +606,13 @@ export class UsersRaw extends BaseRaw { return agent; } - async getLastAvailableAgentRouted(department, ignoreAgentId, isEnabledWhenAgentIdle) { + async getLastAvailableAgentRouted( + department?: string, + ignoreAgentId?: string, + isEnabledWhenAgentIdle?: boolean, + ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; departments?: any[] }> { const match = queryStatusAgentOnline({ ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }) }, isEnabledWhenAgentIdle); - const aggregate = [ + const aggregate: Document[] = [ { $match: match }, { $lookup: { @@ -566,7 +633,9 @@ export class UsersRaw extends BaseRaw { aggregate.push({ $limit: 1 }); - const [agent] = await this.col.aggregate(aggregate).toArray(); + const [agent] = await this.col + .aggregate<{ agentId: string; username?: string; lastRoutingTime?: Date; departments?: any[] }>(aggregate) + .toArray(); if (agent) { await this.setLastRoutingTime(agent.agentId); } @@ -574,7 +643,7 @@ export class UsersRaw extends BaseRaw { return agent; } - async setLastRoutingTime(userId) { + async setLastRoutingTime(userId: IUser['_id']) { const result = await this.findOneAndUpdate( { _id: userId }, { @@ -584,12 +653,17 @@ export class UsersRaw extends BaseRaw { }, { returnDocument: 'after' }, ); - return result.value; + return result; } - setLivechatStatusIf(userId, status, conditions = {}, extraFields = {}) { + setLivechatStatusIf( + userId: IUser['_id'], + status: ILivechatAgentStatus, + conditions: Filter = {}, + extraFields: UpdateFilter['$set'] = {}, + ) { // TODO: Create class Agent - const query = { + const query: Filter = { _id: userId, ...conditions, }; @@ -604,7 +678,13 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - async getAgentAndAmountOngoingChats(userId) { + async getAgentAndAmountOngoingChats(userId: IUser['_id']): Promise<{ + agentId: string; + username?: string; + lastAssignTime?: Date; + lastRoutingTime?: Date; + queueInfo: { chats: number }; + }> { const aggregate = [ { $match: { @@ -643,13 +723,21 @@ export class UsersRaw extends BaseRaw { { $sort: { 'queueInfo.chats': 1, 'lastAssignTime': 1, 'lastRoutingTime': 1, 'username': 1 } }, ]; - const [agent] = await this.col.aggregate(aggregate).toArray(); + const [agent] = await this.col + .aggregate<{ + agentId: string; + username?: string; + lastAssignTime?: Date; + lastRoutingTime?: Date; + queueInfo: { chats: number }; + }>(aggregate) + .toArray(); return agent; } - findAllResumeTokensByUserId(userId) { + findAllResumeTokensByUserId(userId: IUser['_id']): Promise<{ tokens: IMeteorLoginToken[] }[]> { return this.col - .aggregate([ + .aggregate<{ tokens: IMeteorLoginToken[] }>([ { $match: { _id: userId, @@ -675,7 +763,12 @@ export class UsersRaw extends BaseRaw { .toArray(); } - findActiveByUsernameOrNameRegexWithExceptionsAndConditions(termRegex, exceptions, conditions, options) { + findActiveByUsernameOrNameRegexWithExceptionsAndConditions( + termRegex: { $regex: string; $options: string } | RegExp, + exceptions?: string[], + conditions?: Filter, + options?: FindOptions, + ) { if (exceptions == null) { exceptions = []; } @@ -722,16 +815,20 @@ export class UsersRaw extends BaseRaw { ], }; - return this.find(query, options); + return this.find(query, options); } - countAllAgentsStatus({ departmentId = undefined }) { - const match = { + countAllAgentsStatus({ + departmentId, + }: { + departmentId?: string; + }): Promise<{ offline: number; away: number; busy: number; available: number }[]> { + const match: Document = { $match: { roles: { $in: ['livechat-agent'] }, }, }; - const group = { + const group: Document = { $group: { _id: null, offline: { @@ -785,7 +882,7 @@ export class UsersRaw extends BaseRaw { }, }, }; - const lookup = { + const lookup: Document = { $lookup: { from: 'rocketchat_livechat_department_agents', localField: '_id', @@ -793,13 +890,13 @@ export class UsersRaw extends BaseRaw { as: 'departments', }, }; - const unwind = { + const unwind: Document = { $unwind: { path: '$departments', preserveNullAndEmptyArrays: true, }, }; - const departmentsMatch = { + const departmentsMatch: Document = { $match: { 'departments.departmentId': departmentId, }, @@ -811,11 +908,19 @@ export class UsersRaw extends BaseRaw { params.push(departmentsMatch); } params.push(group); - return this.col.aggregate(params).toArray(); + return this.col.aggregate<{ offline: number; away: number; busy: number; available: number }>(params).toArray(); } - getTotalOfRegisteredUsersByDate({ start, end, options = {} }) { - const params = [ + getTotalOfRegisteredUsersByDate({ + start, + end, + options = {}, + }: { + start: Date; + end: Date; + options?: { count?: number; sort?: Record }; + }): Promise<{ date: string; users: number; type: 'users' }[]> { + const params: Document[] = [ { $match: { createdAt: { $gte: start, $lte: end }, @@ -851,10 +956,10 @@ export class UsersRaw extends BaseRaw { if (options.count) { params.push({ $limit: options.count }); } - return this.col.aggregate(params).toArray(); + return this.col.aggregate<{ date: string; users: number; type: 'users' }>(params).toArray(); } - getUserLanguages() { + getUserLanguages(): Promise<{ _id: string; total: number }[]> { const pipeline = [ { $match: { @@ -872,18 +977,10 @@ export class UsersRaw extends BaseRaw { }, ]; - return this.col.aggregate(pipeline).toArray(); + return this.col.aggregate<{ _id: string; total: number }>(pipeline).toArray(); } - /** - * - * @param {string} _id - * @param {string} statusText - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - updateStatusText(_id, statusText, options) { + updateStatusText(_id: IUser['_id'], statusText: string, options?: UpdateOptions) { const update = { $set: { statusText, @@ -893,7 +990,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update, { session: options?.session }); } - updateStatusByAppId(appId, status) { + updateStatusByAppId(appId: string, status: UserStatus) { const query = { appId, status: { $ne: status }, @@ -916,7 +1013,15 @@ export class UsersRaw extends BaseRaw { * @param {string} [status.statusDefault] * @param {string} [status.statusText] */ - updateStatusById(userId, { statusDefault, status, statusConnection, statusText }) { + updateStatusById( + userId: IUser['_id'], + { + statusDefault, + status, + statusConnection, + statusText, + }: { statusDefault?: UserStatus; status: UserStatus; statusConnection: UserStatus; statusText?: string }, + ) { const query = { _id: userId, }; @@ -937,7 +1042,7 @@ export class UsersRaw extends BaseRaw { return this.col.updateOne(query, update); } - openAgentsBusinessHoursByBusinessHourId(businessHourIds) { + openAgentsBusinessHoursByBusinessHourId(businessHourIds: string[]) { const query = { roles: 'livechat-agent', }; @@ -951,7 +1056,7 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - openAgentBusinessHoursByBusinessHourIdsAndAgentId(businessHourIds, agentId) { + openAgentBusinessHoursByBusinessHourIdsAndAgentId(businessHourIds: string[], agentId: IUser['_id']) { const query = { _id: agentId, roles: 'livechat-agent', @@ -966,7 +1071,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - addBusinessHourByAgentIds(agentIds = [], businessHourId) { + addBusinessHourByAgentIds(agentIds: IUser['_id'][] = [], businessHourId: string) { const query = { _id: { $in: agentIds }, roles: 'livechat-agent', @@ -981,20 +1086,20 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - findOnlineButNotAvailableAgents(userIds) { + findOnlineButNotAvailableAgents(userIds?: IUser['_id'][]) { const query = { ...(userIds && { _id: { $in: userIds } }), roles: 'livechat-agent', // Exclude away users - status: 'online', + status: UserStatus.ONLINE, // Exclude users that are already available, maybe due to other business hour - statusLivechat: 'not-available', + statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, }; - return this.find(query); + return this.find(query); } - removeBusinessHourByAgentIds(agentIds = [], businessHourId) { + removeBusinessHourByAgentIds(agentIds: IUser['_id'][] = [], businessHourId: string) { const query = { _id: { $in: agentIds }, roles: 'livechat-agent', @@ -1009,7 +1114,7 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - openBusinessHourToAgentsWithoutDepartment(agentIdsWithDepartment = [], businessHourId) { + openBusinessHourToAgentsWithoutDepartment(agentIdsWithDepartment: IUser['_id'][] = [], businessHourId: string) { const query = { _id: { $nin: agentIdsWithDepartment }, }; @@ -1023,7 +1128,7 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - closeBusinessHourToAgentsWithoutDepartment(agentIdsWithDepartment = [], businessHourId) { + closeBusinessHourToAgentsWithoutDepartment(agentIdsWithDepartment: IUser['_id'][] = [], businessHourId: string) { const query = { _id: { $nin: agentIdsWithDepartment }, }; @@ -1037,7 +1142,7 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - closeAgentsBusinessHoursByBusinessHourIds(businessHourIds) { + closeAgentsBusinessHoursByBusinessHourIds(businessHourIds: string[]) { const query = { roles: 'livechat-agent', }; @@ -1051,15 +1156,15 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - findAgentsAvailableWithoutBusinessHours(userIds = []) { - return this.find( + findAgentsAvailableWithoutBusinessHours(userIds: IUser['_id'][] = []) { + return this.find>( { $or: [{ openBusinessHours: { $exists: false } }, { openBusinessHours: { $size: 0 } }], $and: [{ roles: 'livechat-agent' }, { roles: { $ne: 'bot' } }], // exclude deactivated users active: true, // Avoid unnecessary updates - statusLivechat: 'available', + statusLivechat: ILivechatAgentStatus.AVAILABLE, ...(Array.isArray(userIds) && userIds.length > 0 && { _id: { $in: userIds } }), }, { @@ -1068,10 +1173,10 @@ export class UsersRaw extends BaseRaw { ); } - setLivechatStatusActiveBasedOnBusinessHours(userId) { + setLivechatStatusActiveBasedOnBusinessHours(userId: IUser['_id']) { const query = { _id: userId, - statusDefault: { $ne: 'offline' }, + statusDefault: { $ne: UserStatus.OFFLINE }, openBusinessHours: { $exists: true, $not: { $size: 0 }, @@ -1080,14 +1185,14 @@ export class UsersRaw extends BaseRaw { const update = { $set: { - statusLivechat: 'available', + statusLivechat: ILivechatAgentStatus.AVAILABLE, }, }; return this.updateOne(query, update); } - async isAgentWithinBusinessHours(agentId) { + async isAgentWithinBusinessHours(agentId: IUser['_id']) { const query = { _id: agentId, $or: [ @@ -1116,14 +1221,14 @@ export class UsersRaw extends BaseRaw { const update = { $unset: { - openBusinessHours: 1, + openBusinessHours: 1 as const, }, }; return this.updateMany(query, update); } - resetTOTPById(userId) { + resetTOTPById(userId: IUser['_id']) { return this.col.updateOne( { _id: userId, @@ -1136,7 +1241,7 @@ export class UsersRaw extends BaseRaw { ); } - unsetOneLoginToken(_id, token) { + unsetOneLoginToken(_id: IUser['_id'], token: string) { const update = { $pull: { 'services.resume.loginTokens': { hashedToken: token }, @@ -1146,7 +1251,7 @@ export class UsersRaw extends BaseRaw { return this.col.updateOne({ _id }, update); } - unsetLoginTokens(userId) { + unsetLoginTokens(userId: IUser['_id']) { return this.col.updateOne( { _id: userId, @@ -1159,7 +1264,7 @@ export class UsersRaw extends BaseRaw { ); } - removeNonPATLoginTokensExcept(userId, authToken) { + removeNonPATLoginTokensExcept(userId: IUser['_id'], authToken: string) { return this.col.updateOne( { _id: userId, @@ -1175,7 +1280,7 @@ export class UsersRaw extends BaseRaw { ); } - removeRoomsByRoomIdsAndUserId(rids, userId) { + removeRoomsByRoomIdsAndUserId(rids: IRoom['_id'][], userId: IUser['_id']) { return this.updateMany( { _id: userId, @@ -1183,10 +1288,13 @@ export class UsersRaw extends BaseRaw { }, { $pullAll: { __rooms: rids }, - $unset: rids.reduce((acc, rid) => { - acc[`roomRolePriorities.${rid}`] = ''; - return acc; - }, {}), + $unset: rids.reduce( + (acc, rid) => { + acc[`roomRolePriorities.${rid}`] = ''; + return acc; + }, + {} as Record, + ), }, ); } @@ -1195,7 +1303,7 @@ export class UsersRaw extends BaseRaw { * @param {string} uid * @param {IRole['_id']} roles the list of role ids to remove */ - removeRolesByUserId(uid, roles) { + removeRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][]) { const query = { _id: uid, }; @@ -1209,7 +1317,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - async isUserInRoleScope(uid) { + async isUserInRoleScope(uid: IUser['_id']) { const query = { _id: uid, }; @@ -1222,7 +1330,7 @@ export class UsersRaw extends BaseRaw { return !!found; } - addBannerById(_id, banner) { + addBannerById(_id: IUser['_id'], banner: { id: string }) { const query = { _id, [`banners.${banner.id}.read`]: { @@ -1240,13 +1348,13 @@ export class UsersRaw extends BaseRaw { } // Voip functions - findOneByAgentUsername(username, options) { + findOneByAgentUsername(username: string, options?: FindOptions) { const query = { username, roles: 'livechat-agent' }; return this.findOne(query, options); } - findOneByExtension(extension, options) { + findOneByExtension(extension: string, options?: FindOptions) { const query = { extension, }; @@ -1254,7 +1362,7 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findByExtensions(extensions, options) { + findByExtensions(extensions: string[], options?: FindOptions) { const query = { extension: { $in: extensions, @@ -1264,7 +1372,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - getVoipExtensionByUserId(userId, options) { + getVoipExtensionByUserId(userId: IUser['_id'], options?: FindOptions) { const query = { _id: userId, extension: { $exists: true }, @@ -1272,7 +1380,7 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - setExtension(userId, extension) { + setExtension(userId: IUser['_id'], extension: string) { const query = { _id: userId, }; @@ -1285,33 +1393,33 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - unsetExtension(userId) { + unsetExtension(userId: IUser['_id']) { const query = { _id: userId, }; const update = { $unset: { - extension: true, + extension: 1 as const, }, }; return this.updateOne(query, update); } - getAvailableAgentsIncludingExt(includeExt, text, options) { + getAvailableAgentsIncludingExt(includeExt?: string, text?: string, options?: FindOptions) { const query = { roles: { $in: ['livechat-agent'] }, $and: [ - ...(text && text.trim() + ...(text?.trim() ? [{ $or: [{ username: new RegExp(escapeRegExp(text), 'i') }, { name: new RegExp(escapeRegExp(text), 'i') }] }] : []), { $or: [{ extension: { $exists: false } }, ...(includeExt ? [{ extension: includeExt }] : [])] }, ], }; - return this.findPaginated(query, options); + return this.findPaginated(query, options); } - findActiveUsersTOTPEnable(options) { + findActiveUsersTOTPEnable(options?: FindOptions) { const query = { 'active': true, 'services.totp.enabled': true, @@ -1319,7 +1427,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countActiveUsersTOTPEnable(options) { + countActiveUsersTOTPEnable(options?: FindOptions) { const query = { 'active': true, 'services.totp.enabled': true, @@ -1327,7 +1435,7 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query, options); } - findActiveUsersEmail2faEnable(options) { + findActiveUsersEmail2faEnable(options?: FindOptions) { const query = { 'active': true, 'services.email2fa.enabled': true, @@ -1335,7 +1443,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countActiveUsersEmail2faEnable(options) { + countActiveUsersEmail2faEnable(options?: FindOptions) { const query = { 'active': true, 'services.email2fa.enabled': true, @@ -1343,7 +1451,7 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query, options); } - setAsFederated(uid) { + setAsFederated(uid: IUser['_id']) { const query = { _id: uid, }; @@ -1356,7 +1464,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - removeRoomByRoomId(rid, options) { + removeRoomByRoomId(rid: IRoom['_id'], options?: UpdateOptions) { return this.updateMany( { __rooms: rid, @@ -1369,11 +1477,11 @@ export class UsersRaw extends BaseRaw { ); } - findOneByResetToken(token, options) { + findOneByResetToken(token: string, options?: FindOptions) { return this.findOne({ 'services.password.reset.token': token }, options); } - findOneByIdWithEmailAddress(userId, options) { + findOneByIdWithEmailAddress(userId: IUser['_id'], options?: FindOptions) { return this.findOne( { _id: userId, @@ -1383,7 +1491,7 @@ export class UsersRaw extends BaseRaw { ); } - setFederationAvatarUrlById(userId, federationAvatarUrl) { + setFederationAvatarUrlById(userId: IUser['_id'], federationAvatarUrl: string) { return this.updateOne( { _id: userId, @@ -1396,8 +1504,8 @@ export class UsersRaw extends BaseRaw { ); } - async findSearchedServerNamesByUserId(userId) { - const user = await this.findOne( + async findSearchedServerNamesByUserId(userId: IUser['_id']): Promise { + const user = await this.findOne>( { _id: userId, }, @@ -1408,10 +1516,10 @@ export class UsersRaw extends BaseRaw { }, ); - return user.federation?.searchedServerNames || []; + return user?.federation?.searchedServerNames || []; } - addServerNameToSearchedServerNamesList(userId, serverName) { + addServerNameToSearchedServerNamesList(userId: IUser['_id'], serverName: string) { return this.updateOne( { _id: userId, @@ -1424,7 +1532,7 @@ export class UsersRaw extends BaseRaw { ); } - removeServerNameFromSearchedServerNamesList(userId, serverName) { + removeServerNameFromSearchedServerNamesList(userId: IUser['_id'], serverName: string) { return this.updateOne( { _id: userId, @@ -1443,21 +1551,21 @@ export class UsersRaw extends BaseRaw { }); } - findOnlineUserFromList(userList, isLivechatEnabledWhenAgentIdle) { + findOnlineUserFromList(userList: string | string[], isLivechatEnabledWhenAgentIdle?: boolean) { // TODO: Create class Agent const username = { - $in: [].concat(userList), + $in: ([] as string[]).concat(userList), }; const query = queryStatusAgentOnline({ username }, isLivechatEnabledWhenAgentIdle); - return this.find(query); + return this.find(query); } - countOnlineUserFromList(userList, isLivechatEnabledWhenAgentIdle) { + countOnlineUserFromList(userList: string | string[], isLivechatEnabledWhenAgentIdle?: boolean) { // TODO: Create class Agent const username = { - $in: [].concat(userList), + $in: ([] as string[]).concat(userList), }; const query = queryStatusAgentOnline({ username }, isLivechatEnabledWhenAgentIdle); @@ -1465,10 +1573,10 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query); } - findOneOnlineAgentByUserList(userList, options, isLivechatEnabledWhenAgentIdle) { + findOneOnlineAgentByUserList(userList: string | string[], options?: FindOptions, isLivechatEnabledWhenAgentIdle?: boolean) { // TODO:: Create class Agent const username = { - $in: [].concat(userList), + $in: ([] as string[]).concat(userList), }; const query = queryStatusAgentOnline({ username }, isLivechatEnabledWhenAgentIdle); @@ -1476,11 +1584,30 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - getUnavailableAgents() { - return []; - } - - findBotAgents(usernameList) { + async getUnavailableAgents( + _departmentId?: string | undefined, + _extraQuery?: Document | undefined, + ): Promise< + { + agentId: string; + username: string; + lastAssignTime: string; + lastRoutingTime: string; + livechat: { maxNumberSimultaneousChat: number }; + queueInfo: { chats: number }; + }[] + > { + return [] as { + agentId: string; + username: string; + lastAssignTime: string; + lastRoutingTime: string; + livechat: { maxNumberSimultaneousChat: number }; + queueInfo: { chats: number }; + }[]; + } + + findBotAgents(usernameList?: string | string[]): FindCursor { // TODO:: Create class Agent const query = { roles: { @@ -1488,15 +1615,15 @@ export class UsersRaw extends BaseRaw { }, ...(usernameList && { username: { - $in: [].concat(usernameList), + $in: ([] as string[]).concat(usernameList), }, }), }; - return this.find(query); + return this.find(query); } - countBotAgents(usernameList) { + countBotAgents(usernameList?: string | string[]) { // TODO:: Create class Agent const query = { roles: { @@ -1504,7 +1631,7 @@ export class UsersRaw extends BaseRaw { }, ...(usernameList && { username: { - $in: [].concat(usernameList), + $in: ([] as string[]).concat(usernameList), }, }), }; @@ -1512,7 +1639,7 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query); } - removeAllRoomsByUserId(_id) { + removeAllRoomsByUserId(_id: IUser['_id']) { return this.updateOne( { _id, @@ -1524,7 +1651,7 @@ export class UsersRaw extends BaseRaw { ); } - removeRoomByUserId(_id, rid) { + removeRoomByUserId(_id: IUser['_id'], rid: IRoom['_id']) { return this.updateOne( { _id, @@ -1537,7 +1664,7 @@ export class UsersRaw extends BaseRaw { ); } - addRoomByUserId(_id, rid) { + addRoomByUserId(_id: IUser['_id'], rid: IRoom['_id']) { return this.updateOne( { _id, @@ -1549,7 +1676,7 @@ export class UsersRaw extends BaseRaw { ); } - addRoomByUserIds(uids, rid) { + addRoomByUserIds(uids: IUser['_id'][], rid: IRoom['_id']) { return this.updateMany( { _id: { $in: uids }, @@ -1561,22 +1688,25 @@ export class UsersRaw extends BaseRaw { ); } - removeRoomByRoomIds(rids) { + removeRoomByRoomIds(rids: IRoom['_id'][]) { return this.updateMany( { __rooms: { $in: rids }, }, { $pullAll: { __rooms: rids }, - $unset: rids.reduce((acc, rid) => { - acc[`roomRolePriorities.${rid}`] = ''; - return acc; - }, {}), + $unset: rids.reduce( + (acc, rid) => { + acc[`roomRolePriorities.${rid}`] = ''; + return acc; + }, + {} as Record, + ), }, ); } - addRoomRolePriorityByUserId(userId, rid, priority) { + addRoomRolePriorityByUserId(userId: IUser['_id'], rid: IRoom['_id'], priority: number) { return this.updateOne( { _id: userId, @@ -1589,7 +1719,7 @@ export class UsersRaw extends BaseRaw { ); } - removeRoomRolePriorityByUserId(userId, rid) { + removeRoomRolePriorityByUserId(userId: IUser['_id'], rid: IRoom['_id']) { return this.updateOne( { _id: userId, @@ -1602,7 +1732,7 @@ export class UsersRaw extends BaseRaw { ); } - async assignRoomRolePrioritiesByUserIdPriorityMap(userIdAndrolePriorityMap, rid) { + async assignRoomRolePrioritiesByUserIdPriorityMap(userIdAndrolePriorityMap: Record, rid: IRoom['_id']) { const bulk = this.col.initializeUnorderedBulkOp(); for (const [userId, priority] of Object.entries(userIdAndrolePriorityMap)) { @@ -1617,7 +1747,7 @@ export class UsersRaw extends BaseRaw { return 0; } - unassignRoomRolePrioritiesByRoomId(rid) { + unassignRoomRolePrioritiesByRoomId(rid: IRoom['_id']) { return this.updateMany( { __rooms: rid, @@ -1630,7 +1760,7 @@ export class UsersRaw extends BaseRaw { ); } - getLoginTokensByUserId(userId) { + getLoginTokensByUserId(userId: IUser['_id']) { const query = { 'services.resume.loginTokens.type': { $exists: true, @@ -1639,10 +1769,10 @@ export class UsersRaw extends BaseRaw { '_id': userId, }; - return this.find(query, { projection: { 'services.resume.loginTokens': 1 } }); + return this.find(query, { projection: { 'services.resume.loginTokens': 1 } }); } - addPersonalAccessTokenToUser({ userId, loginTokenObject }) { + addPersonalAccessTokenToUser({ userId, loginTokenObject }: { userId: IUser['_id']; loginTokenObject: ILoginToken }) { return this.updateOne( { _id: userId }, { @@ -1653,7 +1783,13 @@ export class UsersRaw extends BaseRaw { ); } - removePersonalAccessTokenOfUser({ userId, loginTokenObject }) { + removePersonalAccessTokenOfUser({ + userId, + loginTokenObject, + }: { + userId: IUser['_id']; + loginTokenObject: AtLeast; + }) { return this.updateOne( { _id: userId }, { @@ -1664,7 +1800,7 @@ export class UsersRaw extends BaseRaw { ); } - findPersonalAccessTokenByTokenNameAndUserId({ userId, tokenName }) { + findPersonalAccessTokenByTokenNameAndUserId({ userId, tokenName }: { userId: IUser['_id']; tokenName: string }) { const query = { 'services.resume.loginTokens': { $elemMatch: { name: tokenName, type: 'personalAccessToken' }, @@ -1675,7 +1811,8 @@ export class UsersRaw extends BaseRaw { return this.findOne(query); } - setOperator(_id, operator) { + // TODO: check if this is still valid/used for something + setOperator(_id: IUser['_id'], operator: boolean) { // TODO:: Create class Agent const update = { $set: { @@ -1686,21 +1823,28 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - async checkOnlineAgents(agentId, isLivechatEnabledWhenAgentIdle) { + async checkOnlineAgents(agentId: IUser['_id'], isLivechatEnabledWhenAgentIdle?: boolean) { // TODO:: Create class Agent const query = queryStatusAgentOnline(agentId && { _id: agentId }, isLivechatEnabledWhenAgentIdle); return !!(await this.findOne(query)); } - findOnlineAgents(agentId, isLivechatEnabledWhenAgentIdle) { + findOnlineAgents(agentId?: IUser['_id'], isLivechatEnabledWhenAgentIdle?: boolean) { // TODO:: Create class Agent const query = queryStatusAgentOnline(agentId && { _id: agentId }, isLivechatEnabledWhenAgentIdle); - return this.find(query); + return this.find(query); } - findOneBotAgent() { + countOnlineAgents(agentId: IUser['_id']) { + // TODO:: Create class Agent + const query = queryStatusAgentOnline(agentId && { _id: agentId }); + + return this.col.countDocuments(query); + } + + findOneBotAgent() { // TODO:: Create class Agent const query = { roles: { @@ -1708,23 +1852,27 @@ export class UsersRaw extends BaseRaw { }, }; - return this.findOne(query); + return this.findOne(query); } - findOneOnlineAgentById(_id, isLivechatEnabledWhenAgentIdle, options) { + findOneOnlineAgentById( + _id: IUser['_id'], + isLivechatEnabledWhenAgentIdle?: boolean, + options?: FindOptions, + ) { // TODO: Create class Agent const query = queryStatusAgentOnline({ _id }, isLivechatEnabledWhenAgentIdle); - return this.findOne(query, options); + return this.findOne(query, options); } - findAgents() { + findAgents() { // TODO: Create class Agent const query = { roles: 'livechat-agent', }; - return this.find(query); + return this.find(query); } countAgents() { @@ -1737,10 +1885,10 @@ export class UsersRaw extends BaseRaw { } // 2 - async getNextAgent(ignoreAgentId, extraQuery, enabledWhenAgentIdle) { + async getNextAgent(ignoreAgentId?: string, extraQuery?: Filter, enabledWhenAgentIdle?: boolean) { // TODO: Create class Agent // fetch all unavailable agents, and exclude them from the selection - const unavailableAgents = (await this.getUnavailableAgents(null, extraQuery)).map((u) => u.username); + const unavailableAgents = (await this.getUnavailableAgents(undefined, extraQuery)).map((u) => u.username); const extraFilters = { ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }), // limit query to remove booked agents @@ -1749,7 +1897,7 @@ export class UsersRaw extends BaseRaw { const query = queryStatusAgentOnline(extraFilters, enabledWhenAgentIdle); - const sort = { + const sort: Record = { livechatCount: 1, username: 1, }; @@ -1770,7 +1918,7 @@ export class UsersRaw extends BaseRaw { return null; } - async getNextBotAgent(ignoreAgentId) { + async getNextBotAgent(ignoreAgentId?: string) { // TODO: Create class Agent const query = { roles: { @@ -1790,7 +1938,7 @@ export class UsersRaw extends BaseRaw { }, }; - const user = await this.findOneAndUpdate(query, update, { sort, returnDocument: 'after' }); + const user = await this.findOneAndUpdate(query, update, { sort, returnDocument: 'after' } as FindOneAndUpdateOptions); if (user) { return { agentId: user._id, @@ -1800,7 +1948,7 @@ export class UsersRaw extends BaseRaw { return null; } - setLivechatStatus(userId, status) { + setLivechatStatus(userId: IUser['_id'], status: ILivechatAgentStatus) { // TODO: Create class Agent const query = { _id: userId, @@ -1816,7 +1964,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - makeAgentUnavailableAndUnsetExtension(userId) { + makeAgentUnavailableAndUnsetExtension(userId: IUser['_id']) { const query = { _id: userId, roles: 'livechat-agent', @@ -1827,14 +1975,15 @@ export class UsersRaw extends BaseRaw { statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, }, $unset: { - extension: 1, + extension: 1 as const, }, }; return this.updateOne(query, update); } - setLivechatData(userId, data = {}) { + // TODO: improve type of livechatData + setLivechatData(userId: IUser['_id'], data: Record = {}) { // TODO: Create class Agent const query = { _id: userId, @@ -1849,21 +1998,31 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } + // TODO: why this needs to be one by one instead of an updateMany? async closeOffice() { // TODO: Create class Agent - const promises = []; - await this.findAgents().forEach((agent) => promises.push(this.setLivechatStatus(agent._id, 'not-available'))); + const promises: Promise>[] = []; + // TODO: limit the data returned by findAgents + await this.findAgents().forEach((agent) => { + promises.push(this.setLivechatStatus(agent._id, ILivechatAgentStatus.NOT_AVAILABLE)); + }); await Promise.all(promises); } + // Same todo's as the above async openOffice() { // TODO: Create class Agent - const promises = []; - await this.findAgents().forEach((agent) => promises.push(this.setLivechatStatus(agent._id, 'available'))); + const promises: Promise>[] = []; + await this.findAgents().forEach((agent) => { + promises.push(this.setLivechatStatus(agent._id, ILivechatAgentStatus.AVAILABLE)); + }); await Promise.all(promises); } - getAgentInfo(agentId, showAgentEmail = false) { + getAgentInfo( + agentId: IUser['_id'], + showAgentEmail = false, + ): Promise | null> { // TODO: Create class Agent const query = { _id: agentId, @@ -1884,11 +2043,12 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - roleBaseQuery(userId) { + roleBaseQuery(userId: IUser['_id']) { return { _id: userId }; } - setE2EPublicAndPrivateKeysByUserId(userId, { public_key, private_key }) { + // eslint-disable-next-line @typescript-eslint/naming-convention + setE2EPublicAndPrivateKeysByUserId(userId: IUser['_id'], { public_key, private_key }: { public_key: string; private_key: string }) { return this.updateOne( { _id: userId }, { @@ -1900,7 +2060,7 @@ export class UsersRaw extends BaseRaw { ); } - async rocketMailUnsubscribe(_id, createdAt) { + async rocketMailUnsubscribe(_id: IUser['_id'], createdAt: string) { const query = { _id, createdAt: new Date(parseInt(createdAt)), @@ -1910,11 +2070,11 @@ export class UsersRaw extends BaseRaw { 'mailer.unsubscribed': true, }, }; - const affectedRows = (await this.updateOne(query, update)).updatedCount; + const affectedRows = (await this.updateOne(query, update)).modifiedCount; return affectedRows; } - async fetchKeysByUserId(userId) { + async fetchKeysByUserId(userId: IUser['_id']) { const user = await this.findOne({ _id: userId }, { projection: { e2e: 1 } }); if (!user?.e2e?.public_key) { @@ -1927,7 +2087,7 @@ export class UsersRaw extends BaseRaw { }; } - disable2FAAndSetTempSecretByUserId(userId, tempToken) { + disable2FAAndSetTempSecretByUserId(userId: IUser['_id'], tempToken: string) { return this.updateOne( { _id: userId, @@ -1943,7 +2103,7 @@ export class UsersRaw extends BaseRaw { ); } - enable2FAAndSetSecretAndCodesByUserId(userId, secret, backupCodes) { + enable2FAAndSetSecretAndCodesByUserId(userId: IUser['_id'], secret: string, backupCodes: string[]) { return this.updateOne( { _id: userId, @@ -1961,7 +2121,7 @@ export class UsersRaw extends BaseRaw { ); } - disable2FAByUserId(userId) { + disable2FAByUserId(userId: IUser['_id']) { return this.updateOne( { _id: userId, @@ -1976,7 +2136,7 @@ export class UsersRaw extends BaseRaw { ); } - update2FABackupCodesByUserId(userId, backupCodes) { + update2FABackupCodesByUserId(userId: IUser['_id'], backupCodes: string[]) { return this.updateOne( { _id: userId, @@ -1989,7 +2149,7 @@ export class UsersRaw extends BaseRaw { ); } - enableEmail2FAByUserId(userId) { + enableEmail2FAByUserId(userId: IUser['_id']) { return this.updateOne( { _id: userId, @@ -2005,7 +2165,7 @@ export class UsersRaw extends BaseRaw { ); } - disableEmail2FAByUserId(userId) { + disableEmail2FAByUserId(userId: IUser['_id']) { return this.updateOne( { _id: userId, @@ -2021,7 +2181,7 @@ export class UsersRaw extends BaseRaw { ); } - findByIdsWithPublicE2EKey(ids, options) { + findByIdsWithPublicE2EKey(ids: IUser['_id'][], options?: FindOptions) { const query = { '_id': { $in: ids, @@ -2034,7 +2194,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - resetE2EKey(userId) { + resetE2EKey(userId: IUser['_id']) { return this.updateOne( { _id: userId }, { @@ -2045,7 +2205,7 @@ export class UsersRaw extends BaseRaw { ); } - removeExpiredEmailCodeOfUserId(userId) { + removeExpiredEmailCodeOfUserId(userId: IUser['_id']) { return this.updateOne( { '_id': userId, 'services.emailCode.expire': { $lt: new Date() } }, { @@ -2054,7 +2214,7 @@ export class UsersRaw extends BaseRaw { ); } - removeEmailCodeOfUserId(userId) { + removeEmailCodeOfUserId(userId: IUser['_id']) { return this.updateOne( { _id: userId }, { @@ -2063,7 +2223,7 @@ export class UsersRaw extends BaseRaw { ); } - incrementInvalidEmailCodeAttempt(userId) { + incrementInvalidEmailCodeAttempt(userId: IUser['_id']) { return this.findOneAndUpdate( { _id: userId }, { @@ -2078,7 +2238,7 @@ export class UsersRaw extends BaseRaw { ); } - async maxInvalidEmailCodeAttemptsReached(userId, maxAttempts) { + async maxInvalidEmailCodeAttemptsReached(userId: IUser['_id'], maxAttempts: number) { const result = await this.findOne( { '_id': userId, @@ -2093,7 +2253,7 @@ export class UsersRaw extends BaseRaw { return !!result?._id; } - addEmailCodeByUserId(userId, code, expire) { + addEmailCodeByUserId(userId: IUser['_id'], code: string, expire: Date) { return this.updateOne( { _id: userId }, { @@ -2112,8 +2272,8 @@ export class UsersRaw extends BaseRaw { * @param {IRole['_id'][]} roles the list of role ids * @param {any} options */ - findActiveUsersInRoles(roles, options) { - roles = [].concat(roles); + findActiveUsersInRoles(roles: IRole['_id'][], options?: FindOptions) { + roles = ([] as string[]).concat(roles); const query = { roles: { $in: roles }, @@ -2123,8 +2283,8 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countActiveUsersInRoles(roles, options) { - roles = [].concat(roles); + countActiveUsersInRoles(roles: IRole['_id'][], options?: FindOptions) { + roles = ([] as string[]).concat(roles); const query = { roles: { $in: roles }, @@ -2134,7 +2294,12 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query, options); } - findOneByUsernameAndServiceNameIgnoringCase(username, userId, serviceName, options) { + findOneByUsernameAndServiceNameIgnoringCase( + username: string | RegExp, + userId: IUser['_id'], + serviceName: string, + options?: FindOptions, + ) { if (typeof username === 'string') { username = new RegExp(`^${escapeRegExp(username)}$`, 'i'); } @@ -2144,7 +2309,12 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneByEmailAddressAndServiceNameIgnoringCase(emailAddress, userId, serviceName, options) { + findOneByEmailAddressAndServiceNameIgnoringCase( + emailAddress: string, + userId: IUser['_id'], + serviceName: string, + options?: FindOptions, + ) { const query = { 'emails.address': String(emailAddress).trim(), [`services.${serviceName}.id`]: userId, @@ -2156,7 +2326,7 @@ export class UsersRaw extends BaseRaw { }); } - findOneByEmailAddress(emailAddress, options) { + findOneByEmailAddress(emailAddress: string, options?: FindOptions) { const query = { 'emails.address': String(emailAddress).trim() }; return this.findOne(query, { @@ -2165,7 +2335,7 @@ export class UsersRaw extends BaseRaw { }); } - findOneWithoutLDAPByEmailAddress(emailAddress, options) { + findOneWithoutLDAPByEmailAddress(emailAddress: string, options?: FindOptions) { const query = { 'email.address': emailAddress.trim().toLowerCase(), 'services.ldap': { @@ -2176,13 +2346,13 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneAdmin(userId, options) { + findOneAdmin(userId: IUser['_id'], options?: FindOptions) { const query = { roles: { $in: ['admin'] }, _id: userId }; return this.findOne(query, options); } - findOneByIdAndLoginToken(_id, token, options) { + findOneByIdAndLoginToken(_id: IUser['_id'], token: string, options?: FindOptions) { const query = { _id, 'services.resume.loginTokens.hashedToken': token, @@ -2191,13 +2361,13 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneById(userId, options = {}) { + findOneById(userId: IUser['_id'], options: FindOptions = {}) { const query = { _id: userId }; return this.findOne(query, options); } - findOneActiveById(userId, options) { + findOneActiveById(userId?: IUser['_id'], options?: FindOptions) { const query = { _id: userId, active: true, @@ -2206,7 +2376,7 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneByIdOrUsername(idOrUsername, options) { + findOneByIdOrUsername(idOrUsername: IUser['_id'] | IUser['username'], options?: FindOptions) { const query = { $or: [ { @@ -2221,53 +2391,53 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - findOneByRolesAndType(roles, type, options) { + findOneByRolesAndType(roles: IRole['_id'][], type: string, options?: FindOptions) { const query = { roles, type }; - return this.findOne(query, options); + return this.findOne(query, options); } - findNotOfflineByIds(users, options) { + findNotOfflineByIds(users?: IUser['_id'][], options?: FindOptions) { const query = { _id: { $in: users }, status: { - $in: ['online', 'away', 'busy'], + $in: [UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY], }, }; return this.find(query, options); } - findUsersNotOffline(options) { + findUsersNotOffline(options?: FindOptions) { const query = { username: { - $exists: 1, + $exists: true, }, status: { - $in: ['online', 'away', 'busy'], + $in: [UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY], }, }; return this.find(query, options); } - countUsersNotOffline(options) { + countUsersNotOffline(options?: FindOptions) { const query = { username: { - $exists: 1, + $exists: true, }, status: { - $in: ['online', 'away', 'busy'], + $in: [UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY], }, }; return this.col.countDocuments(query, options); } - findNotIdUpdatedFrom(uid, from, options) { - const query = { + findNotIdUpdatedFrom(uid: IUser['_id'], from: Date, options?: FindOptions) { + const query: Filter = { _id: { $ne: uid }, username: { - $exists: 1, + $exists: true, }, _updatedAt: { $gte: from }, }; @@ -2275,7 +2445,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - async findByRoomId(rid, options) { + async findByRoomId(rid: IRoom['_id'], options?: FindOptions) { const data = (await Subscriptions.findByRoomId(rid).toArray()).map((item) => item.u._id); const query = { _id: { @@ -2286,19 +2456,19 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findByUsername(username, options) { + findByUsername(username: string, options?: FindOptions) { const query = { username }; return this.find(query, options); } - findByUsernames(usernames, options) { + findByUsernames(usernames: string[], options?: FindOptions) { const query = { username: { $in: usernames } }; return this.find(query, options); } - findByUsernamesIgnoringCase(usernames, options) { + findByUsernamesIgnoringCase(usernames: string[], options?: FindOptions) { const query = { username: { $in: usernames.filter(Boolean).map((u) => new RegExp(`^${escapeRegExp(u)}$`, 'i')), @@ -2308,7 +2478,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findActiveByUserIds(ids, options = {}) { + findActiveByUserIds(ids: IUser['_id'][], options: FindOptions = {}) { return this.find( { active: true, @@ -2319,8 +2489,8 @@ export class UsersRaw extends BaseRaw { ); } - findActiveLocalGuests(idExceptions = [], options = {}) { - const query = { + findActiveLocalGuests(idExceptions: IUser['_id'] | IUser['_id'][] = [], options: FindOptions = {}) { + const query: Filter = { active: true, type: { $nin: ['app'] }, roles: { @@ -2341,8 +2511,8 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countActiveLocalGuests(idExceptions = []) { - const query = { + countActiveLocalGuests(idExceptions: IUser['_id'] | IUser['_id'][] = []) { + const query: Filter = { active: true, type: { $nin: ['app'] }, roles: { @@ -2364,10 +2534,10 @@ export class UsersRaw extends BaseRaw { } // 4 - findUsersByNameOrUsername(nameOrUsername, options) { + findUsersByNameOrUsername(nameOrUsername: string, options?: FindOptions) { const query = { username: { - $exists: 1, + $exists: true, }, $or: [{ name: nameOrUsername }, { username: nameOrUsername }], @@ -2380,7 +2550,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findByUsernameNameOrEmailAddress(usernameNameOrEmailAddress, options) { + findByUsernameNameOrEmailAddress(usernameNameOrEmailAddress: string, options?: FindOptions) { const query = { $or: [ { name: usernameNameOrEmailAddress }, @@ -2395,29 +2565,29 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findCrowdUsers(options) { + findCrowdUsers(options?: FindOptions) { const query = { crowd: true }; return this.find(query, options); } - async getLastLogin(options = { projection: { _id: 0, lastLogin: 1 } }) { + async getLastLogin(options: FindOptions = { projection: { _id: 0, lastLogin: 1 } }) { options.sort = { lastLogin: -1 }; const user = await this.findOne({}, options); return user?.lastLogin; } - findUsersByUsernames(usernames, options) { + findUsersByUsernames(usernames: string[], options?: FindOptions) { const query = { username: { $in: usernames, }, }; - return this.find(query, options); + return this.find(query, options); } - findUsersByIds(ids, options) { + findUsersByIds(ids: IUser['_id'][], options?: FindOptions) { const query = { _id: { $in: ids, @@ -2426,29 +2596,29 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - findUsersWithUsernameByIds(ids, options) { + findUsersWithUsernameByIds(ids: IUser['_id'][], options?: FindOptions) { const query = { _id: { $in: ids, }, username: { - $exists: 1, + $exists: true, }, }; return this.find(query, options); } - findUsersWithUsernameByIdsNotOffline(ids, options) { + findUsersWithUsernameByIdsNotOffline(ids: IUser['_id'][], options?: FindOptions) { const query = { _id: { $in: ids, }, username: { - $exists: 1, + $exists: true, }, status: { - $in: ['online', 'away', 'busy'], + $in: [UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY], }, }; @@ -2458,14 +2628,14 @@ export class UsersRaw extends BaseRaw { /** * @param {import('mongodb').Filter} projection */ - getOldest(optionsParams) { + getOldest(optionsParams?: FindOptions) { const query = { _id: { $ne: 'rocket.cat', }, }; - const options = { + const options: FindOptions = { ...optionsParams, sort: { createdAt: 1, @@ -2475,11 +2645,11 @@ export class UsersRaw extends BaseRaw { return this.findOne(query, options); } - countRemote(options = {}) { + countRemote(options: FindOptions = {}) { return this.countDocuments({ isRemote: true }, options); } - findActiveRemote(options = {}) { + findActiveRemote(options: FindOptions = {}) { return this.find( { active: true, @@ -2490,7 +2660,7 @@ export class UsersRaw extends BaseRaw { ); } - findActiveFederated(options = {}) { + findActiveFederated(options: FindOptions = {}) { return this.find( { active: true, @@ -2500,38 +2670,44 @@ export class UsersRaw extends BaseRaw { ); } - getSAMLByIdAndSAMLProvider(_id, provider) { + getSAMLByIdAndSAMLProvider(_id: IUser['_id'], provider: string) { return this.findOne( { _id, 'services.saml.provider': provider, }, { - 'services.saml': 1, + projection: { 'services.saml': 1 }, }, ); } - findBySAMLNameIdOrIdpSession(nameID, idpSession) { - return this.find({ - $or: [{ 'services.saml.nameID': nameID }, { 'services.saml.idpSession': idpSession }], - }); + findBySAMLNameIdOrIdpSession(nameID: string, idpSession: string, options?: FindOptions) { + return this.find( + { + $or: [{ 'services.saml.nameID': nameID }, { 'services.saml.idpSession': idpSession }], + }, + options, + ); } - countBySAMLNameIdOrIdpSession(nameID, idpSession) { + countBySAMLNameIdOrIdpSession(nameID: string, idpSession: string) { return this.countDocuments({ $or: [{ 'services.saml.nameID': nameID }, { 'services.saml.idpSession': idpSession }], }); } - findBySAMLInResponseTo(inResponseTo) { - return this.find({ - 'services.saml.inResponseTo': inResponseTo, - }); + findBySAMLInResponseTo(inResponseTo: string, options?: FindOptions) { + return this.find( + { + 'services.saml.inResponseTo': inResponseTo, + }, + options, + ); } - findOneByFreeSwitchExtension(freeSwitchExtension, options = {}) { - return this.findOne( + findOneByFreeSwitchExtension(freeSwitchExtension: string, options: FindOptions = {}) { + return this.findOne( { freeSwitchExtension, }, @@ -2539,8 +2715,8 @@ export class UsersRaw extends BaseRaw { ); } - findOneByFreeSwitchExtensions(freeSwitchExtensions, options = {}) { - return this.findOne( + findOneByFreeSwitchExtensions(freeSwitchExtensions: string[], options: FindOptions = {}) { + return this.findOne( { freeSwitchExtension: { $in: freeSwitchExtensions }, }, @@ -2556,11 +2732,11 @@ export class UsersRaw extends BaseRaw { }).map(({ freeSwitchExtension }) => freeSwitchExtension); } - findUsersWithAssignedFreeSwitchExtensions(options = {}) { - return this.find( + findUsersWithAssignedFreeSwitchExtensions(options: FindOptions = {}) { + return this.find( { freeSwitchExtension: { - $exists: 1, + $exists: true, }, }, options, @@ -2568,8 +2744,8 @@ export class UsersRaw extends BaseRaw { } // UPDATE - addImportIds(_id, importIds) { - importIds = [].concat(importIds); + addImportIds(_id: IUser['_id'], importIds: string[]) { + importIds = ([] as string[]).concat(importIds); const query = { _id }; @@ -2584,7 +2760,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - updateInviteToken(_id, inviteToken) { + updateInviteToken(_id: IUser['_id'], inviteToken: string) { const update = { $set: { inviteToken, @@ -2594,7 +2770,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - updateLastLoginById(_id) { + updateLastLoginById(_id: IUser['_id']) { const update = { $set: { lastLogin: new Date(), @@ -2604,7 +2780,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - addPasswordToHistory(_id, password, passwordHistoryAmount) { + addPasswordToHistory(_id: IUser['_id'], password: string, passwordHistoryAmount: number) { const update = { $push: { 'services.passwordHistory': { @@ -2616,39 +2792,24 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setServiceId(_id, serviceName, serviceId) { - const update = { $set: {} }; + setServiceId(_id: IUser['_id'], serviceName: string, serviceId: string) { + const update: UpdateFilter = { $set: {} }; const serviceIdKey = `services.${serviceName}.id`; - update.$set[serviceIdKey] = serviceId; + if (update.$set) { + update.$set[serviceIdKey] = serviceId; + } return this.updateOne({ _id }, update); } - /** - * - * @param {string} _id - * @param {string} username - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - setUsername(_id, username, options) { + setUsername(_id: IUser['_id'], username: string, options?: UpdateOptions) { const update = { $set: { username } }; return this.updateOne({ _id }, update, { session: options?.session }); } - /** - * - * @param {string} _id - * @param {string} email - * @param {boolean} verified - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - setEmail(_id, email, verified = false, options) { + setEmail(_id: IUser['_id'], email: string, verified = false, options?: UpdateOptions) { const update = { $set: { emails: [ @@ -2664,7 +2825,7 @@ export class UsersRaw extends BaseRaw { } // 5 - setEmailVerified(_id, email) { + setEmailVerified(_id: IUser['_id'], email: string) { const query = { _id, emails: { @@ -2684,15 +2845,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - /** - * - * @param {string} _id - * @param {string} name - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - setName(_id, name, options) { + setName(_id: IUser['_id'], name: string, options?: UpdateOptions) { const update = { $set: { name, @@ -2702,25 +2855,18 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update, { session: options?.session }); } - /** - * - * @param {string} _id - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - unsetName(_id, options) { + unsetName(_id: IUser['_id'], options?: UpdateOptions) { const update = { $unset: { - name, + name: 1 as const, }, }; return this.updateOne({ _id }, update, { session: options?.session }); } - setCustomFields(_id, fields) { - const values = {}; + setCustomFields(_id: IUser['_id'], fields: Record) { + const values: Record = {}; Object.keys(fields).forEach((key) => { values[`customFields.${key}`] = fields[key]; }); @@ -2730,16 +2876,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - /** - * - * @param {string} _id - * @param {string} origin - * @param {string} etag - * @param {Object} options - * @param {ClientSession} options.session - * @returns {Promise} - */ - setAvatarData(_id, origin, etag, options) { + setAvatarData(_id: IUser['_id'], origin: string, etag: string, options?: UpdateOptions) { const update = { $set: { avatarOrigin: origin, @@ -2750,8 +2887,8 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update, { session: options?.session }); } - unsetAvatarData(_id) { - const update = { + unsetAvatarData(_id: IUser['_id']) { + const update: UpdateFilter = { $unset: { avatarOrigin: 1, avatarETag: 1, @@ -2761,7 +2898,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setUserActive(_id, active) { + setUserActive(_id: IUser['_id'], active: boolean | null) { if (active == null) { active = true; } @@ -2774,7 +2911,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setAllUsersActive(active) { + setAllUsersActive(active: boolean) { const update = { $set: { active, @@ -2789,8 +2926,8 @@ export class UsersRaw extends BaseRaw { * @param {IRole['_id']} role the role id * @param {boolean} active */ - setActiveNotLoggedInAfterWithRole(latestLastLoginDate, role = 'user', active = false) { - const neverActive = { lastLogin: { $exists: 0 }, createdAt: { $lte: latestLastLoginDate } }; + setActiveNotLoggedInAfterWithRole(latestLastLoginDate: Date, role: IRole['_id'] = 'user', active = false) { + const neverActive = { lastLogin: { $exists: false }, createdAt: { $lte: latestLastLoginDate } }; const idleTooLong = { lastLogin: { $lte: latestLastLoginDate } }; const query = { @@ -2808,8 +2945,8 @@ export class UsersRaw extends BaseRaw { return this.updateMany(query, update); } - unsetRequirePasswordChange(_id) { - const update = { + unsetRequirePasswordChange(_id: IUser['_id']) { + const update: UpdateFilter = { $unset: { requirePasswordChange: true, requirePasswordChangeReason: true, @@ -2819,10 +2956,10 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - resetPasswordAndSetRequirePasswordChange(_id, requirePasswordChange, requirePasswordChangeReason) { + resetPasswordAndSetRequirePasswordChange(_id: IUser['_id'], requirePasswordChange: boolean, requirePasswordChangeReason: string) { const update = { $unset: { - 'services.password': 1, + 'services.password': 1 as const, }, $set: { requirePasswordChange, @@ -2833,7 +2970,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setLanguage(_id, language) { + setLanguage(_id: IUser['_id'], language: string) { const update = { $set: { language, @@ -2843,7 +2980,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setProfile(_id, profile) { + setProfile(_id: IUser['_id'], profile: Record) { const update = { $set: { 'settings.profile': profile, @@ -2853,8 +2990,8 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setBio(_id, bio = '') { - const update = { + setBio(_id: IUser['_id'], bio = '') { + const update: UpdateFilter = { ...(bio.trim() ? { $set: { @@ -2870,8 +3007,8 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setNickname(_id, nickname = '') { - const update = { + setNickname(_id: IUser['_id'], nickname = '') { + const update: UpdateFilter = { ...(nickname.trim() ? { $set: { @@ -2887,7 +3024,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - clearSettings(_id) { + clearSettings(_id: IUser['_id']) { const update = { $set: { settings: {}, @@ -2897,7 +3034,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - setPreferences(_id, preferences) { + setPreferences(_id: IUser['_id'], preferences: Record) { const settingsObject = Object.assign( {}, ...Object.keys(preferences).map((key) => ({ @@ -2905,18 +3042,18 @@ export class UsersRaw extends BaseRaw { })), ); - const update = { + const update: DeepWritable> = { $set: settingsObject, }; if (parseInt(preferences.clockMode) === 0) { - delete update.$set['settings.preferences.clockMode']; + delete update.$set?.['settings.preferences.clockMode']; update.$unset = { 'settings.preferences.clockMode': 1 }; } return this.updateOne({ _id }, update); } - setTwoFactorAuthorizationHashAndUntilForUserIdAndToken(_id, token, hash, until) { + setTwoFactorAuthorizationHashAndUntilForUserIdAndToken(_id: IUser['_id'], token: string, hash: string, until: Date) { return this.updateOne( { _id, @@ -2931,7 +3068,7 @@ export class UsersRaw extends BaseRaw { ); } - setUtcOffset(_id, utcOffset) { + setUtcOffset(_id: IUser['_id'], utcOffset: number) { const query = { _id, utcOffset: { @@ -2948,52 +3085,7 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } - saveUserById(_id, data) { - const setData = {}; - const unsetData = {}; - - if (data.name != null) { - if (data.name.trim()) { - setData.name = data.name.trim(); - } else { - unsetData.name = 1; - } - } - - if (data.email != null) { - if (data.email.trim()) { - setData.emails = [{ address: data.email.trim() }]; - } else { - unsetData.emails = 1; - } - } - - if (data.phone != null) { - if (data.phone.trim()) { - setData.phone = [{ phoneNumber: data.phone.trim() }]; - } else { - unsetData.phone = 1; - } - } - - const update = {}; - - if (setData) { - update.$set = setData; - } - - if (unsetData) { - update.$unset = unsetData; - } - - if (update) { - return true; - } - - return this.updateOne({ _id }, update); - } - - setReason(_id, reason) { + setReason(_id: IUser['_id'], reason: string) { const update = { $set: { reason, @@ -3003,17 +3095,17 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - unsetReason(_id) { + unsetReason(_id: IUser['_id']) { const update = { $unset: { - reason: true, + reason: true as const, }, }; return this.updateOne({ _id }, update); } - async bannerExistsById(_id, bannerId) { + async bannerExistsById(_id: IUser['_id'], bannerId: string) { const query = { _id, [`banners.${bannerId}`]: { @@ -3024,7 +3116,7 @@ export class UsersRaw extends BaseRaw { return (await this.countDocuments(query)) !== 0; } - setBannerReadById(_id, bannerId) { + setBannerReadById(_id: IUser['_id'], bannerId: string) { const update = { $set: { [`banners.${bannerId}.read`]: true, @@ -3034,27 +3126,27 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - removeBannerById(_id, bannerId) { + removeBannerById(_id: IUser['_id'], bannerId: string) { const update = { $unset: { - [`banners.${bannerId}`]: true, + [`banners.${bannerId}`]: true as const, }, }; return this.updateOne({ _id }, update); } - removeSamlServiceSession(_id) { + removeSamlServiceSession(_id: IUser['_id']) { const update = { $unset: { - 'services.saml.idpSession': '', + 'services.saml.idpSession': 1 as const, }, }; return this.updateOne({ _id }, update); } - updateDefaultStatus(_id, statusDefault) { + updateDefaultStatus(_id: IUser['_id'], statusDefault: UserStatus) { return this.updateOne( { _id, @@ -3068,8 +3160,8 @@ export class UsersRaw extends BaseRaw { ); } - setSamlInResponseTo(_id, inResponseTo) { - this.updateOne( + setSamlInResponseTo(_id: IUser['_id'], inResponseTo: string) { + return this.updateOne( { _id, }, @@ -3081,7 +3173,7 @@ export class UsersRaw extends BaseRaw { ); } - async setFreeSwitchExtension(_id, extension) { + async setFreeSwitchExtension(_id: IUser['_id'], extension?: string) { return this.updateOne( { _id, @@ -3093,11 +3185,11 @@ export class UsersRaw extends BaseRaw { } // INSERT - create(data) { + create(data: InsertionModel) { const user = { createdAt: new Date(), avatarOrigin: 'none', - }; + } as InsertionModel; Object.assign(user, data); @@ -3105,18 +3197,18 @@ export class UsersRaw extends BaseRaw { } // REMOVE - removeById(_id) { + removeById(_id: IUser['_id']) { return this.deleteOne({ _id }); } - removeLivechatData(userId) { + removeLivechatData(userId: IUser['_id']) { const query = { _id: userId, }; const update = { $unset: { - livechat: true, + livechat: 1 as const, }, }; @@ -3130,15 +3222,15 @@ export class UsersRaw extends BaseRaw { - has not disabled email notifications - `active` is equal to true (false means they were deactivated and can't login) */ - getUsersToSendOfflineEmail(usersIds) { + getUsersToSendOfflineEmail(usersIds: IUser['_id'][]) { const query = { '_id': { $in: usersIds, }, 'active': true, - 'status': 'offline', + 'status': UserStatus.OFFLINE, 'statusConnection': { - $ne: 'online', + $ne: UserStatus.ONLINE, }, 'emails.verified': true, }; @@ -3156,7 +3248,7 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } - countActiveUsersByService(serviceName, options) { + countActiveUsersByService(serviceName: string, options?: FindOptions) { const query = { active: true, type: { $nin: ['app'] }, @@ -3168,7 +3260,7 @@ export class UsersRaw extends BaseRaw { } // here - getActiveLocalUserCount() { + async getActiveLocalUserCount() { return Promise.all([ // Count all active users (fast based on index) this.countDocuments({ @@ -3188,8 +3280,8 @@ export class UsersRaw extends BaseRaw { return this.countActiveLocalGuests(idExceptions); } - removeOlderResumeTokensByUserId(userId, fromDate) { - this.updateOne( + removeOlderResumeTokensByUserId(userId: IUser['_id'], fromDate: Date) { + return this.updateOne( { _id: userId }, { $pull: { @@ -3229,7 +3321,7 @@ export class UsersRaw extends BaseRaw { return this.countDocuments(query); } - updateCustomFieldsById(userId, customFields) { + updateCustomFieldsById(userId: IUser['_id'], customFields: Record) { return this.updateOne( { _id: userId }, { @@ -3240,12 +3332,12 @@ export class UsersRaw extends BaseRaw { ); } - countRoomMembers(roomId) { + countRoomMembers(roomId: IRoom['_id']) { return this.countDocuments({ __rooms: roomId, active: true }); } - removeAgent(_id) { - const update = { + removeAgent(_id: IUser['_id']) { + const update: UpdateFilter = { $set: { operator: false, }, @@ -3260,11 +3352,11 @@ export class UsersRaw extends BaseRaw { return this.updateOne({ _id }, update); } - countByRole(role) { + countByRole(role: IRole['_id']) { return this.countDocuments({ roles: role }); } - updateLivechatStatusByAgentIds(userIds, status) { + updateLivechatStatusByAgentIds(userIds: IUser['_id'][], status: ILivechatAgentStatus) { return this.updateMany( { _id: { $in: userIds }, diff --git a/packages/models/tsconfig.json b/packages/models/tsconfig.json index e28418fe4a248..52e9dd8c4976b 100644 --- a/packages/models/tsconfig.json +++ b/packages/models/tsconfig.json @@ -1,11 +1,9 @@ { "extends": "../../tsconfig.base.server.json", "compilerOptions": { - // TODO migrate Users to TS - "allowJs": true, "declaration": true, "rootDir": "./src", - "outDir": "./dist", + "outDir": "./dist" }, "include": ["./src/**/*"] } From 57ce661ae771d6f744ca60cd7c36e49bb7a01eed Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 20 Mar 2025 11:37:37 -0300 Subject: [PATCH 002/187] chore: add mongo-adapter package with initial implementation and types (#35552) --- .../lists/useStreamUpdatesForMessageList.ts | 4 ++-- .../hooks/useThreadMainMessageQuery.ts | 4 ++-- apps/meteor/package.json | 1 + packages/mock-providers/package.json | 1 + packages/mongo-adapter/.eslintrc.json | 4 ++++ packages/mongo-adapter/CHANGELOG.md | 13 ++++++++++++ packages/mongo-adapter/package.json | 20 +++++++++++++++++++ .../mongo-adapter/src}/bson.spec.ts | 0 .../mongo-adapter/src}/bson.ts | 0 .../mongo-adapter/src}/comparisons.spec.ts | 0 .../mongo-adapter/src}/comparisons.ts | 0 .../mongo-adapter/src}/index.ts | 0 .../mongo-adapter/src}/lookups.spec.ts | 0 .../mongo-adapter/src}/lookups.ts | 0 .../mongo-adapter/src}/query.ts | 0 .../mongo-adapter/src}/sort.ts | 0 .../mongo-adapter/src}/types.ts | 0 packages/mongo-adapter/tsconfig.json | 8 ++++++++ yarn.lock | 11 ++++++++++ 19 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 packages/mongo-adapter/.eslintrc.json create mode 100644 packages/mongo-adapter/CHANGELOG.md create mode 100644 packages/mongo-adapter/package.json rename {apps/meteor/client/lib/minimongo => packages/mongo-adapter/src}/bson.spec.ts (100%) rename {apps/meteor/client/lib/minimongo => packages/mongo-adapter/src}/bson.ts (100%) rename {apps/meteor/client/lib/minimongo => packages/mongo-adapter/src}/comparisons.spec.ts (100%) rename {apps/meteor/client/lib/minimongo => packages/mongo-adapter/src}/comparisons.ts (100%) rename {apps/meteor/client/lib/minimongo => packages/mongo-adapter/src}/index.ts (100%) rename {apps/meteor/client/lib/minimongo => packages/mongo-adapter/src}/lookups.spec.ts (100%) rename {apps/meteor/client/lib/minimongo => packages/mongo-adapter/src}/lookups.ts (100%) rename {apps/meteor/client/lib/minimongo => packages/mongo-adapter/src}/query.ts (100%) rename {apps/meteor/client/lib/minimongo => packages/mongo-adapter/src}/sort.ts (100%) rename {apps/meteor/client/lib/minimongo => packages/mongo-adapter/src}/types.ts (100%) create mode 100644 packages/mongo-adapter/tsconfig.json diff --git a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts index a28c4424d313c..2134499ce4eee 100644 --- a/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts +++ b/apps/meteor/client/hooks/lists/useStreamUpdatesForMessageList.ts @@ -1,10 +1,10 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { FieldExpression, Query } from '@rocket.chat/mongo-adapter'; +import { createFilterFromQuery } from '@rocket.chat/mongo-adapter'; import { useStream } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import type { MessageList } from '../../lib/lists/MessageList'; -import type { FieldExpression, Query } from '../../lib/minimongo'; -import { createFilterFromQuery } from '../../lib/minimongo'; type NotifyRoomRidDeleteMessageBulkEvent = { rid: IMessage['rid']; diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts index eef1630d35337..6baed3917707d 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts @@ -1,4 +1,6 @@ import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; +import type { FieldExpression, Query } from '@rocket.chat/mongo-adapter'; +import { createFilterFromQuery } from '@rocket.chat/mongo-adapter'; import { useStream } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQueryClient, useQuery } from '@tanstack/react-query'; @@ -6,8 +8,6 @@ import { useCallback, useEffect, useRef } from 'react'; import { useGetMessageByID } from './useGetMessageByID'; import { withDebouncing } from '../../../../../../lib/utils/highOrderFunctions'; -import type { FieldExpression, Query } from '../../../../../lib/minimongo'; -import { createFilterFromQuery } from '../../../../../lib/minimongo'; import { onClientMessageReceived } from '../../../../../lib/onClientMessageReceived'; import { useRoom } from '../../../contexts/RoomContext'; diff --git a/apps/meteor/package.json b/apps/meteor/package.json index c48feccaf2e6e..a430934accbb6 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -266,6 +266,7 @@ "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/mongo-adapter": "workspace:^", "@rocket.chat/mp3-encoder": "^0.31.26", "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index f7acdfcea6780..1d81b22451007 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -11,6 +11,7 @@ }, "devDependencies": { "@rocket.chat/ddp-client": "workspace:~", + "@rocket.chat/mongo-adapter": "workspace:~", "@rocket.chat/ui-contexts": "workspace:*", "@rocket.chat/ui-video-conf": "workspace:*", "@tanstack/react-query": "~5.65.1", diff --git a/packages/mongo-adapter/.eslintrc.json b/packages/mongo-adapter/.eslintrc.json new file mode 100644 index 0000000000000..a83aeda48e66d --- /dev/null +++ b/packages/mongo-adapter/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/packages/mongo-adapter/CHANGELOG.md b/packages/mongo-adapter/CHANGELOG.md new file mode 100644 index 0000000000000..33beba0c5a98c --- /dev/null +++ b/packages/mongo-adapter/CHANGELOG.md @@ -0,0 +1,13 @@ +# @rocket.chat/account-utils + +## 0.0.2 + +### Patch Changes + +- ([#31138](https://github.com/RocketChat/Rocket.Chat/pull/31138)) feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo + +## 0.0.2-rc.0 + +### Patch Changes + +- b223cbde14: feat(uikit): Move `@rocket.chat/ui-kit` package to the main monorepo diff --git a/packages/mongo-adapter/package.json b/packages/mongo-adapter/package.json new file mode 100644 index 0000000000000..3059f549d42e2 --- /dev/null +++ b/packages/mongo-adapter/package.json @@ -0,0 +1,20 @@ +{ + "name": "@rocket.chat/mongo-adapter", + "version": "0.0.2", + "private": true, + "devDependencies": { + "eslint": "~8.45.0", + "typescript": "~5.7.2" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ] +} diff --git a/apps/meteor/client/lib/minimongo/bson.spec.ts b/packages/mongo-adapter/src/bson.spec.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/bson.spec.ts rename to packages/mongo-adapter/src/bson.spec.ts diff --git a/apps/meteor/client/lib/minimongo/bson.ts b/packages/mongo-adapter/src/bson.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/bson.ts rename to packages/mongo-adapter/src/bson.ts diff --git a/apps/meteor/client/lib/minimongo/comparisons.spec.ts b/packages/mongo-adapter/src/comparisons.spec.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/comparisons.spec.ts rename to packages/mongo-adapter/src/comparisons.spec.ts diff --git a/apps/meteor/client/lib/minimongo/comparisons.ts b/packages/mongo-adapter/src/comparisons.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/comparisons.ts rename to packages/mongo-adapter/src/comparisons.ts diff --git a/apps/meteor/client/lib/minimongo/index.ts b/packages/mongo-adapter/src/index.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/index.ts rename to packages/mongo-adapter/src/index.ts diff --git a/apps/meteor/client/lib/minimongo/lookups.spec.ts b/packages/mongo-adapter/src/lookups.spec.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/lookups.spec.ts rename to packages/mongo-adapter/src/lookups.spec.ts diff --git a/apps/meteor/client/lib/minimongo/lookups.ts b/packages/mongo-adapter/src/lookups.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/lookups.ts rename to packages/mongo-adapter/src/lookups.ts diff --git a/apps/meteor/client/lib/minimongo/query.ts b/packages/mongo-adapter/src/query.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/query.ts rename to packages/mongo-adapter/src/query.ts diff --git a/apps/meteor/client/lib/minimongo/sort.ts b/packages/mongo-adapter/src/sort.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/sort.ts rename to packages/mongo-adapter/src/sort.ts diff --git a/apps/meteor/client/lib/minimongo/types.ts b/packages/mongo-adapter/src/types.ts similarity index 100% rename from apps/meteor/client/lib/minimongo/types.ts rename to packages/mongo-adapter/src/types.ts diff --git a/packages/mongo-adapter/tsconfig.json b/packages/mongo-adapter/tsconfig.json new file mode 100644 index 0000000000000..e2be47cf5499f --- /dev/null +++ b/packages/mongo-adapter/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.client.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/yarn.lock b/yarn.lock index 5cf6a28a991f7..ab07718796212 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8841,6 +8841,7 @@ __metadata: "@rocket.chat/mock-providers": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/mongo-adapter": "workspace:^" "@rocket.chat/mp3-encoder": "npm:^0.31.26" "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" @@ -9166,6 +9167,7 @@ __metadata: "@rocket.chat/ddp-client": "workspace:~" "@rocket.chat/emitter": "npm:~0.31.25" "@rocket.chat/i18n": "workspace:~" + "@rocket.chat/mongo-adapter": "workspace:~" "@rocket.chat/ui-contexts": "workspace:*" "@rocket.chat/ui-video-conf": "workspace:*" "@storybook/react": "npm:^8.6.4" @@ -9214,6 +9216,15 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/mongo-adapter@workspace:^, @rocket.chat/mongo-adapter@workspace:packages/mongo-adapter, @rocket.chat/mongo-adapter@workspace:~": + version: 0.0.0-use.local + resolution: "@rocket.chat/mongo-adapter@workspace:packages/mongo-adapter" + dependencies: + eslint: "npm:~8.45.0" + typescript: "npm:~5.7.2" + languageName: unknown + linkType: soft + "@rocket.chat/mp3-encoder@npm:^0.31.26": version: 0.31.26 resolution: "@rocket.chat/mp3-encoder@npm:0.31.26" From a12d7f235dc254cef059a99fa580c96b488813e7 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 20 Mar 2025 11:52:05 -0300 Subject: [PATCH 003/187] chore: improve-mocked-provider-settings (#35554) --- packages/mock-providers/jest.config.ts | 7 +++ packages/mock-providers/package.json | 8 ++- .../src/MockedAppRootBuilder.tsx | 36 +++++++++++- .../src/tests/useSetting.spec.tsx | 13 +++++ .../src/tests/useSettings.spec.tsx | 38 +++++++++++++ packages/mock-providers/tsconfig.json | 3 +- yarn.lock | 56 +++++++++++++++++-- 7 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 packages/mock-providers/jest.config.ts create mode 100644 packages/mock-providers/src/tests/useSetting.spec.tsx create mode 100644 packages/mock-providers/src/tests/useSettings.spec.tsx diff --git a/packages/mock-providers/jest.config.ts b/packages/mock-providers/jest.config.ts new file mode 100644 index 0000000000000..513d37db1e9c3 --- /dev/null +++ b/packages/mock-providers/jest.config.ts @@ -0,0 +1,7 @@ +import client from '@rocket.chat/jest-presets/client'; +import type { Config } from 'jest'; + +export default { + preset: client.preset, + modulePathIgnorePatterns: ['/__tests__/helpers'], +} satisfies Config; diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 1d81b22451007..097a049f48b1b 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -5,6 +5,7 @@ "dependencies": { "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/i18n": "workspace:~", + "@rocket.chat/ui-contexts": "workspace:^", "@storybook/react": "^8.6.4", "i18next": "~23.4.9", "react-i18next": "~13.2.2" @@ -12,10 +13,15 @@ "devDependencies": { "@rocket.chat/ddp-client": "workspace:~", "@rocket.chat/mongo-adapter": "workspace:~", - "@rocket.chat/ui-contexts": "workspace:*", "@rocket.chat/ui-video-conf": "workspace:*", "@tanstack/react-query": "~5.65.1", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@testing-library/react-hooks": "^8.0.1", + "@types/react": "~18.3.17", + "@types/react-dom": "~18.3.5", "eslint": "~8.45.0", + "jest": "^29.7.0", "react": "~18.3.1", "typescript": "~5.7.2" }, diff --git a/packages/mock-providers/src/MockedAppRootBuilder.tsx b/packages/mock-providers/src/MockedAppRootBuilder.tsx index 508f3a285e1d2..157679db3d24a 100644 --- a/packages/mock-providers/src/MockedAppRootBuilder.tsx +++ b/packages/mock-providers/src/MockedAppRootBuilder.tsx @@ -11,6 +11,7 @@ import type { import type { ServerMethodName, ServerMethodParameters, ServerMethodReturn } from '@rocket.chat/ddp-client'; import { Emitter } from '@rocket.chat/emitter'; import languages from '@rocket.chat/i18n/dist/languages'; +import { createFilterFromQuery } from '@rocket.chat/mongo-adapter'; import type { Method, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings'; import type { Device, ModalContextValue, SubscriptionWithRoom, TranslationKey } from '@rocket.chat/ui-contexts'; import { @@ -40,12 +41,23 @@ type Mutable = { -readonly [P in keyof T]: T[P]; }; +export type SettingsContextQuery = { + readonly _id?: ISetting['_id'][] | RegExp; + readonly group?: ISetting['_id']; + readonly section?: string; + readonly tab?: ISetting['_id']; +}; + // eslint-disable-next-line @typescript-eslint/naming-convention interface MockedAppRootEvents { 'update-modal': void; } +const empty = [] as const; + export class MockedAppRootBuilder { + private _settings: Map = new Map(); + private wrappers: Array<(children: ReactNode) => ReactNode> = []; private connectionStatus: ContextType = { @@ -94,7 +106,7 @@ export class MockedAppRootBuilder { hasPrivateAccess: true, isLoading: false, querySetting: (_id: string) => [() => () => undefined, () => undefined], - querySettings: () => [() => () => undefined, () => []], + querySettings: (_query: SettingsContextQuery) => [() => () => undefined, () => empty as unknown as ISetting[]], dispatch: async () => undefined, }; @@ -373,7 +385,6 @@ export class MockedAppRootBuilder { } as ISetting; const innerFn = this.settings.querySetting; - const outerFn = ( innerSetting: string, ): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting | undefined] => { @@ -386,6 +397,27 @@ export class MockedAppRootBuilder { this.settings.querySetting = outerFn; + this._settings.set(id, setting); + + const cache = new WeakMap(); + + this.settings.querySettings = (query: SettingsContextQuery) => { + const filter = + cache.get(query) ?? + createFilterFromQuery({ + ...query, + ...(query._id ? { _id: { $in: query._id } } : {}), + } as any); + cache.set(query, filter); + const arr = cache.get(filter) ?? Array.from(this._settings.values()).filter(filter); + return [ + () => () => undefined, + () => { + return arr; + }, + ]; + }; + return this; } diff --git a/packages/mock-providers/src/tests/useSetting.spec.tsx b/packages/mock-providers/src/tests/useSetting.spec.tsx new file mode 100644 index 0000000000000..bcad1b4da6e79 --- /dev/null +++ b/packages/mock-providers/src/tests/useSetting.spec.tsx @@ -0,0 +1,13 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { renderHook } from '@testing-library/react-hooks'; + +import { mockAppRoot } from '..'; + +describe('useSetting', () => { + it('should return settings from context', () => { + const { result } = renderHook(() => useSetting('asd'), { + wrapper: mockAppRoot().withSetting('asd', 'qwe').build(), + }); + expect(result.current).toEqual('qwe'); + }); +}); diff --git a/packages/mock-providers/src/tests/useSettings.spec.tsx b/packages/mock-providers/src/tests/useSettings.spec.tsx new file mode 100644 index 0000000000000..5263831e39ed7 --- /dev/null +++ b/packages/mock-providers/src/tests/useSettings.spec.tsx @@ -0,0 +1,38 @@ +import { useSettings } from '@rocket.chat/ui-contexts'; +import { renderHook } from '@testing-library/react-hooks'; + +import { mockAppRoot } from '..'; + +describe('useSettings', () => { + it('should return all settings', () => { + const query = {}; + const { result } = renderHook(() => useSettings(query), { + wrapper: mockAppRoot().withSetting('asd', 'qwe').withSetting('zxc', 'rty').build(), + }); + expect(result.current).toEqual([ + { + _id: 'asd', + value: 'qwe', + }, + { + _id: 'zxc', + value: 'rty', + }, + ]); + }); + + it('should return settings filtered by _id', () => { + const query = { + _id: ['asd'], + }; + const { result } = renderHook(() => useSettings(query), { + wrapper: mockAppRoot().withSetting('asd', 'qwe').withSetting('zxc', 'rty').build(), + }); + expect(result.current).toEqual([ + { + _id: 'asd', + value: 'qwe', + }, + ]); + }); +}); diff --git a/packages/mock-providers/tsconfig.json b/packages/mock-providers/tsconfig.json index e2be47cf5499f..8166e3acd42ab 100644 --- a/packages/mock-providers/tsconfig.json +++ b/packages/mock-providers/tsconfig.json @@ -4,5 +4,6 @@ "rootDir": "./src", "outDir": "./dist" }, - "include": ["./src/**/*"] + "include": ["./src/**/*"], + "exclude": ["./src/**/*.spec.ts"] } diff --git a/yarn.lock b/yarn.lock index ab07718796212..d3468caa822f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9168,12 +9168,18 @@ __metadata: "@rocket.chat/emitter": "npm:~0.31.25" "@rocket.chat/i18n": "workspace:~" "@rocket.chat/mongo-adapter": "workspace:~" - "@rocket.chat/ui-contexts": "workspace:*" + "@rocket.chat/ui-contexts": "workspace:^" "@rocket.chat/ui-video-conf": "workspace:*" "@storybook/react": "npm:^8.6.4" "@tanstack/react-query": "npm:~5.65.1" + "@testing-library/jest-dom": "npm:^6.6.3" + "@testing-library/react": "npm:^16.2.0" + "@testing-library/react-hooks": "npm:^8.0.1" + "@types/react": "npm:~18.3.17" + "@types/react-dom": "npm:~18.3.5" eslint: "npm:~8.45.0" i18next: "npm:~23.4.9" + jest: "npm:^29.7.0" react: "npm:~18.3.1" react-i18next: "npm:~13.2.2" typescript: "npm:~5.7.2" @@ -9843,7 +9849,7 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/ui-contexts@workspace:*, @rocket.chat/ui-contexts@workspace:^, @rocket.chat/ui-contexts@workspace:packages/ui-contexts, @rocket.chat/ui-contexts@workspace:~": +"@rocket.chat/ui-contexts@workspace:^, @rocket.chat/ui-contexts@workspace:packages/ui-contexts, @rocket.chat/ui-contexts@workspace:~": version: 0.0.0-use.local resolution: "@rocket.chat/ui-contexts@workspace:packages/ui-contexts" dependencies: @@ -11280,7 +11286,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/jest-dom@npm:~6.6.3": +"@testing-library/jest-dom@npm:^6.6.3, @testing-library/jest-dom@npm:~6.6.3": version: 6.6.3 resolution: "@testing-library/jest-dom@npm:6.6.3" dependencies: @@ -11295,6 +11301,48 @@ __metadata: languageName: node linkType: hard +"@testing-library/react-hooks@npm:^8.0.1": + version: 8.0.1 + resolution: "@testing-library/react-hooks@npm:8.0.1" + dependencies: + "@babel/runtime": "npm:^7.12.5" + react-error-boundary: "npm:^3.1.0" + peerDependencies: + "@types/react": ^16.9.0 || ^17.0.0 + react: ^16.9.0 || ^17.0.0 + react-dom: ^16.9.0 || ^17.0.0 + react-test-renderer: ^16.9.0 || ^17.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + checksum: 10/f7b69373feebe99bc7d60595822cc5c00a1a5a4801bc4f99b597256a5c1d23c45a51f359051dd8a7bdffcc23b26f324c582e9433c25804934fd351a886812790 + languageName: node + linkType: hard + +"@testing-library/react@npm:^16.2.0": + version: 16.2.0 + resolution: "@testing-library/react@npm:16.2.0" + dependencies: + "@babel/runtime": "npm:^7.12.5" + peerDependencies: + "@testing-library/dom": ^10.0.0 + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10/cf10bfa9a363384e6861417696fff4a464a64f98ec6f0bb7f1fa7cbb550d075d23a2f6a943b7df85dded7bde3234f6ea6b6e36f95211f4544b846ea72c288289 + languageName: node + linkType: hard + "@testing-library/react@npm:~16.0.1": version: 16.0.1 resolution: "@testing-library/react@npm:16.0.1" @@ -31490,7 +31538,7 @@ __metadata: languageName: node linkType: hard -"react-error-boundary@npm:^3.1.4": +"react-error-boundary@npm:^3.1.0, react-error-boundary@npm:^3.1.4": version: 3.1.4 resolution: "react-error-boundary@npm:3.1.4" dependencies: From d649a761edd71e1325a635b757ef1df2e5a778a4 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 20 Mar 2025 10:48:31 -0600 Subject: [PATCH 004/187] feat: Livechat department removed/disabled events for apps (#35280) --- .changeset/big-tips-greet.md | 7 +++++ .../app/apps/server/bridges/listeners.js | 12 ++++++++ .../app/livechat/server/lib/departmentsLib.ts | 8 ++++- .../livechat/ILivechatEventContext.ts | 5 ++++ .../IPostLivechatDepartmentDisabled.ts | 25 ++++++++++++++++ .../IPostLivechatDepartmentRemoved.ts | 25 ++++++++++++++++ .../src/definition/livechat/index.ts | 4 +++ .../src/definition/metadata/AppInterface.ts | 2 ++ .../src/definition/metadata/AppMethod.ts | 2 ++ .../src/server/managers/AppListenerManager.ts | 29 +++++++++++++++++++ packages/apps/src/bridges/IListenerBridge.ts | 6 +++- 11 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 .changeset/big-tips-greet.md create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentDisabled.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentRemoved.ts diff --git a/.changeset/big-tips-greet.md b/.changeset/big-tips-greet.md new file mode 100644 index 0000000000000..701e013dec0da --- /dev/null +++ b/.changeset/big-tips-greet.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/apps-engine": minor +"@rocket.chat/apps": minor +--- + +Allows apps to react to department status changes. diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js index ebf57f7ccceb3..31aa2c0052695 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ b/apps/meteor/app/apps/server/bridges/listeners.js @@ -50,6 +50,8 @@ export class AppListenerBridge { case AppInterface.IPostLivechatRoomTransferred: case AppInterface.IPostLivechatGuestSaved: case AppInterface.IPostLivechatRoomSaved: + case AppInterface.IPostLivechatDepartmentRemoved: + case AppInterface.IPostLivechatDepartmentDisabled: return 'livechatEvent'; case AppInterface.IPostUserCreated: case AppInterface.IPostUserUpdated: @@ -197,6 +199,16 @@ export class AppListenerBridge { .getManager() .getListenerManager() .executeListener(inte, await this.orch.getConverters().get('rooms').convertById(data)); + case AppInterface.IPostLivechatDepartmentDisabled: + return this.orch + .getManager() + .getListenerManager() + .executeListener(inte, await this.orch.getConverters().get('departments').convertDepartment(data)); + case AppInterface.IPostLivechatDepartmentRemoved: + return this.orch + .getManager() + .getListenerManager() + .executeListener(inte, await this.orch.getConverters().get('departments').convertDepartment(data)); default: const room = await this.orch.getConverters().get('rooms').convertRoom(data); diff --git a/apps/meteor/app/livechat/server/lib/departmentsLib.ts b/apps/meteor/app/livechat/server/lib/departmentsLib.ts index 7dec370768f0d..2a9a505064b25 100644 --- a/apps/meteor/app/livechat/server/lib/departmentsLib.ts +++ b/apps/meteor/app/livechat/server/lib/departmentsLib.ts @@ -1,3 +1,4 @@ +import { AppEvents, Apps } from '@rocket.chat/apps'; import type { LivechatDepartmentDTO, ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; import { LivechatDepartment, LivechatDepartmentAgents, LivechatVisitors, LivechatRooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -133,6 +134,10 @@ export async function saveDepartment( // Disable event if (department?.enabled && !departmentDB?.enabled) { await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); + void Apps.self + ?.getBridges() + ?.getListenerBridge() + .livechatEvent(AppEvents.IPostLivechatDepartmentDisabled, { department: departmentDB }); } if (departmentUnit) { @@ -269,7 +274,7 @@ export async function removeDepartment(departmentId: string) { } }); - const { deletedCount } = await removeByDept; + const { deletedCount } = promiseResponses[0].status === 'fulfilled' ? promiseResponses[0].value : { deletedCount: 0 }; if (deletedCount > 0) { removedAgents.forEach(({ _id: docId, agentId }) => { @@ -285,6 +290,7 @@ export async function removeDepartment(departmentId: string) { } await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds: removedAgents.map(({ agentId }) => agentId) }); + void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatDepartmentRemoved, { department }); return ret; } diff --git a/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts b/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts index b94f07ef0250e..c7c554b8376b5 100644 --- a/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts +++ b/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts @@ -1,7 +1,12 @@ import type { IUser } from '../users'; +import type { IDepartment } from './IDepartment'; import type { ILivechatRoom } from './ILivechatRoom'; export interface ILivechatEventContext { agent: IUser; room: ILivechatRoom; } + +export interface ILivechatDepartmentEventContext { + department: IDepartment; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentDisabled.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentDisabled.ts new file mode 100644 index 0000000000000..a08b1d61e446f --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentDisabled.ts @@ -0,0 +1,25 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatDepartmentEventContext } from './ILivechatEventContext'; + +/** + * Handler called after the disablement of a livechat department. + */ +export interface IPostLivechatDepartmentDisabled { + /** + * Handler called *after* the disablement of a livechat department. + * + * @param data the livechat context data which contains the department disabled + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_DISABLED]( + context: ILivechatDepartmentEventContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify?: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentRemoved.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentRemoved.ts new file mode 100644 index 0000000000000..51a08d81eb88e --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatDepartmentRemoved.ts @@ -0,0 +1,25 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatDepartmentEventContext } from './ILivechatEventContext'; + +/** + * Handler called after the removal of a livechat department. + */ +export interface IPostLivechatDepartmentRemoved { + /** + * Handler called *after* the removal of a livechat department. + * + * @param data the livechat context data which contains the department removed + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_REMOVED]( + context: ILivechatDepartmentEventContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify?: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/index.ts b/packages/apps-engine/src/definition/livechat/index.ts index c5045751d5bfd..4533df149fd20 100644 --- a/packages/apps-engine/src/definition/livechat/index.ts +++ b/packages/apps-engine/src/definition/livechat/index.ts @@ -8,6 +8,8 @@ import { ILivechatTransferData } from './ILivechatTransferData'; import { ILivechatTransferEventContext, LivechatTransferEventType } from './ILivechatTransferEventContext'; import { IPostLivechatAgentAssigned } from './IPostLivechatAgentAssigned'; import { IPostLivechatAgentUnassigned } from './IPostLivechatAgentUnassigned'; +import { IPostLivechatDepartmentDisabled } from './IPostLivechatDepartmentDisabled'; +import { IPostLivechatDepartmentRemoved } from './IPostLivechatDepartmentRemoved'; import { IPostLivechatGuestSaved } from './IPostLivechatGuestSaved'; import { IPostLivechatRoomClosed } from './IPostLivechatRoomClosed'; import { IPostLivechatRoomSaved } from './IPostLivechatRoomSaved'; @@ -39,4 +41,6 @@ export { IVisitorEmail, IVisitorPhone, LivechatTransferEventType, + IPostLivechatDepartmentRemoved, + IPostLivechatDepartmentDisabled, }; diff --git a/packages/apps-engine/src/definition/metadata/AppInterface.ts b/packages/apps-engine/src/definition/metadata/AppInterface.ts index 6c599bf56c153..79e73d26679da 100644 --- a/packages/apps-engine/src/definition/metadata/AppInterface.ts +++ b/packages/apps-engine/src/definition/metadata/AppInterface.ts @@ -49,6 +49,8 @@ export enum AppInterface { IPostLivechatRoomTransferred = 'IPostLivechatRoomTransferred', IPostLivechatGuestSaved = 'IPostLivechatGuestSaved', IPostLivechatRoomSaved = 'IPostLivechatRoomSaved', + IPostLivechatDepartmentRemoved = 'IPostLivechatDepartmentRemoved', + IPostLivechatDepartmentDisabled = 'IPostLivechatDepartmentDisabled', // FileUpload IPreFileUpload = 'IPreFileUpload', // Email diff --git a/packages/apps-engine/src/definition/metadata/AppMethod.ts b/packages/apps-engine/src/definition/metadata/AppMethod.ts index 50c4cde74f0d4..67e0611290f23 100644 --- a/packages/apps-engine/src/definition/metadata/AppMethod.ts +++ b/packages/apps-engine/src/definition/metadata/AppMethod.ts @@ -90,6 +90,8 @@ export enum AppMethod { EXECUTE_POST_LIVECHAT_ROOM_TRANSFERRED = 'executePostLivechatRoomTransferred', EXECUTE_POST_LIVECHAT_GUEST_SAVED = 'executePostLivechatGuestSaved', EXECUTE_POST_LIVECHAT_ROOM_SAVED = 'executePostLivechatRoomSaved', + EXECUTE_POST_LIVECHAT_DEPARTMENT_DISABLED = 'executePostLivechatDepartmentDisabled', + EXECUTE_POST_LIVECHAT_DEPARTMENT_REMOVED = 'executePostLivechatDepartmentRemoved', // FileUpload EXECUTE_PRE_FILE_UPLOAD = 'executePreFileUpload', // Email diff --git a/packages/apps-engine/src/server/managers/AppListenerManager.ts b/packages/apps-engine/src/server/managers/AppListenerManager.ts index 882a1fdc9d944..5236223be2987 100644 --- a/packages/apps-engine/src/server/managers/AppListenerManager.ts +++ b/packages/apps-engine/src/server/managers/AppListenerManager.ts @@ -3,6 +3,7 @@ import type { IEmailDescriptor, IPreEmailSentContext } from '../../definition/em import { EssentialAppDisabledException } from '../../definition/exceptions'; import type { IExternalComponent } from '../../definition/externalComponent'; import type { ILivechatEventContext, ILivechatRoom, ILivechatTransferEventContext, IVisitor } from '../../definition/livechat'; +import type { ILivechatDepartmentEventContext } from '../../definition/livechat/ILivechatEventContext'; import type { IMessage, IMessageDeleteContext, @@ -194,6 +195,14 @@ interface IListenerExecutor { args: [IVisitor]; result: void; }; + [AppInterface.IPostLivechatDepartmentRemoved]: { + args: [ILivechatDepartmentEventContext]; + result: void; + }; + [AppInterface.IPostLivechatDepartmentDisabled]: { + args: [ILivechatDepartmentEventContext]; + result: void; + }; // FileUpload [AppInterface.IPreFileUpload]: { args: [IFileUploadContext]; @@ -428,6 +437,10 @@ export class AppListenerManager { return this.executePostLivechatAgentUnassigned(data as ILivechatEventContext); case AppInterface.IPostLivechatRoomTransferred: return this.executePostLivechatRoomTransferred(data as ILivechatTransferEventContext); + case AppInterface.IPostLivechatDepartmentRemoved: + return this.executePostLivechatDepartmentRemoved(data as ILivechatDepartmentEventContext); + case AppInterface.IPostLivechatDepartmentDisabled: + return this.executePostLivechatDepartmentDisabled(data as ILivechatDepartmentEventContext); case AppInterface.IPostLivechatGuestSaved: return this.executePostLivechatGuestSaved(data as IVisitor); // FileUpload @@ -1137,6 +1150,22 @@ export class AppListenerManager { } } + private async executePostLivechatDepartmentRemoved(data: ILivechatDepartmentEventContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentRemoved)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_REMOVED, data); + } + } + + private async executePostLivechatDepartmentDisabled(data: ILivechatDepartmentEventContext): Promise { + for (const appId of this.listeners.get(AppInterface.IPostLivechatDepartmentDisabled)) { + const app = this.manager.getOneById(appId); + + await app.call(AppMethod.EXECUTE_POST_LIVECHAT_DEPARTMENT_DISABLED, data); + } + } + // FileUpload private async executePreFileUpload(data: IFileUploadContext): Promise { for (const appId of this.listeners.get(AppInterface.IPreFileUpload)) { diff --git a/packages/apps/src/bridges/IListenerBridge.ts b/packages/apps/src/bridges/IListenerBridge.ts index 264d86153dfe4..83c3910d152cc 100644 --- a/packages/apps/src/bridges/IListenerBridge.ts +++ b/packages/apps/src/bridges/IListenerBridge.ts @@ -29,7 +29,11 @@ declare module '@rocket.chat/apps-engine/server/bridges' { roomEvent(int: 'IPostRoomCreate' | 'IPostRoomDeleted', room: IRoom): Promise; livechatEvent( - int: 'IPostLivechatAgentAssigned' | 'IPostLivechatAgentUnassigned', + int: + | 'IPostLivechatAgentAssigned' + | 'IPostLivechatAgentUnassigned' + | 'IPostLivechatDepartmentRemoved' + | 'IPostLivechatDepartmentDisabled', data: { user: IUser; room: IOmnichannelRoom }, ): Promise; livechatEvent( From 2db84715997d20e03e8c9bd9c5cb29592790abe0 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 20 Mar 2025 13:59:43 -0300 Subject: [PATCH 005/187] chore: Suspend `SettingsProvider` (#35561) --- .../securityPrivacy/SecurityPrivacyRoute.tsx | 2 +- .../client/providers/SettingsProvider.tsx | 54 +++++-------------- .../meteor/client/sidebar/Sidebar.stories.tsx | 1 - .../client/sidebarv2/Sidebar.stories.tsx | 1 - .../client/views/account/AccountSidebar.tsx | 2 +- .../views/admin/AdministrationRouter.tsx | 4 +- .../AdminFeaturePreviewRoute.tsx | 2 +- .../views/admin/settings/SettingsPage.tsx | 10 ++-- .../views/admin/sidebar/AdminSidebar.tsx | 2 +- .../views/marketplace/MarketplaceSidebar.tsx | 2 +- .../sidebar/OmnichannelSidebar.tsx | 2 +- .../src/MockedAppRootBuilder.tsx | 1 - .../src/MockedSettingsContext.tsx | 1 - .../src/components/Header/Header.stories.tsx | 1 - .../components/HeaderV2/Header.stories.tsx | 1 - packages/ui-contexts/src/SettingsContext.ts | 2 - .../src/hooks/useIsSettingsContextLoading.ts | 5 -- packages/ui-contexts/src/index.ts | 1 - 18 files changed, 24 insertions(+), 70 deletions(-) delete mode 100644 packages/ui-contexts/src/hooks/useIsSettingsContextLoading.ts diff --git a/apps/meteor/client/omnichannel/securityPrivacy/SecurityPrivacyRoute.tsx b/apps/meteor/client/omnichannel/securityPrivacy/SecurityPrivacyRoute.tsx index cef7a6200f4fe..412d7e7e631e7 100644 --- a/apps/meteor/client/omnichannel/securityPrivacy/SecurityPrivacyRoute.tsx +++ b/apps/meteor/client/omnichannel/securityPrivacy/SecurityPrivacyRoute.tsx @@ -4,7 +4,7 @@ import EditableSettingsProvider from '../../views/admin/settings/EditableSetting const SecurityPrivacyRoute = () => { return ( - + diff --git a/apps/meteor/client/providers/SettingsProvider.tsx b/apps/meteor/client/providers/SettingsProvider.tsx index b426b283c74aa..06bce25bc7481 100644 --- a/apps/meteor/client/providers/SettingsProvider.tsx +++ b/apps/meteor/client/providers/SettingsProvider.tsx @@ -4,52 +4,30 @@ import { SettingsContext, useAtLeastOnePermission, useMethod } from '@rocket.cha import { useQueryClient } from '@tanstack/react-query'; import { Tracker } from 'meteor/tracker'; import type { ReactNode } from 'react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { createReactiveSubscriptionFactory } from '../lib/createReactiveSubscriptionFactory'; import { PrivateSettingsCachedCollection } from '../lib/settings/PrivateSettingsCachedCollection'; import { PublicSettingsCachedCollection } from '../lib/settings/PublicSettingsCachedCollection'; +const settingsManagementPermissions = ['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']; + type SettingsProviderProps = { children?: ReactNode; - privileged?: boolean; }; -const SettingsProvider = ({ children, privileged = false }: SettingsProviderProps) => { - const hasPrivilegedPermission = useAtLeastOnePermission( - useMemo(() => ['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings'], []), - ); - - const hasPrivateAccess = privileged && hasPrivilegedPermission; - - const cachedCollection = useMemo( - () => (hasPrivateAccess ? PrivateSettingsCachedCollection : PublicSettingsCachedCollection), - [hasPrivateAccess], - ); - - const [isLoading, setLoading] = useState(() => Tracker.nonreactive(() => !cachedCollection.ready.get())); - - useEffect(() => { - let mounted = true; - - const initialize = async (): Promise => { - if (!Tracker.nonreactive(() => cachedCollection.ready.get())) { - await cachedCollection.init(); - } +const SettingsProvider = ({ children }: SettingsProviderProps) => { + const canManageSettings = useAtLeastOnePermission(settingsManagementPermissions); - if (!mounted) { - return; - } + const cachedCollection = canManageSettings ? PrivateSettingsCachedCollection : PublicSettingsCachedCollection; - setLoading(false); - }; + const isLoading = Tracker.nonreactive(() => !cachedCollection.ready.get()); - initialize(); - - return (): void => { - mounted = false; - }; - }, [cachedCollection]); + if (isLoading) { + throw (async () => { + await cachedCollection.init(); + })(); + } const querySetting = useMemo( () => @@ -108,19 +86,15 @@ const SettingsProvider = ({ children, privileged = false }: SettingsProviderProp const contextValue = useMemo( () => ({ - hasPrivateAccess, - isLoading, + hasPrivateAccess: canManageSettings, querySetting, querySettings, dispatch, }), - [hasPrivateAccess, isLoading, querySetting, querySettings, dispatch], + [canManageSettings, querySetting, querySettings, dispatch], ); return ; }; export default SettingsProvider; - -// '[subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => {}]' -// '[subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting | undefined]' diff --git a/apps/meteor/client/sidebar/Sidebar.stories.tsx b/apps/meteor/client/sidebar/Sidebar.stories.tsx index 944bd45943bb3..61b173a754ad1 100644 --- a/apps/meteor/client/sidebar/Sidebar.stories.tsx +++ b/apps/meteor/client/sidebar/Sidebar.stories.tsx @@ -31,7 +31,6 @@ const settings: Record = { const settingContextValue: ContextType = { hasPrivateAccess: true, - isLoading: false, querySetting: (_id) => [() => () => undefined, () => settings[_id]], querySettings: () => [() => () => undefined, () => []], dispatch: async () => undefined, diff --git a/apps/meteor/client/sidebarv2/Sidebar.stories.tsx b/apps/meteor/client/sidebarv2/Sidebar.stories.tsx index 561ca0aba8ac1..fcf7037c518cb 100644 --- a/apps/meteor/client/sidebarv2/Sidebar.stories.tsx +++ b/apps/meteor/client/sidebarv2/Sidebar.stories.tsx @@ -31,7 +31,6 @@ const settings: Record = { const settingContextValue: ContextType = { hasPrivateAccess: true, - isLoading: false, querySetting: (_id) => [() => () => undefined, () => settings[_id]], querySettings: () => [() => () => undefined, () => []], dispatch: async () => undefined, diff --git a/apps/meteor/client/views/account/AccountSidebar.tsx b/apps/meteor/client/views/account/AccountSidebar.tsx index b6d065fe3683b..a651061f7a41f 100644 --- a/apps/meteor/client/views/account/AccountSidebar.tsx +++ b/apps/meteor/client/views/account/AccountSidebar.tsx @@ -16,7 +16,7 @@ const AccountSidebar = () => { // TODO: uplift this provider return ( - + diff --git a/apps/meteor/client/views/admin/AdministrationRouter.tsx b/apps/meteor/client/views/admin/AdministrationRouter.tsx index 90a93b74fefb0..d1adf66c8cb7f 100644 --- a/apps/meteor/client/views/admin/AdministrationRouter.tsx +++ b/apps/meteor/client/views/admin/AdministrationRouter.tsx @@ -49,9 +49,7 @@ const AdministrationRouter = ({ children }: AdministrationRouterProps): ReactEle return ( - - {children ? }>{children} : } - + {children ? }>{children} : } ); }; diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx index 70db47fcd1e12..3dcecb0c72bb6 100644 --- a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx @@ -15,7 +15,7 @@ const AdminFeaturePreviewRoute = (): ReactElement => { } return ( - + diff --git a/apps/meteor/client/views/admin/settings/SettingsPage.tsx b/apps/meteor/client/views/admin/settings/SettingsPage.tsx index 5bc6acb7160de..abcf72e76bff4 100644 --- a/apps/meteor/client/views/admin/settings/SettingsPage.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsPage.tsx @@ -1,7 +1,6 @@ -import { Icon, SearchInput, Skeleton, CardGrid } from '@rocket.chat/fuselage'; +import { Icon, SearchInput, CardGrid } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useIsSettingsContextLoading } from '@rocket.chat/ui-contexts'; import type { ChangeEvent, ReactElement } from 'react'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,7 +17,6 @@ const SettingsPage = (): ReactElement => { const handleChange = useCallback((e: ChangeEvent) => setFilter(e.currentTarget.value), []); const groups = useSettingsGroups(useDebouncedValue(filter, 400)); - const isLoadingGroups = useIsSettingsContextLoading(); return ( @@ -28,7 +26,6 @@ const SettingsPage = (): ReactElement => { - {isLoadingGroups && } { p: 8, }} > - {!isLoadingGroups && - !!groups.length && + {!!groups.length && groups.map((group) => ( { ))} - {!isLoadingGroups && !groups.length && } + {!groups.length && } ); diff --git a/apps/meteor/client/views/admin/sidebar/AdminSidebar.tsx b/apps/meteor/client/views/admin/sidebar/AdminSidebar.tsx index 59b3c611f1af3..1bcc7161c067a 100644 --- a/apps/meteor/client/views/admin/sidebar/AdminSidebar.tsx +++ b/apps/meteor/client/views/admin/sidebar/AdminSidebar.tsx @@ -15,7 +15,7 @@ const AdminSidebar = () => { // TODO: uplift this provider return ( - + { const currentPath = useCurrentRoutePath(); return ( - + diff --git a/apps/meteor/client/views/omnichannel/sidebar/OmnichannelSidebar.tsx b/apps/meteor/client/views/omnichannel/sidebar/OmnichannelSidebar.tsx index 51c3448b616b0..cae285427cac5 100644 --- a/apps/meteor/client/views/omnichannel/sidebar/OmnichannelSidebar.tsx +++ b/apps/meteor/client/views/omnichannel/sidebar/OmnichannelSidebar.tsx @@ -15,7 +15,7 @@ const OmnichannelSidebar = () => { const currentPath = useCurrentRoutePath(); return ( - + diff --git a/packages/mock-providers/src/MockedAppRootBuilder.tsx b/packages/mock-providers/src/MockedAppRootBuilder.tsx index 157679db3d24a..beca549f6218d 100644 --- a/packages/mock-providers/src/MockedAppRootBuilder.tsx +++ b/packages/mock-providers/src/MockedAppRootBuilder.tsx @@ -104,7 +104,6 @@ export class MockedAppRootBuilder { private settings: Mutable> = { hasPrivateAccess: true, - isLoading: false, querySetting: (_id: string) => [() => () => undefined, () => undefined], querySettings: (_query: SettingsContextQuery) => [() => () => undefined, () => empty as unknown as ISetting[]], dispatch: async () => undefined, diff --git a/packages/mock-providers/src/MockedSettingsContext.tsx b/packages/mock-providers/src/MockedSettingsContext.tsx index 86414992e9e75..eb1181ce5dbc7 100644 --- a/packages/mock-providers/src/MockedSettingsContext.tsx +++ b/packages/mock-providers/src/MockedSettingsContext.tsx @@ -4,7 +4,6 @@ import type { ContextType, ReactNode } from 'react'; const settingContextValue: ContextType = { hasPrivateAccess: true, - isLoading: false, querySetting: (_id: string) => [() => () => undefined, () => undefined], querySettings: () => [() => () => undefined, () => []], dispatch: async () => undefined, diff --git a/packages/ui-client/src/components/Header/Header.stories.tsx b/packages/ui-client/src/components/Header/Header.stories.tsx index e811675b68c9b..cc129f191601a 100644 --- a/packages/ui-client/src/components/Header/Header.stories.tsx +++ b/packages/ui-client/src/components/Header/Header.stories.tsx @@ -40,7 +40,6 @@ export default { [ () => () => undefined, () => ({ diff --git a/packages/ui-client/src/components/HeaderV2/Header.stories.tsx b/packages/ui-client/src/components/HeaderV2/Header.stories.tsx index bf91e16a7be4b..6121f306ea103 100644 --- a/packages/ui-client/src/components/HeaderV2/Header.stories.tsx +++ b/packages/ui-client/src/components/HeaderV2/Header.stories.tsx @@ -41,7 +41,6 @@ export default { [ () => () => undefined, () => ({ diff --git a/packages/ui-contexts/src/SettingsContext.ts b/packages/ui-contexts/src/SettingsContext.ts index f0a005344d468..a4f11b3235f6a 100644 --- a/packages/ui-contexts/src/SettingsContext.ts +++ b/packages/ui-contexts/src/SettingsContext.ts @@ -10,7 +10,6 @@ export type SettingsContextQuery = { export type SettingsContextValue = { readonly hasPrivateAccess: boolean; - readonly isLoading: boolean; readonly querySetting: ( _id: ISetting['_id'], ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting | undefined]; @@ -22,7 +21,6 @@ export type SettingsContextValue = { export const SettingsContext = createContext({ hasPrivateAccess: false, - isLoading: false, querySetting: () => [(): (() => void) => (): void => undefined, (): undefined => undefined], querySettings: () => [(): (() => void) => (): void => undefined, (): ISetting[] => []], dispatch: async () => undefined, diff --git a/packages/ui-contexts/src/hooks/useIsSettingsContextLoading.ts b/packages/ui-contexts/src/hooks/useIsSettingsContextLoading.ts deleted file mode 100644 index fe5155e5abbfb..0000000000000 --- a/packages/ui-contexts/src/hooks/useIsSettingsContextLoading.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { useContext } from 'react'; - -import { SettingsContext } from '../SettingsContext'; - -export const useIsSettingsContextLoading = (): boolean => useContext(SettingsContext).isLoading; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index 313710bbfd4ef..dfcd7ca301455 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -33,7 +33,6 @@ export { useEndpoint } from './hooks/useEndpoint'; export { useGoToRoom } from './hooks/useGoToRoom'; export type { EndpointFunction } from './hooks/useEndpoint'; export { useIsPrivilegedSettingsContext } from './hooks/useIsPrivilegedSettingsContext'; -export { useIsSettingsContextLoading } from './hooks/useIsSettingsContextLoading'; export { useLanguage } from './hooks/useLanguage'; export { useLanguages } from './hooks/useLanguages'; export { useLayout } from './hooks/useLayout'; From 21048fb56e59f4a408d6873860f648818669b3ea Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 20 Mar 2025 14:08:16 -0300 Subject: [PATCH 006/187] chore: Subclasses of `CachedCollection` (#35556) --- .../models/client/models/CachedChatRoom.ts | 9 ++- .../client/models/CachedChatSubscription.ts | 9 ++- .../app/models/client/models/Permissions.ts | 4 +- .../lib/cachedCollections/CachedCollection.ts | 62 ++++++++++--------- .../client/lib/cachedCollections/index.ts | 2 +- .../PrivateSettingsCachedCollection.ts | 4 +- .../PublicSettingsCachedCollection.ts | 5 +- 7 files changed, 51 insertions(+), 44 deletions(-) diff --git a/apps/meteor/app/models/client/models/CachedChatRoom.ts b/apps/meteor/app/models/client/models/CachedChatRoom.ts index 784503a1b0afc..ce09e9da0ee58 100644 --- a/apps/meteor/app/models/client/models/CachedChatRoom.ts +++ b/apps/meteor/app/models/client/models/CachedChatRoom.ts @@ -2,11 +2,14 @@ import type { IOmnichannelRoom, IRoom, IRoomWithRetentionPolicy } from '@rocket. import { DEFAULT_SLA_CONFIG, LivechatPriorityWeight } from '@rocket.chat/core-typings'; import { CachedChatSubscription } from './CachedChatSubscription'; -import { CachedCollection } from '../../../../client/lib/cachedCollections/CachedCollection'; +import { PrivateCachedCollection } from '../../../../client/lib/cachedCollections/CachedCollection'; -class CachedChatRoom extends CachedCollection { +class CachedChatRoom extends PrivateCachedCollection { constructor() { - super({ name: 'rooms' }); + super({ + name: 'rooms', + eventType: 'notify-user', + }); } protected handleLoadFromServer(record: IRoom) { diff --git a/apps/meteor/app/models/client/models/CachedChatSubscription.ts b/apps/meteor/app/models/client/models/CachedChatSubscription.ts index 28b55a70197d4..077d3107b3e0f 100644 --- a/apps/meteor/app/models/client/models/CachedChatSubscription.ts +++ b/apps/meteor/app/models/client/models/CachedChatSubscription.ts @@ -3,7 +3,7 @@ import { DEFAULT_SLA_CONFIG, LivechatPriorityWeight } from '@rocket.chat/core-ty import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; import { CachedChatRoom } from './CachedChatRoom'; -import { CachedCollection } from '../../../../client/lib/cachedCollections/CachedCollection'; +import { PrivateCachedCollection } from '../../../../client/lib/cachedCollections/CachedCollection'; declare module '@rocket.chat/core-typings' { interface ISubscription { @@ -12,9 +12,12 @@ declare module '@rocket.chat/core-typings' { } } -class CachedChatSubscription extends CachedCollection { +class CachedChatSubscription extends PrivateCachedCollection { constructor() { - super({ name: 'subscriptions' }); + super({ + name: 'subscriptions', + eventType: 'notify-user', + }); } protected handleLoadFromServer(record: ISubscription) { diff --git a/apps/meteor/app/models/client/models/Permissions.ts b/apps/meteor/app/models/client/models/Permissions.ts index 5793ab3e897de..18898d07e0b9f 100644 --- a/apps/meteor/app/models/client/models/Permissions.ts +++ b/apps/meteor/app/models/client/models/Permissions.ts @@ -1,8 +1,8 @@ import type { IPermission } from '@rocket.chat/core-typings'; -import { CachedCollection } from '../../../../client/lib/cachedCollections'; +import { PrivateCachedCollection } from '../../../../client/lib/cachedCollections'; -export const AuthzCachedCollection = new CachedCollection({ +export const AuthzCachedCollection = new PrivateCachedCollection({ name: 'permissions', eventType: 'notify-logged', }); diff --git a/apps/meteor/client/lib/cachedCollections/CachedCollection.ts b/apps/meteor/client/lib/cachedCollections/CachedCollection.ts index 9e9e5107d3f2e..c3903ff532c63 100644 --- a/apps/meteor/client/lib/cachedCollections/CachedCollection.ts +++ b/apps/meteor/client/lib/cachedCollections/CachedCollection.ts @@ -36,8 +36,8 @@ const hasUnserializedUpdatedAt = (record: T): record is T & { _updatedAt: Con localforage.config({ name: baseURI }); -export class CachedCollection { - private static MAX_CACHE_TIME = 60 * 60 * 24 * 30; +export abstract class CachedCollection { + private static readonly MAX_CACHE_TIME = 60 * 60 * 24 * 30; public collection: MinimongoCollection; @@ -47,22 +47,19 @@ export class CachedCollection { protected eventType: StreamNames; - protected version = 18; + private readonly version = 18; - protected userRelated: boolean; - - protected updatedAt = new Date(0); + private updatedAt = new Date(0); protected log: (...args: any[]) => void; private timer: ReturnType; - constructor({ name, eventType = 'notify-user', userRelated = true }: { name: Name; eventType?: StreamNames; userRelated?: boolean }) { + constructor({ name, eventType }: { name: Name; eventType: StreamNames }) { this.collection = new Mongo.Collection(null) as MinimongoCollection; this.name = name; this.eventType = eventType; - this.userRelated = userRelated; this.log = [getConfig(`debugCachedCollection-${this.name}`), getConfig('debugCachedCollection'), getConfig('debug')].includes('true') ? console.log.bind(console, `%cCachedCollection ${this.name}`, `color: navy; font-weight: bold;`) @@ -78,13 +75,7 @@ export class CachedCollection { return `${this.name}-changed`; } - getToken() { - if (this.userRelated === false) { - return undefined; - } - - return Accounts._storedLoginToken(); - } + protected abstract getToken(): unknown; private async loadFromCache() { const data = await localforage.getItem<{ version: number; token: unknown; records: unknown[]; updatedAt: Date | string }>(this.name); @@ -195,7 +186,7 @@ export class CachedCollection { await this.save(); } - save = withDebouncing({ wait: 1000 })(async () => { + private save = withDebouncing({ wait: 1000 })(async () => { this.log('saving cache'); const data = this.collection.find().fetch(); await localforage.setItem(this.name, { @@ -207,19 +198,15 @@ export class CachedCollection { this.log('saving cache (done)'); }); - clearCacheOnLogout() { - if (this.userRelated === true) { - void this.clearCache(); - } - } + abstract clearCacheOnLogout(): void; - async clearCache() { + protected async clearCache() { this.log('clearing cache'); await localforage.removeItem(this.name); this.collection.remove({}); } - async setupListener() { + protected async setupListener() { sdk.stream(this.eventType, [this.eventName], (async (action: 'removed' | 'changed', record: any) => { this.log('record received', action, record); await this.handleRecordEvent(action, record); @@ -245,7 +232,7 @@ export class CachedCollection { await this.save(); } - trySync(delay = 10) { + private trySync(delay = 10) { clearTimeout(this.timer); // Wait for an empty queue to load data again and sync this.timer = setTimeout(async () => { @@ -256,7 +243,7 @@ export class CachedCollection { }, delay); } - async sync() { + protected async sync() { if (!this.updatedAt || this.updatedAt.getTime() === 0 || Meteor.connection._outstandingMethodBlocks.length !== 0) { return false; } @@ -355,13 +342,28 @@ export class CachedCollection { } private reconnectionComputation: Tracker.Computation | undefined; +} - listen() { - if (!this.userRelated) { - void this.init(); - return; - } +export class PublicCachedCollection extends CachedCollection { + protected getToken() { + return undefined; + } + clearCacheOnLogout() { + // do nothing + } +} + +export class PrivateCachedCollection extends CachedCollection { + protected getToken() { + return Accounts._storedLoginToken(); + } + + clearCacheOnLogout() { + void this.clearCache(); + } + + listen() { if (process.env.NODE_ENV === 'test') { return; } diff --git a/apps/meteor/client/lib/cachedCollections/index.ts b/apps/meteor/client/lib/cachedCollections/index.ts index fb99c0d3feead..849e96c1cf5b2 100644 --- a/apps/meteor/client/lib/cachedCollections/index.ts +++ b/apps/meteor/client/lib/cachedCollections/index.ts @@ -1,2 +1,2 @@ -export { CachedCollection } from './CachedCollection'; +export { PrivateCachedCollection, PublicCachedCollection } from './CachedCollection'; export { CachedCollectionManager } from './CachedCollectionManager'; diff --git a/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts b/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts index 1b170c82e7d87..74cb76ac9002a 100644 --- a/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts +++ b/apps/meteor/client/lib/settings/PrivateSettingsCachedCollection.ts @@ -1,9 +1,9 @@ import type { ISetting } from '@rocket.chat/core-typings'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { CachedCollection } from '../cachedCollections'; +import { PrivateCachedCollection } from '../cachedCollections'; -class PrivateSettingsCachedCollection extends CachedCollection { +class PrivateSettingsCachedCollection extends PrivateCachedCollection { constructor() { super({ name: 'private-settings', diff --git a/apps/meteor/client/lib/settings/PublicSettingsCachedCollection.ts b/apps/meteor/client/lib/settings/PublicSettingsCachedCollection.ts index 6d01d13d96175..bc12c13997c22 100644 --- a/apps/meteor/client/lib/settings/PublicSettingsCachedCollection.ts +++ b/apps/meteor/client/lib/settings/PublicSettingsCachedCollection.ts @@ -1,13 +1,12 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import { CachedCollection } from '../cachedCollections'; +import { PublicCachedCollection } from '../cachedCollections/CachedCollection'; -class PublicSettingsCachedCollection extends CachedCollection { +class PublicSettingsCachedCollection extends PublicCachedCollection { constructor() { super({ name: 'public-settings', eventType: 'notify-all', - userRelated: false, }); } } From 11d642c7686b6004e6915452ad6a2c098b5d88a0 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 20 Mar 2025 14:09:38 -0300 Subject: [PATCH 007/187] chore: Purge unused model (#35560) --- apps/meteor/app/models/client/models/Base.ts | 65 -------------------- 1 file changed, 65 deletions(-) delete mode 100644 apps/meteor/app/models/client/models/Base.ts diff --git a/apps/meteor/app/models/client/models/Base.ts b/apps/meteor/app/models/client/models/Base.ts deleted file mode 100644 index 68f16d110d00c..0000000000000 --- a/apps/meteor/app/models/client/models/Base.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { check } from 'meteor/check'; -import { Mongo } from 'meteor/mongo'; -import type { Document } from 'mongodb'; - -export abstract class Base { - private model: Mongo.Collection; - - protected _baseName() { - return 'rocketchat_'; - } - - protected _initModel(name: string) { - check(name, String); - this.model = new Mongo.Collection(this._baseName() + name); - return this.model; - } - - find(...args: Parameters['find']>) { - return this.model.find(...args); - } - - findOne(...args: Parameters['findOne']>) { - return this.model.findOne(...args); - } - - insert(...args: Parameters['insert']>) { - return this.model.insert(...args); - } - - update(...args: Parameters['update']>) { - return this.model.update(...args); - } - - upsert(...args: Parameters['upsert']>) { - return this.model.upsert(...args); - } - - remove(...args: Parameters['remove']>) { - return this.model.remove(...args); - } - - allow(...args: Parameters['allow']>) { - return this.model.allow(...args); - } - - deny(...args: Parameters['deny']>) { - return this.model.deny(...args); - } - - ensureIndex() { - // do nothing - } - - dropIndex() { - // do nothing - } - - tryEnsureIndex() { - // do nothing - } - - tryDropIndex() { - // do nothing - } -} From 8606cf601a9602ba4bc77a50e69475b44b853e72 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 20 Mar 2025 14:17:48 -0300 Subject: [PATCH 008/187] chore: Get rid of `CannedResponse` collection (#35562) --- .../client/collections/CannedResponse.ts | 7 -- .../app/canned-responses/client/index.ts | 1 - .../client/startup/responses.ts | 51 --------------- apps/meteor/client/importPackages.ts | 1 - apps/meteor/client/lib/queryKeys.ts | 4 ++ .../room/providers/ComposerPopupProvider.tsx | 36 ++++++----- .../hooks/useCannedResponsesQuery.ts | 64 +++++++++++++++++++ 7 files changed, 87 insertions(+), 77 deletions(-) delete mode 100644 apps/meteor/app/canned-responses/client/collections/CannedResponse.ts delete mode 100644 apps/meteor/app/canned-responses/client/index.ts delete mode 100644 apps/meteor/app/canned-responses/client/startup/responses.ts create mode 100644 apps/meteor/client/views/room/providers/hooks/useCannedResponsesQuery.ts diff --git a/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts b/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts deleted file mode 100644 index 6ea1f70e9897e..0000000000000 --- a/apps/meteor/app/canned-responses/client/collections/CannedResponse.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Mongo } from 'meteor/mongo'; - -export const CannedResponse = new Mongo.Collection<{ - _id: string; - shortcut: string; - text: string; -}>(null); diff --git a/apps/meteor/app/canned-responses/client/index.ts b/apps/meteor/app/canned-responses/client/index.ts deleted file mode 100644 index 36fd7b5cc6af9..0000000000000 --- a/apps/meteor/app/canned-responses/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './startup/responses'; diff --git a/apps/meteor/app/canned-responses/client/startup/responses.ts b/apps/meteor/app/canned-responses/client/startup/responses.ts deleted file mode 100644 index 6d761adb890da..0000000000000 --- a/apps/meteor/app/canned-responses/client/startup/responses.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { hasPermission } from '../../../authorization/client'; -import { settings } from '../../../settings/client'; -import { sdk } from '../../../utils/client/lib/SDKClient'; -import { CannedResponse } from '../collections/CannedResponse'; - -Meteor.startup(() => { - Tracker.autorun(async (c) => { - if (!Meteor.userId()) { - return; - } - if (!settings.get('Canned_Responses_Enable')) { - return; - } - if (!hasPermission('view-canned-responses')) { - return; - } - Tracker.afterFlush(() => { - try { - // TODO: check options - sdk.stream('canned-responses', ['canned-responses'], (...[response, options]) => { - const { agentsId } = options || {}; - if (Array.isArray(agentsId) && !agentsId.includes(Meteor.userId())) { - return; - } - - switch (response.type) { - case 'changed': { - const { type, ...fields } = response; - CannedResponse.upsert({ _id: response._id }, fields); - break; - } - - case 'removed': { - CannedResponse.remove({ _id: response._id }); - break; - } - } - }); - } catch (error) { - console.log(error); - } - }); - - const { responses } = await sdk.rest.get('/v1/canned-responses.get'); - responses.forEach((response) => CannedResponse.insert(response)); - c.stop(); - }); -}); diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index ad7f06e15603a..d47cc47eae4d7 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -1,7 +1,6 @@ import '../app/apple/client'; import '../app/authorization/client'; import '../app/autotranslate/client'; -import '../app/canned-responses/client'; import '../app/custom-sounds/client'; import '../app/emoji/client'; import '../app/emoji-emojione/client'; diff --git a/apps/meteor/client/lib/queryKeys.ts b/apps/meteor/client/lib/queryKeys.ts index a9217935b8219..ad15b225ab88d 100644 --- a/apps/meteor/client/lib/queryKeys.ts +++ b/apps/meteor/client/lib/queryKeys.ts @@ -14,3 +14,7 @@ export const subscriptionsQueryKeys = { all: ['subscriptions'] as const, subscription: (rid: IRoom['_id']) => [...subscriptionsQueryKeys.all, { rid }] as const, }; + +export const cannedResponsesQueryKeys = { + all: ['canned-responses'] as const, +}; diff --git a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx index 22557f4a6d15b..6293a401da8b0 100644 --- a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx +++ b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx @@ -3,16 +3,17 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { useMethod, useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import type { ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { hasAtLeastOnePermission } from '../../../../app/authorization/client'; -import { CannedResponse } from '../../../../app/canned-responses/client/collections/CannedResponse'; import { emoji } from '../../../../app/emoji/client'; import { Subscriptions } from '../../../../app/models/client'; import { usersFromRoomMessages } from '../../../../app/ui-message/client/popup/messagePopupConfig'; import { slashCommands } from '../../../../app/utils/client'; +import { cannedResponsesQueryKeys } from '../../../lib/queryKeys'; import ComposerBoxPopupCannedResponse from '../composer/ComposerBoxPopupCannedResponse'; import type { ComposerBoxPopupEmojiProps } from '../composer/ComposerBoxPopupEmoji'; import ComposerBoxPopupEmoji from '../composer/ComposerBoxPopupEmoji'; @@ -24,6 +25,9 @@ import ComposerBoxPopupUser from '../composer/ComposerBoxPopupUser'; import type { ComposerBoxPopupUserProps } from '../composer/ComposerBoxPopupUser'; import type { ComposerPopupContextValue } from '../contexts/ComposerPopupContext'; import { ComposerPopupContext, createMessageBoxPopupConfig } from '../contexts/ComposerPopupContext'; +import useCannedResponsesQuery from './hooks/useCannedResponsesQuery'; + +export type CannedResponse = { _id: string; shortcut: string; text: string }; type ComposerPopupProviderProps = { children: ReactNode; @@ -32,6 +36,11 @@ type ComposerPopupProviderProps = { const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) => { const { _id: rid, encrypted: isRoomEncrypted } = room; + + // TODO: this is awful because we are just triggering the query to get the data + // and we are not using the data itself, we should find a better way to do this + useCannedResponsesQuery(room); + const userSpotlight = useMethod('spotlight'); const suggestionsCount = useSetting('Number_of_users_autocomplete_suggestions', 5); const cannedResponseEnabled = useSetting('Canned_Responses_Enable', true); @@ -43,6 +52,7 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = const e2eEnabled = useSetting('E2E_Enable', false); const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages', false); const encrypted = isRoomEncrypted && e2eEnabled && !unencryptedMessagesAllowed; + const queryClient = useQueryClient(); const call = useMethod('getSlashCommandPreviews'); const value: ComposerPopupContextValue = useMemo(() => { @@ -334,18 +344,12 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = renderItem: ({ item }) => , getItemsFromLocal: async (filter: string) => { const exp = new RegExp(filter, 'i'); - return CannedResponse.find( - { - shortcut: exp, - }, - { - limit: 12, - sort: { - shortcut: -1, - }, - }, - ) - .fetch() + // TODO: this is bad, but can only be fixed by refactoring the whole thing + const cannedResponses = queryClient.getQueryData(cannedResponsesQueryKeys.all) ?? []; + return cannedResponses + .filter((record) => record.shortcut.match(exp)) + .sort((a, b) => a.shortcut.localeCompare(b.shortcut)) + .slice(0, 11) .map((record) => ({ _id: record._id, text: record.text, @@ -353,9 +357,7 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = })); }, getItemsFromServer: async () => [], - getValue: (item) => { - return item.text; - }, + getValue: (item) => item.text, }), createMessageBoxPopupConfig({ title: previewTitle, @@ -388,8 +390,8 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = rid, recentEmojis, i18n, + queryClient, call, - setPreviewTitle, ]); return ; diff --git a/apps/meteor/client/views/room/providers/hooks/useCannedResponsesQuery.ts b/apps/meteor/client/views/room/providers/hooks/useCannedResponsesQuery.ts new file mode 100644 index 0000000000000..0150ecdf9ae50 --- /dev/null +++ b/apps/meteor/client/views/room/providers/hooks/useCannedResponsesQuery.ts @@ -0,0 +1,64 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { useEndpoint, usePermission, useSetting, useStream, useUserId } from '@rocket.chat/ui-contexts'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { cannedResponsesQueryKeys } from '../../../../lib/queryKeys'; + +type CannedResponse = { _id: string; shortcut: string; text: string }; + +const useCannedResponsesQuery = (room: IRoom) => { + const isOmnichannel = isOmnichannelRoom(room); + const uid = useUserId(); + const isCannedResponsesEnabled = useSetting('Canned_Responses_Enable', true); + const canViewCannedResponses = usePermission('view-canned-responses'); + const subscribeToCannedResponses = useStream('canned-responses'); + + const enabled = isOmnichannel && !!uid && isCannedResponsesEnabled && canViewCannedResponses; + + const getCannedResponses = useEndpoint('GET', '/v1/canned-responses.get'); + + const queryClient = useQueryClient(); + + useEffect(() => { + if (!enabled) return; + + return subscribeToCannedResponses('canned-responses', (...[response, options]) => { + const { agentsId } = options || {}; + if (Array.isArray(agentsId) && !agentsId.includes(uid)) { + return; + } + + switch (response.type) { + case 'changed': { + const { _id, shortcut, text } = response; + queryClient.setQueryData(cannedResponsesQueryKeys.all, (responses) => + responses?.filter((response) => response._id !== _id).concat([{ _id, shortcut, text }]), + ); + break; + } + + case 'removed': { + const { _id } = response; + queryClient.setQueryData(cannedResponsesQueryKeys.all, (responses) => + responses?.filter((response) => response._id !== _id), + ); + break; + } + } + }); + }, [enabled, getCannedResponses, queryClient, subscribeToCannedResponses, uid]); + + return useQuery({ + queryKey: cannedResponsesQueryKeys.all, + queryFn: async () => { + const { responses } = await getCannedResponses(); + return responses.map(({ _id, shortcut, text }) => ({ _id, shortcut, text })); + }, + enabled, + staleTime: Infinity, + }); +}; + +export default useCannedResponsesQuery; From af03a61467c76fd934fdce3ff97ea362a5bbc9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:26:51 -0300 Subject: [PATCH 009/187] refactor: remove KonchatNotification `newRoom` from meteor (#35567) --- .../app/ui/client/lib/KonchatNotification.ts | 27 +------------------ apps/meteor/client/hooks/useNotifyNewRoom.ts | 22 +++++++++++++++ apps/meteor/client/hooks/useNotifyUser.ts | 4 ++- .../client/providers/OmnichannelProvider.tsx | 8 +++--- 4 files changed, 31 insertions(+), 30 deletions(-) create mode 100644 apps/meteor/client/hooks/useNotifyNewRoom.ts diff --git a/apps/meteor/app/ui/client/lib/KonchatNotification.ts b/apps/meteor/app/ui/client/lib/KonchatNotification.ts index 2b463886fdac2..e6a030215ab87 100644 --- a/apps/meteor/app/ui/client/lib/KonchatNotification.ts +++ b/apps/meteor/app/ui/client/lib/KonchatNotification.ts @@ -10,7 +10,7 @@ import { router } from '../../../../client/providers/RouterProvider'; import { stripTags } from '../../../../lib/utils/stringUtils'; import { CustomSounds } from '../../../custom-sounds/client/lib/CustomSounds'; import { e2e } from '../../../e2e/client'; -import { Subscriptions, Users } from '../../../models/client'; +import { Subscriptions } from '../../../models/client'; import { getUserPreference } from '../../../utils/client'; import { getUserAvatarURL } from '../../../utils/client/getUserAvatarURL'; import { getUserNotificationsSoundVolume } from '../../../utils/client/getUserNotificationsSoundVolume'; @@ -206,31 +206,6 @@ class KonchatNotification { // do nothing } } - - public newRoom() { - Tracker.nonreactive(() => { - const uid = Meteor.userId(); - if (!uid) { - return; - } - const user = Users.findOne(uid, { - fields: { - 'settings.preferences.newRoomNotification': 1, - 'settings.preferences.notificationsSoundVolume': 1, - }, - }); - const newRoomNotification = getUserPreference(user, 'newRoomNotification'); - const audioVolume = getUserNotificationsSoundVolume(user?._id); - - if (!newRoomNotification) { - return; - } - - void CustomSounds.play(newRoomNotification, { - volume: Number((audioVolume / 100).toPrecision(2)), - }); - }); - } } const instance = new KonchatNotification(); diff --git a/apps/meteor/client/hooks/useNotifyNewRoom.ts b/apps/meteor/client/hooks/useNotifyNewRoom.ts new file mode 100644 index 0000000000000..0b8eed55f9b4d --- /dev/null +++ b/apps/meteor/client/hooks/useNotifyNewRoom.ts @@ -0,0 +1,22 @@ +import { useUserPreference } from '@rocket.chat/ui-contexts'; +import { useCallback } from 'react'; + +import { useUserSoundPreferences } from './useUserSoundPreferences'; +import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; + +export const useNotifyNewRoom = () => { + const newRoomNotification = useUserPreference('newRoomNotification'); + const { notificationsSoundVolume } = useUserSoundPreferences(); + + const notifyNewRoom = useCallback(() => { + if (!newRoomNotification) { + return; + } + + void CustomSounds.play(newRoomNotification, { + volume: Number((notificationsSoundVolume / 100).toPrecision(2)), + }); + }, [newRoomNotification, notificationsSoundVolume]); + + return notifyNewRoom; +}; diff --git a/apps/meteor/client/hooks/useNotifyUser.ts b/apps/meteor/client/hooks/useNotifyUser.ts index 440c979fed111..2949ad4a14f9d 100644 --- a/apps/meteor/client/hooks/useNotifyUser.ts +++ b/apps/meteor/client/hooks/useNotifyUser.ts @@ -4,6 +4,7 @@ import { useRouter, useStream, useUser, useUserPreference } from '@rocket.chat/u import { useEffect } from 'react'; import { useEmbeddedLayout } from './useEmbeddedLayout'; +import { useNotifyNewRoom } from './useNotifyNewRoom'; import { CachedChatSubscription } from '../../app/models/client'; import { KonchatNotification } from '../../app/ui/client/lib/KonchatNotification'; import { RoomManager } from '../lib/RoomManager'; @@ -15,6 +16,7 @@ export const useNotifyUser = () => { const isLayoutEmbedded = useEmbeddedLayout(); const notifyUserStream = useStream('notify-user'); const muteFocusedConversations = useUserPreference('muteFocusedConversations'); + const newRoomNotification = useNotifyNewRoom(); const notifyNewRoom = useEffectEvent(async (sub: AtLeast): Promise => { if (!user || user.status === 'busy') { @@ -22,7 +24,7 @@ export const useNotifyUser = () => { } if ((!router.getRouteParameters().name || router.getRouteParameters().name !== sub.name) && !sub.ls && sub.alert === true) { - KonchatNotification.newRoom(); + newRoomNotification(); } }); diff --git a/apps/meteor/client/providers/OmnichannelProvider.tsx b/apps/meteor/client/providers/OmnichannelProvider.tsx index ed06c15d68736..00428d6bfcd59 100644 --- a/apps/meteor/client/providers/OmnichannelProvider.tsx +++ b/apps/meteor/client/providers/OmnichannelProvider.tsx @@ -14,11 +14,11 @@ import { useState, useEffect, useMemo, useCallback, memo, useRef } from 'react'; import { LivechatInquiry } from '../../app/livechat/client/collections/LivechatInquiry'; import { initializeLivechatInquiryStream } from '../../app/livechat/client/lib/stream/queueManager'; import { getOmniChatSortQuery } from '../../app/livechat/lib/inquiries'; -import { KonchatNotification } from '../../app/ui/client/lib/KonchatNotification'; import { ClientLogger } from '../../lib/ClientLogger'; import type { OmnichannelContextValue } from '../contexts/OmnichannelContext'; import { OmnichannelContext } from '../contexts/OmnichannelContext'; import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; +import { useNotifyNewRoom } from '../hooks/useNotifyNewRoom'; import { useOmnichannelContinuousSoundNotification } from '../hooks/useOmnichannelContinuousSoundNotification'; import { useReactiveValue } from '../hooks/useReactiveValue'; import { useShouldPreventAction } from '../hooks/useShouldPreventAction'; @@ -76,6 +76,8 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { const isPrioritiesEnabled = isEnterprise && accessible; const enabled = accessible && !!user && !!routeConfig; + const notifyNewRoom = useNotifyNewRoom(); + const { data: { priorities = [] } = {}, isLoading: isLoadingPriorities, @@ -158,10 +160,10 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { useEffect(() => { if (lastQueueSize.current < (queue?.length ?? 0)) { - KonchatNotification.newRoom(); + notifyNewRoom(); } lastQueueSize.current = queue?.length ?? 0; - }, [queue?.length]); + }, [notifyNewRoom, queue?.length]); useOmnichannelContinuousSoundNotification(queue ?? []); From 715b62e48f77a26370d8771680eabe99e0b28e22 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Fri, 21 Mar 2025 01:47:28 +0530 Subject: [PATCH 010/187] fix: 2FA check flashing for mandatory roles (#35340) Co-authored-by: Guilherme Gazzo --- .changeset/chatty-kids-compare.md | 5 ++ .../authentication/server/startup/index.js | 2 +- .../server/functions/getBaseUserFields.ts | 3 +- .../server/functions/getDefaultUserFields.ts | 4 +- apps/meteor/server/startup/initialData.js | 2 +- apps/meteor/tests/e2e/enforce-2FA.spec.ts | 63 +++++++++++++++++++ .../page-objects/fragments/home-sidenav.ts | 4 ++ ee/apps/ddp-streamer/src/DDPStreamer.ts | 33 +++++++++- 8 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 .changeset/chatty-kids-compare.md create mode 100644 apps/meteor/tests/e2e/enforce-2FA.spec.ts diff --git a/.changeset/chatty-kids-compare.md b/.changeset/chatty-kids-compare.md new file mode 100644 index 0000000000000..da889e416ddf5 --- /dev/null +++ b/.changeset/chatty-kids-compare.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where account security page was flashing sometimes for users with mandatory two factor configured. diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index 03c9602e59f00..345aa01e688ea 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -40,7 +40,7 @@ Accounts.config({ * * we are removing the status here because meteor send 'offline' */ -Object.assign(Accounts._defaultPublishFields.projection, (({ status, ...rest }) => rest)(getBaseUserFields())); +Object.assign(Accounts._defaultPublishFields.projection, (({ status, ...rest }) => rest)(getBaseUserFields(true))); Meteor.startup(() => { settings.watchMultiple(['Accounts_LoginExpiration', 'Site_Name', 'From_Email'], () => { diff --git a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts index 5e2a3bf2b4d73..a366f95a2fe54 100644 --- a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts +++ b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts @@ -2,7 +2,7 @@ type UserFields = { [k: string]: number; }; -export const getBaseUserFields = (): UserFields => ({ +export const getBaseUserFields = (allowServiceKeys = false): UserFields => ({ 'name': 1, 'username': 1, 'nickname': 1, @@ -31,4 +31,5 @@ export const getBaseUserFields = (): UserFields => ({ 'avatarETag': 1, 'extension': 1, 'openBusinessHours': 1, + ...(allowServiceKeys && { 'services.totp.enabled': 1, 'services.email2fa.enabled': 1 }), }); diff --git a/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts b/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts index 293eb8607342d..c9d7cb0fc8945 100644 --- a/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts +++ b/apps/meteor/app/utils/server/functions/getDefaultUserFields.ts @@ -5,10 +5,8 @@ type UserFields = { }; export const getDefaultUserFields = (): UserFields => ({ - ...getBaseUserFields(), + ...getBaseUserFields(true), 'services.github': 1, 'services.gitlab': 1, 'services.password.bcrypt': 1, - 'services.totp.enabled': 1, - 'services.email2fa.enabled': 1, }); diff --git a/apps/meteor/server/startup/initialData.js b/apps/meteor/server/startup/initialData.js index 30415e8b20224..5de4aad42aab5 100644 --- a/apps/meteor/server/startup/initialData.js +++ b/apps/meteor/server/startup/initialData.js @@ -217,7 +217,7 @@ Meteor.startup(async () => { emails: [ { address: 'rocketchat.internal.admin.test@rocket.chat', - verified: false, + verified: true, }, ], status: 'offline', diff --git a/apps/meteor/tests/e2e/enforce-2FA.spec.ts b/apps/meteor/tests/e2e/enforce-2FA.spec.ts new file mode 100644 index 0000000000000..cb5671864e3d5 --- /dev/null +++ b/apps/meteor/tests/e2e/enforce-2FA.spec.ts @@ -0,0 +1,63 @@ +import { IS_EE } from './config/constants'; +import { Users } from './fixtures/userStates'; +import { HomeChannel, AccountProfile } from './page-objects'; +import { createCustomRole, deleteCustomRole } from './utils/custom-role'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe('enforce two factor authentication', () => { + test.skip(!IS_EE, 'Enterprise Only'); + + let poHomeChannel: HomeChannel; + let poAccountProfile: AccountProfile; + let customRoleId = ''; + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeChannel(page); + poAccountProfile = new AccountProfile(page); + }); + + test.beforeAll(async ({ api }) => { + const roleResponse = await createCustomRole(api, { name: 'enforce-2FA', mandatory2fa: true }); + expect(roleResponse.status()).toBe(200); + const { role } = await roleResponse.json(); + customRoleId = role._id; + + const userUpdateRes = await api.post('/users.update', { + data: { roles: ['user', customRoleId, 'admin'] }, + userId: 'rocketchat.internal.admin.test', + }); + expect(userUpdateRes.status()).toBe(200); + + const disableEmail2FA = await api.post('/users.2fa.disableEmail', {}); + expect(disableEmail2FA.status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + const userUpdateRes = await api.post('/users.update', { + data: { roles: ['user', 'admin'] }, + userId: 'rocketchat.internal.admin.test', + }); + expect(userUpdateRes.status()).toBe(200); + + const deleteRole = await deleteCustomRole(api, 'enforce-2FA'); + expect(deleteRole.status()).toBe(200); + + const enableEmail2FA = await api.post('/users.2fa.enableEmail', {}); + expect(enableEmail2FA.status()).toBe(200); + }); + + test('should redirect to 2FA setup page and setup email 2FA', async ({ page }) => { + await page.goto('/home'); + await expect(poHomeChannel.sidenav.sidebarHomeAction).not.toBeVisible(); + await expect(poAccountProfile.securityHeader).toBeVisible(); + + await poAccountProfile.security2FASection.click(); + await expect(poAccountProfile.enableEmail2FAButton).toBeVisible(); + await poAccountProfile.enableEmail2FAButton.click(); + + await expect(poHomeChannel.toastSuccess).toBeVisible(); + await expect(poHomeChannel.sidenav.sidebarHomeAction).toBeVisible(); + await expect(poAccountProfile.securityHeader).not.toBeVisible(); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index 60c039cbe9530..bf2eefeb11a37 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -57,6 +57,10 @@ export class HomeSidenav { return this.page.getByRole('toolbar', { name: 'Sidebar actions' }); } + get sidebarHomeAction(): Locator { + return this.sidebarToolbar.getByRole('button', { name: 'Home' }); + } + async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { await this.sidebarToolbar.getByRole('button', { name: 'Display', exact: true }).click(); await this.sidebarToolbar.getByRole('menuitemcheckbox', { name: mode }).click(); diff --git a/ee/apps/ddp-streamer/src/DDPStreamer.ts b/ee/apps/ddp-streamer/src/DDPStreamer.ts index 1c880929650db..1a6a1046ae776 100644 --- a/ee/apps/ddp-streamer/src/DDPStreamer.ts +++ b/ee/apps/ddp-streamer/src/DDPStreamer.ts @@ -127,7 +127,38 @@ export class DDPStreamer extends ServiceClass { async function sendUserData(client: Client, userId: string) { // TODO figure out what fields to send. maybe to to export function getBaseUserFields to a package const loggedUser = await Users.findOneById(userId, { - projection: { name: 1, username: 1, settings: 1, roles: 1, active: 1, statusLivechat: 1, statusDefault: 1, status: 1 }, + projection: { + 'name': 1, + 'username': 1, + 'nickname': 1, + 'emails': 1, + 'status': 1, + 'statusDefault': 1, + 'statusText': 1, + 'statusConnection': 1, + 'bio': 1, + 'avatarOrigin': 1, + 'utcOffset': 1, + 'language': 1, + 'settings': 1, + 'enableAutoAway': 1, + 'idleTimeLimit': 1, + 'roles': 1, + 'active': 1, + 'defaultRoom': 1, + 'customFields': 1, + 'requirePasswordChange': 1, + 'requirePasswordChangeReason': 1, + 'statusLivechat': 1, + 'banners': 1, + 'oauth.authorizedClients': 1, + '_updatedAt': 1, + 'avatarETag': 1, + 'extension': 1, + 'openBusinessHours': 1, + 'services.totp.enabled': 1, + 'services.email2fa.enabled': 1, + }, }); if (!loggedUser) { return; From e92e58d923fca9c44c6bfef2ec29eeab9e6afe08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Thu, 20 Mar 2025 18:14:43 -0300 Subject: [PATCH 011/187] refactor: remove KonchatNotification `newMessage` from meteor (#35570) --- .../app/ui/client/lib/KonchatNotification.ts | 42 +------------------ .../notification/useNewMessageNotification.ts | 32 ++++++++++++++ .../useNewRoomNotification.ts} | 6 +-- .../hooks/{ => notification}/useNotifyUser.ts | 20 +++++---- .../client/providers/OmnichannelProvider.tsx | 4 +- apps/meteor/client/views/root/AppLayout.tsx | 2 +- 6 files changed, 50 insertions(+), 56 deletions(-) create mode 100644 apps/meteor/client/hooks/notification/useNewMessageNotification.ts rename apps/meteor/client/hooks/{useNotifyNewRoom.ts => notification/useNewRoomNotification.ts} (72%) rename apps/meteor/client/hooks/{ => notification}/useNotifyUser.ts (77%) diff --git a/apps/meteor/app/ui/client/lib/KonchatNotification.ts b/apps/meteor/app/ui/client/lib/KonchatNotification.ts index e6a030215ab87..3b1e1f285f2aa 100644 --- a/apps/meteor/app/ui/client/lib/KonchatNotification.ts +++ b/apps/meteor/app/ui/client/lib/KonchatNotification.ts @@ -1,4 +1,4 @@ -import type { INotificationDesktop, IRoom, IUser } from '@rocket.chat/core-typings'; +import type { INotificationDesktop, IUser } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; @@ -8,12 +8,9 @@ import { onClientMessageReceived } from '../../../../client/lib/onClientMessageR import { getAvatarAsPng } from '../../../../client/lib/utils/getAvatarAsPng'; import { router } from '../../../../client/providers/RouterProvider'; import { stripTags } from '../../../../lib/utils/stringUtils'; -import { CustomSounds } from '../../../custom-sounds/client/lib/CustomSounds'; import { e2e } from '../../../e2e/client'; -import { Subscriptions } from '../../../models/client'; import { getUserPreference } from '../../../utils/client'; import { getUserAvatarURL } from '../../../utils/client/getUserAvatarURL'; -import { getUserNotificationsSoundVolume } from '../../../utils/client/getUserNotificationsSoundVolume'; import { sdk } from '../../../utils/client/lib/SDKClient'; declare global { @@ -169,43 +166,6 @@ class KonchatNotification { return this.notify(notification); }); } - - public async newMessage(rid: IRoom['_id'] | undefined) { - if ((Meteor.user() as IUser | null)?.status === 'busy') { - return; - } - - const userId = Meteor.userId(); - const newMessageNotification = getUserPreference(userId, 'newMessageNotification'); - const audioVolume = getUserNotificationsSoundVolume(userId); - - if (!rid) { - return; - } - - const sub = Subscriptions.findOne({ rid }, { fields: { audioNotificationValue: 1 } }); - - if (!sub || sub.audioNotificationValue === 'none') { - return; - } - - try { - if (sub.audioNotificationValue && sub.audioNotificationValue !== '0') { - void CustomSounds.play(sub.audioNotificationValue, { - volume: Number((audioVolume / 100).toPrecision(2)), - }); - return; - } - - if (newMessageNotification && newMessageNotification !== 'none') { - void CustomSounds.play(newMessageNotification, { - volume: Number((audioVolume / 100).toPrecision(2)), - }); - } - } catch (e) { - // do nothing - } - } } const instance = new KonchatNotification(); diff --git a/apps/meteor/client/hooks/notification/useNewMessageNotification.ts b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts new file mode 100644 index 0000000000000..bd69e69e066e7 --- /dev/null +++ b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts @@ -0,0 +1,32 @@ +import type { AtLeast, ISubscription } from '@rocket.chat/core-typings'; +import { useUserPreference } from '@rocket.chat/ui-contexts'; +import { useCallback } from 'react'; + +import { CustomSounds } from '../../../app/custom-sounds/client/lib/CustomSounds'; +import { useUserSoundPreferences } from '../useUserSoundPreferences'; + +export const useNewMessageNotification = () => { + const newMessageNotification = useUserPreference('newMessageNotification'); + const { notificationsSoundVolume } = useUserSoundPreferences(); + + const notifyNewMessage = useCallback( + (sub: AtLeast) => { + if (!sub || sub.audioNotificationValue === 'none') { + return; + } + if (sub.audioNotificationValue && sub.audioNotificationValue !== '0') { + void CustomSounds.play(sub.audioNotificationValue, { + volume: Number((notificationsSoundVolume / 100).toPrecision(2)), + }); + } + + if (newMessageNotification && newMessageNotification !== 'none') { + void CustomSounds.play(newMessageNotification, { + volume: Number((notificationsSoundVolume / 100).toPrecision(2)), + }); + } + }, + [newMessageNotification, notificationsSoundVolume], + ); + return notifyNewMessage; +}; diff --git a/apps/meteor/client/hooks/useNotifyNewRoom.ts b/apps/meteor/client/hooks/notification/useNewRoomNotification.ts similarity index 72% rename from apps/meteor/client/hooks/useNotifyNewRoom.ts rename to apps/meteor/client/hooks/notification/useNewRoomNotification.ts index 0b8eed55f9b4d..7b475745b642c 100644 --- a/apps/meteor/client/hooks/useNotifyNewRoom.ts +++ b/apps/meteor/client/hooks/notification/useNewRoomNotification.ts @@ -1,10 +1,10 @@ import { useUserPreference } from '@rocket.chat/ui-contexts'; import { useCallback } from 'react'; -import { useUserSoundPreferences } from './useUserSoundPreferences'; -import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; +import { CustomSounds } from '../../../app/custom-sounds/client/lib/CustomSounds'; +import { useUserSoundPreferences } from '../useUserSoundPreferences'; -export const useNotifyNewRoom = () => { +export const useNewRoomNotification = () => { const newRoomNotification = useUserPreference('newRoomNotification'); const { notificationsSoundVolume } = useUserSoundPreferences(); diff --git a/apps/meteor/client/hooks/useNotifyUser.ts b/apps/meteor/client/hooks/notification/useNotifyUser.ts similarity index 77% rename from apps/meteor/client/hooks/useNotifyUser.ts rename to apps/meteor/client/hooks/notification/useNotifyUser.ts index 2949ad4a14f9d..a71dd6c25a0fa 100644 --- a/apps/meteor/client/hooks/useNotifyUser.ts +++ b/apps/meteor/client/hooks/notification/useNotifyUser.ts @@ -3,12 +3,13 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useRouter, useStream, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { useEmbeddedLayout } from './useEmbeddedLayout'; -import { useNotifyNewRoom } from './useNotifyNewRoom'; -import { CachedChatSubscription } from '../../app/models/client'; -import { KonchatNotification } from '../../app/ui/client/lib/KonchatNotification'; -import { RoomManager } from '../lib/RoomManager'; -import { fireGlobalEvent } from '../lib/utils/fireGlobalEvent'; +import { useEmbeddedLayout } from '../useEmbeddedLayout'; +import { useNewMessageNotification } from './useNewMessageNotification'; +import { useNewRoomNotification } from './useNewRoomNotification'; +import { CachedChatSubscription } from '../../../app/models/client'; +import { KonchatNotification } from '../../../app/ui/client/lib/KonchatNotification'; +import { RoomManager } from '../../lib/RoomManager'; +import { fireGlobalEvent } from '../../lib/utils/fireGlobalEvent'; export const useNotifyUser = () => { const user = useUser(); @@ -16,7 +17,8 @@ export const useNotifyUser = () => { const isLayoutEmbedded = useEmbeddedLayout(); const notifyUserStream = useStream('notify-user'); const muteFocusedConversations = useUserPreference('muteFocusedConversations'); - const newRoomNotification = useNotifyNewRoom(); + const newRoomNotification = useNewRoomNotification(); + const newMessageNotification = useNewMessageNotification(); const notifyNewRoom = useEffectEvent(async (sub: AtLeast): Promise => { if (!user || user.status === 'busy') { @@ -45,12 +47,12 @@ export const useNotifyUser = () => { if (isLayoutEmbedded) { if (!hasFocus && messageIsInOpenedRoom) { // Play a notification sound - void KonchatNotification.newMessage(rid); + newMessageNotification(notification.payload); void KonchatNotification.showDesktop(notification); } } else if (!hasFocus || !messageIsInOpenedRoom || !muteFocusedConversations) { // Play a notification sound - void KonchatNotification.newMessage(rid); + newMessageNotification(notification.payload); void KonchatNotification.showDesktop(notification); } }); diff --git a/apps/meteor/client/providers/OmnichannelProvider.tsx b/apps/meteor/client/providers/OmnichannelProvider.tsx index 00428d6bfcd59..c20264571ed31 100644 --- a/apps/meteor/client/providers/OmnichannelProvider.tsx +++ b/apps/meteor/client/providers/OmnichannelProvider.tsx @@ -17,8 +17,8 @@ import { getOmniChatSortQuery } from '../../app/livechat/lib/inquiries'; import { ClientLogger } from '../../lib/ClientLogger'; import type { OmnichannelContextValue } from '../contexts/OmnichannelContext'; import { OmnichannelContext } from '../contexts/OmnichannelContext'; +import { useNewRoomNotification } from '../hooks/notification/useNewRoomNotification'; import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; -import { useNotifyNewRoom } from '../hooks/useNotifyNewRoom'; import { useOmnichannelContinuousSoundNotification } from '../hooks/useOmnichannelContinuousSoundNotification'; import { useReactiveValue } from '../hooks/useReactiveValue'; import { useShouldPreventAction } from '../hooks/useShouldPreventAction'; @@ -76,7 +76,7 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { const isPrioritiesEnabled = isEnterprise && accessible; const enabled = accessible && !!user && !!routeConfig; - const notifyNewRoom = useNotifyNewRoom(); + const notifyNewRoom = useNewRoomNotification(); const { data: { priorities = [] } = {}, diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 23a1373f6cfba..656255baa495d 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -19,10 +19,10 @@ import { useGitLabAuth } from '../../../app/gitlab/client/hooks/useGitLabAuth'; import { useLivechatEnterprise } from '../../../app/livechat-enterprise/hooks/useLivechatEnterprise'; import { useNextcloud } from '../../../app/nextcloud/client/useNextcloud'; import { useTokenPassAuth } from '../../../app/tokenpass/client/hooks/useTokenPassAuth'; +import { useNotifyUser } from '../../hooks/notification/useNotifyUser'; import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking'; import { useAutoupdate } from '../../hooks/useAutoupdate'; import { useLoadRoomForAllowedAnonymousRead } from '../../hooks/useLoadRoomForAllowedAnonymousRead'; -import { useNotifyUser } from '../../hooks/useNotifyUser'; import { appLayout } from '../../lib/appLayout'; import { useCustomOAuth } from '../../sidebar/hooks/useCustomOAuth'; import { useRedirectToSetupWizard } from '../../startup/useRedirectToSetupWizard'; From 59525f382662993c13022f2bb09b008117fa8436 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 20 Mar 2025 18:25:21 -0300 Subject: [PATCH 012/187] fix: urlencoded webhook payload parsing (#35532) Co-authored-by: Guilherme Gazzo --- .changeset/real-badgers-clap.md | 5 ++++ .../meteor/app/integrations/server/api/api.js | 10 +++++--- .../end-to-end/api/incoming-integrations.ts | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 .changeset/real-badgers-clap.md diff --git a/.changeset/real-badgers-clap.md b/.changeset/real-badgers-clap.md new file mode 100644 index 0000000000000..aa2249da67efb --- /dev/null +++ b/.changeset/real-badgers-clap.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes receiving webhook payloads encoded as x-www-form-urlencoded JSON. diff --git a/apps/meteor/app/integrations/server/api/api.js b/apps/meteor/app/integrations/server/api/api.js index 80a88b8757c9f..a94cb55bd8677 100644 --- a/apps/meteor/app/integrations/server/api/api.js +++ b/apps/meteor/app/integrations/server/api/api.js @@ -319,19 +319,21 @@ Api.router.use((req, res, next) => { return next(); } - const payloadKeys = Object.keys(req.body); - if (payloadKeys.length !== 1) { + // make sure body has only one key and it is 'payload' + if (!req.body || typeof req.body !== 'object' || !('payload' in req.body) || Object.keys(req.body).length !== 1) { return next(); } try { - // need to compose the full payload in this weird way because body-parser thought it was a form - req.bodyParams = JSON.parse(payloadKeys[0] + req.body[payloadKeys[0]]); + req.bodyParams = JSON.parse(req.body.payload); + return next(); } catch (e) { res.writeHead(400); res.end(JSON.stringify({ success: false, error: e.message })); } + + return next(); }); Api.addRoute( diff --git a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts index 814f246623bb4..952d21b386843 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -305,6 +305,31 @@ describe('[Incoming Integrations]', () => { expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === successfulMesssage)).to.be.true; }); }); + + it('should send a message if the payload is a application/x-www-form-urlencoded JSON', async () => { + const payload = { msg: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; + + await request + .post(`/hooks/${integration._id}/${integration.token}`) + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(`payload=${JSON.stringify(payload)}`) + .expect(200) + .expect(async () => { + return request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; + }); + }); + }); }); describe('[/integrations.history]', () => { From 05a23b453ca9dbdf33d096a19d0b8fd48866b2b1 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Thu, 20 Mar 2025 18:48:43 -0300 Subject: [PATCH 013/187] chore: Helper script to replace sprintf tokens `%s` on translation strings (#35569) --- .../replaceTranslationSprintfParams.ts | 140 ++++++++++++++++++ apps/meteor/package.json | 1 + 2 files changed, 141 insertions(+) create mode 100644 apps/meteor/.scripts/replaceTranslationSprintfParams.ts diff --git a/apps/meteor/.scripts/replaceTranslationSprintfParams.ts b/apps/meteor/.scripts/replaceTranslationSprintfParams.ts new file mode 100644 index 0000000000000..140eee0a168df --- /dev/null +++ b/apps/meteor/.scripts/replaceTranslationSprintfParams.ts @@ -0,0 +1,140 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { stdin as input, stdout as output } from 'node:process'; +import { createInterface } from 'node:readline/promises'; + +import fg from 'fast-glob'; + +const LOCALES_DIR = join(process.cwd(), '..', '..', 'packages', 'i18n', 'src', 'locales'); + +/** + * Counts occurrences of a substring in a string + */ +const countOccurrences = (str: string, substring: string): number => { + let count = 0; + let position = str.indexOf(substring); + + while (position !== -1) { + count++; + position = str.indexOf(substring, position + 1); + } + + return count; +}; + +/** + * Parse a JSON file and return its content + */ +const parseFile = async (path: string): Promise> => { + const content = await readFile(path, 'utf-8'); + try { + return JSON.parse(content); + } catch (e) { + console.error(`Error parsing JSON file at ${path}: ${(e as Error).message}`); + process.exit(1); + } +}; + +/** + * Save a JSON file with proper formatting + */ +const saveFile = async (path: string, json: Record): Promise => { + try { + await writeFile(path, JSON.stringify(json, null, 2), 'utf-8'); + console.log(`Updated ${path}`); + } catch (e) { + console.error(`Error saving file at ${path}: ${(e as Error).message}`); + } +}; + +/** + * Main function to replace %s tokens with named parameters + */ +const replaceTranslationSprintfParams = async (): Promise => { + // Check if a translation key was provided + const translationKey = process.argv[2]; + + if (!translationKey) { + console.error('Please provide a translation key as parameter'); + process.exit(1); + } + + // Find all translation files + const translationFiles = await fg('*.i18n.json', { cwd: LOCALES_DIR, absolute: true }); + + if (translationFiles.length === 0) { + console.error(`No translation files found in ${LOCALES_DIR}`); + process.exit(1); + } + + console.log(`Found ${translationFiles.length} translation files`); + + // Find the key in the English file first to count the %s tokens + const enFilePath = translationFiles.find((file) => file.endsWith('en.i18n.json')); + + if (!enFilePath) { + console.error('English translation file not found'); + process.exit(1); + } + + const enTranslations = await parseFile(enFilePath); + + if (!enTranslations[translationKey]) { + console.error(`Translation key "${translationKey}" not found in English translations`); + process.exit(1); + } + + const englishValue = enTranslations[translationKey]; + const tokenCount = countOccurrences(englishValue, '%s'); + + console.log(`Found translation key "${translationKey}" with value: "${englishValue}"`); + console.log(`This string contains ${tokenCount} "%s" tokens`); + + if (tokenCount === 0) { + console.log('No %s tokens found, nothing to do'); + process.exit(0); + } + + // Prompt for parameter names + const rl = createInterface({ input, output }); + const promptMessage = `Please provide ${tokenCount} parameter names (comma-separated): `; + const paramNamesInput = await rl.question(promptMessage); + rl.close(); + + // Split and trim parameter names + const paramNames = paramNamesInput.split(',').map((name) => name.trim()); + + if (paramNames.length !== tokenCount) { + console.error(`Expected ${tokenCount} parameter names, but got ${paramNames.length}`); + process.exit(1); + } + + // Process all translation files + for (const filePath of translationFiles) { + // eslint-disable-next-line no-await-in-loop + const translations = await parseFile(filePath); + + if (translations[translationKey]) { + let updatedValue = translations[translationKey]; + let paramIndex = 0; + + // Replace each %s token with a named parameter + while (updatedValue.includes('%s') && paramIndex < paramNames.length) { + updatedValue = updatedValue.replace('%s', `{{${paramNames[paramIndex]}}}`); + paramIndex++; + } + + translations[translationKey] = updatedValue; + // eslint-disable-next-line no-await-in-loop + await saveFile(filePath, translations); + } + } + + console.log('All translation files have been updated'); +}; + +// Run the script +replaceTranslationSprintfParams().catch((error) => { + console.error('Error:', error); + process.exit(1); +}); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index a430934accbb6..49c1a6f041e36 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -46,6 +46,7 @@ "test": "yarn testunit && yarn testapi", "translation-diff": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-diff.ts", "translation-check": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-check.ts", + "translation-replace-sprintf-params": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/replaceTranslationSprintfParams.ts", "translation-fix-order": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/translation-fix-order.ts", "version": "node .scripts/version.js", "set-version": "node .scripts/set-version.js", From 6c7f9ea9433a24c184457c823e642fe5111ff315 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Thu, 20 Mar 2025 19:55:54 -0300 Subject: [PATCH 014/187] feat: set busy status on calendar appointments (#35474) Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> --- .changeset/lovely-ways-move.md | 8 + apps/meteor/app/api/server/v1/calendar.ts | 3 +- apps/meteor/app/api/server/v1/users.ts | 6 +- .../ee/server/settings/outlookCalendar.ts | 6 + .../server/services/calendar/service.ts | 95 ++- .../statusEvents/applyStatusChange.ts | 47 ++ .../cancelUpcomingStatusChanges.ts | 22 + .../statusEvents/generateCronJobId.ts | 13 + .../statusEvents/handleOverlappingEvents.ts | 61 ++ .../services/calendar/statusEvents/index.ts | 15 + .../calendar/statusEvents/removeCronJobs.ts | 17 + .../setupAppointmentStatusChange.ts | 39 ++ .../services/calendar/utils/getShiftedTime.ts | 5 + apps/meteor/tests/e2e/presence.spec.ts | 106 +++ .../services/calendar/mocks/cronJobs.ts | 32 + .../server/services/calendar/service.tests.ts | 657 ++++++++++++++++++ .../statusEvents/applyStatusChange.ts | 169 +++++ .../cancelUpcomingStatusChanges.tests.ts | 82 +++ .../statusEvents/generateCronJobId.tests.ts | 38 + .../statusEvents/handleOverlappingEvents.ts | 164 +++++ .../statusEvents/removeCronJobs.tests.ts | 54 ++ .../setupAppointmentStatusChange.ts | 77 ++ .../calendar/utils/getShiftedTime.tests.ts | 22 + .../tests/unit/server/services/utils.ts | 28 + .../src/types/ICalendarService.ts | 5 +- packages/core-typings/src/ICalendarEvent.ts | 4 + .../src/models/ICalendarEventModel.ts | 2 + .../model-typings/src/models/IUsersModel.ts | 2 + packages/models/src/models/CalendarEvent.ts | 28 + packages/models/src/models/Users.ts | 21 + .../v1/calendar/CalendarEventCreateProps.ts | 10 + .../v1/calendar/CalendarEventImportProps.ts | 10 + .../v1/calendar/CalendarEventUpdateProps.ts | 10 + 33 files changed, 1831 insertions(+), 27 deletions(-) create mode 100644 .changeset/lovely-ways-move.md create mode 100644 apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts create mode 100644 apps/meteor/server/services/calendar/statusEvents/cancelUpcomingStatusChanges.ts create mode 100644 apps/meteor/server/services/calendar/statusEvents/generateCronJobId.ts create mode 100644 apps/meteor/server/services/calendar/statusEvents/handleOverlappingEvents.ts create mode 100644 apps/meteor/server/services/calendar/statusEvents/index.ts create mode 100644 apps/meteor/server/services/calendar/statusEvents/removeCronJobs.ts create mode 100644 apps/meteor/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts create mode 100644 apps/meteor/server/services/calendar/utils/getShiftedTime.ts create mode 100644 apps/meteor/tests/unit/server/services/calendar/mocks/cronJobs.ts create mode 100644 apps/meteor/tests/unit/server/services/calendar/service.tests.ts create mode 100644 apps/meteor/tests/unit/server/services/calendar/statusEvents/applyStatusChange.ts create mode 100644 apps/meteor/tests/unit/server/services/calendar/statusEvents/cancelUpcomingStatusChanges.tests.ts create mode 100644 apps/meteor/tests/unit/server/services/calendar/statusEvents/generateCronJobId.tests.ts create mode 100644 apps/meteor/tests/unit/server/services/calendar/statusEvents/handleOverlappingEvents.ts create mode 100644 apps/meteor/tests/unit/server/services/calendar/statusEvents/removeCronJobs.tests.ts create mode 100644 apps/meteor/tests/unit/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts create mode 100644 apps/meteor/tests/unit/server/services/calendar/utils/getShiftedTime.tests.ts create mode 100644 apps/meteor/tests/unit/server/services/utils.ts diff --git a/.changeset/lovely-ways-move.md b/.changeset/lovely-ways-move.md new file mode 100644 index 0000000000000..aea0865a6f7a4 --- /dev/null +++ b/.changeset/lovely-ways-move.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/core-services': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/models': minor +'@rocket.chat/meteor': minor +--- + +Adds automatic presence sync based on calendar events, updating the user’s status to “busy” when a meeting starts and reverting it afterward. diff --git a/apps/meteor/app/api/server/v1/calendar.ts b/apps/meteor/app/api/server/v1/calendar.ts index 4f189229c3227..943967b3ad426 100644 --- a/apps/meteor/app/api/server/v1/calendar.ts +++ b/apps/meteor/app/api/server/v1/calendar.ts @@ -50,11 +50,12 @@ API.v1.addRoute( { async post() { const { userId: uid } = this; - const { startTime, externalId, subject, description, meetingUrl, reminderMinutesBeforeStart } = this.bodyParams; + const { startTime, endTime, externalId, subject, description, meetingUrl, reminderMinutesBeforeStart } = this.bodyParams; const id = await Calendar.create({ uid, startTime: new Date(startTime), + ...(endTime ? { endTime: new Date(endTime) } : {}), externalId, subject, description, diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index c645535ea40cd..3018d3fa7d6ea 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 } from '@rocket.chat/core-services'; +import { MeteorError, Team, api, Calendar } from '@rocket.chat/core-services'; import type { IExportOperation, ILoginToken, IPersonalAccessToken, IUser, UserStatus } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '@rocket.chat/models'; import { @@ -19,7 +19,7 @@ import { isUsersCheckUsernameAvailabilityParamsGET, isUsersSendConfirmationEmailParamsPOST, } from '@rocket.chat/rest-typings'; -import { getLoginExpirationInMs } from '@rocket.chat/tools'; +import { getLoginExpirationInMs, wrapExceptions } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -1337,6 +1337,8 @@ API.v1.addRoute( user: { status, _id, username, statusText, roles, name }, previousStatus: user.status, }); + + 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', diff --git a/apps/meteor/ee/server/settings/outlookCalendar.ts b/apps/meteor/ee/server/settings/outlookCalendar.ts index 02dd51f795108..15f8cedb8f711 100644 --- a/apps/meteor/ee/server/settings/outlookCalendar.ts +++ b/apps/meteor/ee/server/settings/outlookCalendar.ts @@ -35,6 +35,12 @@ export function addSettings(): void { invalidValue: '', }, ); + + await this.add('Calendar_BusyStatus_Enabled', true, { + type: 'boolean', + public: true, + invalidValue: false, + }); }, ); }); diff --git a/apps/meteor/server/services/calendar/service.ts b/apps/meteor/server/services/calendar/service.ts index e3f3e0af83d95..80f6d7aac6a7e 100644 --- a/apps/meteor/server/services/calendar/service.ts +++ b/apps/meteor/server/services/calendar/service.ts @@ -1,12 +1,17 @@ import type { ICalendarService } from '@rocket.chat/core-services'; import { ServiceClassInternal, api } from '@rocket.chat/core-services'; import type { IUser, ICalendarEvent } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; import { Logger } from '@rocket.chat/logger'; import type { InsertionModel } from '@rocket.chat/model-typings'; import { CalendarEvent } from '@rocket.chat/models'; import type { UpdateResult, DeleteResult } from 'mongodb'; +import { cancelUpcomingStatusChanges } from './statusEvents/cancelUpcomingStatusChanges'; +import { removeCronJobs } from './statusEvents/removeCronJobs'; +import { setupAppointmentStatusChange } from './statusEvents/setupAppointmentStatusChange'; +import { getShiftedTime } from './utils/getShiftedTime'; import { settings } from '../../../app/settings/server'; import { getUserPreference } from '../../../app/utils/server/lib/getUserPreference'; @@ -18,24 +23,28 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe protected name = 'calendar'; public async create(data: Omit, 'reminderTime' | 'notificationSent'>): Promise { - const { uid, startTime, subject, description, reminderMinutesBeforeStart, meetingUrl } = data; - + const { uid, startTime, endTime, subject, description, reminderMinutesBeforeStart, meetingUrl, busy } = data; const minutes = reminderMinutesBeforeStart ?? defaultMinutesForNotifications; - const reminderTime = minutes ? this.getShiftedTime(startTime, -minutes) : undefined; + const reminderTime = minutes ? getShiftedTime(startTime, -minutes) : undefined; const insertData: InsertionModel = { uid, startTime, + ...(endTime && { endTime }), subject, description, meetingUrl, reminderMinutesBeforeStart: minutes, reminderTime, notificationSent: false, + ...(busy !== undefined && { busy }), }; const insertResult = await CalendarEvent.insertOne(insertData); await this.setupNextNotification(); + if (busy !== false) { + await setupAppointmentStatusChange(insertResult.insertedId, uid, startTime, endTime, UserStatus.BUSY, true); + } return insertResult.insertedId; } @@ -46,18 +55,20 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe return this.create(data); } - const { uid, startTime, subject, description, reminderMinutesBeforeStart } = data; + const { uid, startTime, endTime, subject, description, reminderMinutesBeforeStart, busy } = data; const meetingUrl = data.meetingUrl ? data.meetingUrl : await this.parseDescriptionForMeetingUrl(description); - const reminderTime = reminderMinutesBeforeStart ? this.getShiftedTime(startTime, -reminderMinutesBeforeStart) : undefined; + const reminderTime = reminderMinutesBeforeStart ? getShiftedTime(startTime, -reminderMinutesBeforeStart) : undefined; const updateData: Omit, 'uid' | 'notificationSent'> = { startTime, + ...(endTime && { endTime }), subject, description, meetingUrl, reminderMinutesBeforeStart, reminderTime, externalId, + ...(busy !== undefined && { busy }), }; const event = await this.findImportedEvent(externalId, uid); @@ -70,12 +81,18 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe }); await this.setupNextNotification(); + if (busy !== false) { + await setupAppointmentStatusChange(insertResult.insertedId, uid, startTime, endTime, UserStatus.BUSY, true); + } return insertResult.insertedId; } const updateResult = await CalendarEvent.updateEvent(event._id, updateData); if (updateResult.modifiedCount > 0) { await this.setupNextNotification(); + if (busy !== false) { + await setupAppointmentStatusChange(event._id, uid, startTime, endTime, UserStatus.BUSY, true); + } } return event._id; @@ -89,30 +106,58 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe return CalendarEvent.findByUserIdAndDate(uid, date).toArray(); } - public async update(eventId: ICalendarEvent['_id'], data: Partial): Promise { - const { startTime, subject, description, reminderMinutesBeforeStart } = data; - const meetingUrl = data.meetingUrl ? data.meetingUrl : await this.parseDescriptionForMeetingUrl(description || ''); - const reminderTime = reminderMinutesBeforeStart && startTime ? this.getShiftedTime(startTime, -reminderMinutesBeforeStart) : undefined; + public async update(eventId: ICalendarEvent['_id'], data: Partial): Promise { + const event = await this.get(eventId); + if (!event) { + return null; + } + + const { startTime, endTime, subject, description, reminderMinutesBeforeStart, busy } = data; + + const meetingUrl = await this.getMeetingUrl(data); + const reminderTime = reminderMinutesBeforeStart && startTime ? getShiftedTime(startTime, -reminderMinutesBeforeStart) : undefined; const updateData: Partial = { startTime, + ...(endTime && { endTime }), subject, description, meetingUrl, reminderMinutesBeforeStart, reminderTime, + ...(busy !== undefined && { busy }), }; const updateResult = await CalendarEvent.updateEvent(eventId, updateData); if (updateResult.modifiedCount > 0) { await this.setupNextNotification(); + + if (startTime || endTime) { + await removeCronJobs(eventId, event.uid); + + const isBusy = busy !== undefined ? busy : event.busy !== false; + if (isBusy) { + const effectiveStartTime = startTime || event.startTime; + const effectiveEndTime = endTime || event.endTime; + + // Only proceed if we have both valid start and end times + if (effectiveStartTime && effectiveEndTime) { + await setupAppointmentStatusChange(eventId, event.uid, effectiveStartTime, effectiveEndTime, UserStatus.BUSY, true); + } + } + } } return updateResult; } public async delete(eventId: ICalendarEvent['_id']): Promise { + const event = await this.get(eventId); + if (event) { + await removeCronJobs(eventId, event.uid); + } + return CalendarEvent.deleteOne({ _id: eventId, }); @@ -122,6 +167,22 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe return this.doSetupNextNotification(false); } + public async cancelUpcomingStatusChanges(uid: IUser['_id'], endTime = new Date()): Promise { + return cancelUpcomingStatusChanges(uid, endTime); + } + + private async getMeetingUrl(eventData: Partial): Promise { + if (eventData.meetingUrl !== undefined) { + return eventData.meetingUrl; + } + + if (eventData.description !== undefined) { + return this.parseDescriptionForMeetingUrl(eventData.description); + } + + return undefined; + } + private async doSetupNextNotification(isRecursive: boolean): Promise { const date = await CalendarEvent.findNextNotificationDate(); if (!date) { @@ -139,19 +200,17 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe await cronJobs.addAtTimestamp('calendar-reminders', date, async () => this.sendCurrentNotifications(date)); } - public async sendCurrentNotifications(date: Date): Promise { + private async sendCurrentNotifications(date: Date): Promise { const events = await CalendarEvent.findEventsToNotify(date, 1).toArray(); - for await (const event of events) { await this.sendEventNotification(event); - await CalendarEvent.flagNotificationSent(event._id); } await this.doSetupNextNotification(true); } - public async sendEventNotification(event: ICalendarEvent): Promise { + private async sendEventNotification(event: ICalendarEvent): Promise { if (!(await getUserPreference(event.uid, 'notifyCalendarEvents'))) { return; } @@ -165,14 +224,14 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe }); } - public async findImportedEvent( + private async findImportedEvent( externalId: Required['externalId'], uid: ICalendarEvent['uid'], ): Promise { return CalendarEvent.findOneByExternalIdAndUserId(externalId, uid); } - public async parseDescriptionForMeetingUrl(description: string): Promise { + private async parseDescriptionForMeetingUrl(description: string): Promise { if (!description) { return; } @@ -224,10 +283,4 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe return undefined; } - - private getShiftedTime(time: Date, minutes: number): Date { - const newTime = new Date(time.valueOf()); - newTime.setMinutes(newTime.getMinutes() + minutes); - return newTime; - } } diff --git a/apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts b/apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts new file mode 100644 index 0000000000000..d47fe85237ef0 --- /dev/null +++ b/apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts @@ -0,0 +1,47 @@ +import { api } from '@rocket.chat/core-services'; +import { UserStatus } from '@rocket.chat/core-typings'; +import type { ICalendarEvent, IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +import { setupAppointmentStatusChange } from './setupAppointmentStatusChange'; + +export async function applyStatusChange({ + eventId, + uid, + startTime, + endTime, + status, + shouldScheduleRemoval, +}: { + eventId: ICalendarEvent['_id']; + uid: IUser['_id']; + startTime: Date; + endTime?: Date; + status?: UserStatus; + shouldScheduleRemoval?: boolean; +}): Promise { + const user = await Users.findOneById(uid, { projection: { roles: 1, username: 1, name: 1, status: 1 } }); + if (!user || user.status === UserStatus.OFFLINE) { + return; + } + + const newStatus = status ?? UserStatus.BUSY; + const previousStatus = user.status; + + await Users.updateStatusAndStatusDefault(uid, newStatus, newStatus); + + await api.broadcast('presence.status', { + user: { + status: newStatus, + _id: uid, + roles: user.roles, + username: user.username, + name: user.name, + }, + previousStatus, + }); + + if (shouldScheduleRemoval && endTime) { + await setupAppointmentStatusChange(eventId, uid, startTime, endTime, previousStatus, false); + } +} diff --git a/apps/meteor/server/services/calendar/statusEvents/cancelUpcomingStatusChanges.ts b/apps/meteor/server/services/calendar/statusEvents/cancelUpcomingStatusChanges.ts new file mode 100644 index 0000000000000..aab3df3a7c7e2 --- /dev/null +++ b/apps/meteor/server/services/calendar/statusEvents/cancelUpcomingStatusChanges.ts @@ -0,0 +1,22 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { cronJobs } from '@rocket.chat/cron'; +import { CalendarEvent } from '@rocket.chat/models'; + +import { generateCronJobId } from './generateCronJobId'; +import { settings } from '../../../../app/settings/server'; + +export async function cancelUpcomingStatusChanges(uid: IUser['_id'], endTime = new Date()): Promise { + const hasBusyStatusSetting = settings.get('Calendar_BusyStatus_Enabled'); + if (!hasBusyStatusSetting) { + return; + } + + const events = await CalendarEvent.findEligibleEventsForCancelation(uid, endTime).toArray(); + + for await (const event of events) { + const statusChangeJobId = generateCronJobId(event._id, event.uid, 'status'); + if (await cronJobs.has(statusChangeJobId)) { + await cronJobs.remove(statusChangeJobId); + } + } +} diff --git a/apps/meteor/server/services/calendar/statusEvents/generateCronJobId.ts b/apps/meteor/server/services/calendar/statusEvents/generateCronJobId.ts new file mode 100644 index 0000000000000..b007efb607eb2 --- /dev/null +++ b/apps/meteor/server/services/calendar/statusEvents/generateCronJobId.ts @@ -0,0 +1,13 @@ +import type { ICalendarEvent, IUser } from '@rocket.chat/core-typings'; + +export function generateCronJobId(eventId: ICalendarEvent['_id'], uid: IUser['_id'], eventType: 'status' | 'reminder'): string { + if (!eventId || !uid || !eventType || (eventType !== 'status' && eventType !== 'reminder')) { + throw new Error('Missing required parameters. Please provide eventId, uid and eventType (status or reminder)'); + } + + if (eventType === 'status') { + return `calendar-presence-status-${eventId}-${uid}`; + } + + return `calendar-reminder-${eventId}-${uid}`; +} diff --git a/apps/meteor/server/services/calendar/statusEvents/handleOverlappingEvents.ts b/apps/meteor/server/services/calendar/statusEvents/handleOverlappingEvents.ts new file mode 100644 index 0000000000000..96ca535b98c15 --- /dev/null +++ b/apps/meteor/server/services/calendar/statusEvents/handleOverlappingEvents.ts @@ -0,0 +1,61 @@ +import type { UserStatus, IUser, ICalendarEvent } from '@rocket.chat/core-typings'; +import { cronJobs } from '@rocket.chat/cron'; +import { CalendarEvent } from '@rocket.chat/models'; + +import { applyStatusChange } from './applyStatusChange'; +import { generateCronJobId } from './generateCronJobId'; + +export async function handleOverlappingEvents( + eventId: ICalendarEvent['_id'], + uid: IUser['_id'], + startTime: Date, + endTime: Date, + status?: UserStatus, +): Promise<{ shouldProceed: boolean }> { + const overlappingEvents = await CalendarEvent.findOverlappingEvents(eventId, uid, startTime, endTime).toArray(); + + if (overlappingEvents.length === 0) { + return { shouldProceed: true }; + } + + const allEvents = [...overlappingEvents, { endTime, startTime }]; + + const latestEndingEvent = allEvents.reduce<{ endTime: Date | null; startTime: Date | null }>( + (latest, event) => { + if (!event.endTime) return latest; + if (!latest.endTime || event.endTime > latest.endTime) { + return { endTime: event.endTime, startTime: event.startTime }; + } + return latest; + }, + { endTime: null, startTime: null }, + ); + + // If this event doesn't have the latest end time, don't schedule removal + // because another event will handle it + if (latestEndingEvent.endTime && latestEndingEvent.endTime.getTime() !== endTime.getTime()) { + const scheduledTime = startTime; + const cronJobId = generateCronJobId(eventId, uid, 'status'); + + if (await cronJobs.has(cronJobId)) { + await cronJobs.remove(cronJobId); + } + + await cronJobs.addAtTimestamp(cronJobId, scheduledTime, async () => + applyStatusChange({ eventId, uid, startTime, endTime, status, shouldScheduleRemoval: false }), + ); + return { shouldProceed: false }; + } + + // For any existing events that end before this one, remove their status removal jobs + for await (const event of overlappingEvents) { + if (event.endTime && event.endTime < endTime) { + const eventCronJobId = generateCronJobId(event._id, uid, 'status'); + if (await cronJobs.has(eventCronJobId)) { + await cronJobs.remove(eventCronJobId); + } + } + } + + return { shouldProceed: true }; +} diff --git a/apps/meteor/server/services/calendar/statusEvents/index.ts b/apps/meteor/server/services/calendar/statusEvents/index.ts new file mode 100644 index 0000000000000..ff4133ca8bc56 --- /dev/null +++ b/apps/meteor/server/services/calendar/statusEvents/index.ts @@ -0,0 +1,15 @@ +import { applyStatusChange } from './applyStatusChange'; +import { cancelUpcomingStatusChanges } from './cancelUpcomingStatusChanges'; +import { generateCronJobId } from './generateCronJobId'; +import { handleOverlappingEvents } from './handleOverlappingEvents'; +import { removeCronJobs } from './removeCronJobs'; +import { setupAppointmentStatusChange } from './setupAppointmentStatusChange'; + +export const statusEventManager = { + applyStatusChange, + cancelUpcomingStatusChanges, + generateCronJobId, + handleOverlappingEvents, + removeCronJobs, + setupAppointmentStatusChange, +} as const; diff --git a/apps/meteor/server/services/calendar/statusEvents/removeCronJobs.ts b/apps/meteor/server/services/calendar/statusEvents/removeCronJobs.ts new file mode 100644 index 0000000000000..30c79098a3eaf --- /dev/null +++ b/apps/meteor/server/services/calendar/statusEvents/removeCronJobs.ts @@ -0,0 +1,17 @@ +import type { ICalendarEvent, IUser } from '@rocket.chat/core-typings'; +import { cronJobs } from '@rocket.chat/cron'; + +import { generateCronJobId } from './generateCronJobId'; + +export async function removeCronJobs(eventId: ICalendarEvent['_id'], uid: IUser['_id']): Promise { + const statusChangeJobId = generateCronJobId(eventId, uid, 'status'); + const reminderJobId = generateCronJobId(eventId, uid, 'reminder'); + + if (await cronJobs.has(statusChangeJobId)) { + await cronJobs.remove(statusChangeJobId); + } + + if (await cronJobs.has(reminderJobId)) { + await cronJobs.remove(reminderJobId); + } +} diff --git a/apps/meteor/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts b/apps/meteor/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts new file mode 100644 index 0000000000000..907f2a6bf2c66 --- /dev/null +++ b/apps/meteor/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts @@ -0,0 +1,39 @@ +import type { ICalendarEvent, IUser, UserStatus } from '@rocket.chat/core-typings'; +import { cronJobs } from '@rocket.chat/cron'; + +import { applyStatusChange } from './applyStatusChange'; +import { generateCronJobId } from './generateCronJobId'; +import { handleOverlappingEvents } from './handleOverlappingEvents'; +import { settings } from '../../../../app/settings/server'; + +export async function setupAppointmentStatusChange( + eventId: ICalendarEvent['_id'], + uid: IUser['_id'], + startTime: Date, + endTime?: Date, + status?: UserStatus, + shouldScheduleRemoval?: boolean, +): Promise { + const hasBusyStatusSetting = settings.get('Calendar_BusyStatus_Enabled'); + if (!endTime || !hasBusyStatusSetting) { + return; + } + + if (shouldScheduleRemoval) { + const { shouldProceed } = await handleOverlappingEvents(eventId, uid, startTime, endTime, status); + if (!shouldProceed) { + return; + } + } + + const scheduledTime = shouldScheduleRemoval ? startTime : endTime; + const cronJobId = generateCronJobId(eventId, uid, 'status'); + + if (await cronJobs.has(cronJobId)) { + await cronJobs.remove(cronJobId); + } + + await cronJobs.addAtTimestamp(cronJobId, scheduledTime, async () => + applyStatusChange({ eventId, uid, startTime, endTime, status, shouldScheduleRemoval }), + ); +} diff --git a/apps/meteor/server/services/calendar/utils/getShiftedTime.ts b/apps/meteor/server/services/calendar/utils/getShiftedTime.ts new file mode 100644 index 0000000000000..aae5ccfb957eb --- /dev/null +++ b/apps/meteor/server/services/calendar/utils/getShiftedTime.ts @@ -0,0 +1,5 @@ +export function getShiftedTime(time: Date, minutes: number): Date { + const newTime = new Date(time.valueOf()); + newTime.setMinutes(newTime.getMinutes() + minutes); + return newTime; +} diff --git a/apps/meteor/tests/e2e/presence.spec.ts b/apps/meteor/tests/e2e/presence.spec.ts index ad96c3cee4bff..4f5dfe7a6db70 100644 --- a/apps/meteor/tests/e2e/presence.spec.ts +++ b/apps/meteor/tests/e2e/presence.spec.ts @@ -1,4 +1,5 @@ import { DEFAULT_USER_CREDENTIALS, IS_EE } from './config/constants'; +import { Users } from './fixtures/userStates'; import { Registration } from './page-objects'; import { setSettingValueById } from './utils/setSettingValueById'; import { test, expect } from './utils/test'; @@ -45,4 +46,109 @@ test.describe.serial('Presence', () => { await expect(page.getByRole('button', { name: 'User menu' }).locator('.rcx-status-bullet--online')).toBeVisible(); }); }); + + // This test is supposed to be ran locally because it is too slow. + // It is also a workaround until we find a better way to test this. + test.describe.skip('Calendar appointment automatic status', () => { + test.describe.configure({ timeout: 1000 * 60 * 10 }); + test.use({ storageState: Users.admin.state }); + + test.beforeAll(async ({ api }) => { + await setSettingValueById(api, 'Calendar_BusyStatus_Enabled', true); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Calendar_BusyStatus_Enabled', false); + }); + + test('Should change user status to busy when there is an appointment', async ({ page, api }) => { + await page.goto('/home'); + + await expect(page.getByRole('button', { name: 'User menu' }).locator('.rcx-status-bullet--online')).toBeVisible(); + expect( + ( + await api.post('/calendar-events.create', { + startTime: new Date(new Date().getTime() + 1000 * 60 * 2).toISOString(), + endTime: new Date(new Date().getTime() + 1000 * 60 * 3).toISOString(), + subject: 'Test appointment', + description: 'Test appointment description', + meetingUrl: 'https://rocket.chat/', + }) + ).status(), + ).toBe(200); + + await test.step('Should change status to busy', async () => { + // wait 2 minutes to ensure the status is changed + await page.waitForTimeout(1000 * 60 * 2); + + await expect(page.getByRole('button', { name: 'User menu' }).locator('.rcx-status-bullet--busy')).toBeVisible(); + }); + + await test.step('Should revert status to online', async () => { + // wait 2 minutes to ensure the status is changed + await page.waitForTimeout(1000 * 60); + + await expect(page.getByRole('button', { name: 'User menu' }).locator('.rcx-status-bullet--online')).toBeVisible(); + }); + }); + + test('Should not change status to busy if the event is deleted', async ({ page, api }) => { + await page.goto('/home'); + + await expect(page.getByRole('button', { name: 'User menu' }).locator('.rcx-status-bullet--online')).toBeVisible(); + + const apiResponse = await api.post('/calendar-events.create', { + startTime: new Date(new Date().getTime() + 1000 * 60 * 2).toISOString(), + endTime: new Date(new Date().getTime() + 1000 * 60 * 3).toISOString(), + subject: 'Test appointment', + description: 'Test appointment description', + meetingUrl: 'https://rocket.chat/', + }); + + expect(apiResponse.status()).toBe(200); + + const eventId = (await apiResponse.json()).id; + + expect((await api.post('/calendar-events.delete', { eventId })).status()).toBe(200); + + await page.waitForTimeout(1000 * 60 * 2); + + await expect(page.getByRole('button', { name: 'User menu' }).locator('.rcx-status-bullet--online')).toBeVisible(); + }); + + test('Should update status to busy when the event is updated', async ({ page, api }) => { + await page.goto('/home'); + + await expect(page.getByRole('button', { name: 'User menu' }).locator('.rcx-status-bullet--online')).toBeVisible(); + + const apiResponse = await api.post('/calendar-events.create', { + startTime: new Date(new Date().getTime() + 1000 * 60 * 50).toISOString(), + endTime: new Date(new Date().getTime() + 1000 * 60 * 55).toISOString(), + subject: 'Test appointment', + description: 'Test appointment description', + meetingUrl: 'https://rocket.chat/', + }); + + expect(apiResponse.status()).toBe(200); + + const eventId = (await apiResponse.json()).id; + + expect( + ( + await api.post('/calendar-events.update', { + eventId, + startTime: new Date(new Date().getTime() + 1000 * 60 * 2).toISOString(), + subject: 'Test appointment updated', + description: 'Test appointment description updated', + meetingUrl: 'https://rocket.chat/updated', + }) + ).status(), + ).toBe(200); + + // wait 2 minutes to ensure the status is changed + await page.waitForTimeout(1000 * 60 * 2); + + await expect(page.getByRole('button', { name: 'User menu' }).locator('.rcx-status-bullet--busy')).toBeVisible(); + }); + }); }); diff --git a/apps/meteor/tests/unit/server/services/calendar/mocks/cronJobs.ts b/apps/meteor/tests/unit/server/services/calendar/mocks/cronJobs.ts new file mode 100644 index 0000000000000..902e90fc8ae8d --- /dev/null +++ b/apps/meteor/tests/unit/server/services/calendar/mocks/cronJobs.ts @@ -0,0 +1,32 @@ +import type { AgendaCronJobs } from '@rocket.chat/cron'; + +// #TODO: Move this to a package and write unit tests there ensuring that the behavior of the mock and the real class match 1:1 +export class MockedCronJobs { + public jobNames = new Set(); + + private _started = false; + + public get started(): boolean { + return this._started; + } + + start: AgendaCronJobs['start'] = async () => { + this._started = true; + }; + + add: AgendaCronJobs['add'] = async (name) => { + this.jobNames.add(name); + }; + + addAtTimestamp: AgendaCronJobs['addAtTimestamp'] = async (name) => { + this.jobNames.add(name); + }; + + remove: AgendaCronJobs['remove'] = async (name) => { + this.jobNames.delete(name); + }; + + has: AgendaCronJobs['has'] = async (jobName) => { + return this.jobNames.has(jobName); + }; +} diff --git a/apps/meteor/tests/unit/server/services/calendar/service.tests.ts b/apps/meteor/tests/unit/server/services/calendar/service.tests.ts new file mode 100644 index 0000000000000..d6fc603089ce9 --- /dev/null +++ b/apps/meteor/tests/unit/server/services/calendar/service.tests.ts @@ -0,0 +1,657 @@ +import { api } from '@rocket.chat/core-services'; +import { expect } from 'chai'; +import { describe, it, beforeEach, afterEach } from 'mocha'; +import type { DeleteResult, UpdateResult } from 'mongodb'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +import { testPrivateMethod, createFreshServiceInstance } from '../utils'; +import { MockedCronJobs } from './mocks/cronJobs'; + +const settingsMock = new Map(); +const cronJobsMock = new MockedCronJobs(); + +const CalendarEventMock = { + insertOne: sinon.stub(), + findOne: sinon.stub(), + findByUserIdAndDate: sinon.stub(), + updateEvent: sinon.stub(), + deleteOne: sinon.stub(), + findNextNotificationDate: sinon.stub(), + findEventsToNotify: sinon.stub(), + flagNotificationSent: sinon.stub(), + findOneByExternalIdAndUserId: sinon.stub(), +}; + +const statusEventManagerMock = { + setupAppointmentStatusChange: sinon.stub().resolves(), + removeCronJobs: sinon.stub().resolves(), + cancelUpcomingStatusChanges: sinon.stub().resolves(), +}; + +const getUserPreferenceMock = sinon.stub(); + +const serviceMocks = { + './statusEvents/cancelUpcomingStatusChanges': { cancelUpcomingStatusChanges: statusEventManagerMock.cancelUpcomingStatusChanges }, + './statusEvents/removeCronJobs': { removeCronJobs: statusEventManagerMock.removeCronJobs }, + './statusEvents/setupAppointmentStatusChange': { setupAppointmentStatusChange: statusEventManagerMock.setupAppointmentStatusChange }, + '../../../app/settings/server': { settings: settingsMock }, + '@rocket.chat/core-services': { api, ServiceClassInternal: class {} }, + '@rocket.chat/cron': { cronJobs: cronJobsMock }, + '@rocket.chat/models': { CalendarEvent: CalendarEventMock }, + '../../../app/utils/server/lib/getUserPreference': { getUserPreference: getUserPreferenceMock }, +}; + +const { CalendarService } = proxyquire.noCallThru().load('../../../../../server/services/calendar/service', serviceMocks); + +describe('CalendarService', () => { + let sandbox: sinon.SinonSandbox; + let service: InstanceType; + const fakeUserId = 'user123'; + const fakeEventId = 'event456'; + const fakeExternalId = 'external789'; + const fakeStartTime = new Date('2025-01-01T10:00:00Z'); + const fakeEndTime = new Date('2025-01-01T11:00:00Z'); + const fakeSubject = 'Test Meeting'; + const fakeDescription = 'This is a test meeting'; + const fakeMeetingUrl = 'https://meet.test/123'; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + service = new CalendarService(); + stubServiceMethods(); + setupCalendarEventMocks(); + setupStatusEventManagerMocks(); + setupOtherMocks(); + }); + + function stubServiceMethods() { + const proto = Object.getPrototypeOf(service); + sandbox.stub(proto, 'parseDescriptionForMeetingUrl').resolves(fakeMeetingUrl); + sandbox.stub(proto, 'findImportedEvent').callsFake(async (externalId, uid) => { + return CalendarEventMock.findOneByExternalIdAndUserId(externalId, uid); + }); + sandbox.stub(proto, 'sendEventNotification').resolves(); + sandbox.stub(proto, 'sendCurrentNotifications').resolves(); + sandbox.stub(proto, 'doSetupNextNotification').resolves(); + + sandbox.stub(service, 'setupNextNotification').resolves(); + } + + function setupCalendarEventMocks() { + const freshMocks = { + insertOne: sinon.stub().resolves({ insertedId: fakeEventId }), + findOne: sinon.stub().resolves(null), + findByUserIdAndDate: sinon.stub().returns({ + toArray: sinon.stub().resolves([]), + }), + updateEvent: sinon.stub().resolves({ modifiedCount: 1, matchedCount: 1 } as UpdateResult), + deleteOne: sinon.stub().resolves({ deletedCount: 1 } as DeleteResult), + findNextNotificationDate: sinon.stub().resolves(null), + findEventsToNotify: sinon.stub().returns({ + toArray: sinon.stub().resolves([]), + }), + flagNotificationSent: sinon.stub().resolves(), + findOneByExternalIdAndUserId: sinon.stub().resolves(null), + }; + + Object.assign(CalendarEventMock, freshMocks); + } + + function setupStatusEventManagerMocks() { + Object.values(statusEventManagerMock).forEach((stub) => stub.resetHistory()); + } + + function setupOtherMocks() { + sandbox.stub(api, 'broadcast').resolves(); + + settingsMock.clear(); + settingsMock.set( + 'Calendar_MeetingUrl_Regex', + '(?:[?&]callUrl=([^\n&<]+))|(?:(?:%3F)|(?:%26))callUrl(?:%3D)((?:(?:[^\n&<](?!%26)))+[^\n&<]?)', + ); + settingsMock.set('Calendar_BusyStatus_Enabled', true); + + cronJobsMock.jobNames.clear(); + + getUserPreferenceMock.reset(); + getUserPreferenceMock.resolves(true); + } + + afterEach(() => { + sandbox.restore(); + }); + + describe('#create', () => { + it('should create a new calendar event', async () => { + const eventData = { + uid: fakeUserId, + startTime: fakeStartTime, + endTime: fakeEndTime, + subject: fakeSubject, + description: fakeDescription, + meetingUrl: fakeMeetingUrl, + reminderMinutesBeforeStart: 5, + }; + + const result = await service.create(eventData); + + expect(result).to.equal(fakeEventId); + expect(CalendarEventMock.insertOne.callCount).to.equal(1); + expect(CalendarEventMock.insertOne.firstCall.args[0]).to.include({ + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + description: fakeDescription, + meetingUrl: fakeMeetingUrl, + reminderMinutesBeforeStart: 5, + notificationSent: false, + }); + sinon.assert.calledOnce(statusEventManagerMock.setupAppointmentStatusChange); + }); + + it('should create event without end time if not provided', async () => { + const eventData = { + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + description: fakeDescription, + }; + + await service.create(eventData); + + expect(CalendarEventMock.insertOne.firstCall.args[0]).to.not.have.property('endTime'); + }); + + it('should use default reminder minutes if not provided', async () => { + const eventData = { + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + description: fakeDescription, + }; + + await service.create(eventData); + + const insertedData = CalendarEventMock.insertOne.firstCall.args[0]; + expect(insertedData).to.have.property('reminderMinutesBeforeStart', 5); + }); + }); + + describe('#import', () => { + it('should create a new event if externalId is not provided', async () => { + const eventData = { + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + description: fakeDescription, + }; + + await service.import(eventData); + + sinon.assert.calledOnce(CalendarEventMock.insertOne); + sinon.assert.calledOnce(statusEventManagerMock.setupAppointmentStatusChange); + }); + + it('should create a new event if event with externalId not found', async () => { + const eventData = { + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + description: fakeDescription, + externalId: fakeExternalId, + }; + + CalendarEventMock.findOneByExternalIdAndUserId.resolves(null); + + await service.import(eventData); + + sinon.assert.calledWith(CalendarEventMock.findOneByExternalIdAndUserId, fakeExternalId, fakeUserId); + sinon.assert.calledOnce(CalendarEventMock.insertOne); + }); + + it('should update existing event if found by externalId', async () => { + const eventData = { + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + description: fakeDescription, + externalId: fakeExternalId, + }; + + CalendarEventMock.findOneByExternalIdAndUserId.resolves({ + _id: fakeEventId, + uid: fakeUserId, + externalId: fakeExternalId, + }); + + await service.import(eventData); + + sinon.assert.calledWith(CalendarEventMock.findOneByExternalIdAndUserId, fakeExternalId, fakeUserId); + sinon.assert.calledOnce(CalendarEventMock.updateEvent); + sinon.assert.notCalled(CalendarEventMock.insertOne); + }); + + it('should extract meeting URL from description if not provided', async () => { + const eventData = { + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + description: 'Description with callUrl=https://meet.test/123', + externalId: fakeExternalId, + }; + + const proto = Object.getPrototypeOf(service); + await service.import(eventData); + + sinon.assert.calledWith(proto.parseDescriptionForMeetingUrl as sinon.SinonStub, eventData.description); + }); + }); + + describe('#get', () => { + it('should retrieve a single event by ID', async () => { + const fakeEvent = { + _id: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + }; + + CalendarEventMock.findOne.resolves(fakeEvent); + + const result = await service.get(fakeEventId); + + sinon.assert.calledWith(CalendarEventMock.findOne, { _id: fakeEventId }); + expect(result).to.equal(fakeEvent); + }); + }); + + describe('#list', () => { + it('should retrieve events for a user on a specific date', async () => { + const fakeEvents = [ + { _id: 'event1', uid: fakeUserId, startTime: fakeStartTime }, + { _id: 'event2', uid: fakeUserId, startTime: fakeStartTime }, + ]; + + CalendarEventMock.findByUserIdAndDate.returns({ + toArray: sinon.stub().resolves(fakeEvents), + }); + + const fakeDate = new Date('2025-01-01'); + const result = await service.list(fakeUserId, fakeDate); + + sinon.assert.calledWith(CalendarEventMock.findByUserIdAndDate, fakeUserId, fakeDate); + expect(result).to.equal(fakeEvents); + }); + }); + + describe('#update', () => { + it('should update an existing event', async () => { + const fakeEvent = { + _id: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + endTime: fakeEndTime, + subject: fakeSubject, + }; + + CalendarEventMock.findOne.resolves(fakeEvent); + + const updateData = { + subject: 'Updated Subject', + description: 'Updated Description', + }; + + await service.update(fakeEventId, updateData); + + sinon.assert.calledWith(CalendarEventMock.updateEvent, fakeEventId, sinon.match.has('subject', 'Updated Subject')); + }); + + it('should do nothing if event not found', async () => { + CalendarEventMock.findOne.resolves(null); + + await service.update(fakeEventId, { subject: 'New Subject' }); + + sinon.assert.notCalled(CalendarEventMock.updateEvent); + }); + + it('should update cron jobs when start/end times change', async () => { + const fakeEvent = { + _id: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + endTime: fakeEndTime, + subject: fakeSubject, + }; + + CalendarEventMock.findOne.resolves(fakeEvent); + + const newStartTime = new Date('2025-01-02T10:00:00Z'); + const newEndTime = new Date('2025-01-02T11:00:00Z'); + + await service.update(fakeEventId, { + startTime: newStartTime, + endTime: newEndTime, + }); + + sinon.assert.calledOnce(statusEventManagerMock.removeCronJobs); + sinon.assert.calledOnce(statusEventManagerMock.setupAppointmentStatusChange); + }); + + it('should extract meeting URL from description if not provided', async () => { + const fakeEvent = { + _id: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + }; + + CalendarEventMock.findOne.resolves(fakeEvent); + + const proto = Object.getPrototypeOf(service); + + await service.update(fakeEventId, { + description: 'Description with callUrl=https://meet.test/123', + }); + + sinon.assert.called(proto.parseDescriptionForMeetingUrl as sinon.SinonStub); + }); + + it('should setup next notification if event was modified', async () => { + const fakeEvent = { + _id: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + }; + + CalendarEventMock.findOne.resolves(fakeEvent); + CalendarEventMock.updateEvent.resolves({ modifiedCount: 1 } as UpdateResult); + + await service.update(fakeEventId, { subject: 'New Subject' }); + + sinon.assert.calledOnce(service.setupNextNotification as sinon.SinonStub); + }); + }); + + describe('#delete', () => { + it('should delete an event and remove cron jobs', async () => { + const fakeEvent = { + _id: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + }; + + CalendarEventMock.findOne.resolves(fakeEvent); + + await service.delete(fakeEventId); + + sinon.assert.calledOnce(statusEventManagerMock.removeCronJobs); + sinon.assert.calledWith(CalendarEventMock.deleteOne, { _id: fakeEventId }); + }); + + it('should only delete the event if not found', async () => { + CalendarEventMock.findOne.resolves(null); + + await service.delete(fakeEventId); + + sinon.assert.notCalled(statusEventManagerMock.removeCronJobs); + sinon.assert.calledOnce(CalendarEventMock.deleteOne); + }); + }); + + describe('#setupNextNotification', () => { + it('should call doSetupNextNotification internally', async () => { + const serviceExports = proxyquire.noCallThru().load('../../../../../server/services/calendar/service', serviceMocks); + + const testService = createFreshServiceInstance>(serviceExports); + + const localSandbox = sinon.createSandbox(); + + try { + const doSetupStub = localSandbox.stub(Object.getPrototypeOf(testService), 'doSetupNextNotification').resolves(); + + await testService.setupNextNotification(); + + sinon.assert.calledOnceWithExactly(doSetupStub, false); + } finally { + localSandbox.restore(); + } + }); + }); + + describe('#cancelUpcomingStatusChanges', () => { + it('should delegate to statusEventManager', async () => { + await service.cancelUpcomingStatusChanges(fakeUserId); + + sinon.assert.calledWith(statusEventManagerMock.cancelUpcomingStatusChanges, fakeUserId); + }); + + it('should pass custom end time if provided', async () => { + const customDate = new Date('2025-02-01'); + + await service.cancelUpcomingStatusChanges(fakeUserId, customDate); + + sinon.assert.calledWith(statusEventManagerMock.cancelUpcomingStatusChanges, fakeUserId, customDate); + }); + }); + + describe('Private: parseDescriptionForMeetingUrl', () => { + it('should return undefined for empty description', async () => { + await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { + const result = await method(''); + expect(result).to.be.undefined; + }); + }); + + it('should extract URL from description with default pattern', async () => { + await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { + const testDescription = 'Join at https://meet.example.com?callUrl=https://special-meeting.com/123'; + const result = await method(testDescription); + expect(result).to.equal('https://special-meeting.com/123'); + }); + }); + + it('should return undefined if regex pattern is empty', async () => { + await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { + settingsMock.set('Calendar_MeetingUrl_Regex', ''); + + const result = await method('Test description with no pattern match'); + expect(result).to.be.undefined; + }); + }); + + it('should handle URL decoding', async () => { + await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { + const encodedUrl = 'Join meeting at link with callUrl%3Dhttps%3A%2F%2Fmeeting.example.com%2F123'; + const result = await method(encodedUrl); + expect(result).to.include('https://meeting.example.com/123'); + }); + }); + }); + + describe('Private: findImportedEvent', () => { + it('should call the model method with correct parameters', async () => { + await testPrivateMethod(service, 'findImportedEvent', async (method) => { + await method(fakeExternalId, fakeUserId); + sinon.assert.calledWith(CalendarEventMock.findOneByExternalIdAndUserId, fakeExternalId, fakeUserId); + }); + }); + + it('should return the event when found', async () => { + await testPrivateMethod(service, 'findImportedEvent', async (method) => { + const fakeEvent = { _id: fakeEventId, externalId: fakeExternalId, uid: fakeUserId }; + CalendarEventMock.findOneByExternalIdAndUserId.resolves(fakeEvent); + + const result = await method(fakeExternalId, fakeUserId); + expect(result).to.equal(fakeEvent); + }); + }); + + it('should return null when event not found', async () => { + await testPrivateMethod(service, 'findImportedEvent', async (method) => { + CalendarEventMock.findOneByExternalIdAndUserId.resolves(null); + + const result = await method(fakeExternalId, fakeUserId); + expect(result).to.be.null; + }); + }); + }); + + describe('Private: sendEventNotification', () => { + it('should not send notification if user preference is disabled', async () => { + await testPrivateMethod(service, 'sendEventNotification', async (method) => { + getUserPreferenceMock.resolves(false); + + const fakeEvent = { + _id: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + }; + + await method(fakeEvent); + + sinon.assert.calledWith(getUserPreferenceMock, fakeUserId, 'notifyCalendarEvents'); + sinon.assert.notCalled(api.broadcast as sinon.SinonStub); + }); + }); + + it('should send notification with correct event data', async () => { + await testPrivateMethod(service, 'sendEventNotification', async (method) => { + getUserPreferenceMock.resolves(true); + + const fakeEvent = { + _id: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + }; + + await method(fakeEvent); + + sinon.assert.calledWith( + api.broadcast as sinon.SinonStub, + 'notify.calendar', + fakeUserId, + sinon.match({ + title: fakeSubject, + payload: { _id: fakeEventId }, + }), + ); + }); + }); + }); + + describe('Private: sendCurrentNotifications', () => { + it('should send notification for all events and flag them as sent', async () => { + await testPrivateMethod(service, 'sendCurrentNotifications', async (method) => { + const proto = Object.getPrototypeOf(service); + (proto.sendEventNotification as sinon.SinonStub).restore(); + sandbox.stub(proto, 'sendEventNotification').resolves(); + + const fakeDate = new Date('2025-01-01T10:00:00Z'); + const fakeEvents = [ + { _id: 'event1', uid: fakeUserId, startTime: fakeStartTime }, + { _id: 'event2', uid: fakeUserId, startTime: fakeStartTime }, + ]; + + CalendarEventMock.findEventsToNotify.returns({ + toArray: sinon.stub().resolves(fakeEvents), + }); + + await method(fakeDate); + + sinon.assert.calledWith(CalendarEventMock.findEventsToNotify, fakeDate, 1); + sinon.assert.calledTwice(proto.sendEventNotification as sinon.SinonStub); + sinon.assert.calledTwice(CalendarEventMock.flagNotificationSent); + sinon.assert.calledWith(CalendarEventMock.flagNotificationSent, 'event1'); + sinon.assert.calledWith(CalendarEventMock.flagNotificationSent, 'event2'); + sinon.assert.calledOnceWithExactly(proto.doSetupNextNotification as sinon.SinonStub, true); + }); + }); + }); + + describe('Private: doSetupNextNotification', () => { + it('should remove calendar-reminders cron job if no events found', async () => { + await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { + CalendarEventMock.findNextNotificationDate.resolves(null); + cronJobsMock.jobNames.add('calendar-reminders'); + + await method(false); + + expect(cronJobsMock.jobNames.has('calendar-reminders')).to.false; + }); + }); + + it('should schedule notifications at the next date', async () => { + await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { + const nextDate = new Date('2025-01-01T10:00:00Z'); + CalendarEventMock.findNextNotificationDate.resolves(nextDate); + + await method(false); + + expect(cronJobsMock.jobNames.has('calendar-reminders')).to.true; + }); + }); + + it('should send current notifications if date is in the past', async () => { + await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { + const proto = Object.getPrototypeOf(service); + (proto.sendCurrentNotifications as sinon.SinonStub).restore(); + sandbox.stub(proto, 'sendCurrentNotifications').resolves(); + + const pastDate = new Date(); + pastDate.setMinutes(pastDate.getMinutes() - 10); + CalendarEventMock.findNextNotificationDate.resolves(pastDate); + + await method(false); + + sinon.assert.calledWith(proto.sendCurrentNotifications as sinon.SinonStub, pastDate); + expect(cronJobsMock.jobNames.size).to.equal(0); + }); + }); + + it('should schedule future notifications even if date is in the past when recursive', async () => { + await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { + const pastDate = new Date(); + pastDate.setMinutes(pastDate.getMinutes() - 10); + CalendarEventMock.findNextNotificationDate.resolves(pastDate); + + await method(true); + + sinon.assert.notCalled(service.sendCurrentNotifications as sinon.SinonStub); + expect(cronJobsMock.jobNames.size).to.equal(1); + }); + }); + }); + + describe('Overlapping events', () => { + it('should not set up status change if no endTime is provided when updating', async () => { + const fakeEvent = { + _id: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + subject: fakeSubject, + }; + + CalendarEventMock.findOne.resolves(fakeEvent); + + await service.update(fakeEventId, { + subject: 'New Subject', + }); + + sinon.assert.notCalled(statusEventManagerMock.setupAppointmentStatusChange); + }); + + it('should cancel upcoming status changes for a user', async () => { + const customDate = new Date('2025-02-01'); + + await service.cancelUpcomingStatusChanges(fakeUserId, customDate); + + sinon.assert.calledOnce(statusEventManagerMock.cancelUpcomingStatusChanges); + sinon.assert.calledWith(statusEventManagerMock.cancelUpcomingStatusChanges, fakeUserId, customDate); + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/calendar/statusEvents/applyStatusChange.ts b/apps/meteor/tests/unit/server/services/calendar/statusEvents/applyStatusChange.ts new file mode 100644 index 0000000000000..30053e86cc1e7 --- /dev/null +++ b/apps/meteor/tests/unit/server/services/calendar/statusEvents/applyStatusChange.ts @@ -0,0 +1,169 @@ +import { api } from '@rocket.chat/core-services'; +import { UserStatus } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { describe, it, beforeEach, afterEach } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const UsersMock = { + findOneById: sinon.stub(), + updateOne: sinon.stub(), + updateStatusAndStatusDefault: sinon.stub().resolves(), +}; + +const setupAppointmentStatusChange = sinon.stub().resolves(); + +const { applyStatusChange } = proxyquire.noCallThru().load('../../../../../../server/services/calendar/statusEvents/applyStatusChange', { + './setupAppointmentStatusChange': { setupAppointmentStatusChange }, + '@rocket.chat/core-services': { api }, + '@rocket.chat/models': { + Users: UsersMock, + }, +}); + +describe('Calendar.StatusEvents', () => { + let sandbox: sinon.SinonSandbox; + const fakeEventId = 'eventId123'; + const fakeUserId = 'userId456'; + const fakeStartTime = new Date('2025-01-01T10:00:00Z'); + const fakeEndTime = new Date('2025-01-01T11:00:00Z'); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + setupUsersMocks(); + setupOtherMocks(); + }); + + function setupUsersMocks() { + const freshMocks = { + findOneById: sinon.stub().resolves({ + _id: fakeUserId, + status: UserStatus.ONLINE, + roles: ['user'], + username: 'testuser', + name: 'Test User', + } as any), + updateOne: sinon.stub().resolves({ modifiedCount: 1 } as any), + updateStatusAndStatusDefault: sinon.stub().resolves(), + }; + + Object.assign(UsersMock, freshMocks); + } + + function setupOtherMocks() { + sandbox.stub(api, 'broadcast').resolves(); + + setupAppointmentStatusChange.resetHistory(); + } + + afterEach(() => { + sandbox.restore(); + }); + + describe('#applyStatusChange', () => { + it('should do nothing if user is not found', async () => { + UsersMock.findOneById.resolves(null); + + await applyStatusChange({ + eventId: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + endTime: fakeEndTime, + status: undefined, + shouldScheduleRemoval: false, + }); + + expect(UsersMock.updateStatusAndStatusDefault.callCount).to.equal(0); + expect((api.broadcast as sinon.SinonStub).callCount).to.equal(0); + }); + + it('should do nothing if user is offline', async () => { + UsersMock.findOneById.resolves({ + _id: fakeUserId, + status: UserStatus.OFFLINE, + }); + + await applyStatusChange({ + eventId: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + endTime: fakeEndTime, + status: undefined, + shouldScheduleRemoval: false, + }); + + expect(UsersMock.updateStatusAndStatusDefault.callCount).to.equal(0); + expect((api.broadcast as sinon.SinonStub).callCount).to.equal(0); + }); + + it('should use UserStatus.BUSY as default if no status provided', async () => { + UsersMock.updateStatusAndStatusDefault.resetHistory(); + + await applyStatusChange({ + eventId: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + endTime: fakeEndTime, + status: undefined, + shouldScheduleRemoval: false, + }); + + expect(UsersMock.updateStatusAndStatusDefault.callCount).to.equal(1); + expect(UsersMock.updateStatusAndStatusDefault.firstCall.args[1]).to.equal(UserStatus.BUSY); + }); + + it('should update user status and broadcast presence update', async () => { + const previousStatus = UserStatus.ONLINE; + const newStatus = UserStatus.AWAY; + + UsersMock.updateStatusAndStatusDefault.resetHistory(); + (api.broadcast as sinon.SinonStub).resetHistory(); + + UsersMock.findOneById.resolves({ + _id: fakeUserId, + status: previousStatus, + roles: ['user'], + username: 'testuser', + name: 'Test User', + }); + + await applyStatusChange({ + eventId: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + endTime: fakeEndTime, + status: newStatus, + shouldScheduleRemoval: false, + }); + + expect(UsersMock.updateStatusAndStatusDefault.callCount).to.equal(1); + expect(UsersMock.updateStatusAndStatusDefault.firstCall.args).to.deep.equal([fakeUserId, newStatus, newStatus]); + + expect((api.broadcast as sinon.SinonStub).callCount).to.equal(1); + expect((api.broadcast as sinon.SinonStub).firstCall.args[0]).to.equal('presence.status'); + }); + + it('should schedule status revert when shouldScheduleRemoval=true', async () => { + const previousStatus = UserStatus.ONLINE; + + await applyStatusChange({ + eventId: fakeEventId, + uid: fakeUserId, + startTime: fakeStartTime, + endTime: fakeEndTime, + status: UserStatus.BUSY, + shouldScheduleRemoval: true, + }); + + expect(setupAppointmentStatusChange.callCount).to.equal(1); + expect(setupAppointmentStatusChange.firstCall.args).to.deep.equal([ + fakeEventId, + fakeUserId, + fakeStartTime, + fakeEndTime, + previousStatus, + false, + ]); + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/calendar/statusEvents/cancelUpcomingStatusChanges.tests.ts b/apps/meteor/tests/unit/server/services/calendar/statusEvents/cancelUpcomingStatusChanges.tests.ts new file mode 100644 index 0000000000000..3b0e23b8ffd32 --- /dev/null +++ b/apps/meteor/tests/unit/server/services/calendar/statusEvents/cancelUpcomingStatusChanges.tests.ts @@ -0,0 +1,82 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +import { MockedCronJobs } from '../mocks/cronJobs'; + +const settingsMock = new Map(); + +const fakeUserId = 'userId456'; +const CalendarEventMock = { + findEligibleEventsForCancelation: sinon.stub().returns({ + toArray: sinon.stub().resolves([ + { _id: 'event1', uid: fakeUserId }, + { _id: 'event2', uid: fakeUserId }, + ]), + }), +}; + +const cronJobsMock = new MockedCronJobs(); + +const { cancelUpcomingStatusChanges } = proxyquire + .noCallThru() + .load('../../../../../../server/services/calendar/statusEvents/cancelUpcomingStatusChanges', { + '../../../../app/settings/server': { settings: settingsMock }, + '@rocket.chat/cron': { cronJobs: cronJobsMock }, + '@rocket.chat/models': { + CalendarEvent: CalendarEventMock, + }, + }); + +describe('Calendar.StatusEvents', () => { + describe('#cancelUpcomingStatusChanges', () => { + it('should do nothing if busy status setting is disabled', async () => { + settingsMock.set('Calendar_BusyStatus_Enabled', false); + + const events = [ + { _id: 'event1', uid: fakeUserId }, + { _id: 'event2', uid: fakeUserId }, + ]; + + cronJobsMock.jobNames.clear(); + cronJobsMock.jobNames.add(`calendar-presence-status-event1-${fakeUserId}`); + cronJobsMock.jobNames.add(`calendar-presence-status-event2-${fakeUserId}`); + cronJobsMock.jobNames.add(`calendar-presence-status-event3-${fakeUserId}`); + + CalendarEventMock.findEligibleEventsForCancelation.returns({ + toArray: sinon.stub().resolves(events), + }); + + await cancelUpcomingStatusChanges(fakeUserId); + + expect(cronJobsMock.jobNames.has(`calendar-presence-status-event1-${fakeUserId}`)).to.true; + expect(cronJobsMock.jobNames.has(`calendar-presence-status-event2-${fakeUserId}`)).to.true; + expect(cronJobsMock.jobNames.has(`calendar-presence-status-event3-${fakeUserId}`)).to.true; + }); + + it('should find and cancel active events', async () => { + settingsMock.set('Calendar_BusyStatus_Enabled', true); + + const events = [ + { _id: 'event1', uid: fakeUserId }, + { _id: 'event2', uid: fakeUserId }, + ]; + + cronJobsMock.jobNames.clear(); + cronJobsMock.jobNames.add(`calendar-presence-status-event1-${fakeUserId}`); + cronJobsMock.jobNames.add(`calendar-presence-status-event2-${fakeUserId}`); + cronJobsMock.jobNames.add(`calendar-presence-status-event3-${fakeUserId}`); + + CalendarEventMock.findEligibleEventsForCancelation.returns({ + toArray: sinon.stub().resolves(events), + }); + + await cancelUpcomingStatusChanges(fakeUserId); + + expect(cronJobsMock.jobNames.has(`calendar-presence-status-event1-${fakeUserId}`)).to.false; + expect(cronJobsMock.jobNames.has(`calendar-presence-status-event2-${fakeUserId}`)).to.false; + expect(cronJobsMock.jobNames.has(`calendar-presence-status-event3-${fakeUserId}`)).to.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/calendar/statusEvents/generateCronJobId.tests.ts b/apps/meteor/tests/unit/server/services/calendar/statusEvents/generateCronJobId.tests.ts new file mode 100644 index 0000000000000..cbc8ab277c5b1 --- /dev/null +++ b/apps/meteor/tests/unit/server/services/calendar/statusEvents/generateCronJobId.tests.ts @@ -0,0 +1,38 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; + +const { generateCronJobId } = proxyquire.noCallThru().load('../../../../../../server/services/calendar/statusEvents/generateCronJobId', {}); + +describe('#generateCronJobId', () => { + const fakeEventId = 'eventId123'; + const fakeUserId = 'userId456'; + + it('should generate correct ID for status events', () => { + const id = generateCronJobId(fakeEventId, fakeUserId, 'status'); + expect(id).to.equal(`calendar-presence-status-${fakeEventId}-${fakeUserId}`); + }); + + it('should generate correct ID for reminder events', () => { + const id = generateCronJobId(fakeEventId, fakeUserId, 'reminder'); + expect(id).to.equal(`calendar-reminder-${fakeEventId}-${fakeUserId}`); + }); + + it('should throw an error if some required parameters are missing', () => { + expect(() => generateCronJobId(undefined, fakeUserId, 'status')).to.throw( + 'Missing required parameters. Please provide eventId, uid and eventType (status or reminder)', + ); + expect(() => generateCronJobId(fakeEventId, undefined, 'status')).to.throw( + 'Missing required parameters. Please provide eventId, uid and eventType (status or reminder)', + ); + expect(() => generateCronJobId(fakeEventId, fakeUserId)).to.throw( + 'Missing required parameters. Please provide eventId, uid and eventType (status or reminder)', + ); + }); + + it('should throw an error if eventType is not "status" or "reminder"', () => { + expect(() => generateCronJobId(fakeEventId, fakeUserId, 'invalid' as any)).to.throw( + 'Missing required parameters. Please provide eventId, uid and eventType (status or reminder)', + ); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/calendar/statusEvents/handleOverlappingEvents.ts b/apps/meteor/tests/unit/server/services/calendar/statusEvents/handleOverlappingEvents.ts new file mode 100644 index 0000000000000..d9997d4bac80f --- /dev/null +++ b/apps/meteor/tests/unit/server/services/calendar/statusEvents/handleOverlappingEvents.ts @@ -0,0 +1,164 @@ +import { UserStatus } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { describe, it, beforeEach } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +import { MockedCronJobs } from '../mocks/cronJobs'; + +const CalendarEventMock = { + findOverlappingEvents: sinon.stub(), +}; + +const cronJobsMock = new MockedCronJobs(); + +const applyStatusChange = sinon.stub(); + +const { handleOverlappingEvents } = proxyquire + .noCallThru() + .load('../../../../../../server/services/calendar/statusEvents/handleOverlappingEvents', { + './applyStatusChange': { applyStatusChange }, + '@rocket.chat/cron': { cronJobs: cronJobsMock }, + '@rocket.chat/models': { + CalendarEvent: CalendarEventMock, + }, + }); + +describe('Calendar.StatusEvents', () => { + const fakeEventId = 'eventId123'; + const fakeUserId = 'userId456'; + const fakeStartTime = new Date('2025-01-01T10:00:00Z'); + const fakeEndTime = new Date('2025-01-01T11:00:00Z'); + const statusId = `calendar-presence-status-${fakeEventId}-${fakeUserId}`; + const containedStatusId = `calendar-presence-status-containedEvent-${fakeUserId}`; + + beforeEach(() => { + cronJobsMock.jobNames.clear(); + setupCalendarEventMocks(); + applyStatusChange.resetHistory(); + }); + + function setupCalendarEventMocks() { + CalendarEventMock.findOverlappingEvents.reset(); + CalendarEventMock.findOverlappingEvents.returns({ + toArray: sinon.stub().resolves([]), + }); + } + + describe('#handleOverlappingEvents', () => { + it('should return shouldProceed=true when no overlapping events', async () => { + // Clear previous calls + CalendarEventMock.findOverlappingEvents.reset(); + + // Set up the mock to return no overlapping events + CalendarEventMock.findOverlappingEvents.returns({ + toArray: sinon.stub().resolves([]), + }); + + const result = await handleOverlappingEvents(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY); + + expect(result).to.deep.equal({ shouldProceed: true }); + expect(cronJobsMock.jobNames.size).to.equal(0); + sinon.assert.calledWith(CalendarEventMock.findOverlappingEvents, fakeEventId, fakeUserId, fakeStartTime, fakeEndTime); + }); + + it('should handle case when current event is not the latest ending', async () => { + const laterEvent = { + _id: 'laterEvent', + startTime: fakeStartTime, + endTime: new Date('2025-01-01T12:00:00Z'), // Later than fakeEndTime + }; + + // Mock a specific response for this test + CalendarEventMock.findOverlappingEvents.returns({ + toArray: sinon.stub().resolves([laterEvent]), + }); + + const result = await handleOverlappingEvents(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY); + + expect(result).to.deep.equal({ shouldProceed: false }); + expect(cronJobsMock.jobNames.has(statusId)).to.equal(true); + }); + + it('should remove status jobs for events ending before the current one', async () => { + const earlierEvent = { + _id: 'earlierEvent', + startTime: new Date('2025-01-01T09:00:00Z'), + endTime: new Date('2025-01-01T10:30:00Z'), // Earlier than fakeEndTime + }; + + // Set up has to return true for the specific job ID + cronJobsMock.jobNames.add(statusId); + + // Mock a specific response for this test + CalendarEventMock.findOverlappingEvents.returns({ + toArray: sinon.stub().resolves([earlierEvent]), + }); + + const result = await handleOverlappingEvents(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY); + + expect(result).to.deep.equal({ shouldProceed: true }); + expect(cronJobsMock.jobNames.has(statusId)).to.equal(false); + }); + + it('should handle multiple overlapping events with different end times', async () => { + const earlierEvent = { + _id: 'earlierEvent', + startTime: new Date('2025-01-01T09:00:00Z'), + endTime: new Date('2025-01-01T10:30:00Z'), // Earlier than fakeEndTime + }; + + const laterEvent = { + _id: 'laterEvent', + startTime: new Date('2025-01-01T10:30:00Z'), + endTime: new Date('2025-01-01T12:00:00Z'), // Later than fakeEndTime + }; + + CalendarEventMock.findOverlappingEvents.returns({ + toArray: sinon.stub().resolves([earlierEvent, laterEvent]), + }); + + const result = await handleOverlappingEvents(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY); + + expect(result).to.deep.equal({ shouldProceed: false }); + expect(cronJobsMock.jobNames.has(statusId)).to.be.true; + }); + + it('should handle an event completely contained within the current event', async () => { + const containedEvent = { + _id: 'containedEvent', + startTime: new Date('2025-01-01T10:15:00Z'), // After fakeStartTime + endTime: new Date('2025-01-01T10:45:00Z'), // Before fakeEndTime + }; + + cronJobsMock.jobNames.add(statusId); + + CalendarEventMock.findOverlappingEvents.returns({ + toArray: sinon.stub().resolves([containedEvent]), + }); + + const result = await handleOverlappingEvents(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY); + + expect(result).to.deep.equal({ shouldProceed: true }); + expect(cronJobsMock.jobNames.has(statusId)).to.be.false; + expect(cronJobsMock.jobNames.has(containedStatusId)).to.be.true; + }); + + it('should handle an event that completely contains the current event', async () => { + const containingEvent = { + _id: 'containingEvent', + startTime: new Date('2025-01-01T09:00:00Z'), // Before fakeStartTime + endTime: new Date('2025-01-01T12:00:00Z'), // After fakeEndTime + }; + + CalendarEventMock.findOverlappingEvents.returns({ + toArray: sinon.stub().resolves([containingEvent]), + }); + + const result = await handleOverlappingEvents(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY); + + expect(result).to.deep.equal({ shouldProceed: false }); + expect(cronJobsMock.jobNames.has(statusId)).to.be.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/calendar/statusEvents/removeCronJobs.tests.ts b/apps/meteor/tests/unit/server/services/calendar/statusEvents/removeCronJobs.tests.ts new file mode 100644 index 0000000000000..ec160abe51d25 --- /dev/null +++ b/apps/meteor/tests/unit/server/services/calendar/statusEvents/removeCronJobs.tests.ts @@ -0,0 +1,54 @@ +import { expect } from 'chai'; +import { describe, it, beforeEach } from 'mocha'; +import proxyquire from 'proxyquire'; + +import { MockedCronJobs } from '../mocks/cronJobs'; + +const cronJobsMock = new MockedCronJobs(); + +const { removeCronJobs } = proxyquire.noCallThru().load('../../../../../../server/services/calendar/statusEvents/removeCronJobs', { + '@rocket.chat/cron': { cronJobs: cronJobsMock }, +}); + +describe('Calendar.StatusEvents', () => { + const fakeEventId = 'eventId123'; + const fakeUserId = 'userId456'; + const fakeUserId2 = 'userId4562'; + + const statusId = `calendar-presence-status-${fakeEventId}-${fakeUserId}`; + const reminderId = `calendar-reminder-${fakeEventId}-${fakeUserId}`; + const statusId2 = `calendar-presence-status-${fakeEventId}-${fakeUserId2}`; + const reminderId2 = `calendar-reminder-${fakeEventId}-${fakeUserId2}`; + + beforeEach(() => { + cronJobsMock.jobNames.clear(); + }); + + describe('#removeCronJobs', () => { + it('should check and remove status and reminder jobs', async () => { + cronJobsMock.jobNames.clear(); + cronJobsMock.jobNames.add(statusId); + cronJobsMock.jobNames.add(reminderId); + + await removeCronJobs(fakeEventId, fakeUserId); + + expect(cronJobsMock.jobNames.has(statusId)).to.equal(false); + expect(cronJobsMock.jobNames.has(reminderId)).to.equal(false); + }); + + it('should not remove jobs from other users', async () => { + cronJobsMock.jobNames.clear(); + cronJobsMock.jobNames.add(statusId); + cronJobsMock.jobNames.add(reminderId); + cronJobsMock.jobNames.add(statusId2); + cronJobsMock.jobNames.add(reminderId2); + + await removeCronJobs(fakeEventId, fakeUserId); + + expect(cronJobsMock.jobNames.has(statusId)).to.equal(false); + expect(cronJobsMock.jobNames.has(reminderId)).to.equal(false); + expect(cronJobsMock.jobNames.has(statusId2)).to.equal(true); + expect(cronJobsMock.jobNames.has(reminderId2)).to.equal(true); + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts b/apps/meteor/tests/unit/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts new file mode 100644 index 0000000000000..7df232de9c700 --- /dev/null +++ b/apps/meteor/tests/unit/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts @@ -0,0 +1,77 @@ +import { UserStatus } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { describe, it, beforeEach } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +import { MockedCronJobs } from '../mocks/cronJobs'; + +const settingsMock = new Map(); +const cronJobsMock = new MockedCronJobs(); + +const applyStatusChange = sinon.stub(); +const handleOverlappingEvents = sinon.stub(); + +const { setupAppointmentStatusChange } = proxyquire + .noCallThru() + .load('../../../../../../server/services/calendar/statusEvents/setupAppointmentStatusChange', { + './applyStatusChange': { applyStatusChange }, + './handleOverlappingEvents': { handleOverlappingEvents }, + '../../../../app/settings/server': { settings: settingsMock }, + '@rocket.chat/cron': { cronJobs: cronJobsMock }, + }); + +describe('Calendar.StatusEvents', () => { + const fakeEventId = 'eventId123'; + const fakeUserId = 'userId456'; + const fakeStartTime = new Date('2025-01-01T10:00:00Z'); + const fakeEndTime = new Date('2025-01-01T11:00:00Z'); + const statusId = `calendar-presence-status-${fakeEventId}-${fakeUserId}`; + + beforeEach(() => { + cronJobsMock.jobNames.clear(); + applyStatusChange.resetHistory(); + handleOverlappingEvents.resetHistory(); + settingsMock.clear(); + settingsMock.set('Calendar_BusyStatus_Enabled', true); + }); + + describe('#setupAppointmentStatusChange', () => { + it('should do nothing if busy status setting is disabled', async () => { + settingsMock.set('Calendar_BusyStatus_Enabled', false); + + await setupAppointmentStatusChange(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, undefined, false); + + expect(cronJobsMock.jobNames.size).to.equal(0); + }); + + it('should do nothing if endTime is not provided', async () => { + await setupAppointmentStatusChange(fakeEventId, fakeUserId, fakeStartTime, undefined, undefined, false); + + expect(cronJobsMock.jobNames.size).to.equal(0); + }); + + it('should handle overlapping events when shouldScheduleRemoval=true', async () => { + handleOverlappingEvents.resolves({ shouldProceed: false }); + + await setupAppointmentStatusChange(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY, true); + + expect(handleOverlappingEvents.callCount).to.equal(1); + expect(cronJobsMock.jobNames.size).to.equal(0); + }); + + it('should schedule status change at the start time when shouldScheduleRemoval=true', async () => { + handleOverlappingEvents.resolves({ shouldProceed: true }); + + await setupAppointmentStatusChange(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY, true); + + expect(cronJobsMock.jobNames.has(statusId)).to.true; + }); + + it('should schedule status change at the end time when shouldScheduleRemoval=false', async () => { + await setupAppointmentStatusChange(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY, false); + + expect(cronJobsMock.jobNames.has(statusId)).to.true; + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/calendar/utils/getShiftedTime.tests.ts b/apps/meteor/tests/unit/server/services/calendar/utils/getShiftedTime.tests.ts new file mode 100644 index 0000000000000..8f068589a895a --- /dev/null +++ b/apps/meteor/tests/unit/server/services/calendar/utils/getShiftedTime.tests.ts @@ -0,0 +1,22 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; + +const { getShiftedTime } = proxyquire.noCallThru().load('../../../../../../server/services/calendar/utils/getShiftedTime', {}); + +describe('#getShiftedTime', () => { + it('should shift time forward by minutes', () => { + const date = new Date('2025-01-01T10:00:00Z'); + const result = getShiftedTime(date, 30); + + expect(result.getTime()).to.equal(new Date('2025-01-01T10:30:00Z').getTime()); + expect(date.getTime()).to.equal(new Date('2025-01-01T10:00:00Z').getTime()); + }); + + it('should shift time backward by negative minutes', () => { + const date = new Date('2025-01-01T10:00:00Z'); + const result = getShiftedTime(date, -15); + + expect(result.getTime()).to.equal(new Date('2025-01-01T09:45:00Z').getTime()); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/utils.ts b/apps/meteor/tests/unit/server/services/utils.ts new file mode 100644 index 0000000000000..20ab317cbd641 --- /dev/null +++ b/apps/meteor/tests/unit/server/services/utils.ts @@ -0,0 +1,28 @@ +import sinon from 'sinon'; + +export async function testPrivateMethod any>( + service: any, + methodName: string, + testFn: (method: T) => Promise | void, +): Promise { + const proto = Object.getPrototypeOf(service); + const originalMethod = proto[methodName]; + const isStubbed = originalMethod && 'restore' in originalMethod; + + if (isStubbed) { + (originalMethod as sinon.SinonStub).restore(); + } + + const method = proto[methodName]; + void testFn(method.bind(service)); + + if (isStubbed) { + sinon.stub(proto, methodName).callsFake(originalMethod); + } +} + +export function createFreshServiceInstance(moduleExports: any, serviceName?: string): T { + const ServiceClass = serviceName ? moduleExports[serviceName] : Object.values(moduleExports)[0]; + + return new ServiceClass(); +} diff --git a/packages/core-services/src/types/ICalendarService.ts b/packages/core-services/src/types/ICalendarService.ts index 098dccadd2e86..f74b63b056b81 100644 --- a/packages/core-services/src/types/ICalendarService.ts +++ b/packages/core-services/src/types/ICalendarService.ts @@ -7,9 +7,8 @@ export interface ICalendarService { import(data: Omit, 'notificationSent'>): Promise; get(eventId: ICalendarEvent['_id']): Promise; list(uid: IUser['_id'], date: Date): Promise; - update(eventId: ICalendarEvent['_id'], data: Partial): Promise; + update(eventId: ICalendarEvent['_id'], data: Partial): Promise; delete(eventId: ICalendarEvent['_id']): Promise; - findImportedEvent(externalId: Required['externalId'], uid: ICalendarEvent['uid']): Promise; - parseDescriptionForMeetingUrl(description: string): Promise; setupNextNotification(): Promise; + cancelUpcomingStatusChanges(uid: IUser['_id'], endTime?: Date): Promise; } diff --git a/packages/core-typings/src/ICalendarEvent.ts b/packages/core-typings/src/ICalendarEvent.ts index 6bb0a7bb58e35..fb58bde124c43 100644 --- a/packages/core-typings/src/ICalendarEvent.ts +++ b/packages/core-typings/src/ICalendarEvent.ts @@ -3,6 +3,8 @@ import type { IUser } from './IUser'; export interface ICalendarEvent extends IRocketChatRecord { startTime: Date; + endTime?: Date; + uid: IUser['_id']; subject: string; description: string; @@ -13,4 +15,6 @@ export interface ICalendarEvent extends IRocketChatRecord { reminderMinutesBeforeStart?: number; reminderTime?: Date; + + busy?: boolean; } diff --git a/packages/model-typings/src/models/ICalendarEventModel.ts b/packages/model-typings/src/models/ICalendarEventModel.ts index 4d2f9035237c9..24c4bc30ac0a3 100644 --- a/packages/model-typings/src/models/ICalendarEventModel.ts +++ b/packages/model-typings/src/models/ICalendarEventModel.ts @@ -13,4 +13,6 @@ export interface ICalendarEventModel extends IBaseModel { externalId: Required['externalId'], uid: ICalendarEvent['uid'], ): Promise; + findOverlappingEvents(eventId: ICalendarEvent['_id'], uid: IUser['_id'], startTime: Date, endTime: Date): FindCursor; + findEligibleEventsForCancelation(uid: IUser['_id'], endTime: Date): FindCursor; } diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 73bcbb5c02812..b3503b493bf2e 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -226,6 +226,8 @@ export interface IUsersModel extends IBaseModel { }: { statusDefault?: UserStatus; status: UserStatus; statusConnection: UserStatus; statusText?: string }, ): Promise; + updateStatusAndStatusDefault(userId: string, status: UserStatus, statusDefault: UserStatus): Promise; + setFederationAvatarUrlById(userId: IUser['_id'], federationAvatarUrl: string): Promise; findSearchedServerNamesByUserId(userId: IUser['_id']): Promise; diff --git a/packages/models/src/models/CalendarEvent.ts b/packages/models/src/models/CalendarEvent.ts index a446dec168b33..4de0c4a638e1a 100644 --- a/packages/models/src/models/CalendarEvent.ts +++ b/packages/models/src/models/CalendarEvent.ts @@ -121,4 +121,32 @@ export class CalendarEventRaw extends BaseRaw implements ICalend }, ); } + + public findOverlappingEvents( + eventId: ICalendarEvent['_id'], + uid: IUser['_id'], + startTime: Date, + endTime: Date, + ): FindCursor { + return this.find({ + _id: { $ne: eventId }, // Exclude current event + uid, + $or: [ + // Event starts during our event + { startTime: { $gte: startTime, $lt: endTime } }, + // Event ends during our event + { endTime: { $gt: startTime, $lte: endTime } }, + // Event completely contains our event + { startTime: { $lte: startTime }, endTime: { $gte: endTime } }, + ], + }); + } + + public findEligibleEventsForCancelation(uid: IUser['_id'], endTime: Date): FindCursor { + return this.find({ + uid, + startTime: { $exists: true, $lte: endTime }, + endTime: { $exists: true, $gte: endTime }, + }); + } } diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index 9f9cb54886e32..4408d494c2404 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -990,6 +990,27 @@ 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, + }, + }; + + return this.updateOne({ _id }, update); + } + + updateStatusAndStatusDefault(_id: IUser['_id'], status: UserStatus, statusDefault: UserStatus) { + const update = { + $set: { + status, + statusDefault, + }, + }; + + return this.updateOne({ _id }, update); + } + updateStatusByAppId(appId: string, status: UserStatus) { const query = { appId, diff --git a/packages/rest-typings/src/v1/calendar/CalendarEventCreateProps.ts b/packages/rest-typings/src/v1/calendar/CalendarEventCreateProps.ts index 5358510c97abe..2a2e378d1b6e7 100644 --- a/packages/rest-typings/src/v1/calendar/CalendarEventCreateProps.ts +++ b/packages/rest-typings/src/v1/calendar/CalendarEventCreateProps.ts @@ -5,11 +5,13 @@ const ajv = new Ajv(); export type CalendarEventCreateProps = { startTime: string; + endTime?: string; externalId?: string; subject: string; description: string; meetingUrl?: string; reminderMinutesBeforeStart?: number; + busy?: boolean; }; const calendarEventCreatePropsSchema: JSONSchemaType = { @@ -19,6 +21,10 @@ const calendarEventCreatePropsSchema: JSONSchemaType = type: 'string', nullable: false, }, + endTime: { + type: 'string', + nullable: true, + }, externalId: { type: 'string', nullable: true, @@ -39,6 +45,10 @@ const calendarEventCreatePropsSchema: JSONSchemaType = type: 'number', nullable: true, }, + busy: { + type: 'boolean', + nullable: true, + }, }, required: ['startTime', 'subject', 'description'], additionalProperties: false, diff --git a/packages/rest-typings/src/v1/calendar/CalendarEventImportProps.ts b/packages/rest-typings/src/v1/calendar/CalendarEventImportProps.ts index 955184a36115c..af9cbce1086a0 100644 --- a/packages/rest-typings/src/v1/calendar/CalendarEventImportProps.ts +++ b/packages/rest-typings/src/v1/calendar/CalendarEventImportProps.ts @@ -5,11 +5,13 @@ const ajv = new Ajv(); export type CalendarEventImportProps = { startTime: string; + endTime?: string; externalId: string; subject: string; description: string; meetingUrl?: string; reminderMinutesBeforeStart?: number; + busy?: boolean; }; const calendarEventImportPropsSchema: JSONSchemaType = { @@ -19,6 +21,10 @@ const calendarEventImportPropsSchema: JSONSchemaType = type: 'string', nullable: false, }, + endTime: { + type: 'string', + nullable: true, + }, externalId: { type: 'string', nullable: false, @@ -39,6 +45,10 @@ const calendarEventImportPropsSchema: JSONSchemaType = type: 'number', nullable: true, }, + busy: { + type: 'boolean', + nullable: true, + }, }, required: ['startTime', 'externalId', 'subject', 'description'], additionalProperties: false, diff --git a/packages/rest-typings/src/v1/calendar/CalendarEventUpdateProps.ts b/packages/rest-typings/src/v1/calendar/CalendarEventUpdateProps.ts index 1004cd09990e8..326a1fb20ea59 100644 --- a/packages/rest-typings/src/v1/calendar/CalendarEventUpdateProps.ts +++ b/packages/rest-typings/src/v1/calendar/CalendarEventUpdateProps.ts @@ -7,10 +7,12 @@ const ajv = new Ajv(); export type CalendarEventUpdateProps = { eventId: ICalendarEvent['_id']; startTime: string; + endTime?: string; subject: string; description: string; meetingUrl?: string; reminderMinutesBeforeStart?: number; + busy?: boolean; }; const calendarEventUpdatePropsSchema: JSONSchemaType = { @@ -24,6 +26,10 @@ const calendarEventUpdatePropsSchema: JSONSchemaType = type: 'string', nullable: false, }, + endTime: { + type: 'string', + nullable: true, + }, subject: { type: 'string', nullable: false, @@ -40,6 +46,10 @@ const calendarEventUpdatePropsSchema: JSONSchemaType = type: 'number', nullable: true, }, + busy: { + type: 'boolean', + nullable: true, + }, }, required: ['eventId', 'startTime', 'subject', 'description'], additionalProperties: false, From 2f3a60263fd95174b9fe3f52f2b3ba0a748ec377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Fri, 21 Mar 2025 01:54:23 -0300 Subject: [PATCH 015/187] refactor: remove KonchatNotification `showDesktop` from meteor (#35576) --- .../app/ui/client/lib/KonchatNotification.ts | 34 +--------------- .../notification/useDesktopNotification.ts | 40 +++++++++++++++++++ .../hooks/notification/useNotifyUser.ts | 7 ++-- 3 files changed, 45 insertions(+), 36 deletions(-) create mode 100644 apps/meteor/client/hooks/notification/useDesktopNotification.ts diff --git a/apps/meteor/app/ui/client/lib/KonchatNotification.ts b/apps/meteor/app/ui/client/lib/KonchatNotification.ts index 3b1e1f285f2aa..cebd0aa4324f1 100644 --- a/apps/meteor/app/ui/client/lib/KonchatNotification.ts +++ b/apps/meteor/app/ui/client/lib/KonchatNotification.ts @@ -1,14 +1,11 @@ -import type { INotificationDesktop, IUser } from '@rocket.chat/core-typings'; +import type { INotificationDesktop } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; -import { RoomManager } from '../../../../client/lib/RoomManager'; import { onClientMessageReceived } from '../../../../client/lib/onClientMessageReceived'; -import { getAvatarAsPng } from '../../../../client/lib/utils/getAvatarAsPng'; import { router } from '../../../../client/providers/RouterProvider'; import { stripTags } from '../../../../lib/utils/stringUtils'; -import { e2e } from '../../../e2e/client'; import { getUserPreference } from '../../../utils/client'; import { getUserAvatarURL } from '../../../utils/client/getUserAvatarURL'; import { sdk } from '../../../utils/client/lib/SDKClient'; @@ -137,35 +134,6 @@ class KonchatNotification { } }; } - - public async showDesktop(notification: INotificationDesktop) { - if (!notification.payload.rid) { - return; - } - - if ( - notification.payload?.rid === RoomManager.opened && - (typeof window.document.hasFocus === 'function' ? window.document.hasFocus() : undefined) - ) { - return; - } - - if ((Meteor.user() as IUser | null)?.status === 'busy') { - return; - } - - if (notification.payload?.message?.t === 'e2e') { - const e2eRoom = await e2e.getInstanceByRoomId(notification.payload.rid); - if (e2eRoom) { - notification.text = (await e2eRoom.decrypt(notification.payload.message.msg)).text; - } - } - - return getAvatarAsPng(notification.payload?.sender?.username, (avatarAsPng) => { - notification.icon = avatarAsPng; - return this.notify(notification); - }); - } } const instance = new KonchatNotification(); diff --git a/apps/meteor/client/hooks/notification/useDesktopNotification.ts b/apps/meteor/client/hooks/notification/useDesktopNotification.ts new file mode 100644 index 0000000000000..cd38506a62fda --- /dev/null +++ b/apps/meteor/client/hooks/notification/useDesktopNotification.ts @@ -0,0 +1,40 @@ +import type { INotificationDesktop } from '@rocket.chat/core-typings'; +import { useUser } from '@rocket.chat/ui-contexts'; +import { useCallback } from 'react'; + +import { e2e } from '../../../app/e2e/client'; +import { KonchatNotification } from '../../../app/ui/client/lib/KonchatNotification'; +import { RoomManager } from '../../lib/RoomManager'; +import { getAvatarAsPng } from '../../lib/utils/getAvatarAsPng'; + +export const useDesktopNotification = () => { + const user = useUser(); + const notifyDesktop = useCallback( + async (notification: INotificationDesktop) => { + if ( + notification.payload.rid === RoomManager.opened && + (typeof window.document.hasFocus === 'function' ? window.document.hasFocus() : undefined) + ) { + return; + } + if (user?.status === 'busy') { + return; + } + + if (notification.payload.message?.t === 'e2e') { + const e2eRoom = await e2e.getInstanceByRoomId(notification.payload.rid); + if (e2eRoom) { + notification.text = (await e2eRoom.decrypt(notification.payload.message.msg)).text; + } + } + + return getAvatarAsPng(notification.payload.sender?.username, (avatarAsPng) => { + notification.icon = avatarAsPng; + return KonchatNotification.notify(notification); + }); + }, + [user?.status], + ); + + return notifyDesktop; +}; diff --git a/apps/meteor/client/hooks/notification/useNotifyUser.ts b/apps/meteor/client/hooks/notification/useNotifyUser.ts index a71dd6c25a0fa..7f2cfb918ab7e 100644 --- a/apps/meteor/client/hooks/notification/useNotifyUser.ts +++ b/apps/meteor/client/hooks/notification/useNotifyUser.ts @@ -4,10 +4,10 @@ import { useRouter, useStream, useUser, useUserPreference } from '@rocket.chat/u import { useEffect } from 'react'; import { useEmbeddedLayout } from '../useEmbeddedLayout'; +import { useDesktopNotification } from './useDesktopNotification'; import { useNewMessageNotification } from './useNewMessageNotification'; import { useNewRoomNotification } from './useNewRoomNotification'; import { CachedChatSubscription } from '../../../app/models/client'; -import { KonchatNotification } from '../../../app/ui/client/lib/KonchatNotification'; import { RoomManager } from '../../lib/RoomManager'; import { fireGlobalEvent } from '../../lib/utils/fireGlobalEvent'; @@ -19,6 +19,7 @@ export const useNotifyUser = () => { const muteFocusedConversations = useUserPreference('muteFocusedConversations'); const newRoomNotification = useNewRoomNotification(); const newMessageNotification = useNewMessageNotification(); + const showDesktopNotification = useDesktopNotification(); const notifyNewRoom = useEffectEvent(async (sub: AtLeast): Promise => { if (!user || user.status === 'busy') { @@ -48,12 +49,12 @@ export const useNotifyUser = () => { if (!hasFocus && messageIsInOpenedRoom) { // Play a notification sound newMessageNotification(notification.payload); - void KonchatNotification.showDesktop(notification); + showDesktopNotification(notification); } } else if (!hasFocus || !messageIsInOpenedRoom || !muteFocusedConversations) { // Play a notification sound newMessageNotification(notification.payload); - void KonchatNotification.showDesktop(notification); + showDesktopNotification(notification); } }); From 600a79341c79a895bee93225c9b9c0c28480068e Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Fri, 21 Mar 2025 19:18:41 +0530 Subject: [PATCH 016/187] fix(federation-v2): can not assign to readonly property #name (#35397) Co-authored-by: Marcos Spessatto Defendi <15324204+MarcosSpessatto@users.noreply.github.com> --- .changeset/odd-waves-destroy.md | 5 +++++ .../application/AbstractFederationApplicationService.ts | 2 +- .../server/services/federation/domain/FederatedUser.ts | 4 ++++ packages/core-typings/package.json | 2 +- packages/core-typings/src/IUser.ts | 1 + 5 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .changeset/odd-waves-destroy.md diff --git a/.changeset/odd-waves-destroy.md b/.changeset/odd-waves-destroy.md new file mode 100644 index 0000000000000..484fe1d3b2616 --- /dev/null +++ b/.changeset/odd-waves-destroy.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a race condition while federating that caused direct messages to not work diff --git a/apps/meteor/server/services/federation/application/AbstractFederationApplicationService.ts b/apps/meteor/server/services/federation/application/AbstractFederationApplicationService.ts index 0bda7529ebe46..5d2ecfbfbb613 100644 --- a/apps/meteor/server/services/federation/application/AbstractFederationApplicationService.ts +++ b/apps/meteor/server/services/federation/application/AbstractFederationApplicationService.ts @@ -70,7 +70,7 @@ export abstract class AbstractFederationApplicationService { return; } if (federatedUser.shouldUpdateDisplayName(displayName)) { - await this.internalUserAdapter.updateRealName(federatedUser.getInternalReference(), displayName); + await this.internalUserAdapter.updateRealName(federatedUser.getInternalReferenceCopy(), displayName); } } diff --git a/apps/meteor/server/services/federation/domain/FederatedUser.ts b/apps/meteor/server/services/federation/domain/FederatedUser.ts index fdd535588b52d..02f08708d9db1 100644 --- a/apps/meteor/server/services/federation/domain/FederatedUser.ts +++ b/apps/meteor/server/services/federation/domain/FederatedUser.ts @@ -70,6 +70,10 @@ export class FederatedUser { }); } + public getInternalReferenceCopy(): IUser { + return structuredClone(this.internalReference); + } + public getStorageRepresentation(): Readonly { return { _id: this.internalId, diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index bc979207939a9..229b982b2ec74 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -14,7 +14,7 @@ "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "echo \"no tests\" && exit 1", "dev": "tsc --watch --preserveWatchOutput -p tsconfig.json", "build": "rm -rf dist && tsc -p tsconfig.json" }, diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index 510d6d4ceed22..f6ae2a0f65347 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -199,6 +199,7 @@ export interface IUser extends IRocketChatRecord { reason?: string; // TODO: move this to a specific federation user type federated?: boolean; + // @deprecated federation?: { avatarUrl?: string; searchedServerNames?: string[]; From 45a93a7713546ed2e3e0b3988e1f989371ebf53a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 21 Mar 2025 11:22:54 -0300 Subject: [PATCH 017/187] fix: Add Apps log TTL index (#35497) Co-authored-by: Diego Sampaio --- .changeset/fifty-moles-boil.md | 7 +++++ apps/meteor/ee/server/apps/startup.ts | 29 +------------------ .../model-typings/src/models/IAppLogsModel.ts | 1 - packages/models/src/models/AppLogsModel.ts | 25 ++++++++++------ packages/models/src/models/BaseRaw.ts | 6 +--- 5 files changed, 25 insertions(+), 43 deletions(-) create mode 100644 .changeset/fifty-moles-boil.md diff --git a/.changeset/fifty-moles-boil.md b/.changeset/fifty-moles-boil.md new file mode 100644 index 0000000000000..d4e1edbf9923c --- /dev/null +++ b/.changeset/fifty-moles-boil.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +"@rocket.chat/models": patch +--- + +Fixes an issue where the app's logs index was not being created by default sometimes, also set to be always 30 days diff --git a/apps/meteor/ee/server/apps/startup.ts b/apps/meteor/ee/server/apps/startup.ts index 683e40dbb6b1e..2b24859ff122f 100644 --- a/apps/meteor/ee/server/apps/startup.ts +++ b/apps/meteor/ee/server/apps/startup.ts @@ -24,7 +24,7 @@ export const startupApp = async function startupApp() { }, ], public: true, - hidden: false, + hidden: true, alert: 'Apps_Logs_TTL_Alert', }); @@ -69,33 +69,6 @@ export const startupApp = async function startupApp() { // Disable apps that depend on add-ons (external modules) if they are invalidated License.onModule(disableAppsWithAddonsCallback); - settings.watch('Apps_Logs_TTL', async (value) => { - // TODO: remove this feature, initialized is always false first time - if (!Apps.isInitialized()) { - return; - } - let expireAfterSeconds = 0; - - switch (value) { - case '7_days': - expireAfterSeconds = 604800; - break; - case '14_days': - expireAfterSeconds = 1209600; - break; - case '30_days': - expireAfterSeconds = 2592000; - break; - } - - if (!expireAfterSeconds) { - return; - } - - const model = Apps._logModel; - await model?.resetTTLIndex(expireAfterSeconds); - }); - Apps.initialize(); void Apps.load(); diff --git a/packages/model-typings/src/models/IAppLogsModel.ts b/packages/model-typings/src/models/IAppLogsModel.ts index 6a6bc765cd8ed..0ff8ae0a00cf5 100644 --- a/packages/model-typings/src/models/IAppLogsModel.ts +++ b/packages/model-typings/src/models/IAppLogsModel.ts @@ -4,6 +4,5 @@ import type { IBaseModel } from './IBaseModel'; // TODO: type for AppLogs export interface IAppLogsModel extends IBaseModel { - resetTTLIndex(expireAfterSeconds: number): Promise; remove(query: Filter): Promise; } diff --git a/packages/models/src/models/AppLogsModel.ts b/packages/models/src/models/AppLogsModel.ts index f9f7db79d86c5..de7301e24743f 100644 --- a/packages/models/src/models/AppLogsModel.ts +++ b/packages/models/src/models/AppLogsModel.ts @@ -4,18 +4,25 @@ import type { Db, DeleteResult, Filter } from 'mongodb'; import { BaseRaw } from './BaseRaw'; export class AppsLogsModel extends BaseRaw implements IAppLogsModel { - constructor(db: Db) { - super(db, 'apps_logs', undefined, { _updatedAtIndexOptions: { expireAfterSeconds: 60 * 60 * 24 * 30 } }); + constructor( + db: Db, + private readonly expireAfterSeconds: number = 60 * 60 * 24 * 30, + ) { + super(db, 'apps_logs', undefined); } - remove(query: Filter): Promise { - return this.col.deleteMany(query); + protected modelIndexes() { + return [ + { + key: { + _updatedAt: 1, + }, + expireAfterSeconds: this.expireAfterSeconds, + }, + ]; } - async resetTTLIndex(expireAfterSeconds: number): Promise { - if (await this.col.indexExists('_updatedAt_1')) { - await this.col.dropIndex('_updatedAt_1'); - } - await this.col.createIndex({ _updatedAt: 1 }, { expireAfterSeconds }); + remove(query: Filter): Promise { + return this.col.deleteMany(query); } } diff --git a/packages/models/src/models/BaseRaw.ts b/packages/models/src/models/BaseRaw.ts index 0c28c17913e3a..d6c75759c3ecf 100644 --- a/packages/models/src/models/BaseRaw.ts +++ b/packages/models/src/models/BaseRaw.ts @@ -44,7 +44,6 @@ type ModelOptions = { preventSetUpdatedAt?: boolean; collectionNameResolver?: (name: string) => string; collection?: CollectionOptions; - _updatedAtIndexOptions?: Omit; }; export abstract class BaseRaw< @@ -74,7 +73,7 @@ export abstract class BaseRaw< private db: Db, protected name: string, protected trash?: Collection, - private options?: ModelOptions, + options?: ModelOptions, ) { this.collectionName = options?.collectionNameResolver ? options.collectionNameResolver(name) : getCollectionName(name); @@ -91,9 +90,6 @@ export abstract class BaseRaw< public async createIndexes() { const indexes = this.modelIndexes(); - if (this.options?._updatedAtIndexOptions) { - indexes?.push({ ...this.options._updatedAtIndexOptions, key: { _updatedAt: 1 } }); - } if (indexes?.length) { if (this.pendingIndexes) { From bffc49f426259925c415651c2b2a58083dac547a Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Fri, 21 Mar 2025 11:25:00 -0300 Subject: [PATCH 018/187] fix: Leave room confirmation modal not displaying room name (#35568) --- .changeset/gorgeous-turtles-flow.md | 6 ++++++ .../client/hooks/menuActions/useLeaveRoom.tsx | 2 +- apps/meteor/client/hooks/useHideRoomAction.tsx | 2 +- .../client/lib/rooms/roomTypes/livechat.ts | 2 +- .../Info/hooks/actions/useRoomLeave.tsx | 2 +- packages/i18n/src/locales/af.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/ar.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/az.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/be-BY.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/bg.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/bs.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/ca.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/cs.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/cy.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/da.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/de-AT.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/de-IN.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/de.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/el.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/en.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/eo.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/es.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/fa.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/fi.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/fr.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/he.i18n.json | 12 ++++++------ packages/i18n/src/locales/hi-IN.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/hr.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/hu.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/id.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/it.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/ja.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/ka-GE.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/km.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/ko.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/ku.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/lo.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/lt.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/lv.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/mn.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/ms-MY.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/nb.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/nl.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/nn.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/pl.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/pt-BR.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/pt.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/ro.i18n.json | 4 ++-- packages/i18n/src/locales/ru.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/sk-SK.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/sl-SI.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/sq.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/sr.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/sv.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/ta-IN.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/th-TH.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/tr.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/ug.i18n.json | 12 ++++++------ packages/i18n/src/locales/uk.i18n.json | 18 +++++++++--------- packages/i18n/src/locales/vi-VN.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/zh-TW.i18n.json | 16 ++++++++-------- packages/i18n/src/locales/zh.i18n.json | 16 ++++++++-------- 62 files changed, 470 insertions(+), 464 deletions(-) create mode 100644 .changeset/gorgeous-turtles-flow.md diff --git a/.changeset/gorgeous-turtles-flow.md b/.changeset/gorgeous-turtles-flow.md new file mode 100644 index 0000000000000..747603a02381a --- /dev/null +++ b/.changeset/gorgeous-turtles-flow.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Fixes an issue with the leave room confirmation modal not displaying the room's name. diff --git a/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx b/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx index b5c90c8f42dac..2700bb676a1a4 100644 --- a/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx +++ b/apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx @@ -52,7 +52,7 @@ export const useLeaveRoomAction = ({ rid, type, name, roomOpen }: LeaveRoomProps setModal( setModal(null)} cancelText={t('Cancel')} diff --git a/apps/meteor/client/hooks/useHideRoomAction.tsx b/apps/meteor/client/hooks/useHideRoomAction.tsx index b6ee1439645d3..2d45b40a2db31 100644 --- a/apps/meteor/client/hooks/useHideRoomAction.tsx +++ b/apps/meteor/client/hooks/useHideRoomAction.tsx @@ -85,7 +85,7 @@ export const useHideRoomAction = ({ rid: roomId, type, name }: HideRoomProps, { label: t('Hide_room'), }} > - {t(warnText as TranslationKey, { postProcess: 'sprintf', sprintf: [name] })} + {t(warnText as TranslationKey, { roomName: name })} , ); }); diff --git a/apps/meteor/client/lib/rooms/roomTypes/livechat.ts b/apps/meteor/client/lib/rooms/roomTypes/livechat.ts index 23a813d6b81a6..4cede49f0065b 100644 --- a/apps/meteor/client/lib/rooms/roomTypes/livechat.ts +++ b/apps/meteor/client/lib/rooms/roomTypes/livechat.ts @@ -39,7 +39,7 @@ roomCoordinator.add( case UiTextContext.HIDE_WARNING: return 'Hide_Livechat_Warning'; case UiTextContext.LEAVE_WARNING: - return 'Hide_Livechat_Warning'; + return 'Leave_Livechat_Warning'; default: return ''; } diff --git a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomLeave.tsx b/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomLeave.tsx index ebcb7abaf3e71..2f53b9d95ed37 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomLeave.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomLeave.tsx @@ -37,7 +37,7 @@ export const useRoomLeave = (room: IRoom, joined = true) => { setModal( setModal(null)} cancelText={t('Cancel')} diff --git a/packages/i18n/src/locales/af.i18n.json b/packages/i18n/src/locales/af.i18n.json index 3aa139a3d5344..b7ecdc2e3f82d 100644 --- a/packages/i18n/src/locales/af.i18n.json +++ b/packages/i18n/src/locales/af.i18n.json @@ -1218,12 +1218,12 @@ "Hide": "Versteek kamer", "Hide_counter": "Versteek toonbank", "Hide_flextab": "Versteek regterkantste zijbalk met klik", - "Hide_Group_Warning": "Is jy seker jy wil die groep \"%s\" versteek?", - "Hide_Livechat_Warning": "Is jy seker jy wil die livechat verberg met \"%s\"?", - "Hide_Private_Warning": "Is jy seker jy wil die bespreking met \"%s\" versteek?", + "Hide_Group_Warning": "Is jy seker jy wil die groep \"{{roomName}}\" versteek?", + "Hide_Livechat_Warning": "Is jy seker jy wil die livechat verberg met \"{{roomName}}\"?", + "Hide_Private_Warning": "Is jy seker jy wil die bespreking met \"{{roomName}}\" versteek?", "Hide_roles": "Versteek rolle", "Hide_room": "Versteek kamer", - "Hide_Room_Warning": "Is jy seker jy wil die kamer \"%s\" versteek?", + "Hide_Room_Warning": "Is jy seker jy wil die kamer \"{{roomName}}\" versteek?", "Hide_Unread_Room_Status": "Versteek Ongelees Kamer Status", "Hide_usernames": "Versteek gebruikersname", "Highlights": "hoogtepunte", @@ -1523,11 +1523,11 @@ "Lead_capture_email_regex": "Lei vang e-pos regex", "Lead_capture_phone_regex": "Lead capture phone regex", "Leave": "Los kamer", - "Leave_Group_Warning": "Is jy seker jy wil die groep \"%s\" verlaat?", - "Leave_Livechat_Warning": "Is jy seker jy wil die livechat met \"%s\" verlaat?", - "Leave_Private_Warning": "Is jy seker jy wil die gesprek met \"%s\" verlaat?", + "Leave_Group_Warning": "Is jy seker jy wil die groep \"{{roomName}}\" verlaat?", + "Leave_Livechat_Warning": "Is jy seker jy wil die livechat met \"{{roomName}}\" verlaat?", + "Leave_Private_Warning": "Is jy seker jy wil die gesprek met \"{{roomName}}\" verlaat?", "Leave_room": "Los kamer", - "Leave_Room_Warning": "Is jy seker jy wil die kamer \"%s\" verlaat?", + "Leave_Room_Warning": "Is jy seker jy wil die kamer \"{{roomName}}\" verlaat?", "Leave_the_current_channel": "Los die huidige kanaal", "leave-c": "Laat kanale", "leave-p": "Verlaat privaat groepe", diff --git a/packages/i18n/src/locales/ar.i18n.json b/packages/i18n/src/locales/ar.i18n.json index 59c26ec6a414f..87085e65a121c 100644 --- a/packages/i18n/src/locales/ar.i18n.json +++ b/packages/i18n/src/locales/ar.i18n.json @@ -1744,10 +1744,10 @@ "Hi_username": "مرحبًا [name]", "Hidden": "مخفي", "Hide": "إخفاء", - "Hide_Group_Warning": "هل تريد فعلاً إخفاء المجموعة \"‎%s\"؟", - "Hide_Livechat_Warning": "هل تريد فعلاً إخفاء الدردشة مع \"‎%s\"؟", - "Hide_Private_Warning": "هل تريد فعلاً إخفاء المناقشة مع \"‎%s\"؟", - "Hide_Room_Warning": "هل تريد فعلاً إخفاء القناة \"‎%s\"؟", + "Hide_Group_Warning": "هل تريد فعلاً إخفاء المجموعة \"‎{{roomName}}\"؟", + "Hide_Livechat_Warning": "هل تريد فعلاً إخفاء الدردشة مع \"‎{{roomName}}\"؟", + "Hide_Private_Warning": "هل تريد فعلاً إخفاء المناقشة مع \"‎{{roomName}}\"؟", + "Hide_Room_Warning": "هل تريد فعلاً إخفاء القناة \"‎{{roomName}}\"؟", "Hide_System_Messages": "إخفاء رسائل النظام", "Hide_Unread_Room_Status": "إخفاء حالة Room غير المقروءة", "Hide_counter": "إخفاء العداد", @@ -2215,10 +2215,10 @@ "Lead_capture_phone_regex": "التعبير النمطي للهاتف المحمول لالتقاط العملاء المتوقعين", "Least_recent_updated": "آخر تحديث", "Leave": "مغادرة أو ترك", - "Leave_Group_Warning": "هل تريد فعلاً مغادرة المجموعة \"%s\"؟", - "Leave_Livechat_Warning": "هل تريد فعلاً مغادرة القناة متعددة الاتجاهات مع \" %s\"؟", - "Leave_Private_Warning": "هل تريد فعلاً مغادرة المناقشة مع \"%s\"؟", - "Leave_Room_Warning": "هل تريد فعلاً مغادرة القناة \"%s\"؟", + "Leave_Group_Warning": "هل تريد فعلاً مغادرة المجموعة \"{{roomName}}\"؟", + "Leave_Livechat_Warning": "هل تريد فعلاً مغادرة القناة متعددة الاتجاهات مع \" {{roomName}}\"؟", + "Leave_Private_Warning": "هل تريد فعلاً مغادرة المناقشة مع \"{{roomName}}\"؟", + "Leave_Room_Warning": "هل تريد فعلاً مغادرة القناة \"{{roomName}}\"؟", "Leave_a_comment": "ترك تعليق", "Leave_room": "مغادرة", "Leave_the_current_channel": "مغادرة القناة الحالية", @@ -4873,4 +4873,4 @@ "you_are_in_preview_mode_of_incoming_livechat": "أنت في وضع المعاينة لهذه الدردشة", "your_message": "رسالتك", "your_message_optional": "رسالتك (اختياري)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/az.i18n.json b/packages/i18n/src/locales/az.i18n.json index 30c42415f4fe8..5689c6b389c57 100644 --- a/packages/i18n/src/locales/az.i18n.json +++ b/packages/i18n/src/locales/az.i18n.json @@ -1218,12 +1218,12 @@ "Hide": "Otaqları gizlədin", "Hide_counter": "Saytı gizlət", "Hide_flextab": "Sağ Kenar Çubuğunu Kliklə Gizlət", - "Hide_Group_Warning": "\"%s\" qrupunu gizlətmək istədiyinizə əminsiniz?", - "Hide_Livechat_Warning": "Livechat'ı \"%s\" ilə gizlətmək istədiyinizə əminsiniz?", - "Hide_Private_Warning": "Müzakirə \"%s\" ilə gizlətmək istədiyinizə əminsiniz?", + "Hide_Group_Warning": "\"{{roomName}}\" qrupunu gizlətmək istədiyinizə əminsiniz?", + "Hide_Livechat_Warning": "Livechat'ı \"{{roomName}}\" ilə gizlətmək istədiyinizə əminsiniz?", + "Hide_Private_Warning": "Müzakirə \"{{roomName}}\" ilə gizlətmək istədiyinizə əminsiniz?", "Hide_roles": "Roles gizlət", "Hide_room": "Otaqları gizlədin", - "Hide_Room_Warning": "\"%s\" otağını gizlətmək istədiyinizə əminsiniz?", + "Hide_Room_Warning": "\"{{roomName}}\" otağını gizlətmək istədiyinizə əminsiniz?", "Hide_Unread_Room_Status": "Oxunmamış Otaq Statusını Gizlət", "Hide_usernames": "İstifadəçi adlarını gizlət", "Highlights": "Zirvələr", @@ -1523,11 +1523,11 @@ "Lead_capture_email_regex": "Nəzarət ələ e-poçt qeydiyyatdan keçirin", "Lead_capture_phone_regex": "Nəzarət aparan telefon regex", "Leave": "Otaq buraxın", - "Leave_Group_Warning": "\"%s\" qrupunu tərk etmək istədiyinizə əminsiniz?", - "Leave_Livechat_Warning": "Livechat'ı \"%s\" ilə tərk etmək istədiyinizə əminsiniz?", - "Leave_Private_Warning": "Müzakirə \"%s\" ilə tərk etmək istədiyinizə əminsiniz?", + "Leave_Group_Warning": "\"{{roomName}}\" qrupunu tərk etmək istədiyinizə əminsiniz?", + "Leave_Livechat_Warning": "Livechat'ı \"{{roomName}}\" ilə tərk etmək istədiyinizə əminsiniz?", + "Leave_Private_Warning": "Müzakirə \"{{roomName}}\" ilə tərk etmək istədiyinizə əminsiniz?", "Leave_room": "Otaq buraxın", - "Leave_Room_Warning": "\"%s\" otağından çıxmaq istəyirsiniz?", + "Leave_Room_Warning": "\"{{roomName}}\" otağından çıxmaq istəyirsiniz?", "Leave_the_current_channel": "Cari kanalı buraxın", "leave-c": "Kanallardan çıxın", "leave-p": "Şəxsi Qruplar buraxın", diff --git a/packages/i18n/src/locales/be-BY.i18n.json b/packages/i18n/src/locales/be-BY.i18n.json index a5a0e2de9054d..caa49546fe7b6 100644 --- a/packages/i18n/src/locales/be-BY.i18n.json +++ b/packages/i18n/src/locales/be-BY.i18n.json @@ -1234,12 +1234,12 @@ "Hide": "схаваць нумар", "Hide_counter": "схаваць лічыльнік", "Hide_flextab": "Схаваць правую бакавую панэль з шчылінкі", - "Hide_Group_Warning": "Вы ўпэўненыя, што хочаце схаваць групу «%s\"?", - "Hide_Livechat_Warning": "Вы ўпэўненыя, што хочаце схаваць звязаўшыся \"%s\"?", - "Hide_Private_Warning": "Вы ўпэўненыя, што хочаце схаваць дыскусію з \"%s\"?", + "Hide_Group_Warning": "Вы ўпэўненыя, што хочаце схаваць групу «{{roomName}}\"?", + "Hide_Livechat_Warning": "Вы ўпэўненыя, што хочаце схаваць звязаўшыся \"{{roomName}}\"?", + "Hide_Private_Warning": "Вы ўпэўненыя, што хочаце схаваць дыскусію з \"{{roomName}}\"?", "Hide_roles": "схаваць ролі", "Hide_room": "схаваць нумар", - "Hide_Room_Warning": "Вы ўпэўненыя, што хочаце схаваць нумар \"%s\"?", + "Hide_Room_Warning": "Вы ўпэўненыя, што хочаце схаваць нумар \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Схаваць Статус непрачытаных нумары", "Hide_usernames": "Схаваць імёны карыстальнікаў", "Highlights": "мелірованіе", @@ -1539,11 +1539,11 @@ "Lead_capture_email_regex": "Свінец захопу электроннай пошты рэгулярных выразаў", "Lead_capture_phone_regex": "Свінец захопу тэлефона рэгулярны выраз", "Leave": "Пакіньце нумар", - "Leave_Group_Warning": "Вы ўпэўненыя, што жадаеце выйсці з групы \"%s\"?", - "Leave_Livechat_Warning": "Вы ўпэўненыя, што жадаеце пакінуць звязаўшыся \"%s\"?", - "Leave_Private_Warning": "Вы ўпэўненыя, што жадаеце пакінуць дыскусію з \"%s\"?", + "Leave_Group_Warning": "Вы ўпэўненыя, што жадаеце выйсці з групы \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Вы ўпэўненыя, што жадаеце пакінуць звязаўшыся \"{{roomName}}\"?", + "Leave_Private_Warning": "Вы ўпэўненыя, што жадаеце пакінуць дыскусію з \"{{roomName}}\"?", "Leave_room": "Пакіньце нумар", - "Leave_Room_Warning": "Вы ўпэўненыя, што хочаце выйсці з пакоя \"%s\"?", + "Leave_Room_Warning": "Вы ўпэўненыя, што хочаце выйсці з пакоя \"{{roomName}}\"?", "Leave_the_current_channel": "Пакіньце бягучы канал", "leave-c": "Пакіньце каналы", "leave-p": "Пакіньце Прыватныя групы", diff --git a/packages/i18n/src/locales/bg.i18n.json b/packages/i18n/src/locales/bg.i18n.json index 47ca415025f93..9de55ec891e56 100644 --- a/packages/i18n/src/locales/bg.i18n.json +++ b/packages/i18n/src/locales/bg.i18n.json @@ -1218,12 +1218,12 @@ "Hide": "Скрий стая", "Hide_counter": "Скриване на брояч", "Hide_flextab": "Скриване на дясната странична лента с кликване", - "Hide_Group_Warning": "Наистина ли искате да скриете групата \"%s\"?", - "Hide_Livechat_Warning": "Наистина ли искате да скриете livechat с \"%s\"?", - "Hide_Private_Warning": "Наистина ли искате да скриете дискусията с \"%s\"?", + "Hide_Group_Warning": "Наистина ли искате да скриете групата \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Наистина ли искате да скриете livechat с \"{{roomName}}\"?", + "Hide_Private_Warning": "Наистина ли искате да скриете дискусията с \"{{roomName}}\"?", "Hide_roles": "Скриване на роли", "Hide_room": "Скрий стая", - "Hide_Room_Warning": "Наистина ли искате да скриете стаята \"%s\"?", + "Hide_Room_Warning": "Наистина ли искате да скриете стаята \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Скриване на състоянието на непрочетената стая", "Hide_usernames": "Скриване на потребителските имена", "Highlights": "Акценти", @@ -1521,11 +1521,11 @@ "Lead_capture_email_regex": "Водещ имейл регекс за улавяне", "Lead_capture_phone_regex": "Водещ телефонен регекс за улавяне", "Leave": "Излез от стаята", - "Leave_Group_Warning": "Наистина ли искате да напуснете групата \"%s\"?", - "Leave_Livechat_Warning": "Сигурни ли сте, че искате да оставите livechat с \"%s\"?", - "Leave_Private_Warning": "Наистина ли искате да оставите дискусията с \"%s\"?", + "Leave_Group_Warning": "Наистина ли искате да напуснете групата \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Сигурни ли сте, че искате да оставите livechat с \"{{roomName}}\"?", + "Leave_Private_Warning": "Наистина ли искате да оставите дискусията с \"{{roomName}}\"?", "Leave_room": "Излез от стаята", - "Leave_Room_Warning": "Наистина ли искате да излезете от стаята \"%s\"?", + "Leave_Room_Warning": "Наистина ли искате да излезете от стаята \"{{roomName}}\"?", "Leave_the_current_channel": "Оставете текущия канал", "leave-c": "Оставете канали", "leave-p": "Напускане на частни групи", diff --git a/packages/i18n/src/locales/bs.i18n.json b/packages/i18n/src/locales/bs.i18n.json index 2ccdddbc6e06b..b8c88cd4fa576 100644 --- a/packages/i18n/src/locales/bs.i18n.json +++ b/packages/i18n/src/locales/bs.i18n.json @@ -1214,12 +1214,12 @@ "Hide": "Sakrij sobu", "Hide_counter": "Sakrij brojač", "Hide_flextab": "Sakrij desni izbornik klikom", - "Hide_Group_Warning": "Jeste li sigurni da želite sakriti grupu \"%s\"?", - "Hide_Livechat_Warning": "Jeste li sigurni da želite sakriti livechat s \"%s\"?", - "Hide_Private_Warning": "Jeste li sigurni da želite sakriti raspravu s \"%s\"?", + "Hide_Group_Warning": "Jeste li sigurni da želite sakriti grupu \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Jeste li sigurni da želite sakriti livechat s \"{{roomName}}\"?", + "Hide_Private_Warning": "Jeste li sigurni da želite sakriti raspravu s \"{{roomName}}\"?", "Hide_roles": "Sakrij uloge", "Hide_room": "Sakrij sobu", - "Hide_Room_Warning": "Jeste li sigurni da želite sakriti sobu \"%s\"?", + "Hide_Room_Warning": "Jeste li sigurni da želite sakriti sobu \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Sakrij status nepročitane sobe", "Hide_usernames": "Sakrij korisnička imena", "Highlights": "Istaknuto", @@ -1519,11 +1519,11 @@ "Lead_capture_email_regex": "Olovo za hvatanje e-pošte regex", "Lead_capture_phone_regex": "Olovo za hvatanje regex telefona", "Leave": "Izađi iz sobe", - "Leave_Group_Warning": "Jeste li sigurni da želite napustiti grupu \"%s\"?", - "Leave_Livechat_Warning": "Jeste li sigurni da želite napustiti livechat s \"%s\"?", - "Leave_Private_Warning": "Jeste li sigurni da želite napustiti razgovor s \"%s\"?", + "Leave_Group_Warning": "Jeste li sigurni da želite napustiti grupu \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Jeste li sigurni da želite napustiti livechat s \"{{roomName}}\"?", + "Leave_Private_Warning": "Jeste li sigurni da želite napustiti razgovor s \"{{roomName}}\"?", "Leave_room": "Izađi iz sobe", - "Leave_Room_Warning": "Jeste li sigurni da želite izaći iz sobe \"%s\"?", + "Leave_Room_Warning": "Jeste li sigurni da želite izaći iz sobe \"{{roomName}}\"?", "Leave_the_current_channel": "Napusti trenutnu sobu", "leave-c": "Ostavite kanale", "leave-p": "Napusti privatne grupe", diff --git a/packages/i18n/src/locales/ca.i18n.json b/packages/i18n/src/locales/ca.i18n.json index 039a76f53a1b2..09b5280aedc8c 100644 --- a/packages/i18n/src/locales/ca.i18n.json +++ b/packages/i18n/src/locales/ca.i18n.json @@ -1724,10 +1724,10 @@ "Hi_username": "Hola [name]", "Hidden": "Ocult", "Hide": "Amagar", - "Hide_Group_Warning": "Segur que voleu ocultar el grup \"%s\"?", - "Hide_Livechat_Warning": "Estàs segur que vols amagar al xat amb \"%s\"?", - "Hide_Private_Warning": "Segur que voleu ocultar la discussió amb \"%s\"?", - "Hide_Room_Warning": "Segur que vols amagar la sala amb \"%s\"?", + "Hide_Group_Warning": "Segur que voleu ocultar el grup \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Estàs segur que vols amagar al xat amb \"{{roomName}}\"?", + "Hide_Private_Warning": "Segur que voleu ocultar la discussió amb \"{{roomName}}\"?", + "Hide_Room_Warning": "Segur que vols amagar la sala amb \"{{roomName}}\"?", "Hide_System_Messages": "Ocultar els missatges del sistema", "Hide_Unread_Room_Status": "Amaga l'estat de sales no llegides", "Hide_counter": "Amaga comptador", @@ -2193,10 +2193,10 @@ "Lead_capture_phone_regex": "Regex de telèfon de captura clients potencials", "Least_recent_updated": "Actualització menys recent", "Leave": "Sortir ", - "Leave_Group_Warning": "Segur que vols deixar el grup \"%s\"?", - "Leave_Livechat_Warning": "Segur que vols sortir de l'LiveChat amb \"%s\"?", - "Leave_Private_Warning": "Segur que vols sortir de la conversa amb \"%s\"?", - "Leave_Room_Warning": "Segur que vols sortir de la sala \"%s\"?", + "Leave_Group_Warning": "Segur que vols deixar el grup \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Segur que vols sortir de l'LiveChat amb \"{{roomName}}\"?", + "Leave_Private_Warning": "Segur que vols sortir de la conversa amb \"{{roomName}}\"?", + "Leave_Room_Warning": "Segur que vols sortir de la sala \"{{roomName}}\"?", "Leave_a_comment": "Deixar un comentari", "Leave_room": "Sortir ", "Leave_the_current_channel": "Surt del canal actual", @@ -4676,4 +4676,4 @@ "you_are_in_preview_mode_of_incoming_livechat": "Esteu en mode de previsualització d'aquest xat", "your_message": "El seu missatge", "your_message_optional": "el seu missatge (opcional)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/cs.i18n.json b/packages/i18n/src/locales/cs.i18n.json index 8f0a504a72e1e..6fb4e275806c9 100644 --- a/packages/i18n/src/locales/cs.i18n.json +++ b/packages/i18n/src/locales/cs.i18n.json @@ -1470,10 +1470,10 @@ "Hi_username": "Ahoj [name]", "Hidden": "Schovaný", "Hide": "Skrýt", - "Hide_Group_Warning": "Jste si jisti, že chcete skrýt skupiny \"%s\"?", - "Hide_Livechat_Warning": "Opravdu chcete skrýt chat s \"%s\"?", - "Hide_Private_Warning": "Jste si jisti, že chcete skrýt diskusi s \"%s\"?", - "Hide_Room_Warning": "Jste si jisti, že chcete skrýt místnost \"%s\"?", + "Hide_Group_Warning": "Jste si jisti, že chcete skrýt skupiny \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Opravdu chcete skrýt chat s \"{{roomName}}\"?", + "Hide_Private_Warning": "Jste si jisti, že chcete skrýt diskusi s \"{{roomName}}\"?", + "Hide_Room_Warning": "Jste si jisti, že chcete skrýt místnost \"{{roomName}}\"?", "Hide_System_Messages": "Skrýt systémové zprávy", "Hide_Unread_Room_Status": "Schovat stav nepřečtených místností", "Hide_counter": "Schovat počítadlo", @@ -1863,10 +1863,10 @@ "Lead_capture_email_regex": "Regulární výraz pro zachycení Leadu na email", "Lead_capture_phone_regex": "Regulární výraz pro zachycení Leadu na telefon", "Leave": "Opustit", - "Leave_Group_Warning": "Jste si jisti, že chcete opustit skupinu \"%s\"?", - "Leave_Livechat_Warning": "Opravdu chcete opustit LiveChat s \"%s\"?", - "Leave_Private_Warning": "Jste si jisti, že chcete opustit diskusi s \"%s\"?", - "Leave_Room_Warning": "Jste si jisti, že chcete opustit místnost \"%s\"?", + "Leave_Group_Warning": "Jste si jisti, že chcete opustit skupinu \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Opravdu chcete opustit LiveChat s \"{{roomName}}\"?", + "Leave_Private_Warning": "Jste si jisti, že chcete opustit diskusi s \"{{roomName}}\"?", + "Leave_Room_Warning": "Jste si jisti, že chcete opustit místnost \"{{roomName}}\"?", "Leave_a_comment": "Zanechat komentář", "Leave_room": "Opustit", "Leave_the_current_channel": "Opustit aktuální místnost", @@ -3963,4 +3963,4 @@ "you_are_in_preview_mode_of_incoming_livechat": "Jste v režimu náhledu tohoto chatu", "your_message": "vaše zpráva", "your_message_optional": "vaše zpráva (nepovinná)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/cy.i18n.json b/packages/i18n/src/locales/cy.i18n.json index aeb8e3a30b7d3..88ccaa5a6f0ce 100644 --- a/packages/i18n/src/locales/cy.i18n.json +++ b/packages/i18n/src/locales/cy.i18n.json @@ -1214,12 +1214,12 @@ "Hide": "Ystafell Guddio", "Hide_counter": "Cuddio cownter", "Hide_flextab": "Cuddio Bar Barhau Cywir gyda Chliciwch", - "Hide_Group_Warning": "Ydych chi'n siŵr eich bod am guddio'r grŵp \"%s\"?", - "Hide_Livechat_Warning": "Ydych chi'n siŵr eich bod am guddio'r bywladwr gyda \"%s\"?", - "Hide_Private_Warning": "Ydych chi'n siŵr eich bod am guddio'r drafodaeth gyda \"%s\"?", + "Hide_Group_Warning": "Ydych chi'n siŵr eich bod am guddio'r grŵp \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ydych chi'n siŵr eich bod am guddio'r bywladwr gyda \"{{roomName}}\"?", + "Hide_Private_Warning": "Ydych chi'n siŵr eich bod am guddio'r drafodaeth gyda \"{{roomName}}\"?", "Hide_roles": "Cuddio Rolau", "Hide_room": "Ystafell Guddio", - "Hide_Room_Warning": "Ydych chi'n siŵr eich bod am guddio'r ystafell \"%s\"?", + "Hide_Room_Warning": "Ydych chi'n siŵr eich bod am guddio'r ystafell \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Cuddio Statws Ystafell Heb ei Darllen", "Hide_usernames": "Cuddio Enwau Defnyddwyr", "Highlights": "Uchafbwyntiau", @@ -1519,11 +1519,11 @@ "Lead_capture_email_regex": "Regex e-bost dal yn arwain", "Lead_capture_phone_regex": "Regex ffôn dal yn arwain", "Leave": "Ystafell gadael", - "Leave_Group_Warning": "Ydych chi'n siŵr eich bod am adael y grŵp \"%s\"?", - "Leave_Livechat_Warning": "Ydych chi'n siŵr eich bod am adael y byw-fyw gyda \"%s\"?", - "Leave_Private_Warning": "Ydych chi'n siŵr eich bod am adael y drafodaeth gyda \"%s\"?", + "Leave_Group_Warning": "Ydych chi'n siŵr eich bod am adael y grŵp \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Ydych chi'n siŵr eich bod am adael y byw-fyw gyda \"{{roomName}}\"?", + "Leave_Private_Warning": "Ydych chi'n siŵr eich bod am adael y drafodaeth gyda \"{{roomName}}\"?", "Leave_room": "Ystafell gadael", - "Leave_Room_Warning": "Ydych chi'n siŵr eich bod am adael yr ystafell \"%s\"?", + "Leave_Room_Warning": "Ydych chi'n siŵr eich bod am adael yr ystafell \"{{roomName}}\"?", "Leave_the_current_channel": "Gadewch y sianel gyfredol", "leave-c": "Gadael Sianeli", "leave-p": "Gadewch Grwpiau Preifat", diff --git a/packages/i18n/src/locales/da.i18n.json b/packages/i18n/src/locales/da.i18n.json index 2049e6a8113cd..48ef392565dea 100644 --- a/packages/i18n/src/locales/da.i18n.json +++ b/packages/i18n/src/locales/da.i18n.json @@ -1775,12 +1775,12 @@ "Hide": "Skul", "Hide_counter": "Skjul tæller", "Hide_flextab": "Skjul højre sidepanel med klik", - "Hide_Group_Warning": "Er du sikker på, at du vil skjule gruppen \"%s\"?", - "Hide_Livechat_Warning": "Er du sikker på at du vil skjule chatten med \"%s\"?", - "Hide_Private_Warning": "Er du sikker på, at du vil skjule diskussionen med \"%s\"?", + "Hide_Group_Warning": "Er du sikker på, at du vil skjule gruppen \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Er du sikker på at du vil skjule chatten med \"{{roomName}}\"?", + "Hide_Private_Warning": "Er du sikker på, at du vil skjule diskussionen med \"{{roomName}}\"?", "Hide_roles": "Skjul roller", "Hide_room": "Skjul", - "Hide_Room_Warning": "Er du sikker på, at du vil skjule kanalen \"%s\"?", + "Hide_Room_Warning": "Er du sikker på, at du vil skjule kanalen \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Skjul ulæst rumstatus", "Hide_usernames": "Skjul brugernavne", "Highlights": "Højdepunkter", @@ -2198,11 +2198,11 @@ "Lead_capture_email_regex": "Lead capture email regex", "Lead_capture_phone_regex": "Lead capture phone regex", "Leave": "Forlad", - "Leave_Group_Warning": "Er du sikker på, at du vil forlade gruppen \"%s\"?", - "Leave_Livechat_Warning": "Er du sikker på at du vil forlade Omnichannel'en med \"%s\"?", - "Leave_Private_Warning": "Er du sikker på at du vil forlade diskussionen med \"%s\"?", + "Leave_Group_Warning": "Er du sikker på, at du vil forlade gruppen \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Er du sikker på at du vil forlade Omnichannel'en med \"{{roomName}}\"?", + "Leave_Private_Warning": "Er du sikker på at du vil forlade diskussionen med \"{{roomName}}\"?", "Leave_room": "Forlad", - "Leave_Room_Warning": "Er du sikker på at du vil forlade kanalen \"%s\"?", + "Leave_Room_Warning": "Er du sikker på at du vil forlade kanalen \"{{roomName}}\"?", "Leave_the_current_channel": "Forlad den nuværende kanal", "leave-c": "Forlad kanaler", "Instance": "Instans", diff --git a/packages/i18n/src/locales/de-AT.i18n.json b/packages/i18n/src/locales/de-AT.i18n.json index bc7ab2741a443..0be9f27f0d1f1 100644 --- a/packages/i18n/src/locales/de-AT.i18n.json +++ b/packages/i18n/src/locales/de-AT.i18n.json @@ -1222,12 +1222,12 @@ "Hide": "Chatraum verstecken", "Hide_counter": "Zähler ausblenden", "Hide_flextab": "Verstecken Sie die rechte Seitenleiste mit Klick", - "Hide_Group_Warning": "Sind sie sicher, die Gruppe\"%s\" zu verstecken?", - "Hide_Livechat_Warning": "Möchtest du den Livechat wirklich mit \"%s\" ausblenden?", - "Hide_Private_Warning": "Sind sie sicher, das Gespräch mit \"%s\" zu verstecken?", + "Hide_Group_Warning": "Sind sie sicher, die Gruppe\"{{roomName}}\" zu verstecken?", + "Hide_Livechat_Warning": "Möchtest du den Livechat wirklich mit \"{{roomName}}\" ausblenden?", + "Hide_Private_Warning": "Sind sie sicher, das Gespräch mit \"{{roomName}}\" zu verstecken?", "Hide_roles": "Rollen ausblenden", "Hide_room": "Chatraum verstecken", - "Hide_Room_Warning": "Sind sie sicher, den Raum \"%s\" zu verstecken?", + "Hide_Room_Warning": "Sind sie sicher, den Raum \"{{roomName}}\" zu verstecken?", "Hide_Unread_Room_Status": "Ungelesenen Zimmerstatus ausblenden", "Hide_usernames": "Benutzernamen ausblenden", "Highlights": "Hervorhebungen", @@ -1526,11 +1526,11 @@ "Lead_capture_email_regex": "Lead Capture E-Mail Regex", "Lead_capture_phone_regex": "Lead Capture Telefon Regex", "Leave": "Chatraum verlassen", - "Leave_Group_Warning": "Sind sie sicher, die Chatgruppe \"%s\" verlassen zu wollen?", - "Leave_Livechat_Warning": "Möchtest du den Livechat wirklich mit \"%s\" verlassen?", - "Leave_Private_Warning": "Sind sie sicher, das Gespräch mit \"%s\" zu verlassen?", + "Leave_Group_Warning": "Sind sie sicher, die Chatgruppe \"{{roomName}}\" verlassen zu wollen?", + "Leave_Livechat_Warning": "Möchtest du den Livechat wirklich mit \"{{roomName}}\" verlassen?", + "Leave_Private_Warning": "Sind sie sicher, das Gespräch mit \"{{roomName}}\" zu verlassen?", "Leave_room": "Chatraum verlassen", - "Leave_Room_Warning": "Sind sie sicher, den Raum \"%s\" zu verlassen?", + "Leave_Room_Warning": "Sind sie sicher, den Raum \"{{roomName}}\" zu verlassen?", "Leave_the_current_channel": "Verlasse den aktuellen Kanal", "leave-c": "Kanäle verlassen", "leave-p": "Verlassen Sie private Gruppen", diff --git a/packages/i18n/src/locales/de-IN.i18n.json b/packages/i18n/src/locales/de-IN.i18n.json index 93bacebaf2cf0..22e7aa59c83df 100644 --- a/packages/i18n/src/locales/de-IN.i18n.json +++ b/packages/i18n/src/locales/de-IN.i18n.json @@ -1409,12 +1409,12 @@ "Hide": "Verstecken", "Hide_counter": "Zähler verstecken", "Hide_flextab": "Rechte Seitenleiste über Klick verstecken", - "Hide_Group_Warning": "Bist Du sicher, dass Du den privaten Kanal \"%s\" verstecken möchtest?", - "Hide_Livechat_Warning": "Bist Du Dir sicher, dass Du den Livechat mit \"%s\" ausblenden möchtest?", - "Hide_Private_Warning": "Bist Du Dir sicher, dass Du das Gespräch mit \"%s\" verstecken möchtest?", + "Hide_Group_Warning": "Bist Du sicher, dass Du den privaten Kanal \"{{roomName}}\" verstecken möchtest?", + "Hide_Livechat_Warning": "Bist Du Dir sicher, dass Du den Livechat mit \"{{roomName}}\" ausblenden möchtest?", + "Hide_Private_Warning": "Bist Du Dir sicher, dass Du das Gespräch mit \"{{roomName}}\" verstecken möchtest?", "Hide_roles": "Rollen ausblenden", "Hide_room": "Raum verstecken", - "Hide_Room_Warning": "Bist Du Dir sicher, dass Du den Raum \"%s\" verstecken möchtest?", + "Hide_Room_Warning": "Bist Du Dir sicher, dass Du den Raum \"{{roomName}}\" verstecken möchtest?", "Hide_Unread_Room_Status": "Ungelesen-Status des Raums nicht anzeigen", "Hide_usernames": "Benutzernamen ausblenden", "Highlights": "Hervorhebungen", @@ -1751,11 +1751,11 @@ "Lead_capture_email_regex": "Lead Capture E-Mail Regex", "Lead_capture_phone_regex": "Lead Capture Telefon Regex", "Leave": "Verlassen", - "Leave_Group_Warning": "Bist Du Dir sicher, dass Du den privaten Kanal \"%s\" verlassen möchtest?", - "Leave_Livechat_Warning": "Bist Du Dir sicher, dass Du den Livechat mit \"%s\" verlassen möchtest?", - "Leave_Private_Warning": "Bist Du Dir sicher, dass Du das Gespräch mit \"%s\" verlassen möchtest?", + "Leave_Group_Warning": "Bist Du Dir sicher, dass Du den privaten Kanal \"{{roomName}}\" verlassen möchtest?", + "Leave_Livechat_Warning": "Bist Du Dir sicher, dass Du den Livechat mit \"{{roomName}}\" verlassen möchtest?", + "Leave_Private_Warning": "Bist Du Dir sicher, dass Du das Gespräch mit \"{{roomName}}\" verlassen möchtest?", "Leave_room": "Raum verlassen", - "Leave_Room_Warning": "Bist Du Dir sicher, dass Du den Kanal \"%s\" verlassen möchtest?", + "Leave_Room_Warning": "Bist Du Dir sicher, dass Du den Kanal \"{{roomName}}\" verlassen möchtest?", "Leave_the_current_channel": "Aktuellen Kanal verlassen", "leave-c": "Kanäle verlassen", "leave-p": "Verlasse private Gruppen", diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 9f01a18645659..73dedb00609ae 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -2163,14 +2163,14 @@ "You_do_not_have_permission_to_do_this": "Sie haben keine Berechtigung, dies zu tun", "Hide_counter": "Zähler ausblenden", "Hide_flextab": "Rechte Seitenleiste mit Klick ausblenden", - "Hide_Group_Warning": "Sind Sie sicher, dass Sie die Gruppe \"%s\" ausblenden wollen?", - "Hide_Livechat_Warning": "Sind Sie sich sicher, dass Sie den Livechat mit \"%s\" ausblenden wollen?", + "Hide_Group_Warning": "Sind Sie sicher, dass Sie die Gruppe \"{{roomName}}\" ausblenden wollen?", + "Hide_Livechat_Warning": "Sind Sie sich sicher, dass Sie den Livechat mit \"{{roomName}}\" ausblenden wollen?", "Estimated_wait_time": "Geschätzte Wartezeit", "Estimated_wait_time_in_minutes": "Geschätzte Wartezeit (Zeit in Minuten)", - "Hide_Private_Warning": "Sind Sie sicher, dass Sie das Gespräch mit \"%s\" ausblenden wollen?", + "Hide_Private_Warning": "Sind Sie sicher, dass Sie das Gespräch mit \"{{roomName}}\" ausblenden wollen?", "Hide_roles": "Rollen ausblenden", "Hide_room": "Raum verstecken", - "Hide_Room_Warning": "Sind Sie sicher, dass Sie den Raum \"%s\" verstecken wollen?", + "Hide_Room_Warning": "Sind Sie sicher, dass Sie den Raum \"{{roomName}}\" verstecken wollen?", "Hide_Unread_Room_Status": "Ungelesen-Status des Rooms nicht anzeigen", "Hide_usernames": "Benutzernamen ausblenden", "every_30_seconds": "Einmal alle 30 Sekunden", @@ -2697,11 +2697,11 @@ "Inline_code": "Inline-Code", "Install_anyway": "Trotzdem installieren", "Leave": "Verlassen", - "Leave_Group_Warning": "Sind Sie sicher, dass Sie die Gruppe \"%s\" verlassen wollen?", - "Leave_Livechat_Warning": "Sind Sie sich sicher, dass Sie den Livechat mit \"%s\" verlassen wollen?", - "Leave_Private_Warning": "Sind Sie sicher, dass Sie die Diskussion mit \"%s\" verlassen wollen?", + "Leave_Group_Warning": "Sind Sie sicher, dass Sie die Gruppe \"{{roomName}}\" verlassen wollen?", + "Leave_Livechat_Warning": "Sind Sie sich sicher, dass Sie den Livechat mit \"{{roomName}}\" verlassen wollen?", + "Leave_Private_Warning": "Sind Sie sicher, dass Sie die Diskussion mit \"{{roomName}}\" verlassen wollen?", "Leave_room": "Verlassen", - "Leave_Room_Warning": "Sind Sie sicher, dass Sie den Raum \"%s\" verlassen wollen?", + "Leave_Room_Warning": "Sind Sie sicher, dass Sie den Raum \"{{roomName}}\" verlassen wollen?", "Leave_the_current_channel": "Aktuellen Channel verlassen", "leave-c": "Channels verlassen", "Instance": "Instanz", diff --git a/packages/i18n/src/locales/el.i18n.json b/packages/i18n/src/locales/el.i18n.json index 30c4c078b9a72..bf9d9972cf1d9 100644 --- a/packages/i18n/src/locales/el.i18n.json +++ b/packages/i18n/src/locales/el.i18n.json @@ -1225,12 +1225,12 @@ "Hide": "Απόκρυψη δωματίου", "Hide_counter": "Απόκρυψη μετρητή", "Hide_flextab": "Απόκρυψη της δεξιάς πλευρικής γραμμής με κλικ", - "Hide_Group_Warning": "Είστε σίγουροι ότι θέλετε να αποκρύψετε την ομάδα \"%s\";", - "Hide_Livechat_Warning": "Είστε βέβαιοι ότι θέλετε να αποκρύψετε το livechat με το \"%s\";", - "Hide_Private_Warning": "Είστε βέβαιοι ότι θέλετε να αποκρύψετε τη συζήτηση με το \"%s\";", + "Hide_Group_Warning": "Είστε σίγουροι ότι θέλετε να αποκρύψετε την ομάδα \"{{roomName}}\";", + "Hide_Livechat_Warning": "Είστε βέβαιοι ότι θέλετε να αποκρύψετε το livechat με το \"{{roomName}}\";", + "Hide_Private_Warning": "Είστε βέβαιοι ότι θέλετε να αποκρύψετε τη συζήτηση με το \"{{roomName}}\";", "Hide_roles": "Απόκρυψη ρόλων", "Hide_room": "Απόκρυψη δωματίου", - "Hide_Room_Warning": "Είστε σίγουροι ότι θέλετε να αποκρύψετε το δωμάτιο \"%s\";", + "Hide_Room_Warning": "Είστε σίγουροι ότι θέλετε να αποκρύψετε το δωμάτιο \"{{roomName}}\";", "Hide_Unread_Room_Status": "Απόκρυψη κατάστασης μη αναγνωσμένου δωματίου", "Hide_usernames": "Απόκρυψη ονόματα", "Highlights": "Ανταύγειες", @@ -1530,11 +1530,11 @@ "Lead_capture_email_regex": "Επικεφαλίδα λήψης μηνυμάτων ηλεκτρονικού ταχυδρομείου", "Lead_capture_phone_regex": "Επικεφαλής κύριου τηλεφώνου σύλληψης", "Leave": "Έξοδος από το δωμάτιο", - "Leave_Group_Warning": "Είστε σίγουροι ότι θέλετε να αποχωρήσετε από την ομάδα \"%s\";", - "Leave_Livechat_Warning": "Είστε βέβαιοι ότι θέλετε να αφήσετε το livechat με το \"%s\";", - "Leave_Private_Warning": "Είστε σίγουροι ότι θέλετε να αφήσετε τη συζήτηση με το \"%s\";", + "Leave_Group_Warning": "Είστε σίγουροι ότι θέλετε να αποχωρήσετε από την ομάδα \"{{roomName}}\";", + "Leave_Livechat_Warning": "Είστε βέβαιοι ότι θέλετε να αφήσετε το livechat με το \"{{roomName}}\";", + "Leave_Private_Warning": "Είστε σίγουροι ότι θέλετε να αφήσετε τη συζήτηση με το \"{{roomName}}\";", "Leave_room": "Έξοδος από το δωμάτιο", - "Leave_Room_Warning": "Είστε σίγουροι ότι θέλετε να φύγετε από το δωμάτιο \"%s\";", + "Leave_Room_Warning": "Είστε σίγουροι ότι θέλετε να φύγετε από το δωμάτιο \"{{roomName}}\";", "Leave_the_current_channel": "Αφήστε το τρέχον κανάλι", "leave-c": "Αφήστε τα κανάλια", "leave-p": "Αφήστε ιδιωτικές ομάδες", diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index eebaae1d817ec..5996179d73368 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -2485,16 +2485,16 @@ "You_do_not_have_permission_to_execute_this_command": "You do not have enough permissions to execute command: `/{{command}}`", "Hide_flextab": "Hide Contextual Bar by clicking outside of it", "You_have_reached_the_limit_active_costumers_this_month": "You have reached the limit of active customers this month", - "Hide_Group_Warning": "Are you sure you want to hide the group \"%s\"?", - "Hide_Livechat_Warning": "Are you sure you want to hide the chat with \"%s\"?", + "Hide_Group_Warning": "Are you sure you want to hide the group \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Are you sure you want to hide the chat with \"{{roomName}}\"?", "Estimated_wait_time": "Estimated wait time", "Estimated_wait_time_in_minutes": "Estimated wait time (time in minutes)", - "Hide_Private_Warning": "Are you sure you want to hide the discussion with \"%s\"?", + "Hide_Private_Warning": "Are you sure you want to hide the discussion with \"{{roomName}}\"?", "Hide_roles": "Hide Roles", "Event_notifications": "Event notifications", "Event_notifications_description": "By disabling this setting you’ll prevent the app from notifying you of upcoming events.", "Hide_room": "Hide", - "Hide_Room_Warning": "Are you sure you want to hide the channel \"%s\"?", + "Hide_Room_Warning": "Are you sure you want to hide the channel \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Hide Unread Room Status", "Hide_usernames": "Hide Usernames", "every_30_seconds": "Once every 30 seconds", @@ -3105,12 +3105,12 @@ "Install_anyway": "Install anyway", "Update_anyway": "Update anyway", "Leave": "Leave", - "Leave_Group_Warning": "Are you sure you want to leave the group \"%s\"?", - "Leave_Livechat_Warning": "Are you sure you want to leave the omnichannel with \"%s\"?", - "Leave_Private_Warning": "Are you sure you want to leave the discussion with \"%s\"?", + "Leave_Group_Warning": "Are you sure you want to leave the group \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Are you sure you want to leave the omnichannel with \"{{roomName}}\"?", + "Leave_Private_Warning": "Are you sure you want to leave the discussion with \"{{roomName}}\"?", "Installing": "Installing", "Leave_room": "Leave", - "Leave_Room_Warning": "Are you sure you want to leave the channel \"%s\"?", + "Leave_Room_Warning": "Are you sure you want to leave the channel \"{{roomName}}\"?", "Leave_the_current_channel": "Leave the current channel", "leave-c": "Leave Channels", "Instance": "Instance", @@ -6792,4 +6792,4 @@ "__unreadTitle__from__roomTitle__": "{{unreadTitle}} from {{roomTitle}}", "An_update_is_available": "An update is available", "Reload_to_update": "Reload to update" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/eo.i18n.json b/packages/i18n/src/locales/eo.i18n.json index 8602c1a38cf99..3df806448b0e8 100644 --- a/packages/i18n/src/locales/eo.i18n.json +++ b/packages/i18n/src/locales/eo.i18n.json @@ -1218,12 +1218,12 @@ "Hide": "Kaŝi ĉambron", "Hide_counter": "Kaŝi nombrilon", "Hide_flextab": "Kaŝi dekstra flankmenuo per klako", - "Hide_Group_Warning": "Ĉu vi certas, ke vi volas kaŝi la grupon \"%s\"?", - "Hide_Livechat_Warning": "Ĉu vi certas, ke vi volas kaŝi la vivkaptanton kun \"%s\"?", - "Hide_Private_Warning": "Ĉu vi certas, ke vi volas kaŝi la diskuton kun \"%s\"?", + "Hide_Group_Warning": "Ĉu vi certas, ke vi volas kaŝi la grupon \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ĉu vi certas, ke vi volas kaŝi la vivkaptanton kun \"{{roomName}}\"?", + "Hide_Private_Warning": "Ĉu vi certas, ke vi volas kaŝi la diskuton kun \"{{roomName}}\"?", "Hide_roles": "Kaŝi Rulojn", "Hide_room": "Kaŝi ĉambron", - "Hide_Room_Warning": "Ĉu vi certas, ke vi volas kaŝi la ĉambron \"%s\"?", + "Hide_Room_Warning": "Ĉu vi certas, ke vi volas kaŝi la ĉambron \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Kaŝi Nelegitan Ĉambron", "Hide_usernames": "Kaŝi uzulnomon", "Highlights": "Plej elstaraj", @@ -1523,11 +1523,11 @@ "Lead_capture_email_regex": "Kondukta kapta retpoŝta regex", "Lead_capture_phone_regex": "Plumbo kapti telefonan regex", "Leave": "Lasu ĉambron", - "Leave_Group_Warning": "Ĉu vi certas, ke vi volas forlasi la grupon \"%s\"?", - "Leave_Livechat_Warning": "Ĉu vi certas, ke vi volas lasi la vivkaptanton kun \"%s\"?", - "Leave_Private_Warning": "Ĉu vi certas, ke vi volas lasi la diskuton kun \"%s\"?", + "Leave_Group_Warning": "Ĉu vi certas, ke vi volas forlasi la grupon \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Ĉu vi certas, ke vi volas lasi la vivkaptanton kun \"{{roomName}}\"?", + "Leave_Private_Warning": "Ĉu vi certas, ke vi volas lasi la diskuton kun \"{{roomName}}\"?", "Leave_room": "Lasu ĉambron", - "Leave_Room_Warning": "Ĉu vi certas, ke vi volas lasi la ĉambron \"%s\"?", + "Leave_Room_Warning": "Ĉu vi certas, ke vi volas lasi la ĉambron \"{{roomName}}\"?", "Leave_the_current_channel": "Lasu la nunan kanalon", "leave-c": "Lasi Kanalojn", "leave-p": "Lasi privatajn grupojn", diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json index dbec30c7e692a..97d0deb19c614 100644 --- a/packages/i18n/src/locales/es.i18n.json +++ b/packages/i18n/src/locales/es.i18n.json @@ -1762,10 +1762,10 @@ "Hi_username": "Hola, [name]", "Hidden": "Oculto", "Hide": "Ocultar", - "Hide_Group_Warning": "¿Seguro que quieres ocultar el grupo \"%s\"?", - "Hide_Livechat_Warning": "¿Seguro que quieres ocultar el chat con \"%s\"?", - "Hide_Private_Warning": "¿Seguro que quieres ocultar la discusión con \"%s\"?", - "Hide_Room_Warning": "¿Seguro que quieres ocultar el canal \"%s\"?", + "Hide_Group_Warning": "¿Seguro que quieres ocultar el grupo \"{{roomName}}\"?", + "Hide_Livechat_Warning": "¿Seguro que quieres ocultar el chat con \"{{roomName}}\"?", + "Hide_Private_Warning": "¿Seguro que quieres ocultar la discusión con \"{{roomName}}\"?", + "Hide_Room_Warning": "¿Seguro que quieres ocultar el canal \"{{roomName}}\"?", "Hide_System_Messages": "Ocultar mensajes de sistema", "Hide_Unread_Room_Status": "Ocultar indicación de que la Room tiene elementos no leídos", "Hide_counter": "Ocultar contador", @@ -2239,10 +2239,10 @@ "Learn_how_to_unlock_the_myriad_possibilities_of_rocket_chat": "Aprenda a desbloquear las innumerables posibilidades de Rocket.Chat.", "Least_recent_updated": "Actualización menos reciente", "Leave": "Salir", - "Leave_Group_Warning": "¿Seguro que quieres salir del grupo \"%s\"?", - "Leave_Livechat_Warning": "¿Seguro que quieres salir de la sala de Omnichannel con \"%s\"?", - "Leave_Private_Warning": "¿Seguro que quieres salir de la discusión con \"%s\"?", - "Leave_Room_Warning": "¿Seguro que quieres salir del canal \"%s\"?", + "Leave_Group_Warning": "¿Seguro que quieres salir del grupo \"{{roomName}}\"?", + "Leave_Livechat_Warning": "¿Seguro que quieres salir de la sala de Omnichannel con \"{{roomName}}\"?", + "Leave_Private_Warning": "¿Seguro que quieres salir de la discusión con \"{{roomName}}\"?", + "Leave_Room_Warning": "¿Seguro que quieres salir del canal \"{{roomName}}\"?", "Leave_a_comment": "Dejar un comentario", "Leave_room": "Salir ", "Leave_the_current_channel": "Salir del canal actual", @@ -5067,4 +5067,4 @@ "you_are_in_preview_please_insert_the_password": "Introduzca la contraseña", "your_message": "tu mensaje", "your_message_optional": "tu mensaje (opcional)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/fa.i18n.json b/packages/i18n/src/locales/fa.i18n.json index be20d35ce9719..31043533336f6 100644 --- a/packages/i18n/src/locales/fa.i18n.json +++ b/packages/i18n/src/locales/fa.i18n.json @@ -1242,10 +1242,10 @@ "Hex_Color_Preview": "پیش نمایش رنگ Hex", "Hidden": "پنهان", "Hide": "پنهان کردن اتاق", - "Hide_Group_Warning": "آیا بابت پنهان کردن گروه \"%s\" مطمئن هستید؟", - "Hide_Livechat_Warning": "آیا مطمئن هستید که میخواهید livechat را با «%s» پنهان کنید؟", - "Hide_Private_Warning": "آیا بابت پنهان کردن بحث با \"%s\" مطمئن هستید؟", - "Hide_Room_Warning": "آیا بابت پنهان کردن اتاق \"%s\" مطمئنید؟", + "Hide_Group_Warning": "آیا بابت پنهان کردن گروه \"{{roomName}}\" مطمئن هستید؟", + "Hide_Livechat_Warning": "آیا مطمئن هستید که میخواهید livechat را با «{{roomName}}» پنهان کنید؟", + "Hide_Private_Warning": "آیا بابت پنهان کردن بحث با \"{{roomName}}\" مطمئن هستید؟", + "Hide_Room_Warning": "آیا بابت پنهان کردن اتاق \"{{roomName}}\" مطمئنید؟", "Hide_Unread_Room_Status": "عدم نمایش وضعیت خوانده نشده برای این اتاق", "Hide_counter": "پنهان کردن شمارنده", "Hide_flextab": "پنهان کردن نوار کناری سمت راست با کلیک کنید", @@ -1557,10 +1557,10 @@ "Lead_capture_email_regex": "سرب گرفتن ایمیل regex", "Lead_capture_phone_regex": "سرب گرفتن مجدد خط تلفن", "Leave": "ترک اتاق", - "Leave_Group_Warning": "آیا واقعا می خواهید گروه \"%s\" را ترک کنید؟", - "Leave_Livechat_Warning": "آیا می خواهید کانال همه‌کاره را با \"%s\" ترک کنید؟", - "Leave_Private_Warning": "آیا واقعا می خواهید بحث با \"%s\" را ترک کنید؟", - "Leave_Room_Warning": "آیا واقعا می خواهید اتاق \"%s\" را ترک کنید؟", + "Leave_Group_Warning": "آیا واقعا می خواهید گروه \"{{roomName}}\" را ترک کنید؟", + "Leave_Livechat_Warning": "آیا می خواهید کانال همه‌کاره را با \"{{roomName}}\" ترک کنید؟", + "Leave_Private_Warning": "آیا واقعا می خواهید بحث با \"{{roomName}}\" را ترک کنید؟", + "Leave_Room_Warning": "آیا واقعا می خواهید اتاق \"{{roomName}}\" را ترک کنید؟", "Leave_room": "ترک اتاق", "Leave_the_current_channel": "کانال فعلی را ترک کنید", "List_of_Channels": "لیست Channelها", @@ -3110,4 +3110,4 @@ "you_are_in_preview_mode_of": "شما در حالت پیش نمایش از کانال # {{room_name}} هستند", "your_message": "پیام شما", "your_message_optional": "پیام شما(انتخابی)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index 7b010ea0fd581..f894c89d6ce7d 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -2194,14 +2194,14 @@ "You_do_not_have_permission_to_do_this": "Sinulla ei ole oikeutta tähän", "Hide_counter": "Piilota laskuri", "Hide_flextab": "Piilota oikea sivupalkki napsauttamalla", - "Hide_Group_Warning": "Haluatko varmasti piilottaa ryhmän \"%s\"?", - "Hide_Livechat_Warning": "Haluatko varmasti piilottaa keskustelun käyttäjän \"%s\" kanssa?", + "Hide_Group_Warning": "Haluatko varmasti piilottaa ryhmän \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Haluatko varmasti piilottaa keskustelun käyttäjän \"{{roomName}}\" kanssa?", "Estimated_wait_time": "Arvioitu odotusaika", "Estimated_wait_time_in_minutes": "Arvioitu odotusaika (minuutteina)", - "Hide_Private_Warning": "Haluatko varmasti piilottaa keskustelun käyttäjän \"%s\" kanssa?", + "Hide_Private_Warning": "Haluatko varmasti piilottaa keskustelun käyttäjän \"{{roomName}}\" kanssa?", "Hide_roles": "Piilota roolit", "Hide_room": "Piilota", - "Hide_Room_Warning": "Haluatko varmasti piilottaa kanavan \"%s\"?", + "Hide_Room_Warning": "Haluatko varmasti piilottaa kanavan \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Piilota lukemattoman Room huoneen tila", "Hide_usernames": "Piilota käyttäjätunnukset", "Highlights": "Kohokohdat", @@ -2733,11 +2733,11 @@ "Inline_code": "Sisäinen koodi", "Install_anyway": "Asenna silti", "Leave": "Poistu", - "Leave_Group_Warning": "Haluatko varmasti poistua ryhmästä \"%s\"?", - "Leave_Livechat_Warning": "Haluatko varmasti poistua monikanavalta käyttäjän \"%s\" kanssa?", - "Leave_Private_Warning": "Haluatko varmasti poistua poistua keskustelusta käyttäjän \"%s\" kanssa?", + "Leave_Group_Warning": "Haluatko varmasti poistua ryhmästä \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Haluatko varmasti poistua monikanavalta käyttäjän \"{{roomName}}\" kanssa?", + "Leave_Private_Warning": "Haluatko varmasti poistua poistua keskustelusta käyttäjän \"{{roomName}}\" kanssa?", "Leave_room": "Poistu", - "Leave_Room_Warning": "Haluatko varmasti poistua kanavalta \"%s\"?", + "Leave_Room_Warning": "Haluatko varmasti poistua kanavalta \"{{roomName}}\"?", "Leave_the_current_channel": "Poistu nykyiseltä kanavalta", "leave-c": "Jätä kanavat Channel", "Instance": "Esiintymä", diff --git a/packages/i18n/src/locales/fr.i18n.json b/packages/i18n/src/locales/fr.i18n.json index 249ea998cbf09..b65fe8a7f51ce 100644 --- a/packages/i18n/src/locales/fr.i18n.json +++ b/packages/i18n/src/locales/fr.i18n.json @@ -1738,10 +1738,10 @@ "Hi_username": "Bonjour [name]", "Hidden": "Caché", "Hide": "Masquer", - "Hide_Group_Warning": "Voulez-vous vraiment masquer le groupe \"%s\" ?", - "Hide_Livechat_Warning": "Voulez-vous vraiment masquer le chat avec \"%s\"?", - "Hide_Private_Warning": "Voulez-vous vraiment masquer la discussion avec \"%s\" ?", - "Hide_Room_Warning": "Voulez-vous vraiment masquer le canal \"%s\" ?", + "Hide_Group_Warning": "Voulez-vous vraiment masquer le groupe \"{{roomName}}\" ?", + "Hide_Livechat_Warning": "Voulez-vous vraiment masquer le chat avec \"{{roomName}}\"?", + "Hide_Private_Warning": "Voulez-vous vraiment masquer la discussion avec \"{{roomName}}\" ?", + "Hide_Room_Warning": "Voulez-vous vraiment masquer le canal \"{{roomName}}\" ?", "Hide_System_Messages": "Masquer les messages système", "Hide_Unread_Room_Status": "Masquer le statut non lu du salon", "Hide_counter": "Masquer le compteur", @@ -2210,10 +2210,10 @@ "Lead_capture_phone_regex": "Expression régulière de numéro de téléphone pour la capture de piste", "Least_recent_updated": "Mise à jour la moins récente", "Leave": "Quitter", - "Leave_Group_Warning": "Voulez-vous vraiment quitter le groupe \"%s\" ?", - "Leave_Livechat_Warning": "Voulez-vous vraiment quitter l'omnicanal avec \"%s\" ?", - "Leave_Private_Warning": "Voulez-vous vraiment quitter la discussion avec \"%s\" ?", - "Leave_Room_Warning": "Voulez-vous vraiment quitter le canal \"%s\" ?", + "Leave_Group_Warning": "Voulez-vous vraiment quitter le groupe \"{{roomName}}\" ?", + "Leave_Livechat_Warning": "Voulez-vous vraiment quitter l'omnicanal avec \"{{roomName}}\" ?", + "Leave_Private_Warning": "Voulez-vous vraiment quitter la discussion avec \"{{roomName}}\" ?", + "Leave_Room_Warning": "Voulez-vous vraiment quitter le canal \"{{roomName}}\" ?", "Leave_a_comment": "Laisser un commentaire", "Leave_room": "Quitter", "Leave_the_current_channel": "Quitter le canal actuel", @@ -4869,4 +4869,4 @@ "you_are_in_preview_mode_of_incoming_livechat": "Vous êtes en mode aperçu de ce chat", "your_message": "votre message", "your_message_optional": "votre message (optionnel)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/he.i18n.json b/packages/i18n/src/locales/he.i18n.json index 2097e7dfdcf42..c01d4496cfbed 100644 --- a/packages/i18n/src/locales/he.i18n.json +++ b/packages/i18n/src/locales/he.i18n.json @@ -629,11 +629,11 @@ "Hide": "להסתיר את החדר", "Hide_counter": "הסתר את המונה", "Hide_flextab": "הסתרת תפריט ימני בלחיצה", - "Hide_Group_Warning": "האם אתה בטוח שאתה מעוניין להסתיר את הקבוצה \"%s\"?", - "Hide_Private_Warning": "האם אתה בטוח שאתה רוצה להסתיר את השיחה עם \"%s\"?", + "Hide_Group_Warning": "האם אתה בטוח שאתה מעוניין להסתיר את הקבוצה \"{{roomName}}\"?", + "Hide_Private_Warning": "האם אתה בטוח שאתה רוצה להסתיר את השיחה עם \"{{roomName}}\"?", "Hide_roles": "הסתר תפקידים", "Hide_room": "להסתיר את החדר", - "Hide_Room_Warning": "האם אתה בטוח שאתה רוצה להסתיר את חדר \"%s\"?", + "Hide_Room_Warning": "האם אתה בטוח שאתה רוצה להסתיר את חדר \"{{roomName}}\"?", "Hide_usernames": "הסתרת שמות משתמשים", "Highlights": "עיקרי הדברים", "Highlights_How_To": "כדי לקבל הודעה כאשר מישהו מזכיר את המילה או הביטוי, להוסיף אותו כאן. ניתן להפריד מילים או ביטויים עם פסיקים. מילות דגש אינן תלויות-רישיות.", @@ -788,10 +788,10 @@ "LDAP_Username_Field": "שדה שם המשתמש", "LDAP_Username_Field_Description": "איזה שדה ישמש * שם משתמש * עבור משתמשים חדשים. השאר ריק להשתמש בשם המשתמש הודיע ​​על דף הכניסה. \n אתה יכול להשתמש בתגי תבנית מדי, כמו `#{givenName}.#{sn}`. \n ערך ברירת המחדל הוא `sAMAccountName`.", "Leave": "לעזוב את החדר", - "Leave_Group_Warning": "האם אתה בטוח שאתה רוצה לעזוב את הקבוצה \"%s\"?", - "Leave_Private_Warning": "האם אתה בטוח שאתה רוצה לעזוב את השיחה עם \"%s\"?", + "Leave_Group_Warning": "האם אתה בטוח שאתה רוצה לעזוב את הקבוצה \"{{roomName}}\"?", + "Leave_Private_Warning": "האם אתה בטוח שאתה רוצה לעזוב את השיחה עם \"{{roomName}}\"?", "Leave_room": "לעזוב את החדר", - "Leave_Room_Warning": "אתה בטוח שאתה מעוניין לעזוב את החדר \"%s\"", + "Leave_Room_Warning": "אתה בטוח שאתה מעוניין לעזוב את החדר \"{{roomName}}\"", "Leave_the_current_channel": "יציאה מהערוץ הנוכחי", "leave-p": "עזוב קבוצות פרטיות", "List_of_Channels": "רשימה של ערוצים", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 58f64802d9ee6..9e54a7ed8bfbc 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -2267,16 +2267,16 @@ "You_do_not_have_permission_to_execute_this_command": "आपके पास कमांड निष्पादित करने के लिए पर्याप्त अनुमतियाँ नहीं हैं: `/{{command}}`", "Hide_flextab": "प्रासंगिक बार के बाहर क्लिक करके उसे छिपाएँ", "You_have_reached_the_limit_active_costumers_this_month": "आप इस महीने सक्रिय ग्राहकों की सीमा तक पहुंच गए हैं", - "Hide_Group_Warning": "क्या आप वाकई समूह \"%s\" को छिपाना चाहते हैं?", - "Hide_Livechat_Warning": "क्या आप वाकई \"%s\" के साथ चैट छिपाना चाहते हैं?", + "Hide_Group_Warning": "क्या आप वाकई समूह \"{{roomName}}\" को छिपाना चाहते हैं?", + "Hide_Livechat_Warning": "क्या आप वाकई \"{{roomName}}\" के साथ चैट छिपाना चाहते हैं?", "Estimated_wait_time": "अनुमानित प्रतीक्षा समय", "Estimated_wait_time_in_minutes": "अनुमानित प्रतीक्षा समय (मिनटों में समय)", - "Hide_Private_Warning": "क्या आप वाकई \"%s\" के साथ चर्चा छिपाना चाहते हैं?", + "Hide_Private_Warning": "क्या आप वाकई \"{{roomName}}\" के साथ चर्चा छिपाना चाहते हैं?", "Hide_roles": "भूमिकाएँ छिपाएँ", "Event_notifications": "घटना सूचनाएं", "Event_notifications_description": "इस सेटिंग को अक्षम करके आप ऐप को आगामी घटनाओं के बारे में सूचित करने से रोकेंगे।", "Hide_room": "छिपाना", - "Hide_Room_Warning": "क्या आप वाकई चैनल \"%s\" को छिपाना चाहते हैं?", + "Hide_Room_Warning": "क्या आप वाकई चैनल \"{{roomName}}\" को छिपाना चाहते हैं?", "Hide_Unread_Room_Status": "अपठित कक्ष की स्थिति छिपाएँ", "Hide_usernames": "उपयोक्तानाम छिपाएँ", "every_30_seconds": "हर 30 सेकंड में एक बार", @@ -2835,12 +2835,12 @@ "Inline_code": "इनलाइन कोड", "Install_anyway": "फिर भी इंस्टॉल करें", "Leave": "छुट्टी", - "Leave_Group_Warning": "क्या आप वाकई समूह \"%s\" छोड़ना चाहते हैं?", - "Leave_Livechat_Warning": "क्या आप वाकई \"%s\" के साथ ओमनीचैनल छोड़ना चाहते हैं?", - "Leave_Private_Warning": "क्या आप वाकई \"%s\" के साथ चर्चा छोड़ना चाहते हैं?", + "Leave_Group_Warning": "क्या आप वाकई समूह \"{{roomName}}\" छोड़ना चाहते हैं?", + "Leave_Livechat_Warning": "क्या आप वाकई \"{{roomName}}\" के साथ ओमनीचैनल छोड़ना चाहते हैं?", + "Leave_Private_Warning": "क्या आप वाकई \"{{roomName}}\" के साथ चर्चा छोड़ना चाहते हैं?", "Installing": "स्थापित कर रहा है", "Leave_room": "छुट्टी", - "Leave_Room_Warning": "क्या आप वाकई चैनल \"%s\" छोड़ना चाहते हैं?", + "Leave_Room_Warning": "क्या आप वाकई चैनल \"{{roomName}}\" छोड़ना चाहते हैं?", "Leave_the_current_channel": "वर्तमान चैनल छोड़ें", "leave-c": "चैनल छोड़ें", "Instance": "उदाहरण", diff --git a/packages/i18n/src/locales/hr.i18n.json b/packages/i18n/src/locales/hr.i18n.json index a8fdf2905949e..6f5d8ac2c0615 100644 --- a/packages/i18n/src/locales/hr.i18n.json +++ b/packages/i18n/src/locales/hr.i18n.json @@ -1347,12 +1347,12 @@ "Hide": "Sakrij sobu", "Hide_counter": "Sakrij brojač", "Hide_flextab": "Sakrij desni izbornik klikom", - "Hide_Group_Warning": "Jeste li sigurni da želite sakriti grupu \"%s\"?", - "Hide_Livechat_Warning": "Jeste li sigurni da želite sakriti livechat s \"%s\"?", - "Hide_Private_Warning": "Jeste li sigurni da želite sakriti raspravu s \"%s\"?", + "Hide_Group_Warning": "Jeste li sigurni da želite sakriti grupu \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Jeste li sigurni da želite sakriti livechat s \"{{roomName}}\"?", + "Hide_Private_Warning": "Jeste li sigurni da želite sakriti raspravu s \"{{roomName}}\"?", "Hide_roles": "Sakrij uloge", "Hide_room": "Sakrij sobu", - "Hide_Room_Warning": "Jeste li sigurni da želite sakriti sobu \"%s\"?", + "Hide_Room_Warning": "Jeste li sigurni da želite sakriti sobu \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Sakrij status nepročitane sobe", "Hide_usernames": "Sakrij korisnička imena", "Highlights": "Istaknuto", @@ -1653,11 +1653,11 @@ "Lead_capture_email_regex": "Olovo za hvatanje e-pošte regex", "Lead_capture_phone_regex": "Olovo za hvatanje regex telefona", "Leave": "Izađi iz sobe", - "Leave_Group_Warning": "Jeste li sigurni da želite napustiti grupu \"%s\"?", - "Leave_Livechat_Warning": "Jeste li sigurni da želite napustiti livechat s \"%s\"?", - "Leave_Private_Warning": "Jeste li sigurni da želite napustiti razgovor s \"%s\"?", + "Leave_Group_Warning": "Jeste li sigurni da želite napustiti grupu \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Jeste li sigurni da želite napustiti livechat s \"{{roomName}}\"?", + "Leave_Private_Warning": "Jeste li sigurni da želite napustiti razgovor s \"{{roomName}}\"?", "Leave_room": "Izađi iz sobe", - "Leave_Room_Warning": "Jeste li sigurni da želite izaći iz sobe \"%s\"?", + "Leave_Room_Warning": "Jeste li sigurni da želite izaći iz sobe \"{{roomName}}\"?", "Leave_the_current_channel": "Napusti trenutnu sobu", "leave-c": "Ostavite kanale", "leave-p": "Napusti privatne grupe", diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index 8d2d483169192..565c86795297a 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -2128,12 +2128,12 @@ "You_do_not_have_permission_to_do_this": "Nincs jogosultsága ahhoz, hogy ezt tegye", "Hide_counter": "Számláló elrejtése", "Hide_flextab": "Jobb oldalsáv elrejtése kattintással", - "Hide_Group_Warning": "Biztosan el szeretné rejteni a(z) „%s” csoportot?", - "Hide_Livechat_Warning": "Biztosan el szeretné rejteni a(z) „%s” felhasználóval történt csevegést?", - "Hide_Private_Warning": "Biztosan el szeretné rejteni a(z) „%s” felhasználóval történt megbeszélést?", + "Hide_Group_Warning": "Biztosan el szeretné rejteni a(z) „{{roomName}}” csoportot?", + "Hide_Livechat_Warning": "Biztosan el szeretné rejteni a(z) „{{roomName}}” felhasználóval történt csevegést?", + "Hide_Private_Warning": "Biztosan el szeretné rejteni a(z) „{{roomName}}” felhasználóval történt megbeszélést?", "Hide_roles": "Szerepek elrejtése", "Hide_room": "Elrejtés", - "Hide_Room_Warning": "Biztosan el szeretné rejteni a(z) „%s” csatornát?", + "Hide_Room_Warning": "Biztosan el szeretné rejteni a(z) „{{roomName}}” csatornát?", "Hide_Unread_Room_Status": "Olvasatlan szobaállapot elrejtése", "Hide_usernames": "Felhasználónevek elrejtése", "Highlights": "Kiemelések", @@ -2646,11 +2646,11 @@ "Lead_capture_email_regex": "Érdeklődő rögzítésének e-mail reguláris kifejezése", "Lead_capture_phone_regex": "Érdeklődő rögzítésének telefon reguláris kifejezése", "Leave": "Elhagyás", - "Leave_Group_Warning": "Biztosan el szeretné hagyni a(z) „%s” csoportot?", - "Leave_Livechat_Warning": "Biztosan el szeretné hagyni a(z) „%s” felhasználóval történt összcsatornát?", - "Leave_Private_Warning": "Biztosan el szeretné hagyni a(z) „%s” felhasználóval történt megbeszélést?", + "Leave_Group_Warning": "Biztosan el szeretné hagyni a(z) „{{roomName}}” csoportot?", + "Leave_Livechat_Warning": "Biztosan el szeretné hagyni a(z) „{{roomName}}” felhasználóval történt összcsatornát?", + "Leave_Private_Warning": "Biztosan el szeretné hagyni a(z) „{{roomName}}” felhasználóval történt megbeszélést?", "Leave_room": "Elhagyás", - "Leave_Room_Warning": "Biztosan el szeretné hagyni a(z) „%s” csatornát?", + "Leave_Room_Warning": "Biztosan el szeretné hagyni a(z) „{{roomName}}” csatornát?", "Leave_the_current_channel": "A jelenlegi csatorna elhagyása", "leave-c": "Csatornák elhagyása", "Instance": "Példány", diff --git a/packages/i18n/src/locales/id.i18n.json b/packages/i18n/src/locales/id.i18n.json index 8cfecd55098ca..9a1690d78b1de 100644 --- a/packages/i18n/src/locales/id.i18n.json +++ b/packages/i18n/src/locales/id.i18n.json @@ -1219,12 +1219,12 @@ "Hide": "Sembunyikan room", "Hide_counter": "Sembunyikan kontra", "Hide_flextab": "Sembunyikan Bilah Kanan dengan Klik", - "Hide_Group_Warning": "Apakah Anda yakin Anda ingin menyembunyikan kelompok \"%s\"?", - "Hide_Livechat_Warning": "Apakah Anda yakin ingin menyembunyikan livechat dengan \"%s\"?", - "Hide_Private_Warning": "Apakah Anda yakin ingin menyembunyikan diskusi dengan \"%s\"?", + "Hide_Group_Warning": "Apakah Anda yakin Anda ingin menyembunyikan kelompok \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Apakah Anda yakin ingin menyembunyikan livechat dengan \"{{roomName}}\"?", + "Hide_Private_Warning": "Apakah Anda yakin ingin menyembunyikan diskusi dengan \"{{roomName}}\"?", "Hide_roles": "Sembunyikan Peran", "Hide_room": "Sembunyikan room", - "Hide_Room_Warning": "Apakah Anda yakin Anda ingin menyembunyikan ruang \"%s\"?", + "Hide_Room_Warning": "Apakah Anda yakin Anda ingin menyembunyikan ruang \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Sembunyikan Status Kamar Belum Dibaca", "Hide_usernames": "menyembunyikan nama pengguna", "Highlights": "Highlight", @@ -1523,11 +1523,11 @@ "Lead_capture_email_regex": "Memimpin menangkap email regex", "Lead_capture_phone_regex": "Memimpin menangkap regex telepon", "Leave": "Keluar dari room", - "Leave_Group_Warning": "Apakah Anda yakin ingin meninggalkan kelompok \"%s\"?", - "Leave_Livechat_Warning": "Apakah Anda yakin ingin meninggalkan livechat dengan \"%s\"?", - "Leave_Private_Warning": "Apakah Anda yakin ingin meninggalkan diskusi dengan \"%s\"?", + "Leave_Group_Warning": "Apakah Anda yakin ingin meninggalkan kelompok \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Apakah Anda yakin ingin meninggalkan livechat dengan \"{{roomName}}\"?", + "Leave_Private_Warning": "Apakah Anda yakin ingin meninggalkan diskusi dengan \"{{roomName}}\"?", "Leave_room": "Keluar dari room", - "Leave_Room_Warning": "Apakah Anda yakin ingin meninggalkan ruangan \"%s\"?", + "Leave_Room_Warning": "Apakah Anda yakin ingin meninggalkan ruangan \"{{roomName}}\"?", "Leave_the_current_channel": "Tinggalkan saluran saat ini", "leave-c": "Tinggalkan Saluran", "leave-p": "Tinggalkan Grup Pribadi", diff --git a/packages/i18n/src/locales/it.i18n.json b/packages/i18n/src/locales/it.i18n.json index 78157d2ddba00..8e1fcf06e947a 100644 --- a/packages/i18n/src/locales/it.i18n.json +++ b/packages/i18n/src/locales/it.i18n.json @@ -1318,11 +1318,11 @@ "Hi_username": "Ciao [name]", "Hidden": "Nascosto", "Hide": "Nascondi", - "Hide_Group_Warning": "Sei sicuro di voler nascondere il gruppo \"%s\"?", - "Hide_Livechat_Warning": "Sei sicuro di voler nascondere il livechat con \"%s\"?", + "Hide_Group_Warning": "Sei sicuro di voler nascondere il gruppo \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Sei sicuro di voler nascondere il livechat con \"{{roomName}}\"?", "Hide_On_Workspace": "Nascondi nell'area di lavoro", - "Hide_Private_Warning": "Sei sicuro di voler nascondere la discussione con \"%s\"?", - "Hide_Room_Warning": "Sei sicuro di voler nascondere il canale \"%s\"?", + "Hide_Private_Warning": "Sei sicuro di voler nascondere la discussione con \"{{roomName}}\"?", + "Hide_Room_Warning": "Sei sicuro di voler nascondere il canale \"{{roomName}}\"?", "Hide_System_Messages": "Nascondi messaggi di sistema", "Hide_Unread_Room_Status": "Nascondi lo stato del canale non letto", "Hide_counter": "Nascondi contatore", @@ -1690,10 +1690,10 @@ "Learn_more": "Per saperne di più", "Least_recent_updated": "Aggiornamento più recente", "Leave": "Lascia", - "Leave_Group_Warning": "Sei sicuro di voler lasciare il gruppo \"%s\"?", - "Leave_Livechat_Warning": "Sei sicuro di voler lasciare il live con \"%s\"?", - "Leave_Private_Warning": "Sei sicuro di volere lasciare la discussione con \"%s\"?", - "Leave_Room_Warning": "Sei sicuro di voler abbandonare il canale \"%s\"?", + "Leave_Group_Warning": "Sei sicuro di voler lasciare il gruppo \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Sei sicuro di voler lasciare il live con \"{{roomName}}\"?", + "Leave_Private_Warning": "Sei sicuro di volere lasciare la discussione con \"{{roomName}}\"?", + "Leave_Room_Warning": "Sei sicuro di voler abbandonare il canale \"{{roomName}}\"?", "Leave_a_comment": "Lascia un commento", "Leave_room": "Lasciare il canale", "Leave_the_current_channel": "Abbandona il canale corrente", @@ -3389,4 +3389,4 @@ "you_are_in_preview_mode_of": "Sei in modalità di anteprima del canale # {{room_name}}", "your_message": "il tuo messaggio", "your_message_optional": "il tuo messaggio (opzionale)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ja.i18n.json b/packages/i18n/src/locales/ja.i18n.json index 938bf7cfa4028..d43aff049dfc4 100644 --- a/packages/i18n/src/locales/ja.i18n.json +++ b/packages/i18n/src/locales/ja.i18n.json @@ -1943,12 +1943,12 @@ "Hide": "非表示", "Hide_counter": "カウンターを非表示", "Hide_flextab": "クリックと同時に右サイドバーを非表示", - "Hide_Group_Warning": "グループ「%s」を非表示にしてよろしいですか?", - "Hide_Livechat_Warning": "「%s」とのチャットを非表示にしてよろしいですか?", - "Hide_Private_Warning": "「%s」とのディスカッションを非表示にしてよろしいですか?", + "Hide_Group_Warning": "グループ「{{roomName}}」を非表示にしてよろしいですか?", + "Hide_Livechat_Warning": "「{{roomName}}」とのチャットを非表示にしてよろしいですか?", + "Hide_Private_Warning": "「{{roomName}}」とのディスカッションを非表示にしてよろしいですか?", "Hide_roles": "ロールを非表示", "Hide_room": "ルームを非表示", - "Hide_Room_Warning": "チャネル「%s」を非表示にしてよろしいですか?", + "Hide_Room_Warning": "チャネル「{{roomName}}」を非表示にしてよろしいですか?", "Hide_Unread_Room_Status": "未読のRoomステータスを非表示", "Hide_usernames": "ユーザー名を非表示", "Highlights": "ハイライト", @@ -2415,11 +2415,11 @@ "Lead_capture_email_regex": "リードキャプチャメールの正規表現", "Lead_capture_phone_regex": "リードキャプチャ電話の正規表現", "Leave": "退出", - "Leave_Group_Warning": "グループ「%s」から退出してよろしいですか?", - "Leave_Livechat_Warning": "「%s」とのオムニチャネルから退出してよろしいですか?", - "Leave_Private_Warning": "「%s」とのディスカッションから退出してよろしいですか?", + "Leave_Group_Warning": "グループ「{{roomName}}」から退出してよろしいですか?", + "Leave_Livechat_Warning": "「{{roomName}}」とのオムニチャネルから退出してよろしいですか?", + "Leave_Private_Warning": "「{{roomName}}」とのディスカッションから退出してよろしいですか?", "Leave_room": "退出", - "Leave_Room_Warning": "チャネル「%s」から退出してよろしいですか?", + "Leave_Room_Warning": "チャネル「{{roomName}}」から退出してよろしいですか?", "Leave_the_current_channel": "現在のチャネルから退出", "leave-c": "Channelから退出", "Instance": "インスタンス", diff --git a/packages/i18n/src/locales/ka-GE.i18n.json b/packages/i18n/src/locales/ka-GE.i18n.json index 5a78d4c7534da..0168d948a9c7a 100644 --- a/packages/i18n/src/locales/ka-GE.i18n.json +++ b/packages/i18n/src/locales/ka-GE.i18n.json @@ -1608,12 +1608,12 @@ "Hide": "დამალვა", "Hide_counter": "მთვლელის დამალვა", "Hide_flextab": "დამალეთ გვერდითა ბარი კლიკით", - "Hide_Group_Warning": "დარწმუნებული ხართ, რომ გსურთ \"%s\" ჯგუფის დამალვა?", - "Hide_Livechat_Warning": "დარწმუნებული ხართ, რომ გსურთ \"%s\"-თან ჩატის დამალვა?", - "Hide_Private_Warning": "დარწმუნებული ხართ, რომ გსურთ \"%s\"-თან დისკუსიის დამალვა?", + "Hide_Group_Warning": "დარწმუნებული ხართ, რომ გსურთ \"{{roomName}}\" ჯგუფის დამალვა?", + "Hide_Livechat_Warning": "დარწმუნებული ხართ, რომ გსურთ \"{{roomName}}\"-თან ჩატის დამალვა?", + "Hide_Private_Warning": "დარწმუნებული ხართ, რომ გსურთ \"{{roomName}}\"-თან დისკუსიის დამალვა?", "Hide_roles": "როლების დამალვა", "Hide_room": "ოთახის დამალვა", - "Hide_Room_Warning": "დარწმუნებული ხართ, რომ გსურთ \"%s\" ოთახის დამალვა?", + "Hide_Room_Warning": "დარწმუნებული ხართ, რომ გსურთ \"{{roomName}}\" ოთახის დამალვა?", "Hide_Unread_Room_Status": "ოთახის წაუკითხავი სტატუსის დამალვა", "Hide_usernames": "მომხმარებლის სახელების დამალვა", "Highlights": "ჰაილაითები", @@ -1997,11 +1997,11 @@ "LDAP_User_Search_Scope": "სფერო", "LDAP_Username_Field": "მომხმარებლის სახელის ველი", "Leave": "დატოვე", - "Leave_Group_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ ჯგუფი \"%s\"?", - "Leave_Livechat_Warning": "დარწმუნებული ხართ, რომ გსურთ Omnichannel- ის დატოვება \"%s\" - ით?", - "Leave_Private_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ განხილვა \"%s\"-ით?", + "Leave_Group_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ ჯგუფი \"{{roomName}}\"?", + "Leave_Livechat_Warning": "დარწმუნებული ხართ, რომ გსურთ Omnichannel- ის დატოვება \"{{roomName}}\" - ით?", + "Leave_Private_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ განხილვა \"{{roomName}}\"-ით?", "Leave_room": "ოთახის დატოვება", - "Leave_Room_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ ოთახი \"%s\"?", + "Leave_Room_Warning": "დარწმუნებული ხართ, რომ გსურთ დატოვოთ ოთახი \"{{roomName}}\"?", "Leave_the_current_channel": "დატოვეთ მიმდინარე არხი", "leave-c": "დატოვეთ არხები", "leave-p": "დატოვე პირადი ჯგუფები", diff --git a/packages/i18n/src/locales/km.i18n.json b/packages/i18n/src/locales/km.i18n.json index 1ff62dac295ea..fb697b3f6b14e 100644 --- a/packages/i18n/src/locales/km.i18n.json +++ b/packages/i18n/src/locales/km.i18n.json @@ -1469,12 +1469,12 @@ "Hide": " ", "Hide_counter": "លាក់រាប់", "Hide_flextab": "លាក់របារចំហៀងខាងស្តាំដោយចុច", - "Hide_Group_Warning": "តើអ្នកពិតជាចង់លាក់ក្រុម \"%s\" ទេ?", - "Hide_Livechat_Warning": "តើអ្នកប្រាកដជាចង់លាក់ livechat ជាមួយ \"%s\" មែនទេ?", - "Hide_Private_Warning": "តើអ្នកប្រាកដថាអ្នកចង់លាក់ការពិភាក្សាជាមួយ \"%s\" ទេ?", + "Hide_Group_Warning": "តើអ្នកពិតជាចង់លាក់ក្រុម \"{{roomName}}\" ទេ?", + "Hide_Livechat_Warning": "តើអ្នកប្រាកដជាចង់លាក់ livechat ជាមួយ \"{{roomName}}\" មែនទេ?", + "Hide_Private_Warning": "តើអ្នកប្រាកដថាអ្នកចង់លាក់ការពិភាក្សាជាមួយ \"{{roomName}}\" ទេ?", "Hide_roles": "លាក់តួនាទី", "Hide_room": "លាក់​បន្ទប់", - "Hide_Room_Warning": "តើអ្នកពិតជាចង់លាក់បន្ទប់ \"%s\"?", + "Hide_Room_Warning": "តើអ្នកពិតជាចង់លាក់បន្ទប់ \"{{roomName}}\"?", "Hide_Unread_Room_Status": "លាក់ស្ថានភាពបន្ទប់មិនទាន់អាន", "Hide_usernames": "លាក់ឈ្មោះអ្នកប្រើ", "Highlights": "ការរំលេច", @@ -1793,11 +1793,11 @@ "Lead_capture_email_regex": "នាំយកអ៊ីមែល regex", "Lead_capture_phone_regex": "នាំយកការហៅទូរស័ព្ទ regex", "Leave": "ចេញ​ពីបន្ទប់", - "Leave_Group_Warning": "តើអ្នកពិតជាចង់ចាកចេញពីក្រុម \"%s\" ទេ?", - "Leave_Livechat_Warning": "តើអ្នកប្រាកដជាចង់ចាកចេញពី livechat ជាមួយ \"%s\" មែនទេ?", - "Leave_Private_Warning": "តើអ្នកពិតជាចង់ទុកការពិភាក្សាជាមួយ \"%s\" ទេ?", + "Leave_Group_Warning": "តើអ្នកពិតជាចង់ចាកចេញពីក្រុម \"{{roomName}}\" ទេ?", + "Leave_Livechat_Warning": "តើអ្នកប្រាកដជាចង់ចាកចេញពី livechat ជាមួយ \"{{roomName}}\" មែនទេ?", + "Leave_Private_Warning": "តើអ្នកពិតជាចង់ទុកការពិភាក្សាជាមួយ \"{{roomName}}\" ទេ?", "Leave_room": "ចេញ​ពីបន្ទប់", - "Leave_Room_Warning": "តើអ្នកពិតជាចង់ចាកចេញពីបន្ទប់ \"%s\"?", + "Leave_Room_Warning": "តើអ្នកពិតជាចង់ចាកចេញពីបន្ទប់ \"{{roomName}}\"?", "Leave_the_current_channel": "ចាកចេញពីឆានែលបច្ចុប្បន្ន", "leave-c": "ចាកចេញពីឆានែល", "leave-p": "ចាកចេញពីក្រុមឯកជន", diff --git a/packages/i18n/src/locales/ko.i18n.json b/packages/i18n/src/locales/ko.i18n.json index fe3a773ae74a1..b85356990e4f9 100644 --- a/packages/i18n/src/locales/ko.i18n.json +++ b/packages/i18n/src/locales/ko.i18n.json @@ -1528,10 +1528,10 @@ "Hi_username": "[name] 님 안녕하세요.", "Hidden": "비표시", "Hide": "숨기기", - "Hide_Group_Warning": "그룹 \"%s\"을(를) 숨기시겠습니까?", - "Hide_Livechat_Warning": "\"%s\" Livechat 대화방을 숨기시겠습니까?", - "Hide_Private_Warning": "\"%s\"님과의 대화를 비표시 하시겠습니까?", - "Hide_Room_Warning": "\"%s\" 대화방을 숨기시겠습니까?", + "Hide_Group_Warning": "그룹 \"{{roomName}}\"을(를) 숨기시겠습니까?", + "Hide_Livechat_Warning": "\"{{roomName}}\" Livechat 대화방을 숨기시겠습니까?", + "Hide_Private_Warning": "\"{{roomName}}\"님과의 대화를 비표시 하시겠습니까?", + "Hide_Room_Warning": "\"{{roomName}}\" 대화방을 숨기시겠습니까?", "Hide_System_Messages": "시스템 메시지 숨기기", "Hide_Unread_Room_Status": "읽지 않은 상태 숨기기", "Hide_counter": "카운터 숨기기", @@ -1921,10 +1921,10 @@ "Lead_capture_email_regex": "리드 캡쳐 이메일 정규식", "Lead_capture_phone_regex": "리드 캡처 전화 정규식", "Leave": "나가기", - "Leave_Group_Warning": "\"%s\" 대화방에서 나가시겠습니까?", - "Leave_Livechat_Warning": "\"%s\" 실시간상담 대화방을 나가시겠습니까?", - "Leave_Private_Warning": "\"%s\"님과의 대화를 종료하시겠습니까?", - "Leave_Room_Warning": "\"%s\" 채널에서 나가시겠습니까?", + "Leave_Group_Warning": "\"{{roomName}}\" 대화방에서 나가시겠습니까?", + "Leave_Livechat_Warning": "\"{{roomName}}\" 실시간상담 대화방을 나가시겠습니까?", + "Leave_Private_Warning": "\"{{roomName}}\"님과의 대화를 종료하시겠습니까?", + "Leave_Room_Warning": "\"{{roomName}}\" 채널에서 나가시겠습니까?", "Leave_a_comment": "코멘트를 남겨주세요", "Leave_room": "나가기", "Leave_the_current_channel": "현재 대화방에서 나가기", @@ -4050,4 +4050,4 @@ "you_are_in_preview_mode_of_incoming_livechat": "이 대화는 미리보기 모드입니다.", "your_message": "메시지", "your_message_optional": "메시지 (선택 사항)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/ku.i18n.json b/packages/i18n/src/locales/ku.i18n.json index 1c0dc903b07df..0e0863bb6c20c 100644 --- a/packages/i18n/src/locales/ku.i18n.json +++ b/packages/i18n/src/locales/ku.i18n.json @@ -1215,12 +1215,12 @@ "Hide": "شاردنەوەی ژوور", "Hide_counter": "Counter counter", "Hide_flextab": "Bişkojka Right Sidebar Bişkojka Veşêre", - "Hide_Group_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina koma \"%s\"?", - "Hide_Livechat_Warning": "Ma hûn bawer dikin ku hûn dixwazin ku bi \"%s\" livechat vekin veşêre?", - "Hide_Private_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina gotûbêjeke bi \"%s\"?", + "Hide_Group_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina koma \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ma hûn bawer dikin ku hûn dixwazin ku bi \"{{roomName}}\" livechat vekin veşêre?", + "Hide_Private_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina gotûbêjeke bi \"{{roomName}}\"?", "Hide_roles": "Roles veşêre", "Hide_room": "شاردنەوەی ژوور", - "Hide_Room_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina room \"%s\"?", + "Hide_Room_Warning": "Ma tu dizanî, tu dixwazî ​​ji bo veşartina room \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Paqijkirina Bixweya Rewş", "Hide_usernames": "Naverokan veşêre bikarhêneran", "Highlights": "Highlights", @@ -1518,11 +1518,11 @@ "Lead_capture_email_regex": "Lead capture email regex", "Lead_capture_phone_regex": "Leşkerên kolektîfê girtinê regex", "Leave": "جێهێشتنی ژوور", - "Leave_Group_Warning": "Ma tu dizanî, tu dixwazî ​​ji koma \"%s\" ku herin?", - "Leave_Livechat_Warning": "Ma hûn bawer dikin ku hûn dixwazin ku \"%s\" bi livechat derkeve?", - "Leave_Private_Warning": "Ma tu dizanî, tu dixwazî ​​li gotûbêjeke bi \"%s\" ku herin?", + "Leave_Group_Warning": "Ma tu dizanî, tu dixwazî ​​ji koma \"{{roomName}}\" ku herin?", + "Leave_Livechat_Warning": "Ma hûn bawer dikin ku hûn dixwazin ku \"{{roomName}}\" bi livechat derkeve?", + "Leave_Private_Warning": "Ma tu dizanî, tu dixwazî ​​li gotûbêjeke bi \"{{roomName}}\" ku herin?", "Leave_room": "جێهێشتنی ژوور", - "Leave_Room_Warning": "Ma tu bawer î ku dixwazî ​​ji odê derkevin \"%s\"?", + "Leave_Room_Warning": "Ma tu bawer î ku dixwazî ​​ji odê derkevin \"{{roomName}}\"?", "Leave_the_current_channel": "Vê kanalek niha bistînin", "leave-c": "Channels", "leave-p": "Komên Taybet", diff --git a/packages/i18n/src/locales/lo.i18n.json b/packages/i18n/src/locales/lo.i18n.json index 0aef041a9228a..8a13478e584d8 100644 --- a/packages/i18n/src/locales/lo.i18n.json +++ b/packages/i18n/src/locales/lo.i18n.json @@ -1251,12 +1251,12 @@ "Hide": "hide ຫ້ອງ", "Hide_counter": "Hide counter", "Hide_flextab": "ຊ່ອນແຖບດ້ານຂວາດ້ວຍກົດ", - "Hide_Group_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເພື່ອຊ່ອນກຸ່ມ \"%s\"?", - "Hide_Livechat_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເຊື່ອງ livechat ດ້ວຍ \"%s\"?", - "Hide_Private_Warning": "ທ່ານວ່າທ່ານແມ່ນແນ່ໃຈວ່າຕ້ອງການບໍ່ໄດ້ສົນທະນາກັບ \"%s\"?", + "Hide_Group_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເພື່ອຊ່ອນກຸ່ມ \"{{roomName}}\"?", + "Hide_Livechat_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເຊື່ອງ livechat ດ້ວຍ \"{{roomName}}\"?", + "Hide_Private_Warning": "ທ່ານວ່າທ່ານແມ່ນແນ່ໃຈວ່າຕ້ອງການບໍ່ໄດ້ສົນທະນາກັບ \"{{roomName}}\"?", "Hide_roles": "Hide Role", "Hide_room": "hide ຫ້ອງ", - "Hide_Room_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເພື່ອຊ່ອນຫ້ອງ \"%s\"?", + "Hide_Room_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການເພື່ອຊ່ອນຫ້ອງ \"{{roomName}}\"?", "Hide_Unread_Room_Status": "ສະແດງສະຖານະຫ້ອງທີ່ບໍ່ໄດ້ອ່ານ", "Hide_usernames": "ຊ່ອນຊື່ຜູ້ໃຊ້", "Highlights": "Highlights", @@ -1557,11 +1557,11 @@ "Lead_capture_email_regex": "ນໍາການຈັບຕົວອີເມວຂອງອີເມວ", "Lead_capture_phone_regex": "ນໍາການຈັບພາບໂທລະສັບ regex", "Leave": "ຫ້ອງໃບ", - "Leave_Group_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການທີ່ຈະອອກຈາກກຸ່ມ \"%s\"?", - "Leave_Livechat_Warning": "ທ່ານແນ່ໃຈແນ່ແທ້ວ່າທ່ານຕ້ອງການທີ່ຈະປ່ອຍ livechat ດ້ວຍ \"%s\"?", - "Leave_Private_Warning": "ທ່ານວ່າທ່ານແມ່ນແນ່ໃຈວ່າຕ້ອງການທີ່ຈະອອກຈາກການສົນທະນາທີ່ມີ \"%s\"?", + "Leave_Group_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການທີ່ຈະອອກຈາກກຸ່ມ \"{{roomName}}\"?", + "Leave_Livechat_Warning": "ທ່ານແນ່ໃຈແນ່ແທ້ວ່າທ່ານຕ້ອງການທີ່ຈະປ່ອຍ livechat ດ້ວຍ \"{{roomName}}\"?", + "Leave_Private_Warning": "ທ່ານວ່າທ່ານແມ່ນແນ່ໃຈວ່າຕ້ອງການທີ່ຈະອອກຈາກການສົນທະນາທີ່ມີ \"{{roomName}}\"?", "Leave_room": "ຫ້ອງໃບ", - "Leave_Room_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການທີ່ຈະອອກຈາກຫ້ອງ \"%s\"?", + "Leave_Room_Warning": "ທ່ານແນ່ໃຈວ່າທ່ານຕ້ອງການທີ່ຈະອອກຈາກຫ້ອງ \"{{roomName}}\"?", "Leave_the_current_channel": "ອອກຈາກຊ່ອງທາງປະຈຸບັນ", "leave-c": "ອອກຈາກຊ່ອງ", "leave-p": "ອອກຈາກກຸ່ມເອກະຊົນ", diff --git a/packages/i18n/src/locales/lt.i18n.json b/packages/i18n/src/locales/lt.i18n.json index 16a051ed78381..fc5ae02a45f46 100644 --- a/packages/i18n/src/locales/lt.i18n.json +++ b/packages/i18n/src/locales/lt.i18n.json @@ -1273,12 +1273,12 @@ "Hide": "Slėpti kambarį", "Hide_counter": "Slėpti skaitiklį", "Hide_flextab": "Slėpti dešinę šoninę juostą su spustelėjimu", - "Hide_Group_Warning": "Ar tikrai norite paslėpti grupę \"%s\"?", - "Hide_Livechat_Warning": "Ar tikrai norite paslėpti livechat su \"%s\"?", - "Hide_Private_Warning": "Ar tikrai norite paslėpti diskusiją su \"%s\"?", + "Hide_Group_Warning": "Ar tikrai norite paslėpti grupę \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ar tikrai norite paslėpti livechat su \"{{roomName}}\"?", + "Hide_Private_Warning": "Ar tikrai norite paslėpti diskusiją su \"{{roomName}}\"?", "Hide_roles": "Slėpti vaidmenis", "Hide_room": "Slėpti kambarį", - "Hide_Room_Warning": "Ar tikrai norite paslėpti kambarį \"%s\"?", + "Hide_Room_Warning": "Ar tikrai norite paslėpti kambarį \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Slėpti neskaitytą kambario būseną", "Hide_usernames": "Slėpti vartotojo vardus", "Highlights": "Pabrėžia", @@ -1578,11 +1578,11 @@ "Lead_capture_email_regex": "Švinas surenkite el. Pašto regex", "Lead_capture_phone_regex": "\"Lead\" užfiksuok telefoną regex", "Leave": "Palikite kambarį", - "Leave_Group_Warning": "Ar tikrai norite išeiti iš grupės \"%s\"?", - "Leave_Livechat_Warning": "Ar tikrai norite palikti livechat su \"%s\"?", - "Leave_Private_Warning": "Ar tikrai norite palikti diskusiją su \"%s\"?", + "Leave_Group_Warning": "Ar tikrai norite išeiti iš grupės \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Ar tikrai norite palikti livechat su \"{{roomName}}\"?", + "Leave_Private_Warning": "Ar tikrai norite palikti diskusiją su \"{{roomName}}\"?", "Leave_room": "Palikite kambarį", - "Leave_Room_Warning": "Ar tikrai norite palikti kambarį \"%s\"?", + "Leave_Room_Warning": "Ar tikrai norite palikti kambarį \"{{roomName}}\"?", "Leave_the_current_channel": "Palikite dabartinį kanalą", "leave-c": "Palikti kanalus", "leave-p": "Palikti privačias grupes", diff --git a/packages/i18n/src/locales/lv.i18n.json b/packages/i18n/src/locales/lv.i18n.json index a97f9850c8cec..8801ddd30d7ba 100644 --- a/packages/i18n/src/locales/lv.i18n.json +++ b/packages/i18n/src/locales/lv.i18n.json @@ -1229,12 +1229,12 @@ "Hide": "Paslēpt istabu", "Hide_counter": "Paslēpt skaitītāju", "Hide_flextab": "Slēpt labo sānu joslu ar klikšķi", - "Hide_Group_Warning": "Vai tiešām vēlaties paslēpt grupu \"%s\"?", - "Hide_Livechat_Warning": "Vai tiešām vēlaties slēpt livechat ar \"%s\"?", - "Hide_Private_Warning": "Vai tiešām vēlaties paslēpt sarunu ar \"%s\"?", + "Hide_Group_Warning": "Vai tiešām vēlaties paslēpt grupu \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Vai tiešām vēlaties slēpt livechat ar \"{{roomName}}\"?", + "Hide_Private_Warning": "Vai tiešām vēlaties paslēpt sarunu ar \"{{roomName}}\"?", "Hide_roles": "Paslēpt lomas", "Hide_room": "Paslēpt istabu", - "Hide_Room_Warning": "Vai tiešām vēlaties paslēpt istabu \"%s\"?", + "Hide_Room_Warning": "Vai tiešām vēlaties paslēpt istabu \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Paslēpt nelasīto istabu statusu", "Hide_usernames": "Paslēpt lietotājvārdus", "Highlights": "Aktualitātes", @@ -1534,11 +1534,11 @@ "Lead_capture_email_regex": "Vadošais uztveršanas e-pasta regex", "Lead_capture_phone_regex": "Vadošais uztveršanas tālruņa regex", "Leave": "Pamest istabu", - "Leave_Group_Warning": "Vai tiešām vēlaties pamest grupu \"%s\"?", - "Leave_Livechat_Warning": "Vai tiešām vēlaties pamest livechat ar \"%s\"?", - "Leave_Private_Warning": "Vai tiešām vēlaties pamest diskusiju ar \"%s\"?", + "Leave_Group_Warning": "Vai tiešām vēlaties pamest grupu \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Vai tiešām vēlaties pamest livechat ar \"{{roomName}}\"?", + "Leave_Private_Warning": "Vai tiešām vēlaties pamest diskusiju ar \"{{roomName}}\"?", "Leave_room": "Pamest istabu", - "Leave_Room_Warning": "Vai tiešām vēlaties pamest istabu \"%s\"?", + "Leave_Room_Warning": "Vai tiešām vēlaties pamest istabu \"{{roomName}}\"?", "Leave_the_current_channel": "Pamest pašreizējo kanālu", "leave-c": "Pamest kanālus", "leave-p": "Pamest privātās grupas", diff --git a/packages/i18n/src/locales/mn.i18n.json b/packages/i18n/src/locales/mn.i18n.json index 0e55f3579a8b9..8b8f3c79173b6 100644 --- a/packages/i18n/src/locales/mn.i18n.json +++ b/packages/i18n/src/locales/mn.i18n.json @@ -1214,12 +1214,12 @@ "Hide": "Өрөө нуух", "Hide_counter": "Күүкийг нуух", "Hide_flextab": "Баруун товчлуурыг дарна уу", - "Hide_Group_Warning": "Та \"%s\" бүлгийг нуухыг хүсч байна уу?", - "Hide_Livechat_Warning": "Та livechat-г \"%s\" -тай нуухыг хүсч байгаадаа итгэлтэй байна уу?", - "Hide_Private_Warning": "Та хэлэлцүүлэгийг \"%s\" -г нуухыг хүсч байна уу?", + "Hide_Group_Warning": "Та \"{{roomName}}\" бүлгийг нуухыг хүсч байна уу?", + "Hide_Livechat_Warning": "Та livechat-г \"{{roomName}}\" -тай нуухыг хүсч байгаадаа итгэлтэй байна уу?", + "Hide_Private_Warning": "Та хэлэлцүүлэгийг \"{{roomName}}\" -г нуухыг хүсч байна уу?", "Hide_roles": "Үүрийг нуух", "Hide_room": "Өрөө нуух", - "Hide_Room_Warning": "Та \"%s\" өрөөг нуухыг хүсч байна уу?", + "Hide_Room_Warning": "Та \"{{roomName}}\" өрөөг нуухыг хүсч байна уу?", "Hide_Unread_Room_Status": "Тодорхойгүй уншлагын өрөөний байдлыг нуух", "Hide_usernames": "Хэрэглэгчийн нэрийг нуух", "Highlights": "Онцлогууд", @@ -1518,11 +1518,11 @@ "Lead_capture_email_regex": "Хар тугалга авах имэйл regex", "Lead_capture_phone_regex": "Хар тугалга барих утасны харьцаа", "Leave": "Өрөө орхи", - "Leave_Group_Warning": "Та \"%s\" бүлгийн үлдээхийг хүсч байгаадаа итгэлтэй байна уу?", - "Leave_Livechat_Warning": "Та \"livechat\" -ийг \"%s\" -тай орхих уу гэдэгт итгэлтэй байна уу?", - "Leave_Private_Warning": "Та хэлэлцүүлэгийг \"%s\" -тай орхих уу?", + "Leave_Group_Warning": "Та \"{{roomName}}\" бүлгийн үлдээхийг хүсч байгаадаа итгэлтэй байна уу?", + "Leave_Livechat_Warning": "Та \"livechat\" -ийг \"{{roomName}}\" -тай орхих уу гэдэгт итгэлтэй байна уу?", + "Leave_Private_Warning": "Та хэлэлцүүлэгийг \"{{roomName}}\" -тай орхих уу?", "Leave_room": "Өрөө орхи", - "Leave_Room_Warning": "Та \"%s\" өрөөнөөс гарахыг хүсч байна уу?", + "Leave_Room_Warning": "Та \"{{roomName}}\" өрөөнөөс гарахыг хүсч байна уу?", "Leave_the_current_channel": "Одоогийн сувгийг орхи", "leave-c": "Сувгийг орхи", "leave-p": "Хувийн бүлгүүдийг орхи", diff --git a/packages/i18n/src/locales/ms-MY.i18n.json b/packages/i18n/src/locales/ms-MY.i18n.json index 6c21de2bd1455..e9518595c8b43 100644 --- a/packages/i18n/src/locales/ms-MY.i18n.json +++ b/packages/i18n/src/locales/ms-MY.i18n.json @@ -1217,12 +1217,12 @@ "Hide": "Menyembunyikan bilik", "Hide_counter": "Sembunyikan kaunter", "Hide_flextab": "Sembunyikan Sidebar Kanan dengan Klik", - "Hide_Group_Warning": "Adakah anda pasti anda mahu menyembunyikan kumpulan \"%s\"?", - "Hide_Livechat_Warning": "Adakah anda pasti mahu menyembunyikan livechat dengan \"%s\"?", - "Hide_Private_Warning": "Adakah anda pasti anda mahu menyembunyikan perbincangan dengan \"%s\"?", + "Hide_Group_Warning": "Adakah anda pasti anda mahu menyembunyikan kumpulan \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Adakah anda pasti mahu menyembunyikan livechat dengan \"{{roomName}}\"?", + "Hide_Private_Warning": "Adakah anda pasti anda mahu menyembunyikan perbincangan dengan \"{{roomName}}\"?", "Hide_roles": "Sembunyikan Peranan", "Hide_room": "Menyembunyikan bilik", - "Hide_Room_Warning": "Adakah anda pasti anda mahu menyembunyikan bilik \"%s\"?", + "Hide_Room_Warning": "Adakah anda pasti anda mahu menyembunyikan bilik \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Sembunyikan Status Bilik Belum Dibaca", "Hide_usernames": "menyembunyikan nama pengguna", "Highlights": "Sorotan", @@ -1521,11 +1521,11 @@ "Lead_capture_email_regex": "Larangan emel tangkap utama", "Lead_capture_phone_regex": "Regex telefon menangkap utama", "Leave": "Meninggalkan bilik", - "Leave_Group_Warning": "Adakah anda pasti anda mahu meninggalkan kumpulan \"%s\"?", - "Leave_Livechat_Warning": "Adakah anda pasti mahu meninggalkan livechat dengan \"%s\"?", - "Leave_Private_Warning": "Adakah anda pasti anda mahu meninggalkan perbincangan dengan \"%s\"?", + "Leave_Group_Warning": "Adakah anda pasti anda mahu meninggalkan kumpulan \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Adakah anda pasti mahu meninggalkan livechat dengan \"{{roomName}}\"?", + "Leave_Private_Warning": "Adakah anda pasti anda mahu meninggalkan perbincangan dengan \"{{roomName}}\"?", "Leave_room": "Meninggalkan bilik", - "Leave_Room_Warning": "Adakah anda pasti anda mahu meninggalkan bilik \"%s\"?", + "Leave_Room_Warning": "Adakah anda pasti anda mahu meninggalkan bilik \"{{roomName}}\"?", "Leave_the_current_channel": "Tinggalkan saluran semasa", "leave-c": "Tinggalkan Saluran", "leave-p": "Tinggalkan Kumpulan Swasta", diff --git a/packages/i18n/src/locales/nb.i18n.json b/packages/i18n/src/locales/nb.i18n.json index 48f7af9afd2d7..faf3c144cd7f0 100644 --- a/packages/i18n/src/locales/nb.i18n.json +++ b/packages/i18n/src/locales/nb.i18n.json @@ -2483,16 +2483,16 @@ "You_do_not_have_permission_to_execute_this_command": "Du har ikke nødvendige tillatelser til å utføre kommandoen: `/{{command}}`", "Hide_flextab": "Skjul innholdslinjen ved å klikke utenfor den", "You_have_reached_the_limit_active_costumers_this_month": "Du har nådd grensen for aktive kunder denne måneden", - "Hide_Group_Warning": "Er du sikker på at du vil skjule gruppen \"%s\"?", - "Hide_Livechat_Warning": "Er du sikker på at du vil skjule chatten med \"%s\"?", + "Hide_Group_Warning": "Er du sikker på at du vil skjule gruppen \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Er du sikker på at du vil skjule chatten med \"{{roomName}}\"?", "Estimated_wait_time": "Beregnet ventetid", "Estimated_wait_time_in_minutes": "Beregnet ventetid (tid i minutter)", - "Hide_Private_Warning": "Er du sikker på at du vil skjule diskusjonen med \"%s\"?", + "Hide_Private_Warning": "Er du sikker på at du vil skjule diskusjonen med \"{{roomName}}\"?", "Hide_roles": "Skjul roller", "Event_notifications": "Hendelsesvarsler", "Event_notifications_description": "Ved å deaktivere denne innstillingen forhindrer du appen i å varsle deg om kommende arrangementer.", "Hide_room": "Skjul rom", - "Hide_Room_Warning": "Er du sikker på at du vil skjule kanalen \"%s\"?", + "Hide_Room_Warning": "Er du sikker på at du vil skjule kanalen \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Skjul ulest romstatus", "Hide_usernames": "Skjul brukernavn", "every_30_seconds": "En gang hvert 30. sekund", @@ -3103,12 +3103,12 @@ "Install_anyway": "Installer allikevel ", "Update_anyway": "Oppdater uansett", "Leave": "Forlat", - "Leave_Group_Warning": "Er du sikker på at du vil forlate gruppen \"%s\"?", - "Leave_Livechat_Warning": "Er du sikker på at du vil forlate omnikanalen med \"%s\"?", - "Leave_Private_Warning": "Er du sikker på at du vil forlate diskusjonen med \"%s\"?", + "Leave_Group_Warning": "Er du sikker på at du vil forlate gruppen \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Er du sikker på at du vil forlate omnikanalen med \"{{roomName}}\"?", + "Leave_Private_Warning": "Er du sikker på at du vil forlate diskusjonen med \"{{roomName}}\"?", "Installing": "Installerer", "Leave_room": "Forlat rom", - "Leave_Room_Warning": "Er du sikker på at du vil forlate kanalen \"%s\"?", + "Leave_Room_Warning": "Er du sikker på at du vil forlate kanalen \"{{roomName}}\"?", "Leave_the_current_channel": "Forlat gjeldende kanal", "leave-c": "Forlat kanaler", "Instance": "Forekomst", @@ -6199,4 +6199,4 @@ "Recent": "Nylig", "On_All_Contacts": "På alle kontakter", "Once": "En gang" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/nl.i18n.json b/packages/i18n/src/locales/nl.i18n.json index d8ab54e9f9059..0ec8f860f5897 100644 --- a/packages/i18n/src/locales/nl.i18n.json +++ b/packages/i18n/src/locales/nl.i18n.json @@ -1957,12 +1957,12 @@ "Hide": "Verbergen", "Hide_counter": "Teller verbergen", "Hide_flextab": "Verberg de rechterzijbalk met klik", - "Hide_Group_Warning": "Weet je zeker dat je groep \"%s\" wilt verbergen?", - "Hide_Livechat_Warning": "Weet je zeker dat je de chat met \"%s\" wilt verbergen?", - "Hide_Private_Warning": "Weet je zeker dat je de discussie met \"%s\" wilt verbergen?", + "Hide_Group_Warning": "Weet je zeker dat je groep \"{{roomName}}\" wilt verbergen?", + "Hide_Livechat_Warning": "Weet je zeker dat je de chat met \"{{roomName}}\" wilt verbergen?", + "Hide_Private_Warning": "Weet je zeker dat je de discussie met \"{{roomName}}\" wilt verbergen?", "Hide_roles": "Rollen verbergen", "Hide_room": "Kamer verbergen", - "Hide_Room_Warning": "Weet je zeker dat je het kanaal \"%s\" wilt verbergen?", + "Hide_Room_Warning": "Weet je zeker dat je het kanaal \"{{roomName}}\" wilt verbergen?", "Hide_Unread_Room_Status": "Verberg ongelezen kamerstatus", "Hide_usernames": "Gebruikersnamen verbergen", "Highlights": "Hoogtepunten", @@ -2431,11 +2431,11 @@ "Lead_capture_email_regex": "Regex voor e-mailregistratie voor leads", "Lead_capture_phone_regex": "Lead capture telefoon regex", "Leave": "Verlaten", - "Leave_Group_Warning": "Weet je zeker dat je groep \"%s\" wilt verlaten?", - "Leave_Livechat_Warning": "Weet je zeker dat je het omnichannel wilt verlaten met \"%s\"?", - "Leave_Private_Warning": "Weet je zeker dat je de discussie met \"%s\" wilt verlaten?", + "Leave_Group_Warning": "Weet je zeker dat je groep \"{{roomName}}\" wilt verlaten?", + "Leave_Livechat_Warning": "Weet je zeker dat je het omnichannel wilt verlaten met \"{{roomName}}\"?", + "Leave_Private_Warning": "Weet je zeker dat je de discussie met \"{{roomName}}\" wilt verlaten?", "Leave_room": "Verlaten", - "Leave_Room_Warning": "Weet je zeker dat je het kanaal \"%s\" wilt verlaten?", + "Leave_Room_Warning": "Weet je zeker dat je het kanaal \"{{roomName}}\" wilt verlaten?", "Leave_the_current_channel": "Verlaat het huidige kanaal", "leave-c": "Kanalen verlaten", "Instance": "Instantie", diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index 6d4a60a559807..7df183ba1af03 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -2483,16 +2483,16 @@ "You_do_not_have_permission_to_execute_this_command": "Du har ikke nødvendige tillatelser til å utføre kommandoen: `/{{command}}`", "Hide_flextab": "Skjul høyre sidefelt med klikk", "You_have_reached_the_limit_active_costumers_this_month": "Du har nådd grensen for aktive kunder denne måneden", - "Hide_Group_Warning": "Er du sikker på at du vil gjemme gruppen \"%s\"?", - "Hide_Livechat_Warning": "Er du sikker på at du vil gjemme livechat med \"%s\"?", + "Hide_Group_Warning": "Er du sikker på at du vil gjemme gruppen \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Er du sikker på at du vil gjemme livechat med \"{{roomName}}\"?", "Estimated_wait_time": "Beregnet ventetid", "Estimated_wait_time_in_minutes": "Beregnet ventetid (tid i minutter)", - "Hide_Private_Warning": "Er du sikker på at du vil gjemme diskusjonen med \"%s\"?", + "Hide_Private_Warning": "Er du sikker på at du vil gjemme diskusjonen med \"{{roomName}}\"?", "Hide_roles": "Skjul roller", "Event_notifications": "Hendelsesvarsler", "Event_notifications_description": "Ved å deaktivere denne innstillingen forhindrer du appen i å varsle deg om kommende arrangementer.", "Hide_room": "Skjul rom", - "Hide_Room_Warning": "Er du sikker på at du vil gjemme rommet \"%s\"?", + "Hide_Room_Warning": "Er du sikker på at du vil gjemme rommet \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Skjul ulest romstatus", "Hide_usernames": "Skjul brukernavn", "every_30_seconds": "En gang hvert 30. sekund", @@ -3103,12 +3103,12 @@ "Install_anyway": "Installer allikevel ", "Update_anyway": "Oppdater uansett", "Leave": "Forlat rom", - "Leave_Group_Warning": "Er du sikker på at du vil forlate gruppen \"%s\"?", - "Leave_Livechat_Warning": "Er du sikker på at du vil forlate livechat med \"%s\"?", - "Leave_Private_Warning": "Er du sikker på at du vil legge diskusjonen med \"%s\"?", + "Leave_Group_Warning": "Er du sikker på at du vil forlate gruppen \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Er du sikker på at du vil forlate livechat med \"{{roomName}}\"?", + "Leave_Private_Warning": "Er du sikker på at du vil legge diskusjonen med \"{{roomName}}\"?", "Installing": "Installerer", "Leave_room": "Forlat rom", - "Leave_Room_Warning": "Er du sikker på at du vil forlate rommet \"%s\"?", + "Leave_Room_Warning": "Er du sikker på at du vil forlate rommet \"{{roomName}}\"?", "Leave_the_current_channel": "La den nåværende kanalen gå", "leave-c": "La kanaler", "Instance": "Forekomst", diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index 843c5ce238c7d..4ab76d056fadc 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -1908,10 +1908,10 @@ "Hi_username": "Witaj [name]", "Hidden": "Ukryty", "Hide": "Ukryj", - "Hide_Group_Warning": "Czy na pewno chcesz ukryć grupę \"%s\"?", - "Hide_Livechat_Warning": "Czy na pewno chcesz ukryć livechat z \"%s\"?", - "Hide_Private_Warning": "Czy na pewno chcesz ukryć dyskusję z \"%s\"?", - "Hide_Room_Warning": "Czy na pewno chcesz ukryć pokój \"%s\"?", + "Hide_Group_Warning": "Czy na pewno chcesz ukryć grupę \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Czy na pewno chcesz ukryć livechat z \"{{roomName}}\"?", + "Hide_Private_Warning": "Czy na pewno chcesz ukryć dyskusję z \"{{roomName}}\"?", + "Hide_Room_Warning": "Czy na pewno chcesz ukryć pokój \"{{roomName}}\"?", "Hide_System_Messages": "Ukryj wiadomości systemowe", "Hide_Unread_Room_Status": "Ukryj nieprzeczytany stan pokoju", "Hide_counter": "Ukryj licznik", @@ -2399,10 +2399,10 @@ "Learn_more_about_accessibility": "Dowiedz się więcej o naszym zaangażowaniu w dostępność tutaj:", "Least_recent_updated": "Najstarsza aktualizacja", "Leave": "Opuść", - "Leave_Group_Warning": "Czy na pewno chcesz opuścić grupę \"%s\"?", - "Leave_Livechat_Warning": "Czy na pewno chcesz opuścić livechat za pomocą \"%s\"?", - "Leave_Private_Warning": "Czy na pewno chcesz opuścić dyskusję z \"%s\"?", - "Leave_Room_Warning": "Czy na pewno chcesz opuścić pokój \"%s\"?", + "Leave_Group_Warning": "Czy na pewno chcesz opuścić grupę \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Czy na pewno chcesz opuścić livechat za pomocą \"{{roomName}}\"?", + "Leave_Private_Warning": "Czy na pewno chcesz opuścić dyskusję z \"{{roomName}}\"?", + "Leave_Room_Warning": "Czy na pewno chcesz opuścić pokój \"{{roomName}}\"?", "Leave_a_comment": "Zostaw komentarz", "Leave_room": "Opuść pokój", "Leave_the_current_channel": "Opuść aktualny kanał", @@ -5399,4 +5399,4 @@ "you_are_in_preview_mode_of_incoming_livechat": "Jesteś w trybie podglądu wiadomości przychodzącej livechat", "your_message": "twoja wiadomość", "your_message_optional": "twoja wiadomość (opcjonalnie)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index 3f6d61ee40fd6..e22554721adfd 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -1817,10 +1817,10 @@ "Hi_username": "Oi [name]", "Hidden": "Oculto", "Hide": "Ocultar", - "Hide_Group_Warning": "Tem certeza de que deseja ocultar o grupo \"%s\"?", - "Hide_Livechat_Warning": "Tem certeza de que deseja ocultar a conversa com \"%s\"?", - "Hide_Private_Warning": "Tem certeza de que deseja ocultar a conversa com \"%s\"?", - "Hide_Room_Warning": "Tem certeza de que deseja ocultar o canal \"%s\"?", + "Hide_Group_Warning": "Tem certeza de que deseja ocultar o grupo \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Tem certeza de que deseja ocultar a conversa com \"{{roomName}}\"?", + "Hide_Private_Warning": "Tem certeza de que deseja ocultar a conversa com \"{{roomName}}\"?", + "Hide_Room_Warning": "Tem certeza de que deseja ocultar o canal \"{{roomName}}\"?", "Hide_System_Messages": "Ocultar mensagens do sistema", "Hide_Unread_Room_Status": "Ocultar status da sala não lida", "Hide_counter": "Ocultar contador", @@ -2297,10 +2297,10 @@ "Lead_capture_phone_regex": "Regex de telefone de captura de lead", "Least_recent_updated": "Atualizado há mais tempo", "Leave": "Sair", - "Leave_Group_Warning": "Tem certeza de que quer sair do grupo \"%s\"?", - "Leave_Livechat_Warning": "Tem certeza de que deseja sair do omnichannel com \"%s\"?", - "Leave_Private_Warning": "Tem certeza de que quer sair da conversa com \"%s\"?", - "Leave_Room_Warning": "Tem certeza de que deseja sair do canal \"%s\"?", + "Leave_Group_Warning": "Tem certeza de que quer sair do grupo \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Tem certeza de que deseja sair do omnichannel com \"{{roomName}}\"?", + "Leave_Private_Warning": "Tem certeza de que quer sair da conversa com \"{{roomName}}\"?", + "Leave_Room_Warning": "Tem certeza de que deseja sair do canal \"{{roomName}}\"?", "Leave_a_comment": "Deixe um comentário", "Leave_room": "Sair", "Leave_the_current_channel": "Sai do canal atual", @@ -5110,4 +5110,4 @@ "you_are_in_preview_mode_of_incoming_livechat": "Você está no modo de visualização desta conversa", "your_message": "sua mensagem", "your_message_optional": "sua mensagem (opcional)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/pt.i18n.json b/packages/i18n/src/locales/pt.i18n.json index 0eeda558f10a7..567fcffa92e3b 100644 --- a/packages/i18n/src/locales/pt.i18n.json +++ b/packages/i18n/src/locales/pt.i18n.json @@ -1457,12 +1457,12 @@ "Hide": "Ocultar", "Hide_counter": "Ocultar contador", "Hide_flextab": "Esconder barra da direita com clique", - "Hide_Group_Warning": "Tem certeza de que deseja ocultar o grupo \"%s\"?", - "Hide_Livechat_Warning": "Tem certeza de que deseja esconder o livechat com \"%s\"?", - "Hide_Private_Warning": "Tem certeza de que deseja ocultar a conversa com \"%s\"?", + "Hide_Group_Warning": "Tem certeza de que deseja ocultar o grupo \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Tem certeza de que deseja esconder o livechat com \"{{roomName}}\"?", + "Hide_Private_Warning": "Tem certeza de que deseja ocultar a conversa com \"{{roomName}}\"?", "Hide_roles": "Ocultar funções", "Hide_room": "Esconder sala", - "Hide_Room_Warning": "Tem certeza de que deseja ocultar a sala \"%s\"?", + "Hide_Room_Warning": "Tem certeza de que deseja ocultar a sala \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Ocultar status de sala não lida", "Hide_usernames": "Esconder nomes de utilizador", "Highlights": "Destaques", @@ -1783,11 +1783,11 @@ "Lead_capture_email_regex": "Regex de e-mail de captura de chumbo", "Lead_capture_phone_regex": "Regex de telefone de captura de chumbo", "Leave": "Sair", - "Leave_Group_Warning": "Tem a certeza de que quer sair do grupo \"%s\"?", - "Leave_Livechat_Warning": "Tem a certeza de que deseja deixar o Livechat com \"%s\"?", - "Leave_Private_Warning": "Tem a certeza de que quer sair da discussão com \"%s\"?", + "Leave_Group_Warning": "Tem a certeza de que quer sair do grupo \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Tem a certeza de que deseja deixar o Livechat com \"{{roomName}}\"?", + "Leave_Private_Warning": "Tem a certeza de que quer sair da discussão com \"{{roomName}}\"?", "Leave_room": "Sair da sala", - "Leave_Room_Warning": "Tem a certeza de que deseja sair da sala \"%s\"?", + "Leave_Room_Warning": "Tem a certeza de que deseja sair da sala \"{{roomName}}\"?", "Leave_the_current_channel": "Sai deste canal", "leave-c": "Sair dos canais", "leave-p": "Sair dos grupos privados", diff --git a/packages/i18n/src/locales/ro.i18n.json b/packages/i18n/src/locales/ro.i18n.json index 2e530dd5a36fc..732c6902f1e1e 100644 --- a/packages/i18n/src/locales/ro.i18n.json +++ b/packages/i18n/src/locales/ro.i18n.json @@ -1218,7 +1218,7 @@ "Hide_counter": "Ascundeți contorul", "Hide_flextab": "Ascundeți bara laterală din dreapta cu clic", "Hide_Group_Warning": "Sunetți sigur că vreți să ascundeți grupul?", - "Hide_Livechat_Warning": "Sigur doriți să ascundeți livechat-ul cu \"%s\"?", + "Hide_Livechat_Warning": "Sigur doriți să ascundeți livechat-ul cu \"{{roomName}}\"?", "Hide_Private_Warning": "Sunteți sigur că vreți să ascundeți discuția?", "Hide_roles": "Ascundeți rolurile", "Hide_room": "Ascunde camera", @@ -1523,7 +1523,7 @@ "Lead_capture_phone_regex": "Plătește regex telefoanele de captare", "Leave": "Părăsește camera", "Leave_Group_Warning": "Sunteți sigur că vreți să părăsiți grupul?", - "Leave_Livechat_Warning": "Sigur doriți să părăsiți livechat-ul cu \"%s\"?", + "Leave_Livechat_Warning": "Sigur doriți să părăsiți livechat-ul cu \"{{roomName}}\"?", "Leave_Private_Warning": "Sunteți sigur că vreți să părăsiți discuția?", "Leave_room": "Părăsește camera", "Leave_Room_Warning": "Sunteți sigur că vreți să părăsiți camera?", diff --git a/packages/i18n/src/locales/ru.i18n.json b/packages/i18n/src/locales/ru.i18n.json index 1b5c7929818e6..5ca0dff552226 100644 --- a/packages/i18n/src/locales/ru.i18n.json +++ b/packages/i18n/src/locales/ru.i18n.json @@ -1848,11 +1848,11 @@ "Hi_username": "Привет [name]", "Hidden": "Скрытый", "Hide": "Скрыть", - "Hide_Group_Warning": "Вы уверены, что хотите спрятать группу \"%s\"?", - "Hide_Livechat_Warning": "Вы уверены, что хотите спрятать Livechat с \"%s\"?", + "Hide_Group_Warning": "Вы уверены, что хотите спрятать группу \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Вы уверены, что хотите спрятать Livechat с \"{{roomName}}\"?", "Hide_On_Workspace": "Скрыть в рабочем пространстве", - "Hide_Private_Warning": "Вы уверены, что хотите спрятать беседу с \"%s\"?", - "Hide_Room_Warning": "Вы уверены, что хотите спрятать комнату \"%s\"?", + "Hide_Private_Warning": "Вы уверены, что хотите спрятать беседу с \"{{roomName}}\"?", + "Hide_Room_Warning": "Вы уверены, что хотите спрятать комнату \"{{roomName}}\"?", "Hide_System_Messages": "Скрыть Системные Сообщения", "Hide_Unread_Room_Status": "Скрыть статус \"непрочитанно\" у комнаты", "Hide_counter": "Скрыть счетчик", @@ -2324,10 +2324,10 @@ "Learn_how_to_unlock_the_myriad_possibilities_of_rocket_chat": "Узнайте о всех возможностях Rocket.Chat.", "Least_recent_updated": "Наименее недавнее обновление", "Leave": "Покинуть", - "Leave_Group_Warning": "Вы уверены, что хотите покинуть группу \"%s\"?", - "Leave_Livechat_Warning": "Вы уверены, что хотите покинуть Livechat с \"%s\"?", - "Leave_Private_Warning": "Вы уверены, что хотите покинуть беседу с \"%s\"?", - "Leave_Room_Warning": "Вы уверены, что хотите покинуть комнату \"%s\"?", + "Leave_Group_Warning": "Вы уверены, что хотите покинуть группу \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Вы уверены, что хотите покинуть Livechat с \"{{roomName}}\"?", + "Leave_Private_Warning": "Вы уверены, что хотите покинуть беседу с \"{{roomName}}\"?", + "Leave_Room_Warning": "Вы уверены, что хотите покинуть комнату \"{{roomName}}\"?", "Leave_a_comment": "Оставить комментарий", "Leave_room": "Покинуть чат", "Leave_the_current_channel": "Покинуть текущий канал", @@ -5072,4 +5072,4 @@ "you_are_in_preview_mode_of_incoming_livechat": "Вы находитесь в режиме предварительного просмотра этого живого чата", "your_message": "ваше сообщение", "your_message_optional": "ваше сообщение (опционально)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/sk-SK.i18n.json b/packages/i18n/src/locales/sk-SK.i18n.json index 1aa632012aa4a..583f959fe5cc8 100644 --- a/packages/i18n/src/locales/sk-SK.i18n.json +++ b/packages/i18n/src/locales/sk-SK.i18n.json @@ -1228,12 +1228,12 @@ "Hide": "Skryť miestnosť", "Hide_counter": "Skryť počítadlo", "Hide_flextab": "Skryť pravý bočný panel kliknutím", - "Hide_Group_Warning": "Naozaj chcete skryť skupinu \"%s\"?", - "Hide_Livechat_Warning": "Naozaj chcete skryť livechat s \"%s\"?", - "Hide_Private_Warning": "Naozaj chcete diskusiu skryť pomocou \"%s\"?", + "Hide_Group_Warning": "Naozaj chcete skryť skupinu \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Naozaj chcete skryť livechat s \"{{roomName}}\"?", + "Hide_Private_Warning": "Naozaj chcete diskusiu skryť pomocou \"{{roomName}}\"?", "Hide_roles": "Skryť role", "Hide_room": "Skryť miestnosť", - "Hide_Room_Warning": "Naozaj chcete skryť miestnosť \"%s\"?", + "Hide_Room_Warning": "Naozaj chcete skryť miestnosť \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Skryť nepresaný stav miestnosti", "Hide_usernames": "Skryť používateľské mená", "Highlights": "prednosti", @@ -1533,11 +1533,11 @@ "Lead_capture_email_regex": "Zachyťte e-mail regex", "Lead_capture_phone_regex": "Olovo zachytiť telefón regex", "Leave": "Nechajte miestnosť", - "Leave_Group_Warning": "Naozaj chcete opustiť skupinu \"%s\"?", - "Leave_Livechat_Warning": "Naozaj chcete opustiť livechat s \"%s\"?", - "Leave_Private_Warning": "Naozaj chcete diskusiu ponechať s názvom \"%s\"?", + "Leave_Group_Warning": "Naozaj chcete opustiť skupinu \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Naozaj chcete opustiť livechat s \"{{roomName}}\"?", + "Leave_Private_Warning": "Naozaj chcete diskusiu ponechať s názvom \"{{roomName}}\"?", "Leave_room": "Nechajte miestnosť", - "Leave_Room_Warning": "Naozaj chcete opustiť miestnosť \"%s\"?", + "Leave_Room_Warning": "Naozaj chcete opustiť miestnosť \"{{roomName}}\"?", "Leave_the_current_channel": "Nechajte aktuálny kanál", "leave-c": "Ponechajte kanály", "leave-p": "Nechajte súkromné ​​skupiny", diff --git a/packages/i18n/src/locales/sl-SI.i18n.json b/packages/i18n/src/locales/sl-SI.i18n.json index 7d7b2c692de96..1df28532e612e 100644 --- a/packages/i18n/src/locales/sl-SI.i18n.json +++ b/packages/i18n/src/locales/sl-SI.i18n.json @@ -1209,12 +1209,12 @@ "Hide": "Skrij sobo", "Hide_counter": "Skrij števec", "Hide_flextab": "S klikom skrijte desno stransko vrstico", - "Hide_Group_Warning": "Ali ste prepričani, da želite skriti skupino \"%s\"?", - "Hide_Livechat_Warning": "Ali ste prepričani, da želite skriti klepet v živo z \"%s\"?", - "Hide_Private_Warning": "Ali ste prepričani, da želite skriti pogovor z \"%s\"?", + "Hide_Group_Warning": "Ali ste prepričani, da želite skriti skupino \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ali ste prepričani, da želite skriti klepet v živo z \"{{roomName}}\"?", + "Hide_Private_Warning": "Ali ste prepričani, da želite skriti pogovor z \"{{roomName}}\"?", "Hide_roles": "Skrij vloge", "Hide_room": "Skrij sobo", - "Hide_Room_Warning": "Ali ste prepričani, da želite skriti sobo \"%s\"?", + "Hide_Room_Warning": "Ali ste prepričani, da želite skriti sobo \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Skrij neprebrani status sobe", "Hide_usernames": "Skrij uporabniška imena", "Highlights": "Zanimivosti", @@ -1513,11 +1513,11 @@ "Lead_capture_email_regex": "Regularni izraz za prepoznavanje e-poštnih naslovov", "Lead_capture_phone_regex": "Regularni izraz za prepoznavanje telefonskih številk", "Leave": "Zapusti sobo", - "Leave_Group_Warning": "Ali ste prepričani, da želite zapustiti skupino \"%s\"?", - "Leave_Livechat_Warning": "Ali ste prepričani, da želite zapustiti klepet v živo z osebo \"%s\"?", - "Leave_Private_Warning": "Ali ste prepričani, da želite zapustiti pogovor z osebo \"%s\"?", + "Leave_Group_Warning": "Ali ste prepričani, da želite zapustiti skupino \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Ali ste prepričani, da želite zapustiti klepet v živo z osebo \"{{roomName}}\"?", + "Leave_Private_Warning": "Ali ste prepričani, da želite zapustiti pogovor z osebo \"{{roomName}}\"?", "Leave_room": "Zapusti sobo", - "Leave_Room_Warning": "Ali ste prepričani, da želite zapustiti sobo \"%s\"?", + "Leave_Room_Warning": "Ali ste prepričani, da želite zapustiti sobo \"{{roomName}}\"?", "Leave_the_current_channel": "Zapusti trenutni kanal", "leave-c": "Zapusti kanale", "leave-p": "Zapustite zasebne skupine", diff --git a/packages/i18n/src/locales/sq.i18n.json b/packages/i18n/src/locales/sq.i18n.json index 1ffb83c6172d5..62f54c3e14cc8 100644 --- a/packages/i18n/src/locales/sq.i18n.json +++ b/packages/i18n/src/locales/sq.i18n.json @@ -1219,12 +1219,12 @@ "Hide": "Fshihe dhomën", "Hide_counter": "Fshih kundër", "Hide_flextab": "Fshiheni Faqen e Djathtë me Klik", - "Hide_Group_Warning": "Jeni te sigurte qe doni te fshehur grupin \"%s\"?", - "Hide_Livechat_Warning": "Jeni i sigurt që doni të fshehni livechat me \"%s\"?", - "Hide_Private_Warning": "A jeni i sigurt që ju doni të fshehur diskutimin me \"%s\"?", + "Hide_Group_Warning": "Jeni te sigurte qe doni te fshehur grupin \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Jeni i sigurt që doni të fshehni livechat me \"{{roomName}}\"?", + "Hide_Private_Warning": "A jeni i sigurt që ju doni të fshehur diskutimin me \"{{roomName}}\"?", "Hide_roles": "Hide Roles", "Hide_room": "Fshihe dhomën", - "Hide_Room_Warning": "Jeni te sigurte qe doni te fshehur në dhomë \"%s\"?", + "Hide_Room_Warning": "Jeni te sigurte qe doni te fshehur në dhomë \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Fshihni statusin e pambrojtur të dhomës", "Hide_usernames": "Fshih përdoruesve", "Highlights": "Pikat kryesore", @@ -1522,11 +1522,11 @@ "Lead_capture_email_regex": "Plotësoni kapjen e regex-it të postës elektronike", "Lead_capture_phone_regex": "Regjimi i kapjes së telefonit të plumbit", "Leave": "Largohu nga dhoma", - "Leave_Group_Warning": "Jeni te sigurte qe doni te largohet nga grupi \"%s\"?", - "Leave_Livechat_Warning": "Je i sigurt që dëshiron të largosh livechat me \"%s\"?", - "Leave_Private_Warning": "Jeni te sigurte qe doni te largohet diskutimin me \"%s\"?", + "Leave_Group_Warning": "Jeni te sigurte qe doni te largohet nga grupi \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Je i sigurt që dëshiron të largosh livechat me \"{{roomName}}\"?", + "Leave_Private_Warning": "Jeni te sigurte qe doni te largohet diskutimin me \"{{roomName}}\"?", "Leave_room": "Largohu nga dhoma", - "Leave_Room_Warning": "Jeni te sigurte qe doni te largohen nga dhoma \"%s\"?", + "Leave_Room_Warning": "Jeni te sigurte qe doni te largohen nga dhoma \"{{roomName}}\"?", "Leave_the_current_channel": "Lëreni kanalin aktual", "leave-c": "Lërini kanalet", "leave-p": "Lini Grupet Private", diff --git a/packages/i18n/src/locales/sr.i18n.json b/packages/i18n/src/locales/sr.i18n.json index caf41c09fa8da..4c3f5ca7bdba6 100644 --- a/packages/i18n/src/locales/sr.i18n.json +++ b/packages/i18n/src/locales/sr.i18n.json @@ -1084,12 +1084,12 @@ "Hidden": "Сакривен", "Hide": "Сакриј собу", "Hide_counter": "Сакриј бројач", - "Hide_Group_Warning": "Да ли заиста желите да сакријете групу \"%s\"?", - "Hide_Livechat_Warning": "Да ли заиста желите да сакријете ћаскање са \"%s\"?", - "Hide_Private_Warning": "Да ли заиста желите да сакријете расправу са \"%s\"?", + "Hide_Group_Warning": "Да ли заиста желите да сакријете групу \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Да ли заиста желите да сакријете ћаскање са \"{{roomName}}\"?", + "Hide_Private_Warning": "Да ли заиста желите да сакријете расправу са \"{{roomName}}\"?", "Hide_roles": "Сакриј улоге", "Hide_room": "Сакриј собу", - "Hide_Room_Warning": "Да ли заиста желите да сакријете собу \"%s\"?", + "Hide_Room_Warning": "Да ли заиста желите да сакријете собу \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Сакриј статус непрописане собе", "Hide_usernames": "Сакриј корисничка имена", "Highlights": "Наглашавања", @@ -1355,11 +1355,11 @@ "Lead_capture_email_regex": "Леад регек е-поште", "Lead_capture_phone_regex": "Оловно снимање регекса телефона", "Leave": "Напусти собу", - "Leave_Group_Warning": "Да ли сте сигурни да желите да напустите групу \"%s\"?", - "Leave_Livechat_Warning": "Јесте ли сигурни да желите да оставите ливецхат са \"%s\"?", - "Leave_Private_Warning": "Да ли сте сигурни да желите да напустите дискусију са \"%s\"?", + "Leave_Group_Warning": "Да ли сте сигурни да желите да напустите групу \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Јесте ли сигурни да желите да оставите ливецхат са \"{{roomName}}\"?", + "Leave_Private_Warning": "Да ли сте сигурни да желите да напустите дискусију са \"{{roomName}}\"?", "Leave_room": "Напусти собу", - "Leave_Room_Warning": "Да ли сте сигурни да желите да напустите просторију \"%s\"?", + "Leave_Room_Warning": "Да ли сте сигурни да желите да напустите просторију \"{{roomName}}\"?", "Leave_the_current_channel": "Напустите тренутни канал", "leave-c": "Леаве Цханнелс", "leave-p": "Оставите приватне групе", diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index 2a84782f92534..343ce66c002e7 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -2198,14 +2198,14 @@ "You_do_not_have_permission_to_do_this": "Du har inte behörighet att göra det här", "Hide_counter": "Dölj räknare", "Hide_flextab": "Dölj höger sidofält med klick", - "Hide_Group_Warning": "Är du säker att du vill dölja gruppen \"%s\"?", - "Hide_Livechat_Warning": "Är du säker på att du vill dölja chat med \"%s\"?", + "Hide_Group_Warning": "Är du säker att du vill dölja gruppen \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Är du säker på att du vill dölja chat med \"{{roomName}}\"?", "Estimated_wait_time": "Beräknad väntetid", "Estimated_wait_time_in_minutes": "Beräknad väntetid (tid i minuter)", - "Hide_Private_Warning": "Är du säker att du vill dölja diskussionen med \"%s\"?", + "Hide_Private_Warning": "Är du säker att du vill dölja diskussionen med \"{{roomName}}\"?", "Hide_roles": "Dölj roller", "Hide_room": "Dölj rum", - "Hide_Room_Warning": "Är du säker att du vill dölja rummet \"%s\"?", + "Hide_Room_Warning": "Är du säker att du vill dölja rummet \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Dölj oläst rums status", "Hide_usernames": "Dölj användarnamn", "Highlights": "Markeringar", @@ -2739,11 +2739,11 @@ "Inline_code": "Inline-kod", "Install_anyway": "Installera ändå", "Leave": "Lämna", - "Leave_Group_Warning": "Är du säker på att du vill lämna gruppen \"%s\"?", - "Leave_Livechat_Warning": "Är du säker på att du vill lämna Omnichannel med \"%s\"?", - "Leave_Private_Warning": "Är du säker på att du vill lämna diskussionen med \"%s\"?", + "Leave_Group_Warning": "Är du säker på att du vill lämna gruppen \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Är du säker på att du vill lämna Omnichannel med \"{{roomName}}\"?", + "Leave_Private_Warning": "Är du säker på att du vill lämna diskussionen med \"{{roomName}}\"?", "Leave_room": "Lämna", - "Leave_Room_Warning": "Är du säker på att du vill lämna kanalen \"%s\"?", + "Leave_Room_Warning": "Är du säker på att du vill lämna kanalen \"{{roomName}}\"?", "Leave_the_current_channel": "Lämna den nuvarande kanalen", "leave-c": "Lämna kanaler", "Instance": "Instans", diff --git a/packages/i18n/src/locales/ta-IN.i18n.json b/packages/i18n/src/locales/ta-IN.i18n.json index fd93c4a8e9e14..52319c9fb2190 100644 --- a/packages/i18n/src/locales/ta-IN.i18n.json +++ b/packages/i18n/src/locales/ta-IN.i18n.json @@ -1218,12 +1218,12 @@ "Hide": "அறையை மறை", "Hide_counter": "எதிர் மறை", "Hide_flextab": "கிளிக் மூலம் வலது பக்க மறை", - "Hide_Group_Warning": "குழு \"%s\" பதில் மறைக்க வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", - "Hide_Livechat_Warning": "\"%s\" உடன் livechat ஐ நிச்சயமாக மறைக்க விரும்புகிறீர்களா?", - "Hide_Private_Warning": "நீங்கள் \"%s\" பதில் மூலம் விவாதம் மறைக்க விரும்பவில்லை நீங்கள் உறுதியாக இருக்கிறீர்களா?", + "Hide_Group_Warning": "குழு \"{{roomName}}\" பதில் மறைக்க வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", + "Hide_Livechat_Warning": "\"{{roomName}}\" உடன் livechat ஐ நிச்சயமாக மறைக்க விரும்புகிறீர்களா?", + "Hide_Private_Warning": "நீங்கள் \"{{roomName}}\" பதில் மூலம் விவாதம் மறைக்க விரும்பவில்லை நீங்கள் உறுதியாக இருக்கிறீர்களா?", "Hide_roles": "பாத்திரங்களை மறை", "Hide_room": "அறையை மறை", - "Hide_Room_Warning": "நீங்கள் அறையில் \"%s\" பதில் மறைக்க வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", + "Hide_Room_Warning": "நீங்கள் அறையில் \"{{roomName}}\" பதில் மறைக்க வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", "Hide_Unread_Room_Status": "படிக்காத அறையின் நிலையை மறை", "Hide_usernames": "பயனர் பெயர்கள் மறை", "Highlights": "ஹைலைட்ஸ்", @@ -1522,11 +1522,11 @@ "Lead_capture_email_regex": "கைப்பற்ற மின்னஞ்சல் regex ஐ முன்னணி", "Lead_capture_phone_regex": "கைப்பற்றும் தொலைபேசி regex முன்னணி", "Leave": "அறையை விட்டுச்செல்", - "Leave_Group_Warning": "குழு \"%s\" பதில் விட்டு வெளியேற வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", - "Leave_Livechat_Warning": "\"%s\" உடன் livechat ஐ விட்டு வைக்க விரும்புகிறீர்களா?", - "Leave_Private_Warning": "நீங்கள் \"%s\" பதில் மூலம் விவாதம் விட்டு வெளியேற வேண்டும் நீங்கள் உறுதியாக இருக்கிறீர்களா?", + "Leave_Group_Warning": "குழு \"{{roomName}}\" பதில் விட்டு வெளியேற வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", + "Leave_Livechat_Warning": "\"{{roomName}}\" உடன் livechat ஐ விட்டு வைக்க விரும்புகிறீர்களா?", + "Leave_Private_Warning": "நீங்கள் \"{{roomName}}\" பதில் மூலம் விவாதம் விட்டு வெளியேற வேண்டும் நீங்கள் உறுதியாக இருக்கிறீர்களா?", "Leave_room": "அறையை விட்டுச்செல்", - "Leave_Room_Warning": "நீங்கள் அறையில் \"%s\" பதில் விட்டு வெளியேற வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", + "Leave_Room_Warning": "நீங்கள் அறையில் \"{{roomName}}\" பதில் விட்டு வெளியேற வேண்டும் என்பதில் உறுதியாக இருக்கிறீர்களா?", "Leave_the_current_channel": "நடப்பு சேனலை விட்டு விடுங்கள்", "leave-c": "சேனல்களை விடு", "leave-p": "தனியார் குழுக்களை விடு", diff --git a/packages/i18n/src/locales/th-TH.i18n.json b/packages/i18n/src/locales/th-TH.i18n.json index 980f3ca8e5bac..5562ffb84d3fa 100644 --- a/packages/i18n/src/locales/th-TH.i18n.json +++ b/packages/i18n/src/locales/th-TH.i18n.json @@ -1213,12 +1213,12 @@ "Hide": "ซ่อนห้อง", "Hide_counter": "ซ่อนเคาน์เตอร์", "Hide_flextab": "ซ่อนแถบข้างขวาด้วยการคลิก", - "Hide_Group_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนกลุ่ม \"%s\"?", - "Hide_Livechat_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อน livechat ด้วย \"%s\"?", - "Hide_Private_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนการสนทนาด้วย \"%s\"?", + "Hide_Group_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนกลุ่ม \"{{roomName}}\"?", + "Hide_Livechat_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อน livechat ด้วย \"{{roomName}}\"?", + "Hide_Private_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนการสนทนาด้วย \"{{roomName}}\"?", "Hide_roles": "ซ่อนบทบาท", "Hide_room": "ซ่อนห้อง", - "Hide_Room_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนห้อง \"%s\"", + "Hide_Room_Warning": "คุณแน่ใจหรือไม่ว่าต้องการซ่อนห้อง \"{{roomName}}\"", "Hide_Unread_Room_Status": "ซ่อนสถานะห้องพักที่ยังไม่ได้อ่าน", "Hide_usernames": "ซ่อนชื่อผู้ใช้", "Highlights": "ไฮไลท์", @@ -1517,11 +1517,11 @@ "Lead_capture_email_regex": "จับ regex อีเมลสำหรับจับภาพผู้นำ", "Lead_capture_phone_regex": "จับ regex โทรศัพท์ที่เป็นผู้นำ", "Leave": "ออกจากห้อง", - "Leave_Group_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจากกลุ่ม \"%s\"?", - "Leave_Livechat_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจาก livechat ด้วย \"%s\"?", - "Leave_Private_Warning": "คุณแน่ใจหรือว่าต้องการออกจากการสนทนากับ \"%s\"?", + "Leave_Group_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจากกลุ่ม \"{{roomName}}\"?", + "Leave_Livechat_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจาก livechat ด้วย \"{{roomName}}\"?", + "Leave_Private_Warning": "คุณแน่ใจหรือว่าต้องการออกจากการสนทนากับ \"{{roomName}}\"?", "Leave_room": "ออกจากห้อง", - "Leave_Room_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจากห้อง \"%s\"?", + "Leave_Room_Warning": "คุณแน่ใจหรือไม่ว่าต้องการออกจากห้อง \"{{roomName}}\"?", "Leave_the_current_channel": "ออกจากช่องปัจจุบัน", "leave-c": "ออกจากช่อง", "leave-p": "ออกจากกลุ่มส่วนตัว", diff --git a/packages/i18n/src/locales/tr.i18n.json b/packages/i18n/src/locales/tr.i18n.json index 2a51e02ab4659..09deddb99da6a 100644 --- a/packages/i18n/src/locales/tr.i18n.json +++ b/packages/i18n/src/locales/tr.i18n.json @@ -1473,12 +1473,12 @@ "Hide": "Gizle", "Hide_counter": "Sayacı gizle", "Hide_flextab": "Sağ Kenar Çubuğu Tıklanarak Gizlensin", - "Hide_Group_Warning": "\"%s\" grubunu gizlemek istediğinize emin misiniz?", - "Hide_Livechat_Warning": "\"%s\" ile canlı çekimi gizlemek istediğinize emin misiniz?", - "Hide_Private_Warning": "\"%s\" ile tartışmayı gizlemek istediğinize emin misiniz?", + "Hide_Group_Warning": "\"{{roomName}}\" grubunu gizlemek istediğinize emin misiniz?", + "Hide_Livechat_Warning": "\"{{roomName}}\" ile canlı çekimi gizlemek istediğinize emin misiniz?", + "Hide_Private_Warning": "\"{{roomName}}\" ile tartışmayı gizlemek istediğinize emin misiniz?", "Hide_roles": "Roller Gizlensin", "Hide_room": "Gizle", - "Hide_Room_Warning": "Oda \"%s\" gizlemek istediğinizden emin misiniz?", + "Hide_Room_Warning": "Oda \"{{roomName}}\" gizlemek istediğinizden emin misiniz?", "Hide_Unread_Room_Status": "Okunmamış Oda Durumunu Gizle", "Hide_usernames": "Kullanıcı Adları Gizlensin", "Highlights": "Vurgular", @@ -1811,11 +1811,11 @@ "Lead_capture_email_regex": "Kurşun yakalama e-posta regex'i", "Lead_capture_phone_regex": "Telefon yakalama regex'ini yönet", "Leave": "Ayrıl", - "Leave_Group_Warning": "\"%s\" grubundan ayrılmak istediğinize emin misiniz?", - "Leave_Livechat_Warning": "Canlı görüşme \"%s\" ile terk etmek istediğinize emin misiniz?", - "Leave_Private_Warning": "\"%s\" ile tartışmadan ayrılmak istediğinize emin misiniz?", + "Leave_Group_Warning": "\"{{roomName}}\" grubundan ayrılmak istediğinize emin misiniz?", + "Leave_Livechat_Warning": "Canlı görüşme \"{{roomName}}\" ile terk etmek istediğinize emin misiniz?", + "Leave_Private_Warning": "\"{{roomName}}\" ile tartışmadan ayrılmak istediğinize emin misiniz?", "Leave_room": "Odadan ayrıl", - "Leave_Room_Warning": "Eğer oda \"%s\" ayrılmak istediğinize emin misiniz?", + "Leave_Room_Warning": "Eğer oda \"{{roomName}}\" ayrılmak istediğinize emin misiniz?", "Leave_the_current_channel": "Geçerli kanalı bırak", "leave-c": "Kanallardan Çık", "leave-p": "Özel Grupları Bırak", diff --git a/packages/i18n/src/locales/ug.i18n.json b/packages/i18n/src/locales/ug.i18n.json index 2dfd2cd60c55e..75983706ed311 100644 --- a/packages/i18n/src/locales/ug.i18n.json +++ b/packages/i18n/src/locales/ug.i18n.json @@ -474,10 +474,10 @@ "Header": "باش", "Hidden": "يوشۇرۇلغان", "Hide": "پاراڭلىشىش ئۆيىنى يوشۇرۇش", - "Hide_Group_Warning": "يوشۇرۇشنى جەزملەشتۈرەمسىز ؟“%s”سىز ئەزا گۇرۇپپسى", - "Hide_Private_Warning": "بىلەن بولغان مۇنازىرىنى يوشۇرۇشنى جەزملەشتۈرەمسىز ؟“%s”سىز", + "Hide_Group_Warning": "يوشۇرۇشنى جەزملەشتۈرەمسىز ؟“{{roomName}}”سىز ئەزا گۇرۇپپسى", + "Hide_Private_Warning": "بىلەن بولغان مۇنازىرىنى يوشۇرۇشنى جەزملەشتۈرەمسىز ؟“{{roomName}}”سىز", "Hide_room": "پاراڭلىشىش ئۆيىنى يوشۇرۇش", - "Hide_Room_Warning": "ئۆينى يوشۇرۇشنى جەزملەشتۈرەمسىز؟“%s”سىز", + "Hide_Room_Warning": "ئۆينى يوشۇرۇشنى جەزملەشتۈرەمسىز؟“{{roomName}}”سىز", "Hide_usernames": "ئەزا ئىسمىنى يوشۇرۇش", "Highlights": "يۇقىرى ئېنىقلىق", "Highlights_How_To": "باشقىلار يوللىغان ئۇچۇرنىڭ ئىچىدە بۇيەردىكى ئاچقۇچلۇق سۆز بولغاندا ، سىز ئەسكەرتىش قوبۇل قىلىسىز.پەش بەلگىسى ئارقىلىق كۆپلىگەن ئاچقۇچلۇق سۆزنى ئايرىڭ . چوڭ-كىچىك يېزىلىشى پەرقلەنمەيدۇ.", @@ -606,10 +606,10 @@ "LDAP_Username_Field": "ئەزا ئىسمى خەت بۆلىكى", "LDAP_Username_Field_Description": "`sAMAccountName`بەلگىلەنگەن خەت بۆلىكى بولسا \n `#{givenName}.#{sn}`خەتكۈچ ئۈلگىسى ئىشلەتسىڭىز بولىدۇ ، مەسلەن \n *ئەزا ئىسمى* قايسى خەت بۆلەك قىممىتىنى قىلىپ ئىشلىتىش ئەگەر كىرىش بېتىدىكى ئەزا نامىنى ئىشلەتكەن بولسا بوش ئورۇن قويۇڭ .LDAP يېڭى ئەزانىڭ كىرىشىنى بېكىتكەندە", "Leave": "پاراڭلىشىش ئۆيىدىن ئايرىلىش", - "Leave_Group_Warning": "ئەزا گۇرۇپپىسىدىن ئايرىلىشنى جەزملەشتۈردىڭىزمۇ ؟ “%s”سىز", - "Leave_Private_Warning": "بىلەن سۆھبەتلىشىشتىن ئايرىلىشنى جەزملەشتۈرەمسىز ؟“%s”سىز", + "Leave_Group_Warning": "ئەزا گۇرۇپپىسىدىن ئايرىلىشنى جەزملەشتۈردىڭىزمۇ ؟ “{{roomName}}”سىز", + "Leave_Private_Warning": "بىلەن سۆھبەتلىشىشتىن ئايرىلىشنى جەزملەشتۈرەمسىز ؟“{{roomName}}”سىز", "Leave_room": "پاراڭلىشىش ئۆيىدىن ئايرىلىش", - "Leave_Room_Warning": "ئۆيدىن ئايرىلىشنى جەزملەشتۈرەمسىز ؟“%s” سىز", + "Leave_Room_Warning": "ئۆيدىن ئايرىلىشنى جەزملەشتۈرەمسىز ؟“{{roomName}}” سىز", "List_of_Channels": "قانال تىزىملىكى", "List_of_Direct_Messages": "بىۋاستە سۆھبەتلىشىش تىزىملىكى", "Livechat_agents": "توردىكى مۇلازىم", diff --git a/packages/i18n/src/locales/uk.i18n.json b/packages/i18n/src/locales/uk.i18n.json index 87ad77563a4a2..13ee35b59623f 100644 --- a/packages/i18n/src/locales/uk.i18n.json +++ b/packages/i18n/src/locales/uk.i18n.json @@ -1365,10 +1365,10 @@ "Hi_username": "Привіт [name]", "Hidden": "Прихований", "Hide": "Сховати", - "Hide_Group_Warning": "Ви впевнені, що хочете приховати групу \"%s\"?", - "Hide_Livechat_Warning": "Ви впевнені, що хочете сховати livechat за допомогою \"%s\"?", - "Hide_Private_Warning": "Ви впевнені, що хочете приховати обговорення з \"%s\"?", - "Hide_Room_Warning": "Ви впевнені, що хочете приховати кімнату \"%s\"?", + "Hide_Group_Warning": "Ви впевнені, що хочете приховати групу \"{{roomName}}\"?", + "Hide_Livechat_Warning": "Ви впевнені, що хочете сховати livechat за допомогою \"{{roomName}}\"?", + "Hide_Private_Warning": "Ви впевнені, що хочете приховати обговорення з \"{{roomName}}\"?", + "Hide_Room_Warning": "Ви впевнені, що хочете приховати кімнату \"{{roomName}}\"?", "Hide_Unread_Room_Status": "Сховати статус непрочитаної кімнати", "Hide_counter": "Сховати лічильник", "Hide_flextab": "Сховати праву бічну панель за допомогою клацання", @@ -1722,10 +1722,10 @@ "Lead_capture_email_regex": "Провести захоплення регекса електронною поштою", "Lead_capture_phone_regex": "Провести захоплення телефону регулярним викликом", "Leave": "Залишити", - "Leave_Group_Warning": "Ви впевнені, що хочете залишити групу \"%s\"?", - "Leave_Livechat_Warning": "Ви впевнені, що хочете залишити livechat з \"%s\"?", - "Leave_Private_Warning": "Ви впевнені, що хочете залишити обговорення з \"%s\"?", - "Leave_Room_Warning": "Ви впевнені, що хочете вийти з кімнати \"%s\"?", + "Leave_Group_Warning": "Ви впевнені, що хочете залишити групу \"{{roomName}}\"?", + "Leave_Livechat_Warning": "Ви впевнені, що хочете залишити livechat з \"{{roomName}}\"?", + "Leave_Private_Warning": "Ви впевнені, що хочете залишити обговорення з \"{{roomName}}\"?", + "Leave_Room_Warning": "Ви впевнені, що хочете вийти з кімнати \"{{roomName}}\"?", "Leave_room": "Покинути кімнату", "Leave_the_current_channel": "Залишити поточний канал", "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "Залиште поле опису порожнім, якщо ви не хочете показувати роль", @@ -3344,4 +3344,4 @@ "you_are_in_preview_mode_of_incoming_livechat": "Ви знаходитесь в режимі попереднього перегляду цього вхідного livechat", "your_message": "Ваше повідомлення", "your_message_optional": "ваше повідомлення (необов'язково)" -} +} \ No newline at end of file diff --git a/packages/i18n/src/locales/vi-VN.i18n.json b/packages/i18n/src/locales/vi-VN.i18n.json index ba24f622fb38a..52bf3b857f120 100644 --- a/packages/i18n/src/locales/vi-VN.i18n.json +++ b/packages/i18n/src/locales/vi-VN.i18n.json @@ -1313,12 +1313,12 @@ "Hide": "Ẩn phòng", "Hide_counter": "Ẩn bộ đếm", "Hide_flextab": "Ẩn Thanh bên Phải với Nhấp", - "Hide_Group_Warning": "Bạn có chắc chắn muốn ẩn nhóm \"%s\" không?", - "Hide_Livechat_Warning": "Bạn có chắc chắn muốn ẩn livechat với \"%s\" không?", - "Hide_Private_Warning": "Bạn có chắc chắn muốn ẩn thảo luận với \"%s\" không?", + "Hide_Group_Warning": "Bạn có chắc chắn muốn ẩn nhóm \"{{roomName}}\" không?", + "Hide_Livechat_Warning": "Bạn có chắc chắn muốn ẩn livechat với \"{{roomName}}\" không?", + "Hide_Private_Warning": "Bạn có chắc chắn muốn ẩn thảo luận với \"{{roomName}}\" không?", "Hide_roles": "Ẩn vai trò", "Hide_room": "Ẩn phòng", - "Hide_Room_Warning": "Bạn có chắc chắn muốn ẩn phòng \"%s\" không?", + "Hide_Room_Warning": "Bạn có chắc chắn muốn ẩn phòng \"{{roomName}}\" không?", "Hide_Unread_Room_Status": "Ẩn trạng thái phòng không đọc", "Hide_usernames": "Ẩn Tên người dùng", "Highlights": "Điểm nổi bật", @@ -1616,11 +1616,11 @@ "Lead_capture_email_regex": "Lead capture email regex", "Lead_capture_phone_regex": "Lead capture phone regex", "Leave": "Rời khỏi phòng", - "Leave_Group_Warning": "Bạn có chắc chắn muốn thoát khỏi nhóm \"%s\" không?", - "Leave_Livechat_Warning": "Bạn có chắc chắn muốn thoát khỏi livechat với \"%s\" không?", - "Leave_Private_Warning": "Bạn có chắc chắn muốn rời khỏi cuộc thảo luận với \"%s\" không?", + "Leave_Group_Warning": "Bạn có chắc chắn muốn thoát khỏi nhóm \"{{roomName}}\" không?", + "Leave_Livechat_Warning": "Bạn có chắc chắn muốn thoát khỏi livechat với \"{{roomName}}\" không?", + "Leave_Private_Warning": "Bạn có chắc chắn muốn rời khỏi cuộc thảo luận với \"{{roomName}}\" không?", "Leave_room": "Rời khỏi phòng", - "Leave_Room_Warning": "Bạn có chắc chắn muốn rời khỏi phòng \"%s\" không?", + "Leave_Room_Warning": "Bạn có chắc chắn muốn rời khỏi phòng \"{{roomName}}\" không?", "Leave_the_current_channel": "Rời khỏi kênh hiện tại", "leave-c": "Rời khỏi kênh", "leave-p": "Rời khỏi Nhóm Riêng tư", diff --git a/packages/i18n/src/locales/zh-TW.i18n.json b/packages/i18n/src/locales/zh-TW.i18n.json index 5ae51259c38ad..0c6715e09a320 100644 --- a/packages/i18n/src/locales/zh-TW.i18n.json +++ b/packages/i18n/src/locales/zh-TW.i18n.json @@ -1933,12 +1933,12 @@ "Hide": "隱藏", "Hide_counter": "隱藏計數器", "Hide_flextab": "點擊右鍵隱藏側邊欄", - "Hide_Group_Warning": "您確定要隱藏群組 “%s” 嗎?", - "Hide_Livechat_Warning": "您確定要隱藏 “%s” 的即時聊天嗎?", - "Hide_Private_Warning": "您確定您要隱藏用 “%s” 的討論?", + "Hide_Group_Warning": "您確定要隱藏群組 “{{roomName}}” 嗎?", + "Hide_Livechat_Warning": "您確定要隱藏 “{{roomName}}” 的即時聊天嗎?", + "Hide_Private_Warning": "您確定您要隱藏用 “{{roomName}}” 的討論?", "Hide_roles": "隱藏角色", "Hide_room": "隱藏", - "Hide_Room_Warning": "您確定您要隱藏的房間 “%s” 嗎?", + "Hide_Room_Warning": "您確定您要隱藏的房間 “{{roomName}}” 嗎?", "Hide_Unread_Room_Status": "隱藏未讀房間狀態", "Hide_usernames": "隱藏使用者名稱", "Highlights": "強調", @@ -2392,11 +2392,11 @@ "Lead_capture_email_regex": "最先抓取電子郵件正規表示法", "Lead_capture_phone_regex": "最先抓取手機號碼正規表示法", "Leave": "離開", - "Leave_Group_Warning": "你確定你要離開組 “%s” 嗎?", - "Leave_Livechat_Warning": "你確定要離開 “%s” 的即時聊天嗎?", - "Leave_Private_Warning": "你確定要離開 “%s” 的討論?", + "Leave_Group_Warning": "你確定你要離開組 “{{roomName}}” 嗎?", + "Leave_Livechat_Warning": "你確定要離開 “{{roomName}}” 的即時聊天嗎?", + "Leave_Private_Warning": "你確定要離開 “{{roomName}}” 的討論?", "Leave_room": "離開", - "Leave_Room_Warning": "您確定要離開頻道 “%s” 嗎?", + "Leave_Room_Warning": "您確定要離開頻道 “{{roomName}}” 嗎?", "Leave_the_current_channel": "離開目前頻道", "leave-c": "保留 Channel", "Instance": "實例", diff --git a/packages/i18n/src/locales/zh.i18n.json b/packages/i18n/src/locales/zh.i18n.json index 4e2ab8cd3480d..ebffcf0228b7d 100644 --- a/packages/i18n/src/locales/zh.i18n.json +++ b/packages/i18n/src/locales/zh.i18n.json @@ -1773,12 +1773,12 @@ "Hide": "隐藏", "Hide_counter": "隐藏计数器", "Hide_flextab": "通过点击隐藏右边栏", - "Hide_Group_Warning": "您确定要隐藏用户组 “%s” 吗?", - "Hide_Livechat_Warning": "您确定要隐藏与 “%s” 的聊天吗?", - "Hide_Private_Warning": "您确定要隐藏与 “%s” 的讨论吗?", + "Hide_Group_Warning": "您确定要隐藏用户组 “{{roomName}}” 吗?", + "Hide_Livechat_Warning": "您确定要隐藏与 “{{roomName}}” 的聊天吗?", + "Hide_Private_Warning": "您确定要隐藏与 “{{roomName}}” 的讨论吗?", "Hide_roles": "隐藏角色", "Hide_room": "隐藏", - "Hide_Room_Warning": "您确定要隐藏频道 “%s” 吗?", + "Hide_Room_Warning": "您确定要隐藏频道 “{{roomName}}” 吗?", "Hide_Unread_Room_Status": "隐藏未读聊天室状态", "Hide_usernames": "隐藏用户名", "Highlights": "高亮", @@ -2195,11 +2195,11 @@ "Lead_capture_email_regex": "领导捕获电子邮件正则表达式", "Lead_capture_phone_regex": "领导捕获手机正则表达式", "Leave": "离开", - "Leave_Group_Warning": "您确定要离开用户组 “%s” 吗?", - "Leave_Livechat_Warning": "你确定要离开和 “%s” 的 omnichannel 吗?", - "Leave_Private_Warning": "您确定要离开和 “%s” 的讨论吗?", + "Leave_Group_Warning": "您确定要离开用户组 “{{roomName}}” 吗?", + "Leave_Livechat_Warning": "你确定要离开和 “{{roomName}}” 的 omnichannel 吗?", + "Leave_Private_Warning": "您确定要离开和 “{{roomName}}” 的讨论吗?", "Leave_room": "离开", - "Leave_Room_Warning": "您确定要离开聊天室 “%s” 吗?", + "Leave_Room_Warning": "您确定要离开聊天室 “{{roomName}}” 吗?", "Leave_the_current_channel": "离开当前频道", "leave-c": "保留频道", "Instance": "实例", From 05c567d1359b724a52e834126f72c8b52fe2ba74 Mon Sep 17 00:00:00 2001 From: Abhinav Kumar Date: Fri, 21 Mar 2025 21:38:50 +0530 Subject: [PATCH 019/187] fix: CodeSettingInput renders duplicate code text box (#35273) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .changeset/honest-toys-guess.md | 5 + .../Setting/inputs/CodeMirror/CodeMirror.tsx | 130 ++++++++---------- .../tests/e2e/administration-settings.spec.ts | 26 +++- apps/meteor/tests/e2e/page-objects/admin.ts | 4 + 4 files changed, 92 insertions(+), 73 deletions(-) create mode 100644 .changeset/honest-toys-guess.md diff --git a/.changeset/honest-toys-guess.md b/.changeset/honest-toys-guess.md new file mode 100644 index 0000000000000..67fae4cb61c43 --- /dev/null +++ b/.changeset/honest-toys-guess.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where the code input type in settings renders duplicate code text boxes. diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirror.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirror.tsx index 1f249c7373c64..bef8372819197 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirror.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeMirror/CodeMirror.tsx @@ -1,7 +1,7 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import type { Editor, EditorFromTextArea } from 'codemirror'; import type { ReactElement } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; const defaultGutters = ['CodeMirror-linenumbers', 'CodeMirror-foldgutter']; @@ -44,77 +44,69 @@ function CodeMirror({ ...props }: CodeMirrorProps): ReactElement { const [value, setValue] = useState(valueProp || defaultValue); - - const textAreaRef = useRef(null); - const editorRef = useRef(null); const handleChange = useEffectEvent(onChange); - useEffect(() => { - if (editorRef.current) { - return; - } - - const setupCodeMirror = async (): Promise => { - const { default: CodeMirror } = await import('codemirror'); - await Promise.all([ - import('../../../../../../../app/ui/client/lib/codeMirror/codeMirror'), - import('codemirror/addon/edit/matchbrackets'), - import('codemirror/addon/edit/closebrackets'), - import('codemirror/addon/edit/matchtags'), - import('codemirror/addon/edit/trailingspace'), - import('codemirror/addon/search/match-highlighter'), - import('codemirror/lib/codemirror.css'), - ]); - - if (!textAreaRef.current) { - return; - } - - editorRef.current = CodeMirror.fromTextArea(textAreaRef.current, { - lineNumbers, - lineWrapping, - mode, - gutters, - foldGutter, - matchBrackets, - autoCloseBrackets, - matchTags, - showTrailingSpace, - highlightSelectionMatches, - readOnly, - }); - - editorRef.current.on('change', (doc: Editor) => { - const value = doc.getValue(); - setValue(value); - handleChange(value); - }); - }; - - setupCodeMirror(); - - return (): void => { - if (!editorRef.current) { - return; + const editorRef = useRef(null); + const textAreaRef = useCallback( + async (node: HTMLTextAreaElement | null) => { + if (!node) return; + + try { + const { default: CodeMirror } = await import('codemirror'); + await Promise.all([ + import('../../../../../../../app/ui/client/lib/codeMirror/codeMirror'), + import('codemirror/addon/edit/matchbrackets'), + import('codemirror/addon/edit/closebrackets'), + import('codemirror/addon/edit/matchtags'), + import('codemirror/addon/edit/trailingspace'), + import('codemirror/addon/search/match-highlighter'), + import('codemirror/lib/codemirror.css'), + ]); + + editorRef.current = CodeMirror.fromTextArea(node, { + lineNumbers, + lineWrapping, + mode, + gutters, + foldGutter, + matchBrackets, + autoCloseBrackets, + matchTags, + showTrailingSpace, + highlightSelectionMatches, + readOnly, + }); + + editorRef.current.on('change', (doc: Editor) => { + const newValue = doc.getValue(); + setValue(newValue); + handleChange(newValue); + }); + + return () => { + if (node.parentNode) { + editorRef.current?.toTextArea(); + } + }; + } catch (error) { + console.error('CodeMirror initialization failed:', error); } - - editorRef.current.toTextArea(); - }; - }, [ - autoCloseBrackets, - foldGutter, - gutters, - highlightSelectionMatches, - lineNumbers, - lineWrapping, - matchBrackets, - matchTags, - mode, - handleChange, - readOnly, - textAreaRef, - showTrailingSpace, - ]); + }, + [ + autoCloseBrackets, + foldGutter, + gutters, + highlightSelectionMatches, + lineNumbers, + lineWrapping, + matchBrackets, + matchTags, + mode, + handleChange, + readOnly, + showTrailingSpace, + ], + ); useEffect(() => { setValue(valueProp); diff --git a/apps/meteor/tests/e2e/administration-settings.spec.ts b/apps/meteor/tests/e2e/administration-settings.spec.ts index d2996d6eac886..24ffa09d34c1c 100644 --- a/apps/meteor/tests/e2e/administration-settings.spec.ts +++ b/apps/meteor/tests/e2e/administration-settings.spec.ts @@ -1,6 +1,6 @@ import { Users } from './fixtures/userStates'; import { Admin } from './page-objects'; -import { getSettingValueById } from './utils'; +import { getSettingValueById, setSettingValueById } from './utils'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); @@ -42,11 +42,29 @@ test.describe.parallel('administration-settings', () => { await page.goto('/admin/settings/Layout'); }); - test('should code mirror full screen be displayed correctly', async ({ page }) => { + test.afterAll(async ({ api }) => setSettingValueById(api, 'theme-custom-css', '')); + + test('should display the code mirror correctly', async ({ page, api }) => { await poAdmin.getAccordionBtnByName('Custom CSS').click(); - await poAdmin.btnFullScreen.click(); - await expect(page.getByRole('code')).toHaveCSS('width', '920px'); + await test.step('should render only one code mirror element', async () => { + const codeMirrorParent = page.getByRole('code'); + await expect(codeMirrorParent.locator('.CodeMirror')).toHaveCount(1); + }); + + await test.step('should display full screen properly', async () => { + await poAdmin.btnFullScreen.click(); + await expect(page.getByRole('code')).toHaveCSS('width', '920px'); + await poAdmin.btnExitFullScreen.click(); + }); + + await test.step('should reflect updated value when valueProp changes after server update', async () => { + const codeValue = `.test-class-${Date.now()} { background-color: red; }`; + await setSettingValueById(api, 'theme-custom-css', codeValue); + + const codeMirrorParent = page.getByRole('code'); + await expect(codeMirrorParent.locator('.CodeMirror-line')).toHaveText(codeValue); + }); }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/admin.ts b/apps/meteor/tests/e2e/page-objects/admin.ts index 49f86a59b6f9b..8c26f47a773b5 100644 --- a/apps/meteor/tests/e2e/page-objects/admin.ts +++ b/apps/meteor/tests/e2e/page-objects/admin.ts @@ -291,6 +291,10 @@ export class Admin { return this.page.getByRole('button', { name: 'Full Screen', exact: true }); } + get btnExitFullScreen(): Locator { + return this.page.getByRole('button', { name: 'Exit Full Screen', exact: true }); + } + async dropdownFilterRoomType(text = 'All rooms'): Promise { return this.page.locator(`div[role="button"]:has-text("${text}")`); } From 1bc8738244c8cf23fe06fe08d94016a2e8264eb9 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Fri, 21 Mar 2025 22:38:40 +0530 Subject: [PATCH 020/187] feat(federation-v2): allow federated user deletion if the user doesn't exist remotely (#35464) --- .changeset/strong-shoes-end.md | 5 ++++ .../app/lib/server/functions/deleteUser.ts | 27 ++++++++++++------- 2 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 .changeset/strong-shoes-end.md diff --git a/.changeset/strong-shoes-end.md b/.changeset/strong-shoes-end.md new file mode 100644 index 0000000000000..b0996aaa8a4da --- /dev/null +++ b/.changeset/strong-shoes-end.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Allows deleting federated remote users in case they are not present in the homeserver. diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index fbdb3215cf00d..17b2dcb340b03 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -1,5 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import { api } from '@rocket.chat/core-services'; +import { api, Federation, FederationEE, License } from '@rocket.chat/core-services'; import { isUserFederated, type IUser } from '@rocket.chat/core-typings'; import { Integrations, @@ -23,6 +23,7 @@ import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; import { updateGroupDMsName } from './updateGroupDMsName'; import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; +import { VerificationStatus } from '../../../../server/services/federation/infrastructure/matrix/helpers/MatrixIdVerificationTypes'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; import { @@ -49,16 +50,22 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele } if (isUserFederated(user)) { - throw new Meteor.Error('error-not-allowed', 'Deleting federated, external user is not allowed', { - method: 'deleteUser', - }); - } + const service = (await License.hasValidLicense()) ? FederationEE : Federation; - const remoteUser = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId); - if (remoteUser) { - throw new Meteor.Error('error-not-allowed', 'User participated in federation, this user can only be deactivated permanently', { - method: 'deleteUser', - }); + const result = await service.verifyMatrixIds([user.username as string]); + + if (result.get(user.username as string) === VerificationStatus.VERIFIED) { + throw new Meteor.Error('error-not-allowed', 'Deleting federated, external user is not allowed', { + method: 'deleteUser', + }); + } + } else { + const remoteUser = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId); + if (remoteUser) { + throw new Meteor.Error('error-not-allowed', 'User participated in federation, this user can only be deactivated permanently', { + method: 'deleteUser', + }); + } } const subscribedRooms = await getSubscribedRoomsForUserWithDetails(userId); From 5cc6b21eda1e31a7f1affb8e12edc6c944aa36ec Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 21 Mar 2025 18:03:25 -0300 Subject: [PATCH 021/187] chore: Move `UserPresenceContext` to `ui-contexts` (#35582) --- apps/meteor/app/otr/client/OTRRoom.ts | 3 +- .../UserStatus/ReactiveUserStatus.tsx | 5 +- .../components/message/MessageHeader.tsx | 5 +- .../message/variants/SystemMessage.tsx | 5 +- .../variants/room/RoomMessageContent.tsx | 6 +- .../variants/thread/ThreadMessageContent.tsx | 6 +- .../client/contexts/UserPresenceContext.ts | 10 --- apps/meteor/client/hooks/usePresence.ts | 28 -------- apps/meteor/client/lib/chats/ChatAPI.ts | 3 +- apps/meteor/client/lib/presence.ts | 6 +- .../client/providers/UserPresenceProvider.tsx | 50 ++++++------- .../DeviceManagementInfo.tsx | 5 +- .../views/room/Header/DirectRoomHeader.tsx | 5 +- .../client/views/room/body/RoomTopic.tsx | 5 +- .../room/contextualBar/OTR/OTRWithData.tsx | 4 +- packages/core-typings/src/IUser.ts | 4 ++ .../core-typings/src}/Subscribable.ts | 0 packages/core-typings/src/index.ts | 1 + packages/mock-providers/jest.config.ts | 1 + .../src/MockedAppRootBuilder.tsx | 72 +++++++++++-------- .../src/tests/useSetting.spec.tsx | 2 +- .../src/tests/useSettings.spec.tsx | 2 +- .../src/tests/useUserPresence.spec.tsx | 43 +++++++++++ .../ui-contexts/src/UserPresenceContext.ts | 10 +++ .../ui-contexts/src/hooks/useUserPresence.ts | 10 +-- packages/ui-contexts/src/index.ts | 2 + 26 files changed, 158 insertions(+), 135 deletions(-) delete mode 100644 apps/meteor/client/contexts/UserPresenceContext.ts delete mode 100644 apps/meteor/client/hooks/usePresence.ts rename {apps/meteor/client/definitions => packages/core-typings/src}/Subscribable.ts (100%) create mode 100644 packages/mock-providers/src/tests/useUserPresence.spec.tsx create mode 100644 packages/ui-contexts/src/UserPresenceContext.ts rename apps/meteor/client/hooks/useUserData.ts => packages/ui-contexts/src/hooks/useUserPresence.ts (55%) diff --git a/apps/meteor/app/otr/client/OTRRoom.ts b/apps/meteor/app/otr/client/OTRRoom.ts index 8291ec840f814..83485a7442cfc 100644 --- a/apps/meteor/app/otr/client/OTRRoom.ts +++ b/apps/meteor/app/otr/client/OTRRoom.ts @@ -1,4 +1,4 @@ -import type { IRoom, IMessage, IUser } from '@rocket.chat/core-typings'; +import type { IRoom, IMessage, IUser, UserPresence } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import EJSON from 'ejson'; @@ -8,7 +8,6 @@ import { Tracker } from 'meteor/tracker'; import GenericModal from '../../../client/components/GenericModal'; import { imperativeModal } from '../../../client/lib/imperativeModal'; -import type { UserPresence } from '../../../client/lib/presence'; import { Presence } from '../../../client/lib/presence'; import { dispatchToastMessage } from '../../../client/lib/toast'; import { getUidDirectMessage } from '../../../client/lib/utils/getUidDirectMessage'; diff --git a/apps/meteor/client/components/UserStatus/ReactiveUserStatus.tsx b/apps/meteor/client/components/UserStatus/ReactiveUserStatus.tsx index caff7b8d08e09..51b683b608e4d 100644 --- a/apps/meteor/client/components/UserStatus/ReactiveUserStatus.tsx +++ b/apps/meteor/client/components/UserStatus/ReactiveUserStatus.tsx @@ -1,16 +1,15 @@ import type { IUser } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/ui-client'; +import { useUserPresence } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import { memo } from 'react'; -import { usePresence } from '../../hooks/usePresence'; - type ReactiveUserStatusProps = { uid: IUser['_id']; } & ComponentProps; const ReactiveUserStatus = ({ uid, ...props }: ReactiveUserStatusProps): ReactElement => { - const status = usePresence(uid)?.status; + const status = useUserPresence(uid)?.status; return ; }; diff --git a/apps/meteor/client/components/message/MessageHeader.tsx b/apps/meteor/client/components/message/MessageHeader.tsx index 8a259a8817865..61ba13171d7d2 100644 --- a/apps/meteor/client/components/message/MessageHeader.tsx +++ b/apps/meteor/client/components/message/MessageHeader.tsx @@ -8,6 +8,7 @@ import { MessageNameContainer, } from '@rocket.chat/fuselage'; import { useUserDisplayName } from '@rocket.chat/ui-client'; +import { useUserPresence } from '@rocket.chat/ui-contexts'; import type { KeyboardEvent, ReactElement } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -17,8 +18,6 @@ import MessageRoles from './header/MessageRoles'; import { useMessageListShowUsername, useMessageListShowRealName, useMessageListShowRoles } from './list/MessageListContext'; import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime'; import { useFormatTime } from '../../hooks/useFormatTime'; -import { useUserData } from '../../hooks/useUserData'; -import type { UserPresence } from '../../lib/presence'; import { useMessageRoles } from './header/hooks/useMessageRoles'; import { useUserCard } from '../../views/room/contexts/UserCardContext'; @@ -34,7 +33,7 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { const { triggerProps, openUserCard } = useUserCard(); const showRealName = useMessageListShowRealName(); - const user: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; + const user = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; const usernameAndRealNameAreSame = !user.name || user.username === user.name; const showUsername = useMessageListShowUsername() && showRealName && !usernameAndRealNameAreSame; const displayName = useUserDisplayName(user); diff --git a/apps/meteor/client/components/message/variants/SystemMessage.tsx b/apps/meteor/client/components/message/variants/SystemMessage.tsx index 34e8b48ee2756..0d3de880908ea 100644 --- a/apps/meteor/client/components/message/variants/SystemMessage.tsx +++ b/apps/meteor/client/components/message/variants/SystemMessage.tsx @@ -14,6 +14,7 @@ import { import { UserAvatar } from '@rocket.chat/ui-avatar'; import { useUserDisplayName } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useUserPresence } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement, KeyboardEvent } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -21,8 +22,6 @@ import { useTranslation } from 'react-i18next'; import { MessageTypes } from '../../../../app/ui-utils/client'; import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; import { useFormatTime } from '../../../hooks/useFormatTime'; -import { useUserData } from '../../../hooks/useUserData'; -import type { UserPresence } from '../../../lib/presence'; import { useIsSelecting, useToggleSelect, @@ -46,7 +45,7 @@ const SystemMessage = ({ message, showUserAvatar, ...props }: SystemMessageProps const { triggerProps, openUserCard } = useUserCard(); const showRealName = useMessageListShowRealName(); - const user: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; + const user = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; const usernameAndRealNameAreSame = !user.name || user.username === user.name; const showUsername = useMessageListShowUsername() && showRealName && !usernameAndRealNameAreSame; const displayName = useUserDisplayName(user); diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index 428c212a8935d..eaf54b7260222 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -2,12 +2,10 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage, isQuoteAttachment } from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useSetting, useTranslation, useUserId } from '@rocket.chat/ui-contexts'; +import { useSetting, useTranslation, useUserId, useUserPresence } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { memo } from 'react'; -import { useUserData } from '../../../../hooks/useUserData'; -import type { UserPresence } from '../../../../lib/presence'; import { useChat } from '../../../../views/room/contexts/ChatContext'; import MessageContentBody from '../../MessageContentBody'; import ReadReceiptIndicator from '../../ReadReceiptIndicator'; @@ -38,7 +36,7 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const subscription = useSubscriptionFromMessageQuery(message).data ?? undefined; const broadcast = subscription?.broadcast ?? false; const uid = useUserId(); - const messageUser: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; + const messageUser = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; const readReceiptEnabled = useSetting('Message_Read_Receipt_Enabled', false); const chat = useChat(); const t = useTranslation(); diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index 643c85e0c518c..a35d70075d67b 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -2,13 +2,11 @@ import type { IThreadMainMessage, IThreadMessage } from '@rocket.chat/core-typin import { isE2EEMessage, isQuoteAttachment } from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useSetting, useUserId } from '@rocket.chat/ui-contexts'; +import { useSetting, useUserId, useUserPresence } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useUserData } from '../../../../hooks/useUserData'; -import type { UserPresence } from '../../../../lib/presence'; import MessageContentBody from '../../MessageContentBody'; import ReadReceiptIndicator from '../../ReadReceiptIndicator'; import Attachments from '../../content/Attachments'; @@ -32,7 +30,7 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem const subscription = useSubscriptionFromMessageQuery(message).data ?? undefined; const broadcast = subscription?.broadcast ?? false; const uid = useUserId(); - const messageUser: UserPresence = { ...message.u, roles: [], ...useUserData(message.u._id) }; + const messageUser = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; const readReceiptEnabled = useSetting('Message_Read_Receipt_Enabled', false); const { t } = useTranslation(); diff --git a/apps/meteor/client/contexts/UserPresenceContext.ts b/apps/meteor/client/contexts/UserPresenceContext.ts deleted file mode 100644 index 4cb45350b0d8b..0000000000000 --- a/apps/meteor/client/contexts/UserPresenceContext.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createContext } from 'react'; - -import type { Subscribable } from '../definitions/Subscribable'; -import type { UserPresence } from '../lib/presence'; - -type UserPresenceContextValue = { - queryUserData: (uid: string) => Subscribable; -}; - -export const UserPresenceContext = createContext(undefined); diff --git a/apps/meteor/client/hooks/usePresence.ts b/apps/meteor/client/hooks/usePresence.ts deleted file mode 100644 index 555b3b4c8a9d2..0000000000000 --- a/apps/meteor/client/hooks/usePresence.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useCallback, useSyncExternalStore } from 'react'; - -import type { UserPresence } from '../lib/presence'; -import { Presence } from '../lib/presence'; - -/** - * @deprecated - * Hook to fetch and subscribe users presence - * - * @param uid - User Id - * @returns UserPresence - * @public - */ -export const usePresence = (uid: string | undefined): UserPresence | undefined => { - const subscribe = useCallback( - (callback: any): any => { - uid && Presence.listen(uid, callback); - return (): void => { - uid && Presence.stop(uid, callback); - }; - }, - [uid], - ); - - const getSnapshot = (): UserPresence | undefined => (uid ? Presence.store.get(uid) : undefined); - - return useSyncExternalStore(subscribe, getSnapshot); -}; diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index dbdaa1b04ac7b..058870cfbd286 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -1,10 +1,9 @@ -import type { IMessage, IRoom, ISubscription, IE2EEMessage, IUpload } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, ISubscription, IE2EEMessage, IUpload, Subscribable } from '@rocket.chat/core-typings'; import type { IActionManager } from '@rocket.chat/ui-contexts'; import type { Upload } from './Upload'; import type { ReadStateManager } from './readStateManager'; import type { FormattingButton } from '../../../app/ui-message/client/messageBox/messageBoxFormatting'; -import type { Subscribable } from '../../definitions/Subscribable'; export type ComposerAPI = { release(): void; diff --git a/apps/meteor/client/lib/presence.ts b/apps/meteor/client/lib/presence.ts index dbaddcbe405b5..ebd42bc41abd9 100644 --- a/apps/meteor/client/lib/presence.ts +++ b/apps/meteor/client/lib/presence.ts @@ -1,4 +1,4 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import type { IUser, UserPresence } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import type { EventHandlerOf } from '@rocket.chat/emitter'; import { Emitter } from '@rocket.chat/emitter'; @@ -22,10 +22,6 @@ const emitter = new Emitter(); const store = new Map(); -export type UserPresence = Readonly< - Partial> & Required> ->; - const isUid = (eventType: keyof Events): eventType is UserPresence['_id'] => Boolean(eventType) && typeof eventType === 'string' && !['reset', 'restart', 'remove'].includes(eventType); diff --git a/apps/meteor/client/providers/UserPresenceProvider.tsx b/apps/meteor/client/providers/UserPresenceProvider.tsx index 04274c5cdbe0b..6936b106e38fc 100644 --- a/apps/meteor/client/providers/UserPresenceProvider.tsx +++ b/apps/meteor/client/providers/UserPresenceProvider.tsx @@ -1,8 +1,8 @@ -import { useSetting } from '@rocket.chat/ui-contexts'; +import type { UserPresenceContextValue } from '@rocket.chat/ui-contexts'; +import { useSetting, UserPresenceContext } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import { useMemo, useEffect } from 'react'; -import { UserPresenceContext } from '../contexts/UserPresenceContext'; import { Presence } from '../lib/presence'; type UserPresenceProviderProps = { @@ -16,30 +16,30 @@ const UserPresenceProvider = ({ children }: UserPresenceProviderProps): ReactEle Presence.setStatus(usePresenceDisabled ? 'disabled' : 'enabled'); }, [usePresenceDisabled]); - return ( - ({ - queryUserData: (uid) => { - const subscribe = (callback: () => void) => { - Presence.listen(uid, callback); - - return () => { - Presence.stop(uid, callback); - }; - }; - - const get = () => Presence.store.get(uid); - - return { subscribe, get }; - }, - }), - [], - )} - > - {children} - + const contextValue: UserPresenceContextValue = useMemo( + () => ({ + queryUserData: (uid) => { + if (!uid) { + return { get: () => undefined, subscribe: () => () => undefined }; + } + + const subscribe = (callback: () => void) => { + Presence.listen(uid, callback); + + return () => { + Presence.stop(uid, callback); + }; + }; + + const get = () => Presence.store.get(uid); + + return { subscribe, get }; + }, + }), + [], ); + + return {children}; }; export default UserPresenceProvider; diff --git a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx index 83ca73a8833e7..a9a9a0d7ab8a7 100644 --- a/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx +++ b/apps/meteor/client/views/admin/deviceManagement/DeviceManagementInfo/DeviceManagementInfo.tsx @@ -1,7 +1,7 @@ import type { DeviceManagementPopulatedSession } from '@rocket.chat/core-typings'; import { Box, Button, ButtonGroup, StatusBullet } from '@rocket.chat/fuselage'; import { UserAvatar } from '@rocket.chat/ui-avatar'; -import { useRoute } from '@rocket.chat/ui-contexts'; +import { useRoute, useUserPresence } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -17,7 +17,6 @@ import { import { InfoPanel, InfoPanelField, InfoPanelLabel, InfoPanelText } from '../../../../components/InfoPanel'; import { useDeviceLogout } from '../../../../hooks/useDeviceLogout'; import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; -import { usePresence } from '../../../../hooks/usePresence'; type DeviceManagementInfoProps = DeviceManagementPopulatedSession & { onReload: () => void; @@ -32,7 +31,7 @@ const DeviceManagementInfo = ({ device, sessionId, loginAt, ip, userId, _user, o const { name: clientName, os, version: rcVersion } = device || {}; const { username, name } = _user || {}; - const userPresence = usePresence(userId); + const userPresence = useUserPresence(userId); const handleCloseContextualBar = useCallback((): void => deviceManagementRouter.push({}), [deviceManagementRouter]); diff --git a/apps/meteor/client/views/room/Header/DirectRoomHeader.tsx b/apps/meteor/client/views/room/Header/DirectRoomHeader.tsx index 457626a7f91cc..359f566afc823 100644 --- a/apps/meteor/client/views/room/Header/DirectRoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/DirectRoomHeader.tsx @@ -1,9 +1,8 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { useUserId } from '@rocket.chat/ui-contexts'; +import { useUserId, useUserPresence } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import RoomHeader from './RoomHeader'; -import { usePresence } from '../../../hooks/usePresence'; type DirectRoomHeaderProps = { room: IRoom; @@ -24,7 +23,7 @@ type DirectRoomHeaderProps = { const DirectRoomHeader = ({ room, slots }: DirectRoomHeaderProps): ReactElement => { const userId = useUserId(); const directUserId = room.uids?.filter((uid) => uid !== userId).shift(); - const directUserData = usePresence(directUserId); + const directUserData = useUserPresence(directUserId); return ; }; diff --git a/apps/meteor/client/views/room/body/RoomTopic.tsx b/apps/meteor/client/views/room/body/RoomTopic.tsx index 4bb414e3d9fbc..98d605eb7944b 100644 --- a/apps/meteor/client/views/room/body/RoomTopic.tsx +++ b/apps/meteor/client/views/room/body/RoomTopic.tsx @@ -2,10 +2,9 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { isTeamRoom } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { RoomBanner, RoomBannerContent } from '@rocket.chat/ui-client'; -import { useUserId, useTranslation, useRouter } from '@rocket.chat/ui-contexts'; +import { useUserId, useTranslation, useRouter, useUserPresence } from '@rocket.chat/ui-contexts'; import MarkdownText from '../../../components/MarkdownText'; -import { usePresence } from '../../../hooks/usePresence'; import { useCanEditRoom } from '../contextualBar/Info/hooks/useCanEditRoom'; type RoomTopicProps = { @@ -18,7 +17,7 @@ export const RoomTopic = ({ room }: RoomTopicProps) => { const canEdit = useCanEditRoom(room); const userId = useUserId(); const directUserId = room.uids?.filter((uid) => uid !== userId).shift(); - const directUserData = usePresence(directUserId); + const directUserData = useUserPresence(directUserId); const router = useRouter(); const currentRoute = router.getLocationPathname(); diff --git a/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx b/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx index 0fbae5ad6bffb..270483ec5e4f0 100644 --- a/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/OTR/OTRWithData.tsx @@ -1,17 +1,17 @@ +import { useUserPresence } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { useEffect } from 'react'; import OTRComponent from './OTR'; import { OtrRoomState } from '../../../../../app/otr/lib/OtrRoomState'; import { useOTR } from '../../../../hooks/useOTR'; -import { usePresence } from '../../../../hooks/usePresence'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; const OTRWithData = (): ReactElement => { const { otr, otrState } = useOTR(); const { closeTab } = useRoomToolbox(); - const peerUserPresence = usePresence(otr?.getPeerId()); + const peerUserPresence = useUserPresence(otr?.getPeerId()); const userStatus = peerUserPresence?.status; const peerUsername = peerUserPresence?.username; const isOnline = !['offline', 'loading'].includes(userStatus || ''); diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index f6ae2a0f65347..b6c2a742643d1 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -255,6 +255,10 @@ export type IUserInRole = Pick< '_id' | 'name' | 'username' | 'emails' | 'avatarETag' | 'createdAt' | 'roles' | 'type' | 'active' | '_updatedAt' >; +export type UserPresence = Readonly< + Partial> & Required> +>; + export type AvatarUrlObj = { avatarUrl: string; }; diff --git a/apps/meteor/client/definitions/Subscribable.ts b/packages/core-typings/src/Subscribable.ts similarity index 100% rename from apps/meteor/client/definitions/Subscribable.ts rename to packages/core-typings/src/Subscribable.ts diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 0f589b22ba229..6908b126aa435 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -9,6 +9,7 @@ export * from './IRoom'; export * from './IMessage'; export * from './federation'; export * from './Serialized'; +export * from './Subscribable'; export * from './ISetting'; export * from './ISubscription'; export * from './ITeam'; diff --git a/packages/mock-providers/jest.config.ts b/packages/mock-providers/jest.config.ts index 513d37db1e9c3..0de4cfdd48e48 100644 --- a/packages/mock-providers/jest.config.ts +++ b/packages/mock-providers/jest.config.ts @@ -4,4 +4,5 @@ import type { Config } from 'jest'; export default { preset: client.preset, modulePathIgnorePatterns: ['/__tests__/helpers'], + testMatch: ['/src/tests/**/**.spec.[jt]s?(x)'], } satisfies Config; diff --git a/packages/mock-providers/src/MockedAppRootBuilder.tsx b/packages/mock-providers/src/MockedAppRootBuilder.tsx index beca549f6218d..4b557b392f650 100644 --- a/packages/mock-providers/src/MockedAppRootBuilder.tsx +++ b/packages/mock-providers/src/MockedAppRootBuilder.tsx @@ -24,6 +24,7 @@ import { UserContext, ActionManagerContext, ModalContext, + UserPresenceContext, } from '@rocket.chat/ui-contexts'; import type { VideoConfPopupPayload } from '@rocket.chat/ui-video-conf'; import { VideoConfContext } from '@rocket.chat/ui-video-conf'; @@ -119,6 +120,10 @@ export class MockedAppRootBuilder { userId: null, }; + private userPresence: ContextType = { + queryUserData: (_uid) => ({ subscribe: () => () => undefined, get: () => undefined }), + }; + private videoConf: ContextType = { queryIncomingCalls: () => [() => () => undefined, () => []], queryRinging: () => [() => () => undefined, () => false], @@ -339,6 +344,14 @@ export class MockedAppRootBuilder { return this; } + withUsers(users: IUser[]): this { + users.forEach((user) => { + this.userPresence.queryUserData = (_uid) => ({ subscribe: () => () => undefined, get: () => user }); + }); + + return this; + } + withSubscriptions(subscriptions: SubscriptionWithRoom[]): this { this.subscriptions = subscriptions; @@ -520,6 +533,7 @@ export class MockedAppRootBuilder { router, settings, user, + userPresence, videoConf, i18n, authorization, @@ -609,36 +623,38 @@ export class MockedAppRootBuilder { {/* - */} - '', - emitInteraction: () => Promise.reject(new Error('not implemented')), - getInteractionPayloadByViewId: () => undefined, - handleServerInteraction: () => undefined, - off: () => undefined, - on: () => undefined, - openView: () => undefined, - disposeView: () => undefined, - notifyBusy: () => undefined, - notifyIdle: () => undefined, - }} - > - - {/* + */} + + '', + emitInteraction: () => Promise.reject(new Error('not implemented')), + getInteractionPayloadByViewId: () => undefined, + handleServerInteraction: () => undefined, + off: () => undefined, + on: () => undefined, + openView: () => undefined, + disposeView: () => undefined, + notifyBusy: () => undefined, + notifyIdle: () => undefined, + }} + > + + {/* */} - {wrappers.reduce( - (children, wrapper) => wrapper(children), - <> - {children} - {modal.currentModal.component} - , - )} - {/* + {wrappers.reduce( + (children, wrapper) => wrapper(children), + <> + {children} + {modal.currentModal.component} + , + )} + {/* */} - - - {/* + + + + {/* */} diff --git a/packages/mock-providers/src/tests/useSetting.spec.tsx b/packages/mock-providers/src/tests/useSetting.spec.tsx index bcad1b4da6e79..90701e06bbe40 100644 --- a/packages/mock-providers/src/tests/useSetting.spec.tsx +++ b/packages/mock-providers/src/tests/useSetting.spec.tsx @@ -1,5 +1,5 @@ import { useSetting } from '@rocket.chat/ui-contexts'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { mockAppRoot } from '..'; diff --git a/packages/mock-providers/src/tests/useSettings.spec.tsx b/packages/mock-providers/src/tests/useSettings.spec.tsx index 5263831e39ed7..8354da97db1a6 100644 --- a/packages/mock-providers/src/tests/useSettings.spec.tsx +++ b/packages/mock-providers/src/tests/useSettings.spec.tsx @@ -1,5 +1,5 @@ import { useSettings } from '@rocket.chat/ui-contexts'; -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react'; import { mockAppRoot } from '..'; diff --git a/packages/mock-providers/src/tests/useUserPresence.spec.tsx b/packages/mock-providers/src/tests/useUserPresence.spec.tsx new file mode 100644 index 0000000000000..dbdfda827222f --- /dev/null +++ b/packages/mock-providers/src/tests/useUserPresence.spec.tsx @@ -0,0 +1,43 @@ +import { faker } from '@faker-js/faker'; +import type { IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; +import { useUserPresence } from '@rocket.chat/ui-contexts'; +import { renderHook } from '@testing-library/react'; + +import { mockAppRoot } from '..'; + +// TODO: this will live in `mock-providers` package +function createFakeUser(overrides?: Partial): IUser { + return { + _id: faker.database.mongodbObjectId(), + _updatedAt: faker.date.recent(), + username: faker.internet.userName(), + name: faker.person.fullName(), + createdAt: faker.date.recent(), + roles: ['user'], + active: faker.datatype.boolean(), + type: 'user', + ...overrides, + }; +} + +const SAMPLE_STATUS = 'Sample Status'; + +const user = createFakeUser({ + active: true, + roles: ['admin'], + type: 'user', + statusText: 'Sample Status', + status: UserStatus.ONLINE, +}); + +describe('useUserPresence', () => { + it('should return presence from context', () => { + const { result } = renderHook(() => useUserPresence(user._id), { + wrapper: mockAppRoot().withUsers([user]).build(), + }); + + expect(result.current?.status).toEqual(UserStatus.ONLINE); + expect(result.current?.statusText).toEqual(SAMPLE_STATUS); + }); +}); diff --git a/packages/ui-contexts/src/UserPresenceContext.ts b/packages/ui-contexts/src/UserPresenceContext.ts new file mode 100644 index 0000000000000..58ccfd6d676e3 --- /dev/null +++ b/packages/ui-contexts/src/UserPresenceContext.ts @@ -0,0 +1,10 @@ +import type { Subscribable, UserPresence } from '@rocket.chat/core-typings'; +import { createContext } from 'react'; + +export type UserPresenceContextValue = { + queryUserData: (uid: string | undefined) => Subscribable; +}; + +export const UserPresenceContext = createContext({ + queryUserData: () => ({ get: () => undefined, subscribe: () => () => undefined }), +}); diff --git a/apps/meteor/client/hooks/useUserData.ts b/packages/ui-contexts/src/hooks/useUserPresence.ts similarity index 55% rename from apps/meteor/client/hooks/useUserData.ts rename to packages/ui-contexts/src/hooks/useUserPresence.ts index acac1fc4f553b..8a3857e858a5f 100644 --- a/apps/meteor/client/hooks/useUserData.ts +++ b/packages/ui-contexts/src/hooks/useUserPresence.ts @@ -1,16 +1,16 @@ +import type { UserPresence } from '@rocket.chat/core-typings'; import { useContext, useMemo, useSyncExternalStore } from 'react'; -import { UserPresenceContext } from '../contexts/UserPresenceContext'; -import type { UserPresence } from '../lib/presence'; +import { UserPresenceContext } from '../UserPresenceContext'; /** - * Hook to fetch and subscribe users data + * Hook to fetch and subscribe user presence data * * @param uid - User Id - * @returns Users data: status, statusText, username, name + * @returns status, statusText, username, name * @public */ -export const useUserData = (uid: string): UserPresence | undefined => { +export const useUserPresence = (uid: string | undefined): UserPresence | undefined => { const userPresence = useContext(UserPresenceContext); const { subscribe, get } = useMemo( diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index dfcd7ca301455..314ce5bc372d5 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -14,6 +14,7 @@ export { ToastMessagesContext, ToastMessagesContextValue } from './ToastMessages export { TooltipContext, TooltipContextValue } from './TooltipContext'; export { TranslationContext, TranslationContextValue } from './TranslationContext'; export { UserContext, UserContextValue } from './UserContext'; +export { UserPresenceContext, UserPresenceContextValue } from './UserPresenceContext'; export { DeviceContext, Device, DeviceContextValue } from './DeviceContext'; export { ActionManagerContext, IActionManager } from './ActionManagerContext'; @@ -90,6 +91,7 @@ export { useIsDeviceManagementEnabled } from './hooks/useIsDeviceManagementEnabl export { useSetOutputMediaDevice } from './hooks/useSetOutputMediaDevice'; export { useSetInputMediaDevice } from './hooks/useSetInputMediaDevice'; export { useAccountsCustomFields } from './hooks/useAccountsCustomFields'; +export { useUserPresence } from './hooks/useUserPresence'; export { UploadResult } from './ServerContext'; export { TranslationKey, TranslationLanguage } from './TranslationContext'; From a6d95d7a159da233131a6749bca5cc62f9dc00de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Mon, 24 Mar 2025 10:18:58 -0300 Subject: [PATCH 022/187] fix: changing email to verified not working on admin > users (#35299) Co-authored-by: Diego Sampaio <8591547+sampaiodiego@users.noreply.github.com> --- .changeset/bright-forks-drop.md | 5 ++ .../lib/server/functions/saveUser/saveUser.ts | 10 +++- apps/meteor/tests/end-to-end/api/users.ts | 57 +++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 .changeset/bright-forks-drop.md diff --git a/.changeset/bright-forks-drop.md b/.changeset/bright-forks-drop.md new file mode 100644 index 0000000000000..2cad496c2e68a --- /dev/null +++ b/.changeset/bright-forks-drop.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes user email verification update not working on admin > users diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts index e4a10c4c5a57f..00baff3135d58 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts @@ -147,8 +147,14 @@ const _saveUser = (session?: ClientSession) => } } - if (typeof userData.verified === 'boolean' && !userData.email) { - updater.set('emails.0.verified', userData.verified); + if (typeof userData.verified === 'boolean') { + if (oldUserData && 'emails' in oldUserData && oldUserData.emails?.some(({ address }) => address === userData.email)) { + const index = oldUserData.emails.findIndex(({ address }) => address === userData.email); + updater.set(`emails.${index}.verified`, userData.verified); + } + if (!userData.email) { + updater.set(`emails.0.verified`, userData.verified); + } } if (userData.customFields) { diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index 84c52e7596fe9..e149de6d83e37 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -2006,6 +2006,63 @@ describe('[Users]', () => { await deleteUser(user); }); + describe('email verification', () => { + let admin: TestUser; + let userToUpdate: TestUser; + let userCredentials: Credentials; + + beforeEach(async () => { + admin = await createUser({ roles: ['admin'] }); + userToUpdate = await createUser(); + userCredentials = await login(admin.username, password); + }); + + afterEach(async () => { + await deleteUser(userToUpdate); + await deleteUser(admin); + }); + + it("should update user's email verified correctly", async () => { + await request + .post(api('users.update')) + .set(userCredentials) + .send({ + userId: userToUpdate._id, + data: { + verified: true, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('user.emails[0].verified', true); + expect(res.body).to.not.have.nested.property('user.e2e'); + }); + }); + + it("should update user's email verified even if email is not changed", (done) => { + void request + .post(api('users.update')) + .set(userCredentials) + .send({ + userId: userToUpdate._id, + data: { + email: userToUpdate.emails[0].address, + verified: true, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('user.emails[0].verified', true); + expect(res.body).to.not.have.nested.property('user.e2e'); + }) + .end(done); + }); + }); + function failUpdateUser(name: string) { it(`should not update an user if the new username is the reserved word ${name}`, (done) => { void request From b1a739c7aff218f917ba7600bda34099b902e7a3 Mon Sep 17 00:00:00 2001 From: Abhinav Kumar Date: Mon, 24 Mar 2025 20:46:40 +0530 Subject: [PATCH 023/187] regression: topic rendering in DMs in new navigation (#35553) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .../client/views/room/body/RoomTopic.spec.tsx | 195 ++++++++++++++++++ .../client/views/room/body/RoomTopic.tsx | 7 +- 2 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 apps/meteor/client/views/room/body/RoomTopic.spec.tsx diff --git a/apps/meteor/client/views/room/body/RoomTopic.spec.tsx b/apps/meteor/client/views/room/body/RoomTopic.spec.tsx new file mode 100644 index 0000000000000..e26c52d71a80a --- /dev/null +++ b/apps/meteor/client/views/room/body/RoomTopic.spec.tsx @@ -0,0 +1,195 @@ +import { UserStatus } from '@rocket.chat/core-typings'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import { render, screen } from '@testing-library/react'; + +import { RoomTopic } from './RoomTopic'; +import FakeRoomProvider from '../../../../tests/mocks/client/FakeRoomProvider'; +import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../../tests/mocks/data'; + +const user = createFakeUser({ + active: true, + roles: ['admin'], + type: 'user', + status: UserStatus.ONLINE, +}); + +const user2 = createFakeUser({ + active: true, + roles: ['admin'], + type: 'user', + statusText: 'Sample Status', + status: UserStatus.ONLINE, +}); + +const user3 = createFakeUser({ + active: true, + roles: ['admin'], + type: 'user', + status: UserStatus.ONLINE, +}); + +describe('RoomTopic', () => { + it('should render Add Topic when no topic is present and user can edit room', () => { + const room = createFakeRoom({ topic: '', t: 'c', u: { _id: user._id, username: user.username, name: user.name } }); + const subscription = createFakeSubscription({ t: 'c', rid: room._id, u: room.u, roles: ['owner'] }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withPermission('edit-room') + .withUser(user) + .build(), + }, + ); + + expect(screen.getByText('Add_topic')).toBeInTheDocument(); + }); + + it('should not render Add Topic when no topic is present and user cannot edit room', () => { + const room = createFakeRoom({ topic: '', t: 'c' }); + const subscription = createFakeSubscription({ t: 'c', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + }); + + it('should not render Add Topic when no statusText is present, user can edit room and room is a direct message room', () => { + const room = createFakeRoom({ topic: '', t: 'd', uids: [user._id, user3._id] }); + const subscription = createFakeSubscription({ t: 'd', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .withUsers([user3]) + .withPermission('edit-room') + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + }); + + it('should render topic when topic is present for rooms', () => { + const room = createFakeRoom({ topic: 'Sample Topic', t: 'c' }); + const subscription = createFakeSubscription({ t: 'c', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .build(), + }, + ); + + expect(screen.getByText('Sample Topic')).toBeInTheDocument(); + }); + + it('should render statusText when statusText is present for direct message user and users length < 3', () => { + const room = createFakeRoom({ topic: '', t: 'd', uids: [user._id, user2._id] }); + const subscription = createFakeSubscription({ t: 'd', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withPermission('edit-room') + .withUser(user) + .withUsers([user2]) + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + expect(screen.getByText('Sample Status')).toBeInTheDocument(); + }); + + it('should not render statusText when statusText is present for direct message user and users length >= 3', () => { + const room = createFakeRoom({ topic: '', t: 'd', uids: [user._id, user2._id, user3._id] }); + const subscription = createFakeSubscription({ t: 'd', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withPermission('edit-room') + .withUser(user) + .withUsers([user2, user3]) + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + expect(screen.queryByText('Sample Status')).not.toBeInTheDocument(); + }); + + it('should not render Add Topic for livechat rooms', () => { + const room = createFakeRoom({ topic: '', t: 'l' }); + const subscription = createFakeSubscription({ t: 'l', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .withPermission('edit-room') + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + }); + + it('should not render Add Topic for voip rooms', () => { + const room = createFakeRoom({ topic: '', t: 'v' }); + const subscription = createFakeSubscription({ t: 'v', rid: room._id }); + + render( + + + , + { + wrapper: mockAppRoot() + .withSubscriptions([{ ...subscription, ...room }] as unknown as SubscriptionWithRoom[]) + .withUser(user) + .withPermission('edit-room') + .build(), + }, + ); + + expect(screen.queryByText('Add_topic')).not.toBeInTheDocument(); + }); +}); diff --git a/apps/meteor/client/views/room/body/RoomTopic.tsx b/apps/meteor/client/views/room/body/RoomTopic.tsx index 98d605eb7944b..0c9a1c79a870a 100644 --- a/apps/meteor/client/views/room/body/RoomTopic.tsx +++ b/apps/meteor/client/views/room/body/RoomTopic.tsx @@ -1,5 +1,5 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; -import { isTeamRoom } from '@rocket.chat/core-typings'; +import { isDirectMessageRoom, isPrivateRoom, isPublicRoom, isTeamRoom } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { RoomBanner, RoomBannerContent } from '@rocket.chat/ui-client'; import { useUserId, useTranslation, useRouter, useUserPresence } from '@rocket.chat/ui-contexts'; @@ -23,7 +23,8 @@ export const RoomTopic = ({ room }: RoomTopicProps) => { const currentRoute = router.getLocationPathname(); const href = isTeamRoom(room) ? `${currentRoute}/team-info` : `${currentRoute}/channel-settings`; - const topic = room.t === 'd' && (room.uids?.length ?? 0) < 3 ? directUserData?.statusText : room.topic; + const topic = isDirectMessageRoom(room) && (room.uids?.length ?? 0) < 3 ? directUserData?.statusText : room.topic; + const canEditTopic = canEdit && (isPublicRoom(room) || isPrivateRoom(room)); if (!topic && !canEdit) { return null; @@ -32,7 +33,7 @@ export const RoomTopic = ({ room }: RoomTopicProps) => { return ( - {!topic && canEdit ? ( + {!topic && canEditTopic ? ( {t('Add_topic')} From b344b56054c90e94c600925adbfa7e8d14cdf216 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Mon, 24 Mar 2025 14:57:46 -0300 Subject: [PATCH 024/187] chore: Replace collection with Zustand store in `EditableSettingsProvider` (#35597) --- .../views/admin/EditableSettingsContext.ts | 139 ++++++--- .../settings/EditableSettingsProvider.tsx | 281 ++++++------------ .../views/admin/settings/SettingsRoute.tsx | 4 +- apps/meteor/package.json | 3 +- yarn.lock | 22 ++ 5 files changed, 215 insertions(+), 234 deletions(-) diff --git a/apps/meteor/client/views/admin/EditableSettingsContext.ts b/apps/meteor/client/views/admin/EditableSettingsContext.ts index 0af5851e823a0..cbfb86259fec5 100644 --- a/apps/meteor/client/views/admin/EditableSettingsContext.ts +++ b/apps/meteor/client/views/admin/EditableSettingsContext.ts @@ -1,68 +1,127 @@ -import type { ISettingBase, ISettingColor, ISetting } from '@rocket.chat/core-typings'; -import type { SettingsContextQuery } from '@rocket.chat/ui-contexts'; -import { createContext, useContext, useMemo, useSyncExternalStore } from 'react'; +import type { ISetting } from '@rocket.chat/core-typings'; +import { createContext, useContext } from 'react'; +import { create, type StoreApi, type UseBoundStore } from 'zustand'; +import { useShallow } from 'zustand/shallow'; -export type EditableSetting = (ISettingBase | ISettingColor) & { +export type EditableSetting = ISetting & { disabled: boolean; changed: boolean; invisible: boolean; }; -type EditableSettingsContextQuery = SettingsContextQuery & { - changed?: boolean; +export const compareSettings = (a: EditableSetting, b: EditableSetting): number => { + const sorter = a.sorter - b.sorter; + if (sorter !== 0) return sorter; + + const tab = (a.tab ?? '').localeCompare(b.tab ?? ''); + if (tab !== 0) return tab; + + const i18nLabel = a.i18nLabel.localeCompare(b.i18nLabel); + + return i18nLabel; }; +type EditableSettingsContextQuery = + | { + group: ISetting['_id']; + } + | { + group: ISetting['_id']; + section: string; + tab?: ISetting['_id']; + } + | { + group: ISetting['_id']; + changed: true; + }; + +export interface IEditableSettingsState { + state: EditableSetting[]; + initialState: ISetting[]; + sync(newInitialState: ISetting[]): void; + mutate(changes: Partial[]): void; +} + export type EditableSettingsContextValue = { - readonly queryEditableSetting: ( - _id: ISetting['_id'], - ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => EditableSetting | undefined]; - readonly queryEditableSettings: ( - query: EditableSettingsContextQuery, - ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => EditableSetting[]]; - readonly queryGroupSections: ( - _id: ISetting['_id'], - tab?: ISetting['_id'], - ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => string[]]; - readonly queryGroupTabs: ( - _id: ISetting['_id'], - ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting['_id'][]]; - readonly dispatch: (changes: Partial[]) => void; + useEditableSettingsStore: UseBoundStore>; }; export const EditableSettingsContext = createContext({ - queryEditableSetting: () => [(): (() => void) => (): void => undefined, (): undefined => undefined], - queryEditableSettings: () => [(): (() => void) => (): void => undefined, (): EditableSetting[] => []], - queryGroupSections: () => [(): (() => void) => (): void => undefined, (): string[] => []], - queryGroupTabs: () => [(): (() => void) => (): void => undefined, (): ISetting['_id'][] => []], - dispatch: () => undefined, + useEditableSettingsStore: create()(() => ({ + state: [], + initialState: [], + sync: () => undefined, + mutate: () => undefined, + })), }); export const useEditableSetting = (_id: ISetting['_id']): EditableSetting | undefined => { - const { queryEditableSetting } = useContext(EditableSettingsContext); + const { useEditableSettingsStore } = useContext(EditableSettingsContext); - const [subscribe, getSnapshot] = useMemo(() => queryEditableSetting(_id), [queryEditableSetting, _id]); - return useSyncExternalStore(subscribe, getSnapshot); + return useEditableSettingsStore((state) => state.state.find((x) => x._id === _id)); }; -export const useEditableSettings = (query?: EditableSettingsContextQuery): EditableSetting[] => { - const { queryEditableSettings } = useContext(EditableSettingsContext); - const [subscribe, getSnapshot] = useMemo(() => queryEditableSettings(query ?? {}), [queryEditableSettings, query]); - return useSyncExternalStore(subscribe, getSnapshot); +export const useEditableSettings = (query: EditableSettingsContextQuery): EditableSetting[] => { + const { useEditableSettingsStore } = useContext(EditableSettingsContext); + + return useEditableSettingsStore( + useShallow((state) => + state.state + .filter((x) => { + if ('changed' in query) { + return x.group === query.group && x.changed; + } + + if ('section' in query) { + return ( + x.group === query.group && + (query.section ? x.section === query.section : !x.section) && + (query.tab ? x.tab === query.tab : !x.tab) + ); + } + + return x.group === query.group; + }) + .sort(compareSettings), + ), + ); }; export const useEditableSettingsGroupSections = (_id: ISetting['_id'], tab?: ISetting['_id']): string[] => { - const { queryGroupSections } = useContext(EditableSettingsContext); + const { useEditableSettingsStore } = useContext(EditableSettingsContext); - const [subscribe, getSnapshot] = useMemo(() => queryGroupSections(_id, tab), [queryGroupSections, _id, tab]); - return useSyncExternalStore(subscribe, getSnapshot); + return useEditableSettingsStore( + useShallow((state) => + Array.from( + new Set( + state.state + .filter((x) => x.group === _id && (tab !== undefined ? x.tab === tab : !x.tab)) + .sort(compareSettings) + .map(({ section }) => section || ''), + ), + ), + ), + ); }; export const useEditableSettingsGroupTabs = (_id: ISetting['_id']): ISetting['_id'][] => { - const { queryGroupTabs } = useContext(EditableSettingsContext); + const { useEditableSettingsStore } = useContext(EditableSettingsContext); - const [subscribe, getSnapshot] = useMemo(() => queryGroupTabs(_id), [queryGroupTabs, _id]); - return useSyncExternalStore(subscribe, getSnapshot); + return useEditableSettingsStore( + useShallow((state) => + Array.from( + new Set( + state.state + .filter((x) => x.group === _id) + .sort(compareSettings) + .map(({ tab }) => tab || ''), + ), + ), + ), + ); }; -export const useEditableSettingsDispatch = (): ((changes: Partial[]) => void) => - useContext(EditableSettingsContext).dispatch; +export const useEditableSettingsDispatch = (): ((changes: Partial[]) => void) => { + const { useEditableSettingsStore } = useContext(EditableSettingsContext); + return useEditableSettingsStore((state) => state.mutate); +}; diff --git a/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx b/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx index 677332261697d..8fd724ee1b361 100644 --- a/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx +++ b/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx @@ -1,209 +1,110 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import type { SettingsContextQuery } from '@rocket.chat/ui-contexts'; +import { createFilterFromQuery } from '@rocket.chat/mongo-adapter'; import { useSettings } from '@rocket.chat/ui-contexts'; -import { Mongo } from 'meteor/mongo'; -import { Tracker } from 'meteor/tracker'; -import type { FilterOperators } from 'mongodb'; -import type { MutableRefObject, ReactNode } from 'react'; -import { useEffect, useMemo, useRef } from 'react'; +import type { ReactNode } from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import { create } from 'zustand'; -import { createReactiveSubscriptionFactory } from '../../../lib/createReactiveSubscriptionFactory'; -import type { EditableSetting, EditableSettingsContextValue } from '../EditableSettingsContext'; +import type { EditableSetting, IEditableSettingsState } from '../EditableSettingsContext'; import { EditableSettingsContext } from '../EditableSettingsContext'; -const defaultQuery: SettingsContextQuery = {}; -const defaultOmit: Array = []; +const defaultOmit: Array = ['Cloud_Workspace_AirGapped_Restrictions_Remaining_Days']; + +const performSettingQuery = ( + query: + | string + | { + _id: string; + value: unknown; + } + | { + _id: string; + value: unknown; + }[] + | undefined, + settings: ISetting[], +) => { + if (!query) { + return true; + } + + const queries = [].concat(typeof query === 'string' ? JSON.parse(query) : query); + return queries.every((query) => settings.some(createFilterFromQuery(query))); +}; type EditableSettingsProviderProps = { children?: ReactNode; - query?: SettingsContextQuery; - omit?: Array; }; -const EditableSettingsProvider = ({ children, query = defaultQuery, omit = defaultOmit }: EditableSettingsProviderProps) => { - const settingsCollectionRef = useRef>(null) as MutableRefObject>; - const persistedSettings = useSettings(query); - - const getSettingsCollection = useEffectEvent(() => { - if (!settingsCollectionRef.current) { - settingsCollectionRef.current = new Mongo.Collection(null); - } - - return settingsCollectionRef.current; - }) as () => Mongo.Collection; - - useEffect(() => { - const settingsCollection = getSettingsCollection(); - - settingsCollection.remove({ _id: { $nin: persistedSettings.map(({ _id }) => _id) } }); - for (const { _id, ...fields } of persistedSettings) { - settingsCollection.upsert(_id, { $set: { ...fields }, $unset: { changed: true } }); - } - // TODO: Remove option to omit settings from admin pages manually - // This is a very wacky workaround due to lack of support to omit settings from the - // admin settings page while keeping them public. - if (omit.length > 0) { - settingsCollection.remove({ _id: { $in: omit } }); - } - }, [getSettingsCollection, persistedSettings, omit]); - - const queryEditableSetting = useMemo(() => { - const validateSettingQueries = ( - query: undefined | string | FilterOperators | FilterOperators[], - settingsCollection: Mongo.Collection, - ): boolean => { - if (!query) { - return true; - } - - const queries = [].concat(typeof query === 'string' ? JSON.parse(query) : query); - return queries.every((query) => settingsCollection.find(query).count() > 0); - }; - - return createReactiveSubscriptionFactory((_id: ISetting['_id']): EditableSetting | undefined => { - const settingsCollection = getSettingsCollection(); - const editableSetting = settingsCollection.findOne(_id); - - if (!editableSetting) { - return undefined; - } - - return { - ...editableSetting, - disabled: editableSetting.blocked || !validateSettingQueries(editableSetting.enableQuery, settingsCollection), - invisible: !validateSettingQueries(editableSetting.displayQuery, settingsCollection), - }; - }); - }, [getSettingsCollection]); - - const queryEditableSettings = useMemo( - () => - createReactiveSubscriptionFactory((query = {}) => - getSettingsCollection() - .find( - { - ...('_id' in query && { _id: { $in: query._id } }), - ...('group' in query && { group: query.group }), - ...('changed' in query && { changed: query.changed }), - $and: [ - { - ...('section' in query && - (query.section - ? { section: query.section } - : { - $or: [{ section: { $exists: false } }, { section: '' }], - })), - }, - { - ...('tab' in query && - (query.tab - ? { tab: query.tab } - : { - $or: [{ tab: { $exists: false } }, { tab: '' }], - })), - }, - ], - }, - { - sort: { - section: 1, - sorter: 1, - i18nLabel: 1, - }, - }, - ) - .fetch(), - ), - [getSettingsCollection], - ); - - const queryGroupSections = useMemo( - () => - createReactiveSubscriptionFactory((_id: ISetting['_id'], tab?: ISetting['_id']) => - Array.from( - new Set( - getSettingsCollection() - .find( - { - group: _id, - ...(tab !== undefined - ? { tab } - : { - $or: [{ tab: { $exists: false } }, { tab: '' }], - }), - }, - { - fields: { - section: 1, - }, - sort: { - sorter: 1, - section: 1, - i18nLabel: 1, - }, - }, - ) - .fetch() - .map(({ section }) => section || ''), - ), +// TODO: this component can be replaced by RHF state management +const EditableSettingsProvider = ({ children }: EditableSettingsProviderProps) => { + const persistedSettings = useSettings(); + + const [useEditableSettingsStore] = useState(() => + create()((set) => ({ + state: persistedSettings + .filter((x) => !defaultOmit.includes(x._id)) + .map( + (persisted): EditableSetting => ({ + ...persisted, + changed: false, + disabled: persisted.blocked || !performSettingQuery(persisted.enableQuery, persistedSettings), + invisible: !performSettingQuery(persisted.displayQuery, persistedSettings), + }), ), - ), - [getSettingsCollection], + initialState: persistedSettings, + sync: (newInitialState) => { + set(({ state }) => ({ + state: newInitialState + .filter((x) => !defaultOmit.includes(x._id)) + .map( + (persisted): EditableSetting => ({ + ...state.find(({ _id }) => _id === persisted._id), + ...persisted, + changed: false, + disabled: persisted.blocked || !performSettingQuery(persisted.enableQuery, state), + invisible: !performSettingQuery(persisted.displayQuery, state), + }), + ), + })); + }, + mutate: (changes) => { + set(({ state, initialState }) => ({ + state: initialState + .filter((x) => !defaultOmit.includes(x._id)) + .map((persisted): EditableSetting => { + const current = state.find(({ _id }) => _id === persisted._id); + if (!current) throw new Error(`Setting ${persisted._id} not found`); + + const change = changes.find(({ _id }) => _id === current._id); + + if (!change) { + return current; + } + + return { + ...current, + ...change, + disabled: persisted.blocked || !performSettingQuery(persisted.enableQuery, state), + invisible: !performSettingQuery(persisted.displayQuery, state), + }; + }), + })); + }, + })), ); - const queryGroupTabs = useMemo( - () => - createReactiveSubscriptionFactory((_id: ISetting['_id']) => - Array.from( - new Set( - getSettingsCollection() - .find( - { - group: _id, - }, - { - fields: { - tab: 1, - }, - sort: { - sorter: 1, - tab: 1, - i18nLabel: 1, - }, - }, - ) - .fetch() - .map(({ tab }) => tab || ''), - ), - ), - ), - [getSettingsCollection], - ); - - const dispatch = useEffectEvent((changes: Partial[]): void => { - for (const { _id, ...data } of changes) { - if (!_id) { - continue; - } + const sync = useEditableSettingsStore((state) => state.sync); - getSettingsCollection().update(_id, { $set: data }); - } - Tracker.flush(); - }); + useEffect(() => { + sync(persistedSettings); + }, [persistedSettings, sync]); - const contextValue = useMemo( - () => ({ - queryEditableSetting, - queryEditableSettings, - queryGroupSections, - queryGroupTabs, - dispatch, - }), - [queryEditableSetting, queryEditableSettings, queryGroupSections, queryGroupTabs, dispatch], + return ( + ({ useEditableSettingsStore }), [useEditableSettingsStore])}> + {children} + ); - - return ; }; export default EditableSettingsProvider; diff --git a/apps/meteor/client/views/admin/settings/SettingsRoute.tsx b/apps/meteor/client/views/admin/settings/SettingsRoute.tsx index 70776dee4fe80..145fb72e3ef3e 100644 --- a/apps/meteor/client/views/admin/settings/SettingsRoute.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsRoute.tsx @@ -6,8 +6,6 @@ import SettingsGroupSelector from './SettingsGroupSelector'; import SettingsPage from './SettingsPage'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; -const omittedSettings = ['Cloud_Workspace_AirGapped_Restrictions_Remaining_Days']; - export const SettingsRoute = (): ReactElement => { const hasPermission = useIsPrivilegedSettingsContext(); const groupId = useRouteParameter('group'); @@ -22,7 +20,7 @@ export const SettingsRoute = (): ReactElement => { } return ( - + router.navigate('/admin/settings')} /> ); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 49c1a6f041e36..c35394fda8df5 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -445,7 +445,8 @@ "xml-encryption": "~3.1.0", "xml2js": "~0.6.2", "yaqrcode": "^0.2.1", - "zod": "^3.24.1" + "zod": "^3.24.1", + "zustand": "~5.0.3" }, "meteor": { "mainModule": { diff --git a/yarn.lock b/yarn.lock index d3468caa822f9..27495dd1e7bf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9157,6 +9157,7 @@ __metadata: xml2js: "npm:~0.6.2" yaqrcode: "npm:^0.2.1" zod: "npm:^3.24.1" + zustand: "npm:~5.0.3" languageName: unknown linkType: soft @@ -38251,6 +38252,27 @@ __metadata: languageName: node linkType: hard +"zustand@npm:~5.0.3": + version: 5.0.3 + resolution: "zustand@npm:5.0.3" + peerDependencies: + "@types/react": ">=18.0.0" + immer: ">=9.0.6" + react: ">=18.0.0" + use-sync-external-store: ">=1.2.0" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + checksum: 10/35728fdaa68291ea3e469524316dda4fe1d8cc22d8be3df309ca99bda0dbc7e66a1c502f66c26f76abfb4bd49a6e1368160353eb3cb173c24042a5f252075462 + languageName: node + linkType: hard + "zwitch@npm:^1.0.0": version: 1.0.5 resolution: "zwitch@npm:1.0.5" From e54434a31f04c037553537fcffa6eb7ce83bea3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:20:42 -0300 Subject: [PATCH 025/187] refactor: remove KonchatNotification `notify` from meteor (#35585) --- .../app/ui/client/lib/KonchatNotification.ts | 117 ----------------- .../notification/useDesktopNotification.ts | 8 +- .../hooks/notification/useNotification.ts | 122 ++++++++++++++++++ .../PreferencesNotificationsSection.tsx | 7 +- 4 files changed, 131 insertions(+), 123 deletions(-) create mode 100644 apps/meteor/client/hooks/notification/useNotification.ts diff --git a/apps/meteor/app/ui/client/lib/KonchatNotification.ts b/apps/meteor/app/ui/client/lib/KonchatNotification.ts index cebd0aa4324f1..3713395084cad 100644 --- a/apps/meteor/app/ui/client/lib/KonchatNotification.ts +++ b/apps/meteor/app/ui/client/lib/KonchatNotification.ts @@ -1,15 +1,5 @@ -import type { INotificationDesktop } from '@rocket.chat/core-typings'; -import { Random } from '@rocket.chat/random'; -import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; -import { onClientMessageReceived } from '../../../../client/lib/onClientMessageReceived'; -import { router } from '../../../../client/providers/RouterProvider'; -import { stripTags } from '../../../../lib/utils/stringUtils'; -import { getUserPreference } from '../../../utils/client'; -import { getUserAvatarURL } from '../../../utils/client/getUserAvatarURL'; -import { sdk } from '../../../utils/client/lib/SDKClient'; - declare global { // eslint-disable-next-line @typescript-eslint/naming-convention interface NotificationEventMap { @@ -27,113 +17,6 @@ class KonchatNotification { }); } } - - public async notify(notification: INotificationDesktop) { - if (typeof window.Notification === 'undefined' || Notification.permission !== 'granted') { - return; - } - - if (!notification.payload) { - return; - } - - const { rid } = notification.payload; - - if (!rid) { - return; - } - const message = await onClientMessageReceived({ - rid, - msg: notification.text, - notification: true, - } as any); - - const requireInteraction = getUserPreference(Meteor.userId(), 'desktopNotificationRequireInteraction'); - const n = new Notification(notification.title, { - icon: notification.icon || getUserAvatarURL(notification.payload.sender?.username as string), - body: stripTags(message.msg), - tag: notification.payload._id, - canReply: true, - silent: true, - requireInteraction, - } as NotificationOptions & { - canReply?: boolean; // TODO is this still needed for the desktop app? - }); - - const notificationDuration = !requireInteraction ? (notification.duration ?? 0) - 0 || 10 : -1; - if (notificationDuration > 0) { - setTimeout(() => n.close(), notificationDuration * 1000); - } - - if (n.addEventListener) { - n.addEventListener( - 'reply', - ({ response }) => - void sdk.call('sendMessage', { - _id: Random.id(), - rid, - msg: response, - }), - ); - } - - n.onclick = function () { - this.close(); - window.focus(); - - if (!notification.payload._id || !notification.payload.rid || !notification.payload.name) { - return; - } - - switch (notification.payload?.type) { - case 'd': - return router.navigate({ - pattern: '/direct/:rid/:tab?/:context?', - params: { - rid: notification.payload.rid, - ...(notification.payload.tmid && { - tab: 'thread', - context: notification.payload.tmid, - }), - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - case 'c': - return router.navigate({ - pattern: '/channel/:name/:tab?/:context?', - params: { - name: notification.payload.name, - ...(notification.payload.tmid && { - tab: 'thread', - context: notification.payload.tmid, - }), - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - case 'p': - return router.navigate({ - pattern: '/group/:name/:tab?/:context?', - params: { - name: notification.payload.name, - ...(notification.payload.tmid && { - tab: 'thread', - context: notification.payload.tmid, - }), - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - case 'l': - return router.navigate({ - pattern: '/live/:id/:tab?/:context?', - params: { - id: notification.payload.rid, - tab: 'room-info', - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - } - }; - } } const instance = new KonchatNotification(); diff --git a/apps/meteor/client/hooks/notification/useDesktopNotification.ts b/apps/meteor/client/hooks/notification/useDesktopNotification.ts index cd38506a62fda..a0b72dbf726b2 100644 --- a/apps/meteor/client/hooks/notification/useDesktopNotification.ts +++ b/apps/meteor/client/hooks/notification/useDesktopNotification.ts @@ -2,13 +2,15 @@ import type { INotificationDesktop } from '@rocket.chat/core-typings'; import { useUser } from '@rocket.chat/ui-contexts'; import { useCallback } from 'react'; +import { useNotification } from './useNotification'; import { e2e } from '../../../app/e2e/client'; -import { KonchatNotification } from '../../../app/ui/client/lib/KonchatNotification'; import { RoomManager } from '../../lib/RoomManager'; import { getAvatarAsPng } from '../../lib/utils/getAvatarAsPng'; export const useDesktopNotification = () => { const user = useUser(); + const notify = useNotification(); + const notifyDesktop = useCallback( async (notification: INotificationDesktop) => { if ( @@ -30,10 +32,10 @@ export const useDesktopNotification = () => { return getAvatarAsPng(notification.payload.sender?.username, (avatarAsPng) => { notification.icon = avatarAsPng; - return KonchatNotification.notify(notification); + return notify(notification); }); }, - [user?.status], + [notify, user?.status], ); return notifyDesktop; diff --git a/apps/meteor/client/hooks/notification/useNotification.ts b/apps/meteor/client/hooks/notification/useNotification.ts new file mode 100644 index 0000000000000..96fe22ffdd6b6 --- /dev/null +++ b/apps/meteor/client/hooks/notification/useNotification.ts @@ -0,0 +1,122 @@ +import type { INotificationDesktop } from '@rocket.chat/core-typings'; +import { Random } from '@rocket.chat/random'; +import { useRouter, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useCallback } from 'react'; + +import { getUserAvatarURL } from '../../../app/utils/client'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { stripTags } from '../../../lib/utils/stringUtils'; +import { onClientMessageReceived } from '../../lib/onClientMessageReceived'; + +export const useNotification = () => { + const requireInteraction = useUserPreference('desktopNotificationRequireInteraction'); + const router = useRouter(); + + const notify = useCallback( + async (notification: INotificationDesktop) => { + if (typeof window.Notification === 'undefined' || Notification.permission !== 'granted') { + return; + } + if (!notification.payload) { + return; + } + + const { rid } = notification.payload; + if (!rid) { + return; + } + const message = await onClientMessageReceived({ + rid, + msg: notification.text, + notification: true, + } as any); + + const n = new Notification(notification.title, { + icon: notification.icon || getUserAvatarURL(notification.payload.sender?.username as string), + body: stripTags(message?.msg), + tag: notification.payload._id, + canReply: true, + silent: true, + requireInteraction, + } as NotificationOptions & { + canReply?: boolean; + }); + const notificationDuration = !requireInteraction ? (notification.duration ?? 0) - 0 || 10 : -1; + if (notificationDuration > 0) { + setTimeout(() => n.close(), notificationDuration * 1000); + } + + if (n.addEventListener) { + n.addEventListener( + 'reply', + ({ response }) => + void sdk.call('sendMessage', { + _id: Random.id(), + rid, + msg: response, + }), + ); + } + + n.onclick = () => { + n.close(); + window.focus(); + + if (!notification.payload._id || !notification.payload.rid || !notification.payload.name) { + return; + } + + switch (notification.payload?.type) { + case 'd': + router.navigate({ + pattern: '/direct/:rid/:tab?/:context?', + params: { + rid: notification.payload.rid, + ...(notification.payload.tmid && { + tab: 'thread', + context: notification.payload.tmid, + }), + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + break; + case 'c': + return router.navigate({ + pattern: '/channel/:name/:tab?/:context?', + params: { + name: notification.payload.name, + ...(notification.payload.tmid && { + tab: 'thread', + context: notification.payload.tmid, + }), + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + case 'p': + return router.navigate({ + pattern: '/group/:name/:tab?/:context?', + params: { + name: notification.payload.name, + ...(notification.payload.tmid && { + tab: 'thread', + context: notification.payload.tmid, + }), + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + case 'l': + return router.navigate({ + pattern: '/live/:id/:tab?/:context?', + params: { + id: notification.payload.rid, + tab: 'room-info', + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + } + }; + }, + [requireInteraction, router], + ); + return notify; +}; diff --git a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx index 0f081a83532f6..f95696afb7d41 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx @@ -7,7 +7,7 @@ import { useId, useCallback, useEffect, useState, useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { KonchatNotification } from '../../../../app/ui/client/lib/KonchatNotification'; +import { useNotification } from '../../../hooks/notification/useNotification'; const notificationOptionsLabelMap = { all: 'All_messages', @@ -39,18 +39,19 @@ const PreferencesNotificationsSection = () => { const showNewLoginEmailPreference = loginEmailEnabled && allowLoginEmailPreference; const showCalendarPreference = useSetting('Outlook_Calendar_Enabled'); const showMobileRinging = useSetting('VideoConf_Mobile_Ringing'); + const notify = useNotification(); const userEmailNotificationMode = useUserPreference('emailNotificationMode') as keyof typeof emailNotificationOptionsLabelMap; useEffect(() => setNotificationsPermission(window.Notification && Notification.permission), []); const onSendNotification = useCallback(() => { - KonchatNotification.notify({ + notify({ payload: { sender: { _id: 'rocket.cat', username: 'rocket.cat' }, rid: 'GENERAL' } as INotificationDesktop['payload'], title: t('Desktop_Notification_Test'), text: t('This_is_a_desktop_notification'), }); - }, [t]); + }, [notify, t]); const onAskNotificationPermission = useCallback(() => { window.Notification && Notification.requestPermission().then((val) => setNotificationsPermission(val)); From c7fe52687154d7916aa865dd758e80a77604b040 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 24 Mar 2025 18:11:10 -0300 Subject: [PATCH 026/187] chore: Prevent `useEmojiOne` operations from running every render (#35587) --- .../client/hooks/useEmojiOne.ts | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts b/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts index 6dfd82c17cfb5..6264f361b6712 100644 --- a/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts +++ b/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts @@ -1,6 +1,7 @@ import { useUserPreference } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; +import { queueMicrotask } from '../../../../client/lib/utils/queueMicrotask'; import { emoji } from '../../../emoji/client'; import { getEmojiConfig } from '../../lib/getEmojiConfig'; import { isSetNotNull } from '../../lib/isSetNotNull'; @@ -10,28 +11,32 @@ const config = getEmojiConfig(); export const useEmojiOne = () => { const convertAsciiToEmoji = useUserPreference('convertAsciiEmoji', true); - emoji.packages.emojione = config.emojione as any; - if (emoji.packages.emojione) { - emoji.packages.emojione.sprites = config.sprites; - emoji.packages.emojione.emojisByCategory = config.emojisByCategory; - emoji.packages.emojione.emojiCategories = config.emojiCategories as any; - emoji.packages.emojione.toneList = config.toneList; - - emoji.packages.emojione.render = config.render; - emoji.packages.emojione.renderPicker = config.renderPicker; - - // RocketChat.emoji.list is the collection of emojis from all emoji packages - for (const [key, currentEmoji] of Object.entries(config.emojione.emojioneList)) { - currentEmoji.emojiPackage = 'emojione'; - emoji.list[key] = currentEmoji; - - if (currentEmoji.shortnames) { - currentEmoji.shortnames.forEach((shortname: string) => { - emoji.list[shortname] = currentEmoji; - }); + useEffect(() => { + queueMicrotask(() => { + emoji.packages.emojione = config.emojione as any; + if (emoji.packages.emojione) { + emoji.packages.emojione.sprites = config.sprites; + emoji.packages.emojione.emojisByCategory = config.emojisByCategory; + emoji.packages.emojione.emojiCategories = config.emojiCategories as any; + emoji.packages.emojione.toneList = config.toneList; + + emoji.packages.emojione.render = config.render; + emoji.packages.emojione.renderPicker = config.renderPicker; + + // RocketChat.emoji.list is the collection of emojis from all emoji packages + for (const [key, currentEmoji] of Object.entries(config.emojione.emojioneList)) { + currentEmoji.emojiPackage = 'emojione'; + emoji.list[key] = currentEmoji; + + if (currentEmoji.shortnames) { + currentEmoji.shortnames.forEach((shortname: string) => { + emoji.list[shortname] = currentEmoji; + }); + } + } } - } - } + }); + }, []); useEffect(() => { if (emoji.packages.emojione) { // Additional settings -- ascii emojis From 5cd142b798ec1dca3012e886d15d2984de087ea3 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 25 Mar 2025 12:12:40 -0300 Subject: [PATCH 027/187] chore: Replace non-reactive collection usage with a `Map` (#35598) --- .../client/popup/messagePopupConfig.ts | 17 ++++----- .../room/providers/ComposerPopupProvider.tsx | 36 +++++-------------- 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/apps/meteor/app/ui-message/client/popup/messagePopupConfig.ts b/apps/meteor/app/ui-message/client/popup/messagePopupConfig.ts index 1d220f8bce8d9..1bd9f0171c610 100644 --- a/apps/meteor/app/ui-message/client/popup/messagePopupConfig.ts +++ b/apps/meteor/app/ui-message/client/popup/messagePopupConfig.ts @@ -1,19 +1,20 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; -import { Mongo } from 'meteor/mongo'; import { Tracker } from 'meteor/tracker'; import { RoomManager } from '../../../../client/lib/RoomManager'; import { asReactiveSource } from '../../../../client/lib/tracker'; import { Messages } from '../../../models/client'; -export const usersFromRoomMessages = new Mongo.Collection<{ +type UserFromRoomMessage = { _id: string; username: string; name: string | undefined; ts: Date; suggestion?: boolean; -}>(null); +}; + +export const usersFromRoomMessages = new Map(); Meteor.startup(() => { Tracker.autorun(() => { @@ -27,7 +28,7 @@ Meteor.startup(() => { return; } - usersFromRoomMessages.remove({}); + usersFromRoomMessages.clear(); const uniqueMessageUsersControl: Record = {}; @@ -53,13 +54,13 @@ Meteor.startup(() => { uniqueMessageUsersControl[username] = true; return notMapped; }) - .forEach(({ u: { username, name, _id }, ts }) => - usersFromRoomMessages.upsert(_id, { + .forEach(({ u: { username, name, _id }, ts }) => { + usersFromRoomMessages.set(_id, { _id, username, name, ts, - }), - ); + }); + }); }); }); diff --git a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx index 6293a401da8b0..42bdb49f43ae8 100644 --- a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx +++ b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx @@ -64,20 +64,10 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = const filterRegex = filter && new RegExp(escapeRegExp(filter), 'i'); const items: ComposerBoxPopupUserProps[] = []; - const users = usersFromRoomMessages - .find( - { - ts: { $exists: true }, - ...(filter && { - $or: [{ username: filterRegex }, { name: filterRegex }], - }), - }, - { - limit: suggestionsCount ?? 5, - sort: { ts: -1 }, - }, - ) - .fetch() + const users = Array.from(usersFromRoomMessages.values()) + .filter((u) => !!u.ts && (filterRegex ? u.username.match(filterRegex) || u.name?.match(filterRegex) : true)) + .sort((a, b) => b.ts.getTime() - a.ts.getTime()) + .slice(0, suggestionsCount ?? 5) .map((u) => { u.suggestion = true; return u; @@ -106,20 +96,10 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = }, getItemsFromServer: async (filter: string) => { const filterRegex = filter && new RegExp(escapeRegExp(filter), 'i'); - const usernames = usersFromRoomMessages - .find( - { - ts: { $exists: true }, - ...(filter && { - $or: [{ username: filterRegex }, { name: filterRegex }], - }), - }, - { - limit: suggestionsCount ?? 5, - sort: { ts: -1 }, - }, - ) - .fetch() + const usernames = Array.from(usersFromRoomMessages.values()) + .filter((u) => !!u.ts && (filterRegex ? u.username.match(filterRegex) || u.name?.match(filterRegex) : true)) + .sort((a, b) => b.ts.getTime() - a.ts.getTime()) + .slice(0, suggestionsCount ?? 5) .map((u) => { return u.username; }); From 47ae69912cd90743e7bf836fdee4be481a01bbba Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:09:23 -0300 Subject: [PATCH 028/187] feat: Audit users.update API endpoint (#34494) --- .changeset/slow-ravens-tie.md | 8 + apps/meteor/app/api/server/v1/users.ts | 11 +- .../lib/server/functions/saveUser/saveUser.ts | 35 ++- apps/meteor/jest.config.ts | 1 + .../lib/auditServerEvents/userChanged.spec.ts | 297 ++++++++++++++++++ .../lib/auditServerEvents/userChanged.ts | 168 ++++++++++ .../src/ServerAudit/IAuditUserChangedEvent.ts | 36 +++ packages/core-typings/src/index.ts | 2 + packages/model-typings/src/updater.ts | 1 + packages/models/src/updater.ts | 6 +- 10 files changed, 557 insertions(+), 8 deletions(-) create mode 100644 .changeset/slow-ravens-tie.md create mode 100644 apps/meteor/server/lib/auditServerEvents/userChanged.spec.ts create mode 100644 apps/meteor/server/lib/auditServerEvents/userChanged.ts create mode 100644 packages/core-typings/src/ServerAudit/IAuditUserChangedEvent.ts diff --git a/.changeset/slow-ravens-tie.md b/.changeset/slow-ravens-tie.md new file mode 100644 index 0000000000000..8714790f332bd --- /dev/null +++ b/.changeset/slow-ravens-tie.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +"@rocket.chat/model-typings": minor +"@rocket.chat/models": minor +--- + +Implements auditing events for `/v1/users.update` API endpoint diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 3018d3fa7d6ea..926ae3415d4a9 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -28,6 +28,7 @@ import type { Filter } from 'mongodb'; import { generatePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/generateToken'; import { regeneratePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/regenerateToken'; import { removePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/removeToken'; +import { UserChangedAuditStore } from '../../../../server/lib/auditServerEvents/userChanged'; import { i18n } from '../../../../server/lib/i18n'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; import { sendWelcomeEmail } from '../../../../server/lib/sendWelcomeEmail'; @@ -114,8 +115,14 @@ API.v1.addRoute( if (userData.name && !validateNameChars(userData.name)) { return API.v1.failure('Name contains invalid characters'); } + const auditStore = new UserChangedAuditStore({ + _id: this.user._id, + ip: this.requestIp, + useragent: this.request.headers['user-agent'] || '', + username: this.user.username || '', + }); - await saveUser(this.userId, userData); + await saveUser(this.userId, userData, { auditStore }); if (typeof this.bodyParams.data.active !== 'undefined') { const { @@ -123,9 +130,9 @@ API.v1.addRoute( data: { active }, confirmRelinquish, } = this.bodyParams; - await executeSetUserActiveStatus(this.userId, userId, active, Boolean(confirmRelinquish)); } + const { fields } = await this.parseJsonQuery(); const user = await Users.findOneById(this.bodyParams.userId, { projection: fields }); diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts index 00baff3135d58..13884b6e34b9b 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts @@ -1,4 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; +import { MeteorError } from '@rocket.chat/core-services'; import { isUserFederated } from '@rocket.chat/core-typings'; import type { IUser, IRole, IUserSettings, RequiredField } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; @@ -7,6 +8,7 @@ import type { ClientSession } from 'mongodb'; import { callbacks } from '../../../../../lib/callbacks'; import { wrapInSessionTransaction, onceTransactionCommitedSuccessfully } from '../../../../../server/database/utils'; +import type { UserChangedAuditStore } from '../../../../../server/lib/auditServerEvents/userChanged'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { safeGetMeteorUser } from '../../../../utils/server/functions/safeGetMeteorUser'; import { generatePassword } from '../../lib/generatePassword'; @@ -50,12 +52,17 @@ export type SaveUserData = { sendWelcomeEmail?: boolean; customFields?: Record; + active?: boolean; }; export type UpdateUserData = RequiredField; export const isUpdateUserData = (params: SaveUserData): params is UpdateUserData => '_id' in params && !!params._id; +type SaveUserOptions = { + auditStore?: UserChangedAuditStore; +}; + const _saveUser = (session?: ClientSession) => - async function (userId: IUser['_id'], userData: SaveUserData) { + async function (userId: IUser['_id'], userData: SaveUserData, options?: SaveUserOptions) { const oldUserData = userData._id && (await Users.findOneById(userData._id)); if (oldUserData && isUserFederated(oldUserData)) { throw new Meteor.Error('Edit_Federated_User_Not_Allowed', 'Not possible to edit a federated user'); @@ -81,10 +88,18 @@ const _saveUser = (session?: ClientSession) => } if (!isUpdateUserData(userData)) { - // pass session? + // TODO audit new users return saveNewUser(userData, sendPassword); } + if (!oldUserData) { + throw new MeteorError('error-user-not-found', 'User not found', { + method: 'saveUser', + }); + } + + options?.auditStore?.setOriginalUser(oldUserData); + await validateUserEditing(userId, userData); // update user @@ -164,6 +179,16 @@ const _saveUser = (session?: ClientSession) => await Users.updateFromUpdater({ _id: userData._id }, updater, { session }); await onceTransactionCommitedSuccessfully(async () => { + if (session && options?.auditStore) { + // setting this inside here to avoid moving `executeSetUserActiveStatus` from the endpoint fn + // updater will be commited by this point, so it won't affect the external user activation/deactivation + if (userData.active !== undefined) { + updater.set('active', userData.active); + } + options.auditStore.setUpdateFilter(updater.getRawUpdateFilter()); + void options.auditStore.commitAuditEvent(); + } + // App IPostUserUpdated event hook // We need to pass the session here to ensure this record is fetched // with the uncommited transaction data. @@ -209,5 +234,9 @@ export const saveUser = (() => { if (!process.env.DEBUG_DISABLE_USER_AUDIT) { return wrapInSessionTransaction(_saveUser); } - return _saveUser(); + + const saveUserNoSession = _saveUser(); + return function saveUser(userId: IUser['_id'], userData: SaveUserData, _options?: any) { + return saveUserNoSession(userId, userData); + }; })(); diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index 5173506d9492b..c3a578ef18fe1 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -38,6 +38,7 @@ export default { '/ee/server/patches/**/*.spec.ts', '/app/cloud/server/functions/supportedVersionsToken/**.spec.ts', '/app/utils/lib/**.spec.ts', + '/server/lib/auditServerEvents/**.spec.ts', '/app/api/server/**.spec.ts', '/app/api/server/middlewares/**.spec.ts', ], diff --git a/apps/meteor/server/lib/auditServerEvents/userChanged.spec.ts b/apps/meteor/server/lib/auditServerEvents/userChanged.spec.ts new file mode 100644 index 0000000000000..4d293b61152f8 --- /dev/null +++ b/apps/meteor/server/lib/auditServerEvents/userChanged.spec.ts @@ -0,0 +1,297 @@ +import { faker } from '@faker-js/faker'; +import type { IAuditServerUserActor, IUser } from '@rocket.chat/core-typings'; +import type { Updater } from '@rocket.chat/models'; +import { UpdaterImpl } from '@rocket.chat/models'; + +import { UserChangedAuditStore } from './userChanged'; +import { createFakeUser } from '../../../tests/mocks/data'; + +const makeFakeActor = (): Omit => { + return { + ip: faker.internet.ip(), + useragent: faker.internet.userAgent(), + _id: faker.database.mongodbObjectId(), + username: faker.internet.userName(), + }; +}; + +const createUserAndUpdater = (overrides?: Partial): [IUser, Updater, Omit] => { + const originalUser = createFakeUser(overrides); + const updater = new UpdaterImpl(); + + const actor = makeFakeActor(); + + return [originalUser, updater, actor]; +}; + +const createEmailsField = (address?: string, verified = true) => { + return { + emails: [ + { + address: address || faker.internet.email(), + verified, + }, + ], + }; +}; + +jest.mock('@rocket.chat/models', () => { + return { + UpdaterImpl: jest.requireActual('@rocket.chat/models').UpdaterImpl, + ServerEvents: { + createAuditServerEvent: (...args: any) => args, + }, + }; +}); + +const createObfuscatedFields = (_2faEnabled = true): Pick => { + return { + services: { + password: { + bcrypt: faker.string.uuid(), + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this field is not in IUser, but is present in DB + enroll: { + token: faker.string.uuid(), + email: faker.internet.email(), + when: faker.date.past(), + reason: 'enroll', + }, + }, + email2fa: { + enabled: _2faEnabled, + changedAt: faker.date.past(), + }, + email: { + verificationTokens: [ + { + token: faker.string.uuid(), + address: faker.internet.email(), + when: faker.date.past(), + }, + ], + }, + resume: { + loginTokens: [ + { + when: faker.date.past(), + hashedToken: faker.string.uuid(), + twoFactorAuthorizedHash: faker.string.uuid(), + twoFactorAuthorizedUntil: faker.date.past(), + }, + ], + }, + }, + e2e: { + private_key: faker.string.uuid(), + public_key: faker.string.uuid(), + }, + inviteToken: faker.string.uuid(), + oauth: { authorizedClients: [faker.string.uuid(), faker.string.uuid()] }, + }; +}; + +const getObfuscatedFields = (email2faState: { enabled: boolean; changedAt: Date }) => ({ + e2e: '****', + oauth: '****', + inviteToken: '****', + services: getObfuscatedServices(email2faState), +}); + +const getObfuscatedServices = (email2faState: { enabled: boolean; changedAt: Date }) => { + return { + password: '****', + email2fa: email2faState, + email: '****', + resume: '****', + }; +}; + +describe('userChanged audit module', () => { + it('should build event with only name and username fields', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField() }); + + const store = new UserChangedAuditStore(actor); + + const [newUsername, newName] = [faker.internet.userName(), faker.person.fullName()]; + + updater.set('username', newUsername); + updater.set('name', newName); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + user_data: { username: user.username, name: user.name }, + operation: { $set: { username: newUsername, name: newName } }, + }, + { ...actor, type: 'user' }, + ]); + }); + + it('should build event with only emails field', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField() }); + + const store = new UserChangedAuditStore(actor); + + const newEmailsField = createEmailsField(); + + updater.set('emails', newEmailsField.emails); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + operation: { $set: { ...newEmailsField } }, + user_data: { emails: user.emails }, + }, + { ...actor, type: 'user' }, + ]); + }); + + it('should build event with every changed field', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField(), active: false }); + + const changes = { + ...createFakeUser(), + ...createEmailsField(), + type: 'bot', + active: true, + }; + + Object.entries(changes).forEach(([key, value]: any) => { + updater.set(key, value); + }); + + updater.addToSet('roles', 'user'); + updater.addToSet('roles', 'bot'); + + const store = new UserChangedAuditStore(actor); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + user_data: user, + operation: { + $set: { + ...changes, + }, + $addToSet: { + roles: { $each: ['user', 'bot'] }, + }, + }, + }, + { + ...actor, + type: 'user', + }, + ]); + }); + it('should obfuscate sensitive fields', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField(), ...createObfuscatedFields(false), active: false }); + + const store = new UserChangedAuditStore(actor); + + const changes = { + ...createFakeUser(), + ...createEmailsField(), + ...createObfuscatedFields(true), + type: 'bot', + active: true, + }; + + Object.entries(changes).forEach(([key, value]: any) => { + updater.set(key, value); + }); + + updater.addToSet('roles', 'user'); + updater.addToSet('roles', 'bot'); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + user_data: { ...user, ...getObfuscatedFields(user.services?.email2fa as any) }, + operation: { + $set: { + ...changes, + ...getObfuscatedFields(changes.services?.email2fa as any), + }, + $addToSet: { + roles: { $each: ['user', 'bot'] }, + }, + }, + }, + { + ...actor, + type: 'user', + }, + ]); + }); + it('should obfuscate nested services', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField(), ...createObfuscatedFields(false), active: false }); + + const store = new UserChangedAuditStore(actor); + + updater.set('services.password.bcrypt', faker.string.uuid()); + updater.set('services.resume.loginTokens', faker.string.uuid()); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + user_data: { services: { password: '****', resume: '****' } }, + operation: { $set: { 'services.password.bcrypt': '****', 'services.resume.loginTokens': '****' } }, + }, + { ...actor, type: 'user' }, + ]); + }); + it('should obfuscate all services when they are set at once', async () => { + const [user, updater, actor] = createUserAndUpdater({ ...createEmailsField(), ...createObfuscatedFields(false), active: false }); + + const store = new UserChangedAuditStore(actor); + + updater.set('services', { password: { bcrypt: faker.string.uuid() } }); + + store.setOriginalUser(user as IUser); + store.setUpdateFilter(updater.getUpdateFilter()); + + const event = await store.commitAuditEvent(); + + expect(event).toEqual([ + 'user.changed', + { + user: { _id: user._id, username: user.username }, + user_data: { services: getObfuscatedServices(user.services?.email2fa as any) }, + operation: { $set: { services: { password: '****' } } }, + }, + { ...actor, type: 'user' }, + ]); + }); +}); diff --git a/apps/meteor/server/lib/auditServerEvents/userChanged.ts b/apps/meteor/server/lib/auditServerEvents/userChanged.ts new file mode 100644 index 0000000000000..ea71794952da8 --- /dev/null +++ b/apps/meteor/server/lib/auditServerEvents/userChanged.ts @@ -0,0 +1,168 @@ +import type { IAuditServerUserActor, IServerEvents, ExtractDataToParams, IUser } from '@rocket.chat/core-typings'; +import { ServerEvents } from '@rocket.chat/models'; +import type { UpdateFilter } from 'mongodb'; + +const userKeysToObfuscate = ['authorizedClients', 'e2e', 'inviteToken', 'oauth']; +const nestableKeysToObfuscate = ['services', 'password', 'bcrypt']; // ex: services.password.bcrypt + +const obfuscateServices = (services: Record): Record => { + return Object.fromEntries( + Object.keys(services).map((key) => { + // Email 2FA is okay, only tells if it's enabled + if (key === 'email2fa') { + return [key, services[key]]; + } + return [key, '****']; + }), + ); +}; +export class UserChangedAuditStore { + private originalUser: Partial | undefined; + + private updateFilter: UpdateFilter | undefined; + + private actor: IAuditServerUserActor; + + constructor(actor: Omit, type: IAuditServerUserActor['type'] = 'user') { + this.actor = { ...actor, type }; + } + + public setOriginalUser(user: Partial) { + this.originalUser = user; + } + + public setUpdateFilter(updateFilter: UpdateFilter) { + this.updateFilter = Object.fromEntries( + Object.entries(updateFilter).map(([key, value]) => { + const obfuscatedValue = Object.entries(value).reduce((acc, [k, v]) => { + if (userKeysToObfuscate.includes(k)) { + return { + ...acc, + [k]: '****', + }; + } + + // In case all services are set at once, we need to obfuscate them + if (k === 'services') { + return { + ...acc, + [k]: obfuscateServices(v as Record), + }; + } + + if (nestableKeysToObfuscate.some((key) => k.includes(key))) { + return { + ...acc, + [k]: '****', + }; + } + + return { ...acc, [k]: v }; + }, {}); + + return [key, obfuscatedValue]; + }), + ); + } + + private filterUserChangedProperties(originalUser: Partial, updateFilter: UpdateFilter): Partial { + if (Object.keys(updateFilter).length === 0) { + return {}; + } + + // extract keys from updateFilter (keys are nested in $set, $unset, $inc, etc) + const updateFilterKeys: string[] = Object.values(updateFilter).reduce((acc, current) => { + const keys = Object.keys(current); + if (keys.length === 0) { + return acc; + } + return [...acc, ...keys]; + }, []); + + return Object.entries(originalUser).reduce((acc, [key, value]) => { + if (!updateFilterKeys.some((k) => k.includes(key))) { + return acc; + } + + if (userKeysToObfuscate.includes(key)) { + return { + ...acc, + [key]: '****', + }; + } + + if (key === 'services') { + // In case all services are set at once we should + // obfuscate all user services, because they'll all change + if (updateFilterKeys.some((k) => k === 'services')) { + return { + ...acc, + [key]: obfuscateServices(value as Record), + }; + } + + const changedNestedServices = updateFilterKeys + .filter((k) => k.includes(key) && k.includes('.')) + .map((serviceKey) => { + // service key can be nested with dot notation + // ex: services.password.bcrypt + const serviceKeyParts = serviceKey.split('.'); + return [serviceKeyParts[1], value[serviceKey as keyof typeof value]]; + }) + .filter(Boolean); + + if (!changedNestedServices.length) { + return acc; + } + + return { + ...acc, + [key]: obfuscateServices(Object.fromEntries(changedNestedServices) as Record), + }; + } + + return { + ...acc, + [key]: value, + }; + }, {}); + } + + private getEventData( + originalUser: Partial, + updateFilter: UpdateFilter, + ): ExtractDataToParams { + const userData = this.filterUserChangedProperties(originalUser, updateFilter); + + return { + user: { _id: originalUser._id || '', username: originalUser.username }, + user_data: userData, + operation: updateFilter, + }; + } + + private buildEvent(): ['user.changed', ExtractDataToParams, IAuditServerUserActor] { + if (!this.updateFilter) { + throw new Error('UserChangedAuditStore - Updater is undefined'); + } + + if (!this.originalUser) { + throw new Error('UserChangedAuditStore - OriginalUser is undefined'); + } + + const eventData = this.getEventData(this.originalUser, this.updateFilter); + + if (Object.keys(eventData.user_data).length === 0 || Object.keys(eventData.operation).length === 0) { + // UpdaterImpl throws an error when trying to build the filter if no changes are detected + // so we should never get here + throw new Error('UserChangedAuditStore - No changes detected'); + } + + return ['user.changed', eventData, this.actor]; + } + + public async commitAuditEvent() { + const event = this.buildEvent(); + return ServerEvents.createAuditServerEvent(...event); + } +} diff --git a/packages/core-typings/src/ServerAudit/IAuditUserChangedEvent.ts b/packages/core-typings/src/ServerAudit/IAuditUserChangedEvent.ts new file mode 100644 index 0000000000000..4218dcbb7c311 --- /dev/null +++ b/packages/core-typings/src/ServerAudit/IAuditUserChangedEvent.ts @@ -0,0 +1,36 @@ +import type { UpdateFilter } from 'mongodb'; + +import type { IAuditServerEventType } from '../IServerEvent'; +import type { IUser } from '../IUser'; +import type { DeepPartial } from '../utils'; + +export type IServerEventAuditedUser = IUser & { + password: string; +}; + +interface IServerEventUserChanged + extends IAuditServerEventType< + | { + key: 'user'; + value: { + _id: IUser['_id']; + username: IUser['username']; + }; + } + | { + key: 'user_data'; + value: DeepPartial; + } + | { + key: 'operation'; + value: UpdateFilter; + } + > { + t: 'user.changed'; +} + +declare module '../IServerEvent' { + interface IServerEvents { + 'user.changed': IServerEventUserChanged; + } +} diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 6908b126aa435..5cb20c24ef859 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -1,5 +1,7 @@ import './ServerAudit/IAuditServerSettingEvent'; +import './ServerAudit/IAuditUserChangedEvent'; +export * from './ServerAudit/IAuditUserChangedEvent'; export * from './Apps'; export * from './AppOverview'; export * from './FeaturedApps'; diff --git a/packages/model-typings/src/updater.ts b/packages/model-typings/src/updater.ts index 7743430ad4b8b..d9ec9a84f57bf 100644 --- a/packages/model-typings/src/updater.ts +++ b/packages/model-typings/src/updater.ts @@ -8,6 +8,7 @@ export interface Updater { addToSet>(key: K, value: ArrayElementType[K]>): Updater; hasChanges(): boolean; getUpdateFilter(): UpdateFilter; + getRawUpdateFilter(): UpdateFilter; } type ArrayElementType = T extends (infer E)[] ? E : T; diff --git a/packages/models/src/updater.ts b/packages/models/src/updater.ts index 4f7ad271f397d..9babf5b5be20f 100644 --- a/packages/models/src/updater.ts +++ b/packages/models/src/updater.ts @@ -46,7 +46,7 @@ export class UpdaterImpl implements Updater { } hasChanges() { - const filter = this._getUpdateFilter(); + const filter = this.getRawUpdateFilter(); return this._hasChanges(filter); } @@ -54,7 +54,7 @@ export class UpdaterImpl implements Updater { return Object.keys(filter).length > 0; } - private _getUpdateFilter() { + public getRawUpdateFilter() { return { ...(this._set && { $set: Object.fromEntries(this._set) }), ...(this._unset && { $unset: Object.fromEntries([...this._unset.values()].map((k) => [k, 1])) }), @@ -68,7 +68,7 @@ export class UpdaterImpl implements Updater { throw new Error('Updater is dirty'); } this.dirty = true; - const filter = this._getUpdateFilter(); + const filter = this.getRawUpdateFilter(); if (!this._hasChanges(filter)) { throw new Error('No changes to update'); } From 0d37ba776b32db6a8286ff057885df709349f4f3 Mon Sep 17 00:00:00 2001 From: Gustavo Reis Bauer Date: Tue, 25 Mar 2025 16:36:36 -0300 Subject: [PATCH 029/187] fix: adding blocks from block builder to the changes field (#35603) --- .changeset/three-olives-explain.md | 5 +++++ .../lib/accessors/builders/MessageBuilder.ts | 10 ++++------ 2 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 .changeset/three-olives-explain.md diff --git a/.changeset/three-olives-explain.md b/.changeset/three-olives-explain.md new file mode 100644 index 0000000000000..79e14c63c8cbc --- /dev/null +++ b/.changeset/three-olives-explain.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/apps-engine": patch +--- + +Fixes an issue where apps where not able to update messages using the BlockBuilder. diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts index d4005e3ec52b3..b549b5174032d 100644 --- a/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts @@ -223,12 +223,10 @@ export class MessageBuilder implements IMessageBuilder { } public setBlocks(blocks: BlockBuilder | Array) { - if (blocks instanceof BlockBuilder) { - this.msg.blocks = blocks.getBlocks(); - } else { - this.msg.blocks = blocks; - this.changes.blocks = blocks; - } + const blockArray: Array = blocks instanceof BlockBuilder ? blocks.getBlocks() : blocks; + + this.msg.blocks = blockArray; + this.changes.blocks = blockArray; return this as IMessageBuilder; } From f271b569e498c11e2d76bfaef35554e62331672e Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 25 Mar 2025 19:03:56 -0300 Subject: [PATCH 030/187] chore: `HeaderState` receiving invalid prop (#35612) --- .../meteor/client/views/room/Header/RoomTitle.tsx | 1 + .../client/views/room/Header/icons/Favorite.tsx | 3 +-- .../client/views/room/HeaderV2/RoomTitle.tsx | 1 + .../views/room/HeaderV2/icons/Encrypted.tsx | 2 +- .../client/views/room/HeaderV2/icons/Favorite.tsx | 3 +-- apps/meteor/tests/e2e/channel-management.spec.ts | 2 +- .../src/components/Header/HeaderState.tsx | 15 +++++++++++++-- .../src/components/HeaderV2/HeaderState.tsx | 8 ++++---- 8 files changed, 23 insertions(+), 12 deletions(-) diff --git a/apps/meteor/client/views/room/Header/RoomTitle.tsx b/apps/meteor/client/views/room/Header/RoomTitle.tsx index 7937051cc2ba3..2ff1a21650156 100644 --- a/apps/meteor/client/views/room/Header/RoomTitle.tsx +++ b/apps/meteor/client/views/room/Header/RoomTitle.tsx @@ -41,6 +41,7 @@ const RoomTitle = ({ room }: { room: IRoom }): ReactElement => { onClick={() => handleOpenRoomInfo()} tabIndex={0} role='button' + mie={4} > {room.name} diff --git a/apps/meteor/client/views/room/Header/icons/Favorite.tsx b/apps/meteor/client/views/room/Header/icons/Favorite.tsx index 3fd30a5d24912..5afbac0355c84 100644 --- a/apps/meteor/client/views/room/Header/icons/Favorite.tsx +++ b/apps/meteor/client/views/room/Header/icons/Favorite.tsx @@ -33,8 +33,7 @@ const Favorite = ({ room: { _id, f: favorite = false, t: type, name } }: { room: title={favoriteLabel} icon={favorite ? 'star-filled' : 'star'} onClick={handleFavoriteClick} - color={favorite ? 'status-font-on-warning' : null} - tiny + color={favorite ? 'status-font-on-warning' : undefined} /> ); }; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx b/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx index 6e45ce2fa94f4..b9910522d59b4 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx @@ -43,6 +43,7 @@ const RoomTitle = ({ room }: RoomTitleProps) => { onClick={() => handleOpenRoomInfo()} tabIndex={0} role='button' + mie={4} > {room.name} diff --git a/apps/meteor/client/views/room/HeaderV2/icons/Encrypted.tsx b/apps/meteor/client/views/room/HeaderV2/icons/Encrypted.tsx index 47a613225ebd1..3e376e3b9d661 100644 --- a/apps/meteor/client/views/room/HeaderV2/icons/Encrypted.tsx +++ b/apps/meteor/client/views/room/HeaderV2/icons/Encrypted.tsx @@ -9,7 +9,7 @@ import { HeaderState } from '../../../../components/Header'; const Encrypted = ({ room }: { room: IRoom }) => { const { t } = useTranslation(); const e2eEnabled = useSetting('E2E_Enable'); - return e2eEnabled && room?.encrypted ? : null; + return e2eEnabled && room?.encrypted ? : null; }; export default memo(Encrypted); diff --git a/apps/meteor/client/views/room/HeaderV2/icons/Favorite.tsx b/apps/meteor/client/views/room/HeaderV2/icons/Favorite.tsx index 3fd30a5d24912..5afbac0355c84 100644 --- a/apps/meteor/client/views/room/HeaderV2/icons/Favorite.tsx +++ b/apps/meteor/client/views/room/HeaderV2/icons/Favorite.tsx @@ -33,8 +33,7 @@ const Favorite = ({ room: { _id, f: favorite = false, t: type, name } }: { room: title={favoriteLabel} icon={favorite ? 'star-filled' : 'star'} onClick={handleFavoriteClick} - color={favorite ? 'status-font-on-warning' : null} - tiny + color={favorite ? 'status-font-on-warning' : undefined} /> ); }; diff --git a/apps/meteor/tests/e2e/channel-management.spec.ts b/apps/meteor/tests/e2e/channel-management.spec.ts index 71c310d9b6f40..f8ff93723c69f 100644 --- a/apps/meteor/tests/e2e/channel-management.spec.ts +++ b/apps/meteor/tests/e2e/channel-management.spec.ts @@ -134,7 +134,7 @@ test.describe.serial('channel-management', () => { targetChannel = hugeName; await page.setViewportSize({ width: 640, height: 460 }); - await expect(page.getByRole('heading', { name: hugeName })).toHaveCSS('width', '419px'); + await expect(page.getByRole('heading', { name: hugeName })).toHaveCSS('width', '411px'); }); test('should open sidebar clicking on sidebar toggler', async ({ page }) => { diff --git a/packages/ui-client/src/components/Header/HeaderState.tsx b/packages/ui-client/src/components/Header/HeaderState.tsx index 01fe2a162129e..5d64f380279c3 100644 --- a/packages/ui-client/src/components/Header/HeaderState.tsx +++ b/packages/ui-client/src/components/Header/HeaderState.tsx @@ -1,6 +1,17 @@ import { Icon, IconButton } from '@rocket.chat/fuselage'; -import type { FC } from 'react'; +import type { Keys as IconName } from '@rocket.chat/icons'; +import type { AllHTMLAttributes, ComponentPropsWithoutRef, FC, MouseEventHandler } from 'react'; -const HeaderState: FC = (props) => (props.onClick ? : ); +type HeaderStateProps = + | (Pick, 'color' | 'title' | 'icon'> & { + onClick: MouseEventHandler; + } & Omit, 'is'>) + | (Omit, 'name' | 'onClick'> & { + icon: IconName; + onClick?: undefined; + }); + +const HeaderState: FC = (props) => + props.onClick ? : ; export default HeaderState; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderState.tsx b/packages/ui-client/src/components/HeaderV2/HeaderState.tsx index a5f29d77b28e4..360f3f67715e8 100644 --- a/packages/ui-client/src/components/HeaderV2/HeaderState.tsx +++ b/packages/ui-client/src/components/HeaderV2/HeaderState.tsx @@ -1,17 +1,17 @@ import { Icon, IconButton } from '@rocket.chat/fuselage'; import type { Keys as IconName } from '@rocket.chat/icons'; -import type { ComponentPropsWithoutRef, MouseEventHandler } from 'react'; +import type { AllHTMLAttributes, ComponentPropsWithoutRef, MouseEventHandler } from 'react'; type HeaderStateProps = - | (Omit, 'onClick'> & { + | (Pick, 'color' | 'title' | 'icon'> & { onClick: MouseEventHandler; - }) + } & Omit, 'is'>) | (Omit, 'name' | 'onClick'> & { icon: IconName; onClick?: undefined; }); const HeaderState = (props: HeaderStateProps) => - props.onClick ? : ; + props.onClick ? : ; export default HeaderState; From 6bf386dcc2a560963cf719fbc2d96569ce23a2de Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 25 Mar 2025 21:38:38 -0300 Subject: [PATCH 031/187] feat: Remove room header avatar (#35615) --- .changeset/hungry-wasps-remember.md | 7 +++++++ apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx | 5 +---- apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx | 6 +----- apps/meteor/tests/e2e/feature-preview.spec.ts | 7 +++++++ packages/ui-client/src/components/HeaderV2/Header.tsx | 2 +- 5 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 .changeset/hungry-wasps-remember.md diff --git a/.changeset/hungry-wasps-remember.md b/.changeset/hungry-wasps-remember.md new file mode 100644 index 0000000000000..7cb6cdd3f77a6 --- /dev/null +++ b/.changeset/hungry-wasps-remember.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-client': minor +'@rocket.chat/meteor': minor +--- + +Removes the avatar in the room header +> This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it diff --git a/apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx b/apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx index 1839a64fb24a2..25d78f466024d 100644 --- a/apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx +++ b/apps/meteor/client/views/room/HeaderV2/HeaderSkeleton.tsx @@ -1,13 +1,10 @@ import { Skeleton } from '@rocket.chat/fuselage'; -import { Header, HeaderAvatar, HeaderContent, HeaderContentRow } from '../../../components/Header'; +import { Header, HeaderContent, HeaderContentRow } from '../../../components/Header'; const HeaderSkeleton = () => { return (
- - - diff --git a/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx index 1683873a7c5cf..87afd65918ddb 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx @@ -1,6 +1,5 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; -import { RoomAvatar } from '@rocket.chat/ui-avatar'; import type { ReactNode } from 'react'; import { Suspense } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,7 +12,7 @@ import RoomToolbox from './RoomToolbox'; import Encrypted from './icons/Encrypted'; import Favorite from './icons/Favorite'; import Translate from './icons/Translate'; -import { Header, HeaderAvatar, HeaderContent, HeaderContentRow, HeaderToolbar } from '../../../components/Header'; +import { Header, HeaderContent, HeaderContentRow, HeaderToolbar } from '../../../components/Header'; export type RoomHeaderProps = { room: IRoom; @@ -38,9 +37,6 @@ const RoomHeader = ({ room, slots = {}, roomToolbox }: RoomHeaderProps) => { return (
{slots?.start} - - - {slots?.preContent} diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index c2c39701d0106..55f51ea62b170 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -171,6 +171,13 @@ test.describe.serial('feature preview', () => { await page.goto(embeddedLayoutURL); await expect(page.locator('role=navigation[name="header"]')).not.toBeVisible(); }); + + test('should not display avatar in room header', async ({ page }) => { + await page.goto('/home'); + + await poHomeChannel.sidebar.openChat(targetChannel); + await expect(page.locator('main').locator('header').getByRole('figure')).not.toBeVisible(); + }); }); test.describe('Sidepanel', () => { diff --git a/packages/ui-client/src/components/HeaderV2/Header.tsx b/packages/ui-client/src/components/HeaderV2/Header.tsx index 4ee887e93cfbd..ee5580b4b7472 100644 --- a/packages/ui-client/src/components/HeaderV2/Header.tsx +++ b/packages/ui-client/src/components/HeaderV2/Header.tsx @@ -12,7 +12,7 @@ const Header = (props: HeaderProps) => { return ( Date: Wed, 26 Mar 2025 12:08:45 -0300 Subject: [PATCH 032/187] chore: expose `LicenseManagerInterface` (#35623) --- ee/packages/license/src/index.ts | 1 + ee/packages/license/src/license.ts | 66 ++++++++++++++++++++++++++- ee/packages/license/src/licenseImp.ts | 37 +-------------- 3 files changed, 68 insertions(+), 36 deletions(-) diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts index bb816ea4183bd..308fab6e1e4bb 100644 --- a/ee/packages/license/src/index.ts +++ b/ee/packages/license/src/index.ts @@ -1,5 +1,6 @@ export { DuplicatedLicenseError } from './errors/DuplicatedLicenseError'; export * from './licenseImp'; +export * from './license'; export * from './MockedLicenseBuilder'; export * from './applyLicense'; export * from './AirGappedRestriction'; diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts index 99433ca1a8e10..677be2bb5112f 100644 --- a/ee/packages/license/src/license.ts +++ b/ee/packages/license/src/license.ts @@ -14,17 +14,33 @@ import type { import { Emitter } from '@rocket.chat/emitter'; import { getLicenseLimit } from './deprecated'; +import type { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { DuplicatedLicenseError } from './errors/DuplicatedLicenseError'; import { InvalidLicenseError } from './errors/InvalidLicenseError'; import { NotReadyForValidation } from './errors/NotReadyForValidation'; +import type { onLicense } from './events/deprecated'; import { behaviorTriggered, behaviorTriggeredToggled, licenseInvalidated, licenseValidated } from './events/emitter'; +import type { + onBehaviorTriggered, + onInvalidFeature, + onInvalidateLicense, + onLimitReached, + onModule, + onToggledFeature, + onValidFeature, + onValidateLicense, +} from './events/listeners'; +import type { overwriteClassOnLicense } from './events/overwriteClassOnLicense'; import { logger } from './logger'; +import type { getModuleDefinition, hasModule } from './modules'; import { getExternalModules, getModules, invalidateAll, replaceModules } from './modules'; import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense'; +import type { getTags } from './tags'; import { replaceTags } from './tags'; import { decrypt } from './token'; import { convertToV3 } from './v2/convertToV3'; import { filterBehaviorsResult } from './validation/filterBehaviorsResult'; +import type { setLicenseLimitCounter } from './validation/getCurrentValueForLicenseLimit'; import { getCurrentValueForLicenseLimit } from './validation/getCurrentValueForLicenseLimit'; import { getModulesToDisable } from './validation/getModulesToDisable'; import { isBehaviorsInResult } from './validation/isBehaviorsInResult'; @@ -36,7 +52,55 @@ import { validateLicenseLimits } from './validation/validateLicenseLimits'; const globalLimitKinds: LicenseLimitKind[] = ['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts']; -export class LicenseManager extends Emitter { +export abstract class LicenseManager extends Emitter { + abstract validateFormat: typeof validateFormat; + + abstract hasModule: typeof hasModule; + + abstract getModules: typeof getModules; + + abstract getModuleDefinition: typeof getModuleDefinition; + + abstract getExternalModules: typeof getExternalModules; + + abstract getTags: typeof getTags; + + abstract overwriteClassOnLicense: typeof overwriteClassOnLicense; + + abstract setLicenseLimitCounter: typeof setLicenseLimitCounter; + + abstract getCurrentValueForLicenseLimit: typeof getCurrentValueForLicenseLimit; + + abstract isLimitReached(action: T, context?: Partial>): Promise; + + abstract onValidFeature: typeof onValidFeature; + + abstract onInvalidFeature: typeof onInvalidFeature; + + abstract onToggledFeature: typeof onToggledFeature; + + abstract onModule: typeof onModule; + + abstract onValidateLicense: typeof onValidateLicense; + + abstract onInvalidateLicense: typeof onInvalidateLicense; + + abstract onLimitReached: typeof onLimitReached; + + abstract onBehaviorTriggered: typeof onBehaviorTriggered; + + // Deprecated: + abstract onLicense: typeof onLicense; + + // Deprecated: + abstract getMaxActiveUsers: typeof getMaxActiveUsers; + + // Deprecated: + abstract getAppsConfig: typeof getAppsConfig; + + // Deprecated: + abstract getUnmodifiedLicenseAndModules: typeof getUnmodifiedLicenseAndModules; + dataCounters = new Map) => Promise>(); pendingLicense = ''; diff --git a/ee/packages/license/src/licenseImp.ts b/ee/packages/license/src/licenseImp.ts index 8305665536501..e5795751942f5 100644 --- a/ee/packages/license/src/licenseImp.ts +++ b/ee/packages/license/src/licenseImp.ts @@ -1,4 +1,4 @@ -import type { LicenseLimitKind, LicenseInfo, LimitContext } from '@rocket.chat/core-typings'; +import type { LicenseLimitKind, LimitContext } from '@rocket.chat/core-typings'; import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated'; import { onLicense } from './events/deprecated'; @@ -27,40 +27,7 @@ import { getCurrentValueForLicenseLimit, setLicenseLimitCounter } from './valida import { validateFormat } from './validation/validateFormat'; // eslint-disable-next-line @typescript-eslint/naming-convention -interface License { - validateFormat: typeof validateFormat; - hasModule: typeof hasModule; - getModules: typeof getModules; - getModuleDefinition: typeof getModuleDefinition; - getExternalModules: typeof getExternalModules; - getTags: typeof getTags; - overwriteClassOnLicense: typeof overwriteClassOnLicense; - setLicenseLimitCounter: typeof setLicenseLimitCounter; - getCurrentValueForLicenseLimit: typeof getCurrentValueForLicenseLimit; - isLimitReached: (action: T, context?: Partial>) => Promise; - onValidFeature: typeof onValidFeature; - onInvalidFeature: typeof onInvalidFeature; - onToggledFeature: typeof onToggledFeature; - onModule: typeof onModule; - onValidateLicense: typeof onValidateLicense; - onInvalidateLicense: typeof onInvalidateLicense; - onLimitReached: typeof onLimitReached; - onBehaviorTriggered: typeof onBehaviorTriggered; - revalidateLicense: () => Promise; - - getInfo: (info: { limits: boolean; currentValues: boolean; license: boolean }) => Promise; - - // Deprecated: - onLicense: typeof onLicense; - // Deprecated: - getMaxActiveUsers: typeof getMaxActiveUsers; - // Deprecated: - getAppsConfig: typeof getAppsConfig; - // Deprecated: - getUnmodifiedLicenseAndModules: typeof getUnmodifiedLicenseAndModules; -} - -export class LicenseImp extends LicenseManager implements License { +export class LicenseImp extends LicenseManager { constructor() { super(); this.onValidateLicense(() => showLicense.call(this, this.getLicense(), this.hasValidLicense())); From dcff0fcbee4fdf568877f36b02c3c2c91b2bfe01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:11:16 -0300 Subject: [PATCH 033/187] refactor: remove deprecated `KonchatNotification` (#35589) --- .../app/ui/client/lib/KonchatNotification.ts | 25 +------------------ apps/meteor/client/definitions/global.d.ts | 4 +++ .../hooks/notification/useNotification.ts | 6 +++-- .../notification/useNotificationAllowed.ts | 19 ++++++++++++++ .../notification/useNotificationPermission.ts | 21 ++++++++++++++++ apps/meteor/client/lib/notificationManager.ts | 6 +++++ .../PreferencesNotificationsSection.tsx | 1 - apps/meteor/client/views/home/HomePage.tsx | 6 ----- apps/meteor/client/views/root/AppLayout.tsx | 2 ++ 9 files changed, 57 insertions(+), 33 deletions(-) create mode 100644 apps/meteor/client/hooks/notification/useNotificationAllowed.ts create mode 100644 apps/meteor/client/hooks/notification/useNotificationPermission.ts create mode 100644 apps/meteor/client/lib/notificationManager.ts diff --git a/apps/meteor/app/ui/client/lib/KonchatNotification.ts b/apps/meteor/app/ui/client/lib/KonchatNotification.ts index 3713395084cad..9d6f3dc6c9c1f 100644 --- a/apps/meteor/app/ui/client/lib/KonchatNotification.ts +++ b/apps/meteor/app/ui/client/lib/KonchatNotification.ts @@ -1,24 +1 @@ -import { ReactiveVar } from 'meteor/reactive-var'; - -declare global { - // eslint-disable-next-line @typescript-eslint/naming-convention - interface NotificationEventMap { - reply: { response: string }; - } -} - -class KonchatNotification { - public notificationStatus = new ReactiveVar(undefined); - - public getDesktopPermission() { - if (window.Notification && Notification.permission !== 'granted') { - return Notification.requestPermission((status) => { - this.notificationStatus.set(status); - }); - } - } -} - -const instance = new KonchatNotification(); - -export { instance as KonchatNotification }; +// KonchatNotification in memoriam diff --git a/apps/meteor/client/definitions/global.d.ts b/apps/meteor/client/definitions/global.d.ts index 0916ef237119a..58e383ee58d8b 100644 --- a/apps/meteor/client/definitions/global.d.ts +++ b/apps/meteor/client/definitions/global.d.ts @@ -80,4 +80,8 @@ declare global { maxHeight: number; }; } + + interface NotificationEventMap { + reply: { response: string }; + } } diff --git a/apps/meteor/client/hooks/notification/useNotification.ts b/apps/meteor/client/hooks/notification/useNotification.ts index 96fe22ffdd6b6..ce20f90c8c3c5 100644 --- a/apps/meteor/client/hooks/notification/useNotification.ts +++ b/apps/meteor/client/hooks/notification/useNotification.ts @@ -3,6 +3,7 @@ import { Random } from '@rocket.chat/random'; import { useRouter, useUserPreference } from '@rocket.chat/ui-contexts'; import { useCallback } from 'react'; +import { useNotificationAllowed } from './useNotificationAllowed'; import { getUserAvatarURL } from '../../../app/utils/client'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { stripTags } from '../../../lib/utils/stringUtils'; @@ -11,10 +12,11 @@ import { onClientMessageReceived } from '../../lib/onClientMessageReceived'; export const useNotification = () => { const requireInteraction = useUserPreference('desktopNotificationRequireInteraction'); const router = useRouter(); + const notificationAllowed = useNotificationAllowed(); const notify = useCallback( async (notification: INotificationDesktop) => { - if (typeof window.Notification === 'undefined' || Notification.permission !== 'granted') { + if (!notificationAllowed) { return; } if (!notification.payload) { @@ -116,7 +118,7 @@ export const useNotification = () => { } }; }, - [requireInteraction, router], + [notificationAllowed, requireInteraction, router], ); return notify; }; diff --git a/apps/meteor/client/hooks/notification/useNotificationAllowed.ts b/apps/meteor/client/hooks/notification/useNotificationAllowed.ts new file mode 100644 index 0000000000000..31f17b0cb0e1d --- /dev/null +++ b/apps/meteor/client/hooks/notification/useNotificationAllowed.ts @@ -0,0 +1,19 @@ +import { useCallback, useSyncExternalStore } from 'react'; + +import { notificationManager } from '../../lib/notificationManager'; + +export const useNotificationAllowed = (): boolean => { + const allowed = useSyncExternalStore( + useCallback( + (callback): (() => void) => + notificationManager.on('change', () => { + notificationManager.allowed = Notification.permission === 'granted'; + callback(); + }), + [], + ), + (): boolean => notificationManager.allowed, + ); + + return allowed; +}; diff --git a/apps/meteor/client/hooks/notification/useNotificationPermission.ts b/apps/meteor/client/hooks/notification/useNotificationPermission.ts new file mode 100644 index 0000000000000..4f021b7c15611 --- /dev/null +++ b/apps/meteor/client/hooks/notification/useNotificationPermission.ts @@ -0,0 +1,21 @@ +import { useCallback } from 'react'; + +import { notificationManager } from '../../lib/notificationManager'; + +export const useNotificationPermission = () => { + const requestPermission = useCallback(async () => { + const response = await Notification.requestPermission(); + notificationManager.allowed = response === 'granted'; + notificationManager.emit('change'); + + const notifications = await navigator.permissions.query({ name: 'notifications' }); + notifications.onchange = () => { + notificationManager.allowed = notifications.state === 'granted'; + notificationManager.emit('change'); + }; + }, []); + + if ('Notification' in window) { + requestPermission(); + } +}; diff --git a/apps/meteor/client/lib/notificationManager.ts b/apps/meteor/client/lib/notificationManager.ts new file mode 100644 index 0000000000000..4ee07ed7cea51 --- /dev/null +++ b/apps/meteor/client/lib/notificationManager.ts @@ -0,0 +1,6 @@ +import { Emitter } from '@rocket.chat/emitter'; + +class NotificationPermissionEmitter extends Emitter { + allowed: boolean; +} +export const notificationManager = new NotificationPermissionEmitter(); diff --git a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx index f95696afb7d41..8d159a81e7a5d 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesNotificationsSection.tsx @@ -20,7 +20,6 @@ const emailNotificationOptionsLabelMap = { nothing: 'Email_Notification_Mode_Disabled', }; -// TODO: Test Notification Button not working const PreferencesNotificationsSection = () => { const { t, i18n } = useTranslation(); diff --git a/apps/meteor/client/views/home/HomePage.tsx b/apps/meteor/client/views/home/HomePage.tsx index a8a0544e3d870..1cc4654eeea12 100644 --- a/apps/meteor/client/views/home/HomePage.tsx +++ b/apps/meteor/client/views/home/HomePage.tsx @@ -1,16 +1,10 @@ import { useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import { useEffect } from 'react'; import CustomHomePage from './CustomHomePage'; import DefaultHomePage from './DefaultHomePage'; -import { KonchatNotification } from '../../../app/ui/client/lib/KonchatNotification'; const HomePage = (): ReactElement => { - useEffect(() => { - KonchatNotification.getDesktopPermission(); - }, []); - const customOnly = useSetting('Layout_Custom_Body_Only'); if (customOnly) { diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 656255baa495d..fe96a49696017 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -19,6 +19,7 @@ import { useGitLabAuth } from '../../../app/gitlab/client/hooks/useGitLabAuth'; import { useLivechatEnterprise } from '../../../app/livechat-enterprise/hooks/useLivechatEnterprise'; import { useNextcloud } from '../../../app/nextcloud/client/useNextcloud'; import { useTokenPassAuth } from '../../../app/tokenpass/client/hooks/useTokenPassAuth'; +import { useNotificationPermission } from '../../hooks/notification/useNotificationPermission'; import { useNotifyUser } from '../../hooks/notification/useNotifyUser'; import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking'; import { useAutoupdate } from '../../hooks/useAutoupdate'; @@ -43,6 +44,7 @@ const AppLayout = () => { useAnalyticsEventTracking(); useLoadRoomForAllowedAnonymousRead(); useNotifyUser(); + useNotificationPermission(); useEmojiOne(); useRedirectToSetupWizard(); useSettingsOnLoadSiteUrl(); From 26bde5caa4c7b6388cb8721e4fb21d3fef8e5642 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 26 Mar 2025 12:33:17 -0300 Subject: [PATCH 034/187] chore: Replace `RoomRoles` and `UserRoles` (#35599) --- apps/meteor/app/models/client/index.ts | 4 - .../app/models/client/models/RoomRoles.ts | 5 - .../app/models/client/models/UserRoles.ts | 9 - .../message/header/hooks/useMessageRoles.ts | 31 +-- apps/meteor/client/hooks/useRoomRolesQuery.ts | 122 ++++++++++++ apps/meteor/client/hooks/useUserRolesQuery.ts | 92 +++++++++ .../client/lib/federation/Federation.spec.ts | 185 +++++++++--------- .../client/lib/federation/Federation.ts | 10 +- apps/meteor/client/lib/queryKeys.ts | 6 + apps/meteor/client/startup/index.ts | 1 - apps/meteor/client/startup/userRoles.ts | 53 ----- .../room/body/hooks/useRoomRolesManagement.ts | 96 --------- .../views/room/hooks/useUserHasRoomRole.ts | 11 +- .../views/room/providers/RoomProvider.tsx | 3 - 14 files changed, 350 insertions(+), 278 deletions(-) delete mode 100644 apps/meteor/app/models/client/models/RoomRoles.ts delete mode 100644 apps/meteor/app/models/client/models/UserRoles.ts create mode 100644 apps/meteor/client/hooks/useRoomRolesQuery.ts create mode 100644 apps/meteor/client/hooks/useUserRolesQuery.ts delete mode 100644 apps/meteor/client/startup/userRoles.ts delete mode 100644 apps/meteor/client/views/room/body/hooks/useRoomRolesManagement.ts diff --git a/apps/meteor/app/models/client/index.ts b/apps/meteor/app/models/client/index.ts index e719791066154..d985275faaa8f 100644 --- a/apps/meteor/app/models/client/index.ts +++ b/apps/meteor/app/models/client/index.ts @@ -3,18 +3,14 @@ import { CachedChatSubscription } from './models/CachedChatSubscription'; import { Messages } from './models/Messages'; import { AuthzCachedCollection, Permissions } from './models/Permissions'; import { Roles } from './models/Roles'; -import { RoomRoles } from './models/RoomRoles'; import { Rooms } from './models/Rooms'; import { Subscriptions } from './models/Subscriptions'; -import { UserRoles } from './models/UserRoles'; import { Users } from './models/Users'; export { Roles, CachedChatRoom, CachedChatSubscription, - RoomRoles, - UserRoles, AuthzCachedCollection, Permissions, /** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ diff --git a/apps/meteor/app/models/client/models/RoomRoles.ts b/apps/meteor/app/models/client/models/RoomRoles.ts deleted file mode 100644 index ab347a59eadac..0000000000000 --- a/apps/meteor/app/models/client/models/RoomRoles.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { ISubscription } from '@rocket.chat/core-typings'; -import { Mongo } from 'meteor/mongo'; - -/** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ -export const RoomRoles = new Mongo.Collection>(null); diff --git a/apps/meteor/app/models/client/models/UserRoles.ts b/apps/meteor/app/models/client/models/UserRoles.ts deleted file mode 100644 index 04a1710e8b9c0..0000000000000 --- a/apps/meteor/app/models/client/models/UserRoles.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { IRocketChatRecord, IRole } from '@rocket.chat/core-typings'; -import { Mongo } from 'meteor/mongo'; - -/** @deprecated new code refer to Minimongo collections like this one; prefer fetching data from the REST API, listening to changes via streamer events, and storing the state in a Tanstack Query */ -export const UserRoles = new Mongo.Collection< - IRocketChatRecord & { - roles?: IRole['_id'][]; - } ->(null); diff --git a/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts b/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts index f39a866d9e1e9..d73fb5c07a3c4 100644 --- a/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts +++ b/apps/meteor/client/components/message/header/hooks/useMessageRoles.ts @@ -1,23 +1,31 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { useCallback } from 'react'; -import { RoomRoles, UserRoles, Roles } from '../../../../../app/models/client'; +import { Roles } from '../../../../../app/models/client'; import { useReactiveValue } from '../../../../hooks/useReactiveValue'; +import type { RoomRoles } from '../../../../hooks/useRoomRolesQuery'; +import { useRoomRolesQuery } from '../../../../hooks/useRoomRolesQuery'; +import type { UserRoles } from '../../../../hooks/useUserRolesQuery'; +import { useUserRolesQuery } from '../../../../hooks/useUserRolesQuery'; -export const useMessageRoles = (userId: IUser['_id'] | undefined, roomId: IRoom['_id'], shouldLoadRoles: boolean): Array => - useReactiveValue( +export const useMessageRoles = (userId: IUser['_id'] | undefined, roomId: IRoom['_id'], shouldLoadRoles: boolean): Array => { + const { data: userRoles } = useUserRolesQuery({ + select: useCallback((records: UserRoles[]) => records.find((record) => record.uid === userId)?.roles ?? [], [userId]), + enabled: shouldLoadRoles && !!userId, + }); + + const { data: roomRoles } = useRoomRolesQuery(roomId, { + select: useCallback((records: RoomRoles[]) => records.find((record) => record.u._id === userId)?.roles ?? [], [userId]), + enabled: shouldLoadRoles && !!userId, + }); + + return useReactiveValue( useCallback(() => { if (!shouldLoadRoles || !userId) { return []; } - const userRoles = UserRoles.findOne(userId); - const roomRoles = RoomRoles.findOne({ - 'u._id': userId, - 'rid': roomId, - }); - - const roles = [...(userRoles?.roles || []), ...(roomRoles?.roles || [])]; + const roles = [...(userRoles ?? []), ...(roomRoles ?? [])]; const result = Roles.find( { @@ -36,5 +44,6 @@ export const useMessageRoles = (userId: IUser['_id'] | undefined, roomId: IRoom[ }, ).fetch(); return result.map(({ description }) => description); - }, [userId, roomId, shouldLoadRoles]), + }, [userId, shouldLoadRoles, userRoles, roomRoles]), ); +}; diff --git a/apps/meteor/client/hooks/useRoomRolesQuery.ts b/apps/meteor/client/hooks/useRoomRolesQuery.ts new file mode 100644 index 0000000000000..ff0980e71ae66 --- /dev/null +++ b/apps/meteor/client/hooks/useRoomRolesQuery.ts @@ -0,0 +1,122 @@ +import type { IUser, IRole, IRoom } from '@rocket.chat/core-typings'; +import { useMethod, useStream, useUserId } from '@rocket.chat/ui-contexts'; +import { useQuery, useQueryClient, type UseQueryOptions } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { roomsQueryKeys } from '../lib/queryKeys'; + +export type RoomRoles = { + rid: IRoom['_id']; + u: Pick; + roles: IRole['_id'][]; +}; + +type UseRoomRolesQueryOptions = Omit< + UseQueryOptions>, + 'queryKey' | 'queryFn' +>; + +export const useRoomRolesQuery = (rid: IRoom['_id'], options?: UseRoomRolesQueryOptions) => { + const queryClient = useQueryClient(); + + const uid = useUserId(); + + const subscribeToNotifyLogged = useStream('notify-logged'); + + const enabled = !!uid && (options?.enabled ?? true); + + useEffect(() => { + if (!enabled) return; + + return subscribeToNotifyLogged('roles-change', (role) => { + switch (role.type) { + case 'added': { + const { _id: roleId, scope, u } = role; + if (!scope || !u) return; + + queryClient.setQueryData(roomsQueryKeys.roles(rid), (data: RoomRoles[] | undefined = []): RoomRoles[] => { + const index = data?.findIndex((record) => record.rid === rid && record.u._id === u._id) ?? -1; + + if (index < 0) { + return [...data, { rid, u, roles: [roleId] }]; + } + + const roles = new Set(data[index].roles); + roles.add(roleId); + data[index] = { ...data[index], roles: [...roles] }; + + return [...data]; + }); + break; + } + + case 'removed': { + const { _id: roleId, scope, u } = role; + if (!!scope || !u) return; + + queryClient.setQueryData(roomsQueryKeys.roles(rid), (data: RoomRoles[] | undefined = []) => { + const index = data?.findIndex((record) => record.rid === rid && record.u._id === u._id) ?? -1; + + if (index < 0) return data; + + const roles = new Set(data[index].roles); + roles.delete(roleId); + data[index] = { ...data[index], roles: [...roles] }; + + return [...data]; + }); + break; + } + } + }); + }, [enabled, queryClient, rid, subscribeToNotifyLogged, uid]); + + useEffect(() => { + if (!enabled) return; + + return subscribeToNotifyLogged('Users:NameChanged', ({ _id: uid, username, name }: Partial) => { + if (!uid) { + return; + } + + queryClient.setQueryData(roomsQueryKeys.roles(rid), (data: RoomRoles[] | undefined = []) => { + const index = data?.findIndex((record) => record.rid === rid && record.u._id === uid) ?? -1; + + if (index < 0) { + return [...data, { rid, u: { _id: uid, username, name }, roles: [] }]; + } + + data[index] = { + ...data[index], + u: { + ...data[index].u, + username, + name, + }, + }; + + return [...data]; + }); + }); + }, [enabled, queryClient, rid, subscribeToNotifyLogged]); + + const getRoomRoles = useMethod('getRoomRoles'); + + return useQuery({ + queryKey: roomsQueryKeys.roles(rid), + queryFn: async () => { + const results = await getRoomRoles(rid); + + return results.map( + (record): RoomRoles => ({ + rid: record.rid, + u: record.u, + roles: record.roles ?? [], + }), + ); + }, + staleTime: Infinity, + ...options, + enabled, + }); +}; diff --git a/apps/meteor/client/hooks/useUserRolesQuery.ts b/apps/meteor/client/hooks/useUserRolesQuery.ts new file mode 100644 index 0000000000000..854d4afbc029c --- /dev/null +++ b/apps/meteor/client/hooks/useUserRolesQuery.ts @@ -0,0 +1,92 @@ +import type { IRole, IUser } from '@rocket.chat/core-typings'; +import { useMethod, useStream, useUserId } from '@rocket.chat/ui-contexts'; +import type { UseQueryOptions } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { rolesQueryKeys } from '../lib/queryKeys'; + +export type UserRoles = { + uid: IUser['_id']; + roles: IRole['_id'][]; +}; + +type UseUserRolesQueryOptions = Omit< + UseQueryOptions>, + 'queryKey' | 'queryFn' +>; + +export const useUserRolesQuery = (options?: UseUserRolesQueryOptions) => { + const queryClient = useQueryClient(); + + const uid = useUserId(); + + const subscribeToNotifyLogged = useStream('notify-logged'); + + const enabled = !!uid && (options?.enabled ?? true); + + useEffect(() => { + if (!enabled) return; + + return subscribeToNotifyLogged('roles-change', (role) => { + switch (role.type) { + case 'added': { + const { _id: roleId, scope, u } = role; + if (!!scope || !u) return; + + queryClient.setQueryData(rolesQueryKeys.userRoles(), (data: UserRoles[] | undefined = []): UserRoles[] => { + const index = data?.findIndex((record) => record.uid === u._id) ?? -1; + + if (index < 0) { + return [...data, { uid: u._id, roles: [roleId] }]; + } + + const roles = new Set(data[index].roles); + roles.add(roleId); + data[index] = { ...data[index], roles: [...roles] }; + + return [...data]; + }); + break; + } + + case 'removed': { + const { _id: roleId, scope, u } = role; + if (!!scope || !u) return; + + queryClient.setQueryData(rolesQueryKeys.userRoles(), (data: UserRoles[] | undefined = []): UserRoles[] => { + const index = data?.findIndex((record) => record.uid === u._id) ?? -1; + + if (index < 0) return data; + + const roles = new Set(data[index].roles); + roles.delete(roleId); + data[index] = { ...data[index], roles: [...roles] }; + + return [...data]; + }); + break; + } + } + }); + }, [enabled, queryClient, subscribeToNotifyLogged, uid]); + + const getUserRoles = useMethod('getUserRoles'); + + return useQuery({ + queryKey: rolesQueryKeys.userRoles(), + queryFn: async () => { + const results = await getUserRoles(); + + return results.map( + (record): UserRoles => ({ + uid: record._id, + roles: record.roles, + }), + ); + }, + staleTime: Infinity, + ...options, + enabled, + }); +}; diff --git a/apps/meteor/client/lib/federation/Federation.spec.ts b/apps/meteor/client/lib/federation/Federation.spec.ts index e4753f7e54220..e6a4ef3fc3845 100644 --- a/apps/meteor/client/lib/federation/Federation.spec.ts +++ b/apps/meteor/client/lib/federation/Federation.spec.ts @@ -1,17 +1,12 @@ import type { IRoom, ISubscription, IUser, ValueOf } from '@rocket.chat/core-typings'; import * as Federation from './Federation'; -import { RoomRoles } from '../../../app/models/client'; import { RoomMemberActions, RoomSettingsEnum } from '../../../definition/IRoomTypeConfig'; - -jest.mock('../../../app/models/client', () => ({ - RoomRoles: { - findOne: jest.fn(), - }, -})); +import { queryClient } from '../queryClient'; +import { roomsQueryKeys } from '../queryKeys'; afterEach(() => { - (RoomRoles.findOne as jest.Mock).mockClear(); + queryClient.resetQueries(); }); describe('#actionAllowed()', () => { @@ -51,8 +46,14 @@ describe('#actionAllowed()', () => { describe('Seeing another owners', () => { const theirRole = ['owner']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [ + { rid: 'room-id', u: { _id: me }, roles: myRole }, + { rid: 'room-id', u: { _id: them }, roles: theirRole }, + ]); + }); + it('should return true if the user want to remove himself as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { u: { _id: me }, @@ -62,7 +63,6 @@ describe('#actionAllowed()', () => { }); it('should return true if the user want to add himself as a moderator (Demoting himself to moderator)', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { u: { _id: me }, @@ -72,20 +72,22 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to remove another owners as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [ + { rid: 'room-id', u: { _id: me }, roles: myRole }, + { rid: 'room-id', u: { _id: them }, roles: theirRole }, + ]); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove another owners from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); @@ -95,31 +97,35 @@ describe('#actionAllowed()', () => { describe('Seeing moderators', () => { const theirRole = ['moderator']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [ + { rid: 'room-id', u: { _id: me }, roles: myRole }, + { rid: 'room-id', u: { _id: them }, roles: theirRole }, + ]); + }); + it('should return true if the user want to add/remove moderators as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); }); it('should return true if the user want to remove moderators as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); }); it('should return true if the user want to remove moderators from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); @@ -128,30 +134,27 @@ describe('#actionAllowed()', () => { describe('Seeing normal users', () => { it('should return true if the user want to add/remove normal users as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); }); it('should return true if the user want to add/remove normal users as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); }); it('should return true if the user want to remove normal users from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); @@ -165,8 +168,14 @@ describe('#actionAllowed()', () => { describe('Seeing owners', () => { const theirRole = ['owner']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [ + { rid: 'room-id', u: { _id: me }, roles: myRole }, + { rid: 'room-id', u: { _id: them }, roles: theirRole }, + ]); + }); + it('should return false if the user want to add/remove owners as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { u: { _id: me }, @@ -176,9 +185,8 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to add/remove owners as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { u: { _id: me }, roles: myRole, } as ISubscription), @@ -186,9 +194,8 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to add/remove owners as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { u: { _id: me }, roles: myRole, } as ISubscription), @@ -196,9 +203,8 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to remove owners from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.REMOVE_USER, them, { u: { _id: me }, roles: myRole, } as ISubscription), @@ -209,20 +215,25 @@ describe('#actionAllowed()', () => { describe('Seeing another moderators', () => { const theirRole = ['moderator']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [ + { rid: 'room-id', u: { _id: me }, roles: myRole }, + { rid: 'room-id', u: { _id: them }, roles: theirRole }, + ]); + }); + it('should return false if the user want to add/remove moderator as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); }); it('should return true if the user want to remove himself as a moderator (Demoting himself)', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { u: { _id: me }, roles: myRole, } as ISubscription), @@ -230,9 +241,8 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to promote himself as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { u: { _id: me }, roles: myRole, } as ISubscription), @@ -240,20 +250,18 @@ describe('#actionAllowed()', () => { }); it('should return false if the user want to remove another moderator from their role', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove another moderator from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ _id: 'room-id', federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); @@ -262,30 +270,27 @@ describe('#actionAllowed()', () => { describe('Seeing normal users', () => { it('should return false if the user want to add/remove normal users as an owner', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(false); }); it('should return true if the user want to add/remove normal users as a moderator', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); }); it('should return true if the user want to remove normal users from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, roles: myRole, } as ISubscription), ).toBe(true); @@ -297,29 +302,30 @@ describe('#actionAllowed()', () => { describe('Seeing owners', () => { const theirRole = ['owner']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [{ rid: 'room-id', u: { _id: them }, roles: theirRole }]); + }); + it('should return false if the user want to add/remove owners as a normal user', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to add/remove moderators as a normal user', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove owners from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); @@ -328,29 +334,30 @@ describe('#actionAllowed()', () => { describe('Seeing moderators', () => { const theirRole = ['owner']; + beforeEach(() => { + queryClient.setQueryData(roomsQueryKeys.roles('room-id'), [{ rid: 'room-id', u: { _id: them }, roles: theirRole }]); + }); + it('should return false if the user want to add/remove owner as a normal user', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove a moderator from their role', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_MODERATOR, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove a moderator from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue({ roles: theirRole }); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); @@ -358,28 +365,25 @@ describe('#actionAllowed()', () => { describe('Seeing another normal users', () => { it('should return false if the user want to add/remove owner as a normal user', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to add/remove moderator as a normal user', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.SET_AS_OWNER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); it('should return false if the user want to remove normal users from the room', () => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( - Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, me, { - u: { _id: them }, + Federation.actionAllowed({ federated: true }, RoomMemberActions.REMOVE_USER, them, { + u: { _id: me }, } as ISubscription), ).toBe(false); }); @@ -387,7 +391,6 @@ describe('#actionAllowed()', () => { it.each([[RoomMemberActions.SET_AS_MODERATOR], [RoomMemberActions.SET_AS_OWNER], [RoomMemberActions.REMOVE_USER]])( 'should return false if the user want to %s for himself', (action) => { - (RoomRoles.findOne as jest.Mock).mockReturnValue(undefined); expect( Federation.actionAllowed({ federated: true }, action, me, { u: { _id: me }, diff --git a/apps/meteor/client/lib/federation/Federation.ts b/apps/meteor/client/lib/federation/Federation.ts index eefa053bac430..8e9391b18bf26 100644 --- a/apps/meteor/client/lib/federation/Federation.ts +++ b/apps/meteor/client/lib/federation/Federation.ts @@ -1,8 +1,10 @@ import type { IRoom, ISubscription, IUser, ValueOf } from '@rocket.chat/core-typings'; import { isRoomFederated, isDirectMessageRoom, isPublicRoom } from '@rocket.chat/core-typings'; -import { RoomRoles } from '../../../app/models/client'; import { RoomMemberActions, RoomSettingsEnum } from '../../../definition/IRoomTypeConfig'; +import type { RoomRoles } from '../../hooks/useRoomRolesQuery'; +import { queryClient } from '../queryClient'; +import { roomsQueryKeys } from '../queryKeys'; const allowedUserActionsInFederatedRooms: ValueOf[] = [ RoomMemberActions.REMOVE_USER, @@ -39,7 +41,11 @@ export const actionAllowed = ( return false; } - const displayingUserRoomRoles = RoomRoles.findOne({ 'rid': room._id, 'u._id': displayingUserId })?.roles || []; + // TODO: there is no guarantee that the room roles are already loaded + const displayingUserRoomRoles = + queryClient + .getQueryData(roomsQueryKeys.roles(room._id)) + ?.find((record) => record.rid === room._id && record.u._id === displayingUserId)?.roles || []; const loggedInUserRoomRoles = userSubscription.roles || []; if (loggedInUserRoomRoles.includes('owner')) { diff --git a/apps/meteor/client/lib/queryKeys.ts b/apps/meteor/client/lib/queryKeys.ts index ad15b225ab88d..1691403d59fe5 100644 --- a/apps/meteor/client/lib/queryKeys.ts +++ b/apps/meteor/client/lib/queryKeys.ts @@ -8,6 +8,7 @@ export const roomsQueryKeys = { messages: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'messages'] as const, message: (rid: IRoom['_id'], mid: IMessage['_id']) => [...roomsQueryKeys.messages(rid), mid] as const, threads: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'threads'] as const, + roles: (rid: IRoom['_id']) => [...roomsQueryKeys.room(rid), 'roles'] as const, }; export const subscriptionsQueryKeys = { @@ -18,3 +19,8 @@ export const subscriptionsQueryKeys = { export const cannedResponsesQueryKeys = { all: ['canned-responses'] as const, }; + +export const rolesQueryKeys = { + all: ['roles'] as const, + userRoles: () => [...rolesQueryKeys.all, 'user-roles'] as const, +}; diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 63980c31561e0..8a68ba39aa60f 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -22,4 +22,3 @@ import './slashCommands'; import './startup'; import './streamMessage'; import './unread'; -import './userRoles'; diff --git a/apps/meteor/client/startup/userRoles.ts b/apps/meteor/client/startup/userRoles.ts deleted file mode 100644 index 1359dfa4d423c..0000000000000 --- a/apps/meteor/client/startup/userRoles.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { UserRoles, Messages } from '../../app/models/client'; -import { sdk } from '../../app/utils/client/lib/SDKClient'; -import { dispatchToastMessage } from '../lib/toast'; - -Meteor.startup(() => { - Tracker.autorun(() => { - if (Meteor.userId()) { - sdk - .call('getUserRoles') - .then((results) => { - for (const record of results) { - UserRoles.upsert({ _id: record._id }, record); - } - }) - .catch((error) => { - dispatchToastMessage({ type: 'error', message: error }); - }); - - sdk.stream('notify-logged', ['roles-change'], (role) => { - if (role.type === 'added') { - if (!role.scope) { - if (!role.u) { - return; - } - UserRoles.upsert({ _id: role.u._id }, { $addToSet: { roles: role._id }, $set: { username: role.u.username } }); - Messages.update({ 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); - } - - return; - } - - if (role.type === 'removed') { - if (!role.scope) { - if (!role.u) { - return; - } - UserRoles.update({ _id: role.u._id }, { $pull: { roles: role._id } }); - Messages.update({ 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); - } - - return; - } - - if (role.type === 'changed') { - Messages.update({ roles: role._id }, { $inc: { rerender: 1 } }, { multi: true }); - } - }); - } - }); -}); diff --git a/apps/meteor/client/views/room/body/hooks/useRoomRolesManagement.ts b/apps/meteor/client/views/room/body/hooks/useRoomRolesManagement.ts deleted file mode 100644 index 7b3716e28d4f6..0000000000000 --- a/apps/meteor/client/views/room/body/hooks/useRoomRolesManagement.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { IRoom, IUser } from '@rocket.chat/core-typings'; -import { useMethod, useStream } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; - -import { RoomRoles, Messages } from '../../../../../app/models/client'; - -// const roomRoles = RoomRoles as Mongo.Collection>; - -export const useRoomRolesManagement = (rid: IRoom['_id']): void => { - const getRoomRoles = useMethod('getRoomRoles'); - - useEffect(() => { - getRoomRoles(rid).then((results) => { - Array.from(results).forEach(({ _id, ...data }) => { - const { - rid, - u: { _id: uid }, - } = data; - RoomRoles.upsert({ rid, 'u._id': uid }, { $set: data }); - }); - }); - }, [getRoomRoles, rid]); - - useEffect(() => { - const rolesObserve = RoomRoles.find({ rid }).observe({ - added: (role) => { - if (!role.u?._id) { - return; - } - Messages.update({ rid, 'u._id': role.u._id }, { $addToSet: { roles: role._id } }, { multi: true }); - }, - changed: (role) => { - if (!role.u?._id) { - return; - } - Messages.update({ rid, 'u._id': role.u._id }, { $inc: { rerender: 1 } }, { multi: true }); - }, - removed: (role) => { - if (!role.u?._id) { - return; - } - Messages.update({ rid, 'u._id': role.u._id }, { $pull: { roles: role._id } }, { multi: true }); - }, - }); - - return (): void => { - rolesObserve.stop(); - }; - }, [getRoomRoles, rid]); - - const subscribeToNotifyLoggedIn = useStream('notify-logged'); - - useEffect( - () => - subscribeToNotifyLoggedIn('roles-change', ({ type, ...role }) => { - if (!role.scope) { - return; - } - - if (!role.u?._id) { - return; - } - - switch (type) { - case 'added': - RoomRoles.upsert({ 'rid': role.scope, 'u._id': role.u._id }, { $setOnInsert: { u: role.u }, $addToSet: { roles: role._id } }); - break; - - case 'removed': - RoomRoles.update({ 'rid': role.scope, 'u._id': role.u._id }, { $pull: { roles: role._id } }); - break; - } - }), - [subscribeToNotifyLoggedIn], - ); - - useEffect( - () => - subscribeToNotifyLoggedIn('Users:NameChanged', ({ _id: uid, name }: Partial) => { - RoomRoles.update( - { - 'u._id': uid, - }, - { - $set: { - 'u.name': name, - }, - }, - { - multi: true, - }, - ); - }), - [subscribeToNotifyLoggedIn], - ); -}; diff --git a/apps/meteor/client/views/room/hooks/useUserHasRoomRole.ts b/apps/meteor/client/views/room/hooks/useUserHasRoomRole.ts index 508393c6c673a..363ee57667498 100644 --- a/apps/meteor/client/views/room/hooks/useUserHasRoomRole.ts +++ b/apps/meteor/client/views/room/hooks/useUserHasRoomRole.ts @@ -1,8 +1,13 @@ import type { IRole, IRoom, IUser } from '@rocket.chat/core-typings'; import { useCallback } from 'react'; -import { RoomRoles } from '../../../../app/models/client'; -import { useReactiveValue } from '../../../hooks/useReactiveValue'; +import type { RoomRoles } from '../../../hooks/useRoomRolesQuery'; +import { useRoomRolesQuery } from '../../../hooks/useRoomRolesQuery'; export const useUserHasRoomRole = (uid: IUser['_id'], rid: IRoom['_id'], role: IRole['name']): boolean => - useReactiveValue(useCallback(() => !!RoomRoles.findOne({ rid, 'u._id': uid, 'roles': role }), [uid, rid, role])); + useRoomRolesQuery(rid, { + select: useCallback( + (records: RoomRoles[]) => records.some((record) => record.u._id === uid && record.roles.includes(role)), + [role, uid], + ), + }).data ?? false; diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index 380eb184851a4..cc40e6d51c8a4 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -23,7 +23,6 @@ import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import ImageGalleryProvider from '../../../providers/ImageGalleryProvider'; import RoomNotFound from '../RoomNotFound'; import RoomSkeleton from '../RoomSkeleton'; -import { useRoomRolesManagement } from '../body/hooks/useRoomRolesManagement'; import type { IRoomWithFederationOriginalName } from '../contexts/RoomContext'; import { RoomContext } from '../contexts/RoomContext'; @@ -33,8 +32,6 @@ type RoomProviderProps = { }; const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { - useRoomRolesManagement(rid); - const resultFromServer = useRoomInfoEndpoint(rid); const resultFromLocal = useRoomQuery(rid); From 45f228adc28bed729c41f2896a8b2357dc02f40f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 26 Mar 2025 16:06:54 -0300 Subject: [PATCH 035/187] chore: implement middleware for handling EE endpoints (#35625) Co-authored-by: Marcos Defendi --- apps/meteor/app/api/server/api.ts | 4 +- apps/meteor/app/api/server/definition.ts | 3 +- apps/meteor/app/api/server/router.ts | 35 ++++++++++++----- .../api-enterprise/server/canned-responses.ts | 5 ++- .../ee/app/api-enterprise/server/index.ts | 11 +----- .../server/middlewares/license.ts | 38 ++++++++++++++++++ .../api-enterprise/server/voip-freeswitch.ts | 28 +++++++++++-- .../livechat-enterprise/server/api/agents.ts | 15 ++++++- .../server/api/business-hours.ts | 2 +- .../server/api/contacts.ts | 2 + .../server/api/departments.ts | 14 ++++++- .../server/api/inquiries.ts | 1 + .../server/api/monitors.ts | 2 + .../server/api/priorities.ts | 3 ++ .../livechat-enterprise/server/api/reports.ts | 39 +++++++++++++++---- .../livechat-enterprise/server/api/rooms.ts | 15 ++++++- .../app/livechat-enterprise/server/api/sla.ts | 2 + .../livechat-enterprise/server/api/tags.ts | 12 +++++- .../server/api/transcript.ts | 2 +- .../server/api/triggers.ts | 2 + .../livechat-enterprise/server/api/units.ts | 10 ++--- .../server/hooks/applyRoomRestrictions.ts | 36 +---------------- .../app/livechat-enterprise/server/index.ts | 2 +- .../server/lib/restrictQuery.ts | 38 ++++++++++++++++++ apps/meteor/ee/server/api/audit.ts | 7 +++- apps/meteor/ee/server/api/chat.ts | 5 ++- .../api/engagementDashboard/channels.ts | 1 + .../api/engagementDashboard/messages.ts | 3 ++ .../server/api/engagementDashboard/users.ts | 5 +++ apps/meteor/ee/server/api/federation/rooms.ts | 5 +++ apps/meteor/ee/server/api/ldap.ts | 1 + apps/meteor/ee/server/api/roles.ts | 4 +- apps/meteor/ee/server/api/sessions.ts | 30 +++++++++++--- apps/meteor/ee/server/index.ts | 2 +- .../tests/end-to-end/api/livechat/00-rooms.ts | 2 +- 35 files changed, 290 insertions(+), 96 deletions(-) create mode 100644 apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts create mode 100644 apps/meteor/ee/app/livechat-enterprise/server/lib/restrictQuery.ts diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index a93cf080140db..3076199c9b256 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -1,4 +1,5 @@ import type { IMethodConnection, IUser, IRoom } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; import { Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; @@ -42,6 +43,7 @@ import { metricsMiddleware } from './middlewares/metrics'; import { tracerSpanMiddleware } from './middlewares/tracer'; import type { Route } from './router'; import { Router } from './router'; +import { license } from '../../../ee/app/api-enterprise/server/middlewares/license'; import { isObject } from '../../../lib/utils/isObject'; import { getNestedProp } from '../../../server/lib/getNestedProp'; import { shouldBreakInVersion } from '../../../server/lib/shouldBreakInVersion'; @@ -897,12 +899,12 @@ export class APIClass< return result; } as InnerAction; - // Allow the endpoints to make usage of the logger which respects the user's settings (operations[method as keyof Operations] as Record).logger = logger; this.router[method.toLowerCase() as 'get' | 'post' | 'put' | 'delete']( `/${route}`.replaceAll('//', '/'), _options as TypedOptions, + license(_options as TypedOptions, License), (operations[method as keyof Operations] as Record).action as any, ); this._routes.push({ diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index 742953a4c3651..18f543500e893 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -1,4 +1,4 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import type { IUser, LicenseModule } from '@rocket.chat/core-typings'; import type { Logger } from '@rocket.chat/logger'; import type { Method, MethodOf, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings'; import type { ValidateFunction } from 'ajv'; @@ -250,6 +250,7 @@ export type TypedOptions = { body?: ValidateFunction; tags?: string[]; typed?: boolean; + license?: LicenseModule[]; } & Options; export type TypedThis = { diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index 7e0ca4f091746..ef35d489254f9 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -4,6 +4,19 @@ import express from 'express'; import type { TypedAction, TypedOptions } from './definition'; +type MiddlewareHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => void; + +type MiddlewareHandlerListAndActionHandler = [ + ...MiddlewareHandler[], + TypedAction, +]; + +function splitArray(arr: [...T[], U]): [T[], U] { + const last = arr[arr.length - 1]; + const rest = arr.slice(0, -1) as T[]; + return [rest, last as U]; +} + export type Route = { responses: Record< number, @@ -107,7 +120,7 @@ export class Router< method: Method, subpath: TSubPathPattern, options: TOptions, - action: TypedAction, + ...actions: MiddlewareHandlerListAndActionHandler ): Router< TBasePath, | TOperations @@ -116,10 +129,12 @@ export class Router< path: TPathPattern; } & Omit) > { + const [middlewares, action] = splitArray(actions); + const prev = this.middleware; this.middleware = (router: express.Router) => { prev(router); - router[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), async (req, res) => { + router[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), ...middlewares, async (req, res) => { if (options.query) { const validatorFn = options.query; if (typeof options.query === 'function' && !validatorFn(req.query)) { @@ -196,7 +211,7 @@ export class Router< get( subpath: TSubPathPattern, options: TOptions, - action: TypedAction, + ...action: MiddlewareHandlerListAndActionHandler ): Router< TBasePath, | TOperations @@ -205,13 +220,13 @@ export class Router< path: TPathPattern; } & Omit) > { - return this.method('GET', subpath, options, action); + return this.method('GET', subpath, options, ...action); } post( subpath: TSubPathPattern, options: TOptions, - action: TypedAction, + ...action: MiddlewareHandlerListAndActionHandler ): Router< TBasePath, | TOperations @@ -220,13 +235,13 @@ export class Router< path: TPathPattern; } & Omit) > { - return this.method('POST', subpath, options, action); + return this.method('POST', subpath, options, ...action); } put( subpath: TSubPathPattern, options: TOptions, - action: TypedAction, + ...action: MiddlewareHandlerListAndActionHandler ): Router< TBasePath, | TOperations @@ -235,13 +250,13 @@ export class Router< path: TPathPattern; } & Omit) > { - return this.method('PUT', subpath, options, action); + return this.method('PUT', subpath, options, ...action); } delete( subpath: TSubPathPattern, options: TOptions, - action: TypedAction, + ...action: MiddlewareHandlerListAndActionHandler ): Router< TBasePath, | TOperations @@ -250,7 +265,7 @@ export class Router< path: TPathPattern; } & Omit) > { - return this.method('DELETE', subpath, options, action); + return this.method('DELETE', subpath, options, ...action); } use void>(fn: FN): Router; diff --git a/apps/meteor/ee/app/api-enterprise/server/canned-responses.ts b/apps/meteor/ee/app/api-enterprise/server/canned-responses.ts index 04afadb2b4bf9..afe018f9a3ff4 100644 --- a/apps/meteor/ee/app/api-enterprise/server/canned-responses.ts +++ b/apps/meteor/ee/app/api-enterprise/server/canned-responses.ts @@ -44,7 +44,7 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'canned-responses.get', - { authRequired: true, permissionsRequired: ['view-canned-responses'] }, + { authRequired: true, permissionsRequired: ['view-canned-responses'], license: ['canned-responses'] }, { async get() { return API.v1.success({ @@ -60,6 +60,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: { GET: ['view-canned-responses'], POST: ['save-canned-responses'], DELETE: ['remove-canned-responses'] }, validateParams: { POST: isPOSTCannedResponsesProps, DELETE: isDELETECannedResponsesProps, GET: isCannedResponsesProps }, + license: ['canned-responses'], }, { async get() { @@ -113,7 +114,7 @@ API.v1.addRoute( API.v1.addRoute( 'canned-responses/:_id', - { authRequired: true, permissionsRequired: ['view-canned-responses'] }, + { authRequired: true, permissionsRequired: ['view-canned-responses'], license: ['canned-responses'] }, { async get() { const { _id } = this.urlParams; diff --git a/apps/meteor/ee/app/api-enterprise/server/index.ts b/apps/meteor/ee/app/api-enterprise/server/index.ts index af62899e1f3a6..7a9a151aa1fc7 100644 --- a/apps/meteor/ee/app/api-enterprise/server/index.ts +++ b/apps/meteor/ee/app/api-enterprise/server/index.ts @@ -1,9 +1,2 @@ -import { License } from '@rocket.chat/license'; - -await License.onLicense('canned-responses', async () => { - await import('./canned-responses'); -}); - -License.onValidateLicense(async () => { - await import('./voip-freeswitch'); -}); +import './canned-responses'; +import './voip-freeswitch'; diff --git a/apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts b/apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts new file mode 100644 index 0000000000000..2d5c8c0faecf8 --- /dev/null +++ b/apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts @@ -0,0 +1,38 @@ +import type { LicenseManager } from '@rocket.chat/license'; +import type { Request, Response, NextFunction } from 'express'; + +import type { FailureResult, TypedOptions } from '../../../../../app/api/server/definition'; + +type ExpressMiddleware = (req: Request, res: Response, next: NextFunction) => void; + +export const license = + (options: TypedOptions, licenseManager: LicenseManager): ExpressMiddleware => + async (_req, res, next) => { + if (!options.license) { + return next(); + } + + const license = options.license.every((license) => licenseManager.hasModule(license)); + + const failure: FailureResult<{ + error: string; + errorType: string; + }> = { + statusCode: 400, + body: { + success: false, + error: 'This is an enterprise feature [error-action-not-allowed]', + errorType: 'error-action-not-allowed', + }, + }; + + if (!license) { + // Explicitly set the content type to application/json to avoid the following issue: + // https://github.com/expressjs/express/issues/2238 + res.writeHead(failure.statusCode, { 'Content-Type': 'application/json' }); + res.write(JSON.stringify(failure.body)); + return res.end(); + } + + return next(); + }; diff --git a/apps/meteor/ee/app/api-enterprise/server/voip-freeswitch.ts b/apps/meteor/ee/app/api-enterprise/server/voip-freeswitch.ts index fdcfcc2ec6e6d..fd4f96471200a 100644 --- a/apps/meteor/ee/app/api-enterprise/server/voip-freeswitch.ts +++ b/apps/meteor/ee/app/api-enterprise/server/voip-freeswitch.ts @@ -13,7 +13,12 @@ import { settings } from '../../../../app/settings/server/cached'; API.v1.addRoute( 'voip-freeswitch.extension.list', - { authRequired: true, permissionsRequired: ['manage-voip-extensions'], validateParams: isVoipFreeSwitchExtensionListProps }, + { + authRequired: true, + permissionsRequired: ['manage-voip-extensions'], + validateParams: isVoipFreeSwitchExtensionListProps, + license: ['voip-enterprise'], + }, { async get() { if (!settings.get('VoIP_TeamCollab_Enabled')) { @@ -59,7 +64,12 @@ API.v1.addRoute( API.v1.addRoute( 'voip-freeswitch.extension.assign', - { authRequired: true, permissionsRequired: ['manage-voip-extensions'], validateParams: isVoipFreeSwitchExtensionAssignProps }, + { + authRequired: true, + permissionsRequired: ['manage-voip-extensions'], + validateParams: isVoipFreeSwitchExtensionAssignProps, + license: ['voip-enterprise'], + }, { async post() { if (!settings.get('VoIP_TeamCollab_Enabled')) { @@ -94,7 +104,12 @@ API.v1.addRoute( API.v1.addRoute( 'voip-freeswitch.extension.getDetails', - { authRequired: true, permissionsRequired: ['view-voip-extension-details'], validateParams: isVoipFreeSwitchExtensionGetDetailsProps }, + { + authRequired: true, + permissionsRequired: ['view-voip-extension-details'], + validateParams: isVoipFreeSwitchExtensionGetDetailsProps, + license: ['voip-enterprise'], + }, { async get() { if (!settings.get('VoIP_TeamCollab_Enabled')) { @@ -124,7 +139,12 @@ API.v1.addRoute( API.v1.addRoute( 'voip-freeswitch.extension.getRegistrationInfoByUserId', - { authRequired: true, permissionsRequired: ['view-user-voip-extension'], validateParams: isVoipFreeSwitchExtensionGetInfoProps }, + { + authRequired: true, + permissionsRequired: ['view-user-voip-extension'], + validateParams: isVoipFreeSwitchExtensionGetInfoProps, + license: ['voip-enterprise'], + }, { async get() { if (!settings.get('VoIP_TeamCollab_Enabled')) { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/agents.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/agents.ts index 9eceae08e3260..388a03dfcb890 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/agents.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/agents.ts @@ -14,7 +14,12 @@ import { API.v1.addRoute( 'livechat/analytics/agents/average-service-time', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsAgentsAverageServiceTimeProps }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsAgentsAverageServiceTimeProps, + license: ['livechat-enterprise'], + }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -47,7 +52,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/agents/total-service-time', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsAgentsTotalServiceTimeProps }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsAgentsTotalServiceTimeProps, + license: ['livechat-enterprise'], + }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -84,6 +94,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsAgentsAvailableForServiceHistoryProps, + license: ['livechat-enterprise'], }, { async get() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/business-hours.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/business-hours.ts index 663c489e114a1..b7389fe87224e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/business-hours.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/business-hours.ts @@ -21,7 +21,7 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'livechat/business-hours', - { authRequired: true, permissionsRequired: ['view-livechat-business-hours'] }, + { authRequired: true, permissionsRequired: ['view-livechat-business-hours'], license: ['livechat-enterprise'] }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/contacts.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/contacts.ts index 96d45de17955d..c740db2bf6872 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/contacts.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/contacts.ts @@ -43,6 +43,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['block-livechat-contact'], validateParams: isBlockContactProps, + license: ['livechat-enterprise'], }, { async post() { @@ -69,6 +70,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['unblock-livechat-contact'], validateParams: isBlockContactProps, + license: ['livechat-enterprise'], }, { async post() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/departments.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/departments.ts index cd29d1b76c58d..7ec0930253d83 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/departments.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/departments.ts @@ -24,7 +24,12 @@ import { API.v1.addRoute( 'livechat/analytics/departments/amount-of-chats', - { authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsAmountOfChatsProps }, + { + authRequired: true, + permissionsRequired: ['view-livechat-manager'], + validateParams: isLivechatAnalyticsDepartmentsAmountOfChatsProps, + license: ['livechat-enterprise'], + }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -64,6 +69,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsAverageServiceTimeProps, + license: ['livechat-enterprise'], }, { async get() { @@ -103,6 +109,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsAverageChatDurationTimeProps, + license: ['livechat-enterprise'], }, { async get() { @@ -142,6 +149,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsTotalServiceTimeProps, + license: ['livechat-enterprise'], }, { async get() { @@ -181,6 +189,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsAverageWaitingTimeProps, + license: ['livechat-enterprise'], }, { async get() { @@ -220,6 +229,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsTotalTransferredChatsProps, + license: ['livechat-enterprise'], }, { async get() { @@ -259,6 +269,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsTotalAbandonedChatsProps, + license: ['livechat-enterprise'], }, { async get() { @@ -298,6 +309,7 @@ API.v1.addRoute( authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatAnalyticsDepartmentsPercentageAbandonedChatsProps, + license: ['livechat-enterprise'], }, { async get() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts index 667529bb10e10..6c2b995caa1f1 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/inquiries.ts @@ -8,6 +8,7 @@ API.v1.addRoute( permissionsRequired: { PUT: { permissions: ['view-l-room', 'manage-livechat-sla'], operation: 'hasAny' }, }, + license: ['livechat-enterprise'], }, { async put() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts index 62293a87ea631..5a1418799e538 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/monitors.ts @@ -9,6 +9,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['manage-livechat-monitors'], + license: ['livechat-enterprise'], }, { async get() { @@ -35,6 +36,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['manage-livechat-monitors'], + license: ['livechat-enterprise'], }, { async get() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts index c3e2e84bb83d5..c12a7f84d6704 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/priorities.ts @@ -12,6 +12,7 @@ API.v1.addRoute( authRequired: true, validateParams: isGETLivechatPrioritiesParams, permissionsRequired: { GET: { permissions: ['manage-livechat-priorities', 'view-l-room'], operation: 'hasAny' } }, + license: ['livechat-enterprise'], }, { async get() { @@ -42,6 +43,7 @@ API.v1.addRoute( PUT: { permissions: ['manage-livechat-priorities'], operation: 'hasAny' }, }, validateParams: { PUT: isPUTLivechatPriority }, + license: ['livechat-enterprise'], }, { async get() { @@ -74,6 +76,7 @@ API.v1.addRoute( POST: { permissions: ['manage-livechat-priorities'], operation: 'hasAny' }, GET: { permissions: ['manage-livechat-priorities'], operation: 'hasAny' }, }, + license: ['livechat-enterprise'], }, { async post() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/reports.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/reports.ts index bd36a919b35b6..31129cf80b1af 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/reports.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/reports.ts @@ -2,8 +2,6 @@ import { isGETDashboardConversationsByType } from '@rocket.chat/rest-typings'; import type { Moment } from 'moment'; import moment from 'moment'; -import { API } from '../../../../../app/api/server'; -import { restrictQuery } from '../hooks/applyRoomRestrictions'; import { findAllConversationsBySourceCached, findAllConversationsByStatusCached, @@ -11,6 +9,8 @@ import { findAllConversationsByTagsCached, findAllConversationsByAgentsCached, } from './lib/dashboards'; +import { API } from '../../../../../app/api/server'; +import { restrictQuery } from '../lib/restrictQuery'; const checkDates = (start: Moment, end: Moment) => { if (!start.isValid()) { @@ -32,7 +32,12 @@ const checkDates = (start: Moment, end: Moment) => { API.v1.addRoute( 'livechat/analytics/dashboards/conversations-by-source', - { authRequired: true, permissionsRequired: ['view-livechat-reports'], validateParams: isGETDashboardConversationsByType }, + { + authRequired: true, + permissionsRequired: ['view-livechat-reports'], + validateParams: isGETDashboardConversationsByType, + license: ['livechat-enterprise'], + }, { async get() { const { start, end } = this.queryParams; @@ -52,7 +57,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/conversations-by-status', - { authRequired: true, permissionsRequired: ['view-livechat-reports'], validateParams: isGETDashboardConversationsByType }, + { + authRequired: true, + permissionsRequired: ['view-livechat-reports'], + validateParams: isGETDashboardConversationsByType, + license: ['livechat-enterprise'], + }, { async get() { const { start, end } = this.queryParams; @@ -71,7 +81,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/conversations-by-department', - { authRequired: true, permissionsRequired: ['view-livechat-reports'], validateParams: isGETDashboardConversationsByType }, + { + authRequired: true, + permissionsRequired: ['view-livechat-reports'], + validateParams: isGETDashboardConversationsByType, + license: ['livechat-enterprise'], + }, { async get() { const { start, end } = this.queryParams; @@ -91,7 +106,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/conversations-by-tags', - { authRequired: true, permissionsRequired: ['view-livechat-reports'], validateParams: isGETDashboardConversationsByType }, + { + authRequired: true, + permissionsRequired: ['view-livechat-reports'], + validateParams: isGETDashboardConversationsByType, + license: ['livechat-enterprise'], + }, { async get() { const { start, end } = this.queryParams; @@ -111,7 +131,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/analytics/dashboards/conversations-by-agent', - { authRequired: true, permissionsRequired: ['view-livechat-reports'], validateParams: isGETDashboardConversationsByType }, + { + authRequired: true, + permissionsRequired: ['view-livechat-reports'], + validateParams: isGETDashboardConversationsByType, + license: ['livechat-enterprise'], + }, { async get() { const { start, end } = this.queryParams; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts index 41c4d4b500f7c..53ae6c1c9275b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/rooms.ts @@ -10,7 +10,12 @@ import { i18n } from '../../../../../server/lib/i18n'; API.v1.addRoute( 'livechat/room.onHold', - { authRequired: true, permissionsRequired: ['on-hold-livechat-room'], validateParams: isLivechatRoomOnHoldProps }, + { + authRequired: true, + permissionsRequired: ['on-hold-livechat-room'], + validateParams: isLivechatRoomOnHoldProps, + license: ['livechat-enterprise'], + }, { async post() { const { roomId } = this.bodyParams; @@ -43,7 +48,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/room.resumeOnHold', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isLivechatRoomResumeOnHoldProps }, + { + authRequired: true, + permissionsRequired: ['view-l-room'], + validateParams: isLivechatRoomResumeOnHoldProps, + license: ['livechat-enterprise'], + }, { async post() { const { roomId } = this.bodyParams; @@ -87,6 +97,7 @@ API.v1.addRoute( POST: { permissions: ['view-l-room'], operation: 'hasAny' }, DELETE: { permissions: ['view-l-room'], operation: 'hasAny' }, }, + license: ['livechat-enterprise'], }, { async post() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/sla.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/sla.ts index 382518bd6e3ae..4ba7b66f1793e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/sla.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/sla.ts @@ -18,6 +18,7 @@ API.v1.addRoute( GET: isLivechatPrioritiesProps, POST: isCreateOrUpdateLivechatSlaProps, }, + license: ['livechat-enterprise'], }, { async get() { @@ -62,6 +63,7 @@ API.v1.addRoute( validateParams: { PUT: isCreateOrUpdateLivechatSlaProps, }, + license: ['livechat-enterprise'], }, { async get() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts index ab40e42aaf82f..d9506c24aa5d1 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/tags.ts @@ -4,7 +4,11 @@ import { getPaginationItems } from '../../../../../app/api/server/helpers/getPag API.v1.addRoute( 'livechat/tags', - { authRequired: true, permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } } }, + { + authRequired: true, + permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } }, + license: ['livechat-enterprise'], + }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -30,7 +34,11 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/tags/:tagId', - { authRequired: true, permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } } }, + { + authRequired: true, + permissionsRequired: { GET: { permissions: ['view-l-room', 'manage-livechat-tags'], operation: 'hasAny' } }, + license: ['livechat-enterprise'], + }, { async get() { const { tagId } = this.urlParams; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts index 19c1c450f5e6e..27b1764bf665d 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts @@ -6,7 +6,7 @@ import { requestPdfTranscript } from '../lib/requestPdfTranscript'; API.v1.addRoute( 'omnichannel/:rid/request-transcript', - { authRequired: true, permissionsRequired: ['request-pdf-transcript'] }, + { authRequired: true, permissionsRequired: ['request-pdf-transcript'], license: ['livechat-enterprise'] }, { async post() { const room = await LivechatRooms.findOneById(this.urlParams.rid); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/triggers.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/triggers.ts index 094c02d729c05..03b72c816822b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/triggers.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/triggers.ts @@ -14,6 +14,7 @@ API.v1.addRoute( permissionsRequired: ['view-livechat-manager'], validateParams: isLivechatTriggerWebhookTestParams, rateLimiterOptions: { numRequestsAllowed: 15, intervalTimeInMS: 60000 }, + license: ['livechat-enterprise'], }, { async post() { @@ -68,6 +69,7 @@ API.v1.addRoute( intervalTimeInMS: 60000, }, validateParams: isLivechatTriggerWebhookCallParams, + license: ['livechat-enterprise'], }, { async post() { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts index 347eeaf187616..a45ef5d14f38d 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/units.ts @@ -28,7 +28,7 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'livechat/units/:unitId/monitors', - { authRequired: true, permissionsRequired: ['manage-livechat-monitors'] }, + { authRequired: true, permissionsRequired: ['manage-livechat-monitors'], license: ['livechat-enterprise'] }, { async get() { const { unitId } = this.urlParams; @@ -47,7 +47,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/units', - { authRequired: true, permissionsRequired: { POST: ['manage-livechat-units'], GET: [] } }, + { authRequired: true, permissionsRequired: { POST: ['manage-livechat-units'], GET: [] }, license: ['livechat-enterprise'] }, { async get() { const params = this.queryParams; @@ -79,7 +79,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/units/:id', - { authRequired: true, permissionsRequired: ['manage-livechat-units'] }, + { authRequired: true, permissionsRequired: ['manage-livechat-units'], license: ['livechat-enterprise'] }, { async get() { const { id } = this.urlParams; @@ -105,7 +105,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/units/:unitId/departments', - { authRequired: true, permissionsRequired: ['manage-livechat-units'] }, + { authRequired: true, permissionsRequired: ['manage-livechat-units'], license: ['livechat-enterprise'] }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -125,7 +125,7 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/units/:unitId/departments/available', - { authRequired: true, permissionsRequired: ['manage-livechat-units'] }, + { authRequired: true, permissionsRequired: ['manage-livechat-units'], license: ['livechat-enterprise'] }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts index 8e1ceb6581083..95b310f3a3f3a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applyRoomRestrictions.ts @@ -1,42 +1,8 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { LivechatDepartment } from '@rocket.chat/models'; import type { FilterOperators } from 'mongodb'; import { callbacks } from '../../../../../lib/callbacks'; -import { cbLogger } from '../lib/logger'; -import { getUnitsFromUser } from '../lib/units'; - -export const restrictQuery = async (originalQuery: FilterOperators = {}, unitsFilter?: string[]) => { - const query = { ...originalQuery }; - - let userUnits = await getUnitsFromUser(); - if (!Array.isArray(userUnits)) { - if (Array.isArray(unitsFilter) && unitsFilter.length) { - return { ...query, departmentAncestors: { $in: unitsFilter } }; - } - return query; - } - - if (Array.isArray(unitsFilter) && unitsFilter.length) { - const userUnit = new Set([...userUnits]); - const filteredUnits = new Set(unitsFilter); - - // IF user is trying to filter by a unit he doens't have access to, apply empty filter (no matches) - userUnits = [...userUnit.intersection(filteredUnits)]; - } - // TODO: units is meant to include units and departments, however, here were only using them as units - // We have to change the filter to something like { $or: [{ ancestors: {$in: units }}, {_id: {$in: units}}] } - const departments = await LivechatDepartment.find({ ancestors: { $in: userUnits } }, { projection: { _id: 1 } }).toArray(); - - const expressions = query.$and || []; - const condition = { - $or: [{ departmentAncestors: { $in: userUnits } }, { departmentId: { $in: departments.map(({ _id }) => _id) } }], - }; - query.$and = [condition, ...expressions]; - - cbLogger.debug({ msg: 'Applying room query restrictions', userUnits }); - return query; -}; +import { restrictQuery } from '../lib/restrictQuery'; callbacks.add( 'livechat.applyRoomRestrictions', diff --git a/apps/meteor/ee/app/livechat-enterprise/server/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/index.ts index 13676e7cbedbf..bde44d7e26f4c 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/index.ts @@ -27,10 +27,10 @@ import './lib/routing/LoadBalancing'; import './lib/routing/LoadRotation'; import './lib/AutoCloseOnHoldScheduler'; import './business-hour'; +import './api'; import { createDefaultPriorities } from './priorities'; await License.onLicense('livechat-enterprise', async () => { - require('./api'); require('./hooks'); await import('./startup'); const { createPermissions } = await import('./permissions'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/restrictQuery.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/restrictQuery.ts new file mode 100644 index 0000000000000..b777ccfb215c6 --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/restrictQuery.ts @@ -0,0 +1,38 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatDepartment } from '@rocket.chat/models'; +import type { FilterOperators } from 'mongodb'; + +import { cbLogger } from './logger'; +import { getUnitsFromUser } from './units'; + +export const restrictQuery = async (originalQuery: FilterOperators = {}, unitsFilter?: string[]) => { + const query = { ...originalQuery }; + + let userUnits = await getUnitsFromUser(); + if (!Array.isArray(userUnits)) { + if (Array.isArray(unitsFilter) && unitsFilter.length) { + return { ...query, departmentAncestors: { $in: unitsFilter } }; + } + return query; + } + + if (Array.isArray(unitsFilter) && unitsFilter.length) { + const userUnit = new Set([...userUnits]); + const filteredUnits = new Set(unitsFilter); + + // IF user is trying to filter by a unit he doens't have access to, apply empty filter (no matches) + userUnits = [...userUnit.intersection(filteredUnits)]; + } + // TODO: units is meant to include units and departments, however, here were only using them as units + // We have to change the filter to something like { $or: [{ ancestors: {$in: units }}, {_id: {$in: units}}] } + const departments = await LivechatDepartment.find({ ancestors: { $in: userUnits } }, { projection: { _id: 1 } }).toArray(); + + const expressions = query.$and || []; + const condition = { + $or: [{ departmentAncestors: { $in: userUnits } }, { departmentId: { $in: departments.map(({ _id }) => _id) } }], + }; + query.$and = [condition, ...expressions]; + + cbLogger.debug({ msg: 'Applying room query restrictions', userUnits }); + return query; +}; diff --git a/apps/meteor/ee/server/api/audit.ts b/apps/meteor/ee/server/api/audit.ts index 748368f0d5690..2b96927e69960 100644 --- a/apps/meteor/ee/server/api/audit.ts +++ b/apps/meteor/ee/server/api/audit.ts @@ -44,7 +44,12 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'audit/rooms.members', - { authRequired: true, permissionsRequired: ['view-members-list-all-rooms'], validateParams: isAuditRoomMembersProps }, + { + authRequired: true, + permissionsRequired: ['view-members-list-all-rooms'], + validateParams: isAuditRoomMembersProps, + license: ['auditing'], + }, { async get() { const { roomId, filter } = this.queryParams; diff --git a/apps/meteor/ee/server/api/chat.ts b/apps/meteor/ee/server/api/chat.ts index e96e8ab9b1c80..568ebe8892788 100644 --- a/apps/meteor/ee/server/api/chat.ts +++ b/apps/meteor/ee/server/api/chat.ts @@ -22,7 +22,10 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'chat.getMessageReadReceipts', - { authRequired: true }, + { + authRequired: true, + // license: ['message-read-receipt'] + }, { async get() { if (!License.hasModule('message-read-receipt')) { diff --git a/apps/meteor/ee/server/api/engagementDashboard/channels.ts b/apps/meteor/ee/server/api/engagementDashboard/channels.ts index 0d2d140bd5750..ebd0924fb2d8a 100644 --- a/apps/meteor/ee/server/api/engagementDashboard/channels.ts +++ b/apps/meteor/ee/server/api/engagementDashboard/channels.ts @@ -38,6 +38,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { diff --git a/apps/meteor/ee/server/api/engagementDashboard/messages.ts b/apps/meteor/ee/server/api/engagementDashboard/messages.ts index 716aafd0f1010..d048369ca502a 100644 --- a/apps/meteor/ee/server/api/engagementDashboard/messages.ts +++ b/apps/meteor/ee/server/api/engagementDashboard/messages.ts @@ -51,6 +51,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -75,6 +76,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -99,6 +101,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { diff --git a/apps/meteor/ee/server/api/engagementDashboard/users.ts b/apps/meteor/ee/server/api/engagementDashboard/users.ts index 0302a36538520..a85148047ff26 100644 --- a/apps/meteor/ee/server/api/engagementDashboard/users.ts +++ b/apps/meteor/ee/server/api/engagementDashboard/users.ts @@ -75,6 +75,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -99,6 +100,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -123,6 +125,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -146,6 +149,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { @@ -169,6 +173,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-engagement-dashboard'], + license: ['engagement-dashboard'], }, { async get() { diff --git a/apps/meteor/ee/server/api/federation/rooms.ts b/apps/meteor/ee/server/api/federation/rooms.ts index dce9049afb375..496e4be90e664 100644 --- a/apps/meteor/ee/server/api/federation/rooms.ts +++ b/apps/meteor/ee/server/api/federation/rooms.ts @@ -14,6 +14,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isFederationSearchPublicRoomsProps, + license: ['federation'], }, { async get() { @@ -31,6 +32,7 @@ API.v1.addRoute( 'federation/listServersByUser', { authRequired: true, + license: ['federation'], }, { async get() { @@ -48,6 +50,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isFederationAddServerProps, + license: ['federation'], }, { async post() { @@ -65,6 +68,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isFederationRemoveServerProps, + license: ['federation'], }, { async post() { @@ -82,6 +86,7 @@ API.v1.addRoute( { authRequired: true, validateParams: isFederationJoinExternalPublicRoomProps, + license: ['federation'], }, { async post() { diff --git a/apps/meteor/ee/server/api/ldap.ts b/apps/meteor/ee/server/api/ldap.ts index 12dea6d5685ee..b69a63407320c 100644 --- a/apps/meteor/ee/server/api/ldap.ts +++ b/apps/meteor/ee/server/api/ldap.ts @@ -9,6 +9,7 @@ API.v1.addRoute( authRequired: true, forceTwoFactorAuthenticationForNonEnterprise: true, twoFactorRequired: true, + // license: ['ldap-enterprise'], }, { async post() { diff --git a/apps/meteor/ee/server/api/roles.ts b/apps/meteor/ee/server/api/roles.ts index 7e8048387ec3b..5164eb0b31db1 100644 --- a/apps/meteor/ee/server/api/roles.ts +++ b/apps/meteor/ee/server/api/roles.ts @@ -93,7 +93,7 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'roles.create', - { authRequired: true }, + { authRequired: true, license: ['custom-roles'] }, { async post() { if (!License.hasModule('custom-roles')) { @@ -137,7 +137,7 @@ API.v1.addRoute( API.v1.addRoute( 'roles.update', - { authRequired: true }, + { authRequired: true, license: ['custom-roles'] }, { async post() { if (!isRoleUpdateProps(this.bodyParams)) { diff --git a/apps/meteor/ee/server/api/sessions.ts b/apps/meteor/ee/server/api/sessions.ts index f4ab45be6462a..3ab1ed31eec38 100644 --- a/apps/meteor/ee/server/api/sessions.ts +++ b/apps/meteor/ee/server/api/sessions.ts @@ -82,7 +82,7 @@ declare module '@rocket.chat/rest-typings' { API.v1.addRoute( 'sessions/list', - { authRequired: true, validateParams: isSessionsPaginateProps }, + { authRequired: true, validateParams: isSessionsPaginateProps, license: ['device-management'] }, { async get() { if (!License.hasModule('device-management')) { @@ -105,7 +105,7 @@ API.v1.addRoute( API.v1.addRoute( 'sessions/info', - { authRequired: true, validateParams: isSessionsProps }, + { authRequired: true, validateParams: isSessionsProps, license: ['device-management'] }, { async get() { if (!License.hasModule('device-management')) { @@ -124,7 +124,7 @@ API.v1.addRoute( API.v1.addRoute( 'sessions/logout.me', - { authRequired: true, validateParams: isSessionsProps }, + { authRequired: true, validateParams: isSessionsProps, license: ['device-management'] }, { async post() { if (!License.hasModule('device-management')) { @@ -150,7 +150,13 @@ API.v1.addRoute( API.v1.addRoute( 'sessions/list.all', - { authRequired: true, twoFactorRequired: true, validateParams: isSessionsPaginateProps, permissionsRequired: ['view-device-management'] }, + { + authRequired: true, + twoFactorRequired: true, + validateParams: isSessionsPaginateProps, + permissionsRequired: ['view-device-management'], + license: ['device-management'], + }, { async get() { if (!License.hasModule('device-management')) { @@ -190,7 +196,13 @@ API.v1.addRoute( API.v1.addRoute( 'sessions/info.admin', - { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['view-device-management'] }, + { + authRequired: true, + twoFactorRequired: true, + validateParams: isSessionsProps, + permissionsRequired: ['view-device-management'], + license: ['device-management'], + }, { async get() { if (!License.hasModule('device-management')) { @@ -209,7 +221,13 @@ API.v1.addRoute( API.v1.addRoute( 'sessions/logout', - { authRequired: true, twoFactorRequired: true, validateParams: isSessionsProps, permissionsRequired: ['logout-device-management'] }, + { + authRequired: true, + twoFactorRequired: true, + validateParams: isSessionsProps, + permissionsRequired: ['logout-device-management'], + license: ['device-management'], + }, { async post() { if (!License.hasModule('device-management')) { diff --git a/apps/meteor/ee/server/index.ts b/apps/meteor/ee/server/index.ts index 1671c15987a06..c9340d1f440a4 100644 --- a/apps/meteor/ee/server/index.ts +++ b/apps/meteor/ee/server/index.ts @@ -6,8 +6,8 @@ import '../app/canned-responses/server/index'; import '../app/livechat-enterprise/server/index'; import '../app/message-read-receipt/server/index'; import '../app/voip-enterprise/server/index'; -import '../app/settings/server/index'; import './api'; +import '../app/settings/server/index'; import './requestSeatsRoute'; import './configuration/index'; import './local-services/ldap/service'; diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index fc1413cd870c4..1679319d8804c 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -82,7 +82,7 @@ describe('LIVECHAT - rooms', () => { if (IS_EE) { // install the app await request - .post(apps('/')) + .post(apps()) .set(credentials) .send({ url: APP_URL }) .expect('Content-Type', 'application/json') From f2cdb6cd429ffeb95e3d2c1eb90b301991363a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Wed, 26 Mar 2025 17:25:03 -0300 Subject: [PATCH 036/187] refactor: Remove meteor functions from WebRTCClass stream (#35544) --- apps/meteor/app/webrtc/client/WebRTCClass.ts | 34 ++-------------- apps/meteor/client/views/root/AppLayout.tsx | 2 + .../client/views/root/hooks/useWebRTC.ts | 39 +++++++++++++++++++ 3 files changed, 44 insertions(+), 31 deletions(-) create mode 100644 apps/meteor/client/views/root/hooks/useWebRTC.ts diff --git a/apps/meteor/app/webrtc/client/WebRTCClass.ts b/apps/meteor/app/webrtc/client/WebRTCClass.ts index 5d942c3656346..b99bea6cd28ba 100644 --- a/apps/meteor/app/webrtc/client/WebRTCClass.ts +++ b/apps/meteor/app/webrtc/client/WebRTCClass.ts @@ -3,7 +3,6 @@ import type { StreamKeys, StreamNames, StreamerCallbackArgs } from '@rocket.chat import { Emitter } from '@rocket.chat/emitter'; import { Meteor } from 'meteor/meteor'; import { ReactiveVar } from 'meteor/reactive-var'; -import { Tracker } from 'meteor/tracker'; import { ChromeScreenShare } from './screenShare'; import GenericModal from '../../../client/components/GenericModal'; @@ -65,9 +64,9 @@ type EventData; type CallData = EventData<'notify-room-users', `${string}/webrtc`, 'call'>; -type CandidateData = EventData<'notify-user', `${string}/webrtc`, 'candidate'>; -type DescriptionData = EventData<'notify-user', `${string}/webrtc`, 'description'>; -type JoinData = EventData<'notify-user', `${string}/webrtc`, 'join'>; +export type CandidateData = EventData<'notify-user', `${string}/webrtc`, 'candidate'>; +export type DescriptionData = EventData<'notify-user', `${string}/webrtc`, 'description'>; +export type JoinData = EventData<'notify-user', `${string}/webrtc`, 'join'>; type RemoteItem = { id: string; @@ -1074,31 +1073,4 @@ const WebRTC = new (class { } })(); -Meteor.startup(() => { - Tracker.autorun(() => { - const uid = Meteor.userId(); - - if (uid) { - sdk.stream('notify-user', [`${uid}/${WEB_RTC_EVENTS.WEB_RTC}`], (type, data) => { - if (data.room == null) { - return; - } - const webrtc = WebRTC.getInstanceByRoomId(data.room); - - switch (type) { - case 'candidate': - webrtc?.onUserStream('candidate', data); - break; - case 'description': - webrtc?.onUserStream('description', data); - break; - case 'join': - webrtc?.onUserStream('join', data); - break; - } - }); - } - }); -}); - export { WebRTC }; diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index fe96a49696017..631d71ac25eaa 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -9,6 +9,7 @@ import { useOTRMessaging } from './hooks/useOTRMessaging'; import { useSettingsOnLoadSiteUrl } from './hooks/useSettingsOnLoadSiteUrl'; import { useStoreCookiesOnLogin } from './hooks/useStoreCookiesOnLogin'; import { useUpdateVideoConfUser } from './hooks/useUpdateVideoConfUser'; +import { useWebRTC } from './hooks/useWebRTC'; import { useAnalytics } from '../../../app/analytics/client/loadScript'; import { useCorsSSLConfig } from '../../../app/cors/client/useCorsSSLConfig'; import { useDolphin } from '../../../app/dolphin/client/hooks/useDolphin'; @@ -59,6 +60,7 @@ const AppLayout = () => { useCorsSSLConfig(); useOTRMessaging(); useUpdateVideoConfUser(); + useWebRTC(); useStoreCookiesOnLogin(); useAutoupdate(); diff --git a/apps/meteor/client/views/root/hooks/useWebRTC.ts b/apps/meteor/client/views/root/hooks/useWebRTC.ts new file mode 100644 index 0000000000000..bb6e66d4f02cd --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useWebRTC.ts @@ -0,0 +1,39 @@ +import { useStream, useUserId } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import type { CandidateData, DescriptionData, JoinData } from '../../../../app/webrtc/client/WebRTCClass'; +import { WebRTC } from '../../../../app/webrtc/client/WebRTCClass'; +import { WEB_RTC_EVENTS } from '../../../../app/webrtc/lib/constants'; + +export const useWebRTC = () => { + const uid = useUserId(); + const notifyUser = useStream('notify-user'); + + useEffect(() => { + if (!uid) return; + + const handleNotifyUser = (type: 'candidate' | 'description' | 'join', data: CandidateData | DescriptionData | JoinData) => { + if (data.room == null) return; + + const webrtc = WebRTC.getInstanceByRoomId(data.room); + + if (!webrtc) return; + + switch (type) { + case 'candidate': + webrtc.onUserStream('candidate', data as CandidateData); + break; + case 'description': + webrtc.onUserStream('description', data as DescriptionData); + break; + case 'join': + webrtc.onUserStream('join', data as JoinData); + break; + default: + console.warn(`WebRTC: Received unexpected event type: ${type}`); + } + }; + + return notifyUser(`${uid}/${WEB_RTC_EVENTS.WEB_RTC}`, handleNotifyUser); + }, [notifyUser, uid]); +}; From 5c868fd1a6be2e53258da75686331e2424a80766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Wed, 26 Mar 2025 17:42:26 -0300 Subject: [PATCH 037/187] refactor: replace `useCallback` with `useEffectEvent` (#35630) --- .../notification/useDesktopNotification.ts | 45 ++-- .../notification/useNewMessageNotification.ts | 35 ++- .../notification/useNewRoomNotification.ts | 6 +- .../hooks/notification/useNotification.ts | 199 +++++++++--------- .../notification/useNotificationPermission.ts | 6 +- 5 files changed, 141 insertions(+), 150 deletions(-) diff --git a/apps/meteor/client/hooks/notification/useDesktopNotification.ts b/apps/meteor/client/hooks/notification/useDesktopNotification.ts index a0b72dbf726b2..c58cccd59f878 100644 --- a/apps/meteor/client/hooks/notification/useDesktopNotification.ts +++ b/apps/meteor/client/hooks/notification/useDesktopNotification.ts @@ -1,6 +1,6 @@ import type { INotificationDesktop } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useUser } from '@rocket.chat/ui-contexts'; -import { useCallback } from 'react'; import { useNotification } from './useNotification'; import { e2e } from '../../../app/e2e/client'; @@ -11,32 +11,29 @@ export const useDesktopNotification = () => { const user = useUser(); const notify = useNotification(); - const notifyDesktop = useCallback( - async (notification: INotificationDesktop) => { - if ( - notification.payload.rid === RoomManager.opened && - (typeof window.document.hasFocus === 'function' ? window.document.hasFocus() : undefined) - ) { - return; - } - if (user?.status === 'busy') { - return; - } + const notifyDesktop = useEffectEvent(async (notification: INotificationDesktop) => { + if ( + notification.payload.rid === RoomManager.opened && + (typeof window.document.hasFocus === 'function' ? window.document.hasFocus() : undefined) + ) { + return; + } + if (user?.status === 'busy') { + return; + } - if (notification.payload.message?.t === 'e2e') { - const e2eRoom = await e2e.getInstanceByRoomId(notification.payload.rid); - if (e2eRoom) { - notification.text = (await e2eRoom.decrypt(notification.payload.message.msg)).text; - } + if (notification.payload.message?.t === 'e2e') { + const e2eRoom = await e2e.getInstanceByRoomId(notification.payload.rid); + if (e2eRoom) { + notification.text = (await e2eRoom.decrypt(notification.payload.message.msg)).text; } + } - return getAvatarAsPng(notification.payload.sender?.username, (avatarAsPng) => { - notification.icon = avatarAsPng; - return notify(notification); - }); - }, - [notify, user?.status], - ); + return getAvatarAsPng(notification.payload.sender?.username, (avatarAsPng) => { + notification.icon = avatarAsPng; + return notify(notification); + }); + }); return notifyDesktop; }; diff --git a/apps/meteor/client/hooks/notification/useNewMessageNotification.ts b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts index bd69e69e066e7..389fa010ffa32 100644 --- a/apps/meteor/client/hooks/notification/useNewMessageNotification.ts +++ b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts @@ -1,6 +1,6 @@ import type { AtLeast, ISubscription } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useUserPreference } from '@rocket.chat/ui-contexts'; -import { useCallback } from 'react'; import { CustomSounds } from '../../../app/custom-sounds/client/lib/CustomSounds'; import { useUserSoundPreferences } from '../useUserSoundPreferences'; @@ -9,24 +9,21 @@ export const useNewMessageNotification = () => { const newMessageNotification = useUserPreference('newMessageNotification'); const { notificationsSoundVolume } = useUserSoundPreferences(); - const notifyNewMessage = useCallback( - (sub: AtLeast) => { - if (!sub || sub.audioNotificationValue === 'none') { - return; - } - if (sub.audioNotificationValue && sub.audioNotificationValue !== '0') { - void CustomSounds.play(sub.audioNotificationValue, { - volume: Number((notificationsSoundVolume / 100).toPrecision(2)), - }); - } + const notifyNewMessage = useEffectEvent((sub: AtLeast) => { + if (!sub || sub.audioNotificationValue === 'none') { + return; + } + if (sub.audioNotificationValue && sub.audioNotificationValue !== '0') { + void CustomSounds.play(sub.audioNotificationValue, { + volume: Number((notificationsSoundVolume / 100).toPrecision(2)), + }); + } - if (newMessageNotification && newMessageNotification !== 'none') { - void CustomSounds.play(newMessageNotification, { - volume: Number((notificationsSoundVolume / 100).toPrecision(2)), - }); - } - }, - [newMessageNotification, notificationsSoundVolume], - ); + if (newMessageNotification && newMessageNotification !== 'none') { + void CustomSounds.play(newMessageNotification, { + volume: Number((notificationsSoundVolume / 100).toPrecision(2)), + }); + } + }); return notifyNewMessage; }; diff --git a/apps/meteor/client/hooks/notification/useNewRoomNotification.ts b/apps/meteor/client/hooks/notification/useNewRoomNotification.ts index 7b475745b642c..ad2807404d070 100644 --- a/apps/meteor/client/hooks/notification/useNewRoomNotification.ts +++ b/apps/meteor/client/hooks/notification/useNewRoomNotification.ts @@ -1,5 +1,5 @@ +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useUserPreference } from '@rocket.chat/ui-contexts'; -import { useCallback } from 'react'; import { CustomSounds } from '../../../app/custom-sounds/client/lib/CustomSounds'; import { useUserSoundPreferences } from '../useUserSoundPreferences'; @@ -8,7 +8,7 @@ export const useNewRoomNotification = () => { const newRoomNotification = useUserPreference('newRoomNotification'); const { notificationsSoundVolume } = useUserSoundPreferences(); - const notifyNewRoom = useCallback(() => { + const notifyNewRoom = useEffectEvent(() => { if (!newRoomNotification) { return; } @@ -16,7 +16,7 @@ export const useNewRoomNotification = () => { void CustomSounds.play(newRoomNotification, { volume: Number((notificationsSoundVolume / 100).toPrecision(2)), }); - }, [newRoomNotification, notificationsSoundVolume]); + }); return notifyNewRoom; }; diff --git a/apps/meteor/client/hooks/notification/useNotification.ts b/apps/meteor/client/hooks/notification/useNotification.ts index ce20f90c8c3c5..6caa2b2ecc966 100644 --- a/apps/meteor/client/hooks/notification/useNotification.ts +++ b/apps/meteor/client/hooks/notification/useNotification.ts @@ -1,7 +1,7 @@ import type { INotificationDesktop } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { Random } from '@rocket.chat/random'; import { useRouter, useUserPreference } from '@rocket.chat/ui-contexts'; -import { useCallback } from 'react'; import { useNotificationAllowed } from './useNotificationAllowed'; import { getUserAvatarURL } from '../../../app/utils/client'; @@ -14,111 +14,108 @@ export const useNotification = () => { const router = useRouter(); const notificationAllowed = useNotificationAllowed(); - const notify = useCallback( - async (notification: INotificationDesktop) => { - if (!notificationAllowed) { - return; - } - if (!notification.payload) { - return; - } + const notify = useEffectEvent(async (notification: INotificationDesktop) => { + if (!notificationAllowed) { + return; + } + if (!notification.payload) { + return; + } - const { rid } = notification.payload; - if (!rid) { - return; - } - const message = await onClientMessageReceived({ - rid, - msg: notification.text, - notification: true, - } as any); + const { rid } = notification.payload; + if (!rid) { + return; + } + const message = await onClientMessageReceived({ + rid, + msg: notification.text, + notification: true, + } as any); - const n = new Notification(notification.title, { - icon: notification.icon || getUserAvatarURL(notification.payload.sender?.username as string), - body: stripTags(message?.msg), - tag: notification.payload._id, - canReply: true, - silent: true, - requireInteraction, - } as NotificationOptions & { - canReply?: boolean; - }); - const notificationDuration = !requireInteraction ? (notification.duration ?? 0) - 0 || 10 : -1; - if (notificationDuration > 0) { - setTimeout(() => n.close(), notificationDuration * 1000); - } + const n = new Notification(notification.title, { + icon: notification.icon || getUserAvatarURL(notification.payload.sender?.username as string), + body: stripTags(message?.msg), + tag: notification.payload._id, + canReply: true, + silent: true, + requireInteraction, + } as NotificationOptions & { + canReply?: boolean; + }); + const notificationDuration = !requireInteraction ? (notification.duration ?? 0) - 0 || 10 : -1; + if (notificationDuration > 0) { + setTimeout(() => n.close(), notificationDuration * 1000); + } - if (n.addEventListener) { - n.addEventListener( - 'reply', - ({ response }) => - void sdk.call('sendMessage', { - _id: Random.id(), - rid, - msg: response, - }), - ); - } + if (n.addEventListener) { + n.addEventListener( + 'reply', + ({ response }) => + void sdk.call('sendMessage', { + _id: Random.id(), + rid, + msg: response, + }), + ); + } - n.onclick = () => { - n.close(); - window.focus(); + n.onclick = () => { + n.close(); + window.focus(); - if (!notification.payload._id || !notification.payload.rid || !notification.payload.name) { - return; - } + if (!notification.payload._id || !notification.payload.rid || !notification.payload.name) { + return; + } - switch (notification.payload?.type) { - case 'd': - router.navigate({ - pattern: '/direct/:rid/:tab?/:context?', - params: { - rid: notification.payload.rid, - ...(notification.payload.tmid && { - tab: 'thread', - context: notification.payload.tmid, - }), - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - break; - case 'c': - return router.navigate({ - pattern: '/channel/:name/:tab?/:context?', - params: { - name: notification.payload.name, - ...(notification.payload.tmid && { - tab: 'thread', - context: notification.payload.tmid, - }), - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - case 'p': - return router.navigate({ - pattern: '/group/:name/:tab?/:context?', - params: { - name: notification.payload.name, - ...(notification.payload.tmid && { - tab: 'thread', - context: notification.payload.tmid, - }), - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - case 'l': - return router.navigate({ - pattern: '/live/:id/:tab?/:context?', - params: { - id: notification.payload.rid, - tab: 'room-info', - }, - search: { ...router.getSearchParameters(), jump: notification.payload._id }, - }); - } - }; - }, - [notificationAllowed, requireInteraction, router], - ); + switch (notification.payload?.type) { + case 'd': + router.navigate({ + pattern: '/direct/:rid/:tab?/:context?', + params: { + rid: notification.payload.rid, + ...(notification.payload.tmid && { + tab: 'thread', + context: notification.payload.tmid, + }), + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + break; + case 'c': + return router.navigate({ + pattern: '/channel/:name/:tab?/:context?', + params: { + name: notification.payload.name, + ...(notification.payload.tmid && { + tab: 'thread', + context: notification.payload.tmid, + }), + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + case 'p': + return router.navigate({ + pattern: '/group/:name/:tab?/:context?', + params: { + name: notification.payload.name, + ...(notification.payload.tmid && { + tab: 'thread', + context: notification.payload.tmid, + }), + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + case 'l': + return router.navigate({ + pattern: '/live/:id/:tab?/:context?', + params: { + id: notification.payload.rid, + tab: 'room-info', + }, + search: { ...router.getSearchParameters(), jump: notification.payload._id }, + }); + } + }; + }); return notify; }; diff --git a/apps/meteor/client/hooks/notification/useNotificationPermission.ts b/apps/meteor/client/hooks/notification/useNotificationPermission.ts index 4f021b7c15611..abd6c95be9f71 100644 --- a/apps/meteor/client/hooks/notification/useNotificationPermission.ts +++ b/apps/meteor/client/hooks/notification/useNotificationPermission.ts @@ -1,9 +1,9 @@ -import { useCallback } from 'react'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { notificationManager } from '../../lib/notificationManager'; export const useNotificationPermission = () => { - const requestPermission = useCallback(async () => { + const requestPermission = useEffectEvent(async () => { const response = await Notification.requestPermission(); notificationManager.allowed = response === 'granted'; notificationManager.emit('change'); @@ -13,7 +13,7 @@ export const useNotificationPermission = () => { notificationManager.allowed = notifications.state === 'granted'; notificationManager.emit('change'); }; - }, []); + }); if ('Notification' in window) { requestPermission(); From 2178835069d8e4a0ee775b51d7f4c068cd654eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Wed, 26 Mar 2025 18:45:07 -0300 Subject: [PATCH 038/187] refactor: Remove Meteor.startup and Tracker.autorun from WordPress OAuth (#35530) Co-authored-by: Guilherme Gazzo --- apps/meteor/app/wordpress/client/index.ts | 1 - apps/meteor/app/wordpress/client/lib.ts | 80 ------------------- apps/meteor/client/views/root/AppLayout.tsx | 2 + .../views/root/hooks/useWordPressOAuth.ts | 72 +++++++++++++++++ 4 files changed, 74 insertions(+), 81 deletions(-) delete mode 100644 apps/meteor/app/wordpress/client/lib.ts create mode 100644 apps/meteor/client/views/root/hooks/useWordPressOAuth.ts diff --git a/apps/meteor/app/wordpress/client/index.ts b/apps/meteor/app/wordpress/client/index.ts index 555f9f19df3c7..1015ce303cc5b 100644 --- a/apps/meteor/app/wordpress/client/index.ts +++ b/apps/meteor/app/wordpress/client/index.ts @@ -1,2 +1 @@ -import './lib'; import './wordpress-login-button.css'; diff --git a/apps/meteor/app/wordpress/client/lib.ts b/apps/meteor/app/wordpress/client/lib.ts deleted file mode 100644 index b213d5fb88c2a..0000000000000 --- a/apps/meteor/app/wordpress/client/lib.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { OauthConfig } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; -import _ from 'underscore'; - -import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; -import { settings } from '../../settings/client'; - -const config: OauthConfig = { - serverURL: '', - identityPath: '/oauth/me', - - addAutopublishFields: { - forLoggedInUser: ['services.wordpress'], - forOtherUsers: ['services.wordpress.user_login'], - }, - accessTokenParam: 'access_token', -}; - -const WordPress = new CustomOAuth('wordpress', config); - -const fillSettings = _.debounce(async (): Promise => { - config.serverURL = settings.get('API_Wordpress_URL'); - if (!config.serverURL) { - if (config.serverURL === undefined) { - return fillSettings(); - } - return; - } - - delete config.identityPath; - delete config.identityTokenSentVia; - delete config.authorizePath; - delete config.tokenPath; - delete config.scope; - - const serverType = settings.get('Accounts_OAuth_Wordpress_server_type'); - switch (serverType) { - case 'custom': - if (settings.get('Accounts_OAuth_Wordpress_identity_path')) { - config.identityPath = settings.get('Accounts_OAuth_Wordpress_identity_path'); - } - - if (settings.get('Accounts_OAuth_Wordpress_identity_token_sent_via')) { - config.identityTokenSentVia = settings.get('Accounts_OAuth_Wordpress_identity_token_sent_via'); - } - - if (settings.get('Accounts_OAuth_Wordpress_token_path')) { - config.tokenPath = settings.get('Accounts_OAuth_Wordpress_token_path'); - } - - if (settings.get('Accounts_OAuth_Wordpress_authorize_path')) { - config.authorizePath = settings.get('Accounts_OAuth_Wordpress_authorize_path'); - } - - if (settings.get('Accounts_OAuth_Wordpress_scope')) { - config.scope = settings.get('Accounts_OAuth_Wordpress_scope'); - } - break; - case 'wordpress-com': - config.identityPath = 'https://public-api.wordpress.com/rest/v1/me'; - config.identityTokenSentVia = 'header'; - config.authorizePath = 'https://public-api.wordpress.com/oauth2/authorize'; - config.tokenPath = 'https://public-api.wordpress.com/oauth2/token'; - config.scope = 'auth'; - break; - default: - config.identityPath = '/oauth/me'; - break; - } - - const result = WordPress.configure(config); - return result; -}, 100); - -Meteor.startup(() => { - return Tracker.autorun(() => { - return fillSettings(); - }); -}); diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 631d71ac25eaa..6617fef33d994 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -10,6 +10,7 @@ import { useSettingsOnLoadSiteUrl } from './hooks/useSettingsOnLoadSiteUrl'; import { useStoreCookiesOnLogin } from './hooks/useStoreCookiesOnLogin'; import { useUpdateVideoConfUser } from './hooks/useUpdateVideoConfUser'; import { useWebRTC } from './hooks/useWebRTC'; +import { useWordPressOAuth } from './hooks/useWordPressOAuth'; import { useAnalytics } from '../../../app/analytics/client/loadScript'; import { useCorsSSLConfig } from '../../../app/cors/client/useCorsSSLConfig'; import { useDolphin } from '../../../app/dolphin/client/hooks/useDolphin'; @@ -56,6 +57,7 @@ const AppLayout = () => { useDrupal(); useDolphin(); useTokenPassAuth(); + useWordPressOAuth(); useCustomOAuth(); useCorsSSLConfig(); useOTRMessaging(); diff --git a/apps/meteor/client/views/root/hooks/useWordPressOAuth.ts b/apps/meteor/client/views/root/hooks/useWordPressOAuth.ts new file mode 100644 index 0000000000000..f6a539be2d60e --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useWordPressOAuth.ts @@ -0,0 +1,72 @@ +import type { OauthConfig } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { CustomOAuth } from '../../../../app/custom-oauth/client/CustomOAuth'; + +const configDefault: OauthConfig = { + serverURL: '', + addAutopublishFields: { + forLoggedInUser: ['services.wordpress'], + forOtherUsers: ['services.wordpress.user_login'], + }, + accessTokenParam: 'access_token', +}; + +const WordPress = new CustomOAuth('wordpress', configDefault); + +const configureServerType = ( + serverType: string, + identityPath?: string, + identityTokenSentVia?: string, + tokenPath?: string, + authorizePath?: string, + scope?: string, +) => { + switch (serverType) { + case 'custom': { + return { + ...configDefault, + ...(identityPath && { identityPath }), + ...(identityTokenSentVia && { identityTokenSentVia }), + ...(tokenPath && { tokenPath }), + ...(authorizePath && { authorizePath }), + ...(scope && { scope }), + }; + } + + case 'wordpress-com': + return { + ...configDefault, + identityPath: 'https://public-api.wordpress.com/rest/v1/me', + identityTokenSentVia: 'header', + authorizePath: 'https://public-api.wordpress.com/oauth2/authorize', + tokenPath: 'https://public-api.wordpress.com/oauth2/token', + scope: 'auth', + }; + + default: + return { + ...configDefault, + identityPath: '/oauth/me', + }; + } +}; + +export const useWordPressOAuth = (): void => { + const wordpressURL = useSetting('API_Wordpress_URL') as string; + const serverType = useSetting('Accounts_OAuth_Wordpress_server_type') as string; + const identityPath = useSetting('Accounts_OAuth_Wordpress_identity_path') as string; + const identityTokenSentVia = useSetting('Accounts_OAuth_Wordpress_identity_token_sent_via') as string; + const tokenPath = useSetting('Accounts_OAuth_Wordpress_token_path') as string; + const authorizePath = useSetting('Accounts_OAuth_Wordpress_authorize_path') as string; + const scope = useSetting('Accounts_OAuth_Wordpress_scope') as string; + + useEffect(() => { + WordPress.configure({ + ...configDefault, + ...configureServerType(serverType, identityPath, identityTokenSentVia, tokenPath, authorizePath, scope), + serverURL: wordpressURL, + }); + }, [authorizePath, identityPath, identityTokenSentVia, scope, serverType, tokenPath, wordpressURL]); +}; From 6f44f0e31931877b799220ebb177352f26001986 Mon Sep 17 00:00:00 2001 From: Sushen Oli <89021773+sushen123@users.noreply.github.com> Date: Thu, 27 Mar 2025 04:42:30 +0545 Subject: [PATCH 039/187] fix: Save button remains in loading state after saving app settings (#35394) Co-authored-by: Abhinav Kumar <15830206+abhinavkrin@users.noreply.github.com> --- .changeset/fuzzy-eyes-clean.md | 5 + .../AppDetailsPage/AppDetailsPage.spec.tsx | 150 ++++++++++++++++++ .../AppDetailsPage/AppDetailsPage.tsx | 34 ++-- 3 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 .changeset/fuzzy-eyes-clean.md create mode 100644 apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.spec.tsx diff --git a/.changeset/fuzzy-eyes-clean.md b/.changeset/fuzzy-eyes-clean.md new file mode 100644 index 0000000000000..e2d9c0cc2b7d6 --- /dev/null +++ b/.changeset/fuzzy-eyes-clean.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the save button loading state in app settings, ensuring it resets properly after saving. diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.spec.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.spec.tsx new file mode 100644 index 0000000000000..d6bae87ccc599 --- /dev/null +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.spec.tsx @@ -0,0 +1,150 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import AppDetailsPage from './AppDetailsPage'; +import { AppClientOrchestratorInstance } from '../../../apps/orchestrator'; +import { useAppInfo } from '../hooks/useAppInfo'; + +jest.mock('../hooks/useAppInfo', () => ({ + useAppInfo: jest.fn(), +})); + +jest.mock('@rocket.chat/ui-contexts', () => { + const originalModule = jest.requireActual('@rocket.chat/ui-contexts'); + return { + ...originalModule, + useRouter: () => ({ navigate: jest.fn() }), + useToastMessageDispatch: () => jest.fn(), + usePermission: () => true, + useRouteParameter: () => 'settings', + }; +}); + +jest.mock('../../../components/Page', () => { + const originalModule = jest.requireActual('../../../components/Page'); + return { + ...originalModule, + PageHeader: ({ children }: { children: React.ReactNode }) =>
{children}
, + PageFooter: ({ children, isDirty }: { children: React.ReactNode; isDirty: boolean }) => isDirty &&
{children}
, + }; +}); + +jest.mock('./AppDetailsPageHeader', () => ({ + __esModule: true, + default: () =>
AppDetailsPageHeader
, +})); + +jest.mock('../../../apps/orchestrator', () => ({ + AppClientOrchestratorInstance: { + setAppSettings: jest.fn(), + }, +})); + +const wrapper = mockAppRoot().withTranslations('en', 'core', { Save_changes: 'Save changes' }); +describe('AppDetailsPage', () => { + beforeEach(() => { + (useAppInfo as jest.Mock).mockReturnValue({ + id: 'app123', + name: 'Test App', + installed: true, + settings: { + setting1: { id: 'setting1', value: 'old-value', packageValue: 'default-value', type: 'string' }, + }, + privacyPolicySummary: '', + permissions: [], + tosLink: '', + privacyLink: '', + }); + (AppClientOrchestratorInstance.setAppSettings as jest.Mock).mockReset(); + }); + + it('should not display the Save button initially', async () => { + render(, { + wrapper: wrapper.build(), + legacyRoot: true, + }); + + await waitFor(() => { + expect(screen.queryByRole('button', { name: 'Save changes' })).not.toBeInTheDocument(); + }); + }); + + it('should display the Save button when a setting is changed', async () => { + render(, { + wrapper: wrapper.build(), + legacyRoot: true, + }); + + const settingInput = screen.getByLabelText('setting1'); + await userEvent.clear(settingInput); + await userEvent.type(settingInput, 'new-value'); + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Save changes' })).toBeVisible(); + }); + }); + + it('should disable the Save button during submission', async () => { + (AppClientOrchestratorInstance.setAppSettings as jest.Mock).mockResolvedValueOnce(new Promise((resolve) => setTimeout(resolve, 500))); + + render(, { + wrapper: wrapper.build(), + legacyRoot: true, + }); + + const settingInput = screen.getByLabelText('setting1'); + await userEvent.clear(settingInput); + await userEvent.type(settingInput, 'new-value'); + + const saveButton = screen.getByRole('button', { name: 'Save changes' }); + + await userEvent.click(saveButton); + + await waitFor(() => { + expect(saveButton).toBeDisabled(); + }); + }); + + it('should hide the Save button after successful save', async () => { + (AppClientOrchestratorInstance.setAppSettings as jest.Mock).mockResolvedValueOnce(new Promise((resolve) => setTimeout(resolve, 500))); + + render(, { + wrapper: wrapper.build(), + legacyRoot: true, + }); + + const settingInput = screen.getByLabelText('setting1'); + await userEvent.clear(settingInput); + await userEvent.type(settingInput, 'new-value'); + + const saveButton = screen.getByRole('button', { name: 'Save changes' }); + await userEvent.click(saveButton); + + await waitFor(() => { + expect(screen.queryByRole('button', { name: 'Save changes' })).not.toBeInTheDocument(); + }); + }); + + it('should call setAppSettings with updated setting value', async () => { + (AppClientOrchestratorInstance.setAppSettings as jest.Mock).mockResolvedValueOnce(new Promise((resolve) => setTimeout(resolve, 500))); + + render(, { + wrapper: wrapper.build(), + legacyRoot: true, + }); + + const settingInput = screen.getByLabelText('setting1'); + await userEvent.clear(settingInput); + await userEvent.type(settingInput, 'new-value'); + + const saveButton = screen.getByRole('button', { name: 'Save changes' }); + await userEvent.click(saveButton); + + await waitFor(() => { + expect(AppClientOrchestratorInstance.setAppSettings as jest.Mock).toHaveBeenCalledWith('app123', [ + { id: 'setting1', packageValue: 'default-value', type: 'string', value: 'new-value' }, + ]); + }); + }); +}); diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx index 45aeef3212a65..f31613ebcd396 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx @@ -51,6 +51,20 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => { const { installed, settings, privacyPolicySummary, permissions, tosLink, privacyLink, name } = appData || {}; const isSecurityVisible = Boolean(privacyPolicySummary || permissions || tosLink || privacyLink); + const reducedSettings = useMemo((): AppDetailsPageFormData => { + return Object.values(settings || {}).reduce( + (ret: AppDetailsPageFormData, { id, value, packageValue }) => ({ ...ret, [id]: value ?? packageValue }), + {}, + ); + }, [settings]); + + const methods = useForm({ values: reducedSettings }); + const { + handleSubmit, + reset, + formState: { isDirty, isSubmitting }, + } = methods; + const saveAppSettings = useCallback( async (data: AppDetailsPageFormData) => { try { @@ -61,29 +75,15 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => { value: data[setting.id], })), ); - + reset(data); dispatchToastMessage({ type: 'success', message: `${name} settings saved succesfully` }); } catch (e: any) { handleAPIError(e); } }, - [dispatchToastMessage, id, name, settings], + [dispatchToastMessage, id, name, settings, reset], ); - const reducedSettings = useMemo((): AppDetailsPageFormData => { - return Object.values(settings || {}).reduce( - (ret: AppDetailsPageFormData, { id, value, packageValue }) => ({ ...ret, [id]: value ?? packageValue }), - {}, - ); - }, [settings]); - - const methods = useForm({ values: reducedSettings }); - const { - handleSubmit, - reset, - formState: { isDirty, isSubmitting, isSubmitted }, - } = methods; - return ( @@ -125,7 +125,7 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => { {installed && isAdminUser && ( - )} From a4f2ecb9a69767aedf1fdd8409f3fdec9ea7a106 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 27 Mar 2025 03:58:00 -0300 Subject: [PATCH 040/187] fix: Chat filtering breaks when "From" and "To" have the same date (#35616) --- .changeset/tidy-cups-smoke.md | 5 ++ .../chats/ChatsTable/useChatsQuery.ts | 6 +- ...annel-contact-center-chats-filters.spec.ts | 77 +++++++++++++++++++ ...mnichannel-contact-center-chats-filters.ts | 21 +++++ .../omnichannel-contact-center-chats.ts | 26 +++++++ 5 files changed, 132 insertions(+), 3 deletions(-) create mode 100644 .changeset/tidy-cups-smoke.md create mode 100644 apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats-filters.spec.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats-filters.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts diff --git a/.changeset/tidy-cups-smoke.md b/.changeset/tidy-cups-smoke.md new file mode 100644 index 0000000000000..d7220b0390a03 --- /dev/null +++ b/.changeset/tidy-cups-smoke.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes Omnichannel Contact Center's chats filter not working when "From" and "To" fields have the same date diff --git a/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/useChatsQuery.ts b/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/useChatsQuery.ts index e1cfab9d36659..04e2530578b23 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/useChatsQuery.ts +++ b/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/useChatsQuery.ts @@ -1,5 +1,5 @@ import { usePermission, useUserId } from '@rocket.chat/ui-contexts'; -import moment from 'moment'; +import { parse, endOfDay, startOfDay } from 'date-fns'; import { useCallback } from 'react'; import type { ChatsFiltersQuery } from '../../contexts/ChatsContext'; @@ -47,10 +47,10 @@ export const useChatsQuery = () => { if (from || to) { query.createdAt = JSON.stringify({ ...(from && { - start: moment(new Date(from)).set({ hour: 0, minutes: 0, seconds: 0 }).toISOString(), + start: startOfDay(parse(from, 'yyyy-MM-dd', new Date())).toISOString(), }), ...(to && { - end: moment(new Date(to)).set({ hour: 23, minutes: 59, seconds: 59 }).toISOString(), + end: endOfDay(parse(to, 'yyyy-MM-dd', new Date())).toISOString(), }), }); } diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats-filters.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats-filters.spec.ts new file mode 100644 index 0000000000000..9a62b6b376fac --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center-chats-filters.spec.ts @@ -0,0 +1,77 @@ +import { faker } from '@faker-js/faker/locale/af_ZA'; + +import { Users } from '../fixtures/userStates'; +import { OmnichannelChats } from '../page-objects/omnichannel-contact-center-chats'; +import { setSettingValueById } from '../utils'; +import { createAgent, makeAgentAvailable } from '../utils/omnichannel/agents'; +import { createConversation } from '../utils/omnichannel/rooms'; +import { test, expect } from '../utils/test'; + +test.use({ storageState: Users.user1.state }); + +test.describe('OC - Contact Center - Chats', () => { + let conversations: Awaited>[]; + let agent: Awaited>; + + let poOmniChats: OmnichannelChats; + + const uuid = faker.string.uuid(); + const visitorA = `visitorA_${uuid}`; + const visitorB = `visitorB_${uuid}`; + + test.beforeAll(async ({ api }) => { + expect((await setSettingValueById(api, 'Livechat_Routing_Method', 'Auto_Selection')).status()).toBe(200); + }); + + test.beforeAll(async ({ api }) => { + agent = await createAgent(api, 'user1'); + + expect((await makeAgentAvailable(api, agent.data._id)).status()).toBe(200); + }); + + test.beforeAll(async ({ api }) => { + conversations = await Promise.all([ + createConversation(api, { agentId: `user1`, visitorName: visitorA }), + createConversation(api, { agentId: `user1`, visitorName: visitorB }), + ]); + }); + + test.beforeEach(async ({ page }) => { + poOmniChats = new OmnichannelChats(page); + + await page.goto('/omnichannel-directory/chats'); + }); + + test.afterEach(async ({ page }) => { + await page.close(); + }); + + test.afterAll(async ({ api }) => { + await Promise.all([...conversations.map((conversation) => conversation.delete()), agent.delete()]); + expect((await setSettingValueById(api, 'Livechat_Routing_Method', 'Auto_Selection')).status()).toBe(200); + }); + + test(`OC - Contact Center - Chats - Filter from and to same date`, async ({ page }) => { + await test.step('expect conversations to be visible', async () => { + await poOmniChats.inputSearch.fill(uuid); + await expect(poOmniChats.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.findRowByName(visitorB)).toBeVisible(); + }); + + await test.step('expect to filter [from] and [to] today', async () => { + const [chatA] = conversations.map((c) => c.data); + const [todayString] = new Date(chatA.room.ts).toISOString().split('T'); + await poOmniChats.btnFilters.click(); + await expect(page).toHaveURL('/omnichannel-directory/chats/filters'); + await poOmniChats.filters.inputFrom.fill(todayString); + await poOmniChats.filters.inputTo.fill(todayString); + await poOmniChats.filters.btnApply.click(); + await page.waitForResponse('**/api/v1/livechat/rooms*'); + }); + + await test.step('expect conversations to be visible', async () => { + await expect(poOmniChats.findRowByName(visitorA)).toBeVisible(); + await expect(poOmniChats.findRowByName(visitorB)).toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats-filters.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats-filters.ts new file mode 100644 index 0000000000000..c39f9e26d45d4 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats-filters.ts @@ -0,0 +1,21 @@ +import type { Locator, Page } from '@playwright/test'; + +export class OmnichannelChatsFilters { + protected readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get inputFrom(): Locator { + return this.page.locator('input[name="from"]'); + } + + get inputTo(): Locator { + return this.page.locator('input[name="to"]'); + } + + get btnApply(): Locator { + return this.page.locator('role=button[name="Apply"]'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts new file mode 100644 index 0000000000000..5cf0652890d19 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts @@ -0,0 +1,26 @@ +import type { Locator, Page } from '@playwright/test'; + +import { OmnichannelChatsFilters } from './omnichannel-contact-center-chats-filters'; + +export class OmnichannelChats { + private readonly page: Page; + + filters: OmnichannelChatsFilters; + + constructor(page: Page) { + this.page = page; + this.filters = new OmnichannelChatsFilters(page); + } + + get btnFilters(): Locator { + return this.page.locator('role=button[name="Filters"]'); + } + + get inputSearch(): Locator { + return this.page.locator('role=textbox[name="Search"]'); + } + + findRowByName(contactName: string) { + return this.page.locator(`td >> text="${contactName}"`); + } +} From 6e2b5834bdba55f9aef51886f641671aedcc6245 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 27 Mar 2025 10:33:51 -0300 Subject: [PATCH 041/187] fix: Missing margin in E2EE room header toolbox items (#35614) --- .changeset/sweet-ravens-refuse.md | 5 +++++ .../room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx | 9 ++++++++- .../room/HeaderV2/RoomToolbox/RoomToolboxE2EESetup.tsx | 9 ++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 .changeset/sweet-ravens-refuse.md diff --git a/.changeset/sweet-ravens-refuse.md b/.changeset/sweet-ravens-refuse.md new file mode 100644 index 0000000000000..52c0ee8214565 --- /dev/null +++ b/.changeset/sweet-ravens-refuse.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where header toolbox items are missing the proper margin in e2ee setup room header diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx index 36bf5701f2152..1d87a4887084e 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx +++ b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolboxE2EESetup.tsx @@ -1,4 +1,6 @@ +import type { Box } from '@rocket.chat/fuselage'; import { useStableArray } from '@rocket.chat/fuselage-hooks'; +import type { ComponentProps } from 'react'; import { useTranslation } from 'react-i18next'; import { HeaderToolbarAction } from '../../../../components/Header'; @@ -8,7 +10,11 @@ import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext' import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import { getRoomGroup } from '../../lib/getRoomGroup'; -const RoomToolboxE2EESetup = () => { +type RoomToolboxE2EESetupProps = { + className?: ComponentProps['className']; +}; + +const RoomToolboxE2EESetup = ({ className }: RoomToolboxE2EESetupProps) => { const { t } = useTranslation(); const toolbox = useRoomToolbox(); const room = useRoom(); @@ -29,6 +35,7 @@ const RoomToolboxE2EESetup = () => { {actions.map(({ id, icon, title, action, disabled, tooltip }, index) => ( { +type RoomToolboxE2EESetupProps = { + className?: ComponentProps['className']; +}; + +const RoomToolboxE2EESetup = ({ className }: RoomToolboxE2EESetupProps) => { const { t } = useTranslation(); const toolbox = useRoomToolbox(); @@ -23,6 +29,7 @@ const RoomToolboxE2EESetup = () => { {actions.map(({ id, icon, title, action, disabled, tooltip }, index) => ( Date: Thu, 27 Mar 2025 11:41:03 -0300 Subject: [PATCH 042/187] chore: remove unreadMessages method usage (#35635) --- .../meteor/app/api/server/v1/subscriptions.ts | 6 +++++- .../server/unreadMessages.ts | 6 +++--- .../message/hooks/useMarkAsUnreadMutation.tsx | 21 +++++++++++++++---- .../hooks/menuActions/useToggleReadAction.ts | 8 ++++--- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts index 200fa7a5eabb9..a3192aeb9ec14 100644 --- a/apps/meteor/app/api/server/v1/subscriptions.ts +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -100,7 +100,11 @@ API.v1.addRoute( }, { async post() { - await unreadMessages(this.userId, (this.bodyParams as any).firstUnreadMessage, (this.bodyParams as any).roomId); + await unreadMessages( + this.userId, + 'firstUnreadMessage' in this.bodyParams ? this.bodyParams.firstUnreadMessage : undefined, + 'roomId' in this.bodyParams ? this.bodyParams.roomId : undefined, + ); return API.v1.success(); }, diff --git a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts index 407ba73806864..202dde93d062d 100644 --- a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts +++ b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts @@ -9,11 +9,11 @@ import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../lib/server/l declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - unreadMessages(firstUnreadMessage?: IMessage, room?: IRoom['_id']): void; + unreadMessages(firstUnreadMessage?: Pick, room?: IRoom['_id']): void; } } -export const unreadMessages = async (userId: string, firstUnreadMessage?: IMessage, room?: IRoom['_id']): Promise => { +export const unreadMessages = async (userId: string, firstUnreadMessage?: Pick, room?: IRoom['_id']): Promise => { if (room && typeof room === 'string') { const lastMessage = ( await Messages.findVisibleByRoomId(room, { @@ -65,7 +65,7 @@ export const unreadMessages = async (userId: string, firstUnreadMessage?: IMessa }); } - if (firstUnreadMessage.ts >= lastSeen) { + if (originalMessage.ts >= lastSeen) { return logger.debug('Provided message is already marked as unread'); } diff --git a/apps/meteor/client/components/message/hooks/useMarkAsUnreadMutation.tsx b/apps/meteor/client/components/message/hooks/useMarkAsUnreadMutation.tsx index cd52c5e34dd85..a8d2c0e4f9ba6 100644 --- a/apps/meteor/client/components/message/hooks/useMarkAsUnreadMutation.tsx +++ b/apps/meteor/client/components/message/hooks/useMarkAsUnreadMutation.tsx @@ -1,17 +1,30 @@ import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; -import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; import { LegacyRoomManager } from '../../../../app/ui-utils/client'; -import { sdk } from '../../../../app/utils/client/lib/SDKClient'; export const useMarkAsUnreadMutation = () => { const dispatchToastMessage = useToastMessageDispatch(); + const unreadMessages = useEndpoint('POST', '/v1/subscriptions.unread'); return useMutation({ - mutationFn: async ({ message, subscription }: { message: IMessage; subscription: ISubscription }) => { + mutationFn: async ({ + subscription, + ...props + }: + | { message: IMessage; subscription: ISubscription } + | { + roomId: string; + subscription: ISubscription; + }) => { await LegacyRoomManager.close(subscription.t + subscription.name); - await sdk.call('unreadMessages', message); + if ('message' in props) { + const { message } = props; + await unreadMessages({ firstUnreadMessage: { _id: message._id } }); + return; + } + await unreadMessages({ roomId: props.roomId }); }, onError: (error) => { dispatchToastMessage({ type: 'error', message: error }); diff --git a/apps/meteor/client/hooks/menuActions/useToggleReadAction.ts b/apps/meteor/client/hooks/menuActions/useToggleReadAction.ts index 133acd9b0f789..d2bdfec8fe305 100644 --- a/apps/meteor/client/hooks/menuActions/useToggleReadAction.ts +++ b/apps/meteor/client/hooks/menuActions/useToggleReadAction.ts @@ -1,9 +1,10 @@ import type { ISubscription } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useEndpoint, useMethod, useRouter, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useRouter, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; import { LegacyRoomManager } from '../../../app/ui-utils/client'; +import { useMarkAsUnreadMutation } from '../../components/message/hooks/useMarkAsUnreadMutation'; type ToggleReadActionProps = { rid: string; @@ -17,7 +18,8 @@ export const useToggleReadAction = ({ rid, isUnread, subscription }: ToggleReadA const router = useRouter(); const readMessages = useEndpoint('POST', '/v1/subscriptions.read'); - const unreadMessages = useMethod('unreadMessages'); + + const unreadMessages = useMarkAsUnreadMutation(); const handleToggleRead = useEffectEvent(async () => { try { @@ -38,7 +40,7 @@ export const useToggleReadAction = ({ rid, isUnread, subscription }: ToggleReadA router.navigate('/home'); - await unreadMessages(undefined, rid); + await unreadMessages.mutateAsync({ roomId: rid, subscription }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } From 0cc6e9e1ea72fa7580557b7febaaaafbae063b51 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 27 Mar 2025 13:46:02 -0300 Subject: [PATCH 043/187] chore: Add `darkMode` configuration to `ui-client` storybook (#35629) --- .../ui-client/.storybook/DocsContainer.tsx | 21 ++++++ packages/ui-client/.storybook/logo.svg | 68 +++++++++++++++++++ packages/ui-client/.storybook/logo.svg.d.ts | 3 + packages/ui-client/.storybook/main.ts | 2 + packages/ui-client/.storybook/preview.tsx | 65 +++++++++++++----- packages/ui-client/package.json | 2 + yarn.lock | 2 + 7 files changed, 147 insertions(+), 16 deletions(-) create mode 100644 packages/ui-client/.storybook/DocsContainer.tsx create mode 100644 packages/ui-client/.storybook/logo.svg create mode 100644 packages/ui-client/.storybook/logo.svg.d.ts diff --git a/packages/ui-client/.storybook/DocsContainer.tsx b/packages/ui-client/.storybook/DocsContainer.tsx new file mode 100644 index 0000000000000..217b6b05910d1 --- /dev/null +++ b/packages/ui-client/.storybook/DocsContainer.tsx @@ -0,0 +1,21 @@ +import { DocsContainer as BaseContainer } from '@storybook/blocks'; +import { addons } from '@storybook/preview-api'; +import { themes } from '@storybook/theming'; +import type { ComponentPropsWithoutRef } from 'react'; +import { useEffect, useState } from 'react'; +import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode'; + +const channel = addons.getChannel(); + +const DocsContainer = (props: ComponentPropsWithoutRef) => { + const [isDark, setDark] = useState(false); + + useEffect(() => { + channel.on(DARK_MODE_EVENT_NAME, setDark); + return () => channel.removeListener(DARK_MODE_EVENT_NAME, setDark); + }, [setDark]); + + return ; +}; + +export default DocsContainer; diff --git a/packages/ui-client/.storybook/logo.svg b/packages/ui-client/.storybook/logo.svg new file mode 100644 index 0000000000000..9f732657872d4 --- /dev/null +++ b/packages/ui-client/.storybook/logo.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/ui-client/.storybook/logo.svg.d.ts b/packages/ui-client/.storybook/logo.svg.d.ts new file mode 100644 index 0000000000000..27c0914b230f0 --- /dev/null +++ b/packages/ui-client/.storybook/logo.svg.d.ts @@ -0,0 +1,3 @@ +declare const path: string; + +export = path; diff --git a/packages/ui-client/.storybook/main.ts b/packages/ui-client/.storybook/main.ts index 53ce127e65afb..0dae84a3b1798 100644 --- a/packages/ui-client/.storybook/main.ts +++ b/packages/ui-client/.storybook/main.ts @@ -5,7 +5,9 @@ import type { StorybookConfig } from '@storybook/react-webpack5'; const config: StorybookConfig = { stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ + getAbsolutePath('@storybook/addon-a11y'), getAbsolutePath('@storybook/addon-essentials'), + getAbsolutePath('storybook-dark-mode'), getAbsolutePath('@storybook/addon-webpack5-compiler-babel'), getAbsolutePath('@storybook/addon-styling-webpack'), ], diff --git a/packages/ui-client/.storybook/preview.tsx b/packages/ui-client/.storybook/preview.tsx index 75d4937b1beda..dbd4c788135d6 100644 --- a/packages/ui-client/.storybook/preview.tsx +++ b/packages/ui-client/.storybook/preview.tsx @@ -1,28 +1,61 @@ +import { PaletteStyleTag } from '@rocket.chat/fuselage'; +import surface from '@rocket.chat/fuselage-tokens/dist/surface.json'; import type { Decorator, Parameters } from '@storybook/react'; +import { themes } from '@storybook/theming'; +import { useDarkMode } from 'storybook-dark-mode'; -import '../../../apps/meteor/app/theme/client/main.css'; -import 'highlight.js/styles/github.css'; +import manifest from '../package.json'; +import DocsContainer from './DocsContainer'; +import logo from './logo.svg'; +import '@rocket.chat/fuselage/dist/fuselage.css'; import '@rocket.chat/icons/dist/rocketchat.css'; export const parameters: Parameters = { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/, + backgrounds: { + grid: { + cellSize: 4, + cellAmount: 4, + opacity: 0.5, + }, + }, + options: { + storySort: { + method: 'alphabetical', + }, + }, + layout: 'fullscreen', + docs: { + container: DocsContainer, + }, + darkMode: { + dark: { + ...themes.dark, + appBg: surface.surface.dark.sidebar, + appContentBg: surface.surface.dark.light, + appPreviewBg: 'transparent', + barBg: surface.surface.dark.light, + brandTitle: manifest.name, + brandImage: logo, + }, + light: { + ...themes.normal, + appPreviewBg: 'transparent', + brandTitle: manifest.name, + brandImage: logo, }, }, }; export const decorators: Decorator[] = [ - (Story) => ( -
- - -
- ), + (Story) => { + const dark = useDarkMode(); + + return ( + <> + + + + ); + }, ]; export const tags = ['autodocs']; diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 9024abb100f95..0775052e651a7 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -28,6 +28,7 @@ "@rocket.chat/mock-providers": "workspace:^", "@rocket.chat/ui-avatar": "workspace:~", "@rocket.chat/ui-contexts": "workspace:~", + "@storybook/addon-a11y": "^8.6.4", "@storybook/addon-actions": "^8.6.4", "@storybook/addon-docs": "^8.6.4", "@storybook/addon-essentials": "^8.6.4", @@ -53,6 +54,7 @@ "react-dom": "~18.3.1", "react-hook-form": "~7.45.4", "storybook": "^8.6.4", + "storybook-dark-mode": "^4.0.2", "typescript": "~5.7.2" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index 27495dd1e7bf8..dad90af782b2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9775,6 +9775,7 @@ __metadata: "@rocket.chat/mock-providers": "workspace:^" "@rocket.chat/ui-avatar": "workspace:~" "@rocket.chat/ui-contexts": "workspace:~" + "@storybook/addon-a11y": "npm:^8.6.4" "@storybook/addon-actions": "npm:^8.6.4" "@storybook/addon-docs": "npm:^8.6.4" "@storybook/addon-essentials": "npm:^8.6.4" @@ -9801,6 +9802,7 @@ __metadata: react-dom: "npm:~18.3.1" react-hook-form: "npm:~7.45.4" storybook: "npm:^8.6.4" + storybook-dark-mode: "npm:^4.0.2" typescript: "npm:~5.7.2" peerDependencies: "@react-aria/toolbar": "*" From 85a02aa02cad66a2b21ea30fb9ae9f19e8ae4fb2 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 28 Mar 2025 13:06:30 -0300 Subject: [PATCH 044/187] chore: remove `useLegacyThreadMessageJump` (#35636) --- .../Threads/components/ThreadMessageList.tsx | 12 +-- .../hooks/useLegacyThreadMessageJump.ts | 73 ------------------- 2 files changed, 6 insertions(+), 79 deletions(-) delete mode 100644 apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageJump.ts 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 8c1be447b989f..ed0ed10a488cc 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx @@ -4,7 +4,7 @@ import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; import { differenceInSeconds } from 'date-fns'; import type { ReactElement } from 'react'; -import { Fragment } from 'react'; +import { Fragment, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { ThreadMessageItem } from './ThreadMessageItem'; @@ -18,7 +18,6 @@ import LoadingMessagesIndicator from '../../../body/LoadingMessagesIndicator'; import { useDateScroll } from '../../../hooks/useDateScroll'; import { useFirstUnreadMessageId } from '../../../hooks/useFirstUnreadMessageId'; import { useMessageListNavigation } from '../../../hooks/useMessageListNavigation'; -import { useLegacyThreadMessageJump } from '../hooks/useLegacyThreadMessageJump'; import { useLegacyThreadMessageListScrolling } from '../hooks/useLegacyThreadMessageListScrolling'; import { useLegacyThreadMessages } from '../hooks/useLegacyThreadMessages'; import './threads.css'; @@ -55,12 +54,12 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen const { innerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll(); const { messages, loading } = useLegacyThreadMessages(mainMessage._id); + const { listWrapperRef: listWrapperScrollRef, listRef: listScrollRef, onScroll: handleScroll, } = useLegacyThreadMessageListScrolling(mainMessage); - const { parentRef: listJumpRef } = useLegacyThreadMessageJump({ enabled: !loading }); const hideUsernames = useUserPreference('hideUsernames'); const showUserAvatar = !!useUserPreference('displayAvatars'); @@ -68,9 +67,10 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen const messageGroupingPeriod = useSetting('Message_GroupingPeriod', 300); const { messageListRef } = useMessageListNavigation(); - const listRef = useMergedRefs(listScrollRef, messageListRef); - const scrollRef = useMergedRefs(innerRef, listWrapperScrollRef, listJumpRef); + const ref = useRef(null); + const listRef = useMergedRefs(listScrollRef, messageListRef); + const scrollRef = useMergedRefs(innerRef, listWrapperScrollRef, ref); return (
@@ -88,7 +88,7 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen ) : ( - + {[mainMessage, ...messages].map((message, index, { [index - 1]: previous }) => { const sequential = isMessageSequential(message, previous, messageGroupingPeriod); const newDay = isMessageNewDay(message, previous); diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageJump.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageJump.ts deleted file mode 100644 index 551d08d5f1420..0000000000000 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessageJump.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { useRouter, useSearchParameter } from '@rocket.chat/ui-contexts'; -import { useRef, useEffect } from 'react'; - -import { waitForElement } from '../../../../../lib/utils/waitForElement'; -import { clearHighlightMessage, setHighlightMessage } from '../../../MessageList/providers/messageHighlightSubscription'; - -export const useLegacyThreadMessageJump = ({ enabled = true }: { enabled?: boolean }) => { - const router = useRouter(); - const mid = useSearchParameter('msg'); - - const clearQueryStringParameter = () => { - const name = router.getRouteName(); - - if (!name) { - return; - } - - const { msg: _, ...search } = router.getSearchParameters(); - - router.navigate( - { - name, - params: router.getRouteParameters(), - search, - }, - { replace: true }, - ); - }; - - const parentRef = useRef(null); - const clearQueryStringParameterRef = useRef(clearQueryStringParameter); - clearQueryStringParameterRef.current = clearQueryStringParameter; - - useEffect(() => { - const parent = parentRef.current; - - if (!enabled || !mid || !parent) { - return; - } - - const abortController = new AbortController(); - - waitForElement(`[data-id='${mid}']`, { parent, signal: abortController.signal }).then((messageElement) => { - if (abortController.signal.aborted) { - return; - } - - setHighlightMessage(mid); - clearQueryStringParameterRef.current?.(); - - setTimeout(() => { - clearHighlightMessage(); - }, 1000); - - setTimeout(() => { - if (abortController.signal.aborted) { - return; - } - - messageElement.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }); - }, 300); - }); - - return () => { - abortController.abort(); - }; - }, [enabled, mid]); - - return { parentRef }; -}; From f77f113542be4fb7df938b1bf7836de6ba3f4f51 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 28 Mar 2025 14:18:34 -0300 Subject: [PATCH 045/187] chore: Replace `LivechatInquiry` collection (#35641) --- .../client/collections/LivechatInquiry.ts | 4 -- .../client/lib/stream/queueManager.ts | 30 +++++++++----- .../client/hooks/useLivechatInquiryStore.ts | 40 +++++++++++++++++++ .../client/providers/OmnichannelProvider.tsx | 25 +++++------- .../QuickActions/hooks/useQuickActions.tsx | 8 ++-- .../QuickActions/hooks/useQuickActions.tsx | 8 ++-- 6 files changed, 82 insertions(+), 33 deletions(-) delete mode 100644 apps/meteor/app/livechat/client/collections/LivechatInquiry.ts create mode 100644 apps/meteor/client/hooks/useLivechatInquiryStore.ts diff --git a/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts b/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts deleted file mode 100644 index 16b9533d1649e..0000000000000 --- a/apps/meteor/app/livechat/client/collections/LivechatInquiry.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { ILivechatInquiryRecord } from '@rocket.chat/core-typings'; -import { Mongo } from 'meteor/mongo'; - -export const LivechatInquiry = new Mongo.Collection(null); diff --git a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts index 78769e5a960c6..caa7e74f81917 100644 --- a/apps/meteor/app/livechat/client/lib/stream/queueManager.ts +++ b/apps/meteor/app/livechat/client/lib/stream/queueManager.ts @@ -1,10 +1,11 @@ -import type { ILivechatDepartment, ILivechatInquiryRecord, IOmnichannelAgent } from '@rocket.chat/core-typings'; +import type { ILivechatDepartment, ILivechatInquiryRecord, IOmnichannelAgent, Serialized } from '@rocket.chat/core-typings'; +import { useLivechatInquiryStore } from '../../../../../client/hooks/useLivechatInquiryStore'; import { queryClient } from '../../../../../client/lib/queryClient'; import { callWithErrorHandling } from '../../../../../client/lib/utils/callWithErrorHandling'; +import { mapMessageFromApi } from '../../../../../client/lib/utils/mapMessageFromApi'; import { settings } from '../../../../settings/client'; import { sdk } from '../../../../utils/client/lib/SDKClient'; -import { LivechatInquiry } from '../../collections/LivechatInquiry'; const departments = new Set(); @@ -14,7 +15,7 @@ const events = { return; } - LivechatInquiry.insert({ ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); + useLivechatInquiryStore.getState().add({ ...inquiry, alert: true }); await invalidateRoomQueries(inquiry.rid); }, changed: async (inquiry: ILivechatInquiryRecord) => { @@ -22,7 +23,7 @@ const events = { return removeInquiry(inquiry); } - LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, alert: true, _updatedAt: new Date(inquiry._updatedAt) }); + useLivechatInquiryStore.getState().merge({ ...inquiry, alert: true }); await invalidateRoomQueries(inquiry.rid); }, removed: (inquiry: ILivechatInquiryRecord) => removeInquiry(inquiry), @@ -49,7 +50,7 @@ const invalidateRoomQueries = async (rid: string) => { }; const removeInquiry = async (inquiry: ILivechatInquiryRecord) => { - LivechatInquiry.remove(inquiry._id); + useLivechatInquiryStore.getState().discard(inquiry._id); return queryClient.invalidateQueries({ queryKey: ['rooms', { reference: inquiry.rid, type: 'l' }] }); }; @@ -76,8 +77,19 @@ const addListenerForeachDepartment = (departments: ILivechatDepartment['_id'][] return () => cleanupFunctions.forEach((cleanup) => cleanup()); }; -const updateInquiries = async (inquiries: ILivechatInquiryRecord[] = []) => - inquiries.forEach((inquiry) => LivechatInquiry.upsert({ _id: inquiry._id }, { ...inquiry, _updatedAt: new Date(inquiry._updatedAt) })); +const updateInquiries = async (inquiries: Serialized[] = []) => + inquiries.forEach((inquiry) => { + useLivechatInquiryStore.getState().merge({ + ...inquiry, + alert: true, + ts: new Date(inquiry.ts), + v: { ...inquiry.v, lastMessageTs: inquiry.v.lastMessageTs ? new Date(inquiry.v.lastMessageTs) : undefined }, + estimatedInactivityCloseTimeAt: inquiry.estimatedInactivityCloseTimeAt ? new Date(inquiry.estimatedInactivityCloseTimeAt) : undefined, + lockedAt: inquiry.lockedAt ? new Date(inquiry.lockedAt) : undefined, + lastMessage: inquiry.lastMessage ? mapMessageFromApi(inquiry.lastMessage) : undefined, + _updatedAt: new Date(inquiry._updatedAt), + }); + }); const getAgentsDepartments = async (userId: IOmnichannelAgent['_id']) => { const { departments } = await sdk.rest.get(`/v1/livechat/agents/${userId}/departments`, { enabledDepartmentsOnly: 'true' }); @@ -118,13 +130,13 @@ const subscribe = async (userId: IOmnichannelAgent['_id']) => { const globalCleanup = addGlobalListener(); const computation = Tracker.autorun(async () => { - const inquiriesFromAPI = (await getInquiriesFromAPI()) as unknown as ILivechatInquiryRecord[]; + const inquiriesFromAPI = await getInquiriesFromAPI(); await updateInquiries(inquiriesFromAPI); }); return () => { - LivechatInquiry.remove({}); + useLivechatInquiryStore.getState().discardAll(); removeGlobalListener(); cleanAgentListener?.(); cleanDepartmentListeners?.(); diff --git a/apps/meteor/client/hooks/useLivechatInquiryStore.ts b/apps/meteor/client/hooks/useLivechatInquiryStore.ts new file mode 100644 index 0000000000000..ba33752c3bbbf --- /dev/null +++ b/apps/meteor/client/hooks/useLivechatInquiryStore.ts @@ -0,0 +1,40 @@ +import type { ILivechatInquiryRecord, IRoom } from '@rocket.chat/core-typings'; +import { create } from 'zustand'; + +export const useLivechatInquiryStore = create<{ + records: (ILivechatInquiryRecord & { alert?: boolean })[]; + add: (record: ILivechatInquiryRecord & { alert?: boolean }) => void; + merge: (record: ILivechatInquiryRecord & { alert?: boolean }) => void; + discard: (id: ILivechatInquiryRecord['_id']) => void; + discardForRoom: (rid: IRoom['_id']) => void; + discardAll: () => void; +}>()((set) => ({ + records: [], + + add: (record) => { + set(({ records }) => ({ records: [...records, record] })); + }, + + merge: (record) => { + set(({ records }) => { + const index = records.findIndex((r) => r._id === record._id); + if (index === -1) { + return { records: [...records, record] }; + } + records[index] = record; + return { records: [...records] }; + }); + }, + + discard: (id) => { + set(({ records }) => ({ records: records.filter((r) => r._id !== id) })); + }, + + discardForRoom: (rid) => { + set(({ records }) => ({ records: records.filter((r) => r.rid !== rid) })); + }, + + discardAll: () => { + set(() => ({ records: [] })); + }, +})); diff --git a/apps/meteor/client/providers/OmnichannelProvider.tsx b/apps/meteor/client/providers/OmnichannelProvider.tsx index c20264571ed31..cb39b9a4abafb 100644 --- a/apps/meteor/client/providers/OmnichannelProvider.tsx +++ b/apps/meteor/client/providers/OmnichannelProvider.tsx @@ -2,16 +2,16 @@ import { type IOmnichannelAgent, type OmichannelRoutingConfig, OmnichannelSortingMechanismSettingType, - type ILivechatInquiryRecord, LivechatInquiryStatus, } from '@rocket.chat/core-typings'; import { useSafely } from '@rocket.chat/fuselage-hooks'; +import { createComparatorFromSort } from '@rocket.chat/mongo-adapter'; import { useUser, useSetting, usePermission, useMethod, useEndpoint, useStream } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { ReactNode } from 'react'; -import { useState, useEffect, useMemo, useCallback, memo, useRef } from 'react'; +import { useState, useEffect, useMemo, memo, useRef } from 'react'; +import { useShallow } from 'zustand/shallow'; -import { LivechatInquiry } from '../../app/livechat/client/collections/LivechatInquiry'; import { initializeLivechatInquiryStream } from '../../app/livechat/client/lib/stream/queueManager'; import { getOmniChatSortQuery } from '../../app/livechat/lib/inquiries'; import { ClientLogger } from '../../lib/ClientLogger'; @@ -19,8 +19,8 @@ import type { OmnichannelContextValue } from '../contexts/OmnichannelContext'; import { OmnichannelContext } from '../contexts/OmnichannelContext'; import { useNewRoomNotification } from '../hooks/notification/useNewRoomNotification'; import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; +import { useLivechatInquiryStore } from '../hooks/useLivechatInquiryStore'; import { useOmnichannelContinuousSoundNotification } from '../hooks/useOmnichannelContinuousSoundNotification'; -import { useReactiveValue } from '../hooks/useReactiveValue'; import { useShouldPreventAction } from '../hooks/useShouldPreventAction'; const emptyContextValue: OmnichannelContextValue = { @@ -142,20 +142,17 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { return streamNotifyUser(`${user._id}/departmentAgentData`, handleDepartmentAgentData); }, [manuallySelected, streamNotifyUser, user?._id]); - const queue = useReactiveValue( - useCallback(() => { + const queue = useLivechatInquiryStore( + useShallow((state) => { if (!manuallySelected) { return undefined; } - return LivechatInquiry.find( - { status: LivechatInquiryStatus.QUEUED }, - { - sort: getOmniChatSortQuery(omnichannelSortingMechanism), - limit: omnichannelPoolMaxIncoming, - }, - ).fetch(); - }, [manuallySelected, omnichannelPoolMaxIncoming, omnichannelSortingMechanism]), + return state.records + .filter((inquiry) => inquiry.status === LivechatInquiryStatus.QUEUED) + .sort(createComparatorFromSort(getOmniChatSortQuery(omnichannelSortingMechanism))) + .slice(...(omnichannelPoolMaxIncoming > 0 ? [0, omnichannelPoolMaxIncoming] : [])); + }), ); useEffect(() => { diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index 7f1c20e343d92..ded93aa4741be 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -16,7 +16,6 @@ import { useCallback, useState, useEffect } from 'react'; import { usePutChatOnHoldMutation } from './usePutChatOnHoldMutation'; import { useReturnChatToQueueMutation } from './useReturnChatToQueueMutation'; -import { LivechatInquiry } from '../../../../../../../app/livechat/client/collections/LivechatInquiry'; import PlaceChatOnHoldModal from '../../../../../../../app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal'; import { LegacyRoomManager } from '../../../../../../../app/ui-utils/client'; import CloseChatModal from '../../../../../../components/Omnichannel/modals/CloseChatModal'; @@ -27,6 +26,7 @@ import TranscriptModal from '../../../../../../components/Omnichannel/modals/Tra import { useIsRoomOverMacLimit } from '../../../../../../hooks/omnichannel/useIsRoomOverMacLimit'; import { useOmnichannelRouteConfig } from '../../../../../../hooks/omnichannel/useOmnichannelRouteConfig'; import { useHasLicenseModule } from '../../../../../../hooks/useHasLicenseModule'; +import { useLivechatInquiryStore } from '../../../../../../hooks/useLivechatInquiryStore'; import { quickActionHooks } from '../../../../../../ui'; import { useOmnichannelRoom } from '../../../../contexts/RoomContext'; import type { QuickActionsActionConfig } from '../../../../lib/quickActions'; @@ -177,6 +177,8 @@ export const useQuickActions = (): { const closeChat = useEndpoint('POST', '/v1/livechat/room.closeByUser'); + const discardForRoom = useLivechatInquiryStore((state) => state.discardForRoom); + const handleClose = useCallback( async ( comment?: string, @@ -199,14 +201,14 @@ export const useQuickActions = (): { } : { transcriptEmail: { sendToVisitor: false } }), }); - LivechatInquiry.remove({ rid }); + discardForRoom(rid); closeModal(); dispatchToastMessage({ type: 'success', message: t('Chat_closed_successfully') }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }, - [closeChat, closeModal, dispatchToastMessage, rid, t], + [closeChat, closeModal, dispatchToastMessage, rid, t, discardForRoom], ); const returnChatToQueueMutation = useReturnChatToQueueMutation({ diff --git a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useQuickActions.tsx index a3a6509bd9a52..b454582f062dc 100644 --- a/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -15,7 +15,6 @@ import { useCallback, useState, useEffect } from 'react'; import { usePutChatOnHoldMutation } from './usePutChatOnHoldMutation'; import { useReturnChatToQueueMutation } from './useReturnChatToQueueMutation'; -import { LivechatInquiry } from '../../../../../../../app/livechat/client/collections/LivechatInquiry'; import PlaceChatOnHoldModal from '../../../../../../../app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal'; import { LegacyRoomManager } from '../../../../../../../app/ui-utils/client'; import CloseChatModal from '../../../../../../components/Omnichannel/modals/CloseChatModal'; @@ -26,6 +25,7 @@ import TranscriptModal from '../../../../../../components/Omnichannel/modals/Tra import { useIsRoomOverMacLimit } from '../../../../../../hooks/omnichannel/useIsRoomOverMacLimit'; import { useOmnichannelRouteConfig } from '../../../../../../hooks/omnichannel/useOmnichannelRouteConfig'; import { useHasLicenseModule } from '../../../../../../hooks/useHasLicenseModule'; +import { useLivechatInquiryStore } from '../../../../../../hooks/useLivechatInquiryStore'; import { quickActionHooks } from '../../../../../../ui'; import { useOmnichannelRoom } from '../../../../contexts/RoomContext'; import type { QuickActionsActionConfig } from '../../../../lib/quickActions'; @@ -175,6 +175,8 @@ export const useQuickActions = (): { const closeChat = useEndpoint('POST', '/v1/livechat/room.closeByUser'); + const discardForRoom = useLivechatInquiryStore((state) => state.discardForRoom); + const handleClose = useCallback( async ( comment?: string, @@ -197,14 +199,14 @@ export const useQuickActions = (): { } : { transcriptEmail: { sendToVisitor: false } }), }); - LivechatInquiry.remove({ rid }); + discardForRoom(rid); closeModal(); dispatchToastMessage({ type: 'success', message: t('Chat_closed_successfully') }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }, - [closeChat, closeModal, dispatchToastMessage, rid, t], + [closeChat, closeModal, dispatchToastMessage, rid, t, discardForRoom], ); const returnChatToQueueMutation = useReturnChatToQueueMutation({ From 1dbf16278dc11d895703fa8e29a107f589c3410c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 28 Mar 2025 15:24:41 -0300 Subject: [PATCH 046/187] test: flaky - stop closing page before sending the message (#35646) --- apps/meteor/tests/e2e/mark-unread.spec.ts | 50 ++++++++++--------- .../tests/e2e/utils/create-target-channel.ts | 8 +++ 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/apps/meteor/tests/e2e/mark-unread.spec.ts b/apps/meteor/tests/e2e/mark-unread.spec.ts index 46e5d206e5b6f..7dcc6762ff07a 100644 --- a/apps/meteor/tests/e2e/mark-unread.spec.ts +++ b/apps/meteor/tests/e2e/mark-unread.spec.ts @@ -1,68 +1,70 @@ -import { createAuxContext } from './fixtures/createAuxContext'; +import type { IRoom } from '@rocket.chat/core-typings'; + import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects'; -import { createTargetChannel } from './utils'; +import { createTargetChannelAndReturnFullRoom } from './utils'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); test.describe.serial('mark-unread', () => { let poHomeChannel: HomeChannel; - let targetChannel: string; + let targetChannel: Required; test.beforeEach(async ({ page, api }) => { poHomeChannel = new HomeChannel(page); - targetChannel = await createTargetChannel(api, { members: ['user2'] }); + const result = await createTargetChannelAndReturnFullRoom(api, { members: ['user2'] }); + targetChannel = result.channel as Required; await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto('/home'); }); test.afterEach(async ({ api }) => { - await api.post('/channels.delete', { roomName: targetChannel }); + await api.post('/channels.delete', { roomName: targetChannel.name }); }); test.describe('Mark Unread - Sidebar Action', () => { test('should not mark empty room as unread', async () => { - await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel.name); - await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel)).not.toBeVisible(); + await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel.name)).not.toBeVisible(); }); test('should mark a populated room as unread', async () => { - await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.sidenav.openChat(targetChannel.name); await poHomeChannel.content.sendMessage('this is a message for reply'); - await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel.name); - await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel)).toBeVisible(); + await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel.name)).toBeVisible(); }); test('should mark a populated room as unread - search', async () => { - await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.sidenav.openChat(targetChannel.name); await poHomeChannel.content.sendMessage('this is a message for reply'); - await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); - await poHomeChannel.sidenav.searchRoom(targetChannel); + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel.name); + await poHomeChannel.sidenav.searchRoom(targetChannel.name); - await expect(poHomeChannel.sidenav.getSearchItemBadge(targetChannel)).toBeVisible(); + await expect(poHomeChannel.sidenav.getSearchItemBadge(targetChannel.name)).toBeVisible(); }); }); test.describe('Mark Unread - Message Action', () => { - let poHomeChannelUser2: HomeChannel; - - test('should mark a populated room as unread', async ({ browser }) => { - const { page: user2Page } = await createAuxContext(browser, Users.user2); - poHomeChannelUser2 = new HomeChannel(user2Page); + test.use({ storageState: Users.user2.state }); - await poHomeChannelUser2.sidenav.openChat(targetChannel); - await poHomeChannelUser2.content.sendMessage('this is a message for reply'); - await user2Page.close(); + test('should mark a populated room as unread', async ({ api }) => { + await api.post('/chat.sendMessage', { + message: { + rid: targetChannel._id, + msg: 'this is a message for reply', + }, + }); await expect(async () => { - await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.sidenav.openChat(targetChannel.name); await poHomeChannel.content.openLastMessageMenu(); await poHomeChannel.markUnread.click(); - await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel)).toBeVisible(); + await expect(poHomeChannel.sidenav.getSidebarItemBadge(targetChannel.name)).toBeVisible(); }).toPass(); }); }); diff --git a/apps/meteor/tests/e2e/utils/create-target-channel.ts b/apps/meteor/tests/e2e/utils/create-target-channel.ts index 777bb99e226d5..1bccd514fb5bd 100644 --- a/apps/meteor/tests/e2e/utils/create-target-channel.ts +++ b/apps/meteor/tests/e2e/utils/create-target-channel.ts @@ -15,6 +15,14 @@ export async function createTargetChannel(api: BaseTest['api'], options?: Omit, +): Promise<{ channel: IRoom }> { + const name = faker.string.uuid(); + return (await api.post('/channels.create', { name, ...options })).json(); +} + export async function sendTargetChannelMessage(api: BaseTest['api'], roomName: string, options?: Partial) { const response = await api.get(`/channels.info?roomName=${roomName}`); From 45e1ab3bc4a03da9cc9d9b3514c049945763436e Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 28 Mar 2025 15:36:15 -0300 Subject: [PATCH 047/187] test: Replace invalid `imageLink` in image-gallery (#35645) --- apps/meteor/tests/e2e/image-gallery.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/image-gallery.spec.ts b/apps/meteor/tests/e2e/image-gallery.spec.ts index 526e695870df6..3afd299101226 100644 --- a/apps/meteor/tests/e2e/image-gallery.spec.ts +++ b/apps/meteor/tests/e2e/image-gallery.spec.ts @@ -120,7 +120,7 @@ test.describe.serial('Image Gallery', async () => { }); test.describe('When sending an image as a link', () => { - const imageLink = 'https://i0.wp.com/merithu.com.br/wp-content/uploads/2019/11/rocket-chat.png'; + const imageLink = 'https://raw.githubusercontent.com/RocketChat/Rocket.Chat.Artwork/master/Logos/2020/png/logo-horizontal-red.png'; test.beforeAll(async () => { await poHomeChannel.content.sendMessage(imageLink); From 5e3ab1a07163cd22ad4c41502ef232845d26bdc2 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 28 Mar 2025 17:00:53 -0300 Subject: [PATCH 048/187] feat: Place the room topic next to the room title (#35631) --- .changeset/plenty-baboons-kneel.md | 7 ++++ .../client/views/room/HeaderV2/RoomHeader.tsx | 2 ++ .../client/views/room/HeaderV2/RoomTitle.tsx | 2 +- .../{body => HeaderV2}/RoomTopic.spec.tsx | 18 +++++------ .../room/{body => HeaderV2}/RoomTopic.tsx | 32 ++++++++----------- .../client/views/room/body/RoomBodyV2.tsx | 2 -- .../src/components/HeaderV2/Header.tsx | 11 ++++++- .../src/components/HeaderV2/HeaderTitle.tsx | 2 +- 8 files changed, 44 insertions(+), 32 deletions(-) create mode 100644 .changeset/plenty-baboons-kneel.md rename apps/meteor/client/views/room/{body => HeaderV2}/RoomTopic.spec.tsx (94%) rename apps/meteor/client/views/room/{body => HeaderV2}/RoomTopic.tsx (63%) diff --git a/.changeset/plenty-baboons-kneel.md b/.changeset/plenty-baboons-kneel.md new file mode 100644 index 0000000000000..338a3a21dfa68 --- /dev/null +++ b/.changeset/plenty-baboons-kneel.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-client': minor +'@rocket.chat/meteor': minor +--- + +Places the room topic next to the room title +> This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it diff --git a/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx index 87afd65918ddb..6e138fca2a9c7 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx @@ -9,6 +9,7 @@ import ParentRoomWithData from './ParentRoomWithData'; import ParentTeam from './ParentTeam'; import RoomTitle from './RoomTitle'; import RoomToolbox from './RoomToolbox'; +import RoomTopic from './RoomTopic'; import Encrypted from './icons/Encrypted'; import Favorite from './icons/Favorite'; import Translate from './icons/Translate'; @@ -47,6 +48,7 @@ const RoomHeader = ({ room, slots = {}, roomToolbox }: RoomHeaderProps) => { {isRoomFederated(room) && } + {slots?.insideContent} diff --git a/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx b/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx index b9910522d59b4..77b9ecafbd3e8 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomTitle.tsx @@ -46,7 +46,7 @@ const RoomTitle = ({ room }: RoomTitleProps) => { mie={4} > - {room.name} + {room.name} ); }; diff --git a/apps/meteor/client/views/room/body/RoomTopic.spec.tsx b/apps/meteor/client/views/room/HeaderV2/RoomTopic.spec.tsx similarity index 94% rename from apps/meteor/client/views/room/body/RoomTopic.spec.tsx rename to apps/meteor/client/views/room/HeaderV2/RoomTopic.spec.tsx index e26c52d71a80a..6703d5836e71e 100644 --- a/apps/meteor/client/views/room/body/RoomTopic.spec.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomTopic.spec.tsx @@ -3,7 +3,7 @@ import { mockAppRoot } from '@rocket.chat/mock-providers'; import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; import { render, screen } from '@testing-library/react'; -import { RoomTopic } from './RoomTopic'; +import RoomTopic from './RoomTopic'; import FakeRoomProvider from '../../../../tests/mocks/client/FakeRoomProvider'; import { createFakeRoom, createFakeSubscription, createFakeUser } from '../../../../tests/mocks/data'; @@ -36,7 +36,7 @@ describe('RoomTopic', () => { render( - + , { wrapper: mockAppRoot() @@ -56,7 +56,7 @@ describe('RoomTopic', () => { render( - + , { wrapper: mockAppRoot() @@ -75,7 +75,7 @@ describe('RoomTopic', () => { render( - + , { wrapper: mockAppRoot() @@ -96,7 +96,7 @@ describe('RoomTopic', () => { render( - + , { wrapper: mockAppRoot() @@ -115,7 +115,7 @@ describe('RoomTopic', () => { render( - + , { wrapper: mockAppRoot() @@ -137,7 +137,7 @@ describe('RoomTopic', () => { render( - + , { wrapper: mockAppRoot() @@ -159,7 +159,7 @@ describe('RoomTopic', () => { render( - + , { wrapper: mockAppRoot() @@ -179,7 +179,7 @@ describe('RoomTopic', () => { render( - + , { wrapper: mockAppRoot() diff --git a/apps/meteor/client/views/room/body/RoomTopic.tsx b/apps/meteor/client/views/room/HeaderV2/RoomTopic.tsx similarity index 63% rename from apps/meteor/client/views/room/body/RoomTopic.tsx rename to apps/meteor/client/views/room/HeaderV2/RoomTopic.tsx index 0c9a1c79a870a..abe34ab0f3252 100644 --- a/apps/meteor/client/views/room/body/RoomTopic.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomTopic.tsx @@ -1,7 +1,6 @@ -import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IRoom } from '@rocket.chat/core-typings'; import { isDirectMessageRoom, isPrivateRoom, isPublicRoom, isTeamRoom } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; -import { RoomBanner, RoomBannerContent } from '@rocket.chat/ui-client'; import { useUserId, useTranslation, useRouter, useUserPresence } from '@rocket.chat/ui-contexts'; import MarkdownText from '../../../components/MarkdownText'; @@ -9,10 +8,9 @@ import { useCanEditRoom } from '../contextualBar/Info/hooks/useCanEditRoom'; type RoomTopicProps = { room: IRoom; - user: IUser | null; }; -export const RoomTopic = ({ room }: RoomTopicProps) => { +const RoomTopic = ({ room }: RoomTopicProps) => { const t = useTranslation(); const canEdit = useCanEditRoom(room); const userId = useUserId(); @@ -26,21 +24,19 @@ export const RoomTopic = ({ room }: RoomTopicProps) => { const topic = isDirectMessageRoom(room) && (room.uids?.length ?? 0) < 3 ? directUserData?.statusText : room.topic; const canEditTopic = canEdit && (isPublicRoom(room) || isPrivateRoom(room)); - if (!topic && !canEdit) { + if (!topic && !canEditTopic) { return null; } - return ( - - - {!topic && canEditTopic ? ( - - {t('Add_topic')} - - ) : ( - - )} - - - ); + if (!topic && canEditTopic) { + return ( + + {t('Add_topic')} + + ); + } + + return ; }; + +export default RoomTopic; diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 98232baa648d2..fc6108a348e65 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -25,7 +25,6 @@ import { useDateScroll } from '../hooks/useDateScroll'; import { useMessageListNavigation } from '../hooks/useMessageListNavigation'; import { useRetentionPolicy } from '../hooks/useRetentionPolicy'; import RoomForeword from './RoomForeword/RoomForeword'; -import { RoomTopic } from './RoomTopic'; import UnreadMessagesIndicator from './UnreadMessagesIndicator'; import { UploadProgressContainer, UploadProgressIndicator } from './UploadProgress'; import { useBannerSection } from './hooks/useBannerSection'; @@ -193,7 +192,6 @@ const RoomBody = (): ReactElement => { <> - {!isLayoutEmbedded && room.announcement && } diff --git a/packages/ui-client/src/components/HeaderV2/Header.tsx b/packages/ui-client/src/components/HeaderV2/Header.tsx index ee5580b4b7472..4515a5fc6104c 100644 --- a/packages/ui-client/src/components/HeaderV2/Header.tsx +++ b/packages/ui-client/src/components/HeaderV2/Header.tsx @@ -10,7 +10,16 @@ const Header = (props: HeaderProps) => { const { isMobile } = useLayout(); return ( - + ; -const HeaderTitle = (props: HeaderTitleProps) => ; +const HeaderTitle = (props: HeaderTitleProps) => ; export default HeaderTitle; From c21a7a6b3b6f38b94dc6cc58ab6e7a2a8a910d21 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 31 Mar 2025 10:46:14 -0300 Subject: [PATCH 049/187] test: prevent useInviteToken multiple times (#35648) --- apps/meteor/client/views/invite/InvitePage.tsx | 8 ++++++-- .../client/views/invite/hooks/useInviteTokenMutation.ts | 4 +--- .../client/views/invite/hooks/useValidateInviteQuery.ts | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/views/invite/InvitePage.tsx b/apps/meteor/client/views/invite/InvitePage.tsx index 97689a832f89b..569d11c4cde04 100644 --- a/apps/meteor/client/views/invite/InvitePage.tsx +++ b/apps/meteor/client/views/invite/InvitePage.tsx @@ -21,9 +21,13 @@ const InvitePage = (): ReactElement => { const getInviteRoomMutation = useInviteTokenMutation(); useEffect(() => { + // TODO: this is so hacky, get from the url and set the storage setToken(token || null); - if (userId && token) { - getInviteRoomMutation(token); + }, [setToken, token]); + + useEffect(() => { + if (userId && token && !getInviteRoomMutation.submittedAt) { + getInviteRoomMutation.mutate(token); } }, [getInviteRoomMutation, setToken, token, userId]); diff --git a/apps/meteor/client/views/invite/hooks/useInviteTokenMutation.ts b/apps/meteor/client/views/invite/hooks/useInviteTokenMutation.ts index f6e0c58e7f102..057118e636bf7 100644 --- a/apps/meteor/client/views/invite/hooks/useInviteTokenMutation.ts +++ b/apps/meteor/client/views/invite/hooks/useInviteTokenMutation.ts @@ -10,7 +10,7 @@ export const useInviteTokenMutation = () => { const getInviteRoom = useEndpoint('POST', '/v1/useInviteToken'); - const { mutate } = useMutation({ + return useMutation({ mutationFn: (token: string) => getInviteRoom({ token }), onSuccess: (result) => { if (!result.room.name) { @@ -31,6 +31,4 @@ export const useInviteTokenMutation = () => { router.navigate('/home'); }, }); - - return mutate; }; diff --git a/apps/meteor/client/views/invite/hooks/useValidateInviteQuery.ts b/apps/meteor/client/views/invite/hooks/useValidateInviteQuery.ts index 869f707f1cf3b..fa8e42cf1d458 100644 --- a/apps/meteor/client/views/invite/hooks/useValidateInviteQuery.ts +++ b/apps/meteor/client/views/invite/hooks/useValidateInviteQuery.ts @@ -41,7 +41,7 @@ export const useValidateInviteQuery = (userId: string | null, token: string | un return; } - return getInviteRoomMutation(token); + return getInviteRoomMutation.mutate(token); }; onSuccess(); From 8dc05445f919acf125cc500842a8230bb92af63d Mon Sep 17 00:00:00 2001 From: julio-rocketchat Date: Mon, 31 Mar 2025 16:30:06 +0200 Subject: [PATCH 050/187] chore(deps): bump `vite` to 6.1.2 (#35621) --- apps/uikit-playground/package.json | 2 +- yarn.lock | 283 ++--------------------------- 2 files changed, 12 insertions(+), 273 deletions(-) diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 93fea78351327..650525ba8c539 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -53,7 +53,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "typescript": "~5.7.2", - "vite": "^6.1.0" + "vite": "^6.1.2" }, "volta": { "extends": "../../package.json" diff --git a/yarn.lock b/yarn.lock index dad90af782b2c..2585df2219f6e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2727,13 +2727,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/aix-ppc64@npm:0.24.2" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/aix-ppc64@npm:0.25.0" @@ -2748,13 +2741,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/android-arm64@npm:0.24.2" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/android-arm64@npm:0.25.0" @@ -2769,13 +2755,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/android-arm@npm:0.24.2" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/android-arm@npm:0.25.0" @@ -2790,13 +2769,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/android-x64@npm:0.24.2" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/android-x64@npm:0.25.0" @@ -2811,13 +2783,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/darwin-arm64@npm:0.24.2" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/darwin-arm64@npm:0.25.0" @@ -2832,13 +2797,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/darwin-x64@npm:0.24.2" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/darwin-x64@npm:0.25.0" @@ -2853,13 +2811,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/freebsd-arm64@npm:0.24.2" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/freebsd-arm64@npm:0.25.0" @@ -2874,13 +2825,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/freebsd-x64@npm:0.24.2" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/freebsd-x64@npm:0.25.0" @@ -2895,13 +2839,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-arm64@npm:0.24.2" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-arm64@npm:0.25.0" @@ -2916,13 +2853,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-arm@npm:0.24.2" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-arm@npm:0.25.0" @@ -2937,13 +2867,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-ia32@npm:0.24.2" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-ia32@npm:0.25.0" @@ -2958,13 +2881,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-loong64@npm:0.24.2" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-loong64@npm:0.25.0" @@ -2979,13 +2895,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-mips64el@npm:0.24.2" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-mips64el@npm:0.25.0" @@ -3000,13 +2909,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-ppc64@npm:0.24.2" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-ppc64@npm:0.25.0" @@ -3021,13 +2923,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-riscv64@npm:0.24.2" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-riscv64@npm:0.25.0" @@ -3042,13 +2937,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-s390x@npm:0.24.2" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-s390x@npm:0.25.0" @@ -3063,13 +2951,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-x64@npm:0.24.2" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/linux-x64@npm:0.25.0" @@ -3084,13 +2965,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/netbsd-arm64@npm:0.24.2" - conditions: os=netbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/netbsd-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/netbsd-arm64@npm:0.25.0" @@ -3105,13 +2979,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/netbsd-x64@npm:0.24.2" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/netbsd-x64@npm:0.25.0" @@ -3126,13 +2993,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/openbsd-arm64@npm:0.24.2" - conditions: os=openbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/openbsd-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/openbsd-arm64@npm:0.25.0" @@ -3147,13 +3007,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/openbsd-x64@npm:0.24.2" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/openbsd-x64@npm:0.25.0" @@ -3168,13 +3021,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/sunos-x64@npm:0.24.2" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/sunos-x64@npm:0.25.0" @@ -3189,13 +3035,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/win32-arm64@npm:0.24.2" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/win32-arm64@npm:0.25.0" @@ -3210,13 +3049,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/win32-ia32@npm:0.24.2" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/win32-ia32@npm:0.25.0" @@ -3231,13 +3063,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/win32-x64@npm:0.24.2" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/win32-x64@npm:0.25.0" @@ -10087,7 +9912,7 @@ __metadata: react-virtuoso: "npm:^4.12.0" reactflow: "npm:^11.11.4" typescript: "npm:~5.7.2" - vite: "npm:^6.1.0" + vite: "npm:^6.1.2" languageName: unknown linkType: soft @@ -20100,92 +19925,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.24.2": - version: 0.24.2 - resolution: "esbuild@npm:0.24.2" - dependencies: - "@esbuild/aix-ppc64": "npm:0.24.2" - "@esbuild/android-arm": "npm:0.24.2" - "@esbuild/android-arm64": "npm:0.24.2" - "@esbuild/android-x64": "npm:0.24.2" - "@esbuild/darwin-arm64": "npm:0.24.2" - "@esbuild/darwin-x64": "npm:0.24.2" - "@esbuild/freebsd-arm64": "npm:0.24.2" - "@esbuild/freebsd-x64": "npm:0.24.2" - "@esbuild/linux-arm": "npm:0.24.2" - "@esbuild/linux-arm64": "npm:0.24.2" - "@esbuild/linux-ia32": "npm:0.24.2" - "@esbuild/linux-loong64": "npm:0.24.2" - "@esbuild/linux-mips64el": "npm:0.24.2" - "@esbuild/linux-ppc64": "npm:0.24.2" - "@esbuild/linux-riscv64": "npm:0.24.2" - "@esbuild/linux-s390x": "npm:0.24.2" - "@esbuild/linux-x64": "npm:0.24.2" - "@esbuild/netbsd-arm64": "npm:0.24.2" - "@esbuild/netbsd-x64": "npm:0.24.2" - "@esbuild/openbsd-arm64": "npm:0.24.2" - "@esbuild/openbsd-x64": "npm:0.24.2" - "@esbuild/sunos-x64": "npm:0.24.2" - "@esbuild/win32-arm64": "npm:0.24.2" - "@esbuild/win32-ia32": "npm:0.24.2" - "@esbuild/win32-x64": "npm:0.24.2" - 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/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/95425071c9f24ff88bf61e0710b636ec0eb24ddf8bd1f7e1edef3044e1221104bbfa7bbb31c18018c8c36fa7902c5c0b843f829b981ebc89160cf5eebdaa58f4 - languageName: node - linkType: hard - "esbuild@npm:^0.25.0": version: 0.25.0 resolution: "esbuild@npm:0.25.0" @@ -30672,14 +30411,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.5.1": - version: 8.5.1 - resolution: "postcss@npm:8.5.1" +"postcss@npm:^8.5.3": + version: 8.5.3 + resolution: "postcss@npm:8.5.3" dependencies: nanoid: "npm:^3.3.8" picocolors: "npm:^1.1.1" source-map-js: "npm:^1.2.1" - checksum: 10/1fbd28753143f7f03e4604813639918182b15343c7ad0f4e72f3875fc2cc0b8494c887f55dc05008fad5fbf1e1e908ce2edbbce364a91f84dcefb71edf7cd31d + checksum: 10/6d7e21a772e8b05bf102636918654dac097bac013f0dc8346b72ac3604fc16829646f94ea862acccd8f82e910b00e2c11c1f0ea276543565d278c7ca35516a7c languageName: node linkType: hard @@ -37006,13 +36745,13 @@ __metadata: languageName: node linkType: hard -"vite@npm:^6.1.0": - version: 6.1.0 - resolution: "vite@npm:6.1.0" +"vite@npm:^6.1.2": + version: 6.2.3 + resolution: "vite@npm:6.2.3" dependencies: - esbuild: "npm:^0.24.2" + esbuild: "npm:^0.25.0" fsevents: "npm:~2.3.3" - postcss: "npm:^8.5.1" + postcss: "npm:^8.5.3" rollup: "npm:^4.30.1" peerDependencies: "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 @@ -37054,7 +36793,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/5de360ac0ecb3cac85f796ec97d5347e2c8102a8845309af87f52296279464a6d5b880beb740bc42740936ec9de8bf0acce6a6ed3b3b24a733162a5d63d9f46b + checksum: 10/3cd7b3a8d9a31c8eb7141004ddb1c9489afa0cdaf0ccbf41dacee121a8f13062d0fb2cee160589aaf1c534a02402aac674f1b1618876ba0bd299b55f69b2b495 languageName: node linkType: hard From 68dbd826c5288a6337ea4e08e54ebaf556404e1c Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 31 Mar 2025 11:42:41 -0300 Subject: [PATCH 051/187] test: Replaces weak locators in notification preferences test (#35647) --- .../components/NotificationByDevice.tsx | 1 - .../fragments/home-flextab-notificationPreferences.ts | 10 +++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx index e88142e9ca008..f91e06acccdec 100644 --- a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx +++ b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/components/NotificationByDevice.tsx @@ -19,7 +19,6 @@ const NotificationByDevice = ({ device, icon, children }: NotificationByDevicePr } - data-qa-id={`${device}-notifications`} > {children} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-notificationPreferences.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-notificationPreferences.ts index f9ff7bff52d36..9a38571b32898 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-notificationPreferences.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-notificationPreferences.ts @@ -11,20 +11,24 @@ export class HomeFlextabNotificationPreferences { return this.page.locator('role=button[name="Save"]'); } + get dialogNotificationPreferences(): Locator { + return this.page.getByRole('dialog', { name: 'Notifications Preferences' }); + } + getPreferenceByDevice(device: string): Locator { return this.page.locator(`//div[@id="${device}Alert"]`); } async selectDropdownById(text: string): Promise { - await this.page.locator(`//div[@id="${text}"]`).click(); + await this.dialogNotificationPreferences.locator(`//div[@id="${text}"]`).click(); } async selectOptionByLabel(text: string): Promise { - await this.page.locator(`li.rcx-option >> text="${text}"`).click(); + await this.page.getByRole('listbox').getByRole('option', { name: text }).click(); } async selectDevice(text: string): Promise { - await this.page.locator(`[data-qa-id="${text}-notifications"]`).click(); + await this.dialogNotificationPreferences.getByRole('button', { name: text }).click(); } async updateDevicePreference(device: string): Promise { From a2b7b628db3aa7d1e5557620af18bbffc88e8f71 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 31 Mar 2025 14:39:20 -0300 Subject: [PATCH 052/187] regression: improves CalendarEvent status change schedule (#35607) --- .../server/configuration/outlookCalendar.ts | 1 + .../server/services/calendar/service.ts | 158 +++++- .../statusEvents/applyStatusChange.ts | 10 +- .../services/calendar/statusEvents/index.ts | 2 - .../setupAppointmentStatusChange.ts | 12 +- .../server/services/calendar/service.tests.ts | 462 +++++------------- .../statusEvents/applyStatusChange.ts | 18 - .../setupAppointmentStatusChange.ts | 77 --- .../src/types/ICalendarService.ts | 1 + packages/core-typings/src/ICalendarEvent.ts | 2 + .../src/models/ICalendarEventModel.ts | 5 + packages/models/src/models/CalendarEvent.ts | 97 +++- 12 files changed, 397 insertions(+), 448 deletions(-) delete mode 100644 apps/meteor/tests/unit/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts diff --git a/apps/meteor/ee/server/configuration/outlookCalendar.ts b/apps/meteor/ee/server/configuration/outlookCalendar.ts index 67c8d79450300..b7a60e655e052 100644 --- a/apps/meteor/ee/server/configuration/outlookCalendar.ts +++ b/apps/meteor/ee/server/configuration/outlookCalendar.ts @@ -9,5 +9,6 @@ Meteor.startup(() => addSettings(); await Calendar.setupNextNotification(); + await Calendar.setupNextStatusChange(); }), ); diff --git a/apps/meteor/server/services/calendar/service.ts b/apps/meteor/server/services/calendar/service.ts index 80f6d7aac6a7e..e96956e4f9462 100644 --- a/apps/meteor/server/services/calendar/service.ts +++ b/apps/meteor/server/services/calendar/service.ts @@ -5,12 +5,12 @@ import { UserStatus } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; import { Logger } from '@rocket.chat/logger'; import type { InsertionModel } from '@rocket.chat/model-typings'; -import { CalendarEvent } from '@rocket.chat/models'; +import { CalendarEvent, Users } from '@rocket.chat/models'; import type { UpdateResult, DeleteResult } from 'mongodb'; +import { applyStatusChange } from './statusEvents/applyStatusChange'; import { cancelUpcomingStatusChanges } from './statusEvents/cancelUpcomingStatusChanges'; import { removeCronJobs } from './statusEvents/removeCronJobs'; -import { setupAppointmentStatusChange } from './statusEvents/setupAppointmentStatusChange'; import { getShiftedTime } from './utils/getShiftedTime'; import { settings } from '../../../app/settings/server'; import { getUserPreference } from '../../../app/utils/server/lib/getUserPreference'; @@ -43,7 +43,7 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe const insertResult = await CalendarEvent.insertOne(insertData); await this.setupNextNotification(); if (busy !== false) { - await setupAppointmentStatusChange(insertResult.insertedId, uid, startTime, endTime, UserStatus.BUSY, true); + await this.setupNextStatusChange(); } return insertResult.insertedId; @@ -82,8 +82,9 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe await this.setupNextNotification(); if (busy !== false) { - await setupAppointmentStatusChange(insertResult.insertedId, uid, startTime, endTime, UserStatus.BUSY, true); + await this.setupNextStatusChange(); } + return insertResult.insertedId; } @@ -91,7 +92,7 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe if (updateResult.modifiedCount > 0) { await this.setupNextNotification(); if (busy !== false) { - await setupAppointmentStatusChange(event._id, uid, startTime, endTime, UserStatus.BUSY, true); + await this.setupNextStatusChange(); } } @@ -135,16 +136,9 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe if (startTime || endTime) { await removeCronJobs(eventId, event.uid); - const isBusy = busy !== undefined ? busy : event.busy !== false; if (isBusy) { - const effectiveStartTime = startTime || event.startTime; - const effectiveEndTime = endTime || event.endTime; - - // Only proceed if we have both valid start and end times - if (effectiveStartTime && effectiveEndTime) { - await setupAppointmentStatusChange(eventId, event.uid, effectiveStartTime, effectiveEndTime, UserStatus.BUSY, true); - } + await this.setupNextStatusChange(); } } } @@ -158,15 +152,25 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe await removeCronJobs(eventId, event.uid); } - return CalendarEvent.deleteOne({ + const result = await CalendarEvent.deleteOne({ _id: eventId, }); + + if (result.deletedCount > 0) { + await this.setupNextStatusChange(); + } + + return result; } public async setupNextNotification(): Promise { return this.doSetupNextNotification(false); } + public async setupNextStatusChange(): Promise { + return this.doSetupNextStatusChange(); + } + public async cancelUpcomingStatusChanges(uid: IUser['_id'], endTime = new Date()): Promise { return cancelUpcomingStatusChanges(uid, endTime); } @@ -200,6 +204,132 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe await cronJobs.addAtTimestamp('calendar-reminders', date, async () => this.sendCurrentNotifications(date)); } + private async doSetupNextStatusChange(): Promise { + // This method is called in the following moments: + // 1. When a new busy event is created or imported + // 2. When a busy event is updated (time/busy status changes) + // 3. When a busy event is deleted + // 4. When a status change job executes and completes + // 5. When an event ends and the status is restored + // 6. From Outlook Calendar integration (ee/server/configuration/outlookCalendar.ts) + + const busyStatusEnabled = settings.get('Calendar_BusyStatus_Enabled'); + if (!busyStatusEnabled) { + const schedulerJobId = 'calendar-status-scheduler'; + if (await cronJobs.has(schedulerJobId)) { + await cronJobs.remove(schedulerJobId); + } + return; + } + + const schedulerJobId = 'calendar-status-scheduler'; + if (await cronJobs.has(schedulerJobId)) { + await cronJobs.remove(schedulerJobId); + } + + const now = new Date(); + const nextStartEvent = await CalendarEvent.findNextFutureEvent(now); + const inProgressEvents = await CalendarEvent.findInProgressEvents(now).toArray(); + const eventsWithEndTime = inProgressEvents.filter((event) => event.endTime && event.busy !== false); + if (eventsWithEndTime.length === 0 && !nextStartEvent) { + return; + } + + let nextEndTime: Date | null = null; + if (eventsWithEndTime.length > 0 && eventsWithEndTime[0].endTime) { + nextEndTime = eventsWithEndTime.reduce((earliest, event) => { + if (!event.endTime) return earliest; + return event.endTime.getTime() < earliest.getTime() ? event.endTime : earliest; + }, eventsWithEndTime[0].endTime); + } + + let nextProcessTime: Date; + if (nextStartEvent && nextEndTime) { + nextProcessTime = nextStartEvent.startTime.getTime() < nextEndTime.getTime() ? nextStartEvent.startTime : nextEndTime; + } else if (nextStartEvent) { + nextProcessTime = nextStartEvent.startTime; + } else if (nextEndTime) { + nextProcessTime = nextEndTime; + } else { + // This should never happen due to the earlier check, but just in case + return; + } + + await cronJobs.addAtTimestamp(schedulerJobId, nextProcessTime, async () => this.processStatusChangesAtTime()); + } + + private async processStatusChangesAtTime(): Promise { + const processTime = new Date(); + + const eventsStartingNow = await CalendarEvent.findEventsStartingNow({ now: processTime, offset: 5000 }).toArray(); + for await (const event of eventsStartingNow) { + if (event.busy === false) { + continue; + } + await this.processEventStart(event); + } + + const eventsEndingNow = await CalendarEvent.findEventsEndingNow({ now: processTime, offset: 5000 }).toArray(); + for await (const event of eventsEndingNow) { + if (event.busy === false) { + continue; + } + await this.processEventEnd(event); + } + + await this.doSetupNextStatusChange(); + } + + private async processEventStart(event: ICalendarEvent): Promise { + if (!event.endTime) { + return; + } + + const user = await Users.findOneById(event.uid, { projection: { status: 1 } }); + if (!user || user.status === UserStatus.OFFLINE) { + return; + } + + if (user.status) { + await CalendarEvent.updateEvent(event._id, { previousStatus: user.status }); + } + + await applyStatusChange({ + eventId: event._id, + uid: event.uid, + startTime: event.startTime, + endTime: event.endTime, + status: UserStatus.BUSY, + }); + } + + private async processEventEnd(event: ICalendarEvent): Promise { + if (!event.endTime) { + return; + } + + const user = await Users.findOneById(event.uid, { projection: { status: 1 } }); + if (!user) { + return; + } + + // Only restore status if: + // 1. The current status is BUSY (meaning it was set by our system, not manually changed by user) + // 2. We have a previousStatus stored from before the event started + + if (event.previousStatus && event.previousStatus === user.status) { + await applyStatusChange({ + eventId: event._id, + uid: event.uid, + startTime: event.startTime, + endTime: event.endTime, + status: event.previousStatus, + }); + } else { + logger.debug(`Not restoring status for user ${event.uid}: current=${user.status}, stored=${event.previousStatus}`); + } + } + private async sendCurrentNotifications(date: Date): Promise { const events = await CalendarEvent.findEventsToNotify(date, 1).toArray(); for await (const event of events) { diff --git a/apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts b/apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts index d47fe85237ef0..860d5df5e4d3b 100644 --- a/apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts +++ b/apps/meteor/server/services/calendar/statusEvents/applyStatusChange.ts @@ -1,9 +1,10 @@ import { api } from '@rocket.chat/core-services'; import { UserStatus } from '@rocket.chat/core-typings'; import type { ICalendarEvent, IUser } from '@rocket.chat/core-typings'; +import { Logger } from '@rocket.chat/logger'; import { Users } from '@rocket.chat/models'; -import { setupAppointmentStatusChange } from './setupAppointmentStatusChange'; +const logger = new Logger('Calendar'); export async function applyStatusChange({ eventId, @@ -11,7 +12,6 @@ export async function applyStatusChange({ startTime, endTime, status, - shouldScheduleRemoval, }: { eventId: ICalendarEvent['_id']; uid: IUser['_id']; @@ -20,6 +20,8 @@ export async function applyStatusChange({ status?: UserStatus; shouldScheduleRemoval?: boolean; }): Promise { + logger.debug(`Applying status change for event ${eventId} at ${startTime} ${endTime ? `to ${endTime}` : ''} to ${status}`); + const user = await Users.findOneById(uid, { projection: { roles: 1, username: 1, name: 1, status: 1 } }); if (!user || user.status === UserStatus.OFFLINE) { return; @@ -40,8 +42,4 @@ export async function applyStatusChange({ }, previousStatus, }); - - if (shouldScheduleRemoval && endTime) { - await setupAppointmentStatusChange(eventId, uid, startTime, endTime, previousStatus, false); - } } diff --git a/apps/meteor/server/services/calendar/statusEvents/index.ts b/apps/meteor/server/services/calendar/statusEvents/index.ts index ff4133ca8bc56..e6eca7f011c7e 100644 --- a/apps/meteor/server/services/calendar/statusEvents/index.ts +++ b/apps/meteor/server/services/calendar/statusEvents/index.ts @@ -3,7 +3,6 @@ import { cancelUpcomingStatusChanges } from './cancelUpcomingStatusChanges'; import { generateCronJobId } from './generateCronJobId'; import { handleOverlappingEvents } from './handleOverlappingEvents'; import { removeCronJobs } from './removeCronJobs'; -import { setupAppointmentStatusChange } from './setupAppointmentStatusChange'; export const statusEventManager = { applyStatusChange, @@ -11,5 +10,4 @@ export const statusEventManager = { generateCronJobId, handleOverlappingEvents, removeCronJobs, - setupAppointmentStatusChange, } as const; diff --git a/apps/meteor/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts b/apps/meteor/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts index 907f2a6bf2c66..0b818a23dedf1 100644 --- a/apps/meteor/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts +++ b/apps/meteor/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts @@ -33,7 +33,13 @@ export async function setupAppointmentStatusChange( await cronJobs.remove(cronJobId); } - await cronJobs.addAtTimestamp(cronJobId, scheduledTime, async () => - applyStatusChange({ eventId, uid, startTime, endTime, status, shouldScheduleRemoval }), - ); + await cronJobs.addAtTimestamp(cronJobId, scheduledTime, async () => { + await applyStatusChange({ eventId, uid, startTime, endTime, status, shouldScheduleRemoval }); + + if (!shouldScheduleRemoval) { + if (await cronJobs.has('calendar-next-status-change')) { + await cronJobs.remove('calendar-next-status-change'); + } + } + }); } diff --git a/apps/meteor/tests/unit/server/services/calendar/service.tests.ts b/apps/meteor/tests/unit/server/services/calendar/service.tests.ts index d6fc603089ce9..e071b65c763e8 100644 --- a/apps/meteor/tests/unit/server/services/calendar/service.tests.ts +++ b/apps/meteor/tests/unit/server/services/calendar/service.tests.ts @@ -21,12 +21,19 @@ const CalendarEventMock = { findEventsToNotify: sinon.stub(), flagNotificationSent: sinon.stub(), findOneByExternalIdAndUserId: sinon.stub(), + findEventsToScheduleNow: sinon.stub(), + findNextFutureEvent: sinon.stub(), + findInProgressEvents: sinon.stub(), +}; + +const UsersMock = { + findOne: sinon.stub(), }; const statusEventManagerMock = { - setupAppointmentStatusChange: sinon.stub().resolves(), removeCronJobs: sinon.stub().resolves(), cancelUpcomingStatusChanges: sinon.stub().resolves(), + applyStatusChange: sinon.stub().resolves(), }; const getUserPreferenceMock = sinon.stub(); @@ -34,11 +41,11 @@ const getUserPreferenceMock = sinon.stub(); const serviceMocks = { './statusEvents/cancelUpcomingStatusChanges': { cancelUpcomingStatusChanges: statusEventManagerMock.cancelUpcomingStatusChanges }, './statusEvents/removeCronJobs': { removeCronJobs: statusEventManagerMock.removeCronJobs }, - './statusEvents/setupAppointmentStatusChange': { setupAppointmentStatusChange: statusEventManagerMock.setupAppointmentStatusChange }, + './statusEvents/applyStatusChange': { applyStatusChange: statusEventManagerMock.applyStatusChange }, '../../../app/settings/server': { settings: settingsMock }, '@rocket.chat/core-services': { api, ServiceClassInternal: class {} }, '@rocket.chat/cron': { cronJobs: cronJobsMock }, - '@rocket.chat/models': { CalendarEvent: CalendarEventMock }, + '@rocket.chat/models': { CalendarEvent: CalendarEventMock, Users: UsersMock }, '../../../app/utils/server/lib/getUserPreference': { getUserPreference: getUserPreferenceMock }, }; @@ -74,8 +81,10 @@ describe('CalendarService', () => { sandbox.stub(proto, 'sendEventNotification').resolves(); sandbox.stub(proto, 'sendCurrentNotifications').resolves(); sandbox.stub(proto, 'doSetupNextNotification').resolves(); + sandbox.stub(proto, 'doSetupNextStatusChange').resolves(); sandbox.stub(service, 'setupNextNotification').resolves(); + sandbox.stub(service, 'setupNextStatusChange').resolves(); } function setupCalendarEventMocks() { @@ -93,6 +102,13 @@ describe('CalendarService', () => { }), flagNotificationSent: sinon.stub().resolves(), findOneByExternalIdAndUserId: sinon.stub().resolves(null), + findEventsToScheduleNow: sinon.stub().returns({ + toArray: sinon.stub().resolves([]), + }), + findNextFutureEvent: sinon.stub().resolves(null), + findInProgressEvents: sinon.stub().returns({ + toArray: sinon.stub().resolves([]), + }), }; Object.assign(CalendarEventMock, freshMocks); @@ -147,34 +163,7 @@ describe('CalendarService', () => { reminderMinutesBeforeStart: 5, notificationSent: false, }); - sinon.assert.calledOnce(statusEventManagerMock.setupAppointmentStatusChange); - }); - - it('should create event without end time if not provided', async () => { - const eventData = { - uid: fakeUserId, - startTime: fakeStartTime, - subject: fakeSubject, - description: fakeDescription, - }; - - await service.create(eventData); - - expect(CalendarEventMock.insertOne.firstCall.args[0]).to.not.have.property('endTime'); - }); - - it('should use default reminder minutes if not provided', async () => { - const eventData = { - uid: fakeUserId, - startTime: fakeStartTime, - subject: fakeSubject, - description: fakeDescription, - }; - - await service.create(eventData); - - const insertedData = CalendarEventMock.insertOne.firstCall.args[0]; - expect(insertedData).to.have.property('reminderMinutesBeforeStart', 5); + sinon.assert.calledOnce(service.setupNextStatusChange); }); }); @@ -190,24 +179,7 @@ describe('CalendarService', () => { await service.import(eventData); sinon.assert.calledOnce(CalendarEventMock.insertOne); - sinon.assert.calledOnce(statusEventManagerMock.setupAppointmentStatusChange); - }); - - it('should create a new event if event with externalId not found', async () => { - const eventData = { - uid: fakeUserId, - startTime: fakeStartTime, - subject: fakeSubject, - description: fakeDescription, - externalId: fakeExternalId, - }; - - CalendarEventMock.findOneByExternalIdAndUserId.resolves(null); - - await service.import(eventData); - - sinon.assert.calledWith(CalendarEventMock.findOneByExternalIdAndUserId, fakeExternalId, fakeUserId); - sinon.assert.calledOnce(CalendarEventMock.insertOne); + sinon.assert.calledOnce(service.setupNextStatusChange); }); it('should update existing event if found by externalId', async () => { @@ -231,58 +203,6 @@ describe('CalendarService', () => { sinon.assert.calledOnce(CalendarEventMock.updateEvent); sinon.assert.notCalled(CalendarEventMock.insertOne); }); - - it('should extract meeting URL from description if not provided', async () => { - const eventData = { - uid: fakeUserId, - startTime: fakeStartTime, - subject: fakeSubject, - description: 'Description with callUrl=https://meet.test/123', - externalId: fakeExternalId, - }; - - const proto = Object.getPrototypeOf(service); - await service.import(eventData); - - sinon.assert.calledWith(proto.parseDescriptionForMeetingUrl as sinon.SinonStub, eventData.description); - }); - }); - - describe('#get', () => { - it('should retrieve a single event by ID', async () => { - const fakeEvent = { - _id: fakeEventId, - uid: fakeUserId, - startTime: fakeStartTime, - subject: fakeSubject, - }; - - CalendarEventMock.findOne.resolves(fakeEvent); - - const result = await service.get(fakeEventId); - - sinon.assert.calledWith(CalendarEventMock.findOne, { _id: fakeEventId }); - expect(result).to.equal(fakeEvent); - }); - }); - - describe('#list', () => { - it('should retrieve events for a user on a specific date', async () => { - const fakeEvents = [ - { _id: 'event1', uid: fakeUserId, startTime: fakeStartTime }, - { _id: 'event2', uid: fakeUserId, startTime: fakeStartTime }, - ]; - - CalendarEventMock.findByUserIdAndDate.returns({ - toArray: sinon.stub().resolves(fakeEvents), - }); - - const fakeDate = new Date('2025-01-01'); - const result = await service.list(fakeUserId, fakeDate); - - sinon.assert.calledWith(CalendarEventMock.findByUserIdAndDate, fakeUserId, fakeDate); - expect(result).to.equal(fakeEvents); - }); }); describe('#update', () => { @@ -307,14 +227,6 @@ describe('CalendarService', () => { sinon.assert.calledWith(CalendarEventMock.updateEvent, fakeEventId, sinon.match.has('subject', 'Updated Subject')); }); - it('should do nothing if event not found', async () => { - CalendarEventMock.findOne.resolves(null); - - await service.update(fakeEventId, { subject: 'New Subject' }); - - sinon.assert.notCalled(CalendarEventMock.updateEvent); - }); - it('should update cron jobs when start/end times change', async () => { const fakeEvent = { _id: fakeEventId, @@ -335,42 +247,7 @@ describe('CalendarService', () => { }); sinon.assert.calledOnce(statusEventManagerMock.removeCronJobs); - sinon.assert.calledOnce(statusEventManagerMock.setupAppointmentStatusChange); - }); - - it('should extract meeting URL from description if not provided', async () => { - const fakeEvent = { - _id: fakeEventId, - uid: fakeUserId, - startTime: fakeStartTime, - subject: fakeSubject, - }; - - CalendarEventMock.findOne.resolves(fakeEvent); - - const proto = Object.getPrototypeOf(service); - - await service.update(fakeEventId, { - description: 'Description with callUrl=https://meet.test/123', - }); - - sinon.assert.called(proto.parseDescriptionForMeetingUrl as sinon.SinonStub); - }); - - it('should setup next notification if event was modified', async () => { - const fakeEvent = { - _id: fakeEventId, - uid: fakeUserId, - startTime: fakeStartTime, - subject: fakeSubject, - }; - - CalendarEventMock.findOne.resolves(fakeEvent); - CalendarEventMock.updateEvent.resolves({ modifiedCount: 1 } as UpdateResult); - - await service.update(fakeEventId, { subject: 'New Subject' }); - - sinon.assert.calledOnce(service.setupNextNotification as sinon.SinonStub); + sinon.assert.calledOnce(service.setupNextStatusChange); }); }); @@ -390,15 +267,6 @@ describe('CalendarService', () => { sinon.assert.calledOnce(statusEventManagerMock.removeCronJobs); sinon.assert.calledWith(CalendarEventMock.deleteOne, { _id: fakeEventId }); }); - - it('should only delete the event if not found', async () => { - CalendarEventMock.findOne.resolves(null); - - await service.delete(fakeEventId); - - sinon.assert.notCalled(statusEventManagerMock.removeCronJobs); - sinon.assert.calledOnce(CalendarEventMock.deleteOne); - }); }); describe('#setupNextNotification', () => { @@ -421,30 +289,7 @@ describe('CalendarService', () => { }); }); - describe('#cancelUpcomingStatusChanges', () => { - it('should delegate to statusEventManager', async () => { - await service.cancelUpcomingStatusChanges(fakeUserId); - - sinon.assert.calledWith(statusEventManagerMock.cancelUpcomingStatusChanges, fakeUserId); - }); - - it('should pass custom end time if provided', async () => { - const customDate = new Date('2025-02-01'); - - await service.cancelUpcomingStatusChanges(fakeUserId, customDate); - - sinon.assert.calledWith(statusEventManagerMock.cancelUpcomingStatusChanges, fakeUserId, customDate); - }); - }); - describe('Private: parseDescriptionForMeetingUrl', () => { - it('should return undefined for empty description', async () => { - await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { - const result = await method(''); - expect(result).to.be.undefined; - }); - }); - it('should extract URL from description with default pattern', async () => { await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { const testDescription = 'Join at https://meet.example.com?callUrl=https://special-meeting.com/123'; @@ -452,199 +297,162 @@ describe('CalendarService', () => { expect(result).to.equal('https://special-meeting.com/123'); }); }); + }); - it('should return undefined if regex pattern is empty', async () => { - await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { - settingsMock.set('Calendar_MeetingUrl_Regex', ''); + describe('Private: doSetupNextNotification', () => { + it('should schedule notifications at the next date', async () => { + await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { + const nextDate = new Date('2025-01-01T10:00:00Z'); + CalendarEventMock.findNextNotificationDate.resolves(nextDate); - const result = await method('Test description with no pattern match'); - expect(result).to.be.undefined; - }); - }); + await method(false); - it('should handle URL decoding', async () => { - await testPrivateMethod(service, 'parseDescriptionForMeetingUrl', async (method) => { - const encodedUrl = 'Join meeting at link with callUrl%3Dhttps%3A%2F%2Fmeeting.example.com%2F123'; - const result = await method(encodedUrl); - expect(result).to.include('https://meeting.example.com/123'); + expect(cronJobsMock.jobNames.has('calendar-reminders')).to.true; }); }); }); - describe('Private: findImportedEvent', () => { - it('should call the model method with correct parameters', async () => { - await testPrivateMethod(service, 'findImportedEvent', async (method) => { - await method(fakeExternalId, fakeUserId); - sinon.assert.calledWith(CalendarEventMock.findOneByExternalIdAndUserId, fakeExternalId, fakeUserId); + describe('Private: doSetupNextStatusChange', () => { + it('should not run when busy status setting is disabled', async () => { + await testPrivateMethod(service, 'doSetupNextStatusChange', async (method) => { + settingsMock.set('Calendar_BusyStatus_Enabled', false); + + const originalHas = cronJobsMock.has; + const originalRemove = cronJobsMock.remove; + const originalAddAtTimestamp = cronJobsMock.addAtTimestamp; + + const hasStub = sinon.stub().resolves(true); + const removeStub = sinon.stub().resolves(); + const addAtTimestampStub = sinon.stub().resolves(); + + cronJobsMock.has = hasStub; + cronJobsMock.remove = removeStub; + cronJobsMock.addAtTimestamp = addAtTimestampStub; + + try { + await method(); + sinon.assert.calledWith(hasStub, 'calendar-next-status-change'); + sinon.assert.calledWith(removeStub, 'calendar-next-status-change'); + sinon.assert.notCalled(addAtTimestampStub); + } finally { + cronJobsMock.has = originalHas; + cronJobsMock.remove = originalRemove; + cronJobsMock.addAtTimestamp = originalAddAtTimestamp; + } }); }); - it('should return the event when found', async () => { - await testPrivateMethod(service, 'findImportedEvent', async (method) => { - const fakeEvent = { _id: fakeEventId, externalId: fakeExternalId, uid: fakeUserId }; - CalendarEventMock.findOneByExternalIdAndUserId.resolves(fakeEvent); + it('should schedule a single chain job to handle all events when busy status setting is enabled', async () => { + await testPrivateMethod(service, 'doSetupNextStatusChange', async (method) => { + settingsMock.set('Calendar_BusyStatus_Enabled', true); - const result = await method(fakeExternalId, fakeUserId); - expect(result).to.equal(fakeEvent); - }); - }); - - it('should return null when event not found', async () => { - await testPrivateMethod(service, 'findImportedEvent', async (method) => { - CalendarEventMock.findOneByExternalIdAndUserId.resolves(null); - - const result = await method(fakeExternalId, fakeUserId); - expect(result).to.be.null; - }); - }); - }); + const startOfNextMinute = new Date(); + startOfNextMinute.setSeconds(0, 0); + startOfNextMinute.setMinutes(startOfNextMinute.getMinutes() + 1); - describe('Private: sendEventNotification', () => { - it('should not send notification if user preference is disabled', async () => { - await testPrivateMethod(service, 'sendEventNotification', async (method) => { - getUserPreferenceMock.resolves(false); + const endOfNextMinute = new Date(startOfNextMinute); + endOfNextMinute.setMinutes(startOfNextMinute.getMinutes() + 1); - const fakeEvent = { - _id: fakeEventId, + const eventStartingSoon = { + _id: 'soon123', uid: fakeUserId, - startTime: fakeStartTime, - subject: fakeSubject, + startTime: startOfNextMinute, + endTime: new Date(startOfNextMinute.getTime() + 3600000), // 1 hour later }; - await method(fakeEvent); - - sinon.assert.calledWith(getUserPreferenceMock, fakeUserId, 'notifyCalendarEvents'); - sinon.assert.notCalled(api.broadcast as sinon.SinonStub); - }); - }); - - it('should send notification with correct event data', async () => { - await testPrivateMethod(service, 'sendEventNotification', async (method) => { - getUserPreferenceMock.resolves(true); - - const fakeEvent = { - _id: fakeEventId, + const futureEvent = { + _id: 'future123', uid: fakeUserId, - startTime: fakeStartTime, - subject: fakeSubject, + startTime: endOfNextMinute, + endTime: new Date(endOfNextMinute.getTime() + 3600000), // 1 hour later }; - await method(fakeEvent); - - sinon.assert.calledWith( - api.broadcast as sinon.SinonStub, - 'notify.calendar', - fakeUserId, - sinon.match({ - title: fakeSubject, - payload: { _id: fakeEventId }, - }), - ); - }); - }); - }); - - describe('Private: sendCurrentNotifications', () => { - it('should send notification for all events and flag them as sent', async () => { - await testPrivateMethod(service, 'sendCurrentNotifications', async (method) => { - const proto = Object.getPrototypeOf(service); - (proto.sendEventNotification as sinon.SinonStub).restore(); - sandbox.stub(proto, 'sendEventNotification').resolves(); - - const fakeDate = new Date('2025-01-01T10:00:00Z'); - const fakeEvents = [ - { _id: 'event1', uid: fakeUserId, startTime: fakeStartTime }, - { _id: 'event2', uid: fakeUserId, startTime: fakeStartTime }, - ]; - - CalendarEventMock.findEventsToNotify.returns({ - toArray: sinon.stub().resolves(fakeEvents), + CalendarEventMock.findEventsToScheduleNow.returns({ + toArray: sinon.stub().resolves([eventStartingSoon]), }); + CalendarEventMock.findNextFutureEvent.resolves(futureEvent); - await method(fakeDate); + const originalHas = cronJobsMock.has; + const originalRemove = cronJobsMock.remove; + const originalAddAtTimestamp = cronJobsMock.addAtTimestamp; - sinon.assert.calledWith(CalendarEventMock.findEventsToNotify, fakeDate, 1); - sinon.assert.calledTwice(proto.sendEventNotification as sinon.SinonStub); - sinon.assert.calledTwice(CalendarEventMock.flagNotificationSent); - sinon.assert.calledWith(CalendarEventMock.flagNotificationSent, 'event1'); - sinon.assert.calledWith(CalendarEventMock.flagNotificationSent, 'event2'); - sinon.assert.calledOnceWithExactly(proto.doSetupNextNotification as sinon.SinonStub, true); - }); - }); - }); + const hasStub = sinon.stub().resolves(false); + const removeStub = sinon.stub().resolves(); + const addAtTimestampStub = sinon.stub().resolves(); - describe('Private: doSetupNextNotification', () => { - it('should remove calendar-reminders cron job if no events found', async () => { - await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { - CalendarEventMock.findNextNotificationDate.resolves(null); - cronJobsMock.jobNames.add('calendar-reminders'); + cronJobsMock.has = hasStub; + cronJobsMock.remove = removeStub; + cronJobsMock.addAtTimestamp = addAtTimestampStub; - await method(false); + try { + await method(); - expect(cronJobsMock.jobNames.has('calendar-reminders')).to.false; - }); - }); + sinon.assert.calledWith(hasStub, 'calendar-next-status-change'); + sinon.assert.notCalled(removeStub); - it('should schedule notifications at the next date', async () => { - await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { - const nextDate = new Date('2025-01-01T10:00:00Z'); - CalendarEventMock.findNextNotificationDate.resolves(nextDate); + sinon.assert.calledOnce(addAtTimestampStub); - await method(false); + sinon.assert.calledWith(addAtTimestampStub, 'calendar-next-status-change', futureEvent.startTime, sinon.match.func); - expect(cronJobsMock.jobNames.has('calendar-reminders')).to.true; + sinon.assert.neverCalledWith(addAtTimestampStub, sinon.match(/^calendar-status-/), sinon.match.any, sinon.match.any); + } finally { + cronJobsMock.has = originalHas; + cronJobsMock.remove = originalRemove; + cronJobsMock.addAtTimestamp = originalAddAtTimestamp; + } }); }); - it('should send current notifications if date is in the past', async () => { - await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { - const proto = Object.getPrototypeOf(service); - (proto.sendCurrentNotifications as sinon.SinonStub).restore(); - sandbox.stub(proto, 'sendCurrentNotifications').resolves(); + it('should fetch events at execution time rather than scheduling them individually', async () => { + await testPrivateMethod(service, 'doSetupNextStatusChange', async (method) => { + settingsMock.set('Calendar_BusyStatus_Enabled', true); - const pastDate = new Date(); - pastDate.setMinutes(pastDate.getMinutes() - 10); - CalendarEventMock.findNextNotificationDate.resolves(pastDate); + const now = new Date(); + const startOfNextMinute = new Date(now); + startOfNextMinute.setSeconds(0, 0); + startOfNextMinute.setMinutes(startOfNextMinute.getMinutes() + 1); - await method(false); + const endOfNextMinute = new Date(startOfNextMinute); + endOfNextMinute.setMinutes(startOfNextMinute.getMinutes() + 1); - sinon.assert.calledWith(proto.sendCurrentNotifications as sinon.SinonStub, pastDate); - expect(cronJobsMock.jobNames.size).to.equal(0); - }); - }); + CalendarEventMock.findEventsToScheduleNow.returns({ + toArray: sinon.stub().resolves([]), + }); + CalendarEventMock.findNextFutureEvent.resolves(null); - it('should schedule future notifications even if date is in the past when recursive', async () => { - await testPrivateMethod(service, 'doSetupNextNotification', async (method) => { - const pastDate = new Date(); - pastDate.setMinutes(pastDate.getMinutes() - 10); - CalendarEventMock.findNextNotificationDate.resolves(pastDate); + const originalHas = cronJobsMock.has; + const originalRemove = cronJobsMock.remove; + const originalAddAtTimestamp = cronJobsMock.addAtTimestamp; - await method(true); + const hasStub = sinon.stub().resolves(false); + const removeStub = sinon.stub().resolves(); + const addAtTimestampStub = sinon.stub().resolves(); - sinon.assert.notCalled(service.sendCurrentNotifications as sinon.SinonStub); - expect(cronJobsMock.jobNames.size).to.equal(1); - }); - }); - }); + cronJobsMock.has = hasStub; + cronJobsMock.remove = removeStub; + cronJobsMock.addAtTimestamp = addAtTimestampStub; - describe('Overlapping events', () => { - it('should not set up status change if no endTime is provided when updating', async () => { - const fakeEvent = { - _id: fakeEventId, - uid: fakeUserId, - startTime: fakeStartTime, - subject: fakeSubject, - }; + try { + await method(); - CalendarEventMock.findOne.resolves(fakeEvent); + sinon.assert.calledWith(addAtTimestampStub, 'calendar-next-status-change', endOfNextMinute, sinon.match.func); - await service.update(fakeEventId, { - subject: 'New Subject', - }); + const callback = addAtTimestampStub.firstCall.args[2]; + const doSetupNextStatusChangeStub = sinon.stub(service, 'doSetupNextStatusChange').resolves(); + await callback(); - sinon.assert.notCalled(statusEventManagerMock.setupAppointmentStatusChange); + sinon.assert.calledOnce(doSetupNextStatusChangeStub); + doSetupNextStatusChangeStub.restore(); + } finally { + cronJobsMock.has = originalHas; + cronJobsMock.remove = originalRemove; + cronJobsMock.addAtTimestamp = originalAddAtTimestamp; + } + }); }); + }); + describe('Overlapping events', () => { it('should cancel upcoming status changes for a user', async () => { const customDate = new Date('2025-02-01'); diff --git a/apps/meteor/tests/unit/server/services/calendar/statusEvents/applyStatusChange.ts b/apps/meteor/tests/unit/server/services/calendar/statusEvents/applyStatusChange.ts index 30053e86cc1e7..65818c9d1f09a 100644 --- a/apps/meteor/tests/unit/server/services/calendar/statusEvents/applyStatusChange.ts +++ b/apps/meteor/tests/unit/server/services/calendar/statusEvents/applyStatusChange.ts @@ -11,10 +11,7 @@ const UsersMock = { updateStatusAndStatusDefault: sinon.stub().resolves(), }; -const setupAppointmentStatusChange = sinon.stub().resolves(); - const { applyStatusChange } = proxyquire.noCallThru().load('../../../../../../server/services/calendar/statusEvents/applyStatusChange', { - './setupAppointmentStatusChange': { setupAppointmentStatusChange }, '@rocket.chat/core-services': { api }, '@rocket.chat/models': { Users: UsersMock, @@ -52,8 +49,6 @@ describe('Calendar.StatusEvents', () => { function setupOtherMocks() { sandbox.stub(api, 'broadcast').resolves(); - - setupAppointmentStatusChange.resetHistory(); } afterEach(() => { @@ -144,26 +139,13 @@ describe('Calendar.StatusEvents', () => { }); it('should schedule status revert when shouldScheduleRemoval=true', async () => { - const previousStatus = UserStatus.ONLINE; - await applyStatusChange({ eventId: fakeEventId, uid: fakeUserId, startTime: fakeStartTime, endTime: fakeEndTime, status: UserStatus.BUSY, - shouldScheduleRemoval: true, }); - - expect(setupAppointmentStatusChange.callCount).to.equal(1); - expect(setupAppointmentStatusChange.firstCall.args).to.deep.equal([ - fakeEventId, - fakeUserId, - fakeStartTime, - fakeEndTime, - previousStatus, - false, - ]); }); }); }); diff --git a/apps/meteor/tests/unit/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts b/apps/meteor/tests/unit/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts deleted file mode 100644 index 7df232de9c700..0000000000000 --- a/apps/meteor/tests/unit/server/services/calendar/statusEvents/setupAppointmentStatusChange.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { UserStatus } from '@rocket.chat/core-typings'; -import { expect } from 'chai'; -import { describe, it, beforeEach } from 'mocha'; -import proxyquire from 'proxyquire'; -import sinon from 'sinon'; - -import { MockedCronJobs } from '../mocks/cronJobs'; - -const settingsMock = new Map(); -const cronJobsMock = new MockedCronJobs(); - -const applyStatusChange = sinon.stub(); -const handleOverlappingEvents = sinon.stub(); - -const { setupAppointmentStatusChange } = proxyquire - .noCallThru() - .load('../../../../../../server/services/calendar/statusEvents/setupAppointmentStatusChange', { - './applyStatusChange': { applyStatusChange }, - './handleOverlappingEvents': { handleOverlappingEvents }, - '../../../../app/settings/server': { settings: settingsMock }, - '@rocket.chat/cron': { cronJobs: cronJobsMock }, - }); - -describe('Calendar.StatusEvents', () => { - const fakeEventId = 'eventId123'; - const fakeUserId = 'userId456'; - const fakeStartTime = new Date('2025-01-01T10:00:00Z'); - const fakeEndTime = new Date('2025-01-01T11:00:00Z'); - const statusId = `calendar-presence-status-${fakeEventId}-${fakeUserId}`; - - beforeEach(() => { - cronJobsMock.jobNames.clear(); - applyStatusChange.resetHistory(); - handleOverlappingEvents.resetHistory(); - settingsMock.clear(); - settingsMock.set('Calendar_BusyStatus_Enabled', true); - }); - - describe('#setupAppointmentStatusChange', () => { - it('should do nothing if busy status setting is disabled', async () => { - settingsMock.set('Calendar_BusyStatus_Enabled', false); - - await setupAppointmentStatusChange(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, undefined, false); - - expect(cronJobsMock.jobNames.size).to.equal(0); - }); - - it('should do nothing if endTime is not provided', async () => { - await setupAppointmentStatusChange(fakeEventId, fakeUserId, fakeStartTime, undefined, undefined, false); - - expect(cronJobsMock.jobNames.size).to.equal(0); - }); - - it('should handle overlapping events when shouldScheduleRemoval=true', async () => { - handleOverlappingEvents.resolves({ shouldProceed: false }); - - await setupAppointmentStatusChange(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY, true); - - expect(handleOverlappingEvents.callCount).to.equal(1); - expect(cronJobsMock.jobNames.size).to.equal(0); - }); - - it('should schedule status change at the start time when shouldScheduleRemoval=true', async () => { - handleOverlappingEvents.resolves({ shouldProceed: true }); - - await setupAppointmentStatusChange(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY, true); - - expect(cronJobsMock.jobNames.has(statusId)).to.true; - }); - - it('should schedule status change at the end time when shouldScheduleRemoval=false', async () => { - await setupAppointmentStatusChange(fakeEventId, fakeUserId, fakeStartTime, fakeEndTime, UserStatus.BUSY, false); - - expect(cronJobsMock.jobNames.has(statusId)).to.true; - }); - }); -}); diff --git a/packages/core-services/src/types/ICalendarService.ts b/packages/core-services/src/types/ICalendarService.ts index f74b63b056b81..a7744ddd7c12d 100644 --- a/packages/core-services/src/types/ICalendarService.ts +++ b/packages/core-services/src/types/ICalendarService.ts @@ -10,5 +10,6 @@ export interface ICalendarService { update(eventId: ICalendarEvent['_id'], data: Partial): Promise; delete(eventId: ICalendarEvent['_id']): Promise; setupNextNotification(): Promise; + setupNextStatusChange(): Promise; cancelUpcomingStatusChanges(uid: IUser['_id'], endTime?: Date): Promise; } diff --git a/packages/core-typings/src/ICalendarEvent.ts b/packages/core-typings/src/ICalendarEvent.ts index fb58bde124c43..5088f99cebfca 100644 --- a/packages/core-typings/src/ICalendarEvent.ts +++ b/packages/core-typings/src/ICalendarEvent.ts @@ -1,5 +1,6 @@ import type { IRocketChatRecord } from './IRocketChatRecord'; import type { IUser } from './IUser'; +import type { UserStatus } from './UserStatus'; export interface ICalendarEvent extends IRocketChatRecord { startTime: Date; @@ -17,4 +18,5 @@ export interface ICalendarEvent extends IRocketChatRecord { reminderTime?: Date; busy?: boolean; + previousStatus?: UserStatus; } diff --git a/packages/model-typings/src/models/ICalendarEventModel.ts b/packages/model-typings/src/models/ICalendarEventModel.ts index 24c4bc30ac0a3..9aae06964d25a 100644 --- a/packages/model-typings/src/models/ICalendarEventModel.ts +++ b/packages/model-typings/src/models/ICalendarEventModel.ts @@ -15,4 +15,9 @@ export interface ICalendarEventModel extends IBaseModel { ): Promise; findOverlappingEvents(eventId: ICalendarEvent['_id'], uid: IUser['_id'], startTime: Date, endTime: Date): FindCursor; findEligibleEventsForCancelation(uid: IUser['_id'], endTime: Date): FindCursor; + findEventsToScheduleNow(now: Date, endTime: Date): FindCursor; + findNextFutureEvent(startTime: Date): Promise; + findInProgressEvents(now: Date): FindCursor; + findEventsStartingNow({ now, offset }: { now: Date; offset?: number }): FindCursor; + findEventsEndingNow({ now, offset }: { now: Date; offset?: number }): FindCursor; } diff --git a/packages/models/src/models/CalendarEvent.ts b/packages/models/src/models/CalendarEvent.ts index 4de0c4a638e1a..afbbf83352fa8 100644 --- a/packages/models/src/models/CalendarEvent.ts +++ b/packages/models/src/models/CalendarEvent.ts @@ -50,7 +50,7 @@ export class CalendarEventRaw extends BaseRaw implements ICalend public async updateEvent( eventId: ICalendarEvent['_id'], - { subject, description, startTime, meetingUrl, reminderMinutesBeforeStart, reminderTime }: Partial, + { subject, description, startTime, meetingUrl, reminderMinutesBeforeStart, reminderTime, previousStatus }: Partial, ): Promise { return this.updateOne( { _id: eventId }, @@ -62,6 +62,7 @@ export class CalendarEventRaw extends BaseRaw implements ICalend ...(meetingUrl !== undefined ? { meetingUrl } : {}), ...(reminderMinutesBeforeStart ? { reminderMinutesBeforeStart } : {}), ...(reminderTime ? { reminderTime } : {}), + ...(previousStatus ? { previousStatus } : {}), }, }, ); @@ -149,4 +150,98 @@ export class CalendarEventRaw extends BaseRaw implements ICalend endTime: { $exists: true, $gte: endTime }, }); } + + public findEventsToScheduleNow(now: Date, endTime: Date): FindCursor { + return this.find( + { + startTime: { $gte: now, $lt: endTime }, + busy: { $ne: false }, + endTime: { $exists: true }, + }, + { + sort: { startTime: 1 }, + projection: { + _id: 1, + uid: 1, + startTime: 1, + endTime: 1, + }, + }, + ); + } + + public async findNextFutureEvent(startTime: Date): Promise { + return this.findOne( + { + startTime: { $gte: startTime }, + busy: { $ne: false }, + endTime: { $exists: true }, + }, + { + sort: { startTime: 1 }, + projection: { + startTime: 1, + }, + }, + ); + } + + public findEventsStartingNow({ now, offset = 1000 }: { now: Date; offset?: number }): FindCursor { + return this.find( + { + startTime: { + $gte: new Date(now.getTime() - offset), + $lt: new Date(now.getTime() + offset), + }, + busy: { $ne: false }, + }, + { + projection: { + _id: 1, + uid: 1, + startTime: 1, + endTime: 1, + }, + }, + ); + } + + public findEventsEndingNow({ now, offset = 1000 }: { now: Date; offset?: number }): FindCursor { + return this.find( + { + endTime: { + $gte: new Date(now.getTime() - offset), + $lt: new Date(now.getTime() + offset), + }, + busy: { $ne: false }, + }, + { + projection: { + _id: 1, + uid: 1, + startTime: 1, + endTime: 1, + previousStatus: 1, + }, + }, + ); + } + + public findInProgressEvents(now: Date): FindCursor { + return this.find( + { + startTime: { $lt: now }, + endTime: { $gt: now }, + busy: { $ne: false }, + }, + { + projection: { + _id: 1, + uid: 1, + startTime: 1, + endTime: 1, + }, + }, + ); + } } From 2cce386ac7cf3daca699c8233e8884fa581b74da Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 31 Mar 2025 14:48:12 -0300 Subject: [PATCH 053/187] chore: don't use versions for internal deps (#35302) --- packages/fuselage-ui-kit/package.json | 6 ++--- packages/gazzodown/package.json | 4 ++-- packages/ui-avatar/package.json | 2 +- packages/ui-client/package.json | 4 ++-- packages/ui-video-conf/package.json | 4 ++-- packages/ui-voip/package.json | 6 ++--- packages/web-ui-registration/package.json | 2 +- yarn.lock | 28 +++++++++++------------ 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 79afa2e0852f2..fa52eadf62af9 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -101,10 +101,10 @@ "@rocket.chat/icons": "*", "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "12.0.0", - "@rocket.chat/ui-contexts": "16.0.0", + "@rocket.chat/ui-avatar": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", "@rocket.chat/ui-kit": "0.37.0", - "@rocket.chat/ui-video-conf": "16.0.0", + "@rocket.chat/ui-video-conf": "workspace:^", "@tanstack/react-query": "*", "react": "*", "react-dom": "*" diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 532c9d286abc8..261fb073e865d 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -76,8 +76,8 @@ "@rocket.chat/fuselage-tokens": "*", "@rocket.chat/message-parser": "0.31.31", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "16.0.0", - "@rocket.chat/ui-contexts": "16.0.0", + "@rocket.chat/ui-client": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", "katex": "*", "react": "*" }, diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 94e5719351974..5bce63f648869 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -30,7 +30,7 @@ ], "peerDependencies": { "@rocket.chat/fuselage": "*", - "@rocket.chat/ui-contexts": "16.0.0", + "@rocket.chat/ui-contexts": "workspace:^", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 0775052e651a7..0c5167eb5e5f0 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -63,8 +63,8 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-avatar": "12.0.0", - "@rocket.chat/ui-contexts": "16.0.0", + "@rocket.chat/ui-avatar": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", "react": "*", "react-i18next": "*" }, diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index d03b12040863c..56e4bd925e83f 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -58,8 +58,8 @@ "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "12.0.0", - "@rocket.chat/ui-contexts": "16.0.0", + "@rocket.chat/ui-avatar": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", "react": "~17.0.2", "react-dom": "^17.0.2" }, diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 74731b3f32888..06808a427f2f1 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -67,9 +67,9 @@ "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "12.0.0", - "@rocket.chat/ui-client": "16.0.0", - "@rocket.chat/ui-contexts": "16.0.0", + "@rocket.chat/ui-avatar": "workspace:^", + "@rocket.chat/ui-client": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", "react": "~17.0.2", "react-aria": "~3.23.1", "react-dom": "^17.0.2" diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index c3cbf289e28d5..fb2db4371d0a7 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -50,7 +50,7 @@ "peerDependencies": { "@rocket.chat/layout": "*", "@rocket.chat/tools": "0.2.2", - "@rocket.chat/ui-contexts": "16.0.0", + "@rocket.chat/ui-contexts": "workspace:^", "@tanstack/react-query": "*", "react": "*", "react-hook-form": "*", diff --git a/yarn.lock b/yarn.lock index 2585df2219f6e..5c11b88e9903d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8231,10 +8231,10 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 12.0.0 - "@rocket.chat/ui-contexts": 16.0.0 + "@rocket.chat/ui-avatar": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" "@rocket.chat/ui-kit": 0.37.0 - "@rocket.chat/ui-video-conf": 16.0.0 + "@rocket.chat/ui-video-conf": "workspace:^" "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -8321,8 +8321,8 @@ __metadata: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": 0.31.31 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 16.0.0 - "@rocket.chat/ui-contexts": 16.0.0 + "@rocket.chat/ui-client": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" katex: "*" react: "*" languageName: unknown @@ -9581,7 +9581,7 @@ __metadata: typescript: "npm:~5.7.2" peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": 16.0.0 + "@rocket.chat/ui-contexts": "workspace:^" react: ~17.0.2 languageName: unknown linkType: soft @@ -9635,8 +9635,8 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-avatar": 12.0.0 - "@rocket.chat/ui-contexts": 16.0.0 + "@rocket.chat/ui-avatar": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" react: "*" react-i18next: "*" languageName: unknown @@ -9803,8 +9803,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 12.0.0 - "@rocket.chat/ui-contexts": 16.0.0 + "@rocket.chat/ui-avatar": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" react: ~17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -9860,9 +9860,9 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 12.0.0 - "@rocket.chat/ui-client": 16.0.0 - "@rocket.chat/ui-contexts": 16.0.0 + "@rocket.chat/ui-avatar": "workspace:^" + "@rocket.chat/ui-client": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" react: ~17.0.2 react-aria: ~3.23.1 react-dom: ^17.0.2 @@ -9953,7 +9953,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": 16.0.0 + "@rocket.chat/ui-contexts": "workspace:^" "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From c5f67341a5604dfbbba45c653d52a14590050292 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:47:00 -0300 Subject: [PATCH 054/187] fix: `EmojiPicker` unnecessary rerenders (#35605) --- .changeset/thirty-cameras-warn.md | 5 ++ .../emoji-custom/client/lib/emojiCustom.ts | 5 ++ .../client/hooks/useEmojiOne.ts | 3 + apps/meteor/app/emoji/client/helpers.ts | 77 +++++++++++++++---- apps/meteor/app/emoji/client/index.ts | 2 +- apps/meteor/app/emoji/client/lib.ts | 8 +- .../client/contexts/EmojiPickerContext.ts | 20 ++--- .../EmojiPickerProvider.tsx | 66 +++++----------- .../composer/EmojiPicker/CategoriesResult.tsx | 23 +++--- .../composer/EmojiPicker/EmojiCategoryRow.tsx | 74 +++++++----------- .../composer/EmojiPicker/EmojiPicker.tsx | 40 +++++----- .../EmojiPicker/EmojiPickerCategoryItem.tsx | 7 +- 12 files changed, 175 insertions(+), 155 deletions(-) create mode 100644 .changeset/thirty-cameras-warn.md diff --git a/.changeset/thirty-cameras-warn.md b/.changeset/thirty-cameras-warn.md new file mode 100644 index 0000000000000..37c9c83761a2a --- /dev/null +++ b/.changeset/thirty-cameras-warn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Improves the performance of the Emoji Picker. diff --git a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts index 8b24cd0c29c76..bb76f7388c179 100644 --- a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts +++ b/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts @@ -46,6 +46,7 @@ export const deleteEmojiCustom = (emojiData: IEmoji) => { } removeFromRecent(emojiData.name, emoji.packages.base.emojisByCategory.recent); + emoji.dispatchUpdate(); }; export const updateEmojiCustom = (emojiData: IEmoji) => { @@ -93,6 +94,8 @@ export const updateEmojiCustom = (emojiData: IEmoji) => { if (previousExists) { replaceEmojiInRecent({ oldEmoji: emojiData.previousName, newEmoji: emojiData.name }); } + + emoji.dispatchUpdate(); }; const customRender = (html: string) => { @@ -103,6 +106,7 @@ const customRender = (html: string) => { `]*>.*?<\/object>|]*>.*?<\/span>|<(?:object|embed|svg|img|div|span|p|a)[^>]*>|(${emojisMatchGroup})`, 'gi', ); + emoji.dispatchUpdate(); } html = html.replace(emoji.packages.emojiCustom._regexp!, (shortname) => { @@ -160,6 +164,7 @@ Meteor.startup(() => { }; } } + emoji.dispatchUpdate(); } catch (e) { console.error('Error getting custom emoji', e); } diff --git a/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts b/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts index 6264f361b6712..12e5496073e7a 100644 --- a/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts +++ b/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts @@ -35,8 +35,10 @@ export const useEmojiOne = () => { } } } + emoji.dispatchUpdate(); }); }, []); + useEffect(() => { if (emoji.packages.emojione) { // Additional settings -- ascii emojis @@ -51,6 +53,7 @@ export const useEmojiOne = () => { }; void ascii(); + emoji.dispatchUpdate(); } }, [convertAsciiToEmoji]); }; diff --git a/apps/meteor/app/emoji/client/helpers.ts b/apps/meteor/app/emoji/client/helpers.ts index a203216640f5e..7a44ede46d19f 100644 --- a/apps/meteor/app/emoji/client/helpers.ts +++ b/apps/meteor/app/emoji/client/helpers.ts @@ -1,37 +1,69 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; import type { EmojiCategory, EmojiItem } from '.'; -import { emoji } from './lib'; +import { emoji, emojiEmitter } from './lib'; export const CUSTOM_CATEGORY = 'rocket'; +type RowItem = Array; +type RowDivider = { category: string; i18n: TranslationKey }; +type LoadMoreItem = { loadMore: true }; +export type EmojiPickerItem = RowItem | RowDivider | LoadMoreItem; + +export type CategoriesIndexes = { key: string; index: number }[]; + +export const isRowDivider = (item: EmojiPickerItem): item is RowDivider => 'i18n' in item; +export const isLoadMore = (item: EmojiPickerItem): item is LoadMoreItem => 'loadMore' in item; + +export const createEmojiListByCategorySubscription = ( + customItemsLimit: number, + actualTone: number, + recentEmojis: string[], + setRecentEmojis: (emojis: string[]) => void, +): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ReturnType] => { + let result: ReturnType = [[], []]; + updateRecent(recentEmojis); + + const sub = (cb: () => void) => { + result = createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis); + + return emojiEmitter.on('updated', () => { + result = createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis); + cb(); + }); + }; + + return [sub, () => result]; +}; + export const createPickerEmojis = ( customItemsLimit: number, actualTone: number, recentEmojis: string[], setRecentEmojis: (emojis: string[]) => void, -) => { +): [EmojiPickerItem[], CategoriesIndexes] => { const categories = getCategoriesList(); + const categoriesIndexes: CategoriesIndexes = []; - const mappedCategories = categories.map((category) => ({ - key: category.key, - i18n: category.i18n, - emojis: { - list: createEmojiList(category.key, actualTone, recentEmojis, setRecentEmojis), - limit: category.key === CUSTOM_CATEGORY ? customItemsLimit : null, - }, - })); + const mappedCategories = categories.reduce((acc, category) => { + categoriesIndexes.push({ key: category.key, index: acc.length }); + acc.push({ category: category.key, i18n: category.i18n }); + acc.push(...createEmojiList(customItemsLimit, category.key, actualTone, recentEmojis, setRecentEmojis)); + return acc; + }, []); - return mappedCategories; + return [mappedCategories, categoriesIndexes]; }; export const createEmojiList = ( + customItemsLimit: number, category: string, actualTone: number | null, recentEmojis: string[], setRecentEmojis: (emojis: string[]) => void, -) => { - const emojiList: EmojiItem[] = []; +): (RowItem | LoadMoreItem)[] => { + const items: RowItem = []; const emojiPackages = Object.values(emoji.packages); emojiPackages.forEach((emojiPackage) => { @@ -57,11 +89,23 @@ export const createEmojiList = ( if (!image) { continue; } - emojiList.push({ emoji: current, image }); + items.push({ emoji: current, image, category }); } }); - return emojiList; + const rowCount = 9; + const rowList: Array = Array.from({ length: Math.ceil(items.length / rowCount) }).map(() => []); + + for (let i = 0; i < rowList.length; i++) { + const row = items.slice(i * rowCount, i * rowCount + rowCount); + rowList[i] = row; + } + + if (category === CUSTOM_CATEGORY && customItemsLimit < items.length) { + rowList.push({ loadMore: true }); + } + + return rowList; }; export const getCategoriesList = () => { @@ -149,6 +193,8 @@ export const removeFromRecent = (emoji: string, recentEmojis: string[], setRecen setRecentEmojis?.(recentEmojis); }; +// There's no need to dispatchUpdate here. This helper is called before the list is generated. +// This means that the recent list will always be up to date by the time it is used. export const updateRecent = (recentList: string[]) => { const recentPkgList: string[] = emoji.packages.base.emojisByCategory.recent; recentList?.forEach((_emoji) => { @@ -162,6 +208,7 @@ export const replaceEmojiInRecent = ({ oldEmoji, newEmoji }: { oldEmoji: string; if (pos !== -1) { recentPkgList[pos] = newEmoji; + emoji.dispatchUpdate(); } }; diff --git a/apps/meteor/app/emoji/client/index.ts b/apps/meteor/app/emoji/client/index.ts index 420abe27f211f..65e3b79cd5a6f 100644 --- a/apps/meteor/app/emoji/client/index.ts +++ b/apps/meteor/app/emoji/client/index.ts @@ -1,3 +1,3 @@ export * from './helpers'; export * from './types'; -export { emoji } from './lib'; +export { emoji, emojiEmitter } from './lib'; diff --git a/apps/meteor/app/emoji/client/lib.ts b/apps/meteor/app/emoji/client/lib.ts index 1d2397c9568d3..12f50a2c888f0 100644 --- a/apps/meteor/app/emoji/client/lib.ts +++ b/apps/meteor/app/emoji/client/lib.ts @@ -1,8 +1,11 @@ +import { Emitter } from '@rocket.chat/emitter'; import emojione from 'emojione'; import type { EmojiPackages } from '../lib/rocketchat'; -export const emoji: EmojiPackages = { +export const emojiEmitter = new Emitter<{ updated: void }>(); + +export const emoji: EmojiPackages & { dispatchUpdate: () => void } = { packages: { base: { emojiCategories: [{ key: 'recent', i18n: 'Frequently_Used' }], @@ -23,4 +26,7 @@ export const emoji: EmojiPackages = { }, }, list: {}, + dispatchUpdate() { + emojiEmitter.emit('updated'); + }, }; diff --git a/apps/meteor/client/contexts/EmojiPickerContext.ts b/apps/meteor/client/contexts/EmojiPickerContext.ts index 77adbe419c1fa..b79239441f188 100644 --- a/apps/meteor/client/contexts/EmojiPickerContext.ts +++ b/apps/meteor/client/contexts/EmojiPickerContext.ts @@ -1,12 +1,6 @@ -import type { MutableRefObject } from 'react'; import { createContext, useContext } from 'react'; -import type { EmojiByCategory } from '../../app/emoji/client'; - -type EmojiCategoryPosition = { - key: string; - top: number; -}; +import type { EmojiPickerItem, CategoriesIndexes } from '../../app/emoji/client'; type EmojiPickerContextValue = { open: (ref: Element, callback: (emoji: string) => void) => void; @@ -16,17 +10,17 @@ type EmojiPickerContextValue = { handlePreview: (emoji: string, name: string) => void; handleRemovePreview: () => void; addRecentEmoji: (emoji: string) => void; - getEmojiListsByCategory: () => EmojiByCategory[]; + emojiListByCategory: EmojiPickerItem[]; recentEmojis: string[]; setRecentEmojis: (emoji: string[]) => void; actualTone: number; currentCategory: string; setCurrentCategory: (category: string) => void; - categoriesPosition: MutableRefObject; customItemsLimit: number; setCustomItemsLimit: (limit: number) => void; setActualTone: (tone: number) => void; quickReactions: { emoji: string; image: string }[]; + categoriesIndexes: CategoriesIndexes; }; export const EmojiPickerContext = createContext(undefined); @@ -56,9 +50,9 @@ export const useEmojiPickerData = () => { actualTone, addRecentEmoji, currentCategory, - categoriesPosition, + categoriesIndexes, customItemsLimit, - getEmojiListsByCategory, + emojiListByCategory, quickReactions, recentEmojis, setActualTone, @@ -69,12 +63,12 @@ export const useEmojiPickerData = () => { return { addRecentEmoji, - getEmojiListsByCategory, + emojiListByCategory, recentEmojis, setRecentEmojis, actualTone, currentCategory, - categoriesPosition, + categoriesIndexes, setCurrentCategory, customItemsLimit, setCustomItemsLimit, diff --git a/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx index b1db9e481df1a..bf3207bbb8d1d 100644 --- a/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx +++ b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx @@ -1,31 +1,38 @@ import { useDebouncedState, useLocalStorage } from '@rocket.chat/fuselage-hooks'; import type { ReactNode, ReactElement, ContextType } from 'react'; -import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; +import { useState, useCallback, useMemo, useSyncExternalStore } from 'react'; import { useUpdateCustomEmoji } from './useUpdateCustomEmoji'; -import type { EmojiByCategory } from '../../../app/emoji/client'; -import { emoji, getFrequentEmoji, updateRecent, createEmojiList, createPickerEmojis, CUSTOM_CATEGORY } from '../../../app/emoji/client'; +import { emoji, getFrequentEmoji, createEmojiListByCategorySubscription } from '../../../app/emoji/client'; import { EmojiPickerContext } from '../../contexts/EmojiPickerContext'; import EmojiPicker from '../../views/composer/EmojiPicker/EmojiPicker'; const DEFAULT_ITEMS_LIMIT = 90; +// limit recent emojis to 27 (3 rows of 9) +const RECENT_EMOJIS_LIMIT = 27; + const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElement => { const [emojiPicker, setEmojiPicker] = useState(null); const [emojiToPreview, setEmojiToPreview] = useDebouncedState<{ emoji: string; name: string } | null>(null, 100); const [recentEmojis, setRecentEmojis] = useLocalStorage('emoji.recent', []); + const [frequentEmojis, setFrequentEmojis] = useLocalStorage<[string, number][]>('emoji.frequent', []); + const [actualTone, setActualTone] = useLocalStorage('emoji.tone', 0); const [currentCategory, setCurrentCategory] = useState('recent'); - const categoriesPosition = useRef([]); const [customItemsLimit, setCustomItemsLimit] = useState(DEFAULT_ITEMS_LIMIT); - const [frequentEmojis, setFrequentEmojis] = useLocalStorage<[string, number][]>('emoji.frequent', []); - const [quickReactions, setQuickReactions] = useState<{ emoji: string; image: string }[]>(() => getFrequentEmoji(frequentEmojis.map(([emoji]) => emoji)), ); + const [sub, getSnapshot] = useMemo(() => { + return createEmojiListByCategorySubscription(customItemsLimit, actualTone, recentEmojis, setRecentEmojis); + }, [customItemsLimit, actualTone, recentEmojis, setRecentEmojis]); + + const [emojiListByCategory, categoriesIndexes] = useSyncExternalStore(sub, getSnapshot); + useUpdateCustomEmoji(); const addFrequentEmojis = useCallback( @@ -44,37 +51,6 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen [frequentEmojis, setFrequentEmojis], ); - const [getEmojiListsByCategory, setEmojiListsByCategoryGetter] = useState<() => EmojiByCategory[]>(() => () => []); - - // TODO: improve this update - const updateEmojiListByCategory = useCallback( - (categoryKey: string, limit: number = DEFAULT_ITEMS_LIMIT) => { - setEmojiListsByCategoryGetter( - (getEmojiListsByCategory) => () => - getEmojiListsByCategory().map((category) => - categoryKey === category.key - ? { - ...category, - emojis: { - list: createEmojiList(category.key, null, recentEmojis, setRecentEmojis), - limit: category.key === CUSTOM_CATEGORY ? limit | customItemsLimit : null, - }, - } - : category, - ), - ); - }, - [customItemsLimit, recentEmojis, setRecentEmojis], - ); - - useEffect(() => { - if (recentEmojis?.length > 0) { - updateRecent(recentEmojis); - } - - setEmojiListsByCategoryGetter(() => () => createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis)); - }, [actualTone, recentEmojis, customItemsLimit, currentCategory, setRecentEmojis, frequentEmojis]); - const addRecentEmoji = useCallback( (_emoji: string) => { addFrequentEmojis(_emoji); @@ -88,14 +64,13 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen recent.unshift(_emoji); - // limit recent emojis to 27 (3 rows of 9) - recent.splice(27); + recent.splice(RECENT_EMOJIS_LIMIT); - setRecentEmojis(recent); + // If this value is not cloned, the recent list will not be updated + setRecentEmojis([...recent]); emoji.packages.base.emojisByCategory.recent = recent; - updateEmojiListByCategory('recent'); }, - [recentEmojis, setRecentEmojis, updateEmojiListByCategory, addFrequentEmojis], + [recentEmojis, setRecentEmojis, addFrequentEmojis], ); const open = useCallback((ref: Element, callback: (emoji: string) => void) => { @@ -115,13 +90,13 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen handlePreview, handleRemovePreview, addRecentEmoji, - getEmojiListsByCategory, + emojiListByCategory, recentEmojis, setRecentEmojis, actualTone, currentCategory, setCurrentCategory, - categoriesPosition, + categoriesIndexes, customItemsLimit, setCustomItemsLimit, setActualTone, @@ -132,7 +107,8 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen open, emojiToPreview, addRecentEmoji, - getEmojiListsByCategory, + emojiListByCategory, + categoriesIndexes, recentEmojis, setRecentEmojis, actualTone, diff --git a/apps/meteor/client/views/composer/EmojiPicker/CategoriesResult.tsx b/apps/meteor/client/views/composer/EmojiPicker/CategoriesResult.tsx index 57869f5aeafd9..68cb34dd27edf 100644 --- a/apps/meteor/client/views/composer/EmojiPicker/CategoriesResult.tsx +++ b/apps/meteor/client/views/composer/EmojiPicker/CategoriesResult.tsx @@ -1,24 +1,24 @@ import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; -import type { MouseEvent, UIEventHandler } from 'react'; +import type { MouseEvent } from 'react'; import { forwardRef, memo, useRef } from 'react'; -import type { VirtuosoHandle } from 'react-virtuoso'; +import type { ListRange, VirtuosoHandle } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso'; import EmojiCategoryRow from './EmojiCategoryRow'; -import type { EmojiByCategory } from '../../../../app/emoji/client'; +import type { EmojiPickerItem } from '../../../../app/emoji/client'; import { VirtualizedScrollbars } from '../../../components/CustomScrollbars'; type CategoriesResultProps = { - emojiListByCategory: EmojiByCategory[]; + items: EmojiPickerItem[]; customItemsLimit: number; handleLoadMore: () => void; handleSelectEmoji: (event: MouseEvent) => void; - handleScroll: UIEventHandler; + handleScroll: (range: ListRange) => void; }; const CategoriesResult = forwardRef(function CategoriesResult( - { emojiListByCategory, customItemsLimit, handleLoadMore, handleSelectEmoji, handleScroll }, + { items, customItemsLimit, handleLoadMore, handleSelectEmoji, handleScroll }, ref, ) { const wrapper = useRef(null); @@ -36,9 +36,9 @@ const CategoriesResult = forwardRef(funct { if (!wrapper.current) { return; @@ -50,13 +50,12 @@ const CategoriesResult = forwardRef(funct wrapper.current.classList.remove('pointer-none'); } }} - itemContent={(_, { key, ...data }) => ( + itemContent={(_, item) => ( )} /> diff --git a/apps/meteor/client/views/composer/EmojiPicker/EmojiCategoryRow.tsx b/apps/meteor/client/views/composer/EmojiPicker/EmojiCategoryRow.tsx index 3dea93f536aa4..76909ba1c78de 100644 --- a/apps/meteor/client/views/composer/EmojiPicker/EmojiCategoryRow.tsx +++ b/apps/meteor/client/views/composer/EmojiPicker/EmojiCategoryRow.tsx @@ -5,20 +5,18 @@ import { memo, type MouseEvent } from 'react'; import { useTranslation } from 'react-i18next'; import EmojiElement from './EmojiElement'; -import { CUSTOM_CATEGORY } from '../../../../app/emoji/client'; -import type { EmojiByCategory } from '../../../../app/emoji/client'; -import { useEmojiPickerData } from '../../../contexts/EmojiPickerContext'; +import { isRowDivider, isLoadMore } from '../../../../app/emoji/client'; +import type { EmojiPickerItem } from '../../../../app/emoji/client'; -type EmojiCategoryRowProps = Omit & { - categoryKey: EmojiByCategory['key']; +type EmojiCategoryRowProps = { customItemsLimit: number; handleLoadMore: () => void; handleSelectEmoji: (e: MouseEvent) => void; + item: EmojiPickerItem; }; -const EmojiCategoryRow = ({ categoryKey, i18n, emojis, customItemsLimit, handleLoadMore, handleSelectEmoji }: EmojiCategoryRowProps) => { +const EmojiCategoryRow = ({ item, handleLoadMore, handleSelectEmoji }: EmojiCategoryRowProps) => { const { t } = useTranslation(); - const { categoriesPosition } = useEmojiPickerData(); const categoryRowStyle = css` button { @@ -30,46 +28,30 @@ const EmojiCategoryRow = ({ categoryKey, i18n, emojis, customItemsLimit, handleL } `; - return ( - - { - if (categoriesPosition.current.find(({ key }) => key === categoryKey)) { - return; - } + if (isRowDivider(item)) { + return ( + <> + + {t(item.i18n)} + + + ); + } + + if (isLoadMore(item)) { + return {t('Load_more')}; + } - categoriesPosition.current.push({ key: categoryKey, top: element?.offsetTop }); - return element; - }} - > - {t(i18n)} - - {emojis.list.length > 0 && ( - - <> - {categoryKey === CUSTOM_CATEGORY && - emojis.list.map( - ({ emoji, image }, index = 1) => - index < customItemsLimit && ( - - ), - )} - {!(categoryKey === CUSTOM_CATEGORY) && - emojis.list.map(({ emoji, image }) => ( - - ))} - - - )} - {emojis.limit && emojis?.limit > 0 && emojis.list.length > emojis.limit && ( - {t('Load_more')} - )} - {emojis.list.length === 0 && {t('No_emojis_found')}} - + if (item.length === 0) { + return {t('No_emojis_found')}; + } + + return ( + + {item.map(({ emoji, image, category }) => ( + + ))} + ); }; diff --git a/apps/meteor/client/views/composer/EmojiPicker/EmojiPicker.tsx b/apps/meteor/client/views/composer/EmojiPicker/EmojiPicker.tsx index ad61d102a03ab..cef5d6c2fcd7c 100644 --- a/apps/meteor/client/views/composer/EmojiPicker/EmojiPicker.tsx +++ b/apps/meteor/client/views/composer/EmojiPicker/EmojiPicker.tsx @@ -10,9 +10,9 @@ import { EmojiPickerPreview, } from '@rocket.chat/ui-client'; import { useTranslation, usePermission, useRoute } from '@rocket.chat/ui-contexts'; -import type { ChangeEvent, KeyboardEvent, MouseEvent, RefObject, UIEvent } from 'react'; +import type { ChangeEvent, KeyboardEvent, MouseEvent, RefObject } from 'react'; import { useLayoutEffect, useState, useEffect, useRef } from 'react'; -import type { VirtuosoHandle } from 'react-virtuoso'; +import type { ListRange, VirtuosoHandle } from 'react-virtuoso'; import CategoriesResult from './CategoriesResult'; import EmojiPickerCategoryItem from './EmojiPickerCategoryItem'; @@ -60,8 +60,8 @@ const EmojiPicker = ({ reference, onClose, onPickEmoji }: EmojiPickerProps) => { setRecentEmojis, actualTone, currentCategory, - categoriesPosition, - getEmojiListsByCategory, + categoriesIndexes, + emojiListByCategory, customItemsLimit, setActualTone, setCustomItemsLimit, @@ -155,24 +155,29 @@ const EmojiPicker = ({ reference, onClose, onPickEmoji }: EmojiPickerProps) => { setCustomItemsLimit(customItemsLimit + 90); }; - const handleScroll = (event: UIEvent) => { - const categoryMargin = 12; - const { scrollTop } = event.currentTarget; + const handleScroll = (range: ListRange) => { + const { startIndex } = range; - const lastCategory = categoriesPosition.current - ?.filter((category, index = 1) => category.top - categoryMargin * index <= scrollTop) - .pop(); + const category = categoriesIndexes.find( + (category, index) => category.index <= startIndex + 1 && categoriesIndexes[index + 1]?.index >= startIndex, + ); - if (!lastCategory) { + if (!category) { return; } - setCurrentCategory(lastCategory.key); + setCurrentCategory(category.key); }; - const handleGoToCategory = (categoryIndex: number) => { + const handleGoToCategory = (category: string) => { setSearching(false); - virtuosoRef.current?.scrollToIndex({ index: categoryIndex }); + const { index } = categoriesIndexes.find((item) => item.key === category) || {}; + + if (index === undefined) { + return; + } + + virtuosoRef.current?.scrollToIndex({ index: index > 0 ? index + 1 : 0 }); }; const handleGoToAddCustom = () => { @@ -196,13 +201,12 @@ const EmojiPicker = ({ reference, onClose, onPickEmoji }: EmojiPickerProps) => { /> - {emojiCategories.map((category, index) => ( + {emojiCategories.map((category) => ( handleGoToCategory(category.key)} /> ))} @@ -212,7 +216,7 @@ const EmojiPicker = ({ reference, onClose, onPickEmoji }: EmojiPickerProps) => { {!searching && ( void; + handleGoToCategory: () => void; } & Omit, 'is'>; const mapCategoryIcon = (category: string) => { @@ -45,7 +44,7 @@ const mapCategoryIcon = (category: string) => { } }; -const EmojiPickerCategoryItem = ({ category, index, active, handleGoToCategory, ...props }: EmojiPickerCategoryItemProps) => { +const EmojiPickerCategoryItem = ({ category, active, handleGoToCategory, ...props }: EmojiPickerCategoryItemProps) => { const { t } = useTranslation(); const icon = mapCategoryIcon(category.key); @@ -58,7 +57,7 @@ const EmojiPickerCategoryItem = ({ category, index, active, handleGoToCategory, className={category.key} small aria-label={t(category.i18n)} - onClick={() => handleGoToCategory(index)} + onClick={handleGoToCategory} icon={icon} {...props} /> From 183c43c92def58b21e55bcbda81bedd768c772a9 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 1 Apr 2025 17:59:38 -0300 Subject: [PATCH 055/187] chore: bump ubuntu (#35676) --- .github/workflows/ci-code-check.yml | 2 +- .github/workflows/ci-deploy-gh-pages.yml | 2 +- .github/workflows/ci-test-e2e.yml | 4 +-- .github/workflows/ci-test-unit.yml | 2 +- .github/workflows/ci.yml | 28 +++++++++---------- .github/workflows/new-release.yml | 2 +- .github/workflows/pr-title-checker.yml | 2 +- .github/workflows/pr-update-description.yml | 2 +- .github/workflows/publish-release.yml | 2 +- .github/workflows/release-candidate.yml | 2 +- .github/workflows/stale.yml | 10 +++---- .../workflows/update-version-durability.yml | 2 +- 12 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 7f0dbd485aabd..4339dc3fd0a47 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -15,7 +15,7 @@ env: jobs: code-check: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 name: ${{ matrix.check == 'ts' && 'TypeScript' || 'Code Lint' }} diff --git a/.github/workflows/ci-deploy-gh-pages.yml b/.github/workflows/ci-deploy-gh-pages.yml index c0f1201196f61..2abc44825094a 100644 --- a/.github/workflows/ci-deploy-gh-pages.yml +++ b/.github/workflows/ci-deploy-gh-pages.yml @@ -9,7 +9,7 @@ on: - develop jobs: deploy-preview: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: rharkor/caching-for-turbo@v1.6 diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 6c609f77dfdd6..12c5aea31216d 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -75,7 +75,7 @@ env: jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 env: RC_DOCKERFILE: ${{ inputs.rc-dockerfile }}.${{ (matrix.mongodb-version == '7.0' && 'debian' && false) || 'alpine' }} RC_DOCKER_TAG: ${{ inputs.rc-docker-tag }}.${{ (matrix.mongodb-version == '7.0' && 'debian' && false) || 'alpine' }} @@ -98,7 +98,7 @@ jobs: - name: Setup kernel limits run: | - sudo sysctl -w net.ipv4.ip_local_port_range="500 65535" + echo "500 65535" > sudo tee -a /proc/sys/net/ipv4/ip_local_port_range sudo sysctl -w net.ipv4.tcp_mem="383865 511820 2303190" echo fs.file-max=20000500 | sudo tee -a /etc/sysctl.conf diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 9705ee17e2f8b..1f701d6d1135e 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -23,7 +23,7 @@ env: jobs: test: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 name: Unit Tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c720817ed6a68..082e54787f63b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ env: jobs: release-versions: name: ⚙️ Variables Setup - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: release: ${{ steps.by-tag.outputs.release }} latest-release: ${{ steps.latest.outputs.latest-release }} @@ -93,7 +93,7 @@ jobs: notify-draft-services: name: 🚀 Notify external services - draft - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: [release-versions] steps: - uses: actions/checkout@v4 @@ -136,7 +136,7 @@ jobs: packages-build: name: 📦 Build Packages needs: [release-versions, notify-draft-services] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Github Info run: | @@ -187,7 +187,7 @@ jobs: include-hidden-files: true deploy-preview: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: [release-versions, packages-build] steps: - uses: actions/checkout@v4 @@ -225,7 +225,7 @@ jobs: build: name: 📦 Meteor Build - coverage needs: [release-versions, packages-build] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Collect Workflow Telemetry @@ -256,7 +256,7 @@ jobs: name: 📦 Meteor Build - official needs: [tests-done, release-versions, packages-build] if: (github.event_name == 'release' || github.ref == 'refs/heads/develop') - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - name: Collect Workflow Telemetry @@ -286,7 +286,7 @@ jobs: # TODO: this should go away once upstream builds are fixed build-matrix-rust-bindings-for-alpine: name: Builds matrix rust bindings against alpine - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: check cache for matrix-rust-sdk-crypto-nodejs id: matrix-rust-sdk-crypto-nodejs @@ -331,7 +331,7 @@ jobs: build-gh-docker-coverage: name: 🚢 Build Docker Images for Testing needs: [build, release-versions, build-matrix-rust-bindings-for-alpine] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 env: RC_DOCKERFILE: ${{ needs.release-versions.outputs.rc-dockerfile }}.${{ matrix.platform }} @@ -376,7 +376,7 @@ jobs: build-gh-docker: name: 🚢 Build Docker Images for Production needs: [build-prod, release-versions] - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 env: RC_DOCKERFILE: ${{ needs.release-versions.outputs.rc-dockerfile }}.${{ matrix.platform }} @@ -563,7 +563,7 @@ jobs: tests-done: name: ✅ Tests Done - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: [checks, test-unit, test-api, test-ui, test-api-ee, test-ui-ee, test-ui-ee-no-watcher] if: always() steps: @@ -601,7 +601,7 @@ jobs: deploy: name: 🚀 Publish build assets - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 if: github.event_name == 'release' || github.ref == 'refs/heads/develop' needs: [build-gh-docker, release-versions] @@ -655,7 +655,7 @@ jobs: docker-image-publish: name: 🚀 Publish Docker Image (main) - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: [deploy, release-versions] strategy: @@ -765,7 +765,7 @@ jobs: services-docker-image-publish: name: 🚀 Publish Docker Image (services) - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: [deploy, release-versions] strategy: @@ -854,7 +854,7 @@ jobs: notify-services: name: 🚀 Notify external services - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 needs: - services-docker-image-publish - docker-image-publish diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 943edbfd370e6..2ede6e5ef04d6 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -22,7 +22,7 @@ env: jobs: new-release: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml index 4fe796118e16c..39e40ba3fd4cf 100644 --- a/.github/workflows/pr-title-checker.yml +++ b/.github/workflows/pr-title-checker.yml @@ -10,7 +10,7 @@ on: jobs: check: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: thehanimo/pr-title-checker@v1.4.3 with: diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index d2d4e6bb99dca..9d8fb5dee34ad 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -9,7 +9,7 @@ concurrency: ${{ github.workflow }}-${{ github.ref }} jobs: update-pr: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: startsWith(github.head_ref, 'release-') steps: - name: Checkout Repo diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 44f7a6d04e05e..8dd1402cc1dd9 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -13,7 +13,7 @@ env: jobs: release: name: Release - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout Repo uses: actions/checkout@v4 diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index e1fc962f59228..42dbb908ee08e 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -4,7 +4,7 @@ on: - cron: '28 12 20 * *' # run at minute 28 to avoid the chance of delay due to high load on GH jobs: new-release: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 0ee119fe43aa8..89fb59d0c5845 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -1,11 +1,11 @@ name: Close inactive issues on: schedule: - - cron: "0 */6 * * *" + - cron: '0 */6 * * *' jobs: close-issues: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: issues: write steps: @@ -14,7 +14,7 @@ jobs: days-before-issue-stale: 14 days-before-issue-close: 14 any-of-labels: 'stat: need more info,stat: waiting response' - stale-issue-label: "stat: no response" - stale-issue-message: "This issue has been marked as stale because there has been no further activity in the last 10 days. If the issue remains stale for the next 4 days (a total of 14 days with no activity), then it will be assumed that the question has been resolved and the issue will be automatically closed." - close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." + stale-issue-label: 'stat: no response' + stale-issue-message: 'This issue has been marked as stale because there has been no further activity in the last 10 days. If the issue remains stale for the next 4 days (a total of 14 days with no activity), then it will be assumed that the question has been resolved and the issue will be automatically closed.' + close-issue-message: 'This issue was closed because it has been inactive for 14 days since being marked as stale.' repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml index 0b3fb34c4e6fd..90d6f5c0d0572 100644 --- a/.github/workflows/update-version-durability.yml +++ b/.github/workflows/update-version-durability.yml @@ -11,7 +11,7 @@ on: jobs: update-versions: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 From 701e11814731cc35fcf2cdbde6e4e9e3b926e41e Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Thu, 3 Apr 2025 00:44:27 +0530 Subject: [PATCH 056/187] chore: Bump fuselage packages (#35682) --- apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 4 +- apps/uikit-playground/package.json | 2 +- ee/packages/ui-theming/package.json | 2 +- packages/core-services/package.json | 2 +- packages/core-typings/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/ui-client/package.json | 2 +- packages/ui-composer/package.json | 2 +- packages/ui-kit/package.json | 2 +- packages/ui-video-conf/package.json | 2 +- packages/ui-voip/package.json | 2 +- yarn.lock | 42 ++++++++++----------- 13 files changed, 34 insertions(+), 34 deletions(-) diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 2f68bf4c152c1..2532b552096a8 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -48,7 +48,7 @@ "ws": "^8.18.0" }, "devDependencies": { - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@types/cookie": "^0.5.4", "@types/cookie-parser": "^1.4.7", "@types/ejson": "^2.2.2", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index c35394fda8df5..cdc88d01edf18 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -255,7 +255,7 @@ "@rocket.chat/fuselage-ui-kit": "workspace:^", "@rocket.chat/gazzodown": "workspace:^", "@rocket.chat/i18n": "workspace:^", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/jwt": "workspace:^", "@rocket.chat/layout": "~0.32.0", @@ -271,7 +271,7 @@ "@rocket.chat/mp3-encoder": "^0.31.26", "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", - "@rocket.chat/onboarding-ui": "~0.35.0", + "@rocket.chat/onboarding-ui": "^0.35.1", "@rocket.chat/password-policies": "workspace:^", "@rocket.chat/patch-injection": "workspace:^", "@rocket.chat/pdf-worker": "workspace:^", diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 650525ba8c539..65f24cd2ab405 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -24,7 +24,7 @@ "@rocket.chat/fuselage-toastbar": "^0.35.0", "@rocket.chat/fuselage-tokens": "~0.33.2", "@rocket.chat/fuselage-ui-kit": "workspace:~", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/logo": "^0.32.0", "@rocket.chat/styled": "~0.32.0", "@rocket.chat/ui-avatar": "workspace:^", diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index 8a5fc44e14c71..8a2bae81c66af 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -6,7 +6,7 @@ "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/fuselage": "~0.61.0", "@rocket.chat/fuselage-hooks": "~0.35.0", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/ui-contexts": "workspace:~", "@types/react": "~18.3.17", "eslint": "~8.45.0", diff --git a/packages/core-services/package.json b/packages/core-services/package.json index de7ab82bc1953..74193aeaf9d58 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 229b982b2ec74..352c5d70f912e 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -24,7 +24,7 @@ "/dist" ], "dependencies": { - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/ui-kit": "workspace:~", "@types/express": "^4.17.21" diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index fa52eadf62af9..290947298b890 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -55,7 +55,7 @@ "@rocket.chat/fuselage": "~0.61.0", "@rocket.chat/fuselage-hooks": "~0.35.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/mock-providers": "workspace:^", "@rocket.chat/prettier-config": "~0.31.25", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 0c5167eb5e5f0..6fa55f32745cf 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -23,7 +23,7 @@ "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/fuselage": "~0.61.0", "@rocket.chat/fuselage-hooks": "~0.35.0", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/mock-providers": "workspace:^", "@rocket.chat/ui-avatar": "workspace:~", diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index ca0c786f8e253..19516edcf4254 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -22,7 +22,7 @@ "@react-aria/toolbar": "^3.0.0-nightly.5042", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "~0.61.0", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@storybook/addon-actions": "^8.6.4", "@storybook/addon-docs": "^8.6.4", "@storybook/addon-essentials": "^8.6.4", diff --git a/packages/ui-kit/package.json b/packages/ui-kit/package.json index 31e62196caec8..44248c15e4f75 100644 --- a/packages/ui-kit/package.json +++ b/packages/ui-kit/package.json @@ -40,7 +40,7 @@ "@babel/plugin-transform-runtime": "~7.25.9", "@babel/preset-env": "~7.26.0", "@rocket.chat/eslint-config": "workspace:~", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", "babel-loader": "~9.2.1", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 56e4bd925e83f..a940ec3215943 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -26,7 +26,7 @@ "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "~0.61.0", "@rocket.chat/fuselage-hooks": "~0.35.0", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/styled": "~0.32.0", "@rocket.chat/ui-avatar": "workspace:^", diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 06808a427f2f1..ebb436fa88bc1 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -31,7 +31,7 @@ "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "~0.61.0", "@rocket.chat/fuselage-hooks": "~0.35.0", - "@rocket.chat/icons": "^0.40.0", + "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/mock-providers": "workspace:~", "@rocket.chat/styled": "~0.32.0", diff --git a/yarn.lock b/yarn.lock index 5c11b88e9903d..44624da0eec0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7933,7 +7933,7 @@ __metadata: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -7956,7 +7956,7 @@ __metadata: dependencies: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" "@types/express": "npm:^4.17.21" @@ -8186,7 +8186,7 @@ __metadata: "@rocket.chat/fuselage-hooks": "npm:~0.35.0" "@rocket.chat/fuselage-polyfills": "npm:~0.31.25" "@rocket.chat/gazzodown": "workspace:^" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/mock-providers": "workspace:^" "@rocket.chat/prettier-config": "npm:~0.31.25" @@ -8339,10 +8339,10 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/icons@npm:^0.40.0": - version: 0.40.0 - resolution: "@rocket.chat/icons@npm:0.40.0" - checksum: 10/9fb696db75919f3f0bacdb4637e226e0bb078ca620c1130ccb3b113144b140d040205dd232c69d0c951d5b26488b5d9295bedcbbe69c006feb1cc53eea370614 +"@rocket.chat/icons@npm:^0.42.0": + version: 0.42.0 + resolution: "@rocket.chat/icons@npm:0.42.0" + checksum: 10/0842e471fb0b6cc943421e2e728f489c3cc4d02d38add8503bda4e1905b3fa4f36c4de1d5589142074a79bb0b987faaf79b2b537ca5b8cd51f6c1f89e73e3aa5 languageName: node linkType: hard @@ -8651,7 +8651,7 @@ __metadata: "@rocket.chat/fuselage-ui-kit": "workspace:^" "@rocket.chat/gazzodown": "workspace:^" "@rocket.chat/i18n": "workspace:^" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/jwt": "workspace:^" @@ -8670,7 +8670,7 @@ __metadata: "@rocket.chat/mp3-encoder": "npm:^0.31.26" "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" - "@rocket.chat/onboarding-ui": "npm:~0.35.0" + "@rocket.chat/onboarding-ui": "npm:^0.35.1" "@rocket.chat/password-policies": "workspace:^" "@rocket.chat/patch-injection": "workspace:^" "@rocket.chat/pdf-worker": "workspace:^" @@ -9157,9 +9157,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/onboarding-ui@npm:~0.35.0": - version: 0.35.0 - resolution: "@rocket.chat/onboarding-ui@npm:0.35.0" +"@rocket.chat/onboarding-ui@npm:^0.35.1": + version: 0.35.1 + resolution: "@rocket.chat/onboarding-ui@npm:0.35.1" dependencies: i18next: "npm:~21.6.16" react-hook-form: "npm:~7.54.2" @@ -9174,7 +9174,7 @@ __metadata: react: "*" react-dom: "*" react-i18next: "*" - checksum: 10/eef2a48b76d9a9f96f55c87883c6b0748c85cf742920d47bddda06fc9284fe521fccd246300a9e77b5845989a6d6f88f0ba03d38fe1c31e228c0dd541a0d04cf + checksum: 10/e56caa9767ecd90ce4453439bda4832ce805b40dea4e73cfe10e8983066bb7e7ea8552025d46e21b31660c9fbd5f2e804cb44c15671d9eba17cb5c4e9d3f22bc languageName: node linkType: hard @@ -9595,7 +9595,7 @@ __metadata: "@rocket.chat/css-in-js": "npm:~0.31.25" "@rocket.chat/fuselage": "npm:~0.61.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/mock-providers": "workspace:^" "@rocket.chat/ui-avatar": "workspace:~" @@ -9650,7 +9650,7 @@ __metadata: "@react-aria/toolbar": "npm:^3.0.0-nightly.5042" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": "npm:~0.61.0" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@storybook/addon-actions": "npm:^8.6.4" "@storybook/addon-docs": "npm:^8.6.4" "@storybook/addon-essentials": "npm:^8.6.4" @@ -9715,7 +9715,7 @@ __metadata: "@babel/plugin-transform-runtime": "npm:~7.25.9" "@babel/preset-env": "npm:~7.26.0" "@rocket.chat/eslint-config": "workspace:~" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@types/jest": "npm:~29.5.14" babel-loader: "npm:~9.2.1" @@ -9742,7 +9742,7 @@ __metadata: "@rocket.chat/css-in-js": "npm:~0.31.25" "@rocket.chat/fuselage": "npm:~0.61.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/ui-contexts": "workspace:~" "@types/react": "npm:~18.3.17" eslint: "npm:~8.45.0" @@ -9772,7 +9772,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": "npm:~0.61.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/styled": "npm:~0.32.0" "@rocket.chat/ui-avatar": "workspace:^" @@ -9822,7 +9822,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": "npm:~0.61.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/mock-providers": "workspace:~" "@rocket.chat/styled": "npm:~0.32.0" @@ -9885,7 +9885,7 @@ __metadata: "@rocket.chat/fuselage-toastbar": "npm:^0.35.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" "@rocket.chat/fuselage-ui-kit": "workspace:~" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/logo": "npm:^0.32.0" "@rocket.chat/styled": "npm:~0.32.0" "@rocket.chat/ui-avatar": "workspace:^" @@ -32439,7 +32439,7 @@ __metadata: "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:~0.31.25" - "@rocket.chat/icons": "npm:^0.40.0" + "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" From a1ec1e3e0bfe0db2fa600d0ae8b5bcd396373e8b Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Wed, 2 Apr 2025 18:16:15 -0300 Subject: [PATCH 057/187] regression: Emoji Picker recent list and load more not working (#35673) --- .../client/hooks/useEmojiOne.ts | 49 +++++++++---------- apps/meteor/app/emoji/client/helpers.ts | 18 +++++-- .../composer/EmojiPicker/EmojiCategoryRow.tsx | 5 +- .../EmojiPicker/EmojiPickerNotFound.tsx | 2 +- 4 files changed, 39 insertions(+), 35 deletions(-) diff --git a/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts b/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts index 12e5496073e7a..bf8919d200175 100644 --- a/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts +++ b/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts @@ -1,7 +1,6 @@ import { useUserPreference } from '@rocket.chat/ui-contexts'; -import { useEffect } from 'react'; +import { useEffect, useLayoutEffect } from 'react'; -import { queueMicrotask } from '../../../../client/lib/utils/queueMicrotask'; import { emoji } from '../../../emoji/client'; import { getEmojiConfig } from '../../lib/getEmojiConfig'; import { isSetNotNull } from '../../lib/isSetNotNull'; @@ -11,32 +10,30 @@ const config = getEmojiConfig(); export const useEmojiOne = () => { const convertAsciiToEmoji = useUserPreference('convertAsciiEmoji', true); - useEffect(() => { - queueMicrotask(() => { - emoji.packages.emojione = config.emojione as any; - if (emoji.packages.emojione) { - emoji.packages.emojione.sprites = config.sprites; - emoji.packages.emojione.emojisByCategory = config.emojisByCategory; - emoji.packages.emojione.emojiCategories = config.emojiCategories as any; - emoji.packages.emojione.toneList = config.toneList; - - emoji.packages.emojione.render = config.render; - emoji.packages.emojione.renderPicker = config.renderPicker; - - // RocketChat.emoji.list is the collection of emojis from all emoji packages - for (const [key, currentEmoji] of Object.entries(config.emojione.emojioneList)) { - currentEmoji.emojiPackage = 'emojione'; - emoji.list[key] = currentEmoji; - - if (currentEmoji.shortnames) { - currentEmoji.shortnames.forEach((shortname: string) => { - emoji.list[shortname] = currentEmoji; - }); - } + useLayoutEffect(() => { + emoji.packages.emojione = config.emojione as any; + if (emoji.packages.emojione) { + emoji.packages.emojione.sprites = config.sprites; + emoji.packages.emojione.emojisByCategory = config.emojisByCategory; + emoji.packages.emojione.emojiCategories = config.emojiCategories as any; + emoji.packages.emojione.toneList = config.toneList; + + emoji.packages.emojione.render = config.render; + emoji.packages.emojione.renderPicker = config.renderPicker; + + // RocketChat.emoji.list is the collection of emojis from all emoji packages + for (const [key, currentEmoji] of Object.entries(config.emojione.emojioneList)) { + currentEmoji.emojiPackage = 'emojione'; + emoji.list[key] = currentEmoji; + + if (currentEmoji.shortnames) { + currentEmoji.shortnames.forEach((shortname: string) => { + emoji.list[shortname] = currentEmoji; + }); } } - emoji.dispatchUpdate(); - }); + } + emoji.dispatchUpdate(); }, []); useEffect(() => { diff --git a/apps/meteor/app/emoji/client/helpers.ts b/apps/meteor/app/emoji/client/helpers.ts index 7a44ede46d19f..cbd1b08b687de 100644 --- a/apps/meteor/app/emoji/client/helpers.ts +++ b/apps/meteor/app/emoji/client/helpers.ts @@ -65,14 +65,15 @@ export const createEmojiList = ( ): (RowItem | LoadMoreItem)[] => { const items: RowItem = []; const emojiPackages = Object.values(emoji.packages); + let count = 0; + let limited = false; emojiPackages.forEach((emojiPackage) => { if (!emojiPackage.emojisByCategory?.[category]) { return; } - - const total = emojiPackage.emojisByCategory[category].length; - + const _total = emojiPackage.emojisByCategory[category].length; + const total = category === CUSTOM_CATEGORY ? customItemsLimit - count : _total; for (let i = 0; i < total; i++) { const current = emojiPackage.emojisByCategory[category][i]; @@ -90,6 +91,11 @@ export const createEmojiList = ( continue; } items.push({ emoji: current, image, category }); + count++; + } + + if (_total > total) { + limited = true; } }); @@ -101,7 +107,11 @@ export const createEmojiList = ( rowList[i] = row; } - if (category === CUSTOM_CATEGORY && customItemsLimit < items.length) { + if (rowList.length === 0) { + rowList.push([]); + } + + if (limited) { rowList.push({ loadMore: true }); } diff --git a/apps/meteor/client/views/composer/EmojiPicker/EmojiCategoryRow.tsx b/apps/meteor/client/views/composer/EmojiPicker/EmojiCategoryRow.tsx index 76909ba1c78de..d9b011dee0ada 100644 --- a/apps/meteor/client/views/composer/EmojiPicker/EmojiCategoryRow.tsx +++ b/apps/meteor/client/views/composer/EmojiPicker/EmojiCategoryRow.tsx @@ -42,12 +42,9 @@ const EmojiCategoryRow = ({ item, handleLoadMore, handleSelectEmoji }: EmojiCate return {t('Load_more')}; } - if (item.length === 0) { - return {t('No_emojis_found')}; - } - return ( + {item.length === 0 && {t('No_emojis_found')}} {item.map(({ emoji, image, category }) => ( ))} diff --git a/packages/ui-client/src/components/EmojiPicker/EmojiPickerNotFound.tsx b/packages/ui-client/src/components/EmojiPicker/EmojiPickerNotFound.tsx index be42891fece0b..170f7793dbec3 100644 --- a/packages/ui-client/src/components/EmojiPicker/EmojiPickerNotFound.tsx +++ b/packages/ui-client/src/components/EmojiPicker/EmojiPickerNotFound.tsx @@ -2,7 +2,7 @@ import { Box } from '@rocket.chat/fuselage'; import type { AllHTMLAttributes } from 'react'; const EmojiPickerNotFound = (props: Omit, 'is' | 'style'>) => ( - + ); export default EmojiPickerNotFound; From 4c0793eb87ef4e25ec6831a0dea476750b7f9870 Mon Sep 17 00:00:00 2001 From: julio-rocketchat Date: Thu, 3 Apr 2025 15:39:24 +0200 Subject: [PATCH 058/187] chore(deps): bump `vite` (#35691) --- apps/uikit-playground/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 65f24cd2ab405..802d893a5c6bd 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -53,7 +53,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.14", "typescript": "~5.7.2", - "vite": "^6.1.2" + "vite": "^6.2.4" }, "volta": { "extends": "../../package.json" diff --git a/yarn.lock b/yarn.lock index 44624da0eec0a..3302a3369d751 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9912,7 +9912,7 @@ __metadata: react-virtuoso: "npm:^4.12.0" reactflow: "npm:^11.11.4" typescript: "npm:~5.7.2" - vite: "npm:^6.1.2" + vite: "npm:^6.2.4" languageName: unknown linkType: soft @@ -36745,9 +36745,9 @@ __metadata: languageName: node linkType: hard -"vite@npm:^6.1.2": - version: 6.2.3 - resolution: "vite@npm:6.2.3" +"vite@npm:^6.2.4": + version: 6.2.4 + resolution: "vite@npm:6.2.4" dependencies: esbuild: "npm:^0.25.0" fsevents: "npm:~2.3.3" @@ -36793,7 +36793,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10/3cd7b3a8d9a31c8eb7141004ddb1c9489afa0cdaf0ccbf41dacee121a8f13062d0fb2cee160589aaf1c534a02402aac674f1b1618876ba0bd299b55f69b2b495 + checksum: 10/3734c8695b4d35a5b3ea617159594835e370b428745f37e90d9c1daf82b53af5248578c1f1d9977fc1460320c0cdd4aef135095d378b2eba2736c03e2cfa019e languageName: node linkType: hard From 4cb4edfbab01c2c2a9c2c30785844304bf594b50 Mon Sep 17 00:00:00 2001 From: julio-rocketchat Date: Thu, 3 Apr 2025 16:04:48 +0200 Subject: [PATCH 059/187] chore(deps): bump `image-size` (#35690) --- apps/meteor/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index cdc88d01edf18..5d37c53d200aa 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -357,7 +357,7 @@ "i18next-http-backend": "^1.4.5", "i18next-sprintf-postprocessor": "^0.2.2", "iconv-lite": "^0.6.3", - "image-size": "^1.1.1", + "image-size": "^1.2.1", "imap": "^0.8.19", "ip-range-check": "^0.2.0", "is-svg": "^5.1.0", diff --git a/yarn.lock b/yarn.lock index 3302a3369d751..c51bd2b6a25aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8862,7 +8862,7 @@ __metadata: i18next-http-backend: "npm:^1.4.5" i18next-sprintf-postprocessor: "npm:^0.2.2" iconv-lite: "npm:^0.6.3" - image-size: "npm:^1.1.1" + image-size: "npm:^1.2.1" imap: "npm:^0.8.19" ip-range-check: "npm:^0.2.0" is-svg: "npm:^5.1.0" @@ -23275,14 +23275,14 @@ __metadata: languageName: node linkType: hard -"image-size@npm:^1.1.1": - version: 1.1.1 - resolution: "image-size@npm:1.1.1" +"image-size@npm:^1.2.1": + version: 1.2.1 + resolution: "image-size@npm:1.2.1" dependencies: queue: "npm:6.0.2" bin: image-size: bin/image-size.js - checksum: 10/f28966dd3f6d4feccc4028400bb7e8047c28b073ab0aa90c7c53039288139dd416c6bc254a976d4bf61113d4bc84871786804113099701cbfe9ccf377effdb54 + checksum: 10/b290c6cc5635565b1da51991472eb6522808430dbe3415823649723dc5f5fd8263f0f98f9bdec46184274ea24fe4f3f7a297c84b647b412e14d2208703dd8a19 languageName: node linkType: hard From 66c2134eaab75c6a33caadbb4c967754aecd8225 Mon Sep 17 00:00:00 2001 From: Debdut Chakraborty Date: Thu, 3 Apr 2025 23:00:03 +0530 Subject: [PATCH 060/187] ci: fix matrix sdk native build (#35694) --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 082e54787f63b..068d65c696d14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -306,13 +306,13 @@ jobs: - if: steps.matrix-rust-sdk-crypto-nodejs.outputs.cache-hit != 'true' uses: actions/setup-node@v4 with: - node-version: '20.18.0' + node-version: '22.13.1' - if: steps.matrix-rust-sdk-crypto-nodejs.outputs.cache-hit != 'true' - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: stable - targets: x86_64-unknown-linux-musl + toolchain: '1.76' + target: x86_64-unknown-linux-musl - if: steps.matrix-rust-sdk-crypto-nodejs.outputs.cache-hit != 'true' name: Install ziglang From c51bd13dcff5f3440c6245334bf4b3f5361d384a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 3 Apr 2025 14:43:24 -0300 Subject: [PATCH 061/187] chore: improve api rest class definitions and adjust path (#35588) --- apps/meteor/app/api/server/api.ts | 14 ++++- apps/meteor/app/api/server/default/openApi.ts | 8 ++- apps/meteor/app/api/server/definition.ts | 63 ++++++++++++------- apps/meteor/app/api/server/router.ts | 2 +- 4 files changed, 58 insertions(+), 29 deletions(-) diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index 3076199c9b256..0761dec1e1b1d 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -34,6 +34,8 @@ import type { TypedAction, TypedOptions, UnauthorizedResult, + RedirectStatusCodes, + RedirectResult, } from './definition'; import { getUserInfo } from './helpers/getUserInfo'; import { parseJsonQuery } from './helpers/parseJsonQuery'; @@ -275,6 +277,13 @@ export class APIClass< return finalResult as SuccessResult; } + public redirect(code: C, result: T): RedirectResult { + return { + statusCode: code, + body: result, + }; + } + public failure(result?: T): FailureResult; public failure( @@ -620,7 +629,7 @@ export class APIClass< path: TPathPattern; } & Omit) > { - this.addRoute([subpath], { ...options, typed: true }, { [method.toLowerCase()]: { action } } as any); + this.addRoute([subpath], { tags: [], ...options, typed: true }, { [method.toLowerCase()]: { action } } as any); this.registerTypedRoutes(method, subpath, options); return this; } @@ -752,6 +761,7 @@ export class APIClass< // Note: This is required due to Restivus calling `addRoute` in the constructor of itself Object.keys(operations).forEach((method) => { const _options = { ...options }; + const { tags = ['Missing Documentation'] } = _options as Record; if (typeof operations[method as keyof Operations] === 'function') { (operations as Record)[method as string] = { @@ -903,7 +913,7 @@ export class APIClass< (operations[method as keyof Operations] as Record).logger = logger; this.router[method.toLowerCase() as 'get' | 'post' | 'put' | 'delete']( `/${route}`.replaceAll('//', '/'), - _options as TypedOptions, + { ..._options, tags } as TypedOptions, license(_options as TypedOptions, License), (operations[method as keyof Operations] as Record).action as any, ); diff --git a/apps/meteor/app/api/server/default/openApi.ts b/apps/meteor/app/api/server/default/openApi.ts index 96570e7495720..6f5b31f2625ea 100644 --- a/apps/meteor/app/api/server/default/openApi.ts +++ b/apps/meteor/app/api/server/default/openApi.ts @@ -77,7 +77,7 @@ API.default.addRoute( get() { const { withUndocumented = false } = this.queryParams; - return API.default.success(makeOpenAPIResponse(getTypedRoutes(API.v1.typedRoutes, { withUndocumented }))); + return API.default.success(makeOpenAPIResponse(getTypedRoutes(API.api.typedRoutes, { withUndocumented }))); }, }, ); @@ -85,6 +85,10 @@ API.default.addRoute( app.use( '/api-docs', swaggerUi.serve, - swaggerUi.setup(makeOpenAPIResponse(getTypedRoutes(API.v1.typedRoutes, { withUndocumented: false }))), + swaggerUi.setup(null, { + swaggerOptions: { + url: `${settings.get('Site_Url')}/api/docs/json`, + }, + }), ); WebApp.connectHandlers.use(app); diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index 18f543500e893..e07ab21446adf 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -6,8 +6,16 @@ import type { Request, Response } from 'express'; import type { ITwoFactorOptions } from '../../2fa/server/code'; -export type SuccessResult = { - statusCode: 200; +export type SuccessStatusCodes = Exclude, Range<200>>; + +export type RedirectStatusCodes = Exclude, Range<300>>; + +export type AuthorizationStatusCodes = Exclude, Range<400>>; + +export type ErrorStatusCodes = Exclude, Range<500>>; + +export type SuccessResult = { + statusCode: TStatusCode; body: T extends object ? { success: true } & T : T; }; @@ -26,6 +34,11 @@ export type FailureResult = { + statusCode: TStatusCode; + body: T; +}; + export type UnauthorizedResult = { statusCode: 401; body: { @@ -43,8 +56,8 @@ export type ForbiddenResult = { }; }; -export type InternalError = { - statusCode: 500; +export type InternalError = { + statusCode: StatusCode; body: { error: T | 'Internal server error'; success: false; @@ -236,15 +249,14 @@ export type ActionOperations as Lowercase]: ActionOperation, TPathPattern, TOptions>; }; +type Range = Result['length'] extends N + ? Result[number] + : Range; + +type HTTPStatusCodes = SuccessStatusCodes | RedirectStatusCodes | AuthorizationStatusCodes | ErrorStatusCodes; export type TypedOptions = { response: { - 200: ValidateFunction; - 300?: ValidateFunction; - 400?: ValidateFunction; - 401?: ValidateFunction; - 403?: ValidateFunction; - 404?: ValidateFunction; - 500?: ValidateFunction; + [K in HTTPStatusCodes]?: ValidateFunction; }; query?: ValidateFunction; body?: ValidateFunction; @@ -255,6 +267,7 @@ export type TypedOptions = { export type TypedThis = { userId: TOptions['authRequired'] extends true ? string : string | undefined; + user: TOptions['authRequired'] extends true ? IUser : IUser | null; token: TOptions['authRequired'] extends true ? string : string | undefined; queryParams: TOptions['query'] extends ValidateFunction ? Query : never; urlParams: UrlParams extends Record ? UrlParams : never; @@ -279,19 +292,21 @@ type PromiseOrValue = T | Promise; type InferResult = TResult extends ValidateFunction ? T : TResult; type Results = { - [K in keyof TResponse]: K extends 200 - ? SuccessResult> - : K extends 400 - ? FailureResult> - : K extends 401 - ? UnauthorizedResult> - : K extends 403 - ? ForbiddenResult> - : K extends 404 - ? NotFoundResult> - : K extends 500 - ? InternalError> - : never; + [K in keyof TResponse]: K extends SuccessStatusCodes + ? SuccessResult, K> + : K extends RedirectStatusCodes + ? RedirectResult, K> + : K extends 400 + ? FailureResult> + : K extends 401 + ? UnauthorizedResult> + : K extends 403 + ? ForbiddenResult> + : K extends 404 + ? NotFoundResult> + : K extends ErrorStatusCodes + ? InternalError, K> + : never; }[keyof TResponse] & { headers?: Record; }; diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index ef35d489254f9..cfb0104de899f 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -60,7 +60,7 @@ export class Router< constructor(readonly base: TBasePath) {} - private typedRoutes: Record> = {}; + public typedRoutes: Record> = {}; private registerTypedRoutes< TSubPathPattern extends string, From f545617c2ac3d67af533e64c2670d8d564a56d15 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 3 Apr 2025 17:07:37 -0300 Subject: [PATCH 062/187] feat: Use `IconButton` to navigate to parent room (#35613) --- .changeset/four-dragons-warn.md | 8 +++ .../client/views/room/HeaderV2/ParentRoom.tsx | 29 -------- .../ParentDiscussion/ParentDiscussion.tsx | 20 ++++++ .../ParentDiscussionRoute.tsx | 27 ++++++++ .../ParentDiscussionWithData.tsx | 16 +++++ .../ParentRoom/ParentDiscussion/index.ts | 1 + .../room/HeaderV2/ParentRoom/ParentRoom.tsx | 22 +++++++ .../HeaderV2/ParentRoom/ParentRoomButton.tsx | 14 ++++ .../HeaderV2/{ => ParentRoom}/ParentTeam.tsx | 34 ++++------ .../views/room/HeaderV2/ParentRoom/index.ts | 1 + .../room/HeaderV2/ParentRoomWithData.tsx | 27 -------- .../HeaderV2/ParentRoomWithEndpointData.tsx | 25 ------- .../client/views/room/HeaderV2/RoomHeader.tsx | 6 +- .../tests/e2e/channel-management.spec.ts | 2 +- apps/meteor/tests/e2e/feature-preview.spec.ts | 66 +++++++++++++++++-- .../tests/e2e/search-discussion.spec.ts | 11 ++-- .../tests/e2e/utils/create-target-channel.ts | 31 +++++++-- packages/i18n/src/locales/ar.i18n.json | 1 - packages/i18n/src/locales/ca.i18n.json | 1 - packages/i18n/src/locales/cs.i18n.json | 1 - packages/i18n/src/locales/da.i18n.json | 1 - packages/i18n/src/locales/de.i18n.json | 1 - packages/i18n/src/locales/en.i18n.json | 3 +- packages/i18n/src/locales/es.i18n.json | 1 - packages/i18n/src/locales/fa.i18n.json | 1 - packages/i18n/src/locales/fi.i18n.json | 1 - packages/i18n/src/locales/fr.i18n.json | 1 - packages/i18n/src/locales/hi-IN.i18n.json | 1 - packages/i18n/src/locales/hu.i18n.json | 1 - packages/i18n/src/locales/ja.i18n.json | 1 - packages/i18n/src/locales/ka-GE.i18n.json | 1 - packages/i18n/src/locales/km.i18n.json | 1 - packages/i18n/src/locales/ko.i18n.json | 1 - packages/i18n/src/locales/lt.i18n.json | 1 - packages/i18n/src/locales/nb.i18n.json | 1 - packages/i18n/src/locales/nl.i18n.json | 1 - packages/i18n/src/locales/nn.i18n.json | 1 - packages/i18n/src/locales/pl.i18n.json | 1 - packages/i18n/src/locales/pt-BR.i18n.json | 1 - packages/i18n/src/locales/pt.i18n.json | 1 - packages/i18n/src/locales/ru.i18n.json | 1 - packages/i18n/src/locales/sv.i18n.json | 1 - packages/i18n/src/locales/tr.i18n.json | 1 - packages/i18n/src/locales/uk.i18n.json | 1 - packages/i18n/src/locales/zh-TW.i18n.json | 1 - packages/i18n/src/locales/zh.i18n.json | 1 - .../src/components/Header/HeaderState.tsx | 2 +- .../src/components/HeaderV2/HeaderState.tsx | 2 +- 48 files changed, 223 insertions(+), 152 deletions(-) create mode 100644 .changeset/four-dragons-warn.md delete mode 100644 apps/meteor/client/views/room/HeaderV2/ParentRoom.tsx create mode 100644 apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussion.tsx create mode 100644 apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionRoute.tsx create mode 100644 apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionWithData.tsx create mode 100644 apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/index.ts create mode 100644 apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoom.tsx create mode 100644 apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoomButton.tsx rename apps/meteor/client/views/room/HeaderV2/{ => ParentRoom}/ParentTeam.tsx (66%) create mode 100644 apps/meteor/client/views/room/HeaderV2/ParentRoom/index.ts delete mode 100644 apps/meteor/client/views/room/HeaderV2/ParentRoomWithData.tsx delete mode 100644 apps/meteor/client/views/room/HeaderV2/ParentRoomWithEndpointData.tsx diff --git a/.changeset/four-dragons-warn.md b/.changeset/four-dragons-warn.md new file mode 100644 index 0000000000000..c8b3da4d93300 --- /dev/null +++ b/.changeset/four-dragons-warn.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/ui-client': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Replaces the parent room tag in room header in favor of a button to back to the parent room +> This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom.tsx deleted file mode 100644 index 5a3df51894de8..0000000000000 --- a/apps/meteor/client/views/room/HeaderV2/ParentRoom.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; - -import { HeaderTag, HeaderTagIcon } from '../../../components/Header'; -import { useRoomIcon } from '../../../hooks/useRoomIcon'; -import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; - -type ParentRoomProps = { - room: Pick; -}; - -const ParentRoom = ({ room }: ParentRoomProps) => { - const icon = useRoomIcon(room); - - const handleRedirect = (): void => roomCoordinator.openRouteLink(room.t, { rid: room._id, ...room }); - - return ( - (e.code === 'Space' || e.code === 'Enter') && handleRedirect()} - onClick={handleRedirect} - > - - {roomCoordinator.getRoomName(room.t, room)} - - ); -}; - -export default ParentRoom; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussion.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussion.tsx new file mode 100644 index 0000000000000..09375e137d1de --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussion.tsx @@ -0,0 +1,20 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useTranslation } from 'react-i18next'; + +import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; +import ParentRoomButton from '../ParentRoomButton'; + +type ParentDiscussionProps = { + loading?: boolean; + room: Pick; +}; + +const ParentDiscussion = ({ loading = false, room }: ParentDiscussionProps) => { + const { t } = useTranslation(); + const roomName = roomCoordinator.getRoomName(room.t, room); + const handleRedirect = (): void => roomCoordinator.openRouteLink(room.t, { rid: room._id, ...room }); + + return ; +}; + +export default ParentDiscussion; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionRoute.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionRoute.tsx new file mode 100644 index 0000000000000..5b407de6ba0c1 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionRoute.tsx @@ -0,0 +1,27 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useUserSubscription } from '@rocket.chat/ui-contexts'; + +import ParentDiscussion from './ParentDiscussion'; +import ParentDiscussionWithData from './ParentDiscussionWithData'; + +type ParentDiscussionRouteProps = { + room: Pick; +}; + +const ParentDiscussionRoute = ({ room }: ParentDiscussionRouteProps) => { + const { prid } = room; + + if (!prid) { + throw new Error('Parent room ID is missing'); + } + + const subscription = useUserSubscription(prid); + + if (subscription) { + return ; + } + + return ; +}; + +export default ParentDiscussionRoute; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionWithData.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionWithData.tsx new file mode 100644 index 0000000000000..d7b5f87944db5 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/ParentDiscussionWithData.tsx @@ -0,0 +1,16 @@ +import type { IRoom } from '@rocket.chat/core-typings'; + +import ParentDiscussion from './ParentDiscussion'; +import { useRoomInfoEndpoint } from '../../../../../hooks/useRoomInfoEndpoint'; + +const ParentDiscussionWithData = ({ rid }: { rid: IRoom['_id'] }) => { + const { data, isPending, isError } = useRoomInfoEndpoint(rid); + + if (isError || !data?.room) { + return null; + } + + return ; +}; + +export default ParentDiscussionWithData; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/index.ts b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/index.ts new file mode 100644 index 0000000000000..6d70f6998d612 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentDiscussion/index.ts @@ -0,0 +1 @@ +export { default } from './ParentDiscussionRoute'; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoom.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoom.tsx new file mode 100644 index 0000000000000..38c6cc5f86597 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoom.tsx @@ -0,0 +1,22 @@ +import type { IRoom } from '@rocket.chat/core-typings'; + +import ParentDiscussion from './ParentDiscussion'; +import ParentTeam from './ParentTeam'; + +const ParentRoom = ({ room }: { room: IRoom }) => { + const parentRoomId = Boolean(room.prid || (room.teamId && !room.teamMain)); + + if (!parentRoomId) { + return null; + } + + if (room.prid) { + return ; + } + + if (room.teamId && !room.teamMain) { + return ; + } +}; + +export default ParentRoom; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoomButton.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoomButton.tsx new file mode 100644 index 0000000000000..05ce1a34b38c8 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentRoomButton.tsx @@ -0,0 +1,14 @@ +import { IconButton, Skeleton } from '@rocket.chat/fuselage'; +import type { ComponentProps } from 'react'; + +type ParentRoomButtonProps = Omit, 'icon'> & { loading: boolean }; + +const ParentRoomButton = ({ loading, ...props }: ParentRoomButtonProps) => { + if (loading) { + return ; + } + + return ; +}; + +export default ParentRoomButton; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentTeam.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentTeam.tsx similarity index 66% rename from apps/meteor/client/views/room/HeaderV2/ParentTeam.tsx rename to apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentTeam.tsx index 475054c07a9b2..9c9460f0a4820 100644 --- a/apps/meteor/client/views/room/HeaderV2/ParentTeam.tsx +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/ParentTeam.tsx @@ -2,9 +2,10 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { TEAM_TYPE } from '@rocket.chat/core-typings'; import { useUserId, useEndpoint } from '@rocket.chat/ui-contexts'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; -import { HeaderTag, HeaderTagIcon, HeaderTagSkeleton } from '../../../components/Header'; -import { goToRoomById } from '../../../lib/utils/goToRoomById'; +import ParentRoomButton from './ParentRoomButton'; +import { goToRoomById } from '../../../../lib/utils/goToRoomById'; type APIErrorResult = { success: boolean; error: string }; @@ -13,7 +14,9 @@ type ParentTeamProps = { }; const ParentTeam = ({ room }: ParentTeamProps) => { + const { t } = useTranslation(); const { teamId } = room; + const userId = useUserId(); if (!teamId) { @@ -43,8 +46,9 @@ const ParentTeam = ({ room }: ParentTeamProps) => { queryFn: async () => userTeamsListEndpoint({ userId }), }); - const userBelongsToTeam = userTeams?.teams?.find((team) => team._id === teamId) || false; - const isTeamPublic = teamInfoData?.teamInfo.type === TEAM_TYPE.PUBLIC; + const userBelongsToTeam = Boolean(userTeams?.teams?.find((team) => team._id === teamId)) || false; + const isPublicTeam = teamInfoData?.teamInfo.type === TEAM_TYPE.PUBLIC; + const shouldDisplayTeam = isPublicTeam || userBelongsToTeam; const redirectToMainRoom = (): void => { const rid = teamInfoData?.teamInfo.roomId; @@ -52,31 +56,19 @@ const ParentTeam = ({ room }: ParentTeamProps) => { return; } - if (!(isTeamPublic || userBelongsToTeam)) { - return; - } - goToRoomById(rid); }; - if (teamInfoLoading || userTeamsLoading) { - return ; - } - - if (teamInfoError) { + if (teamInfoError || !shouldDisplayTeam) { return null; } return ( - (e.code === 'Space' || e.code === 'Enter') && redirectToMainRoom()} + - - {teamInfoData?.teamInfo.name} - + title={t('Back_to__roomName__team', { roomName: teamInfoData?.teamInfo.name })} + /> ); }; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoom/index.ts b/apps/meteor/client/views/room/HeaderV2/ParentRoom/index.ts new file mode 100644 index 0000000000000..31d381a035f27 --- /dev/null +++ b/apps/meteor/client/views/room/HeaderV2/ParentRoom/index.ts @@ -0,0 +1 @@ +export { default } from './ParentRoom'; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoomWithData.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoomWithData.tsx deleted file mode 100644 index 903994533b50a..0000000000000 --- a/apps/meteor/client/views/room/HeaderV2/ParentRoomWithData.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { useUserSubscription } from '@rocket.chat/ui-contexts'; - -import ParentRoom from './ParentRoom'; -import ParentRoomWithEndpointData from './ParentRoomWithEndpointData'; - -type ParentRoomWithDataProps = { - room: IRoom; -}; - -const ParentRoomWithData = ({ room }: ParentRoomWithDataProps) => { - const { prid } = room; - - if (!prid) { - throw new Error('Parent room ID is missing'); - } - - const subscription = useUserSubscription(prid); - - if (subscription) { - return ; - } - - return ; -}; - -export default ParentRoomWithData; diff --git a/apps/meteor/client/views/room/HeaderV2/ParentRoomWithEndpointData.tsx b/apps/meteor/client/views/room/HeaderV2/ParentRoomWithEndpointData.tsx deleted file mode 100644 index 0f8cb89bc4b78..0000000000000 --- a/apps/meteor/client/views/room/HeaderV2/ParentRoomWithEndpointData.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; - -import ParentRoom from './ParentRoom'; -import { HeaderTagSkeleton } from '../../../components/Header'; -import { useRoomInfoEndpoint } from '../../../hooks/useRoomInfoEndpoint'; - -type ParentRoomWithEndpointDataProps = { - rid: IRoom['_id']; -}; - -const ParentRoomWithEndpointData = ({ rid }: ParentRoomWithEndpointDataProps) => { - const { data, isPending, isError } = useRoomInfoEndpoint(rid); - - if (isPending) { - return ; - } - - if (isError || !data?.room) { - return null; - } - - return ; -}; - -export default ParentRoomWithEndpointData; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx index 6e138fca2a9c7..4f3100668afed 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomHeader.tsx @@ -5,8 +5,7 @@ import { Suspense } from 'react'; import { useTranslation } from 'react-i18next'; import FederatedRoomOriginServer from './FederatedRoomOriginServer'; -import ParentRoomWithData from './ParentRoomWithData'; -import ParentTeam from './ParentTeam'; +import ParentRoom from './ParentRoom'; import RoomTitle from './RoomTitle'; import RoomToolbox from './RoomToolbox'; import RoomTopic from './RoomTopic'; @@ -38,13 +37,12 @@ const RoomHeader = ({ room, slots = {}, roomToolbox }: RoomHeaderProps) => { return (
{slots?.start} + {slots?.preContent} - {room.prid && } - {room.teamId && !room.teamMain && } {isRoomFederated(room) && } diff --git a/apps/meteor/tests/e2e/channel-management.spec.ts b/apps/meteor/tests/e2e/channel-management.spec.ts index f8ff93723c69f..a15320c25aff4 100644 --- a/apps/meteor/tests/e2e/channel-management.spec.ts +++ b/apps/meteor/tests/e2e/channel-management.spec.ts @@ -134,7 +134,7 @@ test.describe.serial('channel-management', () => { targetChannel = hugeName; await page.setViewportSize({ width: 640, height: 460 }); - await expect(page.getByRole('heading', { name: hugeName })).toHaveCSS('width', '411px'); + await expect(page.getByRole('heading', { name: hugeName })).toHaveCSS('width', '407px'); }); test('should open sidebar clicking on sidebar toggler', async ({ page }) => { diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index 55f51ea62b170..2b3cc13b5a100 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -1,8 +1,18 @@ import { faker } from '@faker-js/faker'; +import type { Page } from '@playwright/test'; import { Users } from './fixtures/userStates'; import { AccountProfile, HomeChannel } from './page-objects'; -import { createTargetChannel, createTargetTeam, deleteChannel, deleteTeam, setSettingValueById } from './utils'; +import { + createTargetChannel, + createTargetTeam, + deleteChannel, + deleteTeam, + setSettingValueById, + createTargetDiscussion, + createChannelWithTeam, + deleteRoom, +} from './utils'; import { setUserPreferences } from './utils/setUserPreferences'; import { test, expect } from './utils/test'; @@ -12,17 +22,20 @@ test.describe.serial('feature preview', () => { let poHomeChannel: HomeChannel; let poAccountProfile: AccountProfile; let targetChannel: string; + let targetDiscussion: Record; let sidepanelTeam: string; const targetChannelNameInTeam = `channel-from-team-${faker.number.int()}`; test.beforeAll(async ({ api }) => { await setSettingValueById(api, 'Accounts_AllowFeaturePreview', true); targetChannel = await createTargetChannel(api, { members: ['user1'] }); + targetDiscussion = await createTargetDiscussion(api); }); test.afterAll(async ({ api }) => { await setSettingValueById(api, 'Accounts_AllowFeaturePreview', false); await deleteChannel(api, targetChannel); + await deleteRoom(api, targetDiscussion._id); }); test.beforeEach(async ({ page }) => { @@ -172,11 +185,56 @@ test.describe.serial('feature preview', () => { await expect(page.locator('role=navigation[name="header"]')).not.toBeVisible(); }); - test('should not display avatar in room header', async ({ page }) => { + test('should display the room header properly', async ({ page }) => { await page.goto('/home'); + await poHomeChannel.sidebar.openChat(targetDiscussion.fname); - await poHomeChannel.sidebar.openChat(targetChannel); - await expect(page.locator('main').locator('header').getByRole('figure')).not.toBeVisible(); + await test.step('should not display avatar in room header', async () => { + await expect(page.locator('main').locator('header').getByRole('figure')).not.toBeVisible(); + }); + + await test.step('should display the back button in the room header when accessing a room with parent', async () => { + await expect( + page + .locator('main') + .locator('header') + .getByRole('button', { name: /Back to/ }), + ).toBeVisible(); + }); + }); + + test.describe('user is not part of the team', () => { + let targetTeam: string; + let targetChannelWithTeam: string; + let user1Page: Page; + + test.beforeAll(async ({ api, browser }) => { + await setSettingValueById(api, 'Accounts_Default_User_Preferences_featuresPreview', [{ name: 'newNavigation', value: true }]); + + const { channelName, teamName } = await createChannelWithTeam(api); + targetTeam = teamName; + targetChannelWithTeam = channelName; + user1Page = await browser.newPage({ storageState: Users.user1.state }); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Accounts_Default_User_Preferences_featuresPreview', []); + + await deleteChannel(api, targetChannelWithTeam); + await deleteTeam(api, targetTeam); + await user1Page.close(); + }); + + test('should not display back to team button in the room header', async ({ page }) => { + await user1Page.goto(`/channel/${targetChannelWithTeam}`); + + await expect( + page + .locator('main') + .locator('header') + .getByRole('button', { name: /Back to/ }), + ).not.toBeVisible(); + }); }); }); diff --git a/apps/meteor/tests/e2e/search-discussion.spec.ts b/apps/meteor/tests/e2e/search-discussion.spec.ts index 0d645432d777e..a2ec674805296 100644 --- a/apps/meteor/tests/e2e/search-discussion.spec.ts +++ b/apps/meteor/tests/e2e/search-discussion.spec.ts @@ -2,7 +2,7 @@ import type { Page } from '@playwright/test'; import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects'; -import { createTargetDiscussion } from './utils'; +import { createTargetDiscussion, deleteRoom } from './utils'; import { getSettingValueById } from './utils/getSettingValueById'; import { setSettingValueById } from './utils/setSettingValueById'; import { test, expect } from './utils/test'; @@ -12,26 +12,27 @@ test.use({ storageState: Users.user1.state }); test.describe.serial('search-discussion', () => { let settingDefaultValue: unknown; let poHomeChannel: HomeChannel; - let discussionName: string; + let discussion: Record; test.beforeAll(async ({ api }) => { settingDefaultValue = await getSettingValueById(api, 'UI_Allow_room_names_with_special_chars'); }); test.beforeEach(async ({ page, api }) => { - discussionName = await createTargetDiscussion(api); + discussion = await createTargetDiscussion(api); poHomeChannel = new HomeChannel(page); await page.goto('/home'); }); test.afterAll(async ({ api }) => { await setSettingValueById(api, 'UI_Allow_room_names_with_special_chars', settingDefaultValue); + await deleteRoom(api, discussion._id); }); const testDiscussionSearch = async (page: Page) => { await poHomeChannel.sidenav.openSearch(); - await poHomeChannel.sidenav.inputSearch.type(discussionName); - const targetSearchItem = page.locator('role=listbox').getByText(discussionName).first(); + await poHomeChannel.sidenav.inputSearch.fill(discussion.fname); + const targetSearchItem = page.locator('role=listbox').getByText(discussion.fname).first(); await expect(targetSearchItem).toBeVisible(); }; diff --git a/apps/meteor/tests/e2e/utils/create-target-channel.ts b/apps/meteor/tests/e2e/utils/create-target-channel.ts index 1bccd514fb5bd..402d45a2731ba 100644 --- a/apps/meteor/tests/e2e/utils/create-target-channel.ts +++ b/apps/meteor/tests/e2e/utils/create-target-channel.ts @@ -45,6 +45,10 @@ export async function deleteChannel(api: BaseTest['api'], roomName: string): Pro await api.post('/channels.delete', { roomName }); } +export async function deleteRoom(api: BaseTest['api'], roomId: string): Promise { + await api.post('/rooms.delete', { roomId }); +} + export async function createTargetPrivateChannel(api: BaseTest['api'], options?: Omit): Promise { const name = faker.string.uuid(); await api.post('/groups.create', { name, ...options }); @@ -72,13 +76,30 @@ export async function createDirectMessage(api: BaseTest['api']): Promise { }); } -export async function createTargetDiscussion(api: BaseTest['api']): Promise { +export async function createTargetDiscussion(api: BaseTest['api']): Promise> { const channelName = faker.string.uuid(); const discussionName = faker.string.uuid(); - const response = await api.post('/channels.create', { name: channelName }); - const { channel } = await response.json(); - await api.post('/rooms.createDiscussion', { t_name: discussionName, prid: channel._id }); + const channelResponse = await api.post('/channels.create', { name: channelName }); + const { channel } = await channelResponse.json(); + const discussionResponse = await api.post('/rooms.createDiscussion', { t_name: discussionName, prid: channel._id }); + const { discussion } = await discussionResponse.json(); + + if (!discussion) { + throw new Error('Discussion not created'); + } + + return discussion; +} + +export async function createChannelWithTeam(api: BaseTest['api']): Promise> { + const channelName = faker.string.uuid(); + const teamName = faker.string.uuid(); + + const teamResponse = await api.post('/teams.create', { name: teamName, type: 1, members: ['user2'] }); + const { team } = await teamResponse.json(); + + await api.post('/channels.create', { name: channelName, members: ['user1'], extraData: { teamId: team._id } }); - return discussionName; + return { channelName, teamName }; } diff --git a/packages/i18n/src/locales/ar.i18n.json b/packages/i18n/src/locales/ar.i18n.json index 87085e65a121c..a2cbd69e5fccb 100644 --- a/packages/i18n/src/locales/ar.i18n.json +++ b/packages/i18n/src/locales/ar.i18n.json @@ -599,7 +599,6 @@ "Back_to_integrations": "عودة إلى عمليات التكامل", "Back_to_login": "عودة إلى تسجيل الدخول", "Back_to_permissions": "عودة إلى الأذونات", - "Back_to_room": "عودة إلى Room", "Back_to_threads": "عودة إلى المواضيع", "Backup_codes": "أكواد التخزين الاحتياطي", "Belongs_To": "ينتمي إلى", diff --git a/packages/i18n/src/locales/ca.i18n.json b/packages/i18n/src/locales/ca.i18n.json index 09b5280aedc8c..d124edade00fe 100644 --- a/packages/i18n/src/locales/ca.i18n.json +++ b/packages/i18n/src/locales/ca.i18n.json @@ -596,7 +596,6 @@ "Back_to_integrations": "Torna a les integracions", "Back_to_login": "Torna a identificar-me", "Back_to_permissions": "Torna a permisos", - "Back_to_room": "Tornar a Room", "Back_to_threads": "Torna als fils", "Backup_codes": "Codis de recuperació", "Belongs_To": "Pertany a", diff --git a/packages/i18n/src/locales/cs.i18n.json b/packages/i18n/src/locales/cs.i18n.json index 6fb4e275806c9..3b25312eb52e6 100644 --- a/packages/i18n/src/locales/cs.i18n.json +++ b/packages/i18n/src/locales/cs.i18n.json @@ -501,7 +501,6 @@ "Back_to_integrations": "Zpět k integracím", "Back_to_login": "Zpět na přihlašovací formulář", "Back_to_permissions": "Zpět na práva", - "Back_to_room": "Zpět do místnosti", "Backup_codes": "Záložní kódy", "Best_first_response_time": "Nejlepší doba první reakce", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "Beta funkcionalita. Videohovory musí být povoleny.", diff --git a/packages/i18n/src/locales/da.i18n.json b/packages/i18n/src/locales/da.i18n.json index 48ef392565dea..aa8a1e97586e3 100644 --- a/packages/i18n/src/locales/da.i18n.json +++ b/packages/i18n/src/locales/da.i18n.json @@ -648,7 +648,6 @@ "Back_to_imports": "Tilbage til import", "Cancel": "Annullér", "Cancel_message_input": "Annullér", - "Back_to_room": "Tilbage til rum", "Canceled": "Annulleret", "Cannot_invite_users_to_direct_rooms": "Kan ikke invitere brugere til at direkte rum", "Cannot_open_conversation_with_yourself": "Kan ikke oprette direkte besked med dig selv", diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 73dedb00609ae..0508b1b86ae49 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -747,7 +747,6 @@ "Back_to_imports": "Zurück zu den Importen", "Cancel": "Abbrechen", "Cancel_message_input": "Abbrechen", - "Back_to_room": "Zurück zu Room", "Canceled": "Abgebrochen", "Back_to_threads": "Zurück zu den Threads", "BBB_End_Meeting": "Meeting beenden", diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 5996179d73368..1a7ecd479baeb 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -766,6 +766,8 @@ "Back_to_integrations": "Back to integrations", "Back_to_login": "Back to login", "Back_to_Manage_Apps": "Back to Manage Apps", + "Back_to__roomName__channel": "Back to {{roomName}} channel", + "Back_to__roomName__team": "Back to {{roomName}} team", "Back_to_permissions": "Back to permissions", "are_playing": "are playing", "is_playing": "is playing", @@ -860,7 +862,6 @@ "Back_to_imports": "Back to imports", "Cancel": "Cancel", "Cancel_message_input": "Cancel", - "Back_to_room": "Back to Room", "Canceled": "Canceled", "Back_to_threads": "Back to threads", "BBB_End_Meeting": "End Meeting", diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json index 97d0deb19c614..8871569d02be1 100644 --- a/packages/i18n/src/locales/es.i18n.json +++ b/packages/i18n/src/locales/es.i18n.json @@ -604,7 +604,6 @@ "Back_to_integrations": "Volver a las integraciones", "Back_to_login": "Volver al inicio de sesión", "Back_to_permissions": "Volver a los permisos", - "Back_to_room": "Volver a Room", "Back_to_threads": "Volver a los hilos", "Backup_codes": "Códigos de respaldo", "Belongs_To": "Pertenece a", diff --git a/packages/i18n/src/locales/fa.i18n.json b/packages/i18n/src/locales/fa.i18n.json index 31043533336f6..108bd40bac232 100644 --- a/packages/i18n/src/locales/fa.i18n.json +++ b/packages/i18n/src/locales/fa.i18n.json @@ -442,7 +442,6 @@ "Back_to_integrations": "بازگشت به یکپارچگی ها", "Back_to_login": "بازگشت به صفحه ورود", "Back_to_permissions": "بازگشت به مجوزها", - "Back_to_room": "بازگشت به اتاق", "Backup_codes": "کد پشتیبان", "Best_first_response_time": "بهترین زمان پاسخ اول", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "ویژگی بتا وابسته به کنفرانس ویدیویی فعال می شود", diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index f894c89d6ce7d..9131ab922c6aa 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -759,7 +759,6 @@ "Back_to_imports": "Takaisin tuotuihin", "Cancel": "Peruuta", "Cancel_message_input": "Peruuta", - "Back_to_room": "Takaisin huoneeseen", "Canceled": "Peruutettu", "Back_to_threads": "Takaisin viestiketjuihin", "BBB_End_Meeting": "Lopeta kokous", diff --git a/packages/i18n/src/locales/fr.i18n.json b/packages/i18n/src/locales/fr.i18n.json index b65fe8a7f51ce..c766001d6f275 100644 --- a/packages/i18n/src/locales/fr.i18n.json +++ b/packages/i18n/src/locales/fr.i18n.json @@ -598,7 +598,6 @@ "Back_to_integrations": "Retour aux intégrations", "Back_to_login": "Retour à l'écran de connexion", "Back_to_permissions": "Retour aux autorisations", - "Back_to_room": "Retour au salon", "Back_to_threads": "Retour aux fils", "Backup_codes": "Codes de sauvegarde", "Be_the_first_to_join": "Soyez le premier à rejoindre", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 9e54a7ed8bfbc..e598395d668da 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -785,7 +785,6 @@ "Back_to_imports": "आयात पर वापस जाएँ", "Cancel": "रद्द करना", "Cancel_message_input": "रद्द करना", - "Back_to_room": "कक्ष में वापस", "Canceled": "रद्द", "Back_to_threads": "धागों पर वापस जाएँ", "BBB_End_Meeting": "बैठक समाप्त", diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index 565c86795297a..79979192797ca 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -730,7 +730,6 @@ "Back_to_imports": "Vissza az importálásokhoz", "Cancel": "Mégse", "Cancel_message_input": "Mégse", - "Back_to_room": "Vissza a szobához", "Canceled": "Megszakítva", "Back_to_threads": "Vissza a szálakhoz", "BBB_End_Meeting": "Értekezlet befejezése", diff --git a/packages/i18n/src/locales/ja.i18n.json b/packages/i18n/src/locales/ja.i18n.json index d43aff049dfc4..32b2aacab0c04 100644 --- a/packages/i18n/src/locales/ja.i18n.json +++ b/packages/i18n/src/locales/ja.i18n.json @@ -668,7 +668,6 @@ "Back_to_imports": "インポートに戻る", "Cancel": "キャンセル", "Cancel_message_input": "キャンセル", - "Back_to_room": "Roomに戻る", "Canceled": "キャンセルしました", "Back_to_threads": "スレッドに戻る", "BBB_End_Meeting": "ミーティングの終了", diff --git a/packages/i18n/src/locales/ka-GE.i18n.json b/packages/i18n/src/locales/ka-GE.i18n.json index 0168d948a9c7a..cba7e67a66301 100644 --- a/packages/i18n/src/locales/ka-GE.i18n.json +++ b/packages/i18n/src/locales/ka-GE.i18n.json @@ -541,7 +541,6 @@ "Back_to_imports": "იმპორტში დაბრუნება", "Cancel": "გაუქმება", "Cancel_message_input": "გაუქმება", - "Back_to_room": "დაბრუნება Room -ში", "Canceled": "გაუქმდა", "Cannot_invite_users_to_direct_rooms": "მომხმარებლების პირდაპირ ოთახში მოწვევა შეუძლებელია", "Cannot_open_conversation_with_yourself": "თქვენ ვერ შეძლებთ პირდაპირ მესიჯის გაგზავნას საკუთარ თავთან", diff --git a/packages/i18n/src/locales/km.i18n.json b/packages/i18n/src/locales/km.i18n.json index fb697b3f6b14e..5c0b6fe60c8a4 100644 --- a/packages/i18n/src/locales/km.i18n.json +++ b/packages/i18n/src/locales/km.i18n.json @@ -513,7 +513,6 @@ "Avg_of_waiting_time": "ជាមធ្យមនៃការរង់ចាំពេលវេលា", "Cancel": "បញ្ឈប់", "Cancel_message_input": "បញ្ឈប់", - "Back_to_room": "ត្រលប់ទៅ Room។", "Canceled": "បានបោះបង់", "Cannot_invite_users_to_direct_rooms": "មិនអាចអញ្ជើញអ្នកប្រើប្រាស់ដើម្បីដឹកនាំបន្ទប់", "Cannot_open_conversation_with_yourself": "មិនអាចបញ្ជូនសារផ្ទាល់ជាមួយខ្លួនអ្នកបានទេ", diff --git a/packages/i18n/src/locales/ko.i18n.json b/packages/i18n/src/locales/ko.i18n.json index b85356990e4f9..c450217d5d7c3 100644 --- a/packages/i18n/src/locales/ko.i18n.json +++ b/packages/i18n/src/locales/ko.i18n.json @@ -542,7 +542,6 @@ "Back_to_integrations": "Integrations 로 돌아가기", "Back_to_login": "로그인으로 돌아가기", "Back_to_permissions": "권한 으로 돌아가기", - "Back_to_room": "Room으로 돌아가기", "Backup_codes": "백업 코드", "Best_first_response_time": "최상의 첫번째 응답 시간", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "베타 기능입니다. 화상 회의 설정을 따릅니다.", diff --git a/packages/i18n/src/locales/lt.i18n.json b/packages/i18n/src/locales/lt.i18n.json index fc5ae02a45f46..bb14b50807d27 100644 --- a/packages/i18n/src/locales/lt.i18n.json +++ b/packages/i18n/src/locales/lt.i18n.json @@ -457,7 +457,6 @@ "Back_to_imports": "Atgal į importus", "Cancel": "Atšaukti", "Cancel_message_input": "Atšaukti", - "Back_to_room": "Atgal į Room", "Back_to_threads": "Atgal į temas", "Cannot_invite_users_to_direct_rooms": "Negalima pakviesti naudotojų nukreipti kambarius", "Cannot_open_conversation_with_yourself": "Negalima tiesioginio pranešimo su savimi", diff --git a/packages/i18n/src/locales/nb.i18n.json b/packages/i18n/src/locales/nb.i18n.json index faf3c144cd7f0..6c425ba6327c9 100644 --- a/packages/i18n/src/locales/nb.i18n.json +++ b/packages/i18n/src/locales/nb.i18n.json @@ -859,7 +859,6 @@ "Back_to_imports": "Tilbake til import", "Cancel": "Avbryt", "Cancel_message_input": "Avbryt", - "Back_to_room": "Tilbake til rommet", "Canceled": "Avbrutt", "Back_to_threads": "Tilbake til tråder", "BBB_End_Meeting": "Avslutt møte", diff --git a/packages/i18n/src/locales/nl.i18n.json b/packages/i18n/src/locales/nl.i18n.json index 0ec8f860f5897..d357b71c6c176 100644 --- a/packages/i18n/src/locales/nl.i18n.json +++ b/packages/i18n/src/locales/nl.i18n.json @@ -672,7 +672,6 @@ "Back_to_imports": "Terug naar imports", "Cancel": "Annuleren", "Cancel_message_input": "Annuleren", - "Back_to_room": "Terug naar kamer", "Canceled": "Geannuleerd", "Back_to_threads": "Terug naar discussies", "BBB_End_Meeting": "Vergadering beëindigen", diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index 7df183ba1af03..1e73ad4e738d2 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -859,7 +859,6 @@ "Back_to_imports": "Tilbake til import", "Cancel": "Avbryt", "Cancel_message_input": "Avbryt", - "Back_to_room": "Tilbake til Room", "Canceled": "Avbrutt", "Back_to_threads": "Tilbake til tråder", "BBB_End_Meeting": "Avslutt møte", diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index 4ab76d056fadc..892961b05ce78 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -648,7 +648,6 @@ "Back_to_integrations": "Wróć do integracji", "Back_to_login": "Wróć do strony logowania", "Back_to_permissions": "Wróć do uprawnień", - "Back_to_room": "Wróć do pokoju Room", "Back_to_threads": "Wróć do wątków", "Backup_codes": "Kody zapasowe", "Be_the_first_to_join": "Bądź pierwszym, który dołączy", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index e22554721adfd..fed94c6851d0f 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -631,7 +631,6 @@ "Back_to_integrations": "Voltar para integrações", "Back_to_login": "Voltar para o login", "Back_to_permissions": "Voltar para permissões", - "Back_to_room": "Voltar para Sala", "Back_to_threads": "Voltar para tópicos", "Backup_codes": "Códigos de backup", "Belongs_To": "Pertence a", diff --git a/packages/i18n/src/locales/pt.i18n.json b/packages/i18n/src/locales/pt.i18n.json index 567fcffa92e3b..f3df42816fd07 100644 --- a/packages/i18n/src/locales/pt.i18n.json +++ b/packages/i18n/src/locales/pt.i18n.json @@ -502,7 +502,6 @@ "call-management": "Gestão de Chamadas", "Cancel": "Cancelar", "Cancel_message_input": "Cancelar", - "Back_to_room": "Regressar ao canal", "Canceled": "Cancelado", "Cannot_invite_users_to_direct_rooms": "Não é possível convidar pessoas para salas diretas", "Cannot_open_conversation_with_yourself": "Não é possível fazer uma mensagem directa com a mesma origem", diff --git a/packages/i18n/src/locales/ru.i18n.json b/packages/i18n/src/locales/ru.i18n.json index 5ca0dff552226..b7a672c116961 100644 --- a/packages/i18n/src/locales/ru.i18n.json +++ b/packages/i18n/src/locales/ru.i18n.json @@ -656,7 +656,6 @@ "Back_to_integrations": "Назад", "Back_to_login": "На страницу авторизации", "Back_to_permissions": "Назад к настройкам прав", - "Back_to_room": "Вернуться в Room", "Back_to_threads": "Назад к тредам", "Backup_codes": "Коды резервного копирования", "Be_the_first_to_join": "Будьте первым, кто присоединится", diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index 343ce66c002e7..6af66935cf7c9 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -759,7 +759,6 @@ "Back_to_imports": "Tillbaka till importer", "Cancel": "Avbryt", "Cancel_message_input": "Avbryt", - "Back_to_room": "Tillbaka till Room", "Canceled": "Avbröt", "Back_to_threads": "Tillbaka till trådar", "BBB_End_Meeting": "Avsluta mötet", diff --git a/packages/i18n/src/locales/tr.i18n.json b/packages/i18n/src/locales/tr.i18n.json index 09deddb99da6a..f9b10baae432a 100644 --- a/packages/i18n/src/locales/tr.i18n.json +++ b/packages/i18n/src/locales/tr.i18n.json @@ -490,7 +490,6 @@ "call-management": "Arama Yönetimi", "Cancel": "İptal et", "Cancel_message_input": "İptal et", - "Back_to_room": "Odaya geri dön", "Canceled": "İptal edildi", "Cannot_invite_users_to_direct_rooms": "Odalar doğrudan kullanıcıları davet edemezsiniz", "Cannot_open_conversation_with_yourself": "Kendinize doğrudan ileti gönderemezsiniz", diff --git a/packages/i18n/src/locales/uk.i18n.json b/packages/i18n/src/locales/uk.i18n.json index 13ee35b59623f..a028e9a464c06 100644 --- a/packages/i18n/src/locales/uk.i18n.json +++ b/packages/i18n/src/locales/uk.i18n.json @@ -474,7 +474,6 @@ "Back_to_integrations": "Повернутися до інтеграцій", "Back_to_login": "Повернутися до сторінки входу", "Back_to_permissions": "Повернутися до дозволів", - "Back_to_room": "Повернутися до кімнати", "Backup_codes": "Коди резервного копіювання", "Best_first_response_time": "Кращий час першої відповіді", "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "Бета-функція. Залежить від включення відеоконференції.", diff --git a/packages/i18n/src/locales/zh-TW.i18n.json b/packages/i18n/src/locales/zh-TW.i18n.json index 0c6715e09a320..49dea764283fb 100644 --- a/packages/i18n/src/locales/zh-TW.i18n.json +++ b/packages/i18n/src/locales/zh-TW.i18n.json @@ -667,7 +667,6 @@ "Back_to_imports": "回到匯入", "Cancel": "取消", "Cancel_message_input": "取消", - "Back_to_room": "返回 Room", "Canceled": "已取消", "Back_to_threads": "返回主題", "BBB_End_Meeting": "結束會議", diff --git a/packages/i18n/src/locales/zh.i18n.json b/packages/i18n/src/locales/zh.i18n.json index ebffcf0228b7d..336ced25244a9 100644 --- a/packages/i18n/src/locales/zh.i18n.json +++ b/packages/i18n/src/locales/zh.i18n.json @@ -611,7 +611,6 @@ "Back_to_imports": "返回导入", "Cancel": "取消", "Cancel_message_input": "取消", - "Back_to_room": "回到房间", "Canceled": "已取消", "Cannot_invite_users_to_direct_rooms": "不能邀请用户加入私聊", "Cannot_open_conversation_with_yourself": "不能和你自己私聊", diff --git a/packages/ui-client/src/components/Header/HeaderState.tsx b/packages/ui-client/src/components/Header/HeaderState.tsx index 5d64f380279c3..3a21eb949550e 100644 --- a/packages/ui-client/src/components/Header/HeaderState.tsx +++ b/packages/ui-client/src/components/Header/HeaderState.tsx @@ -12,6 +12,6 @@ type HeaderStateProps = }); const HeaderState: FC = (props) => - props.onClick ? : ; + props.onClick ? : ; export default HeaderState; diff --git a/packages/ui-client/src/components/HeaderV2/HeaderState.tsx b/packages/ui-client/src/components/HeaderV2/HeaderState.tsx index 360f3f67715e8..7c7f64fe86cbb 100644 --- a/packages/ui-client/src/components/HeaderV2/HeaderState.tsx +++ b/packages/ui-client/src/components/HeaderV2/HeaderState.tsx @@ -12,6 +12,6 @@ type HeaderStateProps = }); const HeaderState = (props: HeaderStateProps) => - props.onClick ? : ; + props.onClick ? : ; export default HeaderState; From d8a74f4bcf94c4fc3079ed405abb4795e3d0c37a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 3 Apr 2025 17:45:16 -0300 Subject: [PATCH 063/187] chore: bump node types to 22 (#35697) --- apps/meteor/app/ecdh/Session.ts | 4 +- apps/meteor/app/file/server/file.server.ts | 9 ++-- .../webdav/server/lib/uploadFileToWebdav.ts | 2 +- apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- .../lib/dataExport/processDataDownloads.ts | 2 +- ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/package.json | 2 +- ee/packages/network-broker/package.json | 2 +- ee/packages/omnichannel-services/package.json | 2 +- ee/packages/presence/package.json | 2 +- packages/apps-engine/package.json | 2 +- packages/message-parser/package.json | 2 +- packages/peggy-loader/package.json | 2 +- packages/release-action/package.json | 2 +- packages/release-changelog/package.json | 2 +- yarn.lock | 51 +++++++++++-------- 22 files changed, 57 insertions(+), 45 deletions(-) diff --git a/apps/meteor/app/ecdh/Session.ts b/apps/meteor/app/ecdh/Session.ts index e530d9cdb1294..47728667b9071 100644 --- a/apps/meteor/app/ecdh/Session.ts +++ b/apps/meteor/app/ecdh/Session.ts @@ -41,7 +41,9 @@ export class Session { const sodium = await this.sodium(); const nonce = await sodium.randombytes_buf(24); - const ciphertext = await sodium.crypto_secretbox(Buffer.from(plaintext).toString(this.stringFormatRawData), nonce, this.encryptKey); + const buffer = Buffer.isBuffer(plaintext) ? plaintext : Buffer.from(plaintext); + + const ciphertext = await sodium.crypto_secretbox(Buffer.from(buffer).toString(this.stringFormatRawData), nonce, this.encryptKey); return Buffer.concat([nonce, ciphertext]); } diff --git a/apps/meteor/app/file/server/file.server.ts b/apps/meteor/app/file/server/file.server.ts index 94163e314145b..0af01c1bfdab7 100644 --- a/apps/meteor/app/file/server/file.server.ts +++ b/apps/meteor/app/file/server/file.server.ts @@ -183,8 +183,11 @@ class FileSystem implements IRocketChatFileStore { } return new Promise((resolve) => { const data: Buffer[] = []; - file.readStream.on('data', (chunk: Buffer) => { - return data.push(chunk); + file.readStream.on('data', (chunk) => { + if (Buffer.isBuffer(chunk)) { + return data.push(chunk); + } + return data.push(Buffer.from(chunk)); }); file.readStream.on('end', () => { resolve({ @@ -210,7 +213,7 @@ export const RocketChatFile = { }, dataURIParse(dataURI: string | Buffer) { - const imageData = Buffer.from(dataURI).toString().split(';base64,'); + const imageData = (Buffer.isBuffer(dataURI) ? dataURI : Buffer.from(dataURI)).toString().split(';base64,'); return { image: imageData[1], contentType: imageData[0].replace('data:', ''), diff --git a/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts b/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts index 7b4c7a0951481..e7dc0df2cc632 100644 --- a/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts +++ b/apps/meteor/app/webdav/server/lib/uploadFileToWebdav.ts @@ -11,7 +11,7 @@ export const uploadFileToWebdav = async (accountId: IWebdavAccount['_id'], fileD } const uploadFolder = 'Rocket.Chat Uploads/'; - const buffer = Buffer.from(fileData); + const buffer = Buffer.isBuffer(fileData) ? fileData : Buffer.from(fileData); const cred = getWebdavCredentials(account); const client = new WebdavClientAdapter(account.serverURL, cred); diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 2532b552096a8..d1f03cc39117f 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -54,7 +54,7 @@ "@types/ejson": "^2.2.2", "@types/express": "^4.17.21", "@types/fibers": "^3.1.4", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@types/ws": "^8.5.13", "npm-run-all": "^4.1.5", "pino-pretty": "^7.6.1", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 5d37c53d200aa..e3ab260b6dbff 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -129,7 +129,7 @@ "@types/meteor-collection-hooks": "^0.8.9", "@types/mkdirp": "^1.0.2", "@types/mocha": "github:whitecolor/mocha-types", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@types/node-rsa": "^1.1.4", "@types/nodemailer": "^6.4.16", "@types/oauth2-server": "^3.0.18", diff --git a/apps/meteor/server/lib/dataExport/processDataDownloads.ts b/apps/meteor/server/lib/dataExport/processDataDownloads.ts index 0075d33c67bf7..1042e74b0422c 100644 --- a/apps/meteor/server/lib/dataExport/processDataDownloads.ts +++ b/apps/meteor/server/lib/dataExport/processDataDownloads.ts @@ -71,7 +71,7 @@ const generateUserFile = async (exportOperation: IExportOperation, userData?: IU return; } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const stream = createWriteStream(fileName, { encoding: 'utf8' }); stream.on('finish', resolve); diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index 26242ed802036..bba769b4784ec 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -26,7 +26,7 @@ "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tools": "workspace:^", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "bcrypt": "^5.1.1", "ejson": "^2.2.3", "event-loop-stats": "^1.4.1", diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index d45ffd84a1251..f0830a413abaa 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -25,7 +25,7 @@ "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "ejson": "^2.2.3", "event-loop-stats": "^1.4.1", "eventemitter3": "^5.0.1", diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index f718495914e05..a4ac6184a2485 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -49,7 +49,7 @@ "@rocket.chat/eslint-config": "workspace:^", "@types/ejson": "^2.2.2", "@types/gc-stats": "^1.4.3", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@types/polka": "^0.5.7", "@types/underscore": "^1.13.0", "@types/uuid": "^10.0.0", diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 3d75368e719cf..b32ac22307e0b 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -27,7 +27,7 @@ "@rocket.chat/pdf-worker": "workspace:^", "@rocket.chat/tools": "workspace:^", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "ejson": "^2.2.3", "emoji-toolkit": "^7.0.1", "event-loop-stats": "^1.4.1", diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 2d2ce6d467289..b0b03116017b6 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -25,7 +25,7 @@ "@rocket.chat/presence": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "ejson": "^2.2.3", "event-loop-stats": "^1.4.1", "eventemitter3": "^5.0.1", diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index c80e532159ef2..d37b2886d48a4 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -24,7 +24,7 @@ "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "ejson": "^2.2.3", "emoji-toolkit": "^7.0.1", "event-loop-stats": "^1.4.1", diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 043d14c8d5e2f..9055adfccb521 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -24,7 +24,7 @@ "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tracing": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "ejson": "^2.2.3", "event-loop-stats": "^1.4.1", "eventemitter3": "^5.0.1", diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index 354b1ca657f0d..798bdc747b9f5 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -6,7 +6,7 @@ "@rocket.chat/eslint-config": "workspace:^", "@types/chai": "~4.3.20", "@types/ejson": "^2.2.2", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@types/sinon": "^10.0.20", "chai": "^4.5.0", "eslint": "~8.45.0", diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index a61c70639311e..5da5a1aaa36a9 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -21,7 +21,7 @@ "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tools": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "date-fns": "^2.30.0", "ejson": "^2.2.3", "emoji-toolkit": "^7.0.1", diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 2682ef035d031..427a470068832 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -9,7 +9,7 @@ "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "babel-jest": "^29.7.0", "eslint": "~8.45.0", "jest": "~29.7.0", diff --git a/packages/apps-engine/package.json b/packages/apps-engine/package.json index c774d93f84b73..d8dfa07394c59 100644 --- a/packages/apps-engine/package.json +++ b/packages/apps-engine/package.json @@ -72,7 +72,7 @@ "@types/debug": "^4.1.12", "@types/lodash.clonedeep": "^4.5.9", "@types/nedb": "^1.8.16", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@types/semver": "^7.5.8", "@types/stack-trace": "0.0.33", "@types/uuid": "~10.0.0", diff --git a/packages/message-parser/package.json b/packages/message-parser/package.json index bcdb20fc4d209..b40f1f674a691 100644 --- a/packages/message-parser/package.json +++ b/packages/message-parser/package.json @@ -56,7 +56,7 @@ "@rocket.chat/peggy-loader": "workspace:~", "@rocket.chat/prettier-config": "~0.31.25", "@types/jest": "~29.5.14", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "@typescript-eslint/parser": "~5.58.0", "babel-loader": "~9.2.1", "eslint": "~8.45.0", diff --git a/packages/peggy-loader/package.json b/packages/peggy-loader/package.json index 18dfe84f7a8f2..a2e64f028058b 100644 --- a/packages/peggy-loader/package.json +++ b/packages/peggy-loader/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", "@rocket.chat/prettier-config": "~0.31.25", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "eslint": "~8.45.0", "npm-run-all": "^4.1.5", "peggy": "4.1.1", diff --git a/packages/release-action/package.json b/packages/release-action/package.json index 9786588982e31..4b06a2af2dde0 100644 --- a/packages/release-action/package.json +++ b/packages/release-action/package.json @@ -10,7 +10,7 @@ "main": "dist/index.js", "packageManager": "yarn@3.5.1", "devDependencies": { - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "typescript": "~5.7.2" }, "dependencies": { diff --git a/packages/release-changelog/package.json b/packages/release-changelog/package.json index 031afdc5f576f..b448bdda1ca22 100644 --- a/packages/release-changelog/package.json +++ b/packages/release-changelog/package.json @@ -10,7 +10,7 @@ "devDependencies": { "@changesets/types": "^6.0.0", "@rocket.chat/eslint-config": "workspace:^", - "@types/node": "~20.17.8", + "@types/node": "~22.14.0", "eslint": "~8.45.0", "typescript": "~5.7.2" }, diff --git a/yarn.lock b/yarn.lock index c51bd2b6a25aa..5c89d178dca06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7744,7 +7744,7 @@ __metadata: "@rocket.chat/tracing": "workspace:^" "@types/bcrypt": "npm:^5.0.2" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" bcrypt: "npm:^5.1.1" ejson: "npm:^2.2.3" @@ -7820,7 +7820,7 @@ __metadata: "@types/debug": "npm:^4.1.12" "@types/lodash.clonedeep": "npm:^4.5.9" "@types/nedb": "npm:^1.8.16" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/semver": "npm:^7.5.8" "@types/stack-trace": "npm:0.0.33" "@types/uuid": "npm:~10.0.0" @@ -7878,7 +7878,7 @@ __metadata: "@rocket.chat/string-helpers": "npm:~0.31.25" "@rocket.chat/tracing": "workspace:^" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" ejson: "npm:^2.2.3" eslint: "npm:~8.45.0" @@ -8043,7 +8043,7 @@ __metadata: "@rocket.chat/tracing": "workspace:^" "@types/ejson": "npm:^2.2.2" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" "@types/underscore": "npm:^1.13.0" "@types/uuid": "npm:^10.0.0" @@ -8580,7 +8580,7 @@ __metadata: "@rocket.chat/peggy-loader": "workspace:~" "@rocket.chat/prettier-config": "npm:~0.31.25" "@types/jest": "npm:~29.5.14" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@typescript-eslint/parser": "npm:~5.58.0" babel-loader: "npm:~9.2.1" eslint: "npm:~8.45.0" @@ -8749,7 +8749,7 @@ __metadata: "@types/meteor-collection-hooks": "npm:^0.8.9" "@types/mkdirp": "npm:^1.0.2" "@types/mocha": "github:whitecolor/mocha-types" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/node-rsa": "npm:^1.1.4" "@types/nodemailer": "npm:^6.4.16" "@types/oauth2-server": "npm:^3.0.18" @@ -9072,7 +9072,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@types/chai": "npm:~4.3.20" "@types/ejson": "npm:^2.2.2" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/sinon": "npm:^10.0.20" chai: "npm:^4.5.0" ejson: "npm:^2.2.3" @@ -9102,7 +9102,7 @@ __metadata: "@rocket.chat/string-helpers": "npm:~0.31.25" "@rocket.chat/tools": "workspace:^" "@types/jest": "npm:~29.5.14" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" date-fns: "npm:^2.30.0" ejson: "npm:^2.2.3" emoji-toolkit: "npm:^7.0.1" @@ -9136,7 +9136,7 @@ __metadata: "@rocket.chat/tools": "workspace:^" "@rocket.chat/tracing": "workspace:^" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" ejson: "npm:^2.2.3" emoji-toolkit: "npm:^7.0.1" @@ -9239,7 +9239,7 @@ __metadata: dependencies: "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/prettier-config": "npm:~0.31.25" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" eslint: "npm:~8.45.0" npm-run-all: "npm:^4.1.5" peggy: "npm:4.1.1" @@ -9278,7 +9278,7 @@ __metadata: "@rocket.chat/string-helpers": "npm:~0.31.25" "@rocket.chat/tracing": "workspace:^" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" ejson: "npm:^2.2.3" eslint: "npm:~8.45.0" @@ -9309,7 +9309,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" babel-jest: "npm:^29.7.0" eslint: "npm:~8.45.0" jest: "npm:~29.7.0" @@ -9342,7 +9342,7 @@ __metadata: "@rocket.chat/omnichannel-services": "workspace:^" "@rocket.chat/tracing": "workspace:^" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" ejson: "npm:^2.2.3" emoji-toolkit: "npm:^7.0.1" @@ -9388,7 +9388,7 @@ __metadata: "@actions/github": "npm:^6.0.0" "@octokit/plugin-throttling": "npm:^6.1.0" "@rocket.chat/eslint-config": "workspace:^" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" eslint: "npm:~8.45.0" mdast-util-to-string: "npm:2.0.0" remark-parse: "npm:9.0.0" @@ -9405,7 +9405,7 @@ __metadata: dependencies: "@changesets/types": "npm:^6.0.0" "@rocket.chat/eslint-config": "workspace:^" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" dataloader: "npm:^2.2.2" eslint: "npm:~8.45.0" node-fetch: "npm:^2.7.0" @@ -9490,7 +9490,7 @@ __metadata: "@rocket.chat/tracing": "workspace:^" "@types/bcrypt": "npm:^5.0.2" "@types/gc-stats": "npm:^1.4.3" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/polka": "npm:^0.5.7" ejson: "npm:^2.2.3" eslint: "npm:~8.45.0" @@ -12587,12 +12587,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:~20.17.8": - version: 20.17.8 - resolution: "@types/node@npm:20.17.8" +"@types/node@npm:~22.14.0": + version: 22.14.0 + resolution: "@types/node@npm:22.14.0" dependencies: - undici-types: "npm:~6.19.2" - checksum: 10/e3e968b327abc70fd437a223f8950dd4436047e954aa7db09abde5df1f58a5c49f33d6f14524e256d09719e1960d22bf072d62e4bda7375f7895a092c7eb2f9d + undici-types: "npm:~6.21.0" + checksum: 10/d0669a8a37a18532c886ccfa51eb3fe1e46088deb4d3d27ebcd5d7d68bd6343ad1c7a3fcb85164780a57629359c33a6c917ecff748ea232bceac7692acc96537 languageName: node linkType: hard @@ -32452,7 +32452,7 @@ __metadata: "@types/ejson": "npm:^2.2.2" "@types/express": "npm:^4.17.21" "@types/fibers": "npm:^3.1.4" - "@types/node": "npm:~20.17.8" + "@types/node": "npm:~22.14.0" "@types/ws": "npm:^8.5.13" ajv: "npm:^8.17.1" bcrypt: "npm:^5.1.1" @@ -36166,6 +36166,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10/ec8f41aa4359d50f9b59fa61fe3efce3477cc681908c8f84354d8567bb3701fafdddf36ef6bff307024d3feb42c837cf6f670314ba37fc8145e219560e473d14 + languageName: node + linkType: hard + "undici@npm:^5.25.4": version: 5.28.4 resolution: "undici@npm:5.28.4" From 2c72ac44aa48d1187732b6bafb6ac4e976874ed0 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 3 Apr 2025 17:49:58 -0300 Subject: [PATCH 064/187] test: improve tests to be able to run multiple times (#35699) Co-authored-by: Diego Sampaio --- apps/meteor/tests/data/livechat/canned-responses.ts | 2 +- apps/meteor/tests/data/livechat/tags.ts | 2 +- .../tests/end-to-end/api/livechat/15-canned-responses.ts | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/meteor/tests/data/livechat/canned-responses.ts b/apps/meteor/tests/data/livechat/canned-responses.ts index 23cfac13e2369..55a6db0b76dfd 100644 --- a/apps/meteor/tests/data/livechat/canned-responses.ts +++ b/apps/meteor/tests/data/livechat/canned-responses.ts @@ -9,7 +9,7 @@ export const createCannedResponse = (): Promise => { .send({ message: JSON.stringify({ method: 'livechat:saveTag', - params: [undefined, { name: faker.person.firstName(), description: faker.lorem.sentence() }, departments], + params: [undefined, { name: faker.string.uuid(), description: faker.lorem.sentence() }, departments], id: '101', msg: 'method', }), diff --git a/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts b/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts index a71925531f4d5..8099990a6a72e 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/15-canned-responses.ts @@ -168,6 +168,7 @@ import { IS_EE } from '../../../e2e/config/constants'; }); describe('[POST] canned-responses', () => { + const dupshortcut = `shortcut-${faker.string.nanoid(6)}`; it('should fail if user dont have save-canned-responses permission', async () => { await updatePermission('save-canned-responses', []); return request @@ -197,7 +198,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const { body } = await request .post(api('canned-responses')) .set(credentials) - .send({ shortcut: 'shortcutxx', scope: 'user', tags: ['tag'], text: 'text' }) + .send({ shortcut: dupshortcut, scope: 'user', tags: ['tag'], text: 'text' }) .expect(200); expect(body).to.have.property('success', true); }); @@ -205,7 +206,7 @@ import { IS_EE } from '../../../e2e/config/constants'; return request .post(api('canned-responses')) .set(credentials) - .send({ shortcut: 'shortcutxx', scope: 'user', tags: ['tag'], text: 'text' }) + .send({ shortcut: dupshortcut, scope: 'user', tags: ['tag'], text: 'text' }) .expect(400); }); it('should save a canned response related to an EE tag', async () => { @@ -214,7 +215,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const { body } = await request .post(api('canned-responses')) .set(credentials) - .send({ shortcut: 'shortcutxxx', scope: 'user', tags: [tag.name], text: 'text' }) + .send({ shortcut: `eetag-${faker.string.nanoid(6)}`, scope: 'user', tags: [tag.name], text: 'text' }) .expect(200); expect(body).to.have.property('success', true); @@ -232,7 +233,7 @@ import { IS_EE } from '../../../e2e/config/constants'; const { body } = await request .post(api('canned-responses')) .set(credentials) - .send({ shortcut: 'shortcutxxxx', scope: 'user', tags: [tag.name], text: 'text' }) + .send({ shortcut: `remove-${faker.string.nanoid(6)}`, scope: 'user', tags: [tag.name], text: 'text' }) .expect(200); expect(body).to.have.property('success', true); From 14cb40247c84fc9f609586d0d7f5b73975d5f89c Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Fri, 4 Apr 2025 04:00:46 +0530 Subject: [PATCH 065/187] chore: Update video message icon (#35701) --- .../MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts index a7542ba113273..46d72c824b4e1 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts @@ -55,7 +55,7 @@ export const useVideoMessageAction = (disabled: boolean): GenericMenuItemProps = return { id: 'video-message', content: getMediaActionTitle, - icon: 'video', + icon: 'video-message', disabled: !isAllowed || Boolean(disabled), onClick: handleOpenVideoMessage, }; From 910e57813421a5eac2bd75772ab6a9848fee43d5 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 3 Apr 2025 22:03:30 -0300 Subject: [PATCH 066/187] chore: don't `ignoreUndefined` (#31497) --- apps/meteor/app/api/server/v1/chat.ts | 2 +- apps/meteor/app/api/server/v1/teams.ts | 6 +- .../app/apps/server/converters/messages.js | 65 +++--- .../app/apps/server/converters/rooms.js | 203 ++++++++++-------- .../app/apps/server/converters/users.js | 5 +- .../methods/setUserPublicAndPrivateKeys.ts | 6 + .../federation/server/endpoints/dispatch.js | 14 +- .../federation/server/functions/addUser.js | 3 +- .../classes/converters/RoomConverter.ts | 3 +- .../classes/converters/UserConverter.ts | 2 +- .../app/lib/server/functions/createRoom.ts | 3 +- .../server/functions/processWebhookMessage.ts | 3 +- .../app/livechat/server/api/v1/contact.ts | 3 +- apps/meteor/app/livechat/server/lib/Helper.ts | 8 +- .../app/livechat/server/lib/LivechatTyped.ts | 3 +- .../server/lib/contacts/createContact.ts | 12 +- .../server/lib/contacts/updateContact.ts | 10 +- apps/meteor/ee/server/api/audit.ts | 2 +- .../ee/server/apps/communication/rest.ts | 7 +- .../ee/server/models/raw/CannedResponse.ts | 2 +- .../rocketchat-mongo-config/server/index.js | 2 +- .../tests/unit/app/apps/server/rooms.tests.ts | 2 +- packages/apps-engine/package.json | 1 + packages/apps-engine/src/server/AppManager.ts | 10 +- packages/models/src/models/Users.ts | 2 +- packages/tools/src/index.ts | 1 + packages/tools/src/removeEmpty.spec.ts | 87 ++++++++ packages/tools/src/removeEmpty.ts | 7 + yarn.lock | 1 + 29 files changed, 307 insertions(+), 168 deletions(-) create mode 100644 packages/tools/src/removeEmpty.spec.ts create mode 100644 packages/tools/src/removeEmpty.ts diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 7569f321fa203..39b1109a948f6 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -375,7 +375,7 @@ API.v1.addRoute( _id: msg._id, msg: msgFromBody, rid: msg.rid, - customFields: this.bodyParams.customFields as Record | undefined, + ...(this.bodyParams.customFields && { customFields: this.bodyParams.customFields }), }, this.bodyParams.previewUrls, ), diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index 4b54cbffb7226..84d1ba5db264c 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -441,9 +441,9 @@ API.v1.addRoute( const canSeeAllMembers = await hasPermissionAsync(this.userId, 'view-all-teams', team.roomId); const query = { - username: username ? new RegExp(escapeRegExp(username), 'i') : undefined, - name: name ? new RegExp(escapeRegExp(name), 'i') : undefined, - status: status ? { $in: status as UserStatus[] } : undefined, + ...(username && { username: new RegExp(escapeRegExp(username), 'i') }), + ...(name && { name: new RegExp(escapeRegExp(name), 'i') }), + ...(status && { status: { $in: status as UserStatus[] } }), }; const { records, total } = await Team.members(this.userId, team._id, canSeeAllMembers, { offset, count }, query); diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js index 8cc6f4ea270f2..a824df3228396 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.js @@ -1,6 +1,7 @@ import { isMessageFromVisitor } from '@rocket.chat/core-typings'; import { Messages, Rooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; +import { removeEmpty } from '@rocket.chat/tools'; import { cachedFunction } from './cachedFunction'; import { transformMappedData } from './transformMappedData'; @@ -236,39 +237,37 @@ export class AppMessagesConverter { } return attachments.map((attachment) => - Object.assign( - { - collapsed: attachment.collapsed, - color: attachment.color, - text: attachment.text, - ts: attachment.timestamp ? attachment.timestamp.toJSON() : attachment.timestamp, - message_link: attachment.timestampLink, - thumb_url: attachment.thumbnailUrl, - author_name: attachment.author ? attachment.author.name : undefined, - author_link: attachment.author ? attachment.author.link : undefined, - author_icon: attachment.author ? attachment.author.icon : undefined, - title: attachment.title ? attachment.title.value : undefined, - title_link: attachment.title ? attachment.title.link : undefined, - title_link_download: attachment.title ? attachment.title.displayDownloadLink : undefined, - image_dimensions: attachment.imageDimensions, - image_preview: attachment.imagePreview, - image_url: attachment.imageUrl, - image_type: attachment.imageType, - image_size: attachment.imageSize, - audio_url: attachment.audioUrl, - audio_type: attachment.audioType, - audio_size: attachment.audioSize, - video_url: attachment.videoUrl, - video_type: attachment.videoType, - video_size: attachment.videoSize, - fields: attachment.fields, - button_alignment: attachment.actionButtonsAlignment, - actions: attachment.actions, - type: attachment.type, - description: attachment.description, - }, - attachment._unmappedProperties_, - ), + removeEmpty({ + collapsed: attachment.collapsed, + color: attachment.color, + text: attachment.text, + ts: attachment.timestamp ? attachment.timestamp.toJSON() : attachment.timestamp, + message_link: attachment.timestampLink, + thumb_url: attachment.thumbnailUrl, + author_name: attachment.author ? attachment.author.name : undefined, + author_link: attachment.author ? attachment.author.link : undefined, + author_icon: attachment.author ? attachment.author.icon : undefined, + title: attachment.title ? attachment.title.value : undefined, + title_link: attachment.title ? attachment.title.link : undefined, + title_link_download: attachment.title ? attachment.title.displayDownloadLink : undefined, + image_dimensions: attachment.imageDimensions, + image_preview: attachment.imagePreview, + image_url: attachment.imageUrl, + image_type: attachment.imageType, + image_size: attachment.imageSize, + audio_url: attachment.audioUrl, + audio_type: attachment.audioType, + audio_size: attachment.audioSize, + video_url: attachment.videoUrl, + video_type: attachment.videoType, + video_size: attachment.videoSize, + fields: attachment.fields, + button_alignment: attachment.actionButtonsAlignment, + actions: attachment.actions, + type: attachment.type, + description: attachment.description, + ...attachment._unmappedProperties_, + }), ); } diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index b2bbcda49610d..5fc1c5ab57bf8 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -20,120 +20,147 @@ export class AppRoomsConverter { return this.convertRoom(room); } - async convertAppRoom(room, isPartial = false) { - if (!room) { - return undefined; + async __getCreator(user) { + if (!user) { + return; } - let u; - if (room.creator) { - const creator = await Users.findOneById(room.creator.id); - u = { - _id: creator._id, - username: creator.username, - name: creator.name, - }; + const creator = await Users.findOneById(user, { projection: { _id: 1, username: 1, name: 1 } }); + if (!creator) { + return; } - let v; - if (room.visitor) { - const visitor = await LivechatVisitors.findOneEnabledById(room.visitor.id); + return { + _id: creator._id, + username: creator.username, + name: creator.name, + }; + } - const { lastMessageTs, phone } = room.visitorChannelInfo; + async __getVisitor({ visitor: roomVisitor, visitorChannelInfo }) { + if (!roomVisitor) { + return; + } - v = { - _id: visitor._id, - username: visitor.username, - token: visitor.token, - status: visitor.status || 'online', - ...(lastMessageTs && { lastMessageTs }), - ...(phone && { phone }), - }; + const visitor = await LivechatVisitors.findOneEnabledById(roomVisitor.id); + if (!visitor) { + return; } - let departmentId; - if (room.department) { - const department = await LivechatDepartment.findOneById(room.department.id, { projection: { _id: 1 } }); - departmentId = department._id; + const { lastMessageTs, phone } = visitorChannelInfo; + + return { + _id: visitor._id, + username: visitor.username, + token: visitor.token, + status: visitor.status || 'online', + ...(lastMessageTs && { lastMessageTs }), + ...(phone && { phone }), + }; + } + + async __getUserIdAndUsername(uid) { + if (!uid) { + return; + } + + const user = await Users.findOneById(uid, { projection: { _id: 1, username: 1 } }); + if (!user) { + return; + } + + return { + _id: user._id, + username: user.username, + }; + } + + async __getRoomCloser(room, v) { + if (!room.closedBy) { + return; } - let servedBy; - if (room.servedBy) { - const user = await Users.findOneById(room.servedBy.id); - servedBy = { + if (room.closer === 'user') { + const user = await Users.findOneById(room.closedBy.id, { projection: { _id: 1, username: 1 } }); + if (!user) { + return; + } + + return { _id: user._id, username: user.username, }; } - let closedBy; - if (room.closedBy) { - if (room.closer === 'user') { - const user = await Users.findOneById(room.closedBy.id); - closedBy = { - _id: user._id, - username: user.username, - }; - } else if (room.closer === 'visitor') { - closedBy = { - _id: v._id, - username: v.username, - }; - } + if (room.closer === 'visitor' && v) { + return { + _id: v._id, + username: v.username, + }; } + } - let contactId; - if (room.contact?._id) { - const contact = await LivechatContacts.findOneById(room.contact._id, { projection: { _id: 1 } }); - contactId = contact._id; + // TODO do we really need this? + async __getContactId({ contact }) { + if (!contact?._id) { + return; } + const contactFromDb = await LivechatContacts.findOneById(contact._id, { projection: { _id: 1 } }); + return contactFromDb?._id; + } - let _default; - if (typeof room.isDefault !== 'undefined') { - _default = room.isDefault; + // TODO do we really need this? + async __getDepartment({ department }) { + if (!department) { + return; } + const dept = await LivechatDepartment.findOneById(department.id, { projection: { _id: 1 } }); + return dept?._id; + } - let ro; - if (typeof room.isReadOnly !== 'undefined') { - ro = room.isReadOnly; + async convertAppRoom(room, isPartial = false) { + if (!room) { + return undefined; } - let sysMes; - if (typeof room.displaySystemMessages !== 'undefined') { - sysMes = room.displaySystemMessages; - } + const u = await this.__getCreator(room.creator?.id); - let msgs; - if (typeof room.messageCount !== 'undefined') { - msgs = room.messageCount; - } + const v = await this.__getVisitor(room); + + const departmentId = await this.__getDepartment(room); + + const servedBy = await this.__getUserIdAndUsername(room.servedBy); + + const closedBy = await this.__getRoomCloser(room, v); + + const contactId = await this.__getContactId(room); const newRoom = { ...(room.id && { _id: room.id }), - fname: room.displayName, - name: room.slugifiedName, t: room.type, - u, - v, - ro, - sysMes, - msgs, - departmentId, - servedBy, - closedBy, - members: room.members, - uids: room.userIds, - default: _default, - waitingResponse: typeof room.isWaitingResponse === 'undefined' ? undefined : !!room.isWaitingResponse, - open: typeof room.isOpen === 'undefined' ? undefined : !!room.isOpen, ts: room.createdAt, + msgs: room.messageCount || 0, _updatedAt: room.updatedAt, - closedAt: room.closedAt, - lm: room.lastModifiedAt, - customFields: room.customFields, - livechatData: room.livechatData, - prid: typeof room.parentRoom === 'undefined' ? undefined : room.parentRoom.id, - contactId, + ...(room.displayName && { fname: room.displayName }), + ...(room.type !== 'd' && { name: room.slugifiedName }), + ...(room.members && { members: room.members }), + ...(typeof room.isDefault !== 'undefined' && { default: room.isDefault }), + ...(typeof room.isReadOnly !== 'undefined' && { ro: room.isReadOnly }), + ...(typeof room.displaySystemMessages !== 'undefined' && { sysMes: room.displaySystemMessages }), + ...(u && { u }), + ...(v && { v }), + ...(departmentId && { departmentId }), + ...(servedBy && { servedBy }), + ...(closedBy && { closedBy }), + ...(room.userIds && { uids: room.userIds }), + ...(typeof room.isWaitingResponse !== 'undefined' && { waitingResponse: !!room.isWaitingResponse }), + ...(typeof room.isOpen !== 'undefined' && { open: !!room.isOpen }), + ...(room.closedAt && { closedAt: room.closedAt }), + ...(room.lastModifiedAt && { lm: room.lastModifiedAt }), + ...(room.customFields && { customFields: room.customFields }), + ...(room.livechatData && { livechatData: room.livechatData }), + ...(typeof room.parentRoom !== 'undefined' && { prid: room.parentRoom.id }), + ...(contactId && { contactId }), ...(room._USERNAMES && { _USERNAMES: room._USERNAMES }), ...(room.source && { source: { @@ -142,13 +169,7 @@ export class AppRoomsConverter { }), }; - if (isPartial) { - Object.entries(newRoom).forEach(([key, value]) => { - if (typeof value === 'undefined') { - delete newRoom[key]; - } - }); - } else { + if (!isPartial) { Object.assign(newRoom, room._unmappedProperties_); } diff --git a/apps/meteor/app/apps/server/converters/users.js b/apps/meteor/app/apps/server/converters/users.js index e89bf71a04281..b8f4d5c9043b7 100644 --- a/apps/meteor/app/apps/server/converters/users.js +++ b/apps/meteor/app/apps/server/converters/users.js @@ -1,5 +1,6 @@ import { UserStatusConnection, UserType } from '@rocket.chat/apps-engine/definition/users'; import { Users } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; export class AppUsersConverter { constructor(orch) { @@ -56,7 +57,7 @@ export class AppUsersConverter { return undefined; } - return { + return removeEmpty({ _id: user.id, username: user.username, emails: user.emails, @@ -71,7 +72,7 @@ export class AppUsersConverter { _updatedAt: user.updatedAt, lastLogin: user.lastLoginAt, appId: user.appId, - }; + }); } _convertUserTypeToEnum(type) { diff --git a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts index 2477d1547ca25..b852e408eea03 100644 --- a/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts +++ b/apps/meteor/app/e2e/server/methods/setUserPublicAndPrivateKeys.ts @@ -25,6 +25,12 @@ Meteor.methods({ }); } + if (!keyPair.public_key || !keyPair.private_key) { + throw new Meteor.Error('error-invalid-keys', 'Invalid keys', { + method: 'e2e.setUserPublicAndPrivateKeys', + }); + } + if (!keyPair.force) { const keys = await Users.fetchKeysByUserId(userId); diff --git a/apps/meteor/app/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js index d80f74bf18d35..65a675722bba2 100644 --- a/apps/meteor/app/federation/server/endpoints/dispatch.js +++ b/apps/meteor/app/federation/server/endpoints/dispatch.js @@ -1,6 +1,7 @@ import { api } from '@rocket.chat/core-services'; import { eventTypes } from '@rocket.chat/core-typings'; import { FederationServers, FederationRoomEvents, Rooms, Messages, Subscriptions, Users, ReadReceipts } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; import EJSON from 'ejson'; import { API } from '../../../api/server'; @@ -120,8 +121,8 @@ const eventHandlers = { if (persistedUser) { // Update the federation, if its not already set (if it's set, this is likely an event being reprocessed) - if (!persistedUser.federation) { - await Users.updateOne({ _id: persistedUser._id }, { $set: { federation: user.federation } }); + if (!persistedUser.federation && user.federation) { + await Users.updateOne({ _id: persistedUser._id }, { $set: { federation: removeEmpty(user.federation) } }); federationAltered = true; } } else { @@ -139,8 +140,11 @@ const eventHandlers = { try { if (persistedSubscription) { // Update the federation, if its not already set (if it's set, this is likely an event being reprocessed - if (!persistedSubscription.federation) { - await Subscriptions.updateOne({ _id: persistedSubscription._id }, { $set: { federation: subscription.federation } }); + if (!persistedSubscription.federation && subscription.federation) { + await Subscriptions.updateOne( + { _id: persistedSubscription._id }, + { $set: { federation: removeEmpty(subscription.federation) } }, + ); federationAltered = true; } } else { @@ -148,7 +152,7 @@ const eventHandlers = { const denormalizedSubscription = normalizers.denormalizeSubscription(subscription); // Create the subscription - const { insertedId } = await Subscriptions.insertOne(denormalizedSubscription); + const { insertedId } = await Subscriptions.insertOne(removeEmpty(denormalizedSubscription)); if (insertedId) { void notifyOnSubscriptionChangedById(insertedId); } diff --git a/apps/meteor/app/federation/server/functions/addUser.js b/apps/meteor/app/federation/server/functions/addUser.js index 8420a47944af7..0cddb226d263a 100644 --- a/apps/meteor/app/federation/server/functions/addUser.js +++ b/apps/meteor/app/federation/server/functions/addUser.js @@ -1,4 +1,5 @@ import { FederationServers, Users } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; import { getUserByUsername } from '../handler'; @@ -19,7 +20,7 @@ export async function addUser(query) { try { // Create the local user - userId = await Users.create(user); + userId = await Users.create(removeEmpty(user)); // Refresh the servers list await FederationServers.refreshServers(); diff --git a/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts index e9a14fed6a038..ba7d01b0009ee 100644 --- a/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts @@ -1,5 +1,6 @@ import type { IImportChannel, IImportChannelRecord, IRoom } from '@rocket.chat/core-typings'; import { Subscriptions, Rooms, Users } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; import limax from 'limax'; import { RecordConverter } from './RecordConverter'; @@ -150,7 +151,7 @@ export class RoomConverter extends RecordConverter { const roomUpdate: { $set?: Record; $addToSet?: Record } = {}; if (Object.keys(set).length > 0) { - roomUpdate.$set = set; + roomUpdate.$set = removeEmpty(set); } if (roomData.importIds.length) { diff --git a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts index 88ea742de7bc7..03f853832ef51 100644 --- a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts @@ -201,7 +201,7 @@ export class UserConverter extends RecordConverter, currentPath: string): void => { for (const key in source) { - if (!source.hasOwnProperty(key)) { + if (!source.hasOwnProperty(key) || source[key] === undefined) { continue; } diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 914b3d9a5d937..22a5d7c69dc7b 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ import { AppEvents, Apps } from '@rocket.chat/apps'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { Federation, FederationEE, License, Message, Team } from '@rocket.chat/core-services'; @@ -203,7 +202,7 @@ export const createRoom = async ( }, ts: now, ro: readOnly === true, - sidepanel, + ...(sidepanel && { sidepanel }), }; if (teamId) { diff --git a/apps/meteor/app/lib/server/functions/processWebhookMessage.ts b/apps/meteor/app/lib/server/functions/processWebhookMessage.ts index ae304b2af01d3..372df0d8ed77c 100644 --- a/apps/meteor/app/lib/server/functions/processWebhookMessage.ts +++ b/apps/meteor/app/lib/server/functions/processWebhookMessage.ts @@ -1,4 +1,5 @@ import type { IMessage, IUser, RequiredField, MessageAttachment } from '@rocket.chat/core-typings'; +import { removeEmpty } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; @@ -135,7 +136,7 @@ export const processWebhookMessage = async function ( await validateRoomMessagePermissionsAsync(room, { uid: user._id, ...user }); - const messageReturn = await sendMessage(user, message, room); + const messageReturn = await sendMessage(user, removeEmpty(message), room); sentData.push({ channel, message: messageReturn }); } diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 03cc5ddeaabd0..702662c917f25 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -9,6 +9,7 @@ import { isGETOmnichannelContactsCheckExistenceProps, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { removeEmpty } from '@rocket.chat/tools'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -121,7 +122,7 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['update-livechat-contact'], validateParams: isPOSTUpdateOmnichannelContactsProps }, { async post() { - const contact = await updateContact({ ...this.bodyParams }); + const contact = await updateContact(removeEmpty(this.bodyParams)); return API.v1.success({ contact }); }, diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index b5d1b4569a430..7b26f7e18654a 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -31,6 +31,7 @@ import { Users, LivechatContacts, } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { ClientSession } from 'mongodb'; @@ -139,7 +140,6 @@ export const prepareLivechatRoom = async ( alias: 'unknown', }, queuedAt: newRoomAt, - livechatData: undefined, priorityWeight: LivechatPriorityWeight.NOT_SPECIFIED, estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE, ...extraRoomInfo, @@ -148,7 +148,7 @@ export const prepareLivechatRoom = async ( export const createLivechatRoom = async (room: InsertionModel, session: ClientSession) => { const result = await LivechatRooms.findOneAndUpdate( - room, + removeEmpty(room), { $set: {}, }, @@ -212,7 +212,7 @@ export const createLivechatInquiry = async ({ }); const result = await LivechatInquiry.findOneAndUpdate( - { + removeEmpty({ rid, name, ts, @@ -231,7 +231,7 @@ export const createLivechatInquiry = async ({ estimatedWaitingTimeQueue: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE, ...extraInquiryInfo, - }, + }), { $set: { _id: new ObjectId().toHexString(), diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 67230078847f6..38d5375b0380d 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -26,6 +26,7 @@ import { Rooms, LivechatCustomField, } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; @@ -203,7 +204,7 @@ class LivechatClass { throw new Meteor.Error('error-user-is-not-agent', 'User is not a livechat agent'); } - await Users.setLivechatData(_id, agentData); + await Users.setLivechatData(_id, removeEmpty(agentData)); const currentDepartmentsForAgent = await LivechatDepartmentAgents.findByAgentId(_id).toArray(); diff --git a/apps/meteor/app/livechat/server/lib/contacts/createContact.ts b/apps/meteor/app/livechat/server/lib/contacts/createContact.ts index 98cf238d9b5e3..44efef5b2e220 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/createContact.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/createContact.ts @@ -40,12 +40,12 @@ export async function createContact({ return LivechatContacts.insertContact({ name, - emails: emails?.map((address) => ({ address })), - phones: phones?.map((phoneNumber) => ({ phoneNumber })), - contactManager, - channels, - customFields, - lastChat, + ...(emails && { emails: emails?.map((address) => ({ address })) }), + ...(phones && { phones: phones?.map((phoneNumber) => ({ phoneNumber })) }), + ...(contactManager && { contactManager }), + ...(channels && { channels }), + ...(customFields && { customFields }), + ...(lastChat && { lastChat }), unknown, ...(importIds?.length && { importIds }), }); diff --git a/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts b/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts index 6c344274386e6..cabb0359796a3 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/updateContact.ts @@ -73,11 +73,11 @@ export async function updateContact(params: UpdateContactParams): Promise ({ address })), - phones: phones?.map((phoneNumber) => ({ phoneNumber })), - contactManager, - channels, - customFields: customFieldsToUpdate, + ...(emails && { emails: emails?.map((address) => ({ address })) }), + ...(phones && { phones: phones?.map((phoneNumber) => ({ phoneNumber })) }), + ...(contactManager && { contactManager }), + ...(channels && { channels }), + ...(customFieldsToUpdate && { customFields: customFieldsToUpdate }), ...(wipeConflicts && { conflictingFields: [] }), }); diff --git a/apps/meteor/ee/server/api/audit.ts b/apps/meteor/ee/server/api/audit.ts index 2b96927e69960..61a0c618b410d 100644 --- a/apps/meteor/ee/server/api/audit.ts +++ b/apps/meteor/ee/server/api/audit.ts @@ -78,7 +78,7 @@ API.v1.addRoute( _id: this.user._id, username: this.user.username, name: this.user.name, - avatarETag: this.user.avatarETag, + ...(this.user.avatarETag && { avatarETag: this.user.avatarETag }), }, fields: { msg: 'Room_members_list', diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index 32282c78d15cc..53719aee9a87a 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -407,7 +407,12 @@ export class AppsRestApi { ?.get('users') ?.convertToApp(await Meteor.userAsync()); - const aff = await manager.add(buff, { marketplaceInfo, permissionsGranted, enable: false, user }); + const aff = await manager.add(buff, { + ...(marketplaceInfo && { marketplaceInfo }), + permissionsGranted, + enable: false, + user, + }); const info: IAppInfo & { status?: AppStatus } = aff.getAppInfo(); if (aff.hasStorageError()) { diff --git a/apps/meteor/ee/server/models/raw/CannedResponse.ts b/apps/meteor/ee/server/models/raw/CannedResponse.ts index fdfb4a02d97b5..2bae8abd5dd70 100644 --- a/apps/meteor/ee/server/models/raw/CannedResponse.ts +++ b/apps/meteor/ee/server/models/raw/CannedResponse.ts @@ -113,6 +113,6 @@ export class CannedResponseRaw extends BaseRaw imple }, }; - return this.updateMany({}, update); + return this.updateMany({ tags: tagId }, update); } } diff --git a/apps/meteor/packages/rocketchat-mongo-config/server/index.js b/apps/meteor/packages/rocketchat-mongo-config/server/index.js index 63343ac640143..1c72f87dfc41e 100644 --- a/apps/meteor/packages/rocketchat-mongo-config/server/index.js +++ b/apps/meteor/packages/rocketchat-mongo-config/server/index.js @@ -19,7 +19,7 @@ tls.DEFAULT_ECDH_CURVE = 'auto'; const mongoConnectionOptions = { // add retryWrites=false if not present in MONGO_URL ...(!process.env.MONGO_URL.includes('retryWrites') && { retryWrites: false }), - // ignoreUndefined: false, // TODO evaluate adding this config + ignoreUndefined: false, // TODO ideally we should call isTracingEnabled(), but since this is a Meteor package we can't :/ monitorCommands: ['yes', 'true'].includes(String(process.env.TRACING_ENABLED).toLowerCase()), diff --git a/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts b/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts index 718d79baef361..0e7508556cf74 100644 --- a/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts +++ b/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts @@ -112,9 +112,9 @@ describe('The AppMessagesConverter instance', () => { expect(rocketchatRoom).to.have.property('_id', appRoom.id); expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName); expect(rocketchatRoom).to.have.property('sysMes', appRoom.displaySystemMessages); + expect(rocketchatRoom).to.have.property('msgs', 0); expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt); - expect(rocketchatRoom).to.not.have.property('msgs'); expect(rocketchatRoom).to.not.have.property('ro'); expect(rocketchatRoom).to.not.have.property('default'); }); diff --git a/packages/apps-engine/package.json b/packages/apps-engine/package.json index d8dfa07394c59..6bcc996a20b18 100644 --- a/packages/apps-engine/package.json +++ b/packages/apps-engine/package.json @@ -93,6 +93,7 @@ }, "dependencies": { "@msgpack/msgpack": "3.0.0-beta2", + "@rocket.chat/tools": "workspace:^", "adm-zip": "^0.5.16", "debug": "^4.3.7", "esbuild": "^0.25.0", diff --git a/packages/apps-engine/src/server/AppManager.ts b/packages/apps-engine/src/server/AppManager.ts index bbc31eef5e630..cb398d506085a 100644 --- a/packages/apps-engine/src/server/AppManager.ts +++ b/packages/apps-engine/src/server/AppManager.ts @@ -1,5 +1,7 @@ import { Buffer } from 'buffer'; +import { removeEmpty } from '@rocket.chat/tools'; + import type { IGetAppsFilter } from './IGetAppsFilter'; import { ProxiedApp } from './ProxiedApp'; import type { PersistenceBridge, UserBridge } from './bridges'; @@ -607,7 +609,7 @@ export class AppManager { } descriptor.signature = await this.getSignatureManager().signApp(descriptor); - const created = await this.appMetadataStorage.create(descriptor); + const created = await this.appMetadataStorage.create(removeEmpty(descriptor)); if (!created) { aff.setStorageError('Failed to create the App, the storage did not return it.'); @@ -717,9 +719,9 @@ export class AppManager { languageContent: result.languageContent, settings: old.settings, implemented: result.implemented.getValues(), - marketplaceInfo: old.marketplaceInfo, - sourcePath: old.sourcePath, - permissionsGranted, + ...(old.marketplaceInfo && { marketplaceInfo: old.marketplaceInfo }), + ...(old.sourcePath && { sourcePath: old.sourcePath }), + ...(permissionsGranted && { permissionsGranted }), }; try { diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index 4408d494c2404..0c86e727a187f 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -3059,7 +3059,7 @@ export class UsersRaw extends BaseRaw> implements IU const settingsObject = Object.assign( {}, ...Object.keys(preferences).map((key) => ({ - [`settings.preferences.${key}`]: preferences[key], + ...(preferences[key] !== undefined && { [`settings.preferences.${key}`]: preferences[key] }), })), ); diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 410bd711d24a4..76dfbee33ddb0 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -8,3 +8,4 @@ export * from './timezone'; export * from './wrapExceptions'; export * from './getLoginExpiration'; export * from './converter'; +export * from './removeEmpty'; diff --git a/packages/tools/src/removeEmpty.spec.ts b/packages/tools/src/removeEmpty.spec.ts new file mode 100644 index 0000000000000..62c3c15593f1e --- /dev/null +++ b/packages/tools/src/removeEmpty.spec.ts @@ -0,0 +1,87 @@ +import { removeEmpty } from './removeEmpty'; + +describe('removeEmpty', () => { + it('should remove null props', () => { + const obj = { a: 1, b: null }; + + expect(removeEmpty(obj)).toEqual({ a: 1 }); + }); + + it('should remove undefined props', () => { + const obj = { a: 1, b: undefined }; + + expect(removeEmpty(obj)).toEqual({ a: 1 }); + }); + + it('should not remove empty strings', () => { + const obj = { a: 1, b: '' }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: '' }); + }); + + it('should not remove empty arrays', () => { + const obj = { a: 1, b: [] }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: [] }); + }); + + it('should not remove empty objects', () => { + const obj = { a: 1, b: {} }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: {} }); + }); + + it('should not remove 0', () => { + const obj = { a: 1, b: 0 }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: 0 }); + }); + + it('should not remove false', () => { + const obj = { a: 1, b: false }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: false }); + }); + + it('should not remove NaN', () => { + const obj = { a: 1, b: NaN }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: NaN }); + }); + + it('should not remove Infinity', () => { + const obj = { a: 1, b: Infinity }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: Infinity }); + }); + + it('should not remove functions', () => { + const fn = () => { + // noop + }; + + const obj = { + a: 1, + fn, + }; + + expect(removeEmpty(obj)).toEqual({ + a: 1, + fn, + }); + }); + + it('should not remove symbols', () => { + const b = Symbol('test'); + + const obj = { a: 1, b }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b }); + }); + + it('should not remove objects with non-empty props', () => { + const obj = { a: 1, b: { c: 2 } }; + + expect(removeEmpty(obj)).toEqual({ a: 1, b: { c: 2 } }); + }); +}); diff --git a/packages/tools/src/removeEmpty.ts b/packages/tools/src/removeEmpty.ts new file mode 100644 index 0000000000000..2e8bfc3197362 --- /dev/null +++ b/packages/tools/src/removeEmpty.ts @@ -0,0 +1,7 @@ +type NonEmpty = { + [K in keyof T]: Exclude; +}; + +export function removeEmpty>(obj: T): NonEmpty { + return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null)) as NonEmpty; +} diff --git a/yarn.lock b/yarn.lock index 5c89d178dca06..7a12e6c363557 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7815,6 +7815,7 @@ __metadata: dependencies: "@msgpack/msgpack": "npm:3.0.0-beta2" "@rocket.chat/eslint-config": "workspace:~" + "@rocket.chat/tools": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" "@types/adm-zip": "npm:^0.5.6" "@types/debug": "npm:^4.1.12" From dcc0f183085a93ac397e722de1ad58f4b96f5d70 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 4 Apr 2025 00:19:10 -0300 Subject: [PATCH 067/187] chore: bump version to 7.6.0-develop --- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- packages/core-typings/package.json | 2 +- packages/rest-typings/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index c511f5540972c..ce63ec6724133 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.5.0-rc.5" + "version": "7.6.0-develop" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 102ba2d9c561d..20ca8f873fc7e 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.5.0-rc.5", + "version": "7.6.0-develop", "private": true, "type": "commonjs", "author": { diff --git a/package.json b/package.json index ef818569b1b8f..11e18c181b06b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.5.0-rc.5", + "version": "7.6.0-develop", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 1385c5dee5b52..ed2933bdfe31c 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.5.0-rc.5", + "version": "7.6.0-develop", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 785af1fee9707..fd077c48418b0 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.5.0-rc.5", + "version": "7.6.0-develop", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", From a486016f443b5f330d32bcaded6d8b7876ea6b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Fri, 4 Apr 2025 11:09:32 -0300 Subject: [PATCH 068/187] refactor: remove `CustomSounds` from meteor (#35666) --- apps/meteor/app/custom-sounds/client/index.ts | 1 - .../custom-sounds/client/lib/CustomSounds.ts | 147 ------------------ .../getUserNotificationsSoundVolume.tsx | 10 -- .../notification/useNewMessageNotification.ts | 27 ++-- .../notification/useNewRoomNotification.ts | 22 --- .../hooks/notification/useNotifyUser.ts | 10 +- ...eOmnichannelContinuousSoundNotification.ts | 42 +---- apps/meteor/client/importPackages.ts | 1 - .../providers/CallProvider/CallProvider.tsx | 12 +- .../CallProvider/hooks/useVoipSounds.ts | 28 ---- .../client/providers/CustomSoundProvider.tsx | 41 ----- .../CustomSoundProvider.tsx | 123 +++++++++++++++ .../providers/CustomSoundProvider/index.ts | 1 + .../CustomSoundProvider/lib/helpers.ts | 29 ++++ .../client/providers/OmnichannelProvider.tsx | 13 +- .../preferences/PreferencesSoundSection.tsx | 2 +- .../NotificationPreferencesWithData.tsx | 2 +- .../VideoConfPopups/VideoConfPopups.tsx | 14 +- .../tests/e2e/video-conference-ring.spec.ts | 37 ----- .../ui-contexts/src/CustomSoundContext.ts | 60 ++++++- packages/ui-voip/src/hooks/useVoipSounds.ts | 28 ---- .../ui-voip/src/providers/VoipProvider.tsx | 17 +- 22 files changed, 265 insertions(+), 402 deletions(-) delete mode 100644 apps/meteor/app/custom-sounds/client/index.ts delete mode 100644 apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts delete mode 100644 apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx delete mode 100644 apps/meteor/client/hooks/notification/useNewRoomNotification.ts delete mode 100644 apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts delete mode 100644 apps/meteor/client/providers/CustomSoundProvider.tsx create mode 100644 apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx create mode 100644 apps/meteor/client/providers/CustomSoundProvider/index.ts create mode 100644 apps/meteor/client/providers/CustomSoundProvider/lib/helpers.ts delete mode 100644 packages/ui-voip/src/hooks/useVoipSounds.ts diff --git a/apps/meteor/app/custom-sounds/client/index.ts b/apps/meteor/app/custom-sounds/client/index.ts deleted file mode 100644 index 95992988ccfb1..0000000000000 --- a/apps/meteor/app/custom-sounds/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomSounds } from './lib/CustomSounds'; diff --git a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts deleted file mode 100644 index f925caf7f8098..0000000000000 --- a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { ICustomSound } from '@rocket.chat/core-typings'; -import { ReactiveVar } from 'meteor/reactive-var'; - -import { getURL } from '../../../utils/client'; -import { sdk } from '../../../utils/client/lib/SDKClient'; - -const getCustomSoundId = (soundId: ICustomSound['_id']) => `custom-sound-${soundId}`; -const getAssetUrl = (asset: string, params?: Record) => getURL(asset, params, undefined, true); - -const defaultSounds = [ - { _id: 'chime', name: 'Chime', extension: 'mp3', src: getAssetUrl('sounds/chime.mp3') }, - { _id: 'door', name: 'Door', extension: 'mp3', src: getAssetUrl('sounds/door.mp3') }, - { _id: 'beep', name: 'Beep', extension: 'mp3', src: getAssetUrl('sounds/beep.mp3') }, - { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getAssetUrl('sounds/chelle.mp3') }, - { _id: 'ding', name: 'Ding', extension: 'mp3', src: getAssetUrl('sounds/ding.mp3') }, - { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getAssetUrl('sounds/droplet.mp3') }, - { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getAssetUrl('sounds/highbell.mp3') }, - { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getAssetUrl('sounds/seasons.mp3') }, - { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getAssetUrl('sounds/telephone.mp3') }, - { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getAssetUrl('sounds/outbound-call-ringing.mp3') }, - { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getAssetUrl('sounds/call-ended.mp3') }, - { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getAssetUrl('sounds/dialtone.mp3') }, - { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getAssetUrl('sounds/ringtone.mp3') }, -]; - -class CustomSoundsClass { - list: ReactiveVar>; - - initialFetchDone: boolean; - - constructor() { - this.list = new ReactiveVar({}); - this.initialFetchDone = false; - defaultSounds.forEach((sound) => this.add(sound)); - } - - add(sound: ICustomSound) { - if (!sound.src) { - sound.src = this.getURL(sound); - } - - const source = document.createElement('source'); - source.src = sound.src; - - const audio = document.createElement('audio'); - audio.id = getCustomSoundId(sound._id); - audio.preload = 'none'; - audio.appendChild(source); - - document.body.appendChild(audio); - - const list = this.list.get(); - list[sound._id] = sound; - this.list.set(list); - } - - remove(sound: ICustomSound) { - const list = this.list.get(); - delete list[sound._id]; - this.list.set(list); - const audio = document.querySelector(`#${getCustomSoundId(sound._id)}`); - audio?.remove(); - } - - getSound(soundId: ICustomSound['_id']) { - const list = this.list.get(); - return list[soundId]; - } - - update(sound: ICustomSound) { - const audio = document.querySelector(`#${getCustomSoundId(sound._id)}`); - if (audio) { - const list = this.list.get(); - if (!sound.src) { - sound.src = this.getURL(sound); - } - list[sound._id] = sound; - this.list.set(list); - const sourceEl = audio.querySelector('source'); - if (sourceEl) { - sourceEl.src = sound.src; - } - audio.load(); - } else { - this.add(sound); - } - } - - getURL(sound: ICustomSound) { - return getAssetUrl(`/custom-sounds/${sound._id}.${sound.extension}`, { _dc: sound.random || 0 }); - } - - getList() { - const list = Object.values(this.list.get()); - return list.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? '')); - } - - play = async (soundId: ICustomSound['_id'], { volume = 1, loop = false } = {}) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - if (!audio?.play) { - return; - } - - audio.volume = volume; - audio.loop = loop; - await audio.play(); - - return audio; - }; - - pause = (soundId: ICustomSound['_id']) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - if (!audio?.pause) { - return; - } - - audio.pause(); - }; - - stop = (soundId: ICustomSound['_id']) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - if (!audio?.load) { - return; - } - - audio?.load(); - }; - - isPlaying = (soundId: ICustomSound['_id']) => { - const audio = document.querySelector(`#${getCustomSoundId(soundId)}`); - - return audio && audio.duration > 0 && !audio.paused; - }; - - fetchCustomSoundList = async () => { - if (this.initialFetchDone) { - return; - } - const result = await sdk.call('listCustomSounds'); - for (const sound of result) { - this.add(sound); - } - this.initialFetchDone = true; - }; -} - -export const CustomSounds = new CustomSoundsClass(); diff --git a/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx b/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx deleted file mode 100644 index ea89cd51d4e3b..0000000000000 --- a/apps/meteor/app/utils/client/getUserNotificationsSoundVolume.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; - -import { getUserPreference } from './lib/getUserPreference'; - -export const getUserNotificationsSoundVolume = (userId: IUser['_id'] | null | undefined) => { - const masterVolume = getUserPreference(userId, 'masterVolume', 100); - const notificationsSoundVolume = getUserPreference(userId, 'notificationsSoundVolume', 100); - - return (notificationsSoundVolume * masterVolume) / 100; -}; diff --git a/apps/meteor/client/hooks/notification/useNewMessageNotification.ts b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts index 389fa010ffa32..36b94568a27a5 100644 --- a/apps/meteor/client/hooks/notification/useNewMessageNotification.ts +++ b/apps/meteor/client/hooks/notification/useNewMessageNotification.ts @@ -1,29 +1,24 @@ import type { AtLeast, ISubscription } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useUserPreference } from '@rocket.chat/ui-contexts'; - -import { CustomSounds } from '../../../app/custom-sounds/client/lib/CustomSounds'; -import { useUserSoundPreferences } from '../useUserSoundPreferences'; +import { useCustomSound } from '@rocket.chat/ui-contexts'; export const useNewMessageNotification = () => { - const newMessageNotification = useUserPreference('newMessageNotification'); - const { notificationsSoundVolume } = useUserSoundPreferences(); + const { notificationSounds } = useCustomSound(); const notifyNewMessage = useEffectEvent((sub: AtLeast) => { if (!sub || sub.audioNotificationValue === 'none') { return; } - if (sub.audioNotificationValue && sub.audioNotificationValue !== '0') { - void CustomSounds.play(sub.audioNotificationValue, { - volume: Number((notificationsSoundVolume / 100).toPrecision(2)), - }); - } + // TODO: Fix this - Room Notifications Preferences > sound > desktop is not working. + // plays the user notificationSound preference - if (newMessageNotification && newMessageNotification !== 'none') { - void CustomSounds.play(newMessageNotification, { - volume: Number((notificationsSoundVolume / 100).toPrecision(2)), - }); - } + // if (sub.audioNotificationValue && sub.audioNotificationValue !== '0') { + // void CustomSounds.play(sub.audioNotificationValue, { + // volume: Number((notificationsSoundVolume / 100).toPrecision(2)), + // }); + // } + + notificationSounds.playNewMessage(); }); return notifyNewMessage; }; diff --git a/apps/meteor/client/hooks/notification/useNewRoomNotification.ts b/apps/meteor/client/hooks/notification/useNewRoomNotification.ts deleted file mode 100644 index ad2807404d070..0000000000000 --- a/apps/meteor/client/hooks/notification/useNewRoomNotification.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useUserPreference } from '@rocket.chat/ui-contexts'; - -import { CustomSounds } from '../../../app/custom-sounds/client/lib/CustomSounds'; -import { useUserSoundPreferences } from '../useUserSoundPreferences'; - -export const useNewRoomNotification = () => { - const newRoomNotification = useUserPreference('newRoomNotification'); - const { notificationsSoundVolume } = useUserSoundPreferences(); - - const notifyNewRoom = useEffectEvent(() => { - if (!newRoomNotification) { - return; - } - - void CustomSounds.play(newRoomNotification, { - volume: Number((notificationsSoundVolume / 100).toPrecision(2)), - }); - }); - - return notifyNewRoom; -}; diff --git a/apps/meteor/client/hooks/notification/useNotifyUser.ts b/apps/meteor/client/hooks/notification/useNotifyUser.ts index 7f2cfb918ab7e..d9de0d3115e31 100644 --- a/apps/meteor/client/hooks/notification/useNotifyUser.ts +++ b/apps/meteor/client/hooks/notification/useNotifyUser.ts @@ -1,12 +1,11 @@ import type { AtLeast, INotificationDesktop, ISubscription } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useRouter, useStream, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useCustomSound, useRouter, useStream, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { useEmbeddedLayout } from '../useEmbeddedLayout'; import { useDesktopNotification } from './useDesktopNotification'; import { useNewMessageNotification } from './useNewMessageNotification'; -import { useNewRoomNotification } from './useNewRoomNotification'; import { CachedChatSubscription } from '../../../app/models/client'; import { RoomManager } from '../../lib/RoomManager'; import { fireGlobalEvent } from '../../lib/utils/fireGlobalEvent'; @@ -17,7 +16,7 @@ export const useNotifyUser = () => { const isLayoutEmbedded = useEmbeddedLayout(); const notifyUserStream = useStream('notify-user'); const muteFocusedConversations = useUserPreference('muteFocusedConversations'); - const newRoomNotification = useNewRoomNotification(); + const { notificationSounds } = useCustomSound(); const newMessageNotification = useNewMessageNotification(); const showDesktopNotification = useDesktopNotification(); @@ -27,7 +26,7 @@ export const useNotifyUser = () => { } if ((!router.getRouteParameters().name || router.getRouteParameters().name !== sub.name) && !sub.ls && sub.alert === true) { - newRoomNotification(); + notificationSounds.playNewRoom(); } }); @@ -83,6 +82,7 @@ export const useNotifyUser = () => { unsubNotification(); unsubSubs(); handle.stop(); + notificationSounds.stopNewRoom(); }; - }, [isLayoutEmbedded, notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user?._id]); + }, [isLayoutEmbedded, notificationSounds, notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user?._id]); }; diff --git a/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts b/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts index 639b4cb7af22e..d565aa74902d8 100644 --- a/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts +++ b/apps/meteor/client/hooks/useOmnichannelContinuousSoundNotification.ts @@ -1,54 +1,28 @@ -import type { ICustomSound } from '@rocket.chat/core-typings'; -import { useSetting, useUserPreference, useUserSubscriptions } from '@rocket.chat/ui-contexts'; +import { useCustomSound, useSetting, useUserSubscriptions } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { useUserSoundPreferences } from './useUserSoundPreferences'; -import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; - const query = { t: 'l', ls: { $exists: false }, open: true }; export const useOmnichannelContinuousSoundNotification = (queue: T[]) => { const userSubscriptions = useUserSubscriptions(query); + const { notificationSounds } = useCustomSound(); const playNewRoomSoundContinuously = useSetting('Livechat_continuous_sound_notification_new_livechat_room'); - const newRoomNotification = useUserPreference('newRoomNotification'); - const { notificationsSoundVolume } = useUserSoundPreferences(); - - const continuousCustomSoundId = newRoomNotification && `${newRoomNotification}-continuous`; - const hasUnreadRoom = userSubscriptions.length > 0 || queue.length > 0; useEffect(() => { - let audio: ICustomSound; - if (playNewRoomSoundContinuously && continuousCustomSoundId) { - audio = { ...CustomSounds.getSound(newRoomNotification), _id: continuousCustomSoundId }; - CustomSounds.add(audio); - } - - return () => { - if (audio) { - CustomSounds.remove(audio); - } - }; - }, [continuousCustomSoundId, newRoomNotification, playNewRoomSoundContinuously]); - - useEffect(() => { - if (!continuousCustomSoundId) { - return; - } if (!playNewRoomSoundContinuously) { - CustomSounds.pause(continuousCustomSoundId); return; } if (!hasUnreadRoom) { - CustomSounds.pause(continuousCustomSoundId); return; } - CustomSounds.play(continuousCustomSoundId, { - volume: notificationsSoundVolume / 100, - loop: true, - }); - }, [continuousCustomSoundId, playNewRoomSoundContinuously, userSubscriptions, notificationsSoundVolume, hasUnreadRoom]); + notificationSounds.playNewMessageLoop(); + + return () => { + notificationSounds.stopNewRoom(); + }; + }, [playNewRoomSoundContinuously, userSubscriptions, hasUnreadRoom, notificationSounds]); }; diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index d47cc47eae4d7..d82f6460d024f 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -1,7 +1,6 @@ import '../app/apple/client'; import '../app/authorization/client'; import '../app/autotranslate/client'; -import '../app/custom-sounds/client'; import '../app/emoji/client'; import '../app/emoji-emojione/client'; import '../app/emoji-custom/client'; diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx index fd86f6376ee8d..7448b4160aa8c 100644 --- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx +++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx @@ -22,13 +22,13 @@ import { useSetInputMediaDevice, useSetModal, useTranslation, + useCustomSound, } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import { useMemo, useRef, useCallback, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import type { OutgoingByeRequest } from 'sip.js/lib/core'; -import { useVoipSounds } from './hooks/useVoipSounds'; import type { CallContextValue } from '../../contexts/CallContext'; import { CallContext, useIsVoipEnterprise } from '../../contexts/CallContext'; import { useDialModal } from '../../hooks/useDialModal'; @@ -73,7 +73,7 @@ export const CallProvider = ({ children }: CallProviderProps) => { const { openDialModal } = useDialModal(); - const voipSounds = useVoipSounds(); + const { voipSounds } = useCustomSound(); const closeRoom = useCallback( async ( @@ -336,7 +336,9 @@ export const CallProvider = ({ children }: CallProviderProps) => { if (!callDetails.callInfo) { return; } + voipSounds.stopAll(); + if (callDetails.userState !== UserState.UAC) { return; } @@ -377,15 +379,15 @@ export const CallProvider = ({ children }: CallProviderProps) => { }; const onRinging = (): void => { - voipSounds.play('outbound-call-ringing'); + voipSounds.playDialer(); }; const onIncomingCallRinging = (): void => { - voipSounds.play('telephone'); + voipSounds.playRinger(); }; const onCallTerminated = (): void => { - voipSounds.play('call-ended', false); + voipSounds.playCallEnded(); voipSounds.stopAll(); }; diff --git a/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts b/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts deleted file mode 100644 index 7eea3b867f507..0000000000000 --- a/apps/meteor/client/providers/CallProvider/hooks/useVoipSounds.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useCustomSound } from '@rocket.chat/ui-contexts'; -import { useMemo } from 'react'; - -import { useUserSoundPreferences } from '../../../hooks/useUserSoundPreferences'; - -type VoipSound = 'telephone' | 'outbound-call-ringing' | 'call-ended'; - -export const useVoipSounds = () => { - const { play, pause } = useCustomSound(); - const { voipRingerVolume } = useUserSoundPreferences(); - - return useMemo( - () => ({ - play: (soundId: VoipSound, loop = true) => { - play(soundId, { - volume: Number((voipRingerVolume / 100).toPrecision(2)), - loop, - }); - }, - stop: (soundId: VoipSound) => pause(soundId), - stopAll: () => { - pause('telephone'); - pause('outbound-call-ringing'); - }, - }), - [play, pause, voipRingerVolume], - ); -}; diff --git a/apps/meteor/client/providers/CustomSoundProvider.tsx b/apps/meteor/client/providers/CustomSoundProvider.tsx deleted file mode 100644 index 454b16196422f..0000000000000 --- a/apps/meteor/client/providers/CustomSoundProvider.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { CustomSoundContext, useUserId, useStream } from '@rocket.chat/ui-contexts'; -import type { ReactNode } from 'react'; -import { useEffect } from 'react'; - -import { CustomSounds } from '../../app/custom-sounds/client/lib/CustomSounds'; - -type CustomSoundProviderProps = { - children?: ReactNode; -}; - -const CustomSoundProvider = ({ children }: CustomSoundProviderProps) => { - const userId = useUserId(); - useEffect(() => { - if (!userId) { - return; - } - void CustomSounds.fetchCustomSoundList(); - }, [userId]); - - const streamAll = useStream('notify-all'); - - useEffect(() => { - if (!userId) { - return; - } - - return streamAll('public-info', ([key, data]) => { - switch (key) { - case 'updateCustomSound': - CustomSounds.update(data[0].soundData); - break; - case 'deleteCustomSound': - CustomSounds.remove(data[0].soundData); - break; - } - }); - }, [userId, streamAll]); - return ; -}; - -export default CustomSoundProvider; diff --git a/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx b/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx new file mode 100644 index 0000000000000..1f13a15600859 --- /dev/null +++ b/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx @@ -0,0 +1,123 @@ +import type { ICustomSound } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { CustomSoundContext, useStream, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect, useRef, type ReactNode } from 'react'; + +import { defaultSounds, formatVolume, getCustomSoundURL } from './lib/helpers'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { useUserSoundPreferences } from '../../hooks/useUserSoundPreferences'; + +type CustomSoundProviderProps = { + children?: ReactNode; +}; + +const CustomSoundProvider = ({ children }: CustomSoundProviderProps) => { + const audioRefs = useRef([]); + + const queryClient = useQueryClient(); + const streamAll = useStream('notify-all'); + + const newRoomNotification = useUserPreference('newRoomNotification') || 'door'; + const newMessageNotification = useUserPreference('newMessageNotification') || 'chime'; + const { notificationsSoundVolume, voipRingerVolume } = useUserSoundPreferences(); + + const { data: list } = useQuery({ + queryFn: async () => { + const customSoundsList = await sdk.call('listCustomSounds'); + if (!customSoundsList.length) { + return defaultSounds; + } + return [...customSoundsList.map((sound) => ({ ...sound, src: getCustomSoundURL(sound) })), ...defaultSounds]; + }, + queryKey: ['listCustomSounds'], + initialData: defaultSounds, + }); + + const play = useEffectEvent((soundId: ICustomSound['_id'], { volume = 1, loop = false } = {}) => { + stop(soundId); + + const item = list?.find(({ _id }) => _id === soundId); + if (!item?.src) { + console.error('Unable to play sound', soundId); + return; + } + + const audio = new Audio(item.src); + audio.volume = volume; + audio.loop = loop; + audio.id = soundId; + audio.play(); + + audioRefs.current = [...audioRefs.current, audio]; + + return () => { + stop(soundId); + }; + }); + + const pause = useEffectEvent((soundId: ICustomSound['_id']) => { + const current = audioRefs.current?.find(({ id }) => id === soundId); + if (current) { + current.pause(); + audioRefs.current = audioRefs.current.filter(({ id }) => id !== soundId); + } + }); + + const stop = useEffectEvent((soundId: ICustomSound['_id']) => { + const current = audioRefs.current?.find(({ id }) => id === soundId); + if (current) { + current.load(); + audioRefs.current = audioRefs.current.filter(({ id }) => id !== soundId); + } + }); + + const callSounds = { + playRinger: () => play('ringtone', { loop: true, volume: formatVolume(voipRingerVolume) }), + playDialer: () => play('dialtone', { loop: true, volume: formatVolume(voipRingerVolume) }), + stopRinger: () => stop('ringtone'), + stopDialer: () => stop('dialtone'), + }; + + const voipSounds = { + playRinger: () => play('telephone', { loop: true, volume: formatVolume(voipRingerVolume) }), + playDialer: () => play('outbound-call-ringing', { loop: true, volume: formatVolume(voipRingerVolume) }), + playCallEnded: () => play('call-ended', { loop: false, volume: formatVolume(voipRingerVolume) }), + stopRinger: () => stop('telephone'), + stopDialer: () => stop('outbound-call-ringing'), + stopCallEnded: () => stop('call-ended'), + stopAll: () => { + stop('telephone'); + stop('outbound-call-ringing'); + stop('call-ended'); + }, + }; + + const notificationSounds = { + playNewRoom: () => play(newRoomNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }), + playNewMessage: () => play(newMessageNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }), + playNewMessageLoop: () => play(newMessageNotification, { loop: true, volume: formatVolume(notificationsSoundVolume) }), + stopNewRoom: () => stop(newRoomNotification), + stopNewMessage: () => stop(newMessageNotification), + }; + + useEffect(() => { + return streamAll('public-info', ([key]) => { + switch (key) { + case 'updateCustomSound': + queryClient.invalidateQueries({ queryKey: ['listCustomSounds'] }); + break; + case 'deleteCustomSound': + queryClient.invalidateQueries({ queryKey: ['listCustomSounds'] }); + + break; + } + }); + }, [queryClient, streamAll]); + + return ( + + ); +}; + +export default CustomSoundProvider; diff --git a/apps/meteor/client/providers/CustomSoundProvider/index.ts b/apps/meteor/client/providers/CustomSoundProvider/index.ts new file mode 100644 index 0000000000000..a2e563004d267 --- /dev/null +++ b/apps/meteor/client/providers/CustomSoundProvider/index.ts @@ -0,0 +1 @@ +export { default } from './CustomSoundProvider'; diff --git a/apps/meteor/client/providers/CustomSoundProvider/lib/helpers.ts b/apps/meteor/client/providers/CustomSoundProvider/lib/helpers.ts new file mode 100644 index 0000000000000..48e955e5dc2ce --- /dev/null +++ b/apps/meteor/client/providers/CustomSoundProvider/lib/helpers.ts @@ -0,0 +1,29 @@ +import type { ICustomSound } from '@rocket.chat/core-typings'; + +import { getURL } from '../../../../app/utils/client'; + +export const getAssetUrl = (asset: string, params?: Record) => getURL(asset, params, undefined, true); + +export const getCustomSoundURL = (sound: ICustomSound) => { + return getAssetUrl(`/custom-sounds/${sound._id}.${sound.extension}`, { _dc: sound.random || 0 }); +}; + +export const defaultSounds: ICustomSound[] = [ + { _id: 'chime', name: 'Chime', extension: 'mp3', src: getAssetUrl('sounds/chime.mp3') }, + { _id: 'door', name: 'Door', extension: 'mp3', src: getAssetUrl('sounds/door.mp3') }, + { _id: 'beep', name: 'Beep', extension: 'mp3', src: getAssetUrl('sounds/beep.mp3') }, + { _id: 'chelle', name: 'Chelle', extension: 'mp3', src: getAssetUrl('sounds/chelle.mp3') }, + { _id: 'ding', name: 'Ding', extension: 'mp3', src: getAssetUrl('sounds/ding.mp3') }, + { _id: 'droplet', name: 'Droplet', extension: 'mp3', src: getAssetUrl('sounds/droplet.mp3') }, + { _id: 'highbell', name: 'Highbell', extension: 'mp3', src: getAssetUrl('sounds/highbell.mp3') }, + { _id: 'seasons', name: 'Seasons', extension: 'mp3', src: getAssetUrl('sounds/seasons.mp3') }, + { _id: 'telephone', name: 'Telephone', extension: 'mp3', src: getAssetUrl('sounds/telephone.mp3') }, + { _id: 'outbound-call-ringing', name: 'Outbound Call Ringing', extension: 'mp3', src: getAssetUrl('sounds/outbound-call-ringing.mp3') }, + { _id: 'call-ended', name: 'Call Ended', extension: 'mp3', src: getAssetUrl('sounds/call-ended.mp3') }, + { _id: 'dialtone', name: 'Dialtone', extension: 'mp3', src: getAssetUrl('sounds/dialtone.mp3') }, + { _id: 'ringtone', name: 'Ringtone', extension: 'mp3', src: getAssetUrl('sounds/ringtone.mp3') }, +]; + +export const formatVolume = (volume: number) => { + return Number((volume / 100).toPrecision(2)); +}; diff --git a/apps/meteor/client/providers/OmnichannelProvider.tsx b/apps/meteor/client/providers/OmnichannelProvider.tsx index cb39b9a4abafb..ee0f9e6fb0e22 100644 --- a/apps/meteor/client/providers/OmnichannelProvider.tsx +++ b/apps/meteor/client/providers/OmnichannelProvider.tsx @@ -6,7 +6,7 @@ import { } from '@rocket.chat/core-typings'; import { useSafely } from '@rocket.chat/fuselage-hooks'; import { createComparatorFromSort } from '@rocket.chat/mongo-adapter'; -import { useUser, useSetting, usePermission, useMethod, useEndpoint, useStream } from '@rocket.chat/ui-contexts'; +import { useUser, useSetting, usePermission, useMethod, useEndpoint, useStream, useCustomSound } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { ReactNode } from 'react'; import { useState, useEffect, useMemo, memo, useRef } from 'react'; @@ -17,7 +17,6 @@ import { getOmniChatSortQuery } from '../../app/livechat/lib/inquiries'; import { ClientLogger } from '../../lib/ClientLogger'; import type { OmnichannelContextValue } from '../contexts/OmnichannelContext'; import { OmnichannelContext } from '../contexts/OmnichannelContext'; -import { useNewRoomNotification } from '../hooks/notification/useNewRoomNotification'; import { useHasLicenseModule } from '../hooks/useHasLicenseModule'; import { useLivechatInquiryStore } from '../hooks/useLivechatInquiryStore'; import { useOmnichannelContinuousSoundNotification } from '../hooks/useOmnichannelContinuousSoundNotification'; @@ -76,7 +75,7 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { const isPrioritiesEnabled = isEnterprise && accessible; const enabled = accessible && !!user && !!routeConfig; - const notifyNewRoom = useNewRoomNotification(); + const { notificationSounds } = useCustomSound(); const { data: { priorities = [] } = {}, @@ -157,10 +156,14 @@ const OmnichannelProvider = ({ children }: OmnichannelProviderProps) => { useEffect(() => { if (lastQueueSize.current < (queue?.length ?? 0)) { - notifyNewRoom(); + notificationSounds.playNewRoom(); } lastQueueSize.current = queue?.length ?? 0; - }, [notifyNewRoom, queue?.length]); + + return () => { + notificationSounds.stopNewRoom(); + }; + }, [notificationSounds, queue?.length]); useOmnichannelContinuousSoundNotification(queue ?? []); diff --git a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx index 153d03dd078dc..3599e3d381d9c 100644 --- a/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx +++ b/apps/meteor/client/views/account/preferences/PreferencesSoundSection.tsx @@ -8,7 +8,7 @@ const PreferencesSoundSection = () => { const t = useTranslation(); const customSound = useCustomSound(); - const soundsList: SelectOption[] = customSound?.getList()?.map((value) => [value._id, value.name]) || []; + const soundsList: SelectOption[] = customSound.list?.map((value) => [value._id, value.name]) || []; const { control, watch } = useFormContext(); const { newMessageNotification, notificationsSoundVolume = 100, masterVolume = 100, voipRingerVolume = 100 } = watch(); diff --git a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx index 07d8732c5dd80..cbe465a60d7ec 100644 --- a/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/NotificationPreferences/NotificationPreferencesWithData.tsx @@ -20,7 +20,7 @@ const NotificationPreferencesWithData = (): ReactElement => { successMessage: t('Room_updated_successfully'), }); - const customSoundAsset: SelectOption[] | undefined = customSound?.getList()?.map((value) => [value._id, value.name]); + const customSoundAsset: SelectOption[] | undefined = customSound.list?.map((value) => [value._id, value.name]); const defaultOption: SelectOption[] = [ ['default', t('Default')], diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx index d028a2620741e..7ba4d58cbc705 100644 --- a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx +++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx @@ -11,15 +11,13 @@ import { useEffect, useMemo } from 'react'; import { FocusScope } from 'react-aria'; import VideoConfPopup from './VideoConfPopup'; -import { useUserSoundPreferences } from '../../../../../hooks/useUserSoundPreferences'; import VideoConfPopupPortal from '../../../../../portals/VideoConfPopupPortal'; const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): ReactElement => { - const customSound = useCustomSound(); + const { callSounds } = useCustomSound(); const incomingCalls = useVideoConfIncomingCalls(); const isRinging = useVideoConfIsRinging(); const isCalling = useVideoConfIsCalling(); - const { voipRingerVolume } = useUserSoundPreferences(); const popups = useMemo( () => @@ -31,18 +29,18 @@ const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): Re useEffect(() => { if (isRinging) { - customSound.play('ringtone', { loop: true, volume: voipRingerVolume / 100 }); + callSounds.playRinger(); } if (isCalling) { - customSound.play('dialtone', { loop: true, volume: voipRingerVolume / 100 }); + callSounds.playDialer(); } return (): void => { - customSound.stop('ringtone'); - customSound.stop('dialtone'); + callSounds.stopRinger(); + callSounds.stopDialer(); }; - }, [customSound, isRinging, isCalling, voipRingerVolume]); + }, [isRinging, isCalling, callSounds]); return ( <> diff --git a/apps/meteor/tests/e2e/video-conference-ring.spec.ts b/apps/meteor/tests/e2e/video-conference-ring.spec.ts index d5072b00b5cca..d489e1a258ac0 100644 --- a/apps/meteor/tests/e2e/video-conference-ring.spec.ts +++ b/apps/meteor/tests/e2e/video-conference-ring.spec.ts @@ -10,13 +10,11 @@ test.use({ storageState: Users.user1.state }); test.describe('video conference ringing', () => { let poHomeChannel: HomeChannel; - let poAccountProfile: AccountProfile; test.skip(!IS_EE, 'Enterprise Only'); test.beforeEach(async ({ page }) => { poHomeChannel = new HomeChannel(page); - poAccountProfile = new AccountProfile(page); await page.goto('/home'); }); @@ -51,39 +49,4 @@ test.describe('video conference ringing', () => { await expect(poHomeChannel.content.getVideoConfPopupByName('Start a call with user2')).toBeVisible(); }); }); - - const changeCallRingerVolumeFromHome = async (poHomeChannel: HomeChannel, poAccountProfile: AccountProfile, volume: string) => { - await poHomeChannel.sidenav.userProfileMenu.click(); - await poHomeChannel.sidenav.accountPreferencesOption.click(); - - await poAccountProfile.preferencesSoundAccordionOption.click(); - await poAccountProfile.preferencesCallRingerVolumeSlider.fill(volume); - - await poAccountProfile.btnSaveChanges.click(); - await poAccountProfile.btnClose.click(); - }; - - test('should be ringing/dialing according to volume preference', async () => { - await changeCallRingerVolumeFromHome(poHomeChannel, poAccountProfile, '50'); - await changeCallRingerVolumeFromHome(auxContext.poHomeChannel, auxContext.poAccountProfile, '25'); - - await poHomeChannel.sidenav.openChat('user2'); - await auxContext.poHomeChannel.sidenav.openChat('user1'); - - await poHomeChannel.content.btnCall.click(); - await poHomeChannel.content.menuItemVideoCall.click(); - await poHomeChannel.content.btnStartVideoCall.click(); - - await expect(auxContext.poHomeChannel.content.getVideoConfPopupByName('Incoming call from user1')).toBeVisible(); - - const dialToneVolume = await poHomeChannel.audioVideoConfDialtone.evaluate((el: HTMLAudioElement) => el.volume); - const ringToneVolume = await auxContext.poHomeChannel.audioVideoConfRingtone.evaluate((el: HTMLAudioElement) => el.volume); - - expect(dialToneVolume).toBe(0.5); - expect(ringToneVolume).toBe(0.25); - - await auxContext.poHomeChannel.content.btnDeclineVideoCall.click(); - await changeCallRingerVolumeFromHome(poHomeChannel, poAccountProfile, '100'); - await changeCallRingerVolumeFromHome(auxContext.poHomeChannel, auxContext.poAccountProfile, '100'); - }); }); diff --git a/packages/ui-contexts/src/CustomSoundContext.ts b/packages/ui-contexts/src/CustomSoundContext.ts index 30a48b603d5d4..5c3838ba47c6a 100644 --- a/packages/ui-contexts/src/CustomSoundContext.ts +++ b/packages/ui-contexts/src/CustomSoundContext.ts @@ -2,17 +2,67 @@ import type { ICustomSound } from '@rocket.chat/core-typings'; import { createContext } from 'react'; export type CustomSoundContextValue = { - play: (sound: ICustomSound['_id'], options?: { volume?: number; loop?: boolean }) => Promise; + play: ( + soundId: string, + options?: + | { + volume?: number | undefined; + loop?: boolean | undefined; + } + | undefined, + ) => void; pause: (sound: ICustomSound['_id']) => void; stop: (sound: ICustomSound['_id']) => void; - getList: () => ICustomSound[] | undefined; - isPlaying: (sound: ICustomSound['_id']) => boolean | null; + callSounds: { + playRinger: () => void; + playDialer: () => void; + stopRinger: () => void; + stopDialer: () => void; + }; + voipSounds: { + playRinger: () => void; + playDialer: () => void; + playCallEnded: () => void; + stopRinger: () => void; + stopDialer: () => void; + stopCallEnded: () => void; + stopAll: () => void; + }; + notificationSounds: { + playNewRoom: () => void; + playNewMessage: () => void; + playNewMessageLoop: () => void; + stopNewRoom: () => void; + stopNewMessage: () => void; + }; + list: ICustomSound[]; }; export const CustomSoundContext = createContext({ play: () => new Promise(() => undefined), pause: () => undefined, stop: () => undefined, - getList: () => undefined, - isPlaying: () => false, + callSounds: { + playRinger: () => undefined, + playDialer: () => undefined, + stopRinger: () => undefined, + stopDialer: () => undefined, + }, + voipSounds: { + playRinger: () => undefined, + playDialer: () => undefined, + playCallEnded: () => undefined, + stopRinger: () => undefined, + stopDialer: () => undefined, + stopCallEnded: () => undefined, + stopAll: () => undefined, + }, + notificationSounds: { + playNewRoom: () => undefined, + playNewMessage: () => undefined, + playNewMessageLoop: () => undefined, + stopNewRoom: () => undefined, + stopNewMessage: () => undefined, + }, + list: [], }); diff --git a/packages/ui-voip/src/hooks/useVoipSounds.ts b/packages/ui-voip/src/hooks/useVoipSounds.ts deleted file mode 100644 index f08ff00e3da5a..0000000000000 --- a/packages/ui-voip/src/hooks/useVoipSounds.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useCustomSound, useUserPreference } from '@rocket.chat/ui-contexts'; -import { useMemo } from 'react'; - -type VoipSound = 'telephone' | 'outbound-call-ringing' | 'call-ended'; - -export const useVoipSounds = () => { - const { play, pause } = useCustomSound(); - const masterVolume = useUserPreference('masterVolume', 100) || 100; - const voipRingerVolume = useUserPreference('voipRingerVolume', 100) || 100; - const audioVolume = Math.floor((voipRingerVolume * masterVolume) / 100); - - return useMemo( - () => ({ - play: (soundId: VoipSound, loop = true) => { - play(soundId, { - volume: Number((audioVolume / 100).toPrecision(2)), - loop, - }); - }, - stop: (soundId: VoipSound) => pause(soundId), - stopAll: () => { - pause('telephone'); - pause('outbound-call-ringing'); - }, - }), - [play, pause, audioVolume], - ); -}; diff --git a/packages/ui-voip/src/providers/VoipProvider.tsx b/packages/ui-voip/src/providers/VoipProvider.tsx index d6d302fffd0e5..46a0c8b552f87 100644 --- a/packages/ui-voip/src/providers/VoipProvider.tsx +++ b/packages/ui-voip/src/providers/VoipProvider.tsx @@ -1,6 +1,7 @@ import { useEffectEvent, useLocalStorage } from '@rocket.chat/fuselage-hooks'; import type { Device } from '@rocket.chat/ui-contexts'; import { + useCustomSound, usePermission, useSetInputMediaDevice, useSetOutputMediaDevice, @@ -17,7 +18,6 @@ import VoipPopupPortal from '../components/VoipPopupPortal'; import type { VoipContextValue } from '../contexts/VoipContext'; import { VoipContext } from '../contexts/VoipContext'; import { useVoipClient } from '../hooks/useVoipClient'; -import { useVoipSounds } from '../hooks/useVoipSounds'; const VoipProvider = ({ children }: { children: ReactNode }) => { // Settings @@ -29,7 +29,7 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { // Hooks const { t } = useTranslation(); - const voipSounds = useVoipSounds(); + const { voipSounds } = useCustomSound(); const { voipClient, error } = useVoipClient({ enabled: isVoipEnabled, autoRegister: isLocalRegistered, @@ -57,7 +57,8 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { }; const onCallEstablished = async (): Promise => { - voipSounds.stopAll(); + voipSounds.stopDialer(); + voipSounds.stopRinger(); window.addEventListener('beforeunload', onBeforeUnload); }; @@ -68,16 +69,18 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { }; const onOutgoingCallRinging = (): void => { - voipSounds.play('outbound-call-ringing'); + voipSounds.playDialer(); }; const onIncomingCallRinging = (): void => { - voipSounds.play('telephone'); + voipSounds.playRinger(); }; const onCallTerminated = (): void => { - voipSounds.play('call-ended', false); - voipSounds.stopAll(); + voipSounds.playCallEnded(); + voipSounds.stopCallEnded(); + voipSounds.stopDialer(); + voipSounds.stopRinger(); window.removeEventListener('beforeunload', onBeforeUnload); }; From 72725d391e79b44e7380ee2fe640e2e4426c77ca Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 4 Apr 2025 18:02:52 -0300 Subject: [PATCH 069/187] feat: Bring back previous announcement style (#35672) --- .changeset/small-snails-burn.md | 7 ++ .../Announcement/Announcement.stories.tsx | 16 ----- .../views/room/Announcement/Announcement.tsx | 47 ------------- .../client/views/room/Announcement/index.tsx | 1 - .../AnnouncementComponent.tsx | 15 ---- .../RoomAnnouncement.stories.tsx | 3 - .../RoomAnnouncement/RoomAnnouncement.tsx | 52 +++++++++----- .../client/views/room/body/RoomBody.tsx | 20 +++--- .../client/views/room/body/RoomBodyV2.tsx | 31 ++------- .../views/room/body/hooks/useBannerSection.ts | 44 ------------ apps/meteor/package.json | 2 +- apps/uikit-playground/package.json | 2 +- ee/packages/ui-theming/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/package.json | 2 +- packages/ui-avatar/package.json | 2 +- packages/ui-client/package.json | 2 +- .../AnnouncementBanner.spec.tsx | 23 +++++++ .../AnnouncementBanner.stories.tsx | 28 ++++++++ .../AnnouncementBanner/AnnouncementBanner.tsx | 28 ++++---- .../AnnouncementBanner.spec.tsx.snap | 42 ++++++++++++ .../components/AnnouncementBanner/index.ts | 1 + .../components/HeaderV2/Header.stories.tsx | 38 +++++++++-- .../RoomBanner/RoomBanner.stories.tsx | 68 ------------------- .../src/components/RoomBanner/RoomBanner.tsx | 42 ------------ .../RoomBanner/RoomBannerContent.tsx | 16 ----- .../src/components/RoomBanner/index.ts | 2 - packages/ui-client/src/components/index.ts | 2 +- packages/ui-composer/package.json | 2 +- packages/ui-video-conf/package.json | 2 +- packages/ui-voip/package.json | 2 +- yarn.lock | 28 ++++---- 32 files changed, 223 insertions(+), 351 deletions(-) create mode 100644 .changeset/small-snails-burn.md delete mode 100644 apps/meteor/client/views/room/Announcement/Announcement.stories.tsx delete mode 100644 apps/meteor/client/views/room/Announcement/Announcement.tsx delete mode 100644 apps/meteor/client/views/room/Announcement/index.tsx delete mode 100644 apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx delete mode 100644 apps/meteor/client/views/room/body/hooks/useBannerSection.ts create mode 100644 packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.spec.tsx create mode 100644 packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.stories.tsx rename apps/meteor/client/views/room/Announcement/AnnouncementComponent.tsx => packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.tsx (55%) create mode 100644 packages/ui-client/src/components/AnnouncementBanner/__snapshots__/AnnouncementBanner.spec.tsx.snap create mode 100644 packages/ui-client/src/components/AnnouncementBanner/index.ts delete mode 100644 packages/ui-client/src/components/RoomBanner/RoomBanner.stories.tsx delete mode 100644 packages/ui-client/src/components/RoomBanner/RoomBanner.tsx delete mode 100644 packages/ui-client/src/components/RoomBanner/RoomBannerContent.tsx delete mode 100644 packages/ui-client/src/components/RoomBanner/index.ts diff --git a/.changeset/small-snails-burn.md b/.changeset/small-snails-burn.md new file mode 100644 index 0000000000000..47d20011d850f --- /dev/null +++ b/.changeset/small-snails-burn.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/ui-client': minor +'@rocket.chat/meteor': minor +--- + +Restores the previous room announcement style +> This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it diff --git a/apps/meteor/client/views/room/Announcement/Announcement.stories.tsx b/apps/meteor/client/views/room/Announcement/Announcement.stories.tsx deleted file mode 100644 index 383375a72a8b8..0000000000000 --- a/apps/meteor/client/views/room/Announcement/Announcement.stories.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { action } from '@storybook/addon-actions'; -import type { Meta, StoryFn } from '@storybook/react'; - -import Announcement from '.'; - -export default { - title: 'Room/Announcement', - component: Announcement, -} satisfies Meta; - -export const Default: StoryFn = (args) => ; -Default.storyName = 'Announcement'; -Default.args = { - announcement: 'Lorem Ipsum Indolor', - announcementDetails: action('announcementDetails'), -}; diff --git a/apps/meteor/client/views/room/Announcement/Announcement.tsx b/apps/meteor/client/views/room/Announcement/Announcement.tsx deleted file mode 100644 index 1ccc1c5cc217f..0000000000000 --- a/apps/meteor/client/views/room/Announcement/Announcement.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useSetModal } from '@rocket.chat/ui-contexts'; -import type { MouseEvent } from 'react'; -import { useTranslation } from 'react-i18next'; - -import AnnouncementComponent from './AnnouncementComponent'; -import GenericModal from '../../../components/GenericModal'; -import MarkdownText from '../../../components/MarkdownText'; - -type AnnouncementProps = { - announcement: string; - announcementDetails?: () => void; -}; - -const Announcement = ({ announcement, announcementDetails }: AnnouncementProps) => { - const { t } = useTranslation(); - const setModal = useSetModal(); - const closeModal = useEffectEvent(() => setModal(null)); - const handleClick = (e: MouseEvent): void => { - if ((e.target as HTMLAnchorElement).href) { - return; - } - - if (window?.getSelection()?.toString() !== '') { - return; - } - - announcementDetails - ? announcementDetails() - : setModal( - - - - - , - ); - }; - - return announcement ? ( - ): void => handleClick(e)}> - - - ) : null; -}; - -export default Announcement; diff --git a/apps/meteor/client/views/room/Announcement/index.tsx b/apps/meteor/client/views/room/Announcement/index.tsx deleted file mode 100644 index 396eed77f20ee..0000000000000 --- a/apps/meteor/client/views/room/Announcement/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Announcement'; diff --git a/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx b/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx deleted file mode 100644 index 5f5c9b0519f10..0000000000000 --- a/apps/meteor/client/views/room/RoomAnnouncement/AnnouncementComponent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { RoomBanner, RoomBannerContent } from '@rocket.chat/ui-client'; -import type { MouseEvent, ReactNode } from 'react'; - -type AnnouncementComponentProps = { - children: ReactNode; - onClickOpen: (e: MouseEvent) => void; -}; - -const AnnouncementComponent = ({ children, onClickOpen }: AnnouncementComponentProps) => ( - - {children} - -); - -export default AnnouncementComponent; diff --git a/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.stories.tsx b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.stories.tsx index da55c54b06df3..da8818dd919e1 100644 --- a/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.stories.tsx +++ b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.stories.tsx @@ -1,4 +1,3 @@ -import { action } from '@storybook/addon-actions'; import type { Meta, StoryFn } from '@storybook/react'; import RoomAnnouncement from '.'; @@ -9,8 +8,6 @@ export default { } satisfies Meta; export const Default: StoryFn = (args) => ; -Default.storyName = 'Announcement'; Default.args = { announcement: 'Lorem Ipsum Indolor', - announcementDetails: action('announcementDetails'), }; diff --git a/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.tsx b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.tsx index a2dcca82cd37d..e32ef2a9c4974 100644 --- a/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.tsx +++ b/apps/meteor/client/views/room/RoomAnnouncement/RoomAnnouncement.tsx @@ -1,23 +1,38 @@ import { Box } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { AnnouncementBanner } from '@rocket.chat/ui-client'; import { useSetModal } from '@rocket.chat/ui-contexts'; -import type { MouseEvent } from 'react'; +import type { KeyboardEvent, MouseEvent } from 'react'; import { useTranslation } from 'react-i18next'; -import AnnouncementComponent from './AnnouncementComponent'; import GenericModal from '../../../components/GenericModal'; import MarkdownText from '../../../components/MarkdownText'; type RoomAnnouncementParams = { announcement: string; - announcementDetails?: () => void; }; -const RoomAnnouncement = ({ announcement, announcementDetails }: RoomAnnouncementParams) => { +const RoomAnnouncement = ({ announcement }: RoomAnnouncementParams) => { const { t } = useTranslation(); const setModal = useSetModal(); - const closeModal = useEffectEvent(() => setModal(null)); - const handleClick = (e: MouseEvent): void => { + + const handleOpenAnnouncement = useEffectEvent(() => { + setModal( + setModal(null)} + onClose={() => setModal(null)} + > + + + + , + ); + }); + + const handleClick = (e: MouseEvent) => { if ((e.target as HTMLAnchorElement).href) { return; } @@ -26,21 +41,24 @@ const RoomAnnouncement = ({ announcement, announcementDetails }: RoomAnnouncemen return; } - announcementDetails - ? announcementDetails() - : setModal( - - - - - , - ); + handleOpenAnnouncement(); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.target as HTMLAnchorElement).href) { + return; + } + + if (e.code === 'Enter' || e.code === 'Space') { + e.preventDefault(); + handleOpenAnnouncement(); + } }; return announcement ? ( - + - + ) : null; }; diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 6e4e3b3db3e80..2c5e9d3c09a16 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -6,17 +6,21 @@ import { memo, useCallback, useMemo, useRef } from 'react'; import DropTargetOverlay from './DropTargetOverlay'; import JumpToRecentMessageButton from './JumpToRecentMessageButton'; +import LoadingMessagesIndicator from './LoadingMessagesIndicator'; +import RetentionPolicyWarning from './RetentionPolicyWarning'; +import RoomForeword from './RoomForeword/RoomForeword'; +import UnreadMessagesIndicator from './UnreadMessagesIndicator'; +import { UploadProgressContainer, UploadProgressIndicator } from './UploadProgress'; +import { MessageList } from '../MessageList'; +import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import { isTruthy } from '../../../../lib/isTruthy'; import { CustomScrollbars } from '../../../components/CustomScrollbars'; import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; -import Announcement from '../Announcement'; import { BubbleDate } from '../BubbleDate'; -import { MessageList } from '../MessageList'; -import LoadingMessagesIndicator from './LoadingMessagesIndicator'; -import RetentionPolicyWarning from './RetentionPolicyWarning'; -import UnreadMessagesIndicator from './UnreadMessagesIndicator'; import MessageListErrorBoundary from '../MessageList/MessageListErrorBoundary'; +import RoomAnnouncement from '../RoomAnnouncement'; import ComposerContainer from '../composer/ComposerContainer'; +import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop'; import RoomComposer from '../composer/RoomComposer/RoomComposer'; import { useChat } from '../contexts/ChatContext'; import { useRoom, useRoomSubscription, useRoomMessages } from '../contexts/RoomContext'; @@ -24,17 +28,13 @@ import { useRoomToolbox } from '../contexts/RoomToolboxContext'; import { useDateScroll } from '../hooks/useDateScroll'; import { useMessageListNavigation } from '../hooks/useMessageListNavigation'; import { useRetentionPolicy } from '../hooks/useRetentionPolicy'; -import RoomForeword from './RoomForeword/RoomForeword'; -import { UploadProgressContainer, UploadProgressIndicator } from './UploadProgress'; import { useFileUpload } from './hooks/useFileUpload'; import { useGetMore } from './hooks/useGetMore'; import { useGoToHomeOnRemoved } from './hooks/useGoToHomeOnRemoved'; import { useHasNewMessages } from './hooks/useHasNewMessages'; import { useListIsAtBottom } from './hooks/useListIsAtBottom'; import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; -import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; -import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop'; import { useHandleUnread } from './hooks/useUnreadMessages'; const RoomBody = (): ReactElement => { @@ -176,7 +176,7 @@ const RoomBody = (): ReactElement => { return ( <> - {!isLayoutEmbedded && room.announcement && } + {!isLayoutEmbedded && room.announcement && }
{ const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); - const { wrapperRef: sectionWrapperRef, hideSection, innerRef: sectionScrollRef } = useBannerSection(); - const { uploads, handleUploadFiles, @@ -126,7 +122,6 @@ const RoomBody = (): ReactElement => { restoreScrollPositionInnerRef, isAtBottomInnerRef, newMessagesScrollRef, - sectionScrollRef, unreadBarInnerRef, getMoreInnerRef, selectAndScrollRef, @@ -177,25 +172,9 @@ const RoomBody = (): ReactElement => { useReadMessageWindowEvents(); useQuoteMessageByUrl(); - const wrapperStyle = css` - position: absolute; - width: 100%; - z-index: 5; - top: 0px; - - &.animated-hidden { - top: -88px; - } - `; - return ( <> - - - {!isLayoutEmbedded && room.announcement && } - - - + {!isLayoutEmbedded && room.announcement && }
{ - const [hideSection, setHideSection] = useState(false); - - const wrapperBoxRef = useRef(null); - - const innerScrollRef = useCallback((node: HTMLElement | null) => { - if (!node) { - return; - } - let lastScrollTopRef = 0; - - wrapperBoxRef.current?.addEventListener('mouseover', () => setHideSection(false)); - - node.addEventListener( - 'scroll', - withThrottling({ wait: 100 })((event) => { - const bannerSection = wrapperBoxRef.current?.querySelector('.rcx-header-section'); - - if (bannerSection) { - if (isAtBottom(node, 0)) { - setHideSection(false); - } else if (event.target.scrollTop < lastScrollTopRef) { - setHideSection(true); - } else if (!isAtBottom(node, 100) && event.target.scrollTop > parseFloat(getComputedStyle(bannerSection).height)) { - setHideSection(true); - } - } - lastScrollTopRef = event.target.scrollTop; - }), - { passive: true }, - ); - }, []); - - return { - wrapperRef: wrapperBoxRef, - hideSection, - innerRef: innerScrollRef, - }; -}; diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 20ca8f873fc7e..1d22f40b2590f 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -247,7 +247,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/favicon": "workspace:^", "@rocket.chat/freeswitch": "workspace:^", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.35.0", diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index f006e38ca5720..45a2ff690ca72 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -18,7 +18,7 @@ "@lezer/highlight": "^1.2.1", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.35.0", diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index c54cc7730365b..8334489fb0931 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", "@rocket.chat/icons": "^0.42.0", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index f4cf05a166324..538643c69ba68 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -52,7 +52,7 @@ "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/icons": "^0.42.0", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 64f0c2ae9cb91..1d1ba98e220e2 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -30,7 +30,7 @@ "@babel/core": "~7.26.0", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-tokens": "~0.33.2", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/message-parser": "workspace:^", diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index c6c799401595c..c9cfabdbaf666 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@babel/core": "~7.26.0", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/ui-contexts": "workspace:^", "@types/react": "~18.3.17", "@types/react-dom": "~18.3.5", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index b1e7c5610d817..96df27eae6be1 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -21,7 +21,7 @@ "@babel/core": "~7.26.0", "@react-aria/toolbar": "^3.0.0-nightly.5042", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.spec.tsx b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.spec.tsx new file mode 100644 index 0000000000000..0c58ce6d4876a --- /dev/null +++ b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.spec.tsx @@ -0,0 +1,23 @@ +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './AnnouncementBanner.stories'; + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); + +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const tree = render(); + expect(tree.baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(); + + /** + ** We are disabling the rule in this case because ideally we should not have nested interactive + ** but in this case we need to open a modal when clicking in the `AnnouncementBanner` + **/ + const results = await axe(container, { rules: { 'nested-interactive': { enabled: false } } }); + expect(results).toHaveNoViolations(); +}); diff --git a/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.stories.tsx b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.stories.tsx new file mode 100644 index 0000000000000..165c0154d3361 --- /dev/null +++ b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.stories.tsx @@ -0,0 +1,28 @@ +import { action } from '@storybook/addon-actions'; +import { Meta, StoryFn } from '@storybook/react'; + +import AnnouncementBanner from './AnnouncementBanner'; + +export default { + title: 'Components/AnnouncementBanner', + component: AnnouncementBanner, + args: { + onClick: action('clicked'), + }, +} satisfies Meta; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + children: 'Announcement', +}; + +export const WithLink = Template.bind({}); +WithLink.args = { + children: ( + + Announcement + + ), +}; diff --git a/apps/meteor/client/views/room/Announcement/AnnouncementComponent.tsx b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.tsx similarity index 55% rename from apps/meteor/client/views/room/Announcement/AnnouncementComponent.tsx rename to packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.tsx index 6772d1d9c6ed9..fcdd254f75853 100644 --- a/apps/meteor/client/views/room/Announcement/AnnouncementComponent.tsx +++ b/packages/ui-client/src/components/AnnouncementBanner/AnnouncementBanner.tsx @@ -1,13 +1,13 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Palette } from '@rocket.chat/fuselage'; -import type { MouseEvent, ReactNode } from 'react'; +import type { AllHTMLAttributes, ReactNode, MouseEvent } from 'react'; -type AnnouncementComponentProps = { - children?: ReactNode; - onClickOpen: (e: MouseEvent) => void; -}; +type AnnouncementBannerProps = { + children: ReactNode; + onClick?: (e: MouseEvent) => void; +} & Omit, 'is'>; -const AnnouncementComponent = ({ children, onClickOpen }: AnnouncementComponentProps) => { +const AnnouncementBanner = ({ children, className, onClick, ...props }: AnnouncementBannerProps) => { const announcementBar = css` background-color: ${Palette.status['status-background-info'].theme('announcement-background')}; color: ${Palette.text['font-pure-black'].theme('announcement-text')}; @@ -20,28 +20,32 @@ const AnnouncementComponent = ({ children, onClickOpen }: AnnouncementComponentP > * { flex: auto; } - &:hover, - &:focus { + &:hover { text-decoration: underline; } `; return ( - + {children} ); }; -export default AnnouncementComponent; +export default AnnouncementBanner; diff --git a/packages/ui-client/src/components/AnnouncementBanner/__snapshots__/AnnouncementBanner.spec.tsx.snap b/packages/ui-client/src/components/AnnouncementBanner/__snapshots__/AnnouncementBanner.spec.tsx.snap new file mode 100644 index 0000000000000..d2fc70f9f1cc3 --- /dev/null +++ b/packages/ui-client/src/components/AnnouncementBanner/__snapshots__/AnnouncementBanner.spec.tsx.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders Default without crashing 1`] = ` + +
+
+
+ Announcement +
+
+
+ +`; + +exports[`renders WithLink without crashing 1`] = ` + +
+ +
+ +`; diff --git a/packages/ui-client/src/components/AnnouncementBanner/index.ts b/packages/ui-client/src/components/AnnouncementBanner/index.ts new file mode 100644 index 0000000000000..22e294d3a3e3f --- /dev/null +++ b/packages/ui-client/src/components/AnnouncementBanner/index.ts @@ -0,0 +1 @@ +export { default } from './AnnouncementBanner'; diff --git a/packages/ui-client/src/components/HeaderV2/Header.stories.tsx b/packages/ui-client/src/components/HeaderV2/Header.stories.tsx index 6121f306ea103..2438dfb90bb7a 100644 --- a/packages/ui-client/src/components/HeaderV2/Header.stories.tsx +++ b/packages/ui-client/src/components/HeaderV2/Header.stories.tsx @@ -1,5 +1,5 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { Avatar } from '@rocket.chat/fuselage'; +import { Avatar, Box } from '@rocket.chat/fuselage'; import { SettingsContext } from '@rocket.chat/ui-contexts'; import { action } from '@storybook/addon-actions'; import type { Meta } from '@storybook/react'; @@ -17,8 +17,7 @@ import { HeaderV2Title as HeaderTitle, HeaderV2State as HeaderState, } from '.'; -import { RoomBanner } from '../RoomBanner'; -import { RoomBannerContent } from '../RoomBanner/RoomBannerContent'; +import AnnouncementBanner from '../AnnouncementBanner'; const avatarUrl = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z'; @@ -73,6 +72,8 @@ const room: IRoom = { t: 'c', name: 'general general general general general general general general general general general general general general general general general general general', _id: 'GENERAL', + topic: 'lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum', + announcement: 'Announcement', encrypted: true, autoTranslate: true, autoTranslateLanguage: 'pt-BR', @@ -163,7 +164,7 @@ export const WithActionBadge = () => (
); -export const WithTopicBanner = () => ( +export const WithTopic = () => ( <>
@@ -176,6 +177,7 @@ export const WithTopicBanner = () => ( + {room.topic} @@ -184,8 +186,30 @@ export const WithTopicBanner = () => (
- - Topic {room.name} - + +); + +export const WithAnnouncement = () => ( + <> +
+ + + + + + {icon && } + {room.name} + + + + + + + + + + +
+ {room.announcement} ); diff --git a/packages/ui-client/src/components/RoomBanner/RoomBanner.stories.tsx b/packages/ui-client/src/components/RoomBanner/RoomBanner.stories.tsx deleted file mode 100644 index 90e14b18b3e2a..0000000000000 --- a/packages/ui-client/src/components/RoomBanner/RoomBanner.stories.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Avatar, Box, IconButton } from '@rocket.chat/fuselage'; -import { ComponentProps } from 'react'; - -import { RoomBanner } from './RoomBanner'; -import { RoomBannerContent } from './RoomBannerContent'; - -export default { - title: 'Components/RoomBanner', - component: RoomBanner, -}; -const avatarUrl = - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAoACgDASIAAhEBAxEB/8QAGwAAAgIDAQAAAAAAAAAAAAAAAAcEBgIDBQj/xAAuEAACAQQAAwcEAQUAAAAAAAABAgMABAUREiExBhMUIkFRYQcWcYGhFTJSgpH/xAAYAQADAQEAAAAAAAAAAAAAAAACAwQBAP/EAB4RAAIBBQEBAQAAAAAAAAAAAAABAgMREiExE0HR/9oADAMBAAIRAxEAPwBuXuIkhBuMe5ib/AHQP49q4L3mLitryTLTSpOiHQI5k/HzXa/qbFOEudVTu1dumWvcTaNCZYZ7vU6g6LxqjOU/24dfs1Ouh9FnkMpd3Reeyx83hAxZZEhkdV9/MBrX71WGPvJcqrJBGveKATtuXXqNU0pu02bTHXD/AGvJAluyxxRd6F4x00o+NdKoVrjbzJdvVe1t5cVLc2ck8qjnohgpPtz2v7G6JtPQ2VJwjlcw+37mchpnK6GtIuv5NFWeTsLNPvxWTvpfjvOEfwKKzEVkSct2vscS/BIzSN0YRkeX81UpPqO8masJETu7OOccY4dswYFQeftv096XV5knuJGdm2T1+agvMXj8jEaHX905QihabvcbuS7X566mLWLwSY8PuRnk/u4eZ0deTl71Ef6hY+0yM88TzeNZY4luYwpVYyduOfrvhPTnr0pXSX9y5mCsyJMdyxxvwq599em+taItqCSNc90ChvZRUruUcT0JiO18Elpk7t8v41LWzacxkBSuvjQ/FFJayjDWrCTepAQ2vUH0oo/Jk3ovpwJJeVCP5CN+lFFaaMqy+nAyuChvrTI2kN9JAsi2ZOy4IBHMnkSCP+iqBexSWdxLazoUljJVlPUH2oorkV10pRc7b1zXb/hZOzuJvM86QWEXeELxOzHSIPcmiiiunVlF2RNTpRkrs//Z'; - -const CustomAvatar = (props: Omit, 'url'>) => ; - -export const Default = () => ( - - - Plain text long long long loooooooooooooong loooong loooong loooong loooong loooong loooong teeeeeeext - - - - - Will Bell - - - - -); - -export const WithoutTopic = () => ( - - - - Add topic - - - - - - Will Bell - - - - -); - -export const TopicAndAnnouncement = () => ( -
- - - Topic long long long loooooooooooooong loooong loooong loooong loooong loooong loooong loooong loooong teeeeeeext - - - - - Will Bell - - - - - - - Announcement banner google.com - - -
-); diff --git a/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx b/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx deleted file mode 100644 index 8b9bda9e40207..0000000000000 --- a/packages/ui-client/src/components/RoomBanner/RoomBanner.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { css } from '@rocket.chat/css-in-js'; -import { Box, Divider, Palette } from '@rocket.chat/fuselage'; -import { useLayout } from '@rocket.chat/ui-contexts'; -import { ComponentProps } from 'react'; - -const clickable = css` - cursor: pointer; - &:focus-visible { - outline: ${Palette.stroke['stroke-highlight']} solid 1px; - } -`; - -const style = css` - background-color: ${Palette.surface['surface-room']}; -`; - -export const RoomBanner = ({ onClick, className, ...props }: ComponentProps) => { - const { isMobile } = useLayout(); - - return ( - <> - - - - ); -}; diff --git a/packages/ui-client/src/components/RoomBanner/RoomBannerContent.tsx b/packages/ui-client/src/components/RoomBanner/RoomBannerContent.tsx deleted file mode 100644 index 36b5d74569eab..0000000000000 --- a/packages/ui-client/src/components/RoomBanner/RoomBannerContent.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { css } from '@rocket.chat/css-in-js'; -import { Box, Palette } from '@rocket.chat/fuselage'; -import { HTMLAttributes } from 'react'; - -export const RoomBannerContent = (props: Omit, 'is'>) => ( - -); diff --git a/packages/ui-client/src/components/RoomBanner/index.ts b/packages/ui-client/src/components/RoomBanner/index.ts deleted file mode 100644 index 9c79ab469b624..0000000000000 --- a/packages/ui-client/src/components/RoomBanner/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './RoomBanner'; -export * from './RoomBannerContent'; diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 4b637518ebd5f..dd3d037772a46 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -11,6 +11,6 @@ export * from './Header'; export * from './HeaderV2'; export * from './MultiSelectCustom/MultiSelectCustom'; export * from './FeaturePreview'; -export * from './RoomBanner'; +export { default as AnnouncementBanner } from './AnnouncementBanner'; export { default as UserAutoComplete } from './UserAutoComplete'; export * from './GenericMenu'; diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 37b9132467a60..d51540b92c5e4 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -21,7 +21,7 @@ "@babel/core": "~7.26.0", "@react-aria/toolbar": "^3.0.0-nightly.5042", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/icons": "^0.42.0", "@storybook/addon-actions": "^8.6.4", "@storybook/addon-docs": "^8.6.4", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 5523b6be76cb5..ed5e6c4d6dfff 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -24,7 +24,7 @@ "@babel/core": "~7.26.0", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 80cdebd0b7c5b..d997b22dcb838 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -29,7 +29,7 @@ "@react-spectrum/test-utils": "~1.0.0-alpha.2", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "~0.61.0", + "@rocket.chat/fuselage": "~0.62.0", "@rocket.chat/fuselage-hooks": "~0.35.0", "@rocket.chat/icons": "^0.42.0", "@rocket.chat/jest-presets": "workspace:~", diff --git a/yarn.lock b/yarn.lock index f0ad9f552d329..34c6f079c747f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8183,7 +8183,7 @@ __metadata: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" "@rocket.chat/fuselage-polyfills": "npm:~0.31.25" "@rocket.chat/gazzodown": "workspace:^" @@ -8242,9 +8242,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:~0.61.0": - version: 0.61.0 - resolution: "@rocket.chat/fuselage@npm:0.61.0" +"@rocket.chat/fuselage@npm:~0.62.0": + version: 0.62.0 + resolution: "@rocket.chat/fuselage@npm:0.62.0" dependencies: "@rocket.chat/css-in-js": "npm:^0.31.25" "@rocket.chat/css-supports": "npm:^0.31.25" @@ -8262,7 +8262,7 @@ __metadata: react: "*" react-dom: "*" react-virtuoso: 1.2.4 - checksum: 10/11e391d9fd8191e9e62073629430062365c13a85e406db417a2f06c24589176b83a23ec2abdbd80d5ea51bd8da87d011a9769362104ecc87f20c5eb15df072c3 + checksum: 10/493225ab219172fec6061735dab0e07f265adb991af0e66713a9771089cb917a472bd0147d4f4d48b6950002aef9e562c298288babd6118bc7083cd2681d99fe languageName: node linkType: hard @@ -8273,7 +8273,7 @@ __metadata: "@babel/core": "npm:~7.26.0" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": "npm:~0.31.25" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/message-parser": "workspace:^" @@ -8644,7 +8644,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/favicon": "workspace:^" "@rocket.chat/freeswitch": "workspace:^" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" "@rocket.chat/fuselage-polyfills": "npm:~0.31.25" "@rocket.chat/fuselage-toastbar": "npm:^0.35.0" @@ -9569,7 +9569,7 @@ __metadata: resolution: "@rocket.chat/ui-avatar@workspace:packages/ui-avatar" dependencies: "@babel/core": "npm:~7.26.0" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/ui-contexts": "workspace:^" "@types/react": "npm:~18.3.17" "@types/react-dom": "npm:~18.3.5" @@ -9594,7 +9594,7 @@ __metadata: "@babel/core": "npm:~7.26.0" "@react-aria/toolbar": "npm:^3.0.0-nightly.5042" "@rocket.chat/css-in-js": "npm:~0.31.25" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" @@ -9650,7 +9650,7 @@ __metadata: "@babel/core": "npm:~7.26.0" "@react-aria/toolbar": "npm:^3.0.0-nightly.5042" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/icons": "npm:^0.42.0" "@storybook/addon-actions": "npm:^8.6.4" "@storybook/addon-docs": "npm:^8.6.4" @@ -9741,7 +9741,7 @@ __metadata: resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming" dependencies: "@rocket.chat/css-in-js": "npm:~0.31.25" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/ui-contexts": "workspace:~" @@ -9771,7 +9771,7 @@ __metadata: "@rocket.chat/css-in-js": "npm:~0.31.25" "@rocket.chat/emitter": "npm:~0.31.25" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" @@ -9821,7 +9821,7 @@ __metadata: "@rocket.chat/css-in-js": "npm:~0.31.25" "@rocket.chat/emitter": "npm:~0.31.25" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" "@rocket.chat/icons": "npm:^0.42.0" "@rocket.chat/jest-presets": "workspace:~" @@ -9880,7 +9880,7 @@ __metadata: "@lezer/highlight": "npm:^1.2.1" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": "npm:~0.31.25" - "@rocket.chat/fuselage": "npm:~0.61.0" + "@rocket.chat/fuselage": "npm:~0.62.0" "@rocket.chat/fuselage-hooks": "npm:~0.35.0" "@rocket.chat/fuselage-polyfills": "npm:~0.31.25" "@rocket.chat/fuselage-toastbar": "npm:^0.35.0" From 02fa09f4c00695ff7bacdc75a4d1a1e92a84d35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:05:35 -0300 Subject: [PATCH 070/187] chore: Remove meteor functions from `messagePopupConfig.ts` (#35610) Co-authored-by: Guilherme Gazzo --- .../client/popup/messagePopupConfig.ts | 66 ------------- .../room/providers/ComposerPopupProvider.tsx | 95 ++++++++++++++----- 2 files changed, 71 insertions(+), 90 deletions(-) delete mode 100644 apps/meteor/app/ui-message/client/popup/messagePopupConfig.ts diff --git a/apps/meteor/app/ui-message/client/popup/messagePopupConfig.ts b/apps/meteor/app/ui-message/client/popup/messagePopupConfig.ts deleted file mode 100644 index 1bd9f0171c610..0000000000000 --- a/apps/meteor/app/ui-message/client/popup/messagePopupConfig.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { RoomManager } from '../../../../client/lib/RoomManager'; -import { asReactiveSource } from '../../../../client/lib/tracker'; -import { Messages } from '../../../models/client'; - -type UserFromRoomMessage = { - _id: string; - username: string; - name: string | undefined; - ts: Date; - suggestion?: boolean; -}; - -export const usersFromRoomMessages = new Map(); - -Meteor.startup(() => { - Tracker.autorun(() => { - const uid = Meteor.userId(); - const rid = asReactiveSource( - (cb) => RoomManager.on('changed', cb), - () => RoomManager.opened, - ); - const user = uid ? (Meteor.users.findOne(uid, { fields: { username: 1 } }) as IUser | undefined) : undefined; - if (!rid || !user) { - return; - } - - usersFromRoomMessages.clear(); - - const uniqueMessageUsersControl: Record = {}; - - Messages.find( - { - rid, - 'u.username': { $ne: user.username }, - 't': { $exists: false }, - }, - { - fields: { - 'u.username': 1, - 'u.name': 1, - 'u._id': 1, - 'ts': 1, - }, - sort: { ts: -1 }, - }, - ) - .fetch() - .filter(({ u: { username } }) => { - const notMapped = !uniqueMessageUsersControl[username]; - uniqueMessageUsersControl[username] = true; - return notMapped; - }) - .forEach(({ u: { username, name, _id }, ts }) => { - usersFromRoomMessages.set(_id, { - _id, - username, - name, - ts, - }); - }); - }); -}); diff --git a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx index 42bdb49f43ae8..08f4a60b88737 100644 --- a/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx +++ b/apps/meteor/client/views/room/providers/ComposerPopupProvider.tsx @@ -2,7 +2,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { useMethod, useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useMethod, useSetting, useUserId, useUserPreference } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import type { ReactNode } from 'react'; @@ -10,8 +10,7 @@ import { useTranslation } from 'react-i18next'; import { hasAtLeastOnePermission } from '../../../../app/authorization/client'; import { emoji } from '../../../../app/emoji/client'; -import { Subscriptions } from '../../../../app/models/client'; -import { usersFromRoomMessages } from '../../../../app/ui-message/client/popup/messagePopupConfig'; +import { Messages, Subscriptions } from '../../../../app/models/client'; import { slashCommands } from '../../../../app/utils/client'; import { cannedResponsesQueryKeys } from '../../../lib/queryKeys'; import ComposerBoxPopupCannedResponse from '../composer/ComposerBoxPopupCannedResponse'; @@ -34,6 +33,46 @@ type ComposerPopupProviderProps = { room: IRoom; }; +const getLastRecentUsers = (rid: string, uid: string) => { + const uniqueUsers = new Map< + string, + { + _id: string; + username: string; + name?: string; + ts: Date; + suggestion?: boolean; + } + >(); + Messages.find( + { + rid, + 'u._id': { $ne: uid }, + 't': { $exists: false }, + 'ts': { $exists: true }, + }, + { + fields: { + 'u.username': 1, + 'u.name': 1, + 'u._id': 1, + 'ts': 1, + }, + sort: { ts: -1 }, + }, + ).forEach(({ u: { username, name, _id }, ts }) => { + if (!uniqueUsers.has(username)) { + uniqueUsers.set(username, { + _id, + username, + name, + ts, + }); + } + }); + + return Array.from(uniqueUsers.values()); +}; const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) => { const { _id: rid, encrypted: isRoomEncrypted } = room; @@ -53,8 +92,9 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages', false); const encrypted = isRoomEncrypted && e2eEnabled && !unencryptedMessagesAllowed; const queryClient = useQueryClient(); - + const uid = useUserId(); const call = useMethod('getSlashCommandPreviews'); + const value: ComposerPopupContextValue = useMemo(() => { return [ createMessageBoxPopupConfig({ @@ -64,14 +104,18 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = const filterRegex = filter && new RegExp(escapeRegExp(filter), 'i'); const items: ComposerBoxPopupUserProps[] = []; - const users = Array.from(usersFromRoomMessages.values()) - .filter((u) => !!u.ts && (filterRegex ? u.username.match(filterRegex) || u.name?.match(filterRegex) : true)) + const roomMessageUsers = getLastRecentUsers(rid, uid!) + .filter((u) => { + if (!filterRegex) return true; + return filterRegex.test(u.username) || (u.name && filterRegex.test(u.name)); + }) .sort((a, b) => b.ts.getTime() - a.ts.getTime()) .slice(0, suggestionsCount ?? 5) - .map((u) => { - u.suggestion = true; - return u; - }); + .map((u) => ({ + ...u, + suggestion: true, + })); + if (!filterRegex || filterRegex.test('all')) { items.push({ _id: 'all', @@ -92,17 +136,19 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = }); } - return [...users, ...items]; + return [...roomMessageUsers, ...items]; }, getItemsFromServer: async (filter: string) => { const filterRegex = filter && new RegExp(escapeRegExp(filter), 'i'); - const usernames = Array.from(usersFromRoomMessages.values()) - .filter((u) => !!u.ts && (filterRegex ? u.username.match(filterRegex) || u.name?.match(filterRegex) : true)) + const usernames = getLastRecentUsers(rid, uid!) + .filter((u) => { + if (!filterRegex) return true; + return filterRegex.test(u.username) || (u.name && filterRegex.test(u.name)); + }) .sort((a, b) => b.ts.getTime() - a.ts.getTime()) .slice(0, suggestionsCount ?? 5) - .map((u) => { - return u.username; - }); + .map((u) => u.username); + const { users = [] } = await userSpotlight(filter, usernames, { users: true, mentions: true }, rid); return users.map(({ _id, username, nickname, name, status, avatarETag, outside }) => { @@ -359,19 +405,20 @@ const ComposerPopupProvider = ({ children, room }: ComposerPopupProviderProps) = }), ].filter(Boolean); }, [ - t, - useEmoji, - encrypted, + call, cannedResponseEnabled, + encrypted, + i18n, isOmnichannel, previewTitle, + queryClient, + recentEmojis, + rid, suggestionsCount, + t, + uid, + useEmoji, userSpotlight, - rid, - recentEmojis, - i18n, - queryClient, - call, ]); return ; From 803b485bbf723fb4f7284824258bb4eb34e1616c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Mon, 7 Apr 2025 11:10:49 -0300 Subject: [PATCH 071/187] chore: Remove meteor functions from `forceLogout.ts` (#35683) --- apps/meteor/client/startup/forceLogout.ts | 19 ---------------- apps/meteor/client/startup/index.ts | 1 - apps/meteor/client/views/root/AppLayout.tsx | 2 ++ .../client/views/root/hooks/useForceLogout.ts | 22 +++++++++++++++++++ 4 files changed, 24 insertions(+), 20 deletions(-) delete mode 100644 apps/meteor/client/startup/forceLogout.ts create mode 100644 apps/meteor/client/views/root/hooks/useForceLogout.ts diff --git a/apps/meteor/client/startup/forceLogout.ts b/apps/meteor/client/startup/forceLogout.ts deleted file mode 100644 index f882354062cda..0000000000000 --- a/apps/meteor/client/startup/forceLogout.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Session } from 'meteor/session'; -import { Tracker } from 'meteor/tracker'; - -import { sdk } from '../../app/utils/client/lib/SDKClient'; - -Meteor.startup(() => { - Tracker.autorun(() => { - const userId = Meteor.userId(); - - if (!userId) { - return; - } - Session.set('force_logout', false); - sdk.stream('notify-user', [`${userId}/force_logout`], () => { - Session.set('force_logout', true); - }); - }); -}); diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 8a68ba39aa60f..0fcc2beb18968 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -6,7 +6,6 @@ import './audit'; import './callbacks'; import './deviceManagement'; import './e2e'; -import './forceLogout'; import './iframeCommands'; import './incomingMessages'; import './loadMissedMessages'; diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 6617fef33d994..864ba631efee5 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -3,6 +3,7 @@ import { useEffect, Suspense, useSyncExternalStore } from 'react'; import DocumentTitleWrapper from './DocumentTitleWrapper'; import PageLoading from './PageLoading'; import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke'; +import { useForceLogout } from './hooks/useForceLogout'; import { useGoogleTagManager } from './hooks/useGoogleTagManager'; import { useMessageLinkClicks } from './hooks/useMessageLinkClicks'; import { useOTRMessaging } from './hooks/useOTRMessaging'; @@ -65,6 +66,7 @@ const AppLayout = () => { useWebRTC(); useStoreCookiesOnLogin(); useAutoupdate(); + useForceLogout(); const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot); diff --git a/apps/meteor/client/views/root/hooks/useForceLogout.ts b/apps/meteor/client/views/root/hooks/useForceLogout.ts new file mode 100644 index 0000000000000..080dcb9a42377 --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useForceLogout.ts @@ -0,0 +1,22 @@ +import { useUserId, useStream, useSessionDispatch } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +export const useForceLogout = () => { + const userId = useUserId(); + const getNotifyUserStream = useStream('notify-user'); + const setForceLogout = useSessionDispatch('forceLogout'); + + useEffect(() => { + if (!userId) { + return; + } + + setForceLogout(false); + + const unsubscribe = getNotifyUserStream(`${userId}/force_logout`, () => { + setForceLogout(true); + }); + + return unsubscribe; + }, [getNotifyUserStream, setForceLogout, userId]); +}; From 9e8806c1539a6b459a3e6b63d84922cace6b6fdb Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Mon, 7 Apr 2025 13:28:03 -0300 Subject: [PATCH 072/187] fix: RecordList not considering itemCount when value is zero (#35557) --- .../client/lib/lists/RecordList.spec.ts | 138 ++++++++++++++++++ apps/meteor/client/lib/lists/RecordList.ts | 2 +- 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 apps/meteor/client/lib/lists/RecordList.spec.ts diff --git a/apps/meteor/client/lib/lists/RecordList.spec.ts b/apps/meteor/client/lib/lists/RecordList.spec.ts new file mode 100644 index 0000000000000..88d846970f773 --- /dev/null +++ b/apps/meteor/client/lib/lists/RecordList.spec.ts @@ -0,0 +1,138 @@ +import { AsyncStatePhase } from '../asyncState'; +import { RecordList } from './RecordList'; // Adjust the import path if necessary + +type TestItem = { + _id: string; + _updatedAt?: Date; +}; + +describe('RecordList', () => { + let recordList: RecordList; + + beforeEach(() => { + recordList = new RecordList(); + recordList.emit = jest.fn(); + }); + + test('should initialize with loading phase', () => { + expect(recordList.phase).toBe(AsyncStatePhase.LOADING); + expect(recordList.items).toEqual([]); + }); + + it('should insert a new item and emit an "inserted" event', async () => { + const item = { _id: '1', _updatedAt: new Date() }; + await recordList.handle(item); + + expect(recordList.items).toContainEqual(item); + expect(recordList.emit).toHaveBeenCalledWith('1/inserted', item); + }); + + it('should update an existing item and emit an "updated" event', async () => { + const item = { _id: '1', _updatedAt: new Date() }; + await recordList.handle(item); + + const updatedItem = { _id: '1', _updatedAt: new Date() }; + await recordList.handle(updatedItem); + + expect(recordList.items).toContainEqual(updatedItem); + expect(recordList.items.length).toBe(1); + expect(recordList.emit).toHaveBeenCalledWith('1/updated', updatedItem); + }); + + it('should delete an item and emit a "deleted" event', async () => { + const item = { _id: '1', _updatedAt: new Date() }; + await recordList.handle(item); + await recordList.remove('1'); + + expect(recordList.items).not.toContainEqual(item); + expect(recordList.emit).toHaveBeenCalledWith('1/deleted'); + }); + + it('should emit "errored" event if an error occurs during mutation', async () => { + const error = new Error('Mutation error'); + const getInfo = jest.fn().mockRejectedValue(error); + + await recordList.batchHandle(getInfo); + + expect(recordList.emit).toHaveBeenCalledWith('errored', error); + }); + + test('should batch handle multiple items', async () => { + const changes = { + items: [ + { _id: '1', _updatedAt: new Date() }, + { _id: '2', _updatedAt: new Date() }, + ], + itemCount: 2, + }; + + const getInfo = jest.fn().mockResolvedValue(changes); + + await recordList.batchHandle(getInfo); + + expect(recordList.items).toEqual(changes.items); + expect(recordList.itemCount).toBe(changes.itemCount); + expect(recordList.emit).toHaveBeenCalledWith('1/inserted', changes.items[0]); + expect(recordList.emit).toHaveBeenCalledWith('2/inserted', changes.items[1]); + expect(recordList.emit).toHaveBeenCalledWith('mutated', true); + }); + + test('should fallback to index count if itemCount is not present', async () => { + const batchData = async () => ({ + items: [ + { _id: '1', _updatedAt: new Date() }, + { _id: '2', _updatedAt: new Date() }, + ], + }); + await recordList.batchHandle(batchData); + expect(recordList.itemCount).toBe(2); + }); + + test('should consider itemCount even if value is 0', async () => { + const batchData = async () => ({ + items: [ + { _id: '1', _updatedAt: new Date() }, + { _id: '2', _updatedAt: new Date() }, + ], + itemCount: 0, + }); + await recordList.batchHandle(batchData); + expect(recordList.itemCount).toBe(0); + }); + + test('should clear all items and emit cleared event', async () => { + const item = { _id: '1', _updatedAt: new Date() }; + + await await recordList.handle(item); + await recordList.clear(); + + expect(recordList.items).toEqual([]); + expect(recordList.itemCount).toBe(0); + expect(recordList.items.length).toBe(0); + expect(recordList.emit).toHaveBeenCalledWith('cleared'); + }); + + it('should prune items based on match criteria and emit delete events', async () => { + const item1 = { _id: '1', _updatedAt: new Date() }; + const item2 = { _id: '2', _updatedAt: new Date() }; + await await recordList.handle(item1); + await await recordList.handle(item2); + + const matchCriteria = (item: TestItem) => item._id === '1'; + await recordList.prune(matchCriteria); + + expect(recordList.items).not.toContainEqual(item1); + expect(recordList.emit).toHaveBeenCalledWith('1/deleted'); + expect(recordList.items).toContainEqual(item2); + }); + + test('should sort items based on _updatedAt', async () => { + const oldItem = { _id: '2', _updatedAt: new Date(Date.now() - 1000) }; + await await recordList.handle(oldItem); + + const newItem = { _id: '1', _updatedAt: new Date() }; + await await recordList.handle(newItem); + + expect(recordList.items[0]).toBe(newItem); + }); +}); diff --git a/apps/meteor/client/lib/lists/RecordList.ts b/apps/meteor/client/lib/lists/RecordList.ts index d4f802f3267b6..fed03c272e85f 100644 --- a/apps/meteor/client/lib/lists/RecordList.ts +++ b/apps/meteor/client/lib/lists/RecordList.ts @@ -121,7 +121,7 @@ export class RecordList extends Em } } - if (info.itemCount) { + if (Number.isInteger(info.itemCount)) { this.#itemCount = info.itemCount; this.#hasChanges = true; } From eb55cdc8b6d5833b905e74eab60941c3190eceda Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:13:44 -0300 Subject: [PATCH 073/187] chore: add option to enable sip.js debug logs (#35677) --- packages/ui-voip/src/lib/VoipClient.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ui-voip/src/lib/VoipClient.ts b/packages/ui-voip/src/lib/VoipClient.ts index 7f8aa0e5e5393..86850aa9c93fb 100644 --- a/packages/ui-voip/src/lib/VoipClient.ts +++ b/packages/ui-voip/src/lib/VoipClient.ts @@ -66,6 +66,9 @@ class VoipClient extends Emitter { peerConnectionConfiguration: { iceServers }, }; + const searchParams = new URLSearchParams(window.location.search); + const debug = Boolean(searchParams.get('debug') || searchParams.get('debug-voip')); + this.userAgent = new UserAgent({ authorizationPassword: authPassword, authorizationUsername: authUserName, @@ -73,7 +76,7 @@ class VoipClient extends Emitter { transportOptions, sessionDescriptionHandlerFactoryOptions: sdpFactoryOptions, logConfiguration: false, - logLevel: 'error', + logLevel: debug ? 'debug' : 'error', delegate: { onInvite: this.onIncomingCall, onRefer: this.onTransferedCall, From 212af41e9fba4c617c4e4e71ed03bfea5561fcdb Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 8 Apr 2025 10:44:52 -0300 Subject: [PATCH 074/187] fix: allow to load dynamic rest endpoint (#35715) Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> --- .changeset/serious-grapes-smell.md | 5 + apps/meteor/app/api/server/router.ts | 158 ++++++++++++--------------- 2 files changed, 77 insertions(+), 86 deletions(-) create mode 100644 .changeset/serious-grapes-smell.md diff --git a/.changeset/serious-grapes-smell.md b/.changeset/serious-grapes-smell.md new file mode 100644 index 0000000000000..ad30efee76014 --- /dev/null +++ b/.changeset/serious-grapes-smell.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue with dynamic API routes requiring a server restart to be operable. diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index cfb0104de899f..514e86ecc872d 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -56,9 +56,17 @@ export class Router< [x: string]: unknown; } = NonNullable, > { - private middleware: (router: express.Router) => void = () => void 0; + public router; - constructor(readonly base: TBasePath) {} + private innerRouter: express.Router; + + constructor(readonly base: TBasePath) { + // eslint-disable-next-line new-cap + this.router = express.Router(); + // eslint-disable-next-line new-cap + this.innerRouter = express.Router(); + this.router.use(this.base, this.innerRouter); + } public typedRoutes: Record> = {}; @@ -131,79 +139,75 @@ export class Router< > { const [middlewares, action] = splitArray(actions); - const prev = this.middleware; - this.middleware = (router: express.Router) => { - prev(router); - router[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), ...middlewares, async (req, res) => { - if (options.query) { - const validatorFn = options.query; - if (typeof options.query === 'function' && !validatorFn(req.query)) { - return res.status(400).json({ - success: false, - errorType: 'error-invalid-params', - error: validatorFn.errors?.map((error: any) => error.message).join('\n '), - }); - } + this.innerRouter[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), ...middlewares, async (req, res) => { + if (options.query) { + const validatorFn = options.query; + if (typeof options.query === 'function' && !validatorFn(req.query)) { + return res.status(400).json({ + success: false, + errorType: 'error-invalid-params', + error: validatorFn.errors?.map((error: any) => error.message).join('\n '), + }); } + } - if (options.body) { - const validatorFn = options.body; - if (typeof options.body === 'function' && !validatorFn((req as any).bodyParams || req.body)) { - return res.status(400).json({ - success: false, - errorType: 'error-invalid-params', - error: validatorFn.errors?.map((error: any) => error.message).join('\n '), - }); - } + if (options.body) { + const validatorFn = options.body; + if (typeof options.body === 'function' && !validatorFn((req as any).bodyParams || req.body)) { + return res.status(400).json({ + success: false, + errorType: 'error-invalid-params', + error: validatorFn.errors?.map((error: any) => error.message).join('\n '), + }); } + } - const { - body, - statusCode = 200, - headers = {}, - } = await action.apply( - { - urlParams: req.params, - queryParams: req.query, - bodyParams: (req as any).bodyParams || req.body, - request: req, - response: res, - } as any, - [req], - ); - if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE) { - const responseValidatorFn = options?.response?.[statusCode]; - if (!responseValidatorFn && options.typed) { - throw new Error(`Missing response validator for endpoint ${req.method} - ${req.url} with status code ${statusCode}`); - } - if (responseValidatorFn && !responseValidatorFn(body) && options.typed) { - throw new Error( - `Invalid response for endpoint ${req.method} - ${req.url}. Error: ${responseValidatorFn.errors?.map((error: any) => error.message).join('\n ')}`, - ); - } + const { + body, + statusCode = 200, + headers = {}, + } = await action.apply( + { + urlParams: req.params, + queryParams: req.query, + bodyParams: (req as any).bodyParams || req.body, + request: req, + response: res, + } as any, + [req], + ); + if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE) { + const responseValidatorFn = options?.response?.[statusCode]; + if (!responseValidatorFn && options.typed) { + throw new Error(`Missing response validator for endpoint ${req.method} - ${req.url} with status code ${statusCode}`); + } + if (responseValidatorFn && !responseValidatorFn(body) && options.typed) { + throw new Error( + `Invalid response for endpoint ${req.method} - ${req.url}. Error: ${responseValidatorFn.errors?.map((error: any) => error.message).join('\n ')}`, + ); } + } - const responseHeaders = Object.fromEntries( - Object.entries({ - ...res.header, - 'Content-Type': 'application/json', - 'Cache-Control': 'no-store', - 'Pragma': 'no-cache', - ...headers, - }).map(([key, value]) => [key.toLowerCase(), value]), - ); + const responseHeaders = Object.fromEntries( + Object.entries({ + ...res.header, + 'Content-Type': 'application/json', + 'Cache-Control': 'no-store', + 'Pragma': 'no-cache', + ...headers, + }).map(([key, value]) => [key.toLowerCase(), value]), + ); - res.writeHead(statusCode, responseHeaders); + res.writeHead(statusCode, responseHeaders); - if (responseHeaders['content-type']?.match(/json|javascript/) !== null) { - body !== undefined && res.write(JSON.stringify(body)); - } else { - body !== undefined && res.write(body); - } + if (responseHeaders['content-type']?.match(/json|javascript/) !== null) { + body !== undefined && res.write(JSON.stringify(body)); + } else { + body !== undefined && res.write(body); + } - res.end(); - }); - }; + res.end(); + }); this.registerTypedRoutes(method, subpath, options); return this; } @@ -281,31 +285,13 @@ export class Router< ...Object.fromEntries(Object.entries(innerRouter.typedRoutes).map(([path, routes]) => [`${this.base}${path}`, routes])), }; - const prev = this.middleware; - this.middleware = (router: express.Router) => { - prev(router); - router.use(innerRouter.router); - }; + this.innerRouter.use(innerRouter.router); } if (typeof innerRouter === 'function') { - const prev = this.middleware; - this.middleware = (router: express.Router) => { - prev(router); - router.use(innerRouter); - }; + this.innerRouter.use(innerRouter); } return this as any; } - - get router(): express.Router { - // eslint-disable-next-line new-cap - const router = express.Router(); - // eslint-disable-next-line new-cap - const innerRouter = express.Router(); - this.middleware(innerRouter); - router.use(this.base, innerRouter); - return router; - } } type Prettify = { From 409c6d847a3a476c35c382570e964514483b1118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:21:17 -0300 Subject: [PATCH 075/187] refactor: remove calendar konchatNotifications from meteor (#35714) --- .../useNotificationUserCalendar.ts | 44 +++++++++++++++++ apps/meteor/client/startup/index.ts | 1 - .../client/startup/notifications/index.ts | 1 - .../notifications/konchatNotifications.ts | 47 ------------------- apps/meteor/client/views/root/AppLayout.tsx | 2 + 5 files changed, 46 insertions(+), 49 deletions(-) create mode 100644 apps/meteor/client/hooks/notification/useNotificationUserCalendar.ts delete mode 100644 apps/meteor/client/startup/notifications/index.ts delete mode 100644 apps/meteor/client/startup/notifications/konchatNotifications.ts diff --git a/apps/meteor/client/hooks/notification/useNotificationUserCalendar.ts b/apps/meteor/client/hooks/notification/useNotificationUserCalendar.ts new file mode 100644 index 0000000000000..bb7d497194d1d --- /dev/null +++ b/apps/meteor/client/hooks/notification/useNotificationUserCalendar.ts @@ -0,0 +1,44 @@ +import type { ICalendarNotification } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useSetting, useStream, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { imperativeModal } from '../../lib/imperativeModal'; +import OutlookCalendarEventModal from '../../views/outlookCalendar/OutlookCalendarEventModal'; + +export const useNotificationUserCalendar = () => { + const user = useUser(); + const requireInteraction = useUserPreference('desktopNotificationRequireInteraction'); + const outLookEnabled = useSetting('Outlook_Calendar_Enabled'); + const notifyUserStream = useStream('notify-user'); + + const notifyUserCalendar = useEffectEvent(async (notification: ICalendarNotification) => { + if (!user || user.status === 'busy') { + return; + } + + const n = new Notification(notification.title, { + body: notification.text, + tag: notification.payload._id, + silent: true, + requireInteraction, + } as NotificationOptions); + + n.onclick = function () { + this.close(); + window.focus(); + imperativeModal.open({ + component: OutlookCalendarEventModal, + props: { id: notification.payload._id, onClose: imperativeModal.close, onCancel: imperativeModal.close }, + }); + }; + }); + + useEffect(() => { + if (!user?._id || !outLookEnabled) { + return; + } + + return notifyUserStream(`${user._id}/calendar`, notifyUserCalendar); + }, [notifyUserCalendar, notifyUserStream, outLookEnabled, user?._id]); +}; diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 0fcc2beb18968..4089326d356f5 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -12,7 +12,6 @@ import './loadMissedMessages'; import './loginViaQuery'; import './messageObserve'; import './messageTypes'; -import './notifications'; import './reloadRoomAfterLogin'; import './roles'; import './rootUrlChange'; diff --git a/apps/meteor/client/startup/notifications/index.ts b/apps/meteor/client/startup/notifications/index.ts deleted file mode 100644 index a3d13d46c5fbb..0000000000000 --- a/apps/meteor/client/startup/notifications/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './konchatNotifications'; diff --git a/apps/meteor/client/startup/notifications/konchatNotifications.ts b/apps/meteor/client/startup/notifications/konchatNotifications.ts deleted file mode 100644 index 494e6ab08d5ba..0000000000000 --- a/apps/meteor/client/startup/notifications/konchatNotifications.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { IUser, ICalendarNotification } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; -import { lazy } from 'react'; - -import { settings } from '../../../app/settings/client'; -import { getUserPreference } from '../../../app/utils/client'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; -import { imperativeModal } from '../../lib/imperativeModal'; - -const OutlookCalendarEventModal = lazy(() => import('../../views/outlookCalendar/OutlookCalendarEventModal')); - -Meteor.startup(() => { - const notifyUserCalendar = async function (notification: ICalendarNotification): Promise { - const user = Meteor.user() as IUser | null; - if (!user || user.status === 'busy') { - return; - } - - const requireInteraction = getUserPreference(Meteor.userId(), 'desktopNotificationRequireInteraction'); - - const n = new Notification(notification.title, { - body: notification.text, - tag: notification.payload._id, - silent: true, - requireInteraction, - } as NotificationOptions); - - n.onclick = function () { - this.close(); - window.focus(); - imperativeModal.open({ - component: OutlookCalendarEventModal, - props: { id: notification.payload._id, onClose: imperativeModal.close, onCancel: imperativeModal.close }, - }); - }; - }; - - Tracker.autorun(() => { - if (!Meteor.userId() || !settings.get('Outlook_Calendar_Enabled')) { - sdk.stop('notify-user', `${Meteor.userId()}/calendar`); - return; - } - - sdk.stream('notify-user', [`${Meteor.userId()}/calendar`], notifyUserCalendar); - }); -}); diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 864ba631efee5..fafb6a9f3176d 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -23,6 +23,7 @@ import { useLivechatEnterprise } from '../../../app/livechat-enterprise/hooks/us import { useNextcloud } from '../../../app/nextcloud/client/useNextcloud'; import { useTokenPassAuth } from '../../../app/tokenpass/client/hooks/useTokenPassAuth'; import { useNotificationPermission } from '../../hooks/notification/useNotificationPermission'; +import { useNotificationUserCalendar } from '../../hooks/notification/useNotificationUserCalendar'; import { useNotifyUser } from '../../hooks/notification/useNotifyUser'; import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking'; import { useAutoupdate } from '../../hooks/useAutoupdate'; @@ -67,6 +68,7 @@ const AppLayout = () => { useStoreCookiesOnLogin(); useAutoupdate(); useForceLogout(); + useNotificationUserCalendar(); const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot); From 939cc28235e50dabd991f6a229dc8fc3085951f4 Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Tue, 8 Apr 2025 13:01:35 -0300 Subject: [PATCH 076/187] chore: Only public channels cannot be previewed by permissions (#35611) --- apps/meteor/client/views/room/hooks/useOpenRoom.ts | 4 ++-- apps/meteor/tests/e2e/embedded-layout.spec.ts | 3 +-- apps/meteor/tests/e2e/preview-public-channel.spec.ts | 12 +++++++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/views/room/hooks/useOpenRoom.ts b/apps/meteor/client/views/room/hooks/useOpenRoom.ts index 8faad251dc8b0..3c2da5c0d01ee 100644 --- a/apps/meteor/client/views/room/hooks/useOpenRoom.ts +++ b/apps/meteor/client/views/room/hooks/useOpenRoom.ts @@ -1,4 +1,4 @@ -import { isOmnichannelRoom, type IRoom, type RoomType } from '@rocket.chat/core-typings'; +import { isPublicRoom, type IRoom, type RoomType } from '@rocket.chat/core-typings'; import { useMethod, usePermission, useRoute, useSetting, useUser } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect } from 'react'; @@ -90,7 +90,7 @@ export function useOpenRoom({ type, reference }: { type: RoomType; reference: st const sub = Subscriptions.findOne({ rid: room._id }); // if user doesn't exist at this point, anonymous read is enabled, otherwise an error would have been thrown - if (user && !sub && !hasPreviewPermission && !isOmnichannelRoom(room)) { + if (user && !sub && !hasPreviewPermission && isPublicRoom(room)) { throw new NotSubscribedToRoomError(undefined, { rid: room._id }); } diff --git a/apps/meteor/tests/e2e/embedded-layout.spec.ts b/apps/meteor/tests/e2e/embedded-layout.spec.ts index 380a740b5e542..9561dadf7302b 100644 --- a/apps/meteor/tests/e2e/embedded-layout.spec.ts +++ b/apps/meteor/tests/e2e/embedded-layout.spec.ts @@ -74,8 +74,7 @@ test.describe.serial('embedded-layout', () => { }); }); - // TODO: Fix intermittent failure where direct messages sometimes shows "channel not joined" screen - test.fixme('direct message', () => { + test.describe('direct message', () => { test('should allow sending messages', async ({ page, api }) => { await createDirectMessage(api); await page.goto('/home'); diff --git a/apps/meteor/tests/e2e/preview-public-channel.spec.ts b/apps/meteor/tests/e2e/preview-public-channel.spec.ts index fd24a2aae6202..774ab2d2ab782 100644 --- a/apps/meteor/tests/e2e/preview-public-channel.spec.ts +++ b/apps/meteor/tests/e2e/preview-public-channel.spec.ts @@ -2,7 +2,7 @@ import { IS_EE } from './config/constants'; import { Users } from './fixtures/userStates'; import { HomeChannel, Utils } from './page-objects'; import { Directory } from './page-objects/directory'; -import { createTargetChannel, sendTargetChannelMessage } from './utils'; +import { createDirectMessage, createTargetChannel, sendTargetChannelMessage } from './utils'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); @@ -44,6 +44,16 @@ test.describe('Preview public channel', () => { await expect(poHomeChannel.content.lastUserMessageBody).toContainText(targetChannelMessage); }); + test('should let user view direct rooms', async ({ api }) => { + await api.post('/permissions.update', { permissions: [{ _id: 'preview-c-room', roles: ['admin'] }] }); + await createDirectMessage(api); + + await poHomeChannel.sidenav.openChat(Users.user2.data.username); + + await expect(poHomeChannel.content.btnJoinChannel).not.toBeVisible(); + await expect(poHomeChannel.composer).toBeEnabled(); + }); + test('should not let user role preview public rooms', async ({ api }) => { await api.post('/permissions.update', { permissions: [{ _id: 'preview-c-room', roles: ['admin'] }] }); From eecdcaf333a6d6548b98b3ee6b06d2aa43a38bf9 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 8 Apr 2025 15:20:54 -0300 Subject: [PATCH 077/187] chore(federation): always load endpoints (#35735) --- apps/meteor/ee/server/local-services/federation/service.ts | 1 - apps/meteor/ee/server/startup/services.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/ee/server/local-services/federation/service.ts b/apps/meteor/ee/server/local-services/federation/service.ts index 0fc952d2b6f20..9e6b331874f1c 100644 --- a/apps/meteor/ee/server/local-services/federation/service.ts +++ b/apps/meteor/ee/server/local-services/federation/service.ts @@ -124,7 +124,6 @@ abstract class AbstractBaseFederationServiceEE extends AbstractFederationService this.bridge.logFederationStartupInfo('Running Federation Enterprise V2'); FederationFactoryEE.removeCEValidators(); await import('./infrastructure/rocket-chat/slash-commands'); - await import('../../api/federation'); } private async stopFederation(): Promise { diff --git a/apps/meteor/ee/server/startup/services.ts b/apps/meteor/ee/server/startup/services.ts index efaf1ab8be684..c54831eb774ea 100644 --- a/apps/meteor/ee/server/startup/services.ts +++ b/apps/meteor/ee/server/startup/services.ts @@ -7,6 +7,7 @@ import { LicenseService } from '../../app/license/server/license.internalService import { OmnichannelEE } from '../../app/livechat-enterprise/server/services/omnichannel.internalService'; import { EnterpriseSettings } from '../../app/settings/server/settings.internalService'; import { FederationServiceEE } from '../local-services/federation/service'; +import '../api/federation'; import { InstanceService } from '../local-services/instance/service'; import { LDAPEEService } from '../local-services/ldap/service'; import { MessageReadsService } from '../local-services/message-reads/service'; From bbd14f84214b4785f2b58cfeb8e9117bdfbf18e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=87=E3=83=AF=E3=83=B3=E3=82=B7=E3=83=A5?= <61188295+Dnouv@users.noreply.github.com> Date: Wed, 9 Apr 2025 01:12:25 +0530 Subject: [PATCH 078/187] feat: Apps "select" setting updater (#35644) Co-authored-by: Douglas Gubert <1810309+d-gubert@users.noreply.github.com> --- .changeset/eighty-wombats-smile.md | 6 + .../definition/accessors/ISettingUpdater.ts | 1 + .../src/server/accessors/SettingUpdater.ts | 51 ++++++++- .../server/accessors/SettingUpdater.spec.ts | 104 ++++++++++++++++++ 4 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 .changeset/eighty-wombats-smile.md create mode 100644 packages/apps-engine/tests/server/accessors/SettingUpdater.spec.ts diff --git a/.changeset/eighty-wombats-smile.md b/.changeset/eighty-wombats-smile.md new file mode 100644 index 0000000000000..23ceecd25050b --- /dev/null +++ b/.changeset/eighty-wombats-smile.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps-engine': minor +'@rocket.chat/meteor': minor +--- + +Adds the ability to dynamically add and remove options from select/multi-select settings in the Apps Engine to support more flexible configuration scenarios by exposing two new methods on the settings API. diff --git a/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts b/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts index 3826286df6c93..9cdc15c7b1464 100644 --- a/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts +++ b/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts @@ -2,4 +2,5 @@ import type { ISetting } from '../settings/ISetting'; export interface ISettingUpdater { updateValue(id: ISetting['id'], value: ISetting['value']): Promise; + updateSelectOptions(id: ISetting['id'], values: ISetting['values']): Promise; } diff --git a/packages/apps-engine/src/server/accessors/SettingUpdater.ts b/packages/apps-engine/src/server/accessors/SettingUpdater.ts index 879c1282ee58a..8af37b4bccf75 100644 --- a/packages/apps-engine/src/server/accessors/SettingUpdater.ts +++ b/packages/apps-engine/src/server/accessors/SettingUpdater.ts @@ -3,23 +3,64 @@ import type { ISetting } from '../../definition/settings'; import type { ProxiedApp } from '../ProxiedApp'; import type { AppSettingsManager } from '../managers'; +/** + * Implementation of ISettingUpdater that provides methods to update app settings. + */ export class SettingUpdater implements ISettingUpdater { constructor( private readonly app: ProxiedApp, private readonly manager: AppSettingsManager, ) {} - public async updateValue(id: ISetting['id'], value: ISetting['value']) { - if (!this.app.getStorageItem().settings[id]) { - return; + /** + * Updates a single setting value + * @param id The setting ID to update + * @param value The new value to set + * @returns Promise that resolves when the update is complete + * @throws Error if the setting doesn't exist + */ + public async updateValue(id: ISetting['id'], value: ISetting['value']): Promise { + const appId = this.app.getID(); + const storageItem = this.app.getStorageItem(); + + if (!storageItem.settings?.[id]) { + throw new Error(`Setting "${id}" not found for app ${appId}`); } - const setting = this.manager.getAppSetting(this.app.getID(), id); + const setting = this.manager.getAppSetting(appId, id); - this.manager.updateAppSetting(this.app.getID(), { + this.manager.updateAppSetting(appId, { ...setting, updatedAt: new Date(), value, }); } + + /** + * Updates the values for a multi-value setting by overwriting them + * @param id The setting ID to update + * @param values The new values to set + * @returns Promise that resolves when the update is complete + * @throws Error if the setting doesn't exist + */ + public async updateSelectOptions(id: ISetting['id'], values: ISetting['values']): Promise { + const appId = this.app.getID(); + const storageItem = this.app.getStorageItem(); + + if (!storageItem.settings?.[id]) { + throw new Error(`Setting "${id}" not found for app ${appId}`); + } + + const setting = this.manager.getAppSetting(appId, id); + + // TODO: This operation completely overwrites existing values + // which could lead to loss of selected values. Consider: + // Adding warning logs when selected value will be removed + + this.manager.updateAppSetting(appId, { + ...setting, + updatedAt: new Date(), + values, // Overwrite the values instead of merging + }); + } } diff --git a/packages/apps-engine/tests/server/accessors/SettingUpdater.spec.ts b/packages/apps-engine/tests/server/accessors/SettingUpdater.spec.ts new file mode 100644 index 0000000000000..ebd982640bb81 --- /dev/null +++ b/packages/apps-engine/tests/server/accessors/SettingUpdater.spec.ts @@ -0,0 +1,104 @@ +import { AsyncTest, Expect, SetupFixture, SpyOn } from 'alsatian'; + +import type { ProxiedApp } from '../../../src/server/ProxiedApp'; +import { SettingUpdater } from '../../../src/server/accessors'; +import type { AppSettingsManager } from '../../../src/server/managers'; +import type { IAppStorageItem } from '../../../src/server/storage'; +import { TestData } from '../../test-data/utilities'; + +export class SettingUpdaterAccessorTestFixture { + private mockStorageItem: IAppStorageItem; + + private mockProxiedApp: ProxiedApp; + + private mockSettingsManager: AppSettingsManager; + + @SetupFixture + public setupFixture() { + // Set up mock storage with test settings + this.mockStorageItem = { + settings: {}, + } as IAppStorageItem; + + this.mockStorageItem.settings.singleValue = TestData.getSetting('singleValue'); + this.mockStorageItem.settings.multiValue = { + ...TestData.getSetting('multiValue'), + values: [ + { key: 'key1', i18nLabel: 'value1' }, + { key: 'key2', i18nLabel: 'value2' }, + ], + }; + + // Mock ProxiedApp + const si = this.mockStorageItem; + this.mockProxiedApp = { + getStorageItem(): IAppStorageItem { + return si; + }, + getID(): string { + return 'test-app-id'; + }, + } as ProxiedApp; + + // Mock AppSettingsManager + this.mockSettingsManager = {} as AppSettingsManager; + this.mockSettingsManager.getAppSetting = (appId: string, settingId: string) => { + return this.mockStorageItem.settings[settingId]; + }; + this.mockSettingsManager.updateAppSetting = (appId: string, setting: any) => { + this.mockStorageItem.settings[setting.id] = setting; + return Promise.resolve(); + }; + + SpyOn(this.mockSettingsManager, 'getAppSetting'); + SpyOn(this.mockSettingsManager, 'updateAppSetting'); + } + + @AsyncTest() + public async updateValueSuccessfully() { + const settingUpdater = new SettingUpdater(this.mockProxiedApp, this.mockSettingsManager); + + await settingUpdater.updateValue('singleValue', 'updated value'); + + Expect(this.mockSettingsManager.updateAppSetting).toHaveBeenCalled(); + Expect(this.mockStorageItem.settings.singleValue.value).toBe('updated value'); + // Verify updatedAt was set + Expect(this.mockStorageItem.settings.singleValue.updatedAt).toBeDefined(); + } + + @AsyncTest() + public async updateValueThrowsErrorForNonExistentSetting() { + const settingUpdater = new SettingUpdater(this.mockProxiedApp, this.mockSettingsManager); + + await Expect(() => settingUpdater.updateValue('nonExistent', 'value')).toThrowErrorAsync(Error, 'Setting "nonExistent" not found for app test-app-id'); + } + + @AsyncTest() + public async updateSelectOptionsSuccessfully() { + const settingUpdater = new SettingUpdater(this.mockProxiedApp, this.mockSettingsManager); + const newValues = [ + { key: 'key3', i18nLabel: 'value3' }, + { key: 'key4', i18nLabel: 'value4' }, + ]; + + await settingUpdater.updateSelectOptions('multiValue', newValues); + + Expect(this.mockSettingsManager.updateAppSetting).toHaveBeenCalled(); + const updatedValues = this.mockStorageItem.settings.multiValue.values; + // Should completely replace old values + Expect((updatedValues ?? []).length).toBe(2); + Expect(updatedValues).toEqual(newValues); + // Verify updatedAt was set + Expect(this.mockStorageItem.settings.multiValue.updatedAt).toBeDefined(); + } + + @AsyncTest() + public async updateSelectOptionsThrowsErrorForNonExistentSetting() { + const settingUpdater = new SettingUpdater(this.mockProxiedApp, this.mockSettingsManager); + + await Expect(() => settingUpdater.updateSelectOptions('nonExistent', [{ key: 'test', i18nLabel: 'value' }])).toThrowErrorAsync( + Error, + 'Setting "nonExistent" not found for app test-app-id', + ); + } +} From 87a15899adfdcb1c04c7a79e2fdf66d2fb27e962 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 8 Apr 2025 17:15:43 -0300 Subject: [PATCH 079/187] chore: add hono as router (#35078) Co-authored-by: Marcos Defendi Co-authored-by: Debdut Chakraborty --- apps/meteor/app/api/server/api.ts | 93 +++------- apps/meteor/app/api/server/definition.ts | 4 +- .../app/api/server/helpers/getLoggedInUser.ts | 5 +- .../meteor/app/api/server/helpers/isWidget.ts | 4 +- .../app/api/server/helpers/parseJsonQuery.ts | 10 +- .../api/server/lib/getUploadFormData.spec.ts | 48 ++++-- .../app/api/server/lib/getUploadFormData.ts | 31 +++- .../meteor/app/api/server/middlewares/cors.ts | 102 +++++------ .../app/api/server/middlewares/honoAdapter.ts | 31 ++++ .../app/api/server/middlewares/logger.ts | 48 +++--- .../app/api/server/middlewares/metrics.ts | 19 +- .../middlewares/remoteAddressMiddleware.ts | 40 +++++ .../app/api/server/middlewares/tracer.ts | 45 ++--- apps/meteor/app/api/server/router.spec.ts | 22 +-- apps/meteor/app/api/server/router.ts | 162 +++++++++++++----- apps/meteor/app/api/server/v1/assets.ts | 4 +- apps/meteor/app/api/server/v1/misc.ts | 4 +- apps/meteor/app/api/server/v1/push.ts | 2 +- apps/meteor/app/api/server/v1/settings.ts | 2 +- apps/meteor/app/api/server/v1/users.ts | 8 +- apps/meteor/app/apps/server/bridges/api.ts | 7 +- .../meteor/app/integrations/server/api/api.js | 44 ++--- .../functions/getModifiedHttpHeaders.ts | 4 +- .../server/lib/deprecationWarningLogger.ts | 7 +- .../imports/server/rest/appearance.ts | 2 +- .../app/livechat/imports/server/rest/sms.ts | 2 +- .../livechat/imports/server/rest/upload.ts | 4 +- .../app/livechat/server/api/lib/livechat.ts | 4 +- .../app/livechat/server/api/v1/integration.ts | 2 +- .../meteor/app/livechat/server/api/v1/room.ts | 2 +- .../meteor-accounts-saml/server/listener.ts | 18 +- .../server/middlewares/license.ts | 14 +- apps/meteor/ee/server/api/index.ts | 1 + apps/meteor/ee/server/api/licenses.ts | 2 +- .../ee/server/apps/communication/rest.ts | 11 +- .../ee/server/apps/communication/uikit.ts | 5 +- apps/meteor/package.json | 3 + apps/meteor/server/main.ts | 2 + apps/meteor/server/oauth2-server/oauth.ts | 11 ++ .../providers/mobex.ts | 3 +- .../providers/twilio.ts | 17 +- .../providers/voxtelesys.ts | 3 +- .../functions/getModifiedHttpHeaders.tests.ts | 12 +- .../providers/twilio.spec.ts | 100 +++++++---- packages/core-typings/src/omnichannel/sms.ts | 4 +- yarn.lock | 26 +++ 46 files changed, 606 insertions(+), 388 deletions(-) create mode 100644 apps/meteor/app/api/server/middlewares/honoAdapter.ts create mode 100644 apps/meteor/app/api/server/middlewares/remoteAddressMiddleware.ts diff --git a/apps/meteor/app/api/server/api.ts b/apps/meteor/app/api/server/api.ts index 0761dec1e1b1d..b603274ab8597 100644 --- a/apps/meteor/app/api/server/api.ts +++ b/apps/meteor/app/api/server/api.ts @@ -7,8 +7,7 @@ import type { JoinPathPattern, Method } from '@rocket.chat/rest-typings'; import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv'; import { wrapExceptions } from '@rocket.chat/tools'; import type { ValidateFunction } from 'ajv'; -import express from 'express'; -import type { Request, Response } from 'express'; +import type express from 'express'; import { Accounts } from 'meteor/accounts-base'; import { DDP } from 'meteor/ddp'; import { DDPCommon } from 'meteor/ddp-common'; @@ -42,6 +41,7 @@ import { parseJsonQuery } from './helpers/parseJsonQuery'; import { cors } from './middlewares/cors'; import { loggerMiddleware } from './middlewares/logger'; import { metricsMiddleware } from './middlewares/metrics'; +import { remoteAddressMiddleware } from './middlewares/remoteAddressMiddleware'; import { tracerSpanMiddleware } from './middlewares/tracer'; import type { Route } from './router'; import { Router } from './router'; @@ -105,34 +105,6 @@ const rateLimiterDictionary: Record< } > = {}; -const getRequestIP = (req: Request): string | null => { - const socket = req.socket || (req.connection as any)?.socket; - const remoteAddress = String( - req.headers['x-real-ip'] || (typeof socket !== 'string' && (socket?.remoteAddress || req.connection?.remoteAddress || null)), - ); - const forwardedFor = String(req.headers['x-forwarded-for']); - - if (!socket) { - return remoteAddress || forwardedFor || null; - } - - const httpForwardedCount = parseInt(String(process.env.HTTP_FORWARDED_COUNT)) || 0; - if (httpForwardedCount <= 0) { - return remoteAddress; - } - - if (!forwardedFor || typeof forwardedFor.valueOf() !== 'string') { - return remoteAddress; - } - - const forwardedForIPs = forwardedFor.trim().split(/\s*,\s*/); - if (httpForwardedCount > forwardedForIPs.length) { - return remoteAddress; - } - - return forwardedForIPs[forwardedForIPs.length - httpForwardedCount]; -}; - const generateConnection = ( ipAddress: string, httpHeaders: Record, @@ -220,7 +192,6 @@ export class APIClass< services: 0, inviteToken: 0, }; - this.router = new Router(`/${this.apiPath}`.replace(/\/$/, '').replaceAll('//', '/')); if (useDefaultAuth) { @@ -408,9 +379,12 @@ export class APIClass< rateLimiterDictionary[objectForRateLimitMatch.route].rateLimiter.increment(objectForRateLimitMatch); const attemptResult = await rateLimiterDictionary[objectForRateLimitMatch.route].rateLimiter.check(objectForRateLimitMatch); const timeToResetAttempsInSeconds = Math.ceil(attemptResult.timeToReset / 1000); - response.setHeader('X-RateLimit-Limit', rateLimiterDictionary[objectForRateLimitMatch.route].options.numRequestsAllowed ?? ''); - response.setHeader('X-RateLimit-Remaining', attemptResult.numInvocationsLeft); - response.setHeader('X-RateLimit-Reset', new Date().getTime() + attemptResult.timeToReset); + response.headers.set( + 'X-RateLimit-Limit', + String(rateLimiterDictionary[objectForRateLimitMatch.route].options.numRequestsAllowed ?? ''), + ); + response.headers.set('X-RateLimit-Remaining', String(attemptResult.numInvocationsLeft)); + response.headers.set('X-RateLimit-Reset', String(new Date().getTime() + attemptResult.timeToReset)); if (!attemptResult.allowed) { throw new Meteor.Error( @@ -494,8 +468,8 @@ export class APIClass< if (options && (!('twoFactorRequired' in options) || !options.twoFactorRequired)) { return; } - const code = request.headers['x-2fa-code'] ? String(request.headers['x-2fa-code']) : undefined; - const method = request.headers['x-2fa-method'] ? String(request.headers['x-2fa-method']) : undefined; + const code = request.headers.get('x-2fa-code') ? String(request.headers.get('x-2fa-code')) : undefined; + const method = request.headers.get('x-2fa-method') ? String(request.headers.get('x-2fa-method')) : undefined; await checkCodeForUser({ user: userId, @@ -781,14 +755,12 @@ export class APIClass< const api = this; (operations[method as keyof Operations] as Record).action = async function _internalRouteActionHandler() { - this.requestIp = getRequestIP(this.request)!; - if (options.authRequired || options.authOrAnonRequired) { - const user = await api.authenticatedRoute(this.request); + const user = await api.authenticatedRoute.call(this, this.request); this.user = user!; - this.userId = String(this.request.headers['x-user-id']); - this.token = (this.request.headers['x-auth-token'] && - Accounts._hashLoginToken(String(this.request.headers['x-auth-token'])))!; + this.userId = String(this.request.headers.get('x-user-id')); + const authToken = this.request.headers.get('x-auth-token'); + this.token = (authToken && Accounts._hashLoginToken(String(authToken)))!; } if (!this.user && options.authRequired && !options.authOrAnonRequired && !settings.get('Accounts_AllowAnonymousRead')) { @@ -806,7 +778,7 @@ export class APIClass< const objectForRateLimitMatch = { IPAddr: this.requestIp, - route: `/${api.apiPath}${this.request.route.path}${this.request.method.toLowerCase()}`, + route: `/${route}${this.request.method.toLowerCase()}`, }; let result; @@ -932,9 +904,11 @@ export class APIClass< } protected async authenticatedRoute(req: Request): Promise { - const { 'x-user-id': userId } = req.headers; + const headers = Object.fromEntries(req.headers.entries()); - const userToken = String(req.headers['x-auth-token']); + const { 'x-user-id': userId } = headers; + + const userToken = String(headers['x-auth-token']); if (userId && userToken) { return Users.findOne( @@ -973,7 +947,7 @@ export class APIClass< return bodyParams; } - const code = bodyCode || request.headers['x-2fa-code']; + const code = bodyCode || request.headers.get('x-2fa-code'); const auth: Record = { password, @@ -1035,7 +1009,7 @@ export class APIClass< const args = loginCompatibility(this.bodyParams, request); const invocation = new DDPCommon.MethodInvocation({ - connection: generateConnection(getRequestIP(request) || '', this.request.headers), + connection: generateConnection(this.requestIp || '', this.request.headers), }); try { @@ -1227,13 +1201,10 @@ settings.watch('API_Enable_Rate_Limiter_Limit_Calls_Default', (value) => API.v1.reloadRoutesToRefreshRateLimiter(); }); -Meteor.startup(() => { - (WebApp.connectHandlers as unknown as ReturnType).use( +export const startRestAPI = () => { + (WebApp.rawConnectHandlers as unknown as ReturnType).use( API.api - .use((_req, res, next) => { - res.removeHeader('X-Powered-By'); - next(); - }) + .use(remoteAddressMiddleware) .use(cors(settings)) .use(loggerMiddleware(logger)) .use(metricsMiddleware(API.v1, settings, metrics.rocketchatRestApi)) @@ -1241,18 +1212,4 @@ Meteor.startup(() => { .use(API.v1.router) .use(API.default.router).router, ); -}); - -(WebApp.connectHandlers as unknown as ReturnType) - .use( - express.json({ - limit: '50mb', - }), - ) - .use( - express.urlencoded({ - extended: true, - limit: '50mb', - }), - ) - .use(express.query({})); +}; diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index e07ab21446adf..d2da73d500d36 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -2,7 +2,6 @@ import type { IUser, LicenseModule } from '@rocket.chat/core-typings'; import type { Logger } from '@rocket.chat/logger'; import type { Method, MethodOf, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings'; import type { ValidateFunction } from 'ajv'; -import type { Request, Response } from 'express'; import type { ITwoFactorOptions } from '../../2fa/server/code'; @@ -12,7 +11,7 @@ export type RedirectStatusCodes = Exclude, Range<300>>; export type AuthorizationStatusCodes = Exclude, Range<400>>; -export type ErrorStatusCodes = Exclude, Range<500>>; +export type ErrorStatusCodes = Exclude, Range<500>>, 509>; export type SuccessResult = { statusCode: TStatusCode; @@ -137,6 +136,7 @@ export type PartialThis = { readonly response: Response; readonly userId: string; readonly bodyParams: Record; + readonly path: string; readonly queryParams: Record; readonly queryOperations?: string[]; readonly queryFields?: string[]; diff --git a/apps/meteor/app/api/server/helpers/getLoggedInUser.ts b/apps/meteor/app/api/server/helpers/getLoggedInUser.ts index 55c7c2d219557..d3fc562eeb20f 100644 --- a/apps/meteor/app/api/server/helpers/getLoggedInUser.ts +++ b/apps/meteor/app/api/server/helpers/getLoggedInUser.ts @@ -1,11 +1,10 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; -import type { Request } from 'express'; import { Accounts } from 'meteor/accounts-base'; export async function getLoggedInUser(request: Request): Promise | null> { - const token = request.headers['x-auth-token']; - const userId = request.headers['x-user-id']; + const token = request.headers.get('x-auth-token'); + const userId = request.headers.get('x-user-id'); if (!token || !userId || typeof token !== 'string' || typeof userId !== 'string') { return null; } diff --git a/apps/meteor/app/api/server/helpers/isWidget.ts b/apps/meteor/app/api/server/helpers/isWidget.ts index 49fbe84111d7b..258820ff7de5e 100644 --- a/apps/meteor/app/api/server/helpers/isWidget.ts +++ b/apps/meteor/app/api/server/helpers/isWidget.ts @@ -1,7 +1,7 @@ import { parse } from 'cookie'; -export const isWidget = (headers: Record = {}): boolean => { - const { rc_room_type: roomType, rc_is_widget: isWidget } = parse(headers.cookie || ''); +export const isWidget = (headers: Headers): boolean => { + const { rc_room_type: roomType, rc_is_widget: isWidget } = parse(headers.get('cookie') || ''); const isLivechatRoom = roomType && roomType === 'l'; return !!(isLivechatRoom && isWidget === 't'); diff --git a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts index 16f370e15bd40..068e808751e52 100644 --- a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts +++ b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts @@ -25,13 +25,13 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ query: Record; }> { const { - request: { path: route }, userId, queryParams: params, logger, queryFields, queryOperations, response, + request: { route }, } = api; let sort; @@ -60,7 +60,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ let fields: Record | undefined; if (params.fields && isUnsafeQueryParamsAllowed) { try { - apiDeprecationLogger.parameter(route, 'fields', '8.0.0', response, messageGenerator); + apiDeprecationLogger.parameter(api.path, 'fields', '8.0.0', response, messageGenerator); fields = JSON.parse(params.fields) as Record; Object.entries(fields).forEach(([key, value]) => { if (value !== 1 && value !== 0) { @@ -99,7 +99,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ // Limit the fields by default fields = Object.assign({}, fields, API.v1.defaultFieldsToExclude); - if (route.includes('/v1/users.')) { + if (api.path.includes('/v1/users.')) { if (await hasPermissionAsync(userId, 'view-full-other-user-info')) { fields = Object.assign(fields, API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser); } else { @@ -109,7 +109,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ let query: Record = {}; if (params.query && isUnsafeQueryParamsAllowed) { - apiDeprecationLogger.parameter(route, 'query', '8.0.0', response, messageGenerator); + apiDeprecationLogger.parameter(api.path, 'query', '8.0.0', response, messageGenerator); try { query = ejson.parse(params.query); query = clean(query, pathAllowConf.def); @@ -125,7 +125,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ if (typeof query === 'object') { let nonQueryableFields = Object.keys(API.v1.defaultFieldsToExclude); - if (route.includes('/v1/users.')) { + if (api.path.includes('/v1/users.')) { if (await hasPermissionAsync(userId, 'view-full-other-user-info')) { nonQueryableFields = nonQueryableFields.concat(Object.keys(API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser)); } else { diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts b/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts index dc7afb77bd197..c9cced78f9591 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.spec.ts @@ -1,7 +1,4 @@ -import { Readable } from 'stream'; - import { expect } from 'chai'; -import type { Request } from 'express'; import { getUploadFormData } from './getUploadFormData'; @@ -13,7 +10,7 @@ const createMockRequest = ( content: string | Buffer; mimetype?: string; }, -): Readable & { headers: Record } => { +): Request => { const boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW'; const parts: string[] = []; @@ -33,18 +30,31 @@ const createMockRequest = ( parts.push(`--${boundary}--`); - const mockRequest: any = new Readable({ - read() { - this.push(Buffer.from(parts.join('\r\n'))); - this.push(null); - }, - }); + const buffer = Buffer.from(parts.join('\r\n')); - mockRequest.headers = { - 'content-type': `multipart/form-data; boundary=${boundary}`, + const mockRequest: any = { + headers: { + entries: () => [['content-type', `multipart/form-data; boundary=${boundary}`]], + }, + blob: async () => ({ + stream: () => { + let hasRead = false; + return { + getReader: () => ({ + read: async () => { + if (!hasRead) { + hasRead = true; + return { value: buffer, done: false }; + } + return { done: true }; + }, + }), + }; + }, + }), }; - return mockRequest as Readable & { headers: Record }; + return mockRequest as Request & { headers: Record }; }; describe('getUploadFormData', () => { @@ -59,7 +69,7 @@ describe('getUploadFormData', () => { }, ); - const result = await getUploadFormData({ request: mockRequest as Request }, { field: 'fileField' }); + const result = await getUploadFormData({ request: mockRequest }, { field: 'fileField' }); expect(result).to.deep.include({ fieldname: 'fileField', @@ -86,7 +96,7 @@ describe('getUploadFormData', () => { }, ); - const result = await getUploadFormData({ request: mockRequest as Request }, { field: 'fileField' }); + const result = await getUploadFormData({ request: mockRequest }, { field: 'fileField' }); expect(result).to.deep.include({ fieldname: 'fileField', @@ -114,7 +124,7 @@ describe('getUploadFormData', () => { }, ); - const result = await getUploadFormData({ request: mockRequest as Request }, { fileOptional: true }); + const result = await getUploadFormData({ request: mockRequest }, { fileOptional: true }); expect(result).to.deep.include({ fieldname: 'fileField', @@ -131,7 +141,7 @@ describe('getUploadFormData', () => { const mockRequest = createMockRequest({ fieldName: 'fieldValue' }); try { - await getUploadFormData({ request: mockRequest as Request }, { fileOptional: false }); + await getUploadFormData({ request: mockRequest }, { fileOptional: false }); throw new Error('Expected function to throw'); } catch (error) { expect((error as Error).message).to.equal('[No file uploaded]'); @@ -141,7 +151,7 @@ describe('getUploadFormData', () => { it('should return fields without errors when no file is uploaded but fileOptional is true', async () => { const mockRequest = createMockRequest({ fieldName: 'fieldValue' }); // No file - const result = await getUploadFormData({ request: mockRequest as Request }, { fileOptional: true }); + const result = await getUploadFormData({ request: mockRequest }, { fileOptional: true }); expect(result).to.deep.equal({ fields: { fieldName: 'fieldValue' }, @@ -167,7 +177,7 @@ describe('getUploadFormData', () => { try { await getUploadFormData( - { request: mockRequest as Request }, + { request: mockRequest }, { sizeLimit: 1024 * 1024 }, // 1 MB limit ); throw new Error('Expected function to throw'); diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index 93ceafdde92f8..bf9b792e9b08e 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -1,9 +1,8 @@ -import type { Readable } from 'stream'; +import { Readable } from 'stream'; import { MeteorError } from '@rocket.chat/core-services'; import type { ValidateFunction } from 'ajv'; import busboy from 'busboy'; -import type { Request } from 'express'; import { getMimeType } from '../../../utils/lib/mimeTypes'; @@ -71,7 +70,7 @@ export async function getUploadFormData< ...(options.sizeLimit && options.sizeLimit > -1 && { fileSize: options.sizeLimit }), }; - const bb = busboy({ headers: request.headers, defParamCharset: 'utf8', limits }); + const bb = busboy({ headers: Object.fromEntries(request.headers.entries()), defParamCharset: 'utf8', limits }); const fields = Object.create(null) as K; let uploadedFile: UploadResultWithOptionalFile | undefined = { @@ -142,8 +141,6 @@ export async function getUploadFormData< } function cleanup() { - request.unpipe(bb); - request.on('readable', request.read.bind(request)); bb.removeAllListeners(); } @@ -167,7 +164,29 @@ export async function getUploadFormData< returnError(); }); - request.pipe(bb); + const webReadableStream = await request.blob().then((blob) => blob.stream()); + + const nodeReadableStream = new Readable({ + async read() { + const reader = webReadableStream.getReader(); + try { + const processChunk = async () => { + const { done, value } = await reader.read(); + if (done) { + this.push(null); + return; + } + this.push(Buffer.from(value)); + await processChunk(); + }; + await processChunk(); + } catch (err: any) { + this.destroy(err); + } + }, + }); + + nodeReadableStream.pipe(bb); return new Promise>((resolve, reject) => { returnResult = resolve; diff --git a/apps/meteor/app/api/server/middlewares/cors.ts b/apps/meteor/app/api/server/middlewares/cors.ts index db6dde775918a..44f7e39acafc0 100644 --- a/apps/meteor/app/api/server/middlewares/cors.ts +++ b/apps/meteor/app/api/server/middlewares/cors.ts @@ -1,4 +1,4 @@ -import type { NextFunction, Request, Response } from 'express'; +import type { MiddlewareHandler } from 'hono'; import type { CachedSettings } from '../../../settings/server/CachedSettings'; @@ -7,55 +7,55 @@ const defaultHeaders = { 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-User-Id, X-Auth-Token, x-visitor-token, Authorization', }; -export const cors = (settings: CachedSettings) => (req: Request, res: Response, next: NextFunction) => { - if (req.method !== 'OPTIONS') { - if (settings.get('API_Enable_CORS')) { - res.setHeader('Vary', 'Origin'); - res.setHeader('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); - res.setHeader('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); +export const cors = + (settings: CachedSettings): MiddlewareHandler => + async (c, next) => { + const { req, res } = c; + if (req.method !== 'OPTIONS') { + if (settings.get('API_Enable_CORS')) { + res.headers.set('Vary', 'Origin'); + res.headers.set('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); + res.headers.set('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); + } + + await next(); + return; } - next(); - return; - } - - // check if a pre-flight request - if (!req.headers['access-control-request-method'] && !req.headers.origin) { - next(); - return; - } - - if (!settings.get('API_Enable_CORS')) { - res.writeHead(405); - res.write('CORS not enabled. Go to "Admin > General > REST Api" to enable it.'); - res.end(); - return; - } - - const CORSOriginSetting = String(settings.get('API_CORS_Origin')); - - if (CORSOriginSetting === '*') { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); - res.setHeader('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); - next(); - return; - } - - const origins = CORSOriginSetting.trim() - .split(',') - .map((origin) => String(origin).trim().toLocaleLowerCase()); - - // if invalid origin reply without required CORS headers - if (!req.headers.origin || !origins.includes(req.headers.origin)) { - res.writeHead(403, 'Forbidden'); - res.end(); - return; - } - - res.setHeader('Vary', 'Origin'); - res.setHeader('Access-Control-Allow-Origin', req.headers.origin); - res.setHeader('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); - res.setHeader('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); - next(); -}; + // check if a pre-flight request + if (!req.header('access-control-request-method') && !req.header('origin')) { + await next(); + return; + } + + if (!settings.get('API_Enable_CORS')) { + return c.body('CORS not enabled. Go to "Admin > General > REST Api" to enable it.', 405); + } + + const CORSOriginSetting = String(settings.get('API_CORS_Origin')); + + if (CORSOriginSetting === '*') { + res.headers.set('Access-Control-Allow-Origin', '*'); + res.headers.set('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); + res.headers.set('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); + await next(); + return; + } + + const origins = CORSOriginSetting.trim() + .split(',') + .map((origin) => String(origin).trim().toLocaleLowerCase()); + + const originHeader = req.header('origin'); + + // if invalid origin reply without required CORS headers + if (!originHeader || !origins.includes(originHeader)) { + return c.body('Invalid origin', 403); + } + + res.headers.set('Vary', 'Origin'); + res.headers.set('Access-Control-Allow-Origin', originHeader); + res.headers.set('Access-Control-Allow-Methods', defaultHeaders['Access-Control-Allow-Methods']); + res.headers.set('Access-Control-Allow-Headers', defaultHeaders['Access-Control-Allow-Headers']); + await next(); + }; diff --git a/apps/meteor/app/api/server/middlewares/honoAdapter.ts b/apps/meteor/app/api/server/middlewares/honoAdapter.ts new file mode 100644 index 0000000000000..86d9e83cb3a1b --- /dev/null +++ b/apps/meteor/app/api/server/middlewares/honoAdapter.ts @@ -0,0 +1,31 @@ +import { Readable } from 'stream'; + +import type { Request, Response } from 'express'; +import type { Hono } from 'hono'; + +export const honoAdapter = (hono: Hono) => async (expressReq: Request, res: Response) => { + (expressReq as unknown as any).duplex = 'half'; + + if (Readable.isDisturbed(expressReq)) { + return; + } + + const { body, ...req } = expressReq; + + const honoRes = await hono.request( + expressReq.originalUrl, + { + ...req, + ...(['POST', 'PUT', 'DELETE'].includes(expressReq.method) && { body: expressReq as unknown as ReadableStream }), + headers: new Headers(Object.fromEntries(Object.entries(expressReq.headers)) as Record), + }, + { + incoming: expressReq, + }, + ); + res.status(honoRes.status); + honoRes.headers.forEach((value, key) => res.setHeader(key, value)); + // Converting it to a Buffer because res.send appends always a charset to the Content-Type + // https://github.com/expressjs/express/issues/2238 + res.send(Buffer.from(await honoRes.text())); +}; diff --git a/apps/meteor/app/api/server/middlewares/logger.ts b/apps/meteor/app/api/server/middlewares/logger.ts index 5233435a19a41..a9a733de86c4c 100644 --- a/apps/meteor/app/api/server/middlewares/logger.ts +++ b/apps/meteor/app/api/server/middlewares/logger.ts @@ -1,28 +1,36 @@ import type { Logger } from '@rocket.chat/logger'; -import type { Request, Response, NextFunction } from 'express'; +import type { MiddlewareHandler } from 'hono'; import { getRestPayload } from '../../../../server/lib/logger/logPayloads'; -export const loggerMiddleware = (logger: Logger) => async (req: Request, res: Response, next: NextFunction) => { - const startTime = Date.now(); +export const loggerMiddleware = + (logger: Logger): MiddlewareHandler => + async (c, next) => { + const startTime = Date.now(); + + let payload = {}; + + try { + payload = await c.req.raw.clone().json(); + // eslint-disable-next-line no-empty + } catch {} + + const log = logger.logger.child({ + method: c.req.method, + url: c.req.url, + userId: c.req.header('x-user-id'), + userAgent: c.req.header('user-agent'), + length: c.req.header('content-length'), + host: c.req.header('host'), + referer: c.req.header('referer'), + remoteIP: c.get('remoteAddress'), + ...(['POST', 'PUT', 'PATCH', 'DELETE'].includes(c.req.method) && getRestPayload(payload)), + }); + + await next(); - const log = logger.logger.child({ - method: req.method, - url: req.url, - userId: req.headers['x-user-id'], - userAgent: req.headers['user-agent'], - length: req.headers['content-length'], - host: req.headers.host, - referer: req.headers.referer, - remoteIP: req.ip, - ...getRestPayload(req.body), - }); - res.once('finish', () => { log.http({ - status: res.statusCode, + status: c.res.status, responseTime: Date.now() - startTime, }); - }); - - next(); -}; + }; diff --git a/apps/meteor/app/api/server/middlewares/metrics.ts b/apps/meteor/app/api/server/middlewares/metrics.ts index 9206a51375001..518febc7132a4 100644 --- a/apps/meteor/app/api/server/middlewares/metrics.ts +++ b/apps/meteor/app/api/server/middlewares/metrics.ts @@ -1,24 +1,23 @@ -import type { Request, Response, NextFunction } from 'express'; +import type { MiddlewareHandler } from 'hono'; import type { Summary } from 'prom-client'; import type { CachedSettings } from '../../../settings/server/CachedSettings'; import type { APIClass } from '../api'; export const metricsMiddleware = - (api: APIClass, settings: CachedSettings, summary: Summary) => async (req: Request, res: Response, next: NextFunction) => { - const { method, path } = req; + (api: APIClass, settings: CachedSettings, summary: Summary): MiddlewareHandler => + async (c, next) => { + const { method, path } = c.req; const rocketchatRestApiEnd = summary.startTimer({ method, version: api.version, - ...(settings.get('Prometheus_API_User_Agent') && { user_agent: req.headers['user-agent'] }), - entrypoint: path.startsWith('method.call') ? decodeURIComponent(req.url.slice(8)) : path, + ...(settings.get('Prometheus_API_User_Agent') && { user_agent: c.req.header('user-agent') }), + entrypoint: path.startsWith('method.call') ? decodeURIComponent(c.req.url.slice(8)) : path, }); - res.once('finish', () => { - rocketchatRestApiEnd({ - status: res.statusCode, - }); + await next(); + rocketchatRestApiEnd({ + status: c.res.status, }); - next(); }; diff --git a/apps/meteor/app/api/server/middlewares/remoteAddressMiddleware.ts b/apps/meteor/app/api/server/middlewares/remoteAddressMiddleware.ts new file mode 100644 index 0000000000000..6e129e88f467a --- /dev/null +++ b/apps/meteor/app/api/server/middlewares/remoteAddressMiddleware.ts @@ -0,0 +1,40 @@ +import type { IncomingMessage } from 'http'; + +import type { Context, MiddlewareHandler } from 'hono'; + +type HttpBindings = { + incoming: IncomingMessage; +}; + +const getRemoteAddress = (c: Context) => { + const bindings = (c.env?.server ? c.env.server : c.env) as HttpBindings; + + const forwardedFor = c.req.header('x-forwarded-for'); + const socket = bindings.incoming.socket.remoteAddress || bindings.incoming.connection.remoteAddress; + const remoteAddress = c.req.header('x-real-ip') || socket; + + if (!socket) { + return remoteAddress || forwardedFor; + } + + const httpForwardedCount = parseInt(String(process.env.HTTP_FORWARDED_COUNT)) || 0; + if (httpForwardedCount <= 0) { + return remoteAddress; + } + + if (!forwardedFor || typeof forwardedFor.valueOf() !== 'string') { + return remoteAddress; + } + + const forwardedForIPs = forwardedFor.trim().split(/\s*,\s*/); + if (httpForwardedCount > forwardedForIPs.length) { + return remoteAddress; + } + return forwardedForIPs[forwardedForIPs.length - httpForwardedCount]; +}; + +export const remoteAddressMiddleware: MiddlewareHandler = async function (c, next) { + const remoteAddress = getRemoteAddress(c); + c.set('remoteAddress', remoteAddress); + return next(); +}; diff --git a/apps/meteor/app/api/server/middlewares/tracer.ts b/apps/meteor/app/api/server/middlewares/tracer.ts index e229672598aa1..bc3f03778cda7 100644 --- a/apps/meteor/app/api/server/middlewares/tracer.ts +++ b/apps/meteor/app/api/server/middlewares/tracer.ts @@ -1,32 +1,25 @@ import { tracerSpan } from '@rocket.chat/tracing'; -import type { Request, Response, NextFunction } from 'express'; +import type { MiddlewareHandler } from 'hono'; -export const tracerSpanMiddleware = async (req: Request, res: Response, next: NextFunction) => { - try { - await tracerSpan( - `${req.method} ${req.url}`, - { - attributes: { - url: req.url, - route: req.route?.path, - method: req.method, - userId: req.userId, // Assuming userId is attached to the request object - }, +export const tracerSpanMiddleware: MiddlewareHandler = async (c, next) => { + return tracerSpan( + `${c.req.method} ${c.req.url}`, + { + attributes: { + url: c.req.url, + // route: c.req.route?.path, + method: c.req.method, + userId: (c.req.raw.clone() as any).userId, // Assuming userId is attached to the request object }, - async (span) => { - if (span) { - res.setHeader('X-Trace-Id', span.spanContext().traceId); - } + }, + async (span) => { + if (span) { + c.header('X-Trace-Id', span.spanContext().traceId); + } - next(); + await next(); - await new Promise((resolve) => { - res.once('finish', resolve); - }); - span?.setAttribute('status', res.statusCode); - }, - ); - } catch (error) { - next(error); - } + span?.setAttribute('status', c.res.status); + }, + ); }; diff --git a/apps/meteor/app/api/server/router.spec.ts b/apps/meteor/app/api/server/router.spec.ts index 36997fc23d604..ca5bd8970ae84 100644 --- a/apps/meteor/app/api/server/router.spec.ts +++ b/apps/meteor/app/api/server/router.spec.ts @@ -9,7 +9,10 @@ describe('Router use method', () => { const ajv = new Ajv(); const app = express(); const api = new Router('/api'); - const v1 = new Router('/v1'); + const v1 = new Router('/v1').use(async (x, next) => { + x.header('x-api-version', 'v1'); + await next(); + }); const v2 = new Router('/v2'); const test = new Router('/test').get( '/', @@ -33,27 +36,16 @@ describe('Router use method', () => { }, ); - app.use( - api - .use( - v1 - .use((req, _res, next) => { - (req as any).customProperty = 'customValue'; - next(); - }) - .use(test), - ) - .use(v2.use(test)).router, - ); + app.use(api.use(v1.use(test)).use(v2.use(test)).router); const response1 = await request(app).get('/api/v1/test'); expect(response1.statusCode).toBe(200); - expect(response1.body).toHaveProperty('customProperty', 'customValue'); + expect(response1.headers).toHaveProperty('x-api-version', 'v1'); const response2 = await request(app).get('/api/v2/test'); expect(response2.statusCode).toBe(200); - expect(response2.body).not.toHaveProperty('customProperty', 'customValue'); + expect(response2.headers).not.toHaveProperty('x-api-version'); }); }); diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index 514e86ecc872d..3d06278d9d9eb 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -1,10 +1,13 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import type { Method } from '@rocket.chat/rest-typings'; import type { AnySchema } from 'ajv'; import express from 'express'; +import type { HonoRequest, MiddlewareHandler } from 'hono'; +import { Hono } from 'hono'; +import qs from 'qs'; // Using qs specifically to keep express compatibility import type { TypedAction, TypedOptions } from './definition'; - -type MiddlewareHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => void; +import { honoAdapter } from './middlewares/honoAdapter'; type MiddlewareHandlerListAndActionHandler = [ ...MiddlewareHandler[], @@ -49,6 +52,18 @@ export type Route = { }[]; tags?: string[]; }; +declare module 'hono' { + interface ContextVariableMap { + 'route': string; + 'bodyParams-override'?: Record; + } +} + +declare global { + interface Request { + route: string; + } +} export class Router< TBasePath extends string, @@ -56,16 +71,14 @@ export class Router< [x: string]: unknown; } = NonNullable, > { - public router; - - private innerRouter: express.Router; + protected innerRouter: Hono<{ + Variables: { + remoteAddress: string; + }; + }>; constructor(readonly base: TBasePath) { - // eslint-disable-next-line new-cap - this.router = express.Router(); - // eslint-disable-next-line new-cap - this.innerRouter = express.Router(); - this.router.use(this.base, this.innerRouter); + this.innerRouter = new Hono(); } public typedRoutes: Record> = {}; @@ -124,6 +137,38 @@ export class Router< }; } + private async parseBodyParams(request: HonoRequest, overrideBodyParams: Record = {}) { + try { + let parsedBody = {}; + const contentType = request.header('content-type'); + + if (contentType?.includes('application/json')) { + parsedBody = await request.raw.clone().json(); + } else if (contentType?.includes('multipart/form-data')) { + parsedBody = await request.raw.clone().formData(); + } else { + parsedBody = await request.raw.clone().text(); + } + // This is necessary to keep the compatibility with the previous version, otherwise the bodyParams will be an empty string when no content-type is sent + if (parsedBody === '') { + return { ...overrideBodyParams }; + } + + if (Array.isArray(parsedBody)) { + return parsedBody; + } + + return { ...parsedBody, ...overrideBodyParams }; + // eslint-disable-next-line no-empty + } catch {} + + return { ...overrideBodyParams }; + } + + private parseQueryParams(request: HonoRequest) { + return qs.parse(request.raw.url.split('?')?.[1] || ''); + } + private method( method: Method, subpath: TSubPathPattern, @@ -139,26 +184,36 @@ export class Router< > { const [middlewares, action] = splitArray(actions); - this.innerRouter[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), ...middlewares, async (req, res) => { + this.innerRouter[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), ...middlewares, async (c) => { + const { req, res } = c; + req.raw.route = `${c.var.route ?? ''}${subpath}`; if (options.query) { const validatorFn = options.query; - if (typeof options.query === 'function' && !validatorFn(req.query)) { - return res.status(400).json({ - success: false, - errorType: 'error-invalid-params', - error: validatorFn.errors?.map((error: any) => error.message).join('\n '), - }); + if (typeof options.query === 'function' && !validatorFn(req.query())) { + return c.json( + { + success: false, + errorType: 'error-invalid-params', + error: validatorFn.errors?.map((error: any) => error.message).join('\n '), + }, + 400, + ); } } + const bodyParams = await this.parseBodyParams(req, c.var['bodyParams-override']); + if (options.body) { const validatorFn = options.body; - if (typeof options.body === 'function' && !validatorFn((req as any).bodyParams || req.body)) { - return res.status(400).json({ - success: false, - errorType: 'error-invalid-params', - error: validatorFn.errors?.map((error: any) => error.message).join('\n '), - }); + if (typeof options.body === 'function' && !validatorFn((req as any).bodyParams || bodyParams)) { + return c.json( + { + success: false, + errorType: 'error-invalid-params', + error: validatorFn.errors?.map((error: any) => error.message).join('\n '), + }, + 400, + ); } } @@ -168,13 +223,15 @@ export class Router< headers = {}, } = await action.apply( { - urlParams: req.params, - queryParams: req.query, - bodyParams: (req as any).bodyParams || req.body, - request: req, + requestIp: c.get('remoteAddress'), + urlParams: req.param(), + queryParams: this.parseQueryParams(req), + bodyParams, + request: req.raw.clone(), + path: req.path, response: res, } as any, - [req], + [req.raw.clone()], ); if (process.env.NODE_ENV === 'test' || process.env.TEST_MODE) { const responseValidatorFn = options?.response?.[statusCode]; @@ -190,7 +247,7 @@ export class Router< const responseHeaders = Object.fromEntries( Object.entries({ - ...res.header, + ...res.headers, 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'Pragma': 'no-cache', @@ -198,15 +255,21 @@ export class Router< }).map(([key, value]) => [key.toLowerCase(), value]), ); - res.writeHead(statusCode, responseHeaders); + const contentType = (responseHeaders['content-type'] || 'application/json') as string; - if (responseHeaders['content-type']?.match(/json|javascript/) !== null) { - body !== undefined && res.write(JSON.stringify(body)); - } else { - body !== undefined && res.write(body); + const isContentLess = (statusCode: number): statusCode is 101 | 204 | 205 | 304 => { + return [101, 204, 205, 304].includes(statusCode); + }; + + if (isContentLess(statusCode)) { + return c.status(statusCode); } - res.end(); + return c.body( + (contentType?.match(/json|javascript/) ? JSON.stringify(body) : body) as any, + statusCode, + responseHeaders as Record, + ); }); this.registerTypedRoutes(method, subpath, options); return this; @@ -272,26 +335,47 @@ export class Router< return this.method('DELETE', subpath, options, ...action); } - use void>(fn: FN): Router; + use(fn: FN): Router; use>( innerRouter: IRouter, ): IRouter extends Router ? Router> : never; - use(innerRouter: any): any { + use(innerRouter: unknown): any { if (innerRouter instanceof Router) { this.typedRoutes = { ...this.typedRoutes, ...Object.fromEntries(Object.entries(innerRouter.typedRoutes).map(([path, routes]) => [`${this.base}${path}`, routes])), }; - this.innerRouter.use(innerRouter.router); + this.innerRouter.route(innerRouter.base, innerRouter.innerRouter); } if (typeof innerRouter === 'function') { - this.innerRouter.use(innerRouter); + this.innerRouter.use(innerRouter as any); } return this as any; } + + get router(): express.Router { + // eslint-disable-next-line new-cap + const router = express.Router(); + const hono = new Hono(); + router.use( + this.base, + honoAdapter( + hono + .use(`${this.base}/*`, (c, next) => { + c.set('route', `${c.var.route || ''}${this.base}`); + return next(); + }) + .route(this.base, this.innerRouter) + .options('*', (c) => { + return c.body('OK'); + }), + ), + ); + return router; + } } type Prettify = { diff --git a/apps/meteor/app/api/server/v1/assets.ts b/apps/meteor/app/api/server/v1/assets.ts index fd9f31d40923a..2843cf8627d51 100644 --- a/apps/meteor/app/api/server/v1/assets.ts +++ b/apps/meteor/app/api/server/v1/assets.ts @@ -41,7 +41,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', })(Settings.updateValueById, key, value); if (modifiedCount) { @@ -78,7 +78,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', })(Settings.updateValueById, key, value); if (modifiedCount) { diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index bf273b75070dc..40d30fd8d2450 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -671,7 +671,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', }); const promises = settingsIds.map((settingId) => { @@ -691,7 +691,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', })(Settings.resetValueById, settingId); }); diff --git a/apps/meteor/app/api/server/v1/push.ts b/apps/meteor/app/api/server/v1/push.ts index a2c29f85db407..b9a79b5d317bd 100644 --- a/apps/meteor/app/api/server/v1/push.ts +++ b/apps/meteor/app/api/server/v1/push.ts @@ -37,7 +37,7 @@ API.v1.addRoute( const result = await Meteor.callAsync('raix:push-update', { id: deviceId, token: { [type]: value }, - authToken: this.request.headers['x-auth-token'], + authToken: this.request.headers.get('x-auth-token'), appName, userId: this.userId, }); diff --git a/apps/meteor/app/api/server/v1/settings.ts b/apps/meteor/app/api/server/v1/settings.ts index 6d2bbab89afd8..438fda41850b3 100644 --- a/apps/meteor/app/api/server/v1/settings.ts +++ b/apps/meteor/app/api/server/v1/settings.ts @@ -212,7 +212,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', }); if (isSettingColor(setting) && isSettingsUpdatePropsColor(this.bodyParams)) { diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 926ae3415d4a9..a065cc47407c7 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -81,7 +81,7 @@ API.v1.addRoute( const user = await getUserFromParams(this.queryParams); const url = getURL(`/avatar/${user.username}`, { cdn: false, full: true }); - this.response.setHeader('Location', url); + this.response.headers.set('Location', url); return { statusCode: 307, @@ -118,7 +118,7 @@ API.v1.addRoute( const auditStore = new UserChangedAuditStore({ _id: this.user._id, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', username: this.user.username || '', }); @@ -892,7 +892,7 @@ API.v1.addRoute( await Users.enableEmail2FAByUserId(this.userId); // When 2FA is enable we logout all other clients - const xAuthToken = this.request.headers['x-auth-token'] as string; + const xAuthToken = this.request.headers.get('x-auth-token') as string; if (!xAuthToken) { return API.v1.success(); } @@ -1070,7 +1070,7 @@ API.v1.addRoute( { authRequired: true }, { async post() { - const xAuthToken = this.request.headers['x-auth-token'] as string; + const xAuthToken = this.request.headers.get('x-auth-token') as string; if (!xAuthToken) { throw new Meteor.Error('error-parameter-required', 'x-auth-token is required'); diff --git a/apps/meteor/app/apps/server/bridges/api.ts b/apps/meteor/app/apps/server/bridges/api.ts index 46bb70e3339a3..947ce62d690e0 100644 --- a/apps/meteor/app/apps/server/bridges/api.ts +++ b/apps/meteor/app/apps/server/bridges/api.ts @@ -3,6 +3,7 @@ import type { RequestMethod } from '@rocket.chat/apps-engine/definition/accessor import type { IApiRequest, IApiEndpoint, IApi } from '@rocket.chat/apps-engine/definition/api'; import { ApiBridge } from '@rocket.chat/apps-engine/server/bridges/ApiBridge'; import type { AppApi } from '@rocket.chat/apps-engine/server/managers/AppApi'; +import bodyParser from 'body-parser'; import type { Response, Request, IRouter, RequestHandler } from 'express'; import express from 'express'; import { Meteor } from 'meteor/meteor'; @@ -14,7 +15,7 @@ const apiServer = express(); apiServer.disable('x-powered-by'); -WebApp.connectHandlers.use(apiServer); +WebApp.rawConnectHandlers.use(apiServer); interface IRequestWithPrivateHash extends Request { _privateHash?: string; @@ -28,7 +29,7 @@ export class AppApisBridge extends ApiBridge { super(); this.appRouters = new Map(); - apiServer.use('/api/apps/private/:appId/:hash', (req: IRequestWithPrivateHash, res: Response) => { + apiServer.use('/api/apps/private/:appId/:hash', bodyParser.json(), (req: IRequestWithPrivateHash, res: Response) => { const notFound = (): Response => res.sendStatus(404); const router = this.appRouters.get(req.params.appId); @@ -41,7 +42,7 @@ export class AppApisBridge extends ApiBridge { notFound(); }); - apiServer.use('/api/apps/public/:appId', (req: Request, res: Response) => { + apiServer.use('/api/apps/public/:appId', bodyParser.json(), (req: Request, res: Response) => { const notFound = (): Response => res.sendStatus(404); const router = this.appRouters.get(req.params.appId); diff --git a/apps/meteor/app/integrations/server/api/api.js b/apps/meteor/app/integrations/server/api/api.js index a94cb55bd8677..5541616c7cbe9 100644 --- a/apps/meteor/app/integrations/server/api/api.js +++ b/apps/meteor/app/integrations/server/api/api.js @@ -239,14 +239,15 @@ function integrationInfoRest() { } class WebHookAPI extends APIClass { - async authenticatedRoute(request) { - request.integration = await Integrations.findOne({ - _id: request.params.integrationId, - token: decodeURIComponent(request.params.token), + async authenticatedRoute() { + const { integrationId, token } = this.urlParams; + this.request.integration = await Integrations.findOne({ + _id: integrationId, + token: decodeURIComponent(token), }); - if (!request.integration) { - incomingLogger.info(`Invalid integration id ${request.params.integrationId} or token ${request.params.token}`); + if (!this.request.integration) { + incomingLogger.info(`Invalid integration id ${integrationId} or token ${token}`); return { error: { @@ -259,7 +260,7 @@ class WebHookAPI extends APIClass { }; } - return Users.findOneById(request.integration.userId); + return Users.findOneById(this.request.integration.userId); } /* Webhooks are not versioned, so we must not validate we know a version before adding a rate limiter */ @@ -313,28 +314,29 @@ const Api = new WebHookAPI({ apiPath: 'hooks/', }); -// middleware for special requests that are urlencoded but have a json payload (like GitHub webhooks) -Api.router.use((req, res, next) => { - if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') { - return next(); - } - - // make sure body has only one key and it is 'payload' - if (!req.body || typeof req.body !== 'object' || !('payload' in req.body) || Object.keys(req.body).length !== 1) { +const middleware = async (c, next) => { + const { req } = c; + if (req.raw.headers.get('content-type') !== 'application/x-www-form-urlencoded') { return next(); } try { - req.bodyParams = JSON.parse(req.body.payload); + const body = await (req.header('content-type')?.includes('application/json') ? req.raw.clone().json() : req.raw.clone().text()); + if (!body || typeof body !== 'object' || !('payload' in body) || Object.keys(body).length !== 1) { + return next(); + } - return next(); + // need to compose the full payload in this weird way because body-parser thought it was a form + c.set('bodyParams-override', JSON.parse(body.payload)); } catch (e) { - res.writeHead(400); - res.end(JSON.stringify({ success: false, error: e.message })); + c.body(JSON.stringify({ success: false, error: e.message }), 400); } return next(); -}); +}; + +// middleware for special requests that are urlencoded but have a json payload (like GitHub webhooks) +Api.router.use(middleware); Api.addRoute( ':integrationId/:userId/:token', @@ -419,5 +421,5 @@ Api.addRoute( ); Meteor.startup(() => { - WebApp.connectHandlers.use(Api.router.router); + WebApp.rawConnectHandlers.use(Api.router.router); }); diff --git a/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts b/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts index e62727814de38..2eb9c704b604b 100644 --- a/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts +++ b/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts @@ -1,5 +1,5 @@ -export const getModifiedHttpHeaders = (httpHeaders: Record) => { - const modifiedHttpHeaders = { ...httpHeaders }; +export const getModifiedHttpHeaders = (httpHeaders: Headers) => { + const modifiedHttpHeaders = { ...Object.fromEntries(httpHeaders.entries()) }; if ('x-auth-token' in modifiedHttpHeaders) { modifiedHttpHeaders['x-auth-token'] = '[redacted]'; diff --git a/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts b/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts index be6b107dc044d..5b76b007c1620 100644 --- a/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts +++ b/apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts @@ -1,5 +1,4 @@ import { Logger } from '@rocket.chat/logger'; -import type { Response } from 'express'; import semver from 'semver'; import { metrics } from '../../../metrics/server'; @@ -12,9 +11,9 @@ const throwErrorsForVersionsUnder = process.env.ROCKET_CHAT_DEPRECATION_THROW_ER const writeDeprecationHeader = (res: Response | undefined, type: string, message: string, version: string) => { if (res) { - res.setHeader('x-deprecation-type', type); - res.setHeader('x-deprecation-message', message); - res.setHeader('x-deprecation-version', version); + res.headers.set('x-deprecation-type', type); + res.headers.set('x-deprecation-message', message); + res.headers.set('x-deprecation-version', version); } }; diff --git a/apps/meteor/app/livechat/imports/server/rest/appearance.ts b/apps/meteor/app/livechat/imports/server/rest/appearance.ts index 215f208c06dc7..126c93d5fc93c 100644 --- a/apps/meteor/app/livechat/imports/server/rest/appearance.ts +++ b/apps/meteor/app/livechat/imports/server/rest/appearance.ts @@ -98,7 +98,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', }); const promises = eligibleSettings.map(({ _id, value }) => auditSettingOperation(Settings.updateValueById, _id, value)); diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.ts b/apps/meteor/app/livechat/imports/server/rest/sms.ts index 3b7aa07773071..8b476c58886cd 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts @@ -108,7 +108,7 @@ API.v1.addRoute('livechat/sms-incoming/:service', { const smsDepartment = settings.get('SMS_Default_Omnichannel_Department'); const SMSService = await OmnichannelIntegration.getSmsService(service); - if (!SMSService.validateRequest(this.request)) { + if (!(await SMSService.validateRequest(this.request.clone()))) { return API.v1.failure('Invalid request'); } diff --git a/apps/meteor/app/livechat/imports/server/rest/upload.ts b/apps/meteor/app/livechat/imports/server/rest/upload.ts index 86c815cce72c9..8cb0a0511eade 100644 --- a/apps/meteor/app/livechat/imports/server/rest/upload.ts +++ b/apps/meteor/app/livechat/imports/server/rest/upload.ts @@ -10,7 +10,7 @@ import { sendFileLivechatMessage } from '../../../server/methods/sendFileLivecha API.v1.addRoute('livechat/upload/:rid', { async post() { - if (!this.request.headers['x-visitor-token']) { + if (!this.request.headers.get('x-visitor-token')) { return API.v1.forbidden(); } @@ -22,7 +22,7 @@ API.v1.addRoute('livechat/upload/:rid', { }); } - const visitorToken = this.request.headers['x-visitor-token']; + const visitorToken = this.request.headers.get('x-visitor-token'); const visitor = await LivechatVisitors.getVisitorByToken(visitorToken as string, {}); if (!visitor) { diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index a6c774fb4ddfb..bd8df571884ec 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -93,10 +93,10 @@ export async function findAgent(agentId?: string): Promise = {}): { +export function normalizeHttpHeaderData(headers: Headers = new Headers()): { httpHeaders: Record; } { - const httpHeaders = Object.assign({}, headers); + const httpHeaders = Object.fromEntries(headers.entries()); return { httpHeaders }; } diff --git a/apps/meteor/app/livechat/server/api/v1/integration.ts b/apps/meteor/app/livechat/server/api/v1/integration.ts index 7e56c8ca8e591..48b748f4fb7be 100644 --- a/apps/meteor/app/livechat/server/api/v1/integration.ts +++ b/apps/meteor/app/livechat/server/api/v1/integration.ts @@ -55,7 +55,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', }); const promises = settingsIds.map((setting) => auditSettingOperation(Settings.updateValueById, setting._id, setting.value)); diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 071016456db0b..19e9a716c4635 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -78,7 +78,7 @@ API.v1.addRoute( const roomInfo = { source: { ...(isWidget(this.request.headers) - ? { type: OmnichannelSourceType.WIDGET, destination: this.request.headers.host } + ? { type: OmnichannelSourceType.WIDGET, destination: this.request.headers.get('host')! } : { type: OmnichannelSourceType.API }), }, }; diff --git a/apps/meteor/app/meteor-accounts-saml/server/listener.ts b/apps/meteor/app/meteor-accounts-saml/server/listener.ts index 92a0c520ab651..8cdf7c9e6f636 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/listener.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/listener.ts @@ -1,7 +1,7 @@ import type { IncomingMessage, ServerResponse } from 'http'; -import type { IIncomingMessage } from '@rocket.chat/core-typings'; import bodyParser from 'body-parser'; +import express from 'express'; import { Meteor } from 'meteor/meteor'; import { RoutePolicy } from 'meteor/routepolicy'; import { WebApp } from 'meteor/webapp'; @@ -38,11 +38,11 @@ const samlUrlToObject = function (url: string | undefined): ISAMLAction | null { return result; }; -const middleware = async function (req: IIncomingMessage, res: ServerResponse, next: (err?: any) => void): Promise { +const middleware = async function (req: express.Request, res: ServerResponse, next: (err?: any) => void): Promise { // Make sure to catch any exceptions because otherwise we'd crash // the runner try { - const samlObject = samlUrlToObject(req.url); + const samlObject = samlUrlToObject(req.originalUrl); if (!samlObject?.serviceName) { next(); return; @@ -72,6 +72,12 @@ const middleware = async function (req: IIncomingMessage, res: ServerResponse, n }; // Listen to incoming SAML http requests -WebApp.connectHandlers - .use(bodyParser.json()) - .use(async (req: IncomingMessage, res: ServerResponse, next: (err?: any) => void) => middleware(req as IIncomingMessage, res, next)); +WebApp.connectHandlers.use( + /^\/_saml/, + bodyParser.json(), + express.urlencoded({ + extended: true, + limit: '50mb', + }), + async (req: IncomingMessage, res: ServerResponse, next: (err?: any) => void) => middleware(req as express.Request, res, next), +); diff --git a/apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts b/apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts index 2d5c8c0faecf8..d285ab11ccdcb 100644 --- a/apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts +++ b/apps/meteor/ee/app/api-enterprise/server/middlewares/license.ts @@ -1,13 +1,11 @@ import type { LicenseManager } from '@rocket.chat/license'; -import type { Request, Response, NextFunction } from 'express'; +import type { MiddlewareHandler } from 'hono'; import type { FailureResult, TypedOptions } from '../../../../../app/api/server/definition'; -type ExpressMiddleware = (req: Request, res: Response, next: NextFunction) => void; - export const license = - (options: TypedOptions, licenseManager: LicenseManager): ExpressMiddleware => - async (_req, res, next) => { + (options: TypedOptions, licenseManager: LicenseManager): MiddlewareHandler => + async (c, next) => { if (!options.license) { return next(); } @@ -27,11 +25,7 @@ export const license = }; if (!license) { - // Explicitly set the content type to application/json to avoid the following issue: - // https://github.com/expressjs/express/issues/2238 - res.writeHead(failure.statusCode, { 'Content-Type': 'application/json' }); - res.write(JSON.stringify(failure.body)); - return res.end(); + return c.json(failure.body, failure.statusCode); } return next(); diff --git a/apps/meteor/ee/server/api/index.ts b/apps/meteor/ee/server/api/index.ts index 96dc64c5ced41..264cf37e329eb 100644 --- a/apps/meteor/ee/server/api/index.ts +++ b/apps/meteor/ee/server/api/index.ts @@ -4,3 +4,4 @@ import './licenses'; import './sessions'; import './chat'; import './roles'; +import '../apps/communication/uikit'; diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index 5c686c0e532e1..30484a1301b0b 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -60,7 +60,7 @@ API.v1.addRoute( _id: this.userId, username: this.user.username!, ip: this.requestIp, - useragent: this.request.headers['user-agent'] || '', + useragent: this.request.headers.get('user-agent') || '', }); (await auditSettingOperation(Settings.updateValueById, 'Enterprise_License', license)).modifiedCount && diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index 53719aee9a87a..58088a9d0289f 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -6,9 +6,7 @@ import type { IUser, IMessage } from '@rocket.chat/core-typings'; import { License } from '@rocket.chat/license'; import { Settings, Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import type express from 'express'; import { Meteor } from 'meteor/meteor'; -import { WebApp } from 'meteor/webapp'; import { ZodError } from 'zod'; import { actionButtonsHandler } from './endpoints/actionButtonsHandler'; @@ -55,14 +53,17 @@ export class AppsRestApi { async loadAPI() { this.api = new API.ApiClass({ - version: 'apps', - apiPath: '/api', + apiPath: '', useDefaultAuth: true, prettyJson: false, enableCors: false, + version: 'apps', }); + await this.addManagementRoutes(); - (WebApp.connectHandlers as unknown as ReturnType).use(this.api.router.router); + + // Using the same instance of the existing API for now, to be able to use the same api prefix(/api) + API.api.use(this.api.router); } addManagementRoutes() { diff --git a/apps/meteor/ee/server/apps/communication/uikit.ts b/apps/meteor/ee/server/apps/communication/uikit.ts index 0392076704d72..7d490406d007f 100644 --- a/apps/meteor/ee/server/apps/communication/uikit.ts +++ b/apps/meteor/ee/server/apps/communication/uikit.ts @@ -1,6 +1,7 @@ import type { UiKitCoreAppPayload } from '@rocket.chat/core-services'; import { UiKitCoreApp } from '@rocket.chat/core-services'; import type { OperationParams, UrlParams } from '@rocket.chat/rest-typings'; +import bodyParser from 'body-parser'; import cors from 'cors'; import type { Request, Response } from 'express'; import express from 'express'; @@ -33,7 +34,7 @@ settings.watch('API_CORS_Origin', (value: string) => { : []; }); -WebApp.connectHandlers.use(apiServer); +WebApp.rawConnectHandlers.use(apiServer); // eslint-disable-next-line new-cap const router = express.Router(); @@ -89,7 +90,7 @@ const corsOptions: cors.CorsOptions = { }, }; -apiServer.use('/api/apps/ui.interaction/', cors(corsOptions), router); // didn't have the rateLimiter option +apiServer.use('/api/apps/ui.interaction/', bodyParser.json(), cors(corsOptions), router); // didn't have the rateLimiter option type UiKitUserInteractionRequest = Request< UrlParams<'/apps/ui.interaction/:id'>, diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 1d22f40b2590f..c913ffb323134 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -139,6 +139,7 @@ "@types/proxy-from-env": "^1.0.4", "@types/proxyquire": "^1.3.31", "@types/psl": "^1.1.3", + "@types/qs": "^6", "@types/react": "~18.3.17", "@types/react-dom": "~18.3.5", "@types/sanitize-html": "^2.13.0", @@ -352,6 +353,7 @@ "he": "^1.2.0", "highlight.js": "11.8.0", "hljs9": "npm:highlight.js@^9.18.5", + "hono": "^4.6.19", "http-proxy-agent": "^7.0.2", "human-interval": "^2.0.1", "i18next-http-backend": "^1.4.5", @@ -406,6 +408,7 @@ "prometheus-gc-stats": "^0.6.5", "proxy-from-env": "^1.1.0", "psl": "^1.10.0", + "qs": "^6.14.0", "query-string": "^7.1.3", "queue-fifo": "^0.2.6", "re-resizable": "^6.10.1", diff --git a/apps/meteor/server/main.ts b/apps/meteor/server/main.ts index 90ee58fb8a6a1..10f724c745e79 100644 --- a/apps/meteor/server/main.ts +++ b/apps/meteor/server/main.ts @@ -10,6 +10,7 @@ import './settings'; import { configureServer } from './configuration'; import { registerServices } from './services/startup'; import { startup } from './startup'; +import { startRestAPI } from '../app/api/server/api'; import { settings } from '../app/settings/server'; import { startupApp } from '../ee/server'; import { startRocketChat } from '../startRocketChat'; @@ -27,3 +28,4 @@ await Promise.all([configureServer(settings), registerServices(), startup()]); await startRocketChat(); await startupApp(); +await startRestAPI(); diff --git a/apps/meteor/server/oauth2-server/oauth.ts b/apps/meteor/server/oauth2-server/oauth.ts index 7cf7b24d453d3..e52e5fabfac4f 100644 --- a/apps/meteor/server/oauth2-server/oauth.ts +++ b/apps/meteor/server/oauth2-server/oauth.ts @@ -21,6 +21,17 @@ export class OAuth2Server { this.config = config; this.app = express(); + this.app.use( + '/oauth/*', + express.json({ + limit: '50mb', + }), + express.urlencoded({ + extended: true, + limit: '50mb', + }), + express.query({}), + ); this.oauth = new OAuthServer({ model: new Model(this.config), diff --git a/apps/meteor/server/services/omnichannel-integrations/providers/mobex.ts b/apps/meteor/server/services/omnichannel-integrations/providers/mobex.ts index d036345663cd4..c1e5e32018926 100644 --- a/apps/meteor/server/services/omnichannel-integrations/providers/mobex.ts +++ b/apps/meteor/server/services/omnichannel-integrations/providers/mobex.ts @@ -1,7 +1,6 @@ import { Base64 } from '@rocket.chat/base64'; import type { ISMSProvider, ServiceData, SMSProviderResult, SMSProviderResponse } from '@rocket.chat/core-typings'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import type { Request } from 'express'; import { settings } from '../../../../app/settings/server'; import { SystemLogger } from '../../../lib/logger/system'; @@ -197,7 +196,7 @@ export class Mobex implements ISMSProvider { }; } - validateRequest(_request: Request): boolean { + async validateRequest(_request: Request): Promise { return true; } diff --git a/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts b/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts index d2f89d35e7c5d..7d4c4a48e1a20 100644 --- a/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts +++ b/apps/meteor/server/services/omnichannel-integrations/providers/twilio.ts @@ -1,7 +1,6 @@ import { api } from '@rocket.chat/core-services'; import type { ISMSProvider, ServiceData, SMSProviderResponse, SMSProviderResult } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; -import type { Request } from 'express'; import filesize from 'filesize'; import twilio from 'twilio'; @@ -245,7 +244,7 @@ export class Twilio implements ISMSProvider { }; } - isRequestFromTwilio(signature: string, request: Request): boolean { + async isRequestFromTwilio(signature: string, request: Request): Promise { const authToken = settings.get('SMS_Twilio_authToken'); let siteUrl = settings.get('Site_Url'); if (siteUrl.endsWith('/')) { @@ -257,17 +256,23 @@ export class Twilio implements ISMSProvider { return false; } - const twilioUrl = request.originalUrl ? `${siteUrl}${request.originalUrl}` : `${siteUrl}/api/v1/livechat/sms-incoming/twilio`; + const twilioUrl = request.url ? `${siteUrl}${request.url}` : `${siteUrl}/api/v1/livechat/sms-incoming/twilio`; - return twilio.validateRequest(authToken, signature, twilioUrl, request.body); + let body = {}; + try { + body = await request.json(); + // eslint-disable-next-line no-empty + } catch {} + + return twilio.validateRequest(authToken, signature, twilioUrl, body); } - validateRequest(request: Request): boolean { + async validateRequest(request: Request): Promise { // We're not getting original twilio requests on CI :p if (process.env.TEST_MODE === 'true') { return true; } - const twilioHeader = request.headers['x-twilio-signature'] || ''; + const twilioHeader = request.headers.get('x-twilio-signature') || ''; const twilioSignature = Array.isArray(twilioHeader) ? twilioHeader[0] : twilioHeader; return this.isRequestFromTwilio(twilioSignature, request); } diff --git a/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts b/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts index aa42bacad6243..063070a30e7e8 100644 --- a/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts +++ b/apps/meteor/server/services/omnichannel-integrations/providers/voxtelesys.ts @@ -2,7 +2,6 @@ import { api } from '@rocket.chat/core-services'; import type { ISMSProvider, ServiceData, SMSProviderResponse } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; -import type { Request } from 'express'; import filesize from 'filesize'; import { settings } from '../../../../app/settings/server'; @@ -163,7 +162,7 @@ export class Voxtelesys implements ISMSProvider { }; } - validateRequest(_request: Request): boolean { + async validateRequest(_request: Request): Promise { return true; } diff --git a/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts b/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts index 5130bbe59a99f..119abd3e2f0ec 100644 --- a/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts +++ b/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts @@ -8,7 +8,7 @@ describe('getModifiedHttpHeaders', () => { 'x-auth-token': '12345', 'some-other-header': 'value', }; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result['x-auth-token']).to.equal('[redacted]'); expect(result['some-other-header']).to.equal('value'); }); @@ -17,7 +17,7 @@ describe('getModifiedHttpHeaders', () => { const inputHeaders = { 'some-other-header': 'value', }; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result).to.deep.equal(inputHeaders); }); @@ -26,7 +26,7 @@ describe('getModifiedHttpHeaders', () => { cookie: 'session_id=abc123; rc_token=98765; other_cookie=value', }; const expectedCookies = 'session_id=abc123; rc_token=[redacted]; other_cookie=value'; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result.cookie).to.equal(expectedCookies); }); @@ -34,7 +34,7 @@ describe('getModifiedHttpHeaders', () => { const inputHeaders = { cookie: 'session_id=abc123; other_cookie=value', }; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result.cookie).to.equal(inputHeaders.cookie); }); @@ -42,7 +42,7 @@ describe('getModifiedHttpHeaders', () => { const inputHeaders = { 'some-other-header': 'value', }; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result).to.deep.equal(inputHeaders); }); @@ -52,7 +52,7 @@ describe('getModifiedHttpHeaders', () => { 'cookie': 'session_id=abc123; rc_token=98765; other_cookie=value', }; const expectedCookies = 'session_id=abc123; rc_token=[redacted]; other_cookie=value'; - const result = getModifiedHttpHeaders(inputHeaders); + const result = getModifiedHttpHeaders(new Headers(inputHeaders)); expect(result['x-auth-token']).to.equal('[redacted]'); expect(result.cookie).to.equal(expectedCookies); }); diff --git a/apps/meteor/tests/unit/server/services/omnichannel-integrations/providers/twilio.spec.ts b/apps/meteor/tests/unit/server/services/omnichannel-integrations/providers/twilio.spec.ts index dd0a62da3ad1e..9ee9471d8b41b 100644 --- a/apps/meteor/tests/unit/server/services/omnichannel-integrations/providers/twilio.spec.ts +++ b/apps/meteor/tests/unit/server/services/omnichannel-integrations/providers/twilio.spec.ts @@ -53,7 +53,7 @@ describe('Twilio Request Validation', () => { twilioStub.isRequestFromTwilio.reset(); }); - it('should not validate a request when process.env.TEST_MODE is true', () => { + it('should not validate a request when process.env.TEST_MODE is true', async () => { process.env.TEST_MODE = 'true'; const twilio = new Twilio(); @@ -63,10 +63,10 @@ describe('Twilio Request Validation', () => { }, }; - expect(twilio.validateRequest(request)).to.be.true; + expect(await twilio.validateRequest(request)).to.be.true; }); - it('should validate a request when process.env.TEST_MODE is false', () => { + it('should validate a request when process.env.TEST_MODE is false', async () => { process.env.TEST_MODE = 'false'; settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); @@ -81,15 +81,21 @@ describe('Twilio Request Validation', () => { const request = { headers: { - 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.true; + expect(await twilio.validateRequest(request)).to.be.true; }); - it('should validate a request when query string is present', () => { + it('should validate a request when query string is present', async () => { process.env.TEST_MODE = 'false'; settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); @@ -103,17 +109,23 @@ describe('Twilio Request Validation', () => { }; const request = { - originalUrl: '/api/v1/livechat/sms-incoming/twilio?department=1', + url: '/api/v1/livechat/sms-incoming/twilio?department=1', headers: { - 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio?department=1', requestBody), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio?department=1', requestBody), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.true; + expect(await twilio.validateRequest(request)).to.be.true; }); - it('should reject a request where signature doesnt match', () => { + it('should reject a request where signature doesnt match', async () => { settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); settingsStub.get.withArgs('Site_Url').returns('https://example.com'); @@ -126,15 +138,21 @@ describe('Twilio Request Validation', () => { const request = { headers: { - 'x-twilio-signature': getSignature('anotherAuthToken', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('anotherAuthToken', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.false; + expect(await twilio.validateRequest(request)).to.be.false; }); - it('should reject a request where signature is missing', () => { + it('should reject a request where signature is missing', async () => { settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); settingsStub.get.withArgs('Site_Url').returns('https://example.com'); @@ -146,14 +164,16 @@ describe('Twilio Request Validation', () => { }; const request = { - headers: {}, - body: requestBody, + headers: { + get: () => null, + }, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.false; + expect(await twilio.validateRequest(request)).to.be.false; }); - it('should reject a request where the signature doesnt correspond body', () => { + it('should reject a request where the signature doesnt correspond body', async () => { settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); settingsStub.get.withArgs('Site_Url').returns('https://example.com'); @@ -166,15 +186,21 @@ describe('Twilio Request Validation', () => { const request = { headers: { - 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', {}), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', {}), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.false; + expect(await twilio.validateRequest(request)).to.be.false; }); - it('should return false if URL is not provided', () => { + it('should return false if URL is not provided', async () => { process.env.TEST_MODE = 'false'; settingsStub.get.withArgs('SMS_Twilio_authToken').returns('test'); @@ -189,15 +215,21 @@ describe('Twilio Request Validation', () => { const request = { headers: { - 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.false; + expect(await twilio.validateRequest(request)).to.be.false; }); - it('should return false if authToken is not provided', () => { + it('should return false if authToken is not provided', async () => { process.env.TEST_MODE = 'false'; settingsStub.get.withArgs('SMS_Twilio_authToken').returns(''); @@ -212,11 +244,17 @@ describe('Twilio Request Validation', () => { const request = { headers: { - 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + get: (param: string) => { + const headers: Record = { + 'x-twilio-signature': getSignature('test', 'https://example.com/api/v1/livechat/sms-incoming/twilio', requestBody), + }; + + return headers[param]; + }, }, - body: requestBody, + json: () => requestBody, }; - expect(twilio.validateRequest(request)).to.be.false; + expect(await twilio.validateRequest(request)).to.be.false; }); }); diff --git a/packages/core-typings/src/omnichannel/sms.ts b/packages/core-typings/src/omnichannel/sms.ts index 49364da2b8c36..c29437910066d 100644 --- a/packages/core-typings/src/omnichannel/sms.ts +++ b/packages/core-typings/src/omnichannel/sms.ts @@ -1,5 +1,3 @@ -import type { Request } from 'express'; - type ServiceMedia = { url: string; contentType: string; @@ -29,7 +27,7 @@ export interface ISMSProviderConstructor { export interface ISMSProvider { parse(data: unknown): ServiceData; - validateRequest(request: Request): boolean; + validateRequest(request: Request): Promise; sendBatch?(from: string, to: string[], message: string): Promise; response(): SMSProviderResponse; diff --git a/yarn.lock b/yarn.lock index ab2940cbbf036..c5571301a3671 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8760,6 +8760,7 @@ __metadata: "@types/proxy-from-env": "npm:^1.0.4" "@types/proxyquire": "npm:^1.3.31" "@types/psl": "npm:^1.1.3" + "@types/qs": "npm:^6" "@types/react": "npm:~18.3.17" "@types/react-dom": "npm:~18.3.5" "@types/sanitize-html": "npm:^2.13.0" @@ -8857,6 +8858,7 @@ __metadata: he: "npm:^1.2.0" highlight.js: "npm:11.8.0" hljs9: "npm:highlight.js@^9.18.5" + hono: "npm:^4.6.19" http-proxy-agent: "npm:^7.0.2" human-interval: "npm:^2.0.1" i18next: "npm:~23.4.9" @@ -8929,6 +8931,7 @@ __metadata: proxy-from-env: "npm:^1.1.0" proxyquire: "npm:^2.1.3" psl: "npm:^1.10.0" + qs: "npm:^6.14.0" query-string: "npm:^7.1.3" queue-fifo: "npm:^0.2.6" raw-loader: "npm:~4.0.2" @@ -12724,6 +12727,13 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:^6": + version: 6.9.18 + resolution: "@types/qs@npm:6.9.18" + checksum: 10/152fab96efd819cc82ae67c39f089df415da6deddb48f1680edaaaa4e86a2a597de7b2ff0ad391df66d11a07006a08d52c9405e86b8cb8f3d5ba15881fe56cc7 + languageName: node + linkType: hard + "@types/range-parser@npm:*": version: 1.2.4 resolution: "@types/range-parser@npm:1.2.4" @@ -22714,6 +22724,13 @@ __metadata: languageName: node linkType: hard +"hono@npm:^4.6.19": + version: 4.6.19 + resolution: "hono@npm:4.6.19" + checksum: 10/b3e317bdaf868359b68271d5aa395b1561ceedf3ac97f2d76cc5b9e00e01a794f862bdb5d5574b96b284f5456e2be4d42e6f9511a479d1afdf8c09dff75150e7 + languageName: node + linkType: hard + "hosted-git-info@npm:^2.1.4": version: 2.8.9 resolution: "hosted-git-info@npm:2.8.9" @@ -30929,6 +30946,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.14.0": + version: 6.14.0 + resolution: "qs@npm:6.14.0" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10/a60e49bbd51c935a8a4759e7505677b122e23bf392d6535b8fc31c1e447acba2c901235ecb192764013cd2781723dc1f61978b5fdd93cc31d7043d31cdc01974 + languageName: node + linkType: hard + "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3" From be67bb771294c337c28da5e61ae47ab4e32244d1 Mon Sep 17 00:00:00 2001 From: Gustavo Reis Bauer Date: Wed, 9 Apr 2025 08:22:50 -0300 Subject: [PATCH 080/187] fix: typo on app setting save (#35733) --- .changeset/witty-foxes-thank.md | 6 ++++++ .../views/marketplace/AppDetailsPage/AppDetailsPage.tsx | 2 +- packages/i18n/src/locales/en.i18n.json | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/witty-foxes-thank.md diff --git a/.changeset/witty-foxes-thank.md b/.changeset/witty-foxes-thank.md new file mode 100644 index 0000000000000..99ce5cd6e37e4 --- /dev/null +++ b/.changeset/witty-foxes-thank.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Fixes a typo in the app update success toast diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx index f31613ebcd396..2b384e27082f1 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx @@ -76,7 +76,7 @@ const AppDetailsPage = ({ id }: AppDetailsPageProps): ReactElement => { })), ); reset(data); - dispatchToastMessage({ type: 'success', message: `${name} settings saved succesfully` }); + dispatchToastMessage({ type: 'success', message: t('App_Settings_Saved_Successfully', { appName: name }) }); } catch (e: any) { handleAPIError(e); } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 1a7ecd479baeb..4f3c109638bc1 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -562,6 +562,7 @@ "api-bypass-rate-limit_description": "Permission to call api without rate limitation", "APIs": "APIs", "App_Info": "App Info", + "App_Settings_Saved_Successfully": "{{appName}} saved successfully", "Apps_context_enterprise": "Enterprise", "App_has_been_disabled_addon_message_one": "The app {{appNames}} has been disabled because of an invalid add-on. A valid add-on subscription is required to re-enable it", "App_has_been_disabled_addon_message_other": "The apps {{appNames}} have been disabled because of invalid add-ons. A valid add-on subscription is required to re-enable them", From ce8f8b985727c5d6e7877a1ef021f785a8f1111c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Wed, 9 Apr 2025 10:34:56 -0300 Subject: [PATCH 081/187] refactor: remove Meteor from `useAnalytics` (#35723) --- apps/meteor/app/analytics/README.md | 29 ---------------- .../hooks/useAnalytics.ts} | 33 ++++++++++--------- apps/meteor/client/views/root/AppLayout.tsx | 2 +- 3 files changed, 18 insertions(+), 46 deletions(-) delete mode 100644 apps/meteor/app/analytics/README.md rename apps/meteor/{app/analytics/client/loadScript.ts => client/hooks/useAnalytics.ts} (79%) diff --git a/apps/meteor/app/analytics/README.md b/apps/meteor/app/analytics/README.md deleted file mode 100644 index 4381e889f807d..0000000000000 --- a/apps/meteor/app/analytics/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Analytics Tracking - -## Google Analytics - -### Settings -* **Tracking ID**: Google Analytics tracking id found on analytics admin page - looks like UA-xxxxxxxx-x - -## Piwik -Rocket.Chat supports adding Piwik url and site id to track the analytics of your -server. Through this you will be able to see details analytics of per user, -including how many messages a session they send via custom events in Piwik and -how many channels they interact with. - -### Piwik & Google Chrome -Google Chrome has a setting which sends a Do Not Track with each request and by -default Piwik respects that and you have to manually disable that feature inside -of Piwik. [Piwik has great documentation on how to disable this feature.](http://piwik.org/docs/privacy/#step-4-respect-donottrack-preference) - -### Settings -* **URL**: The url where your piwik is located. This is used for generating the tracking code and is required. Recommended format is: `//rocketchat.piwikpro.com/` -* **Client ID**: The client id which this website is. This is a number without any decimals, example: `1` - -## Settings in Rocket.Chat -Settings -> Analytics - -## Features Enabled -* **Messages**: `true/false` determines whether to use custom events to track how many times a user does something with a message. This ranges from sending messages, editing messages, reacting to messages, pinning, starring, and etc. -* **Rooms**: `true/false` determines whether to use custom events to track how many times a user does actions related to a room (channel, direct message, group). This ranges from creating, leaving, archiving, renaming, and etc. -* **Users**: `true/false` determines whether to use custom events to track how many times a user does actions related to users. This ranges from resetting passwords, creating new users, updating profile pictures, etc. diff --git a/apps/meteor/app/analytics/client/loadScript.ts b/apps/meteor/client/hooks/useAnalytics.ts similarity index 79% rename from apps/meteor/app/analytics/client/loadScript.ts rename to apps/meteor/client/hooks/useAnalytics.ts index e6dc12cd77da0..67a2d4d43b819 100644 --- a/apps/meteor/app/analytics/client/loadScript.ts +++ b/apps/meteor/client/hooks/useAnalytics.ts @@ -1,9 +1,6 @@ -import { Meteor } from 'meteor/meteor'; +import { useSetting, useUserId } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { useReactiveValue } from '../../../client/hooks/useReactiveValue'; -import { settings } from '../../settings/client'; - declare global { // eslint-disable-next-line @typescript-eslint/naming-convention interface Window { @@ -20,10 +17,19 @@ declare global { } export const useAnalytics = (): void => { - const uid = useReactiveValue(() => Meteor.userId()); + const uid = useUserId(); + + const googleAnalyticsEnabled = useSetting('GoogleAnalytics_enabled', false); + const googleId = useSetting('GoogleAnalytics_ID', ''); + + const piwiEnabled = useSetting('PiwikAnalytics_enabled', false); + const piwikUrl = useSetting('PiwikAnalytics_url', ''); - const googleId = useReactiveValue(() => settings.get('GoogleAnalytics_enabled') && settings.get('GoogleAnalytics_ID')); - const piwikUrl = useReactiveValue(() => settings.get('PiwikAnalytics_enabled') && settings.get('PiwikAnalytics_url')); + const piwikSiteId = useSetting('PiwikAnalytics_siteId', ''); + const piwikPrependDomain = useSetting('PiwikAnalytics_prependDomain', ''); + const piwikCookieDomain = useSetting('PiwikAnalytics_cookieDomain', ''); + const piwikDomains = useSetting('PiwikAnalytics_domains', ''); + const piwikAdditionalTracker = useSetting('PiwikAdditionalTrackers', ''); useEffect(() => { if (uid) { @@ -33,7 +39,7 @@ export const useAnalytics = (): void => { }, [uid]); useEffect(() => { - if (!googleId) { + if (!googleAnalyticsEnabled || !googleId) { return; } if (googleId.startsWith('G-')) { @@ -73,20 +79,15 @@ export const useAnalytics = (): void => { window.ga?.('create', googleId, 'auto'); window.ga?.('send', 'pageview'); } - }, [googleId, uid]); + }, [googleAnalyticsEnabled, googleId, uid]); useEffect(() => { - if (!piwikUrl) { + if (!piwiEnabled || !piwikUrl) { document.getElementById('piwik-analytics')?.remove(); window._paq = []; return; } - const piwikSiteId = piwikUrl && settings.get('PiwikAnalytics_siteId'); - const piwikPrependDomain = piwikUrl && settings.get('PiwikAnalytics_prependDomain'); - const piwikCookieDomain = piwikUrl && settings.get('PiwikAnalytics_cookieDomain'); - const piwikDomains = piwikUrl && settings.get('PiwikAnalytics_domains'); - const piwikAdditionalTracker = piwikUrl && settings.get('PiwikAdditionalTrackers'); window._paq = window._paq || []; window._paq.push(['trackPageView']); @@ -136,5 +137,5 @@ export const useAnalytics = (): void => { g.src = `${piwikUrl}js/`; s.parentNode?.insertBefore(g, s); })(); - }, [piwikUrl]); + }, [piwiEnabled, piwikAdditionalTracker, piwikCookieDomain, piwikDomains, piwikPrependDomain, piwikSiteId, piwikUrl]); }; diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index fafb6a9f3176d..6e2d04fa934b3 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -12,7 +12,6 @@ import { useStoreCookiesOnLogin } from './hooks/useStoreCookiesOnLogin'; import { useUpdateVideoConfUser } from './hooks/useUpdateVideoConfUser'; import { useWebRTC } from './hooks/useWebRTC'; import { useWordPressOAuth } from './hooks/useWordPressOAuth'; -import { useAnalytics } from '../../../app/analytics/client/loadScript'; import { useCorsSSLConfig } from '../../../app/cors/client/useCorsSSLConfig'; import { useDolphin } from '../../../app/dolphin/client/hooks/useDolphin'; import { useDrupal } from '../../../app/drupal/client/hooks/useDrupal'; @@ -25,6 +24,7 @@ import { useTokenPassAuth } from '../../../app/tokenpass/client/hooks/useTokenPa import { useNotificationPermission } from '../../hooks/notification/useNotificationPermission'; import { useNotificationUserCalendar } from '../../hooks/notification/useNotificationUserCalendar'; import { useNotifyUser } from '../../hooks/notification/useNotifyUser'; +import { useAnalytics } from '../../hooks/useAnalytics'; import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking'; import { useAutoupdate } from '../../hooks/useAutoupdate'; import { useLoadRoomForAllowedAnonymousRead } from '../../hooks/useLoadRoomForAllowedAnonymousRead'; From 3008945dc3bbcb6eae05bea9d0d53842451cf286 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 9 Apr 2025 14:08:18 -0300 Subject: [PATCH 082/187] refactor: Break big `LivechatTyped` into smaller files - move guest functions (#35465) --- .../app/apps/server/bridges/livechat.ts | 5 +- .../app/livechat/imports/server/rest/sms.ts | 4 +- .../app/livechat/server/api/v1/message.ts | 4 +- .../meteor/app/livechat/server/api/v1/room.ts | 3 +- .../app/livechat/server/api/v1/visitor.ts | 5 +- .../app/livechat/server/lib/LivechatTyped.ts | 84 --------- .../server/lib/contacts/registerGuestData.ts | 41 ----- apps/meteor/app/livechat/server/lib/guests.ts | 162 ++++++++++++++++++ .../app/livechat/server/lib/tracking.ts | 34 +--- .../EmailInbox/EmailInbox_Incoming.ts | 4 +- 10 files changed, 177 insertions(+), 169 deletions(-) delete mode 100644 apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts create mode 100644 apps/meteor/app/livechat/server/lib/guests.ts diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 361ba2098bbd0..896b7d0c540da 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -13,6 +13,7 @@ import { deasyncPromise } from '../../../../server/deasync/deasync'; import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; import { closeRoom } from '../../../livechat/server/lib/closeRoom'; import { getRoomMessages } from '../../../livechat/server/lib/getRoomMessages'; +import { registerGuest } from '../../../livechat/server/lib/guests'; import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes'; import { updateMessage, sendMessage } from '../../../livechat/server/lib/messages'; import { createRoom } from '../../../livechat/server/lib/rooms'; @@ -210,7 +211,7 @@ export class AppLivechatBridge extends LivechatBridge { ...(visitor.visitorEmails?.length && { email: visitor.visitorEmails[0].address }), }; - const livechatVisitor = await LivechatTyped.registerGuest(registerData); + const livechatVisitor = await registerGuest(registerData); if (!livechatVisitor) { throw new Error('Invalid visitor, cannot create'); @@ -234,7 +235,7 @@ export class AppLivechatBridge extends LivechatBridge { ...(visitor.visitorEmails?.length && { email: visitor.visitorEmails[0].address }), }; - const livechatVisitor = await LivechatTyped.registerGuest(registerData); + const livechatVisitor = await registerGuest(registerData); return this.orch.getConverters()?.get('visitors').convertVisitor(livechatVisitor); } diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.ts b/apps/meteor/app/livechat/imports/server/rest/sms.ts index 8b476c58886cd..b579f490a4d88 100644 --- a/apps/meteor/app/livechat/imports/server/rest/sms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/sms.ts @@ -20,7 +20,7 @@ import { FileUpload } from '../../../../file-upload/server'; import { checkUrlForSsrf } from '../../../../lib/server/functions/checkUrlForSsrf'; import { settings } from '../../../../settings/server'; import { setCustomField } from '../../../server/api/lib/customFields'; -import { Livechat as LivechatTyped } from '../../../server/lib/LivechatTyped'; +import { registerGuest } from '../../../server/lib/guests'; import type { ILivechatMessage } from '../../../server/lib/localTypes'; import { sendMessage } from '../../../server/lib/messages'; import { createRoom } from '../../../server/lib/rooms'; @@ -76,7 +76,7 @@ const defineVisitor = async (smsNumber: string, targetDepartment?: string) => { data.department = targetDepartment; } - const livechatVisitor = await LivechatTyped.registerGuest(data); + const livechatVisitor = await registerGuest(data); if (!livechatVisitor) { throw new Meteor.Error('error-invalid-visitor', 'Invalid visitor'); diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts index 5fbeb138e67f2..75efff450948c 100644 --- a/apps/meteor/app/livechat/server/api/v1/message.ts +++ b/apps/meteor/app/livechat/server/api/v1/message.ts @@ -17,7 +17,7 @@ import { isWidget } from '../../../../api/server/helpers/isWidget'; import { loadMessageHistory } from '../../../../lib/server/functions/loadMessageHistory'; import { settings } from '../../../../settings/server'; import { normalizeMessageFileUpload } from '../../../../utils/server/functions/normalizeMessageFileUpload'; -import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; +import { registerGuest } from '../../lib/guests'; import { updateMessage, deleteMessage, sendMessage } from '../../lib/messages'; import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat'; @@ -269,7 +269,7 @@ API.v1.addRoute( const guest: typeof this.bodyParams.visitor & { connectionData?: unknown } = this.bodyParams.visitor; guest.connectionData = normalizeHttpHeaderData(this.request.headers); - const visitor = await LivechatTyped.registerGuest(guest); + const visitor = await registerGuest(guest); if (!visitor) { throw new Error('error-livechat-visitor-registration'); } diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 19e9a716c4635..9c07a1656d293 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -25,6 +25,7 @@ import { settings as rcSettings } from '../../../../settings/server'; import { normalizeTransferredByData } from '../../lib/Helper'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { closeRoom } from '../../lib/closeRoom'; +import { saveGuest } from '../../lib/guests'; import type { CloseRoomParams } from '../../lib/localTypes'; import { livechatLogger } from '../../lib/logger'; import { createRoom, saveRoomInfo } from '../../lib/rooms'; @@ -410,7 +411,7 @@ API.v1.addRoute( } // We want this both operations to be concurrent, so we have to go with Promise.allSettled - const result = await Promise.allSettled([LivechatTyped.saveGuest(guestData, this.userId), saveRoomInfo(roomData)]); + const result = await Promise.allSettled([saveGuest(guestData, this.userId), saveRoomInfo(roomData)]); const firstError = result.find((item) => item.status === 'rejected'); if (firstError) { diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index def6ef84edc9b..cddcd8a5ca02d 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -7,6 +7,7 @@ import { callbacks } from '../../../../../lib/callbacks'; import { API } from '../../../../api/server'; import { settings } from '../../../../settings/server'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; +import { registerGuest, removeGuest } from '../../lib/guests'; import { saveRoomInfo } from '../../lib/rooms'; import { validateRequiredCustomFields } from '../../lib/validateRequiredCustomFields'; import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; @@ -57,7 +58,7 @@ API.v1.addRoute( connectionData: normalizeHttpHeaderData(this.request.headers), }; - const visitor = await LivechatTyped.registerGuest(guest); + const visitor = await registerGuest(guest); if (!visitor) { throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', { method: 'livechat/visitor', @@ -183,7 +184,7 @@ API.v1.addRoute('livechat/visitor/:token', { } const { _id } = visitor; - const result = await LivechatTyped.removeGuest(_id); + const result = await removeGuest(_id); if (!result.modifiedCount) { throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor'); } diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 38d5375b0380d..62a2163af1bc9 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,4 +1,3 @@ -import { Apps, AppEvents } from '@rocket.chat/apps'; import { Message, VideoConf, api } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, @@ -33,12 +32,10 @@ import type { Filter } from 'mongodb'; import UAParser from 'ua-parser-js'; import { callbacks } from '../../../../lib/callbacks'; -import { trim } from '../../../../lib/utils/stringUtils'; import { i18n } from '../../../../server/lib/i18n'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; import { canAccessRoomAsync } from '../../../authorization/server'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; import { @@ -51,9 +48,6 @@ import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; import { parseAgentCustomFields, updateDepartmentAgents, normalizeTransferredByData } from './Helper'; import { RoutingManager } from './RoutingManager'; -import { Visitors, type RegisterGuestType } from './Visitors'; -import { registerGuestData } from './contacts/registerGuestData'; -import { cleanGuestHistory } from './tracking'; type AKeyOf = { [K in keyof T]?: T[K]; @@ -168,16 +162,6 @@ class LivechatClass { } } - async registerGuest(newData: RegisterGuestType): Promise { - const result = await Visitors.registerGuest(newData); - - if (result) { - await registerGuestData(newData, result); - } - - return result; - } - private async getBotAgents(department?: string) { if (department) { return LivechatDepartmentAgents.getBotsForDepartment(department); @@ -331,16 +315,6 @@ class LivechatClass { } } - async removeGuest(_id: string) { - const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } }); - if (!guest) { - throw new Error('error-invalid-guest'); - } - - await cleanGuestHistory(guest); - return LivechatVisitors.disableById(_id); - } - async setUserStatusLivechatIf(userId: string, status: ILivechatAgentStatus, condition?: Filter, fields?: AKeyOf) { const result = await Users.setLivechatStatusIf(userId, status, condition, fields); @@ -431,64 +405,6 @@ class LivechatClass { await Message.saveSystemMessageAndNotifyUser('livechat_transfer_history', room._id, '', { _id, username }, transferMessage); } - async saveGuest(guestData: Pick & { email?: string; phone?: string }, userId: string) { - const { _id, name, email, phone, livechatData = {} } = guestData; - - const visitor = await LivechatVisitors.findOneById(_id, { projection: { _id: 1 } }); - if (!visitor) { - throw new Error('error-invalid-visitor'); - } - - this.logger.debug({ msg: 'Saving guest', guestData }); - const updateData: { - name?: string | undefined; - username?: string | undefined; - email?: string | undefined; - phone?: string | undefined; - livechatData: { - [k: string]: any; - }; - } = { livechatData: {} }; - - if (name) { - updateData.name = name; - } - if (email) { - updateData.email = email; - } - if (phone) { - updateData.phone = phone; - } - - const customFields: Record = {}; - - if ((!userId || (await hasPermissionAsync(userId, 'edit-livechat-room-customfields'))) && Object.keys(livechatData).length) { - this.logger.debug({ msg: `Saving custom fields for visitor ${_id}`, livechatData }); - for await (const field of LivechatCustomField.findByScope('visitor')) { - if (!livechatData.hasOwnProperty(field._id)) { - continue; - } - const value = trim(livechatData[field._id]); - if (value !== '' && field.regexp !== undefined && field.regexp !== '') { - const regexp = new RegExp(field.regexp); - if (!regexp.test(value)) { - throw new Error(i18n.t('error-invalid-custom-field-value')); - } - } - customFields[field._id] = value; - } - updateData.livechatData = customFields; - Livechat.logger.debug(`About to update ${Object.keys(customFields).length} custom fields for visitor ${_id}`); - } - const ret = await LivechatVisitors.saveGuestById(_id, updateData); - - setImmediate(() => { - void Apps.self?.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id); - }); - - return ret; - } - async setCustomFields({ token, key, value, overwrite }: { key: string; value: string; overwrite: boolean; token: string }) { Livechat.logger.debug(`Setting custom fields data for visitor with token ${token}`); diff --git a/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts b/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts deleted file mode 100644 index 471104aecae9d..0000000000000 --- a/apps/meteor/app/livechat/server/lib/contacts/registerGuestData.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { AtLeast, ILivechatVisitor } from '@rocket.chat/core-typings'; -import { LivechatContacts } from '@rocket.chat/models'; -import { wrapExceptions } from '@rocket.chat/tools'; - -import { validateEmail } from '../Helper'; -import type { RegisterGuestType } from '../Visitors'; -import { ContactMerger, type FieldAndValue } from './ContactMerger'; - -export async function registerGuestData( - { name, phone, email, username }: Pick, - visitor: AtLeast, -): Promise { - const validatedEmail = - email && - wrapExceptions(() => { - const trimmedEmail = email.trim().toLowerCase(); - validateEmail(trimmedEmail); - return trimmedEmail; - }).suppress(); - - const fields = [ - { type: 'name', value: name }, - { type: 'phone', value: phone?.number }, - { type: 'email', value: validatedEmail }, - { type: 'username', value: username || visitor.username }, - ].filter((field) => Boolean(field.value)) as FieldAndValue[]; - - if (!fields.length) { - return; - } - - // If a visitor was updated who already had contacts, load up the contacts and update that information as well - const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray(); - for await (const contact of contacts) { - await ContactMerger.mergeFieldsIntoContact({ - fields, - contact, - conflictHandlingMode: contact.unknown ? 'overwrite' : 'conflict', - }); - } -} diff --git a/apps/meteor/app/livechat/server/lib/guests.ts b/apps/meteor/app/livechat/server/lib/guests.ts new file mode 100644 index 0000000000000..e32c6c9c49372 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/guests.ts @@ -0,0 +1,162 @@ +import { Apps, AppEvents } from '@rocket.chat/apps'; +import type { ILivechatVisitor } from '@rocket.chat/core-typings'; +import { + LivechatVisitors, + LivechatCustomField, + LivechatInquiry, + LivechatRooms, + Messages, + ReadReceipts, + Subscriptions, + LivechatContacts, +} from '@rocket.chat/models'; +import { wrapExceptions } from '@rocket.chat/tools'; + +import { validateEmail } from './Helper'; +import type { RegisterGuestType } from './Visitors'; +import { Visitors } from './Visitors'; +import { ContactMerger, type FieldAndValue } from './contacts/ContactMerger'; +import { livechatLogger } from './logger'; +import { trim } from '../../../../lib/utils/stringUtils'; +import { i18n } from '../../../../server/lib/i18n'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { FileUpload } from '../../../file-upload/server'; +import { notifyOnSubscriptionChanged, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; + +export async function saveGuest( + guestData: Pick & { email?: string; phone?: string }, + userId: string, +) { + const { _id, name, email, phone, livechatData = {} } = guestData; + + const visitor = await LivechatVisitors.findOneById(_id, { projection: { _id: 1 } }); + if (!visitor) { + throw new Error('error-invalid-visitor'); + } + + livechatLogger.debug({ msg: 'Saving guest', guestData }); + const updateData: { + name?: string | undefined; + username?: string | undefined; + email?: string | undefined; + phone?: string | undefined; + livechatData: { + [k: string]: any; + }; + } = { livechatData: {} }; + + if (name) { + updateData.name = name; + } + if (email) { + updateData.email = email; + } + if (phone) { + updateData.phone = phone; + } + + const customFields: Record = {}; + + if ((!userId || (await hasPermissionAsync(userId, 'edit-livechat-room-customfields'))) && Object.keys(livechatData).length) { + livechatLogger.debug({ msg: `Saving custom fields for visitor ${_id}`, livechatData }); + for await (const field of LivechatCustomField.findByScope('visitor')) { + if (!livechatData.hasOwnProperty(field._id)) { + continue; + } + const value = trim(livechatData[field._id]); + if (value !== '' && field.regexp !== undefined && field.regexp !== '') { + const regexp = new RegExp(field.regexp); + if (!regexp.test(value)) { + throw new Error(i18n.t('error-invalid-custom-field-value')); + } + } + customFields[field._id] = value; + } + updateData.livechatData = customFields; + livechatLogger.debug(`About to update ${Object.keys(customFields).length} custom fields for visitor ${_id}`); + } + const ret = await LivechatVisitors.saveGuestById(_id, updateData); + + setImmediate(() => { + void Apps.self?.triggerEvent(AppEvents.IPostLivechatGuestSaved, _id); + }); + + return ret; +} + +export async function removeGuest(_id: string) { + const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } }); + if (!guest) { + throw new Error('error-invalid-guest'); + } + + await cleanGuestHistory(guest.token); + return LivechatVisitors.disableById(_id); +} + +export async function registerGuest(newData: RegisterGuestType): Promise { + const visitor = await Visitors.registerGuest(newData); + if (!visitor) { + return null; + } + + const { name, phone, email, username } = newData; + + const validatedEmail = + email && + wrapExceptions(() => { + const trimmedEmail = email.trim().toLowerCase(); + validateEmail(trimmedEmail); + return trimmedEmail; + }).suppress(); + + const fields = [ + { type: 'name', value: name }, + { type: 'phone', value: phone?.number }, + { type: 'email', value: validatedEmail }, + { type: 'username', value: username || visitor.username }, + ].filter((field) => Boolean(field.value)) as FieldAndValue[]; + + if (!fields.length) { + return null; + } + + // If a visitor was updated who already had contacts, load up the contacts and update that information as well + const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray(); + for await (const contact of contacts) { + await ContactMerger.mergeFieldsIntoContact({ + fields, + contact, + conflictHandlingMode: contact.unknown ? 'overwrite' : 'conflict', + }); + } + + return visitor; +} + +async function cleanGuestHistory(token: string) { + // This shouldn't be possible, but just in case + if (!token) { + throw new Error('error-invalid-guest'); + } + + const cursor = LivechatRooms.findByVisitorToken(token); + for await (const room of cursor) { + await Promise.all([ + Subscriptions.removeByRoomId(room._id, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), + FileUpload.removeFilesByRoomId(room._id), + Messages.removeByRoomId(room._id), + ReadReceipts.removeByRoomId(room._id), + ]); + } + + await LivechatRooms.removeByVisitorToken(token); + + const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray(); + await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id)); + void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed'); +} diff --git a/apps/meteor/app/livechat/server/lib/tracking.ts b/apps/meteor/app/livechat/server/lib/tracking.ts index 5e21bb4c38e46..bfbcf9912212e 100644 --- a/apps/meteor/app/livechat/server/lib/tracking.ts +++ b/apps/meteor/app/livechat/server/lib/tracking.ts @@ -1,10 +1,7 @@ import { Message } from '@rocket.chat/core-services'; -import type { ILivechatVisitor } from '@rocket.chat/core-typings'; -import { LivechatInquiry, LivechatRooms, Messages, ReadReceipts, Subscriptions, Users } from '@rocket.chat/models'; +import { Users } from '@rocket.chat/models'; import { livechatLogger } from './logger'; -import { FileUpload } from '../../../file-upload/server'; -import { notifyOnSubscriptionChanged, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; type PageInfo = { title: string; location: { href: string }; change: string }; @@ -55,32 +52,3 @@ export async function savePageHistory(token: string, roomId: string | undefined, // @ts-expect-error: Investigating on which case we won't receive a roomId and where that history is supposed to be stored return Message.saveSystemMessage('livechat_navigation_history', roomId, `${pageTitle} - ${pageUrl}`, user, extraData); } - -export async function cleanGuestHistory(guest: ILivechatVisitor) { - const { token } = guest; - - // This shouldn't be possible, but just in case - if (!token) { - throw new Error('error-invalid-guest'); - } - - const cursor = LivechatRooms.findByVisitorToken(token); - for await (const room of cursor) { - await Promise.all([ - Subscriptions.removeByRoomId(room._id, { - async onTrash(doc) { - void notifyOnSubscriptionChanged(doc, 'removed'); - }, - }), - FileUpload.removeFilesByRoomId(room._id), - Messages.removeByRoomId(room._id), - ReadReceipts.removeByRoomId(room._id), - ]); - } - - await LivechatRooms.removeByVisitorToken(token); - - const livechatInquiries = await LivechatInquiry.findIdsByVisitorToken(token).toArray(); - await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id)); - void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed'); -} diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts index 1825442be8e78..2fc4f164f9ed5 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts @@ -14,9 +14,9 @@ import { stripHtml } from 'string-strip-html'; import { logger } from './logger'; import { FileUpload } from '../../../app/file-upload/server'; import { notifyOnMessageChange } from '../../../app/lib/server/lib/notifyListener'; -import { Livechat as LivechatTyped } from '../../../app/livechat/server/lib/LivechatTyped'; import { QueueManager } from '../../../app/livechat/server/lib/QueueManager'; import { setDepartmentForGuest } from '../../../app/livechat/server/lib/departmentsLib'; +import { registerGuest } from '../../../app/livechat/server/lib/guests'; import { sendMessage } from '../../../app/livechat/server/lib/messages'; import { settings } from '../../../app/settings/server'; import { i18n } from '../../lib/i18n'; @@ -42,7 +42,7 @@ async function getGuestByEmail(email: string, name: string, department = ''): Pr return guest; } - const livechatVisitor = await LivechatTyped.registerGuest({ + const livechatVisitor = await registerGuest({ token: Random.id(), name: name || email, email, From 00609987ba051ef328481c79626957768da3ca23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:09:40 -0300 Subject: [PATCH 083/187] regression: new room notification being called wrongly (#35753) --- .../hooks/notification/useNotifyUser.ts | 13 +--- .../CustomSoundProvider.tsx | 71 ++++++++++--------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/apps/meteor/client/hooks/notification/useNotifyUser.ts b/apps/meteor/client/hooks/notification/useNotifyUser.ts index d9de0d3115e31..d2762144e94ba 100644 --- a/apps/meteor/client/hooks/notification/useNotifyUser.ts +++ b/apps/meteor/client/hooks/notification/useNotifyUser.ts @@ -6,7 +6,6 @@ import { useEffect } from 'react'; import { useEmbeddedLayout } from '../useEmbeddedLayout'; import { useDesktopNotification } from './useDesktopNotification'; import { useNewMessageNotification } from './useNewMessageNotification'; -import { CachedChatSubscription } from '../../../app/models/client'; import { RoomManager } from '../../lib/RoomManager'; import { fireGlobalEvent } from '../../lib/utils/fireGlobalEvent'; @@ -72,17 +71,11 @@ export const useNotifyUser = () => { void notifyNewRoom(sub); }); - const handle = CachedChatSubscription.collection.find().observe({ - added: (sub) => { - void notifyNewRoom(sub); - }, - }); - return () => { unsubNotification(); unsubSubs(); - handle.stop(); - notificationSounds.stopNewRoom(); }; - }, [isLayoutEmbedded, notificationSounds, notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user?._id]); + }, [notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user?._id]); + + useEffect(() => () => notificationSounds.stopNewRoom(), [notificationSounds]); }; diff --git a/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx b/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx index 1f13a15600859..82c12f4febb83 100644 --- a/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx +++ b/apps/meteor/client/providers/CustomSoundProvider/CustomSoundProvider.tsx @@ -2,7 +2,7 @@ import type { ICustomSound } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { CustomSoundContext, useStream, useUserPreference } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { useEffect, useRef, type ReactNode } from 'react'; +import { useEffect, useMemo, useRef, type ReactNode } from 'react'; import { defaultSounds, formatVolume, getCustomSoundURL } from './lib/helpers'; import { sdk } from '../../../app/utils/client/lib/SDKClient'; @@ -72,34 +72,43 @@ const CustomSoundProvider = ({ children }: CustomSoundProviderProps) => { } }); - const callSounds = { - playRinger: () => play('ringtone', { loop: true, volume: formatVolume(voipRingerVolume) }), - playDialer: () => play('dialtone', { loop: true, volume: formatVolume(voipRingerVolume) }), - stopRinger: () => stop('ringtone'), - stopDialer: () => stop('dialtone'), - }; - - const voipSounds = { - playRinger: () => play('telephone', { loop: true, volume: formatVolume(voipRingerVolume) }), - playDialer: () => play('outbound-call-ringing', { loop: true, volume: formatVolume(voipRingerVolume) }), - playCallEnded: () => play('call-ended', { loop: false, volume: formatVolume(voipRingerVolume) }), - stopRinger: () => stop('telephone'), - stopDialer: () => stop('outbound-call-ringing'), - stopCallEnded: () => stop('call-ended'), - stopAll: () => { - stop('telephone'); - stop('outbound-call-ringing'); - stop('call-ended'); - }, - }; - - const notificationSounds = { - playNewRoom: () => play(newRoomNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }), - playNewMessage: () => play(newMessageNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }), - playNewMessageLoop: () => play(newMessageNotification, { loop: true, volume: formatVolume(notificationsSoundVolume) }), - stopNewRoom: () => stop(newRoomNotification), - stopNewMessage: () => stop(newMessageNotification), - }; + const contextValue = useMemo(() => { + const notificationSounds = { + playNewRoom: () => play(newRoomNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }), + playNewMessage: () => play(newMessageNotification, { loop: false, volume: formatVolume(notificationsSoundVolume) }), + playNewMessageLoop: () => play(newMessageNotification, { loop: true, volume: formatVolume(notificationsSoundVolume) }), + stopNewRoom: () => stop(newRoomNotification), + stopNewMessage: () => stop(newMessageNotification), + }; + const voipSounds = { + playRinger: () => play('telephone', { loop: true, volume: formatVolume(voipRingerVolume) }), + playDialer: () => play('outbound-call-ringing', { loop: true, volume: formatVolume(voipRingerVolume) }), + playCallEnded: () => play('call-ended', { loop: false, volume: formatVolume(voipRingerVolume) }), + stopRinger: () => stop('telephone'), + stopDialer: () => stop('outbound-call-ringing'), + stopCallEnded: () => stop('call-ended'), + stopAll: () => { + stop('telephone'); + stop('outbound-call-ringing'); + stop('call-ended'); + }, + }; + const callSounds = { + playRinger: () => play('ringtone', { loop: true, volume: formatVolume(voipRingerVolume) }), + playDialer: () => play('dialtone', { loop: true, volume: formatVolume(voipRingerVolume) }), + stopRinger: () => stop('ringtone'), + stopDialer: () => stop('dialtone'), + }; + return { + list, + notificationSounds, + callSounds, + voipSounds, + play, + pause, + stop, + }; + }, [list, newMessageNotification, newRoomNotification, notificationsSoundVolume, pause, play, stop, voipRingerVolume]); useEffect(() => { return streamAll('public-info', ([key]) => { @@ -115,9 +124,7 @@ const CustomSoundProvider = ({ children }: CustomSoundProviderProps) => { }); }, [queryClient, streamAll]); - return ( - - ); + return ; }; export default CustomSoundProvider; From f59cbb9949341169122d7854a5730e2018344433 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 9 Apr 2025 14:18:24 -0300 Subject: [PATCH 084/187] chore: remove custom status meteor calls (server) (#35743) --- .../app/api/server/v1/custom-user-status.ts | 10 +- .../server/methods/deleteCustomUserStatus.ts | 28 ++-- .../methods/insertOrUpdateUserStatus.ts | 150 ++++++++++-------- 3 files changed, 104 insertions(+), 84 deletions(-) diff --git a/apps/meteor/app/api/server/v1/custom-user-status.ts b/apps/meteor/app/api/server/v1/custom-user-status.ts index 4f17c0db1ae89..037928cf1cdcf 100644 --- a/apps/meteor/app/api/server/v1/custom-user-status.ts +++ b/apps/meteor/app/api/server/v1/custom-user-status.ts @@ -4,6 +4,8 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { deleteCustomUserStatus } from '../../../user-status/server/methods/deleteCustomUserStatus'; +import { insertOrUpdateUserStatus } from '../../../user-status/server/methods/insertOrUpdateUserStatus'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; @@ -53,10 +55,10 @@ API.v1.addRoute( const userStatusData = { name: this.bodyParams.name, - statusType: this.bodyParams.statusType, + statusType: this.bodyParams.statusType || '', }; - await Meteor.callAsync('insertOrUpdateUserStatus', userStatusData); + await insertOrUpdateUserStatus(this.userId, userStatusData); const customUserStatus = await CustomUserStatus.findOneByName(userStatusData.name); if (!customUserStatus) { @@ -80,7 +82,7 @@ API.v1.addRoute( return API.v1.failure('The "customUserStatusId" params is required!'); } - await Meteor.callAsync('deleteCustomUserStatus', customUserStatusId); + await deleteCustomUserStatus(this.userId, customUserStatusId); return API.v1.success(); }, @@ -111,7 +113,7 @@ API.v1.addRoute( return API.v1.failure(`No custom user status found with the id of "${userStatusData._id}".`); } - await Meteor.callAsync('insertOrUpdateUserStatus', userStatusData); + await insertOrUpdateUserStatus(this.userId, userStatusData); const customUserStatus = await CustomUserStatus.findOneById(userStatusData._id); diff --git a/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts b/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts index 416bc6f678ede..72ccb9ccbd07d 100644 --- a/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts +++ b/apps/meteor/app/user-status/server/methods/deleteCustomUserStatus.ts @@ -12,20 +12,28 @@ declare module '@rocket.chat/ddp-client' { } } +export const deleteCustomUserStatus = async (userId: string, userStatusID: string): Promise => { + if (!(await hasPermissionAsync(userId, 'manage-user-status'))) { + throw new Meteor.Error('not_authorized'); + } + + const userStatus = await CustomUserStatus.findOneById(userStatusID); + if (userStatus == null) { + throw new Meteor.Error('Custom_User_Status_Error_Invalid_User_Status', 'Invalid user status', { method: 'deleteCustomUserStatus' }); + } + + await CustomUserStatus.removeById(userStatusID); + void api.broadcast('user.deleteCustomStatus', userStatus); + + return true; +}; + Meteor.methods({ async deleteCustomUserStatus(userStatusID) { - if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-user-status'))) { + if (!this.userId) { throw new Meteor.Error('not_authorized'); } - const userStatus = await CustomUserStatus.findOneById(userStatusID); - if (userStatus == null) { - throw new Meteor.Error('Custom_User_Status_Error_Invalid_User_Status', 'Invalid user status', { method: 'deleteCustomUserStatus' }); - } - - await CustomUserStatus.removeById(userStatusID); - void api.broadcast('user.deleteCustomStatus', userStatus); - - return true; + return deleteCustomUserStatus(this.userId, userStatusID); }, }); diff --git a/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts b/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts index 6e034f0306797..16f8dc716b06a 100644 --- a/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts +++ b/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts @@ -8,96 +8,106 @@ import { Meteor } from 'meteor/meteor'; import { trim } from '../../../../lib/utils/stringUtils'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +type InsertOrUpdateUserStatus = { + _id?: string; + name: string; + statusType: string; + status?: string; + emoji?: string; + message?: string; + order?: number; + previousName?: string; + previousStatusType?: string; +}; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - insertOrUpdateUserStatus(userStatusData: { - _id?: string; - name: string; - statusType: string; - status: string; - emoji: string; - message: string; - order: number; - previousName?: string; - previousStatusType?: string; - }): string | boolean; + insertOrUpdateUserStatus(userStatusData: InsertOrUpdateUserStatus): string | boolean; } } -Meteor.methods({ - async insertOrUpdateUserStatus(userStatusData) { - if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-user-status'))) { - throw new Meteor.Error('not_authorized'); - } +export const insertOrUpdateUserStatus = async (userId: string, userStatusData: InsertOrUpdateUserStatus): Promise => { + if (!(await hasPermissionAsync(userId, 'manage-user-status'))) { + throw new Meteor.Error('not_authorized'); + } - if (!trim(userStatusData.name)) { - throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { - method: 'insertOrUpdateUserStatus', - field: 'Name', - }); - } + if (!trim(userStatusData.name)) { + throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { + method: 'insertOrUpdateUserStatus', + field: 'Name', + }); + } - // allow all characters except >, <, &, ", ' - // more practical than allowing specific sets of characters; also allows foreign languages - const nameValidation = /[><&"']/; + // allow all characters except >, <, &, ", ' + // more practical than allowing specific sets of characters; also allows foreign languages + const nameValidation = /[><&"']/; - if (nameValidation.test(userStatusData.name)) { - throw new Meteor.Error('error-input-is-not-a-valid-field', `${userStatusData.name} is not a valid name`, { - method: 'insertOrUpdateUserStatus', - input: userStatusData.name, - field: 'Name', - }); - } + if (nameValidation.test(userStatusData.name)) { + throw new Meteor.Error('error-input-is-not-a-valid-field', `${userStatusData.name} is not a valid name`, { + method: 'insertOrUpdateUserStatus', + input: userStatusData.name, + field: 'Name', + }); + } - let matchingResults = []; + let matchingResults = []; - if (userStatusData._id) { - matchingResults = await CustomUserStatus.findByNameExceptId(userStatusData.name, userStatusData._id).toArray(); - } else { - matchingResults = await CustomUserStatus.findByName(userStatusData.name).toArray(); - } + if (userStatusData._id) { + matchingResults = await CustomUserStatus.findByNameExceptId(userStatusData.name, userStatusData._id).toArray(); + } else { + matchingResults = await CustomUserStatus.findByName(userStatusData.name).toArray(); + } - if (matchingResults.length > 0) { - throw new Meteor.Error('Custom_User_Status_Error_Name_Already_In_Use', 'The custom user status name is already in use', { - method: 'insertOrUpdateUserStatus', - }); - } + if (matchingResults.length > 0) { + throw new Meteor.Error('Custom_User_Status_Error_Name_Already_In_Use', 'The custom user status name is already in use', { + method: 'insertOrUpdateUserStatus', + }); + } - const validStatusTypes = ['online', 'away', 'busy', 'offline']; - if (userStatusData.statusType && validStatusTypes.indexOf(userStatusData.statusType) < 0) { - throw new Meteor.Error('error-input-is-not-a-valid-field', `${userStatusData.statusType} is not a valid status type`, { - method: 'insertOrUpdateUserStatus', - input: userStatusData.statusType, - field: 'StatusType', - }); - } + const validStatusTypes = ['online', 'away', 'busy', 'offline']; + if (userStatusData.statusType && validStatusTypes.indexOf(userStatusData.statusType) < 0) { + throw new Meteor.Error('error-input-is-not-a-valid-field', `${userStatusData.statusType} is not a valid status type`, { + method: 'insertOrUpdateUserStatus', + input: userStatusData.statusType, + field: 'StatusType', + }); + } - if (!userStatusData._id) { - // insert user status - const createUserStatus: InsertionModel = { - name: userStatusData.name, - statusType: userStatusData.statusType, - }; + if (!userStatusData._id) { + // insert user status + const createUserStatus: InsertionModel = { + name: userStatusData.name, + statusType: userStatusData.statusType, + }; - const _id = (await CustomUserStatus.create(createUserStatus)).insertedId; + const _id = (await CustomUserStatus.create(createUserStatus)).insertedId; - void api.broadcast('user.updateCustomStatus', { ...createUserStatus, _id }); + void api.broadcast('user.updateCustomStatus', { ...createUserStatus, _id }); - return _id; - } + return _id; + } - // update User status - if (userStatusData.name !== userStatusData.previousName) { - await CustomUserStatus.setName(userStatusData._id, userStatusData.name); - } + // update User status + if (userStatusData.name !== userStatusData.previousName) { + await CustomUserStatus.setName(userStatusData._id, userStatusData.name); + } - if (userStatusData.statusType !== userStatusData.previousStatusType) { - await CustomUserStatus.setStatusType(userStatusData._id, userStatusData.statusType); - } + if (userStatusData.statusType !== userStatusData.previousStatusType) { + await CustomUserStatus.setStatusType(userStatusData._id, userStatusData.statusType); + } + + void api.broadcast('user.updateCustomStatus', { ...userStatusData, _id: userStatusData._id }); - void api.broadcast('user.updateCustomStatus', { ...userStatusData, _id: userStatusData._id }); + return true; +}; + +Meteor.methods({ + async insertOrUpdateUserStatus(userStatusData) { + if (!this.userId) { + throw new Meteor.Error('not_authorized'); + } - return true; + return insertOrUpdateUserStatus(this.userId, userStatusData); }, }); From 1b3ae4581b2c39d54202336fbb553ac7d63107d3 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 9 Apr 2025 14:20:30 -0300 Subject: [PATCH 085/187] chore: remove delete emoji custom meteor calls (server) (#35744) --- apps/meteor/app/api/server/v1/emoji-custom.ts | 3 +- .../server/methods/deleteEmojiCustom.ts | 34 ++++++++++++------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/apps/meteor/app/api/server/v1/emoji-custom.ts b/apps/meteor/app/api/server/v1/emoji-custom.ts index 5b255b6ef0c18..a2f886afb1c11 100644 --- a/apps/meteor/app/api/server/v1/emoji-custom.ts +++ b/apps/meteor/app/api/server/v1/emoji-custom.ts @@ -8,6 +8,7 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import type { EmojiData } from '../../../emoji-custom/server/lib/insertOrUpdateEmoji'; import { insertOrUpdateEmoji } from '../../../emoji-custom/server/lib/insertOrUpdateEmoji'; import { uploadEmojiCustomWithBuffer } from '../../../emoji-custom/server/lib/uploadEmojiCustom'; +import { deleteEmojiCustom } from '../../../emoji-custom/server/methods/deleteEmojiCustom'; import { settings } from '../../../settings/server'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; @@ -203,7 +204,7 @@ API.v1.addRoute( return API.v1.failure('The "emojiId" params is required!'); } - await Meteor.callAsync('deleteEmojiCustom', emojiId); + await deleteEmojiCustom(this.userId, emojiId); return API.v1.success(); }, diff --git a/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts b/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts index c16d3b82449bf..ce6aaeaf417f6 100644 --- a/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts +++ b/apps/meteor/app/emoji-custom/server/methods/deleteEmojiCustom.ts @@ -14,23 +14,31 @@ declare module '@rocket.chat/ddp-client' { } } +export const deleteEmojiCustom = async (userId: string, emojiID: ICustomEmojiDescriptor['_id']): Promise => { + if (!(await hasPermissionAsync(userId, 'manage-emoji'))) { + throw new Meteor.Error('not_authorized'); + } + + const emoji = await EmojiCustom.findOneById(emojiID); + if (emoji == null) { + throw new Meteor.Error('Custom_Emoji_Error_Invalid_Emoji', 'Invalid emoji', { + method: 'deleteEmojiCustom', + }); + } + + await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emoji.name}.${emoji.extension}`)); + await EmojiCustom.removeById(emojiID); + void api.broadcast('emoji.deleteCustom', emoji); + + return true; +}; + Meteor.methods({ async deleteEmojiCustom(emojiID) { - if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-emoji'))) { + if (!this.userId) { throw new Meteor.Error('not_authorized'); } - const emoji = await EmojiCustom.findOneById(emojiID); - if (emoji == null) { - throw new Meteor.Error('Custom_Emoji_Error_Invalid_Emoji', 'Invalid emoji', { - method: 'deleteEmojiCustom', - }); - } - - await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emoji.name}.${emoji.extension}`)); - await EmojiCustom.removeById(emojiID); - void api.broadcast('emoji.deleteCustom', emoji); - - return true; + return deleteEmojiCustom(this.userId, emojiID); }, }); From ff3e08f18d3d8c03c7d7cf9e8e3def7d5b02b0a1 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 9 Apr 2025 17:35:53 -0300 Subject: [PATCH 086/187] chore: remove meteor calls from rooms (server) (#35752) --- apps/meteor/app/api/server/v1/rooms.ts | 20 +- .../lib/server/methods/cleanRoomHistory.ts | 78 ++++-- .../methods/saveNotificationSettings.ts | 224 +++++++++--------- apps/meteor/server/methods/toggleFavorite.ts | 34 +-- apps/meteor/server/publications/room/index.ts | 37 +-- 5 files changed, 222 insertions(+), 171 deletions(-) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index e34a0fd94696e..446374d934684 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -25,7 +25,9 @@ import { findUsersOfRoomOrderedByRole } from '../../../../server/lib/findUsersOf import { openRoom } from '../../../../server/lib/openRoom'; import { hideRoomMethod } from '../../../../server/methods/hideRoom'; import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom'; +import { toggleFavoriteMethod } from '../../../../server/methods/toggleFavorite'; import { unmuteUserInRoom } from '../../../../server/methods/unmuteUserInRoom'; +import { roomsGetMethod } from '../../../../server/publications/room'; import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings'; @@ -34,9 +36,12 @@ import { FileUpload } from '../../../file-upload/server'; import { sendFileMessage } from '../../../file-upload/server/methods/sendFileMessage'; import { syncRolePrioritiesForRoomIfRequired } from '../../../lib/server/functions/syncRolePrioritiesForRoomIfRequired'; import { executeArchiveRoom } from '../../../lib/server/methods/archiveRoom'; +import { cleanRoomHistoryMethod } from '../../../lib/server/methods/cleanRoomHistory'; import { leaveRoomMethod } from '../../../lib/server/methods/leaveRoom'; import { executeUnarchiveRoom } from '../../../lib/server/methods/unarchiveRoom'; import { applyAirGappedRestrictionsValidation } from '../../../license/server/airGappedRestrictionsWrapper'; +import type { NotificationFieldType } from '../../../push-notifications/server/methods/saveNotificationSettings'; +import { saveNotificationSettingsMethod } from '../../../push-notifications/server/methods/saveNotificationSettings'; import { settings } from '../../../settings/server'; import { API } from '../api'; import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage'; @@ -144,7 +149,7 @@ API.v1.addRoute( } } - let result: { update: IRoom[]; remove: IRoom[] } = await Meteor.callAsync('rooms/get', updatedSinceDate); + let result = await roomsGetMethod(this.userId, updatedSinceDate); if (Array.isArray(result)) { result = { @@ -353,7 +358,12 @@ API.v1.addRoute( await Promise.all( Object.keys(notifications as Notifications).map(async (notificationKey) => - Meteor.callAsync('saveNotificationSettings', roomId, notificationKey, notifications[notificationKey as keyof Notifications]), + saveNotificationSettingsMethod( + this.userId, + roomId, + notificationKey as NotificationFieldType, + notifications[notificationKey as keyof Notifications], + ), ), ); @@ -375,7 +385,7 @@ API.v1.addRoute( const room = await findRoomByIdOrName({ params: this.bodyParams }); - await Meteor.callAsync('toggleFavorite', room._id, favorite); + await toggleFavoriteMethod(this.userId, room._id, favorite); return API.v1.success(); }, @@ -414,7 +424,7 @@ API.v1.addRoute( return API.v1.failure('Body parameter "oldest" is required.'); } - const count = await Meteor.callAsync('cleanRoomHistory', { + const count = await cleanRoomHistoryMethod(this.userId, { roomId: _id, latest: new Date(latest), oldest: new Date(oldest), @@ -424,7 +434,7 @@ API.v1.addRoute( filesOnly: [true, 'true', 1, '1'].includes(filesOnly ?? false), ignoreThreads: [true, 'true', 1, '1'].includes(ignoreThreads ?? false), ignoreDiscussion: [true, 'true', 1, '1'].includes(ignoreDiscussion ?? false), - fromUsers: users, + fromUsers: users?.filter(isTruthy) || [], }); return API.v1.success({ _id, count }); diff --git a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts b/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts index c804128d27bd8..23bd39f3090fb 100644 --- a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts @@ -7,24 +7,64 @@ import { canAccessRoomAsync } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { cleanRoomHistory } from '../functions/cleanRoomHistory'; +type CleanRoomHistoryParams = { + roomId: string; + latest: Date; + oldest: Date; + inclusive?: boolean; + limit?: number; + excludePinned?: boolean; + ignoreDiscussion?: boolean; + filesOnly?: boolean; + fromUsers?: string[]; + ignoreThreads?: boolean; +}; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - cleanRoomHistory(data: { - roomId: string; - latest: Date; - oldest: Date; - inclusive?: boolean; - limit?: number; - excludePinned?: boolean; - ignoreDiscussion?: boolean; - filesOnly?: boolean; - fromUsers?: string[]; - ignoreThreads?: boolean; - }): number; + cleanRoomHistory(data: CleanRoomHistoryParams): number; } } +export const cleanRoomHistoryMethod = async ( + userId: string, + { + roomId, + latest, + oldest, + inclusive = true, + limit, + excludePinned = false, + ignoreDiscussion = true, + filesOnly = false, + fromUsers = [], + ignoreThreads, + }: CleanRoomHistoryParams, +): Promise => { + if (!(await hasPermissionAsync(userId, 'clean-channel-history', roomId))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); + } + + const room = await findRoomByIdOrName({ params: { roomId } }); + + if (!room || !(await canAccessRoomAsync(room, { _id: userId }))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); + } + + return cleanRoomHistory({ + rid: roomId, + latest, + oldest, + inclusive, + limit, + excludePinned, + ignoreDiscussion, + filesOnly, + fromUsers, + ignoreThreads, + }); +}; + Meteor.methods({ async cleanRoomHistory({ roomId, @@ -54,18 +94,8 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cleanRoomHistory' }); } - if (!(await hasPermissionAsync(userId, 'clean-channel-history', roomId))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); - } - - const room = await findRoomByIdOrName({ params: { roomId } }); - - if (!room || !(await canAccessRoomAsync(room, { _id: userId }))) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); - } - - return cleanRoomHistory({ - rid: roomId, + return cleanRoomHistoryMethod(userId, { + roomId, latest, oldest, inclusive, diff --git a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts index ddac51b4eca94..ea2a2d4545192 100644 --- a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts +++ b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.ts @@ -10,27 +10,126 @@ import { getUserNotificationPreference } from '../../../utils/server/getUserNoti const saveAudioNotificationValue = (subId: ISubscription['_id'], value: string) => value === 'default' ? Subscriptions.clearAudioNotificationValueById(subId) : Subscriptions.updateAudioNotificationValueById(subId, value); +export type NotificationFieldType = + | 'desktopNotifications' + | 'mobilePushNotifications' + | 'emailNotifications' + | 'unreadAlert' + | 'disableNotifications' + | 'hideUnreadStatus' + | 'hideMentionStatus' + | 'muteGroupMentions' + | 'audioNotificationValue'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - saveNotificationSettings( - roomId: string, - field: - | 'desktopNotifications' - | 'mobilePushNotifications' - | 'emailNotifications' - | 'unreadAlert' - | 'disableNotifications' - | 'hideUnreadStatus' - | 'hideMentionStatus' - | 'muteGroupMentions' - | 'audioNotificationValue', - value: string, - ): boolean; + saveNotificationSettings(roomId: string, field: NotificationFieldType, value: string): boolean; saveAudioNotificationValue(subId: string, value: string): boolean; } } +export const saveNotificationSettingsMethod = async ( + userId: string, + roomId: string, + field: NotificationFieldType, + value: string, +): Promise => { + const getNotificationPrefValue = async (field: string, value: unknown) => { + if (value === 'default') { + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'saveNotificationSettings', + }); + } + + const userPref = await getUserNotificationPreference(userId, field); + return userPref?.origin === 'server' ? null : userPref; + } + return { value, origin: 'subscription' }; + }; + + const notifications = { + desktopNotifications: { + updateMethod: async (subscription: ISubscription, value: unknown) => + Subscriptions.updateNotificationsPrefById( + subscription._id, + await getNotificationPrefValue('desktop', value), + 'desktopNotifications', + 'desktopPrefOrigin', + ), + }, + mobilePushNotifications: { + updateMethod: async (subscription: ISubscription, value: unknown) => + Subscriptions.updateNotificationsPrefById( + subscription._id, + await getNotificationPrefValue('mobile', value), + 'mobilePushNotifications', + 'mobilePrefOrigin', + ), + }, + emailNotifications: { + updateMethod: async (subscription: ISubscription, value: unknown) => + Subscriptions.updateNotificationsPrefById( + subscription._id, + await getNotificationPrefValue('email', value), + 'emailNotifications', + 'emailPrefOrigin', + ), + }, + unreadAlert: { + // @ts-expect-error - Check types of model. The way the method is defined makes difficult to type it, check proper types for `value` + updateMethod: (subscription: ISubscription, value: string) => Subscriptions.updateUnreadAlertById(subscription._id, value), + }, + disableNotifications: { + updateMethod: (subscription: ISubscription, value: unknown) => + Subscriptions.updateDisableNotificationsById(subscription._id, value === '1'), + }, + hideUnreadStatus: { + updateMethod: (subscription: ISubscription, value: string) => + Subscriptions.updateHideUnreadStatusById(subscription._id, value === '1'), + }, + hideMentionStatus: { + updateMethod: (subscription: ISubscription, value: unknown) => + Subscriptions.updateHideMentionStatusById(subscription._id, value === '1'), + }, + muteGroupMentions: { + updateMethod: (subscription: ISubscription, value: unknown) => Subscriptions.updateMuteGroupMentions(subscription._id, value === '1'), + }, + audioNotificationValue: { + updateMethod: (subscription: ISubscription, value: string) => saveAudioNotificationValue(subscription._id, value), + }, + }; + const isInvalidNotification = !Object.keys(notifications).includes(field); + const basicValuesForNotifications = ['all', 'mentions', 'nothing', 'default']; + const fieldsMustHaveBasicValues = ['emailNotifications', 'mobilePushNotifications', 'desktopNotifications']; + + if (isInvalidNotification) { + throw new Meteor.Error('error-invalid-settings', 'Invalid settings field', { + method: 'saveNotificationSettings', + }); + } + + if (fieldsMustHaveBasicValues.includes(field) && !basicValuesForNotifications.includes(value)) { + throw new Meteor.Error('error-invalid-settings', 'Invalid settings value', { + method: 'saveNotificationSettings', + }); + } + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, userId); + if (!subscription) { + throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { + method: 'saveNotificationSettings', + }); + } + + const updateResponse = await notifications[field].updateMethod(subscription, value); + if (updateResponse.modifiedCount) { + void notifyOnSubscriptionChangedById(subscription._id); + } + + return true; +}; + Meteor.methods({ async saveNotificationSettings(roomId, field, value) { const userId = Meteor.userId(); @@ -43,102 +142,7 @@ Meteor.methods({ check(field, String); check(value, String); - const getNotificationPrefValue = async (field: string, value: unknown) => { - if (value === 'default') { - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'saveNotificationSettings', - }); - } - - const userPref = await getUserNotificationPreference(userId, field); - return userPref?.origin === 'server' ? null : userPref; - } - return { value, origin: 'subscription' }; - }; - - const notifications = { - desktopNotifications: { - updateMethod: async (subscription: ISubscription, value: unknown) => - Subscriptions.updateNotificationsPrefById( - subscription._id, - await getNotificationPrefValue('desktop', value), - 'desktopNotifications', - 'desktopPrefOrigin', - ), - }, - mobilePushNotifications: { - updateMethod: async (subscription: ISubscription, value: unknown) => - Subscriptions.updateNotificationsPrefById( - subscription._id, - await getNotificationPrefValue('mobile', value), - 'mobilePushNotifications', - 'mobilePrefOrigin', - ), - }, - emailNotifications: { - updateMethod: async (subscription: ISubscription, value: unknown) => - Subscriptions.updateNotificationsPrefById( - subscription._id, - await getNotificationPrefValue('email', value), - 'emailNotifications', - 'emailPrefOrigin', - ), - }, - unreadAlert: { - // @ts-expect-error - Check types of model. The way the method is defined makes difficult to type it, check proper types for `value` - updateMethod: (subscription: ISubscription, value: string) => Subscriptions.updateUnreadAlertById(subscription._id, value), - }, - disableNotifications: { - updateMethod: (subscription: ISubscription, value: unknown) => - Subscriptions.updateDisableNotificationsById(subscription._id, value === '1'), - }, - hideUnreadStatus: { - updateMethod: (subscription: ISubscription, value: string) => - Subscriptions.updateHideUnreadStatusById(subscription._id, value === '1'), - }, - hideMentionStatus: { - updateMethod: (subscription: ISubscription, value: unknown) => - Subscriptions.updateHideMentionStatusById(subscription._id, value === '1'), - }, - muteGroupMentions: { - updateMethod: (subscription: ISubscription, value: unknown) => - Subscriptions.updateMuteGroupMentions(subscription._id, value === '1'), - }, - audioNotificationValue: { - updateMethod: (subscription: ISubscription, value: string) => saveAudioNotificationValue(subscription._id, value), - }, - }; - const isInvalidNotification = !Object.keys(notifications).includes(field); - const basicValuesForNotifications = ['all', 'mentions', 'nothing', 'default']; - const fieldsMustHaveBasicValues = ['emailNotifications', 'mobilePushNotifications', 'desktopNotifications']; - - if (isInvalidNotification) { - throw new Meteor.Error('error-invalid-settings', 'Invalid settings field', { - method: 'saveNotificationSettings', - }); - } - - if (fieldsMustHaveBasicValues.includes(field) && !basicValuesForNotifications.includes(value)) { - throw new Meteor.Error('error-invalid-settings', 'Invalid settings value', { - method: 'saveNotificationSettings', - }); - } - - const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, userId); - if (!subscription) { - throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { - method: 'saveNotificationSettings', - }); - } - - const updateResponse = await notifications[field].updateMethod(subscription, value); - if (updateResponse.modifiedCount) { - void notifyOnSubscriptionChangedById(subscription._id); - } - - return true; + return saveNotificationSettingsMethod(userId, roomId, field, value); }, async saveAudioNotificationValue(rid, value) { diff --git a/apps/meteor/server/methods/toggleFavorite.ts b/apps/meteor/server/methods/toggleFavorite.ts index 912b9a8f3e5cc..251637253fb30 100644 --- a/apps/meteor/server/methods/toggleFavorite.ts +++ b/apps/meteor/server/methods/toggleFavorite.ts @@ -9,14 +9,29 @@ import { notifyOnSubscriptionChangedByRoomIdAndUserId } from '../../app/lib/serv declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - toggleFavorite(rid: IRoom['_id'], f?: boolean): Promise; + toggleFavorite(rid: IRoom['_id'], favorite?: boolean): Promise; } } +export const toggleFavoriteMethod = async (userId: string, rid: IRoom['_id'], favorite?: boolean): Promise => { + const userSubscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); + if (!userSubscription) { + throw new Meteor.Error('error-invalid-subscription', 'You must be part of a room to favorite it', { method: 'toggleFavorite' }); + } + + const { modifiedCount } = await Subscriptions.setFavoriteByRoomIdAndUserId(rid, userId, favorite); + + if (modifiedCount) { + void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, userId); + } + + return modifiedCount; +}; + Meteor.methods({ - async toggleFavorite(rid, f) { + async toggleFavorite(rid, favorite) { check(rid, String); - check(f, Match.Optional(Boolean)); + check(favorite, Match.Optional(Boolean)); const userId = Meteor.userId(); if (!userId) { @@ -25,17 +40,6 @@ Meteor.methods({ }); } - const userSubscription = await Subscriptions.findOneByRoomIdAndUserId(rid, userId); - if (!userSubscription) { - throw new Meteor.Error('error-invalid-subscription', 'You must be part of a room to favorite it', { method: 'toggleFavorite' }); - } - - const { modifiedCount } = await Subscriptions.setFavoriteByRoomIdAndUserId(rid, userId, f); - - if (modifiedCount) { - void notifyOnSubscriptionChangedByRoomIdAndUserId(rid, userId); - } - - return modifiedCount; + return toggleFavoriteMethod(userId, rid, favorite); }, }); diff --git a/apps/meteor/server/publications/room/index.ts b/apps/meteor/server/publications/room/index.ts index 07dd94be23d00..dcb9c741f2376 100644 --- a/apps/meteor/server/publications/room/index.ts +++ b/apps/meteor/server/publications/room/index.ts @@ -25,26 +25,29 @@ const roomMap = (record: IRoom | IOmnichannelRoom) => { return _.pick(record, ...Object.keys(roomFields)) as PublicRoom; }; -Meteor.methods({ - async 'rooms/get'(updatedAt) { - const options = { projection: roomFields }; - const user = Meteor.userId(); - - if (!user) { - if (settings.get('Accounts_AllowAnonymousRead')) { - return Rooms.findByDefaultAndTypes(true, ['c'], options).toArray(); - } - return []; - } +export const roomsGetMethod = async (userId?: string | null, updatedAt?: Date): Promise => { + const options = { projection: roomFields }; - if (updatedAt instanceof Date) { - return { - update: await (await Rooms.findBySubscriptionUserIdUpdatedAfter(user, updatedAt, options)).toArray(), - remove: await Rooms.trashFindDeletedAfter(updatedAt, {}, { projection: { _id: 1, _deletedAt: 1 } }).toArray(), - }; + if (!userId) { + if (settings.get('Accounts_AllowAnonymousRead')) { + return Rooms.findByDefaultAndTypes(true, ['c'], options).toArray(); } + return []; + } + + if (updatedAt instanceof Date) { + return { + update: await (await Rooms.findBySubscriptionUserIdUpdatedAfter(userId, updatedAt, options)).toArray(), + remove: await Rooms.trashFindDeletedAfter(updatedAt, {}, { projection: { _id: 1, _deletedAt: 1 } }).toArray(), + }; + } + + return (await Rooms.findBySubscriptionUserId(userId, options)).toArray(); +}; - return (await Rooms.findBySubscriptionUserId(user, options)).toArray(); +Meteor.methods({ + async 'rooms/get'(updatedAt) { + return roomsGetMethod(Meteor.userId(), updatedAt); }, async 'getRoomByTypeAndName'(type, name) { From d837f57bc79f6319b8080d49eae3cf77ceb62931 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 9 Apr 2025 17:42:58 -0300 Subject: [PATCH 087/187] chore: Simplify `getClosedPeriod` (#35751) --- .../dashboards/getClosedPeriod.spec.ts | 101 ++++++------------ .../client/components/dashboards/periods.ts | 18 +--- 2 files changed, 35 insertions(+), 84 deletions(-) diff --git a/apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts b/apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts index 518f5843fa0d4..ec61665a39abb 100644 --- a/apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts +++ b/apps/meteor/client/components/dashboards/getClosedPeriod.spec.ts @@ -1,101 +1,62 @@ import { getClosedPeriod } from './periods'; -jest.mock('moment', () => { - return () => jest.requireActual('moment')('2024-05-19T12:00:00.000Z'); -}); +jest.useFakeTimers(); +jest.setSystemTime(Date.parse('2024-05-19T12:00:00.000Z')); it('should return the correct period range for this month', () => { - const monthExpectedReturn = { - start: new Date('5/1/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - const period = getClosedPeriod({ startOf: 'month' })(true); - expect(period.start.toISOString().split('T')[0]).toEqual(monthExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(monthExpectedReturn.end); + expect(period.start).toEqual(new Date('2024-05-01T00:00:00.000Z')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999Z')); }); it('should return the correct period range for this year', () => { - const yearExpectedReturn = { - start: new Date('1/1/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - const period = getClosedPeriod({ startOf: 'year' })(true); - expect(period.start.toISOString().split('T')[0]).toEqual(yearExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(yearExpectedReturn.end); + expect(period.start).toEqual(new Date('2024-01-01T00:00:00.000Z')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999Z')); }); it('should return the correct period range for last 6 months', () => { - const last6MonthsExpectedReturn = { - start: new Date('11/1/2023').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - const period = getClosedPeriod({ startOf: 'month', subtract: { amount: 6, unit: 'months' } })(true); - expect(period.start.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.end); + expect(period.start).toEqual(new Date('2023-11-01T00:00:00.000Z')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999Z')); }); it('should return the correct period range for this week', () => { - const weekExpectedReturn = { - start: new Date('5/19/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - const period = getClosedPeriod({ startOf: 'week' })(true); - expect(period.start.toISOString().split('T')[0]).toEqual(weekExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(weekExpectedReturn.end); + expect(period.start).toEqual(new Date('2024-05-19T00:00:00.000Z')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999Z')); }); -it('should return the correct period range for this month using local time', () => { - const monthExpectedReturn = { - start: new Date('5/1/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - - const period = getClosedPeriod({ startOf: 'month' })(false); +describe('using local time', () => { + it('should return the correct period range for this month', () => { + const period = getClosedPeriod({ startOf: 'month' })(false); - expect(period.start.toISOString().split('T')[0]).toEqual(monthExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(monthExpectedReturn.end); -}); + expect(period.start).toEqual(new Date('2024-05-01T00:00:00.000')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999')); + }); -it('should return the correct period range for this year using local time', () => { - const yearExpectedReturn = { - start: new Date('1/1/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; + it('should return the correct period range for this year', () => { + const period = getClosedPeriod({ startOf: 'year' })(false); - const period = getClosedPeriod({ startOf: 'year' })(false); + expect(period.start).toEqual(new Date('2024-01-01T00:00:00.000')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999')); + }); - expect(period.start.toISOString().split('T')[0]).toEqual(yearExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(yearExpectedReturn.end); -}); - -it('should return the correct period range for last 6 months using local time', () => { - const last6MonthsExpectedReturn = { - start: new Date('11/1/2023').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; - - const period = getClosedPeriod({ startOf: 'month', subtract: { amount: 6, unit: 'months' } })(true); - - expect(period.start.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(last6MonthsExpectedReturn.end); -}); + it('should return the correct period range for last 6 months', () => { + const period = getClosedPeriod({ startOf: 'month', subtract: { amount: 6, unit: 'months' } })(false); -it('should return the correct period range for this week using local time', () => { - const weekExpectedReturn = { - start: new Date('5/19/2024').toISOString().split('T')[0], - end: new Date('5/19/2024').toISOString().split('T')[0], - }; + expect(period.start).toEqual(new Date('2023-11-01T00:00:00.000')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999')); + }); - const period = getClosedPeriod({ startOf: 'week' })(false); + it('should return the correct period range for this week', () => { + const period = getClosedPeriod({ startOf: 'week' })(false); - expect(period.start.toISOString().split('T')[0]).toEqual(weekExpectedReturn.start); - expect(period.end.toISOString().split('T')[0]).toEqual(weekExpectedReturn.end); + expect(period.start).toEqual(new Date('2024-05-19T00:00:00.000')); + expect(period.end).toEqual(new Date('2024-05-19T23:59:59.999')); + }); }); diff --git a/apps/meteor/client/components/dashboards/periods.ts b/apps/meteor/client/components/dashboards/periods.ts index 5ae1626e976cf..637a2e0ac0b9f 100644 --- a/apps/meteor/client/components/dashboards/periods.ts +++ b/apps/meteor/client/components/dashboards/periods.ts @@ -16,27 +16,17 @@ export const getClosedPeriod = end: Date; }) => (utc): { start: Date; end: Date } => { - const date = new Date(); - const offsetForMoment = -(date.getTimezoneOffset() / 60); - let start = moment(date).utc(); - let end = moment(date).utc(); + const start = utc ? moment().utc() : moment(); + const end = utc ? moment().utc() : moment(); if (subtract) { const { amount, unit } = subtract; start.subtract(amount, unit); } - if (!utc) { - start = start.utcOffset(offsetForMoment); - end = end.utcOffset(offsetForMoment); - } - - // moment.toDate() can only return the date in localtime, that's why we do the new Date conversion - // https://github.com/moment/moment-timezone/issues/644 - return { - start: new Date(start.startOf(startOf).format('YYYY-MM-DD HH:mm:ss')), - end: new Date(end.endOf('day').format('YYYY-MM-DD HH:mm:ss')), + start: start.startOf(startOf).toDate(), + end: end.endOf('day').toDate(), }; }; From 01cf76ec5b5e2d3dac35e9da8b056f0af605e0ca Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 9 Apr 2025 23:33:42 -0300 Subject: [PATCH 088/187] chore: remove meteor calls from integrations (server) (#35747) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/meteor/app/api/server/v1/integrations.ts | 7 +- .../incoming/updateIncomingIntegration.ts | 300 +++++++++--------- .../outgoing/updateOutgoingIntegration.ts | 172 +++++----- 3 files changed, 248 insertions(+), 231 deletions(-) diff --git a/apps/meteor/app/api/server/v1/integrations.ts b/apps/meteor/app/api/server/v1/integrations.ts index 1c1dd9dd50f3b..5fb1781aa1411 100644 --- a/apps/meteor/app/api/server/v1/integrations.ts +++ b/apps/meteor/app/api/server/v1/integrations.ts @@ -10,7 +10,6 @@ import { } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; import { @@ -19,8 +18,10 @@ import { } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { addIncomingIntegration } from '../../../integrations/server/methods/incoming/addIncomingIntegration'; import { deleteIncomingIntegration } from '../../../integrations/server/methods/incoming/deleteIncomingIntegration'; +import { updateIncomingIntegration } from '../../../integrations/server/methods/incoming/updateIncomingIntegration'; import { addOutgoingIntegration } from '../../../integrations/server/methods/outgoing/addOutgoingIntegration'; import { deleteOutgoingIntegration } from '../../../integrations/server/methods/outgoing/deleteOutgoingIntegration'; +import { updateOutgoingIntegration } from '../../../integrations/server/methods/outgoing/updateOutgoingIntegration'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { findOneIntegration } from '../lib/integrations'; @@ -248,7 +249,7 @@ API.v1.addRoute( return API.v1.failure('No integration found.'); } - await Meteor.callAsync('updateOutgoingIntegration', integration._id, bodyParams); + await updateOutgoingIntegration(this.userId, integration._id, bodyParams); return API.v1.success({ integration: await Integrations.findOne({ _id: integration._id }), @@ -260,7 +261,7 @@ API.v1.addRoute( return API.v1.failure('No integration found.'); } - await Meteor.callAsync('updateIncomingIntegration', integration._id, bodyParams); + await updateIncomingIntegration(this.userId, integration._id, bodyParams); return API.v1.success({ integration: await Integrations.findOne({ _id: integration._id }), diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index 1ce65a03deb00..2f45a6c9f7297 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -23,181 +23,189 @@ declare module '@rocket.chat/ddp-client' { } } -Meteor.methods({ - // eslint-disable-next-line complexity - async updateIncomingIntegration(integrationId, integration) { - if (!this.userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'updateOutgoingIntegration', - }); - } +export const updateIncomingIntegration = async ( + userId: string, + integrationId: string, + integration: INewIncomingIntegration | IUpdateIncomingIntegration, +): Promise => { + if (!integration.channel || typeof integration.channel.valueOf() !== 'string' || integration.channel.trim() === '') { + throw new Meteor.Error('error-invalid-channel', 'Invalid channel', { + method: 'updateIncomingIntegration', + }); + } - if (!integration.channel || typeof integration.channel.valueOf() !== 'string' || integration.channel.trim() === '') { - throw new Meteor.Error('error-invalid-channel', 'Invalid channel', { + const channels = integration.channel.split(',').map((channel) => channel.trim()); + + for (const channel of channels) { + if (!validChannelChars.includes(channel[0])) { + throw new Meteor.Error('error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', { method: 'updateIncomingIntegration', }); } + } - const channels = integration.channel.split(',').map((channel) => channel.trim()); + let currentIntegration; + + if (await hasPermissionAsync(userId, 'manage-incoming-integrations')) { + currentIntegration = await Integrations.findOneById(integrationId); + } else if (await hasPermissionAsync(userId, 'manage-own-incoming-integrations')) { + currentIntegration = await Integrations.findOne({ + '_id': integrationId, + '_createdBy._id': userId, + }); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { + method: 'updateIncomingIntegration', + }); + } - for (const channel of channels) { - if (!validChannelChars.includes(channel[0])) { - throw new Meteor.Error('error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', { - method: 'updateIncomingIntegration', - }); - } - } + if (!currentIntegration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { + method: 'updateIncomingIntegration', + }); + } - let currentIntegration; + const oldScriptEngine = currentIntegration.scriptEngine; + const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm'; + if ( + integration.script?.trim() && + (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) + ) { + wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { + throw new Meteor.Error(e.message); + }); + } - if (await hasPermissionAsync(this.userId, 'manage-incoming-integrations')) { - currentIntegration = await Integrations.findOneById(integrationId); - } else if (await hasPermissionAsync(this.userId, 'manage-own-incoming-integrations')) { - currentIntegration = await Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': this.userId, - }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'updateIncomingIntegration', - }); - } + const isFrozen = isScriptEngineFrozen(scriptEngine); - if (!currentIntegration) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { - method: 'updateIncomingIntegration', - }); - } + if (!isFrozen) { + let scriptCompiled: string | undefined; + let scriptError: Pick | undefined; - const oldScriptEngine = currentIntegration.scriptEngine; - const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm'; - if ( - integration.script?.trim() && - (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) - ) { - wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { - throw new Meteor.Error(e.message); - }); - } + if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { + try { + let babelOptions = Babel.getDefaultOptions({ runtime: false }); + babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }); - const isFrozen = isScriptEngineFrozen(scriptEngine); - - if (!isFrozen) { - let scriptCompiled: string | undefined; - let scriptError: Pick | undefined; - - if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { - try { - let babelOptions = Babel.getDefaultOptions({ runtime: false }); - babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }); - - scriptCompiled = Babel.compile(integration.script, babelOptions).code; - scriptError = undefined; - await Integrations.updateOne( - { _id: integrationId }, - { - $set: { - scriptCompiled, - }, - $unset: { scriptError: 1 as const }, + scriptCompiled = Babel.compile(integration.script, babelOptions).code; + scriptError = undefined; + await Integrations.updateOne( + { _id: integrationId }, + { + $set: { + scriptCompiled, }, - ); - } catch (e) { - scriptCompiled = undefined; - if (e instanceof Error) { - const { name, message, stack } = e; - scriptError = { name, message, stack }; - } - await Integrations.updateOne( - { _id: integrationId }, - { - $set: { - scriptError, - }, - $unset: { - scriptCompiled: 1 as const, - }, - }, - ); + $unset: { scriptError: 1 as const }, + }, + ); + } catch (e) { + scriptCompiled = undefined; + if (e instanceof Error) { + const { name, message, stack } = e; + scriptError = { name, message, stack }; } + await Integrations.updateOne( + { _id: integrationId }, + { + $set: { + scriptError, + }, + $unset: { + scriptCompiled: 1 as const, + }, + }, + ); } } + } - for await (let channel of channels) { - const channelType = channel[0]; - channel = channel.slice(1); - let record; - - switch (channelType) { - case '#': - record = await Rooms.findOne({ - $or: [{ _id: channel }, { name: channel }], - }); - break; - case '@': - record = await Users.findOne({ - $or: [{ _id: channel }, { username: channel }], - }); - break; - } + for await (let channel of channels) { + const channelType = channel[0]; + channel = channel.slice(1); + let record; - if (!record) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'updateIncomingIntegration', + switch (channelType) { + case '#': + record = await Rooms.findOne({ + $or: [{ _id: channel }, { name: channel }], }); - } - - if ( - !(await hasAllPermissionAsync(this.userId, ['manage-incoming-integrations', 'manage-own-incoming-integrations'])) && - !(await Subscriptions.findOneByRoomIdAndUserId(record._id, this.userId, { projection: { _id: 1 } })) - ) { - throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', { - method: 'updateIncomingIntegration', + break; + case '@': + record = await Users.findOne({ + $or: [{ _id: channel }, { username: channel }], }); - } + break; } - const user = await Users.findOne({ username: currentIntegration.username }); + if (!record) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { + method: 'updateIncomingIntegration', + }); + } - if (!user?._id) { - throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', { + if ( + !(await hasAllPermissionAsync(userId, ['manage-incoming-integrations', 'manage-own-incoming-integrations'])) && + !(await Subscriptions.findOneByRoomIdAndUserId(record._id, userId, { projection: { _id: 1 } })) + ) { + throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', { method: 'updateIncomingIntegration', }); } + } - await addUserRolesAsync(user._id, ['bot']); - - const updatedIntegration = await Integrations.findOneAndUpdate( - { _id: integrationId }, - { - $set: { - enabled: integration.enabled, - name: integration.name, - avatar: integration.avatar, - emoji: integration.emoji, - alias: integration.alias, - channel: channels, - ...('username' in integration && { username: integration.username }), - ...(isFrozen - ? {} - : { - script: integration.script, - scriptEnabled: integration.scriptEnabled, - scriptEngine, - }), - ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { - overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, - }), - _updatedAt: new Date(), - _updatedBy: await Users.findOne({ _id: this.userId }, { projection: { username: 1 } }), - }, + const user = await Users.findOne({ username: currentIntegration.username }); + + if (!user?._id) { + throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', { + method: 'updateIncomingIntegration', + }); + } + + await addUserRolesAsync(user._id, ['bot']); + + const updatedIntegration = await Integrations.findOneAndUpdate( + { _id: integrationId }, + { + $set: { + enabled: integration.enabled, + name: integration.name, + avatar: integration.avatar, + emoji: integration.emoji, + alias: integration.alias, + channel: channels, + ...('username' in integration && { username: integration.username }), + ...(isFrozen + ? {} + : { + script: integration.script, + scriptEnabled: integration.scriptEnabled, + scriptEngine, + }), + ...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && { + overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled, + }), + _updatedAt: new Date(), + _updatedBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }), }, - ); + }, + ); - if (updatedIntegration) { - void notifyOnIntegrationChanged(updatedIntegration); + if (updatedIntegration) { + void notifyOnIntegrationChanged(updatedIntegration); + } + + return updatedIntegration; +}; + +Meteor.methods({ + // eslint-disable-next-line complexity + async updateIncomingIntegration(integrationId, integration) { + if (!this.userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'updateOutgoingIntegration', + }); } - return updatedIntegration; + return updateIncomingIntegration(this.userId, integrationId, integration); }, }); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index e4c1c48e04875..2df3dfcb13ffa 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -19,102 +19,110 @@ declare module '@rocket.chat/ddp-client' { } } -Meteor.methods({ - async updateOutgoingIntegration(integrationId, _integration) { - if (!this.userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'updateOutgoingIntegration', - }); - } - - const integration = await validateOutgoingIntegration(_integration, this.userId); +export const updateOutgoingIntegration = async ( + userId: string, + integrationId: string, + _integration: INewOutgoingIntegration | IUpdateOutgoingIntegration, +): Promise => { + const integration = await validateOutgoingIntegration(_integration, userId); - if (!integration.token || integration.token.trim() === '') { - throw new Meteor.Error('error-invalid-token', 'Invalid token', { - method: 'updateOutgoingIntegration', - }); - } + if (!integration.token || integration.token.trim() === '') { + throw new Meteor.Error('error-invalid-token', 'Invalid token', { + method: 'updateOutgoingIntegration', + }); + } - let currentIntegration: IIntegration | null; + let currentIntegration: IIntegration | null; - if (await hasPermissionAsync(this.userId, 'manage-outgoing-integrations')) { - currentIntegration = await Integrations.findOneById(integrationId); - } else if (await hasPermissionAsync(this.userId, 'manage-own-outgoing-integrations')) { - currentIntegration = await Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': this.userId, - }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'updateOutgoingIntegration', - }); - } + if (await hasPermissionAsync(userId, 'manage-outgoing-integrations')) { + currentIntegration = await Integrations.findOneById(integrationId); + } else if (await hasPermissionAsync(userId, 'manage-own-outgoing-integrations')) { + currentIntegration = await Integrations.findOne({ + '_id': integrationId, + '_createdBy._id': userId, + }); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { + method: 'updateOutgoingIntegration', + }); + } - if (!currentIntegration) { - throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found'); - } + if (!currentIntegration) { + throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found'); + } - const oldScriptEngine = currentIntegration.scriptEngine; - const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm'; - if ( - integration.script?.trim() && - (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) - ) { - wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { - throw new Meteor.Error(e.message); - }); - } + const oldScriptEngine = currentIntegration.scriptEngine; + const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm'; + if ( + integration.script?.trim() && + (scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim()) + ) { + wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => { + throw new Meteor.Error(e.message); + }); + } - const isFrozen = isScriptEngineFrozen(scriptEngine); + const isFrozen = isScriptEngineFrozen(scriptEngine); - const updatedIntegration = await Integrations.findOneAndUpdate( - { _id: integrationId }, - { - $set: { - event: integration.event, - enabled: integration.enabled, - name: integration.name, - avatar: integration.avatar, - emoji: integration.emoji, - alias: integration.alias, - channel: typeof integration.channel === 'string' ? [integration.channel] : integration.channel, - targetRoom: integration.targetRoom, - impersonateUser: integration.impersonateUser, - username: integration.username, - userId: integration.userId, - urls: integration.urls, - token: integration.token, - ...(isFrozen - ? {} - : { - script: integration.script, - scriptEnabled: integration.scriptEnabled, - scriptEngine, - ...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }), - }), - triggerWords: integration.triggerWords, - retryFailedCalls: integration.retryFailedCalls, - retryCount: integration.retryCount, - retryDelay: integration.retryDelay, - triggerWordAnywhere: integration.triggerWordAnywhere, - runOnEdits: integration.runOnEdits, - _updatedAt: new Date(), - _updatedBy: await Users.findOne({ _id: this.userId }, { projection: { username: 1 } }), - }, + const updatedIntegration = await Integrations.findOneAndUpdate( + { _id: integrationId }, + { + $set: { + event: integration.event, + enabled: integration.enabled, + name: integration.name, + avatar: integration.avatar, + emoji: integration.emoji, + alias: integration.alias, + channel: typeof integration.channel === 'string' ? [integration.channel] : integration.channel, + targetRoom: integration.targetRoom, + impersonateUser: integration.impersonateUser, + username: integration.username, + userId: integration.userId, + urls: integration.urls, + token: integration.token, ...(isFrozen ? {} : { - $unset: { - ...(integration.scriptCompiled ? { scriptError: 1 as const } : { scriptCompiled: 1 as const }), - }, + script: integration.script, + scriptEnabled: integration.scriptEnabled, + scriptEngine, + ...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }), }), + triggerWords: integration.triggerWords, + retryFailedCalls: integration.retryFailedCalls, + retryCount: integration.retryCount, + retryDelay: integration.retryDelay, + triggerWordAnywhere: integration.triggerWordAnywhere, + runOnEdits: integration.runOnEdits, + _updatedAt: new Date(), + _updatedBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }), }, - ); + ...(isFrozen + ? {} + : { + $unset: { + ...(integration.scriptCompiled ? { scriptError: 1 as const } : { scriptCompiled: 1 as const }), + }, + }), + }, + ); + + if (updatedIntegration) { + await notifyOnIntegrationChanged(updatedIntegration); + } - if (updatedIntegration) { - await notifyOnIntegrationChanged(updatedIntegration); + return updatedIntegration; +}; + +Meteor.methods({ + async updateOutgoingIntegration(integrationId, _integration) { + if (!this.userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'updateOutgoingIntegration', + }); } - return updatedIntegration; + return updateOutgoingIntegration(this.userId, integrationId, _integration); }, }); From 6d19824111793e2e041efaa3701062dccb0703cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Thu, 10 Apr 2025 00:12:26 -0300 Subject: [PATCH 089/187] chore: Remove meteor functions from code highlight (#35720) --- apps/meteor/client/startup/startup.ts | 10 ---------- apps/meteor/client/views/root/AppLayout.tsx | 2 ++ .../views/root/hooks/useCodeHighlight.ts | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 apps/meteor/client/views/root/hooks/useCodeHighlight.ts diff --git a/apps/meteor/client/startup/startup.ts b/apps/meteor/client/startup/startup.ts index 45c102a72bf49..e75afdabc7a74 100644 --- a/apps/meteor/client/startup/startup.ts +++ b/apps/meteor/client/startup/startup.ts @@ -5,8 +5,6 @@ import { Session } from 'meteor/session'; import { Tracker } from 'meteor/tracker'; import moment from 'moment'; -import { register } from '../../app/markdown/lib/hljs'; -import { settings } from '../../app/settings/client'; import { getUserPreference } from '../../app/utils/client'; import 'hljs9/styles/github.css'; import { sdk } from '../../app/utils/client/lib/SDKClient'; @@ -63,11 +61,3 @@ Meteor.startup(() => { } }); }); -Meteor.startup(() => { - Tracker.autorun(() => { - const code = settings.get('Message_Code_highlight') as string | undefined; - code?.split(',').forEach((language: string) => { - language.trim() && register(language.trim()); - }); - }); -}); diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 6e2d04fa934b3..7cad62d46c6fc 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -2,6 +2,7 @@ import { useEffect, Suspense, useSyncExternalStore } from 'react'; import DocumentTitleWrapper from './DocumentTitleWrapper'; import PageLoading from './PageLoading'; +import { useCodeHighlight } from './hooks/useCodeHighlight'; import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke'; import { useForceLogout } from './hooks/useForceLogout'; import { useGoogleTagManager } from './hooks/useGoogleTagManager'; @@ -68,6 +69,7 @@ const AppLayout = () => { useStoreCookiesOnLogin(); useAutoupdate(); useForceLogout(); + useCodeHighlight(); useNotificationUserCalendar(); const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot); diff --git a/apps/meteor/client/views/root/hooks/useCodeHighlight.ts b/apps/meteor/client/views/root/hooks/useCodeHighlight.ts new file mode 100644 index 0000000000000..ad65adce3361d --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useCodeHighlight.ts @@ -0,0 +1,19 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +import { register } from '../../../../app/markdown/lib/hljs'; + +export const useCodeHighlight = (): void => { + const codeHighlight = useSetting('Message_Code_highlight'); + + useEffect(() => { + if (typeof codeHighlight === 'string') { + codeHighlight.split(',').forEach((language: string) => { + const trimmedLanguage = language.trim(); + if (trimmedLanguage) { + register(trimmedLanguage); + } + }); + } + }, [codeHighlight]); +}; From e868a6f6598b7eb2843ef79126d18abd1f604b4f Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 10 Apr 2025 10:58:42 -0300 Subject: [PATCH 090/187] fix: bypass ddp streamer method calls (#35757) --- .changeset/polite-turkeys-fetch.md | 6 ++++++ apps/meteor/server/services/meteor/service.ts | 13 ++++++++++--- packages/core-services/src/types/IMeteor.ts | 9 ++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 .changeset/polite-turkeys-fetch.md diff --git a/.changeset/polite-turkeys-fetch.md b/.changeset/polite-turkeys-fetch.md new file mode 100644 index 0000000000000..b442a2e682426 --- /dev/null +++ b/.changeset/polite-turkeys-fetch.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-services": patch +--- + +Fixes an issue where the bypass to call methods over microservices always returns to `{}` diff --git a/apps/meteor/server/services/meteor/service.ts b/apps/meteor/server/services/meteor/service.ts index a3653f2c36356..1378e9d41466d 100644 --- a/apps/meteor/server/services/meteor/service.ts +++ b/apps/meteor/server/services/meteor/service.ts @@ -262,18 +262,25 @@ export class MeteorService extends ServiceClassInternal implements IMeteor { return LoginServiceConfigurationModel.find({}, { projection: { secret: 0 } }).toArray(); } - async callMethodWithToken(userId: string, token: string, method: string, args: any[]): Promise { + async callMethodWithToken( + userId: string, + token: string, + method: string, + args: any[], + ): Promise<{ + result: unknown; + }> { const user = await Users.findOneByIdAndLoginHashedToken(userId, token, { projection: { _id: 1 }, }); if (!user) { return { - result: Meteor.callAsync(method, ...args), + result: await Meteor.callAsync(method, ...args), }; } return { - result: Meteor.runAsUser(userId, () => Meteor.callAsync(method, ...args)), + result: await Meteor.runAsUser(userId, () => Meteor.callAsync(method, ...args)), }; } diff --git a/packages/core-services/src/types/IMeteor.ts b/packages/core-services/src/types/IMeteor.ts index f905f7d7cddce..09f4e4470a20c 100644 --- a/packages/core-services/src/types/IMeteor.ts +++ b/packages/core-services/src/types/IMeteor.ts @@ -17,7 +17,14 @@ export type AutoUpdateRecord = { export interface IMeteor extends IServiceClass { getAutoUpdateClientVersions(): Promise>; getLoginServiceConfiguration(): Promise; - callMethodWithToken(userId: string | undefined, token: string | undefined, method: string, args: any[]): Promise; + callMethodWithToken( + userId: string | undefined, + token: string | undefined, + method: string, + args: any[], + ): Promise<{ + result: unknown; + }>; notifyGuestStatusChanged(token: string, status: string): Promise; getURL(path: string, params?: Record, cloudDeepLinkUrl?: string): Promise; } From 2351a29223c865ee5956dd9129ff6aca59085823 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 10 Apr 2025 08:11:54 -0600 Subject: [PATCH 091/187] chore: Break big `livechatTyped` file - 5 (#35758) --- .../app/apps/server/bridges/livechat.ts | 3 +- .../app/livechat/server/api/v1/agent.ts | 5 +- .../meteor/app/livechat/server/api/v1/room.ts | 6 +- .../livechat/server/hooks/afterUserActions.ts | 10 +- apps/meteor/app/livechat/server/lib/Helper.ts | 7 +- .../app/livechat/server/lib/LivechatTyped.ts | 174 +----------------- apps/meteor/app/livechat/server/lib/hooks.ts | 30 +++ apps/meteor/app/livechat/server/lib/rooms.ts | 65 ++++++- .../livechat/server/lib/stream/agentStatus.ts | 4 +- .../app/livechat/server/lib/transfer.ts | 84 +++++++++ apps/meteor/app/livechat/server/lib/utils.ts | 23 +++ .../server/methods/changeLivechatStatus.ts | 5 +- .../server/methods/returnAsInquiry.ts | 4 +- .../app/livechat/server/methods/transfer.ts | 4 +- .../server/lib/AutoTransferChatScheduler.ts | 4 +- .../lib/AutoTransferChatsScheduler.tests.ts | 2 +- 16 files changed, 235 insertions(+), 195 deletions(-) create mode 100644 apps/meteor/app/livechat/server/lib/hooks.ts create mode 100644 apps/meteor/app/livechat/server/lib/transfer.ts diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 896b7d0c540da..35f31e6e57a86 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -17,6 +17,7 @@ import { registerGuest } from '../../../livechat/server/lib/guests'; import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes'; import { updateMessage, sendMessage } from '../../../livechat/server/lib/messages'; import { createRoom } from '../../../livechat/server/lib/rooms'; +import { transfer } from '../../../livechat/server/lib/transfer'; import { settings } from '../../../settings/server'; declare module '@rocket.chat/apps/dist/converters/IAppMessagesConverter' { @@ -277,7 +278,7 @@ export class AppLivechatBridge extends LivechatBridge { } // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. - return LivechatTyped.transfer( + return transfer( (await this.orch.getConverters()?.get('rooms').convertAppRoom(currentRoom)) as IOmnichannelRoom, this.orch.getConverters()?.get('visitors').convertAppVisitor(visitor), { userId, departmentId, transferredBy, transferredTo }, diff --git a/apps/meteor/app/livechat/server/api/v1/agent.ts b/apps/meteor/app/livechat/server/api/v1/agent.ts index 0cd3139bf6a5c..0acedc924b0c3 100644 --- a/apps/meteor/app/livechat/server/api/v1/agent.ts +++ b/apps/meteor/app/livechat/server/api/v1/agent.ts @@ -8,6 +8,7 @@ import { hasPermissionAsync } from '../../../../authorization/server/functions/h import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { RoutingManager } from '../../lib/RoutingManager'; import { getRequiredDepartment } from '../../lib/departmentsLib'; +import { setUserStatusLivechat } from '../../lib/utils'; import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat'; API.v1.addRoute('livechat/agent.info/:rid/:token', { @@ -107,7 +108,7 @@ API.v1.addRoute( // Next version we'll update this to return an error // And update the FE accordingly if (canChangeStatus) { - await LivechatTyped.setUserStatusLivechat(agentId, newStatus); + await setUserStatusLivechat(agentId, newStatus); return API.v1.success({ status: newStatus }); } @@ -118,7 +119,7 @@ API.v1.addRoute( return API.v1.failure('error-business-hours-are-closed'); } - await LivechatTyped.setUserStatusLivechat(agentId, newStatus); + await setUserStatusLivechat(agentId, newStatus); return API.v1.success({ status: newStatus }); }, diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 9c07a1656d293..faec79b0208de 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -23,12 +23,12 @@ import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; import { closeLivechatRoom } from '../../../../lib/server/functions/closeLivechatRoom'; import { settings as rcSettings } from '../../../../settings/server'; import { normalizeTransferredByData } from '../../lib/Helper'; -import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { closeRoom } from '../../lib/closeRoom'; import { saveGuest } from '../../lib/guests'; import type { CloseRoomParams } from '../../lib/localTypes'; import { livechatLogger } from '../../lib/logger'; import { createRoom, saveRoomInfo } from '../../lib/rooms'; +import { transfer } from '../../lib/transfer'; import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat'; const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj); @@ -244,7 +244,7 @@ API.v1.addRoute( const { _id, username, name } = guest; const transferredBy = normalizeTransferredByData({ _id, username, name, userType: 'visitor' }, room); - if (!(await LivechatTyped.transfer(room, guest, { departmentId: department, transferredBy }))) { + if (!(await transfer(room, guest, { departmentId: department, transferredBy }))) { return API.v1.failure(); } @@ -340,7 +340,7 @@ API.v1.addRoute( } } - const chatForwardedResult = await LivechatTyped.transfer(room, guest, transferData); + const chatForwardedResult = await transfer(room, guest, transferData); if (!chatForwardedResult) { throw new Error('error-forwarding-chat'); } diff --git a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts index 2d0120c93c821..026eb8224d8c4 100644 --- a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts +++ b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts @@ -2,7 +2,7 @@ import { type IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; -import { Livechat as LivechatTyped } from '../lib/LivechatTyped'; +import { afterAgentUserActivated, afterAgentAdded, afterRemoveAgent } from '../lib/hooks'; type IAfterSaveUserProps = { user: IUser; @@ -16,18 +16,18 @@ const handleAgentUpdated = async (userData: IAfterSaveUserProps) => { const { user: newUser, oldUser } = userData; if (wasAgent(oldUser) && !isAgent(newUser)) { - await LivechatTyped.afterRemoveAgent(newUser); + await afterRemoveAgent(newUser); } if (!wasAgent(oldUser) && isAgent(newUser)) { - await LivechatTyped.afterAgentAdded(newUser); + await afterAgentAdded(newUser); } }; const handleAgentCreated = async (user: IUser) => { // created === no prev roles :) if (isAgent(user)) { - await LivechatTyped.afterAgentAdded(user); + await afterAgentAdded(user); } }; @@ -39,7 +39,7 @@ const handleDeactivateUser = async (user: IUser) => { const handleActivateUser = async (user: IUser) => { if (isAgent(user) && user.username) { - await LivechatTyped.afterAgentUserActivated(user); + await afterAgentUserActivated(user); } }; diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 7b26f7e18654a..b8b993d19dd9d 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -43,6 +43,7 @@ import { RoutingManager } from './RoutingManager'; import { isVerifiedChannelInSource } from './contacts/isVerifiedChannelInSource'; import { migrateVisitorIfMissingContact } from './contacts/migrateVisitorIfMissingContact'; import { getOnlineAgents } from './getOnlineAgents'; +import { saveTransferHistory } from './transfer'; import { callbacks } from '../../../../lib/callbacks'; import { validateEmail as validatorFunc } from '../../../../lib/emailValidator'; import { i18n } from '../../../../server/lib/i18n'; @@ -516,7 +517,7 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T return false; } - await LivechatTyped.saveTransferHistory(room, transferData); + await saveTransferHistory(room, transferData); const { servedBy } = roomTaken; if (servedBy) { @@ -632,7 +633,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi (department?.allowReceiveForwardOffline && !(await LivechatTyped.checkOnlineAgents(departmentId))) ) { logger.debug(`Room ${room._id} will be on department queue`); - await LivechatTyped.saveTransferHistory(room, transferData); + await saveTransferHistory(room, transferData); return RoutingManager.unassignAgent(inquiry, departmentId, true); } @@ -692,7 +693,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi ); } - await LivechatTyped.saveTransferHistory(room, transferData); + await saveTransferHistory(room, transferData); if (oldServedBy) { // if chat is queued then we don't ignore the new servedBy agent bcs at this // point the chat is not assigned to him/her and it is still in the queue diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 62a2163af1bc9..95f03ee12c173 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,12 +1,10 @@ -import { Message, VideoConf, api } from '@rocket.chat/core-services'; +import { VideoConf, api } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, IUser, ILivechatVisitor, ILivechatAgent, ILivechatDepartment, - AtLeast, - TransferData, IOmnichannelAgent, UserStatus, } from '@rocket.chat/core-typings'; @@ -26,7 +24,7 @@ import { LivechatCustomField, } from '@rocket.chat/models'; import { removeEmpty } from '@rocket.chat/tools'; -import { Match, check } from 'meteor/check'; +import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; import UAParser from 'ua-parser-js'; @@ -46,8 +44,8 @@ import { } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; -import { parseAgentCustomFields, updateDepartmentAgents, normalizeTransferredByData } from './Helper'; -import { RoutingManager } from './RoutingManager'; +import { parseAgentCustomFields, updateDepartmentAgents } from './Helper'; +import { afterAgentAdded, afterRemoveAgent } from './hooks'; type AKeyOf = { [K in keyof T]?: T[K]; @@ -272,49 +270,6 @@ class LivechatClass { }); } - async transfer(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData) { - this.logger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`); - if (room.onHold) { - throw new Error('error-room-onHold'); - } - - if (transferData.departmentId) { - const department = await LivechatDepartment.findOneById>(transferData.departmentId, { - projection: { name: 1 }, - }); - if (!department) { - throw new Error('error-invalid-department'); - } - - transferData.department = department; - this.logger.debug(`Transfering room ${room._id} to department ${transferData.department?._id}`); - } - - return RoutingManager.transferRoom(room, guest, transferData); - } - - async forwardOpenChats(userId: string) { - this.logger.debug(`Transferring open chats for user ${userId}`); - const user = await Users.findOneById(userId); - if (!user) { - throw new Error('error-invalid-user'); - } - - const { _id, username, name } = user; - for await (const room of LivechatRooms.findOpenByAgent(userId)) { - const guest = await LivechatVisitors.findOneEnabledById(room.v._id); - if (!guest) { - continue; - } - - const transferredBy = normalizeTransferredByData({ _id, username, name }, room); - await this.transfer(room, guest, { - transferredBy, - departmentId: guest.department, - }); - } - } - async setUserStatusLivechatIf(userId: string, status: ILivechatAgentStatus, condition?: Filter, fields?: AKeyOf) { const result = await Users.setLivechatStatusIf(userId, status, condition, fields); @@ -330,81 +285,6 @@ class LivechatClass { return result; } - async returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: any = {}) { - this.logger.debug({ msg: `Transfering room to ${departmentId ? 'department' : ''} queue`, room }); - if (!room.open) { - throw new Meteor.Error('room-closed'); - } - - if (room.onHold) { - throw new Meteor.Error('error-room-onHold'); - } - - if (!room.servedBy) { - return false; - } - - const user = await Users.findOneById(room.servedBy._id); - if (!user?._id) { - throw new Meteor.Error('error-invalid-user'); - } - - // find inquiry corresponding to room - const inquiry = await LivechatInquiry.findOne({ rid: room._id }); - if (!inquiry) { - return false; - } - - const transferredBy = normalizeTransferredByData(user, room); - this.logger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); - const transferData = { roomId: room._id, scope: 'queue', departmentId, transferredBy, ...overrideTransferData }; - try { - await this.saveTransferHistory(room, transferData); - await RoutingManager.unassignAgent(inquiry, departmentId); - } catch (e) { - this.logger.error(e); - throw new Meteor.Error('error-returning-inquiry'); - } - - callbacks.runAsync('livechat:afterReturnRoomAsInquiry', { room }); - - return true; - } - - async saveTransferHistory(room: IOmnichannelRoom, transferData: TransferData) { - const { departmentId: previousDepartment } = room; - const { department: nextDepartment, transferredBy, transferredTo, scope, comment } = transferData; - - check( - transferredBy, - Match.ObjectIncluding({ - _id: String, - username: String, - name: Match.Maybe(String), - userType: String, - }), - ); - - const { _id, username } = transferredBy; - const scopeData = scope || (nextDepartment ? 'department' : 'agent'); - this.logger.info(`Storing new chat transfer of ${room._id} [Transfered by: ${_id} to ${scopeData}]`); - - const transferMessage = { - ...(transferData.transferredBy.userType === 'visitor' && { token: room.v.token }), - transferData: { - transferredBy, - ts: new Date(), - scope: scopeData, - comment, - ...(previousDepartment && { previousDepartment }), - ...(nextDepartment && { nextDepartment }), - ...(transferredTo && { transferredTo }), - }, - }; - - await Message.saveSystemMessageAndNotifyUser('livechat_transfer_history', room._id, '', { _id, username }, transferMessage); - } - async setCustomFields({ token, key, value, overwrite }: { key: string; value: string; overwrite: boolean; token: string }) { Livechat.logger.debug(`Setting custom fields data for visitor with token ${token}`); @@ -435,11 +315,6 @@ class LivechatClass { return result.modifiedCount; } - async afterRemoveAgent(user: AtLeast) { - await callbacks.run('livechat.afterAgentRemoved', { agent: user }); - return true; - } - async removeAgent(username: string) { const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); @@ -450,13 +325,14 @@ class LivechatClass { const { _id } = user; if (await removeUserFromRolesAsync(_id, ['livechat-agent'])) { - return this.afterRemoveAgent(user); + return afterRemoveAgent(user); } return false; } async removeManager(username: string) { + // TODO: we already validated user exists at this point, remove this check const user = await Users.findOneByUsername(username, { projection: { _id: 1 } }); if (!user) { @@ -545,34 +421,6 @@ class LivechatClass { } } - async setUserStatusLivechat(userId: string, status: ILivechatAgentStatus) { - const user = await Users.setLivechatStatus(userId, status); - callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); - - if (user.modifiedCount > 0) { - void notifyOnUserChange({ - id: userId, - clientAction: 'updated', - diff: { - statusLivechat: status, - livechatStatusSystemModified: false, - }, - }); - } - - return user; - } - - async afterAgentAdded(user: IUser) { - await Promise.all([ - Users.setOperator(user._id, true), - this.setUserStatusLivechat(user._id, user.status !== 'offline' ? ILivechatAgentStatus.AVAILABLE : ILivechatAgentStatus.NOT_AVAILABLE), - ]); - callbacks.runAsync('livechat.onNewAgentCreated', user._id); - - return user; - } - async addAgent(username: string) { check(username, String); @@ -583,20 +431,12 @@ class LivechatClass { } if (await addUserRolesAsync(user._id, ['livechat-agent'])) { - return this.afterAgentAdded(user); + return afterAgentAdded(user); } return false; } - async afterAgentUserActivated(user: IUser) { - if (!user.roles.includes('livechat-agent')) { - throw new Error('invalid-user-role'); - } - await Users.setOperator(user._id, true); - callbacks.runAsync('livechat.onNewAgentCreated', user._id); - } - async addManager(username: string) { check(username, String); diff --git a/apps/meteor/app/livechat/server/lib/hooks.ts b/apps/meteor/app/livechat/server/lib/hooks.ts new file mode 100644 index 0000000000000..d877ce13b8f6e --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/hooks.ts @@ -0,0 +1,30 @@ +import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; +import type { AtLeast, IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +import { setUserStatusLivechat } from './utils'; +import { callbacks } from '../../../../lib/callbacks'; + +export async function afterAgentUserActivated(user: IUser) { + if (!user.roles.includes('livechat-agent')) { + throw new Error('invalid-user-role'); + } + // TODO: deprecate this `operator` property + await Users.setOperator(user._id, true); + callbacks.runAsync('livechat.onNewAgentCreated', user._id); +} + +export async function afterAgentAdded(user: IUser) { + await Promise.all([ + Users.setOperator(user._id, true), + setUserStatusLivechat(user._id, user.status !== 'offline' ? ILivechatAgentStatus.AVAILABLE : ILivechatAgentStatus.NOT_AVAILABLE), + ]); + callbacks.runAsync('livechat.onNewAgentCreated', user._id); + + return user; +} + +export async function afterRemoveAgent(user: AtLeast) { + await callbacks.run('livechat.afterAgentRemoved', { agent: user }); + return true; +} diff --git a/apps/meteor/app/livechat/server/lib/rooms.ts b/apps/meteor/app/livechat/server/lib/rooms.ts index 4db8f526e7c4c..1d8cc1c043569 100644 --- a/apps/meteor/app/livechat/server/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/lib/rooms.ts @@ -1,11 +1,30 @@ import { AppEvents, Apps } from '@rocket.chat/apps'; -import type { ILivechatVisitor, IMessage, IOmnichannelRoomInfo, SelectedAgent, IOmnichannelRoomExtraData } from '@rocket.chat/core-typings'; -import { LivechatRooms, LivechatContacts, Messages, LivechatCustomField, LivechatInquiry, Rooms, Subscriptions } from '@rocket.chat/models'; - +import type { + ILivechatVisitor, + IMessage, + IOmnichannelRoomInfo, + SelectedAgent, + IOmnichannelRoomExtraData, + IOmnichannelRoom, +} from '@rocket.chat/core-typings'; +import { + LivechatRooms, + LivechatContacts, + Messages, + LivechatCustomField, + LivechatInquiry, + Rooms, + Subscriptions, + Users, +} from '@rocket.chat/models'; + +import { normalizeTransferredByData } from './Helper'; import { QueueManager } from './QueueManager'; +import { RoutingManager } from './RoutingManager'; import { Visitors } from './Visitors'; import { getRequiredDepartment } from './departmentsLib'; import { livechatLogger } from './logger'; +import { saveTransferHistory } from './transfer'; import { callbacks } from '../../../../lib/callbacks'; import { trim } from '../../../../lib/utils/stringUtils'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -182,3 +201,43 @@ export async function saveRoomInfo( return true; } + +export async function returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: any = {}) { + livechatLogger.debug({ msg: `Transfering room to ${departmentId ? 'department' : ''} queue`, room }); + if (!room.open) { + throw new Meteor.Error('room-closed'); + } + + if (room.onHold) { + throw new Meteor.Error('error-room-onHold'); + } + + if (!room.servedBy) { + return false; + } + + const user = await Users.findOneById(room.servedBy._id); + if (!user?._id) { + throw new Meteor.Error('error-invalid-user'); + } + + const inquiry = await LivechatInquiry.findOne({ rid: room._id }); + if (!inquiry) { + return false; + } + + const transferredBy = normalizeTransferredByData(user, room); + livechatLogger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); + const transferData = { roomId: room._id, scope: 'queue', departmentId, transferredBy, ...overrideTransferData }; + try { + await saveTransferHistory(room, transferData); + await RoutingManager.unassignAgent(inquiry, departmentId); + } catch (e) { + livechatLogger.error(e); + throw new Meteor.Error('error-returning-inquiry'); + } + + callbacks.runAsync('livechat:afterReturnRoomAsInquiry', { room }); + + return true; +} diff --git a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts index 71bc21f600323..d2862c93847fa 100644 --- a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts +++ b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts @@ -1,8 +1,8 @@ import { Logger } from '@rocket.chat/logger'; import { settings } from '../../../../settings/server'; -import { Livechat } from '../LivechatTyped'; import { closeOpenChats } from '../closeRoom'; +import { forwardOpenChats } from '../transfer'; const logger = new Logger('AgentStatusWatcher'); @@ -73,7 +73,7 @@ export const onlineAgents = { } if (action === 'forward') { - return await Livechat.forwardOpenChats(userId); + return await forwardOpenChats(userId); } } catch (e) { logger.error({ diff --git a/apps/meteor/app/livechat/server/lib/transfer.ts b/apps/meteor/app/livechat/server/lib/transfer.ts new file mode 100644 index 0000000000000..74dcca09893ae --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/transfer.ts @@ -0,0 +1,84 @@ +import { Message } from '@rocket.chat/core-services'; +import type { ILivechatDepartment, ILivechatVisitor, IOmnichannelRoom, TransferData } from '@rocket.chat/core-typings'; +import { Users, LivechatRooms, LivechatVisitors, LivechatDepartment } from '@rocket.chat/models'; + +import { normalizeTransferredByData } from './Helper'; +import { RoutingManager } from './RoutingManager'; +import { livechatLogger } from './logger'; + +export async function saveTransferHistory(room: IOmnichannelRoom, transferData: TransferData) { + const { departmentId: previousDepartment } = room; + const { department: nextDepartment, transferredBy, transferredTo, scope, comment } = transferData; + + check( + transferredBy, + Match.ObjectIncluding({ + _id: String, + username: String, + name: Match.Maybe(String), + userType: String, + }), + ); + + const { _id, username } = transferredBy; + const scopeData = scope || (nextDepartment ? 'department' : 'agent'); + livechatLogger.info(`Storing new chat transfer of ${room._id} [Transfered by: ${_id} to ${scopeData}]`); + + const transferMessage = { + ...(transferData.transferredBy.userType === 'visitor' && { token: room.v.token }), + transferData: { + transferredBy, + ts: new Date(), + scope: scopeData, + comment, + ...(previousDepartment && { previousDepartment }), + ...(nextDepartment && { nextDepartment }), + ...(transferredTo && { transferredTo }), + }, + }; + + await Message.saveSystemMessageAndNotifyUser('livechat_transfer_history', room._id, '', { _id, username }, transferMessage); +} + +export async function forwardOpenChats(userId: string) { + livechatLogger.debug(`Transferring open chats for user ${userId}`); + const user = await Users.findOneById(userId); + if (!user) { + throw new Error('error-invalid-user'); + } + + const { _id, username, name } = user; + for await (const room of LivechatRooms.findOpenByAgent(userId)) { + const guest = await LivechatVisitors.findOneEnabledById(room.v._id); + if (!guest) { + continue; + } + + const transferredBy = normalizeTransferredByData({ _id, username, name }, room); + await transfer(room, guest, { + transferredBy, + departmentId: guest.department, + }); + } +} + +export async function transfer(room: IOmnichannelRoom, guest: ILivechatVisitor, transferData: TransferData) { + livechatLogger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`); + if (room.onHold) { + throw new Error('error-room-onHold'); + } + + if (transferData.departmentId) { + const department = await LivechatDepartment.findOneById>(transferData.departmentId, { + projection: { name: 1 }, + }); + if (!department) { + throw new Error('error-invalid-department'); + } + + transferData.department = department; + livechatLogger.debug(`Transfering room ${room._id} to department ${transferData.department?._id}`); + } + + return RoutingManager.transferRoom(room, guest, transferData); +} diff --git a/apps/meteor/app/livechat/server/lib/utils.ts b/apps/meteor/app/livechat/server/lib/utils.ts index 21a7b9e91d672..ad1eb0a793033 100644 --- a/apps/meteor/app/livechat/server/lib/utils.ts +++ b/apps/meteor/app/livechat/server/lib/utils.ts @@ -1,5 +1,28 @@ +import type { ILivechatAgentStatus } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + import { RoutingManager } from './RoutingManager'; +import { callbacks } from '../../../../lib/callbacks'; +import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; export function showConnecting() { return RoutingManager.getConfig()?.showConnecting || false; } + +export async function setUserStatusLivechat(userId: string, status: ILivechatAgentStatus) { + const user = await Users.setLivechatStatus(userId, status); + callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); + + if (user.modifiedCount > 0) { + void notifyOnUserChange({ + id: userId, + clientAction: 'updated', + diff: { + statusLivechat: status, + livechatStatusSystemModified: false, + }, + }); + } + + return user; +} diff --git a/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts b/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts index f45588cad6d37..816bd77266d9c 100644 --- a/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts +++ b/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { Livechat as LivechatTS } from '../lib/LivechatTyped'; +import { setUserStatusLivechat } from '../lib/utils'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -59,7 +60,7 @@ Meteor.methods({ method: 'livechat:changeLivechatStatus', }); } - return LivechatTS.setUserStatusLivechat(agentId, newStatus); + return setUserStatusLivechat(agentId, newStatus); } if (!(await LivechatTS.allowAgentChangeServiceStatus(newStatus, agentId))) { @@ -68,6 +69,6 @@ Meteor.methods({ }); } - return LivechatTS.setUserStatusLivechat(agentId, newStatus); + return setUserStatusLivechat(agentId, newStatus); }, }); diff --git a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts b/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts index bf76519a5afb7..df4f4f2a5f76b 100644 --- a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts +++ b/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts @@ -5,7 +5,7 @@ import { LivechatRooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Livechat } from '../lib/LivechatTyped'; +import { returnRoomAsInquiry } from '../lib/rooms'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -38,6 +38,6 @@ Meteor.methods({ throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:returnAsInquiry' }); } - return Livechat.returnRoomAsInquiry(room, departmentId); + return returnRoomAsInquiry(room, departmentId); }, }); diff --git a/apps/meteor/app/livechat/server/methods/transfer.ts b/apps/meteor/app/livechat/server/methods/transfer.ts index e0516fa279817..d4ce08b0a5619 100644 --- a/apps/meteor/app/livechat/server/methods/transfer.ts +++ b/apps/meteor/app/livechat/server/methods/transfer.ts @@ -8,7 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; import { normalizeTransferredByData } from '../lib/Helper'; -import { Livechat } from '../lib/LivechatTyped'; +import { transfer } from '../lib/transfer'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -100,6 +100,6 @@ Meteor.methods({ }; } - return Livechat.transfer(room, guest, normalizedTransferData); + return transfer(room, guest, normalizedTransferData); }, }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index b58aee89f5401..aedc1a77ac3bc 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -7,8 +7,8 @@ import { MongoInternals } from 'meteor/mongo'; import { schedulerLogger } from './logger'; import { forwardRoomToAgent } from '../../../../../app/livechat/server/lib/Helper'; -import { Livechat as LivechatTyped } from '../../../../../app/livechat/server/lib/LivechatTyped'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; +import { returnRoomAsInquiry } from '../../../../../app/livechat/server/lib/rooms'; import { settings } from '../../../../../app/settings/server'; const SCHEDULER_NAME = 'omnichannel_scheduler'; @@ -91,7 +91,7 @@ export class AutoTransferChatSchedulerClass { if (!RoutingManager.getConfig()?.autoAssignAgent) { this.logger.debug(`Auto-assign agent is disabled, returning room ${roomId} as inquiry`); - await LivechatTyped.returnRoomAsInquiry(room, departmentId, { + await returnRoomAsInquiry(room, departmentId, { scope: 'autoTransferUnansweredChatsToQueue', comment: timeoutDuration, transferredBy: await this.getSchedulerUser(), diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/AutoTransferChatsScheduler.tests.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/AutoTransferChatsScheduler.tests.ts index fc51201c7b316..5bf2ad56a903e 100644 --- a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/AutoTransferChatsScheduler.tests.ts +++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/lib/AutoTransferChatsScheduler.tests.ts @@ -72,7 +72,7 @@ const mocks = { }, '../../../../../app/livechat/server/lib/RoutingManager': { RoutingManager: { getConfig: routingConfigMock, getNextAgent } }, '../../../../../app/livechat/server/lib/Helper': { forwardRoomToAgent }, - '../../../../../app/livechat/server/lib/LivechatTyped': { Livechat: { returnRoomAsInquiry: returnRoomAsInquiryMock } }, + '../../../../../app/livechat/server/lib/rooms': { returnRoomAsInquiry: returnRoomAsInquiryMock }, '../../../../../app/settings/server': { settings: { get: settingsGet } }, './logger': { schedulerLogger: mockLogger }, '@rocket.chat/models': { From a8896a7ed96021f1c0d0b1eb44945ee3f69a080b Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 10 Apr 2025 14:22:42 -0300 Subject: [PATCH 092/187] fix: Omnichannel making a request for room data after every message sent (#35618) --- .changeset/heavy-boats-mix.md | 6 ++ .../app/livechat/server/lib/RoutingManager.ts | 2 +- apps/meteor/app/livechat/server/lib/rooms.ts | 6 ++ .../tests/end-to-end/api/livechat/00-rooms.ts | 62 ++++++++++++++ .../end-to-end/api/livechat/05-inquiries.ts | 85 +++++++++++++++++++ .../src/models/ILivechatInquiryModel.ts | 3 +- packages/models/src/models/LivechatInquiry.ts | 21 ++++- 7 files changed, 180 insertions(+), 5 deletions(-) create mode 100644 .changeset/heavy-boats-mix.md diff --git a/.changeset/heavy-boats-mix.md b/.changeset/heavy-boats-mix.md new file mode 100644 index 0000000000000..1477fce4c3564 --- /dev/null +++ b/.changeset/heavy-boats-mix.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue when sending a message on an omnichannel room would cause the web client to fetch the room data again. diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index a62c7dfea46be..11140feb08a10 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -175,7 +175,7 @@ export const RoutingManager: Routing = { const { servedBy } = room; if (shouldQueue) { - const queuedInquiry = await LivechatInquiry.queueInquiry(inquiry._id); + const queuedInquiry = await LivechatInquiry.queueInquiry(inquiry._id, room.lastMessage); if (queuedInquiry) { inquiry = queuedInquiry; void notifyOnLivechatInquiryChanged(inquiry, 'updated', { diff --git a/apps/meteor/app/livechat/server/lib/rooms.ts b/apps/meteor/app/livechat/server/lib/rooms.ts index 1d8cc1c043569..f6609fe1fc2b8 100644 --- a/apps/meteor/app/livechat/server/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/lib/rooms.ts @@ -226,6 +226,12 @@ export async function returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: return false; } + // update inquiry's last message with room's last message to correctly display in the queue + // because we stop updating the inquiry when it's been taken + if (room.lastMessage) { + await LivechatInquiry.setLastMessageById(inquiry._id, room.lastMessage); + } + const transferredBy = normalizeTransferredByData(user, room); livechatLogger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); const transferData = { roomId: room._id, scope: 'queue', departmentId, transferredBy, ...overrideTransferData }; diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 1679319d8804c..4949ba2e002f9 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -40,6 +40,7 @@ import { makeAgentUnavailable, sendAgentMessage, fetchInquiry, + takeInquiry, } from '../../../data/livechat/rooms'; import { saveTags } from '../../../data/livechat/tags'; import { createMonitor, createUnit, deleteUnit } from '../../../data/livechat/units'; @@ -1170,6 +1171,67 @@ describe('LIVECHAT - rooms', () => { await deleteDepartment(forwardToOfflineDepartment._id); }); + (IS_EE ? it : it.skip)( + 'should update inquiry last message when manager forward to offline department and the inquiry returns to queued', + async () => { + await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); + const { department: initialDepartment, agent } = await createDepartmentWithAnOnlineAgent(); + const { department: forwardToOfflineDepartment, agent: offlineAgent } = await createDepartmentWithAnOfflineAgent({ + allowReceiveForwardOffline: true, + }); + + const newVisitor = await createVisitor(initialDepartment._id); + const newRoom = await createLivechatRoom(newVisitor.token); + + const inq = await fetchInquiry(newRoom._id); + await takeInquiry(inq._id, agent.credentials); + + const msgText = `return to queue ${Date.now()}`; + await request.post(api('livechat/message')).send({ token: newVisitor.token, rid: newRoom._id, msg: msgText }).expect(200); + + await makeAgentUnavailable(offlineAgent.credentials); + + const manager = await createUser(); + const managerCredentials = await login(manager.username, password); + await createManager(manager.username); + + await request.post(api('livechat/room.forward')).set(managerCredentials).send({ + roomId: newRoom._id, + departmentId: forwardToOfflineDepartment._id, + clientAction: true, + comment: 'test comment', + }); + + await request + .get(api(`livechat/queue`)) + .set(credentials) + .query({ + count: 1, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.queue).to.be.an('array'); + expect(res.body.queue[0].chats).not.to.undefined; + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + }); + + const inquiry = await fetchInquiry(newRoom._id); + + expect(inquiry).to.have.property('_id', inquiry._id); + expect(inquiry).to.have.property('rid', newRoom._id); + expect(inquiry).to.have.property('lastMessage'); + expect(inquiry.lastMessage).to.have.property('msg', ''); + expect(inquiry.lastMessage).to.have.property('t', 'livechat_transfer_history'); + + await deleteDepartment(initialDepartment._id); + await deleteDepartment(forwardToOfflineDepartment._id); + }, + ); + let roomId: string; let visitorToken: string; (IS_EE ? it : it.skip)('should return a success message when transferring to a fallback department', async () => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts b/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts index 05702deaaf709..b504de6da0be6 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.ts @@ -11,6 +11,7 @@ import { createDepartment, createLivechatRoom, createVisitor, + deleteVisitor, fetchInquiry, getLivechatRoomInfo, makeAgentAvailable, @@ -456,4 +457,88 @@ describe('LIVECHAT - inquiries', () => { expect(depInq.length).to.be.equal(1); }); }); + + describe('keep inquiry last message updated', () => { + let room: any; + let visitor: any; + let agent: any; + + before(async () => { + agent = await createAgent(); + visitor = await createVisitor(); + + await makeAgentAvailable(); + room = await createLivechatRoom(visitor.token); + }); + + after(async () => { + await deleteVisitor(visitor.token); + }); + + it('should update inquiry last message', async () => { + const msgText = `update inquiry ${Date.now()}`; + + await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200); + + const inquiry = await fetchInquiry(room._id); + + expect(inquiry).to.have.property('_id', inquiry._id); + expect(inquiry).to.have.property('rid', room._id); + expect(inquiry).to.have.property('lastMessage'); + expect(inquiry.lastMessage).to.have.property('msg', msgText); + }); + + it('should update room last message after inquiry is taken', async () => { + const msgText = `update room ${Date.now()}`; + + const inquiry = await fetchInquiry(room._id); + + await request + .post(api('livechat/inquiries.take')) + .set(credentials) + .send({ + inquiryId: inquiry._id, + userId: agent._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200); + + // check room + const roomInfo = await getLivechatRoomInfo(room._id); + expect(roomInfo).to.have.property('lastMessage'); + expect(roomInfo.lastMessage).to.have.property('msg', msgText); + }); + + it('should have the correct last message when room is returned to queue', async () => { + const msgText = `return to queue ${Date.now()}`; + + await request.post(api('livechat/message')).send({ token: visitor.token, rid: room._id, msg: msgText }).expect(200); + + await request + .post(methodCall('livechat:returnAsInquiry')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:returnAsInquiry', + params: [room._id], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200); + + const inquiry = await fetchInquiry(room._id); + + expect(inquiry).to.have.property('_id', inquiry._id); + expect(inquiry).to.have.property('rid', room._id); + expect(inquiry).to.have.property('lastMessage'); + expect(inquiry.lastMessage).to.have.property('msg', msgText); + }); + }); }); diff --git a/packages/model-typings/src/models/ILivechatInquiryModel.ts b/packages/model-typings/src/models/ILivechatInquiryModel.ts index 19b52f3f90e9c..5b5283aec610c 100644 --- a/packages/model-typings/src/models/ILivechatInquiryModel.ts +++ b/packages/model-typings/src/models/ILivechatInquiryModel.ts @@ -16,6 +16,7 @@ export interface ILivechatInquiryModel extends IBaseModel; setDepartmentByInquiryId(inquiryId: string, department: string): Promise; setLastMessageByRoomId(rid: ILivechatInquiryRecord['rid'], message: IMessage): Promise; + setLastMessageById(inquiryId: string, lastMessage: IMessage): Promise; findNextAndLock( queueSortBy: FindOptions['sort'], department: string | null, @@ -31,7 +32,7 @@ export interface ILivechatInquiryModel extends IBaseModel): FindCursor; takeInquiry(inquiryId: string): Promise; openInquiry(inquiryId: string): Promise; - queueInquiry(inquiryId: string): Promise; + queueInquiry(inquiryId: string, lastMessage?: IMessage): Promise; queueInquiryAndRemoveDefaultAgent(inquiryId: string): Promise; readyInquiry(inquiryId: string): Promise; changeDepartmentIdByRoomId(rid: string, department: string): Promise; diff --git a/packages/models/src/models/LivechatInquiry.ts b/packages/models/src/models/LivechatInquiry.ts index d1361f265494c..79d581ce888ff 100644 --- a/packages/models/src/models/LivechatInquiry.ts +++ b/packages/models/src/models/LivechatInquiry.ts @@ -153,8 +153,19 @@ export class LivechatInquiryRaw extends BaseRaw implemen return this.findOneAndUpdate({ _id: inquiryId }, { $set: { department } }, { returnDocument: 'after' }); } + /** + * Updates the `lastMessage` of inquiries that are not taken yet, after they're taken we only need to update room's `lastMessage` + */ async setLastMessageByRoomId(rid: ILivechatInquiryRecord['rid'], message: IMessage): Promise { - return this.findOneAndUpdate({ rid }, { $set: { lastMessage: message } }, { returnDocument: 'after' }); + return this.findOneAndUpdate( + { rid, status: { $ne: LivechatInquiryStatus.TAKEN } }, + { $set: { lastMessage: message } }, + { returnDocument: 'after' }, + ); + } + + async setLastMessageById(inquiryId: string, lastMessage: IMessage): Promise { + return this.updateOne({ _id: inquiryId }, { $set: { lastMessage } }); } async findNextAndLock( @@ -316,13 +327,17 @@ export class LivechatInquiryRaw extends BaseRaw implemen ); } - async queueInquiry(inquiryId: string): Promise { + async queueInquiry(inquiryId: string, lastMessage?: IMessage): Promise { return this.findOneAndUpdate( { _id: inquiryId, }, { - $set: { status: LivechatInquiryStatus.QUEUED, queuedAt: new Date() }, + $set: { + status: LivechatInquiryStatus.QUEUED, + queuedAt: new Date(), + ...(lastMessage && { lastMessage }), + }, $unset: { takenAt: 1 }, }, { returnDocument: 'after' }, From cd5af1776cd5928b1280ae8c1b475ecc282f2353 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 10 Apr 2025 14:43:29 -0300 Subject: [PATCH 093/187] chore: stop reporting outside the main repo (#35745) Co-authored-by: Diego Sampaio --- .github/workflows/ci-test-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 12c5aea31216d..7ff84fed5d6d2 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -276,7 +276,7 @@ jobs: REPORTER_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_ROCKETCHAT_API_KEY }} REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} - REPORTER_ROCKETCHAT_REPORT: ${{ github.event.pull_request.draft != 'true' && 'true' || '' }} + REPORTER_ROCKETCHAT_REPORT: ${{ github.event.pull_request.draft != 'true' && secrets.REPORTER_ROCKETCHAT_URL != '' && 'true' || '' }} REPORTER_ROCKETCHAT_RUN: ${{ github.run_number }} REPORTER_ROCKETCHAT_BRANCH: ${{ github.ref }} REPORTER_ROCKETCHAT_DRAFT: ${{ github.event.pull_request.draft }} From 5ef707f27ae56ff59aaea1702244bc67f13effb0 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Thu, 10 Apr 2025 15:05:42 -0300 Subject: [PATCH 094/187] chore: remove meteor calls from permissions (server) (#35750) --- apps/meteor/app/api/server/v1/permissions.ts | 5 ++-- .../server/streamer/permissions/index.ts | 27 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/apps/meteor/app/api/server/v1/permissions.ts b/apps/meteor/app/api/server/v1/permissions.ts index 65dfafe5ccce2..9b7cee5d3c7b5 100644 --- a/apps/meteor/app/api/server/v1/permissions.ts +++ b/apps/meteor/app/api/server/v1/permissions.ts @@ -3,6 +3,7 @@ import { Permissions, Roles } from '@rocket.chat/models'; import { isBodyParamsValidPermissionUpdate } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; +import { permissionsGetMethod } from '../../../authorization/server/streamer/permissions'; import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; import { API } from '../api'; @@ -21,7 +22,7 @@ API.v1.addRoute( updatedSinceDate = new Date(updatedSince); } - const result = (await Meteor.callAsync('permissions/get', updatedSinceDate)) as { + const result = (await permissionsGetMethod(updatedSinceDate)) as { update: IPermission[]; remove: IPermission[]; }; @@ -69,7 +70,7 @@ API.v1.addRoute( void notifyOnPermissionChangedById(permission._id); } - const result = (await Meteor.callAsync('permissions/get')) as IPermission[]; + const result = (await permissionsGetMethod()) as IPermission[]; return API.v1.success({ permissions: result, diff --git a/apps/meteor/app/authorization/server/streamer/permissions/index.ts b/apps/meteor/app/authorization/server/streamer/permissions/index.ts index e74cf37869fd8..545b7067e677a 100644 --- a/apps/meteor/app/authorization/server/streamer/permissions/index.ts +++ b/apps/meteor/app/authorization/server/streamer/permissions/index.ts @@ -14,22 +14,27 @@ declare module '@rocket.chat/ddp-client' { } } +export const permissionsGetMethod = async ( + updatedAt?: Date, +): Promise>[] }> => { + const records = await Permissions.find(updatedAt && { _updatedAt: { $gt: updatedAt } }).toArray(); + + if (updatedAt instanceof Date) { + return { + update: records, + remove: await Permissions.trashFindDeletedAfter(updatedAt, {}, { projection: { _id: 1, _deletedAt: 1 } }).toArray(), + }; + } + + return records; +}; + Meteor.methods({ async 'permissions/get'(updatedAt?: Date) { check(updatedAt, Match.Maybe(Date)); - // TODO: should we return this for non logged users? // TODO: we could cache this collection - const records = await Permissions.find(updatedAt && { _updatedAt: { $gt: updatedAt } }).toArray(); - - if (updatedAt instanceof Date) { - return { - update: records, - remove: await Permissions.trashFindDeletedAfter(updatedAt, {}, { projection: { _id: 1, _deletedAt: 1 } }).toArray(), - }; - } - - return records; + return permissionsGetMethod(updatedAt); }, }); From 3f7e7f823f37886c8614d5caa963b2b9cbda5b5b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 10 Apr 2025 13:21:42 -0600 Subject: [PATCH 095/187] fix: Unhandled rejection when removing an agent from a department when multiple business hours are enabled (#35736) --- .changeset/fluffy-weeks-float.md | 5 ++ .../server/business-hour/Multiple.ts | 3 +- .../end-to-end/api/livechat/10-departments.ts | 48 +++++++++++++++++-- 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 .changeset/fluffy-weeks-float.md diff --git a/.changeset/fluffy-weeks-float.md b/.changeset/fluffy-weeks-float.md new file mode 100644 index 0000000000000..cca4e2c037d11 --- /dev/null +++ b/.changeset/fluffy-weeks-float.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an error causing the server to throw an "unhandled promise rejection" when removing an agent from a department without a business hour when using `Multiple` business hours diff --git a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts index 00f67d0d8fd00..45f19dc2eda77 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts @@ -157,6 +157,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior if (!department || !agentsId.length) { return options; } + return this.handleRemoveAgentsFromDepartments(department, agentsId, options); } @@ -325,7 +326,7 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior const [agentsWithDepartment, [agentsOfDepartment] = []] = await Promise.all([ LivechatDepartmentAgents.findByAgentIds(agentsIds, { projection: { agentId: 1 } }).toArray(), - LivechatDepartment.findAgentsByBusinessHourId(department.businessHourId).toArray(), + ...[department?.businessHourId ? LivechatDepartment.findAgentsByBusinessHourId(department.businessHourId).toArray() : []], ]); for (const agentId of agentsIds) { diff --git a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts index 6b377674bc646..9a92b6332d92e 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts @@ -1,5 +1,6 @@ import { faker } from '@faker-js/faker'; -import type { ILivechatDepartment } from '@rocket.chat/core-typings'; +import type { Credentials } from '@rocket.chat/api-client'; +import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; import type { Response } from 'supertest'; @@ -15,8 +16,9 @@ import { getLivechatRoomInfo, } from '../../../data/livechat/rooms'; import { createMonitor, createUnit } from '../../../data/livechat/units'; -import { restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper'; -import { createUser, deleteUser } from '../../../data/users.helper'; +import { restorePermissionToRoles, updateEESetting, updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { password } from '../../../data/user'; +import { createUser, deleteUser, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; (IS_EE ? describe.skip : describe)('LIVECHAT - Departments[CE]', () => { @@ -919,4 +921,44 @@ import { IS_EE } from '../../../e2e/config/constants'; .expect(200); }); }); + + describe('With multiple bussines hours', () => { + before(async () => + Promise.all([updateEESetting('Livechat_enable_business_hours', true), updateEESetting('Livechat_business_hour_type', 'Multiple')]), + ); + after(async () => + Promise.all([updateEESetting('Livechat_enable_business_hours', false), updateEESetting('Livechat_business_hour_type', 'Single')]), + ); + + let testUser: { user: IUser; credentials: Credentials }; + let testDepartment: ILivechatDepartment; + before(async () => { + const user = await createUser(); + await createAgent(user.username); + const credentials3 = await login(user.username, password); + await makeAgentAvailable(credentials3); + + testUser = { + user, + credentials: credentials3, + }; + }); + + before(async () => { + testDepartment = await createDepartment([{ agentId: testUser.user._id }], `${new Date().toISOString()}-department`, true); + }); + + after(async () => { + await Promise.all([deleteUser(testUser.user), deleteDepartment(testDepartment._id)]); + }); + + it('should allow to remove an agent from a department when multiple business hours are enabled', async () => { + const res = await request + .post(api(`livechat/department/${testDepartment._id}/agents`)) + .set(credentials) + .send({ upsert: [], remove: [{ agentId: testUser.user._id, username: testUser.user.username }] }) + .expect(200); + expect(res.body).to.have.property('success', true); + }); + }); }); From 7416204529c9c1d8e272ad803656423cf6bdd473 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:12:09 -0300 Subject: [PATCH 096/187] fix: Quick Reactions missing after refresh (#35766) --- .changeset/tasty-pianos-bathe.md | 5 +++++ apps/meteor/app/emoji/client/helpers.ts | 4 +++- .../EmojiPickerProvider/EmojiPickerProvider.tsx | 11 ++++++----- 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 .changeset/tasty-pianos-bathe.md diff --git a/.changeset/tasty-pianos-bathe.md b/.changeset/tasty-pianos-bathe.md new file mode 100644 index 0000000000000..f247a9a24fd22 --- /dev/null +++ b/.changeset/tasty-pianos-bathe.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue where quick reactions (Feature Preview) would be absent on first access and after page refreshes diff --git a/apps/meteor/app/emoji/client/helpers.ts b/apps/meteor/app/emoji/client/helpers.ts index cbd1b08b687de..d36dc958cf20e 100644 --- a/apps/meteor/app/emoji/client/helpers.ts +++ b/apps/meteor/app/emoji/client/helpers.ts @@ -21,15 +21,17 @@ export const createEmojiListByCategorySubscription = ( actualTone: number, recentEmojis: string[], setRecentEmojis: (emojis: string[]) => void, + setQuickReactions: () => void, ): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ReturnType] => { let result: ReturnType = [[], []]; updateRecent(recentEmojis); const sub = (cb: () => void) => { result = createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis); - + setQuickReactions(); return emojiEmitter.on('updated', () => { result = createPickerEmojis(customItemsLimit, actualTone, recentEmojis, setRecentEmojis); + setQuickReactions(); cb(); }); }; diff --git a/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx index bf3207bbb8d1d..70ed30697649b 100644 --- a/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx +++ b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx @@ -1,4 +1,4 @@ -import { useDebouncedState, useLocalStorage } from '@rocket.chat/fuselage-hooks'; +import { useDebouncedState, useEffectEvent, useLocalStorage } from '@rocket.chat/fuselage-hooks'; import type { ReactNode, ReactElement, ContextType } from 'react'; import { useState, useCallback, useMemo, useSyncExternalStore } from 'react'; @@ -23,13 +23,14 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen const [customItemsLimit, setCustomItemsLimit] = useState(DEFAULT_ITEMS_LIMIT); - const [quickReactions, setQuickReactions] = useState<{ emoji: string; image: string }[]>(() => + const [quickReactions, _setQuickReactions] = useState<{ emoji: string; image: string }[]>(() => getFrequentEmoji(frequentEmojis.map(([emoji]) => emoji)), ); + const setQuickReactions = useEffectEvent(() => _setQuickReactions(getFrequentEmoji(frequentEmojis.map(([emoji]) => emoji)))); const [sub, getSnapshot] = useMemo(() => { - return createEmojiListByCategorySubscription(customItemsLimit, actualTone, recentEmojis, setRecentEmojis); - }, [customItemsLimit, actualTone, recentEmojis, setRecentEmojis]); + return createEmojiListByCategorySubscription(customItemsLimit, actualTone, recentEmojis, setRecentEmojis, setQuickReactions); + }, [customItemsLimit, actualTone, recentEmojis, setRecentEmojis, setQuickReactions]); const [emojiListByCategory, categoriesIndexes] = useSyncExternalStore(sub, getSnapshot); @@ -46,7 +47,7 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen .sort(([, frequentA], [, frequentB]) => frequentB - frequentA); setFrequentEmojis(sortedFrequent); - setQuickReactions(getFrequentEmoji(sortedFrequent.map(([emoji]) => emoji))); + _setQuickReactions(getFrequentEmoji(sortedFrequent.map(([emoji]) => emoji))); }, [frequentEmojis, setFrequentEmojis], ); From f51943ff3049b64ba9b7ab6d772f2d9ea326fd4e Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 10 Apr 2025 15:41:43 -0600 Subject: [PATCH 097/187] chore: Break down big `livechatTyped` file - 6 (#35764) --- .../app/apps/server/bridges/livechat.ts | 3 +- .../app/livechat/server/api/v1/customField.ts | 6 +- .../app/livechat/server/api/v1/visitor.ts | 2 +- .../app/livechat/server/hooks/sendToCRM.ts | 6 +- .../app/livechat/server/lib/LivechatTyped.ts | 246 +----------------- .../app/livechat/server/lib/Visitors.ts | 2 +- .../app/livechat/server/lib/custom-fields.ts | 50 ++++ apps/meteor/app/livechat/server/lib/guests.ts | 68 ++++- .../app/livechat/server/lib/localTypes.ts | 34 ++- .../app/livechat/server/lib/omni-users.ts | 24 ++ apps/meteor/app/livechat/server/lib/rooms.ts | 37 +++ apps/meteor/app/livechat/server/lib/utils.ts | 26 +- .../lib/validateRequiredCustomFields.ts | 16 -- .../server/methods/removeAllClosedRooms.ts | 4 +- .../app/livechat/server/methods/removeRoom.ts | 4 +- apps/meteor/app/livechat/server/startup.ts | 10 +- .../server/services/omnichannel/service.ts | 4 +- .../livechat/server/hooks/sendToCRM.tests.ts | 5 +- .../lib/validateRequiredCustomFields.spec.ts | 2 +- 19 files changed, 263 insertions(+), 286 deletions(-) create mode 100644 apps/meteor/app/livechat/server/lib/custom-fields.ts create mode 100644 apps/meteor/app/livechat/server/lib/omni-users.ts delete mode 100644 apps/meteor/app/livechat/server/lib/validateRequiredCustomFields.ts diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 35f31e6e57a86..73397b728f034 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -12,6 +12,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { deasyncPromise } from '../../../../server/deasync/deasync'; import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; import { closeRoom } from '../../../livechat/server/lib/closeRoom'; +import { setCustomFields } from '../../../livechat/server/lib/custom-fields'; import { getRoomMessages } from '../../../livechat/server/lib/getRoomMessages'; import { registerGuest } from '../../../livechat/server/lib/guests'; import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes'; @@ -369,6 +370,6 @@ export class AppLivechatBridge extends LivechatBridge { ): Promise { this.orch.debugLog(`The App ${appId} is setting livechat visitor's custom fields.`); - return LivechatTyped.setCustomFields(data); + return setCustomFields(data); } } diff --git a/apps/meteor/app/livechat/server/api/v1/customField.ts b/apps/meteor/app/livechat/server/api/v1/customField.ts index 80fe83bf55096..a4de976407871 100644 --- a/apps/meteor/app/livechat/server/api/v1/customField.ts +++ b/apps/meteor/app/livechat/server/api/v1/customField.ts @@ -2,7 +2,7 @@ import { isLivechatCustomFieldsProps, isPOSTLivechatCustomFieldParams, isPOSTLiv import { API } from '../../../../api/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; -import { Livechat } from '../../lib/LivechatTyped'; +import { setCustomFields } from '../../lib/custom-fields'; import { findLivechatCustomFields, findCustomFieldById } from '../lib/customFields'; import { findGuest } from '../lib/livechat'; @@ -18,7 +18,7 @@ API.v1.addRoute( throw new Error('invalid-token'); } - if (!(await Livechat.setCustomFields({ token, key, value, overwrite }))) { + if (!(await setCustomFields({ token, key, value, overwrite }))) { return API.v1.failure(); } @@ -46,7 +46,7 @@ API.v1.addRoute( overwrite: boolean; }): Promise<{ Key: string; value: string; overwrite: boolean }> => { const data = Object.assign({ token }, customField); - if (!(await Livechat.setCustomFields(data))) { + if (!(await setCustomFields(data))) { throw new Error('error-setting-custom-field'); } diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index cddcd8a5ca02d..19f3aca1fc535 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -7,9 +7,9 @@ import { callbacks } from '../../../../../lib/callbacks'; import { API } from '../../../../api/server'; import { settings } from '../../../../settings/server'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; +import { validateRequiredCustomFields } from '../../lib/custom-fields'; import { registerGuest, removeGuest } from '../../lib/guests'; import { saveRoomInfo } from '../../lib/rooms'; -import { validateRequiredCustomFields } from '../../lib/validateRequiredCustomFields'; import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; API.v1.addRoute( diff --git a/apps/meteor/app/livechat/server/hooks/sendToCRM.ts b/apps/meteor/app/livechat/server/hooks/sendToCRM.ts index 4189f84cbfd8a..f3b6dbccbf937 100644 --- a/apps/meteor/app/livechat/server/hooks/sendToCRM.ts +++ b/apps/meteor/app/livechat/server/hooks/sendToCRM.ts @@ -5,7 +5,7 @@ import { LivechatRooms, Messages } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { settings } from '../../../settings/server'; import { normalizeMessageFileUpload } from '../../../utils/server/functions/normalizeMessageFileUpload'; -import { Livechat as LivechatTyped } from '../lib/LivechatTyped'; +import { getLivechatRoomGuestInfo } from '../lib/guests'; import { sendRequest } from '../lib/webhooks'; type AdditionalFields = @@ -97,11 +97,11 @@ async function sendToCRM( return room; } - const postData: Awaited> & { + const postData: Awaited> & { type: string; messages: IOmnichannelSystemMessage[]; } = { - ...(await LivechatTyped.getLivechatRoomGuestInfo(room)), + ...(await getLivechatRoomGuestInfo(room)), type, messages: [], }; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 95f03ee12c173..5f7c7c9dcd58c 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,76 +1,22 @@ -import { VideoConf, api } from '@rocket.chat/core-services'; -import type { - IOmnichannelRoom, - IUser, - ILivechatVisitor, - ILivechatAgent, - ILivechatDepartment, - IOmnichannelAgent, - UserStatus, -} from '@rocket.chat/core-typings'; +import { VideoConf } from '@rocket.chat/core-services'; +import type { IUser, ILivechatVisitor, ILivechatDepartment, UserStatus } from '@rocket.chat/core-typings'; import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; -import { - LivechatDepartment, - LivechatInquiry, - LivechatRooms, - Subscriptions, - LivechatVisitors, - Messages, - Users, - LivechatDepartmentAgents, - ReadReceipts, - Rooms, - LivechatCustomField, -} from '@rocket.chat/models'; +import { LivechatDepartment, LivechatInquiry, LivechatRooms, Users, LivechatDepartmentAgents, Rooms } from '@rocket.chat/models'; import { removeEmpty } from '@rocket.chat/tools'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import type { Filter } from 'mongodb'; -import UAParser from 'ua-parser-js'; -import { callbacks } from '../../../../lib/callbacks'; -import { i18n } from '../../../../server/lib/i18n'; import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; -import { canAccessRoomAsync } from '../../../authorization/server'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; -import { - notifyOnLivechatInquiryChanged, - notifyOnLivechatInquiryChangedByToken, - notifyOnUserChange, - notifyOnSubscriptionChanged, -} from '../../../lib/server/lib/notifyListener'; +import { notifyOnLivechatInquiryChangedByToken } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; -import { parseAgentCustomFields, updateDepartmentAgents } from './Helper'; +import { updateDepartmentAgents } from './Helper'; import { afterAgentAdded, afterRemoveAgent } from './hooks'; -type AKeyOf = { - [K in keyof T]?: T[K]; -}; - -type ICRMData = { - _id: string; - label?: string; - topic?: string; - createdAt: Date; - lastMessageAt?: Date; - tags?: string[]; - customFields?: IOmnichannelRoom['livechatData']; - visitor: Pick & { - email?: ILivechatVisitor['visitorEmails']; - os?: string; - browser?: string; - customFields: ILivechatVisitor['livechatData']; - }; - agent?: Pick & { - email?: NonNullable[number]['address']; - }; - crmData?: IOmnichannelRoom['crmData']; -}; - class LivechatClass { logger: Logger; @@ -126,40 +72,6 @@ class LivechatClass { return Users.checkOnlineAgents(undefined, settings.get('Livechat_enabled_when_agent_idle')); } - async removeRoom(rid: string) { - Livechat.logger.debug(`Deleting room ${rid}`); - check(rid, String); - const room = await LivechatRooms.findOneById(rid); - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room'); - } - - const inquiry = await LivechatInquiry.findOneByRoomId(rid); - - const result = await Promise.allSettled([ - Messages.removeByRoomId(rid), - ReadReceipts.removeByRoomId(rid), - Subscriptions.removeByRoomId(rid, { - async onTrash(doc) { - void notifyOnSubscriptionChanged(doc, 'removed'); - }, - }), - LivechatInquiry.removeByRoomId(rid), - LivechatRooms.removeById(rid), - ]); - - if (result[3]?.status === 'fulfilled' && result[3].value?.deletedCount && inquiry) { - void notifyOnLivechatInquiryChanged(inquiry, 'removed'); - } - - for (const r of result) { - if (r.status === 'rejected') { - this.logger.error(`Error removing room ${rid}: ${r.reason}`); - throw new Meteor.Error('error-removing-room', 'Error removing room'); - } - } - } - private async getBotAgents(department?: string) { if (department) { return LivechatDepartmentAgents.getBotsForDepartment(department); @@ -228,93 +140,6 @@ class LivechatClass { } } - notifyRoomVisitorChange(roomId: string, visitor: ILivechatVisitor) { - void api.broadcast('omnichannel.room', roomId, { - type: 'visitorData', - visitor, - }); - } - - async changeRoomVisitor(userId: string, room: IOmnichannelRoom, visitor: ILivechatVisitor) { - const user = await Users.findOneById(userId, { projection: { _id: 1 } }); - if (!user) { - throw new Error('error-user-not-found'); - } - - if (!(await canAccessRoomAsync(room, user))) { - throw new Error('error-not-allowed'); - } - - await LivechatRooms.changeVisitorByRoomId(room._id, visitor); - - this.notifyRoomVisitorChange(room._id, visitor); - - return LivechatRooms.findOneById(room._id); - } - - async notifyAgentStatusChanged(userId: string, status?: UserStatus) { - if (!status) { - return; - } - - void callbacks.runAsync('livechat.agentStatusChanged', { userId, status }); - if (!settings.get('Livechat_show_agent_info')) { - return; - } - - await LivechatRooms.findOpenByAgent(userId).forEach((room) => { - void api.broadcast('omnichannel.room', room._id, { - type: 'agentStatus', - status, - }); - }); - } - - async setUserStatusLivechatIf(userId: string, status: ILivechatAgentStatus, condition?: Filter, fields?: AKeyOf) { - const result = await Users.setLivechatStatusIf(userId, status, condition, fields); - - if (result.modifiedCount > 0) { - void notifyOnUserChange({ - id: userId, - clientAction: 'updated', - diff: { ...fields, statusLivechat: status }, - }); - } - - callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); - return result; - } - - async setCustomFields({ token, key, value, overwrite }: { key: string; value: string; overwrite: boolean; token: string }) { - Livechat.logger.debug(`Setting custom fields data for visitor with token ${token}`); - - const customField = await LivechatCustomField.findOneById(key); - if (!customField) { - throw new Error('invalid-custom-field'); - } - - if (customField.regexp !== undefined && customField.regexp !== '') { - const regexp = new RegExp(customField.regexp); - if (!regexp.test(value)) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: key })); - } - } - - let result; - if (customField.scope === 'room') { - result = await LivechatRooms.updateDataByToken(token, key, value, overwrite); - } else { - result = await LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite); - } - - if (typeof result === 'boolean') { - // Note: this only happens when !overwrite is passed, in this case we don't do any db update - return 0; - } - - return result.modifiedCount; - } - async removeAgent(username: string) { const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); @@ -342,67 +167,6 @@ class LivechatClass { return removeUserFromRolesAsync(user._id, ['livechat-manager']); } - async getLivechatRoomGuestInfo(room: IOmnichannelRoom) { - const visitor = await LivechatVisitors.findOneEnabledById(room.v._id); - if (!visitor) { - throw new Error('error-invalid-visitor'); - } - - const agent = room.servedBy?._id ? await Users.findOneById(room.servedBy?._id) : null; - - const ua = new UAParser(); - ua.setUA(visitor.userAgent || ''); - - const postData: ICRMData = { - _id: room._id, - label: room.fname || room.label, // using same field for compatibility - topic: room.topic, - createdAt: room.ts, - lastMessageAt: room.lm, - tags: room.tags, - customFields: room.livechatData, - visitor: { - _id: visitor._id, - token: visitor.token, - name: visitor.name, - username: visitor.username, - department: visitor.department, - ip: visitor.ip, - os: ua.getOS().name && `${ua.getOS().name} ${ua.getOS().version}`, - browser: ua.getBrowser().name && `${ua.getBrowser().name} ${ua.getBrowser().version}`, - customFields: visitor.livechatData, - }, - }; - - if (agent) { - const customFields = parseAgentCustomFields(agent.customFields); - - postData.agent = { - _id: agent._id, - username: agent.username, - name: agent.name, - ...(customFields && { customFields }), - }; - - if (agent.emails && agent.emails.length > 0) { - postData.agent.email = agent.emails[0].address; - } - } - - if (room.crmData) { - postData.crmData = room.crmData; - } - - if (visitor.visitorEmails && visitor.visitorEmails.length > 0) { - postData.visitor.email = visitor.visitorEmails; - } - if (visitor.phone && visitor.phone.length > 0) { - postData.visitor.phone = visitor.phone; - } - - return postData; - } - async allowAgentChangeServiceStatus(statusLivechat: ILivechatAgentStatus, agentId: string) { if (statusLivechat !== ILivechatAgentStatus.AVAILABLE) { return true; diff --git a/apps/meteor/app/livechat/server/lib/Visitors.ts b/apps/meteor/app/livechat/server/lib/Visitors.ts index 83e37e76d12a0..6ff046b3fc211 100644 --- a/apps/meteor/app/livechat/server/lib/Visitors.ts +++ b/apps/meteor/app/livechat/server/lib/Visitors.ts @@ -64,7 +64,7 @@ export const Visitors = { const agent = await Users.findOneOnlineAgentById(contact.contactManager, shouldConsiderIdleAgent, { projection: { _id: 1, username: 1, name: 1, emails: 1 }, }); - if (agent && agent.username && agent.name && agent.emails) { + if (agent?.username && agent.name && agent.emails) { visitorDataToUpdate.contactManager = { _id: agent._id, username: agent.username, diff --git a/apps/meteor/app/livechat/server/lib/custom-fields.ts b/apps/meteor/app/livechat/server/lib/custom-fields.ts new file mode 100644 index 0000000000000..c52781c3c4602 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/custom-fields.ts @@ -0,0 +1,50 @@ +import type { ILivechatCustomField } from '@rocket.chat/core-typings'; +import { LivechatCustomField, LivechatRooms, LivechatVisitors } from '@rocket.chat/models'; + +import { livechatLogger } from './logger'; +import { i18n } from '../../../utils/lib/i18n'; + +export const validateRequiredCustomFields = (customFields: string[], livechatCustomFields: ILivechatCustomField[]) => { + const errors: string[] = []; + const requiredCustomFields = livechatCustomFields.filter((field) => field.required); + + requiredCustomFields.forEach((field) => { + if (!customFields.find((f) => f === field._id)) { + errors.push(field._id); + } + }); + + if (errors.length > 0) { + throw new Error(`Missing required custom fields: ${errors.join(', ')}`); + } +}; + +export async function setCustomFields({ token, key, value, overwrite }: { key: string; value: string; overwrite: boolean; token: string }) { + livechatLogger.debug(`Setting custom fields data for visitor with token ${token}`); + + const customField = await LivechatCustomField.findOneById(key); + if (!customField) { + throw new Error('invalid-custom-field'); + } + + if (customField.regexp !== undefined && customField.regexp !== '') { + const regexp = new RegExp(customField.regexp); + if (!regexp.test(value)) { + throw new Error(i18n.t('error-invalid-custom-field-value', { field: key })); + } + } + + let result; + if (customField.scope === 'room') { + result = await LivechatRooms.updateDataByToken(token, key, value, overwrite); + } else { + result = await LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite); + } + + if (typeof result === 'boolean') { + // Note: this only happens when !overwrite is passed, in this case we don't do any db update + return 0; + } + + return result.modifiedCount; +} diff --git a/apps/meteor/app/livechat/server/lib/guests.ts b/apps/meteor/app/livechat/server/lib/guests.ts index e32c6c9c49372..1ab2a75f86a0a 100644 --- a/apps/meteor/app/livechat/server/lib/guests.ts +++ b/apps/meteor/app/livechat/server/lib/guests.ts @@ -1,5 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import type { ILivechatVisitor } from '@rocket.chat/core-typings'; +import type { ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { LivechatVisitors, LivechatCustomField, @@ -9,13 +9,16 @@ import { ReadReceipts, Subscriptions, LivechatContacts, + Users, } from '@rocket.chat/models'; import { wrapExceptions } from '@rocket.chat/tools'; +import UAParser from 'ua-parser-js'; -import { validateEmail } from './Helper'; +import { parseAgentCustomFields, validateEmail } from './Helper'; import type { RegisterGuestType } from './Visitors'; import { Visitors } from './Visitors'; import { ContactMerger, type FieldAndValue } from './contacts/ContactMerger'; +import type { ICRMData } from './localTypes'; import { livechatLogger } from './logger'; import { trim } from '../../../../lib/utils/stringUtils'; import { i18n } from '../../../../server/lib/i18n'; @@ -160,3 +163,64 @@ async function cleanGuestHistory(token: string) { await LivechatInquiry.removeByIds(livechatInquiries.map(({ _id }) => _id)); void notifyOnLivechatInquiryChanged(livechatInquiries, 'removed'); } + +export async function getLivechatRoomGuestInfo(room: IOmnichannelRoom) { + const visitor = await LivechatVisitors.findOneEnabledById(room.v._id); + if (!visitor) { + throw new Error('error-invalid-visitor'); + } + + const agent = room.servedBy?._id ? await Users.findOneById(room.servedBy?._id) : null; + + const ua = new UAParser(); + ua.setUA(visitor.userAgent || ''); + + const postData: ICRMData = { + _id: room._id, + label: room.fname || room.label, // using same field for compatibility + topic: room.topic, + createdAt: room.ts, + lastMessageAt: room.lm, + tags: room.tags, + customFields: room.livechatData, + visitor: { + _id: visitor._id, + token: visitor.token, + name: visitor.name, + username: visitor.username, + department: visitor.department, + ip: visitor.ip, + os: ua.getOS().name && `${ua.getOS().name} ${ua.getOS().version}`, + browser: ua.getBrowser().name && `${ua.getBrowser().name} ${ua.getBrowser().version}`, + customFields: visitor.livechatData, + }, + }; + + if (agent) { + const customFields = parseAgentCustomFields(agent.customFields); + + postData.agent = { + _id: agent._id, + username: agent.username, + name: agent.name, + ...(customFields && { customFields }), + }; + + if (agent.emails && agent.emails.length > 0) { + postData.agent.email = agent.emails[0].address; + } + } + + if (room.crmData) { + postData.crmData = room.crmData; + } + + if (visitor.visitorEmails && visitor.visitorEmails.length > 0) { + postData.visitor.email = visitor.visitorEmails; + } + if (visitor.phone && visitor.phone.length > 0) { + postData.visitor.phone = visitor.phone; + } + + return postData; +} diff --git a/apps/meteor/app/livechat/server/lib/localTypes.ts b/apps/meteor/app/livechat/server/lib/localTypes.ts index b82ad13b05313..2676443e6e5a2 100644 --- a/apps/meteor/app/livechat/server/lib/localTypes.ts +++ b/apps/meteor/app/livechat/server/lib/localTypes.ts @@ -1,4 +1,12 @@ -import type { IOmnichannelRoom, IUser, ILivechatVisitor, IMessage, MessageAttachment, IMessageInbox } from '@rocket.chat/core-typings'; +import type { + IOmnichannelRoom, + IUser, + ILivechatVisitor, + IMessage, + MessageAttachment, + IMessageInbox, + IOmnichannelAgent, +} from '@rocket.chat/core-typings'; type GenericCloseRoomParams = { room: IOmnichannelRoom; @@ -54,3 +62,27 @@ export interface ILivechatMessage { blocks?: IMessage['blocks']; email?: IMessageInbox['email']; } + +export type ICRMData = { + _id: string; + label?: string; + topic?: string; + createdAt: Date; + lastMessageAt?: Date; + tags?: string[]; + customFields?: IOmnichannelRoom['livechatData']; + visitor: Pick & { + email?: ILivechatVisitor['visitorEmails']; + os?: string; + browser?: string; + customFields: ILivechatVisitor['livechatData']; + }; + agent?: Pick & { + email?: NonNullable[number]['address']; + }; + crmData?: IOmnichannelRoom['crmData']; +}; + +export type AKeyOf = { + [K in keyof T]?: T[K]; +}; diff --git a/apps/meteor/app/livechat/server/lib/omni-users.ts b/apps/meteor/app/livechat/server/lib/omni-users.ts new file mode 100644 index 0000000000000..c96bc84d3825f --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/omni-users.ts @@ -0,0 +1,24 @@ +import { api } from '@rocket.chat/core-services'; +import type { UserStatus } from '@rocket.chat/core-typings'; +import { LivechatRooms } from '@rocket.chat/models'; + +import { callbacks } from '../../../../lib/callbacks'; +import { settings } from '../../../settings/server'; + +export async function notifyAgentStatusChanged(userId: string, status?: UserStatus) { + if (!status) { + return; + } + + void callbacks.runAsync('livechat.agentStatusChanged', { userId, status }); + if (!settings.get('Livechat_show_agent_info')) { + return; + } + + await LivechatRooms.findOpenByAgent(userId).forEach((room) => { + void api.broadcast('omnichannel.room', room._id, { + type: 'agentStatus', + status, + }); + }); +} diff --git a/apps/meteor/app/livechat/server/lib/rooms.ts b/apps/meteor/app/livechat/server/lib/rooms.ts index f6609fe1fc2b8..ee231f899b875 100644 --- a/apps/meteor/app/livechat/server/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/lib/rooms.ts @@ -16,6 +16,7 @@ import { Rooms, Subscriptions, Users, + ReadReceipts, } from '@rocket.chat/models'; import { normalizeTransferredByData } from './Helper'; @@ -32,6 +33,8 @@ import { notifyOnLivechatInquiryChangedByRoom, notifyOnSubscriptionChangedByRoomId, notifyOnRoomChangedById, + notifyOnLivechatInquiryChanged, + notifyOnSubscriptionChanged, } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { i18n } from '../../../utils/lib/i18n'; @@ -247,3 +250,37 @@ export async function returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: return true; } + +export async function removeOmnichannelRoom(rid: string) { + livechatLogger.debug(`Deleting room ${rid}`); + check(rid, String); + const room = await LivechatRooms.findOneById(rid); + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room'); + } + + const inquiry = await LivechatInquiry.findOneByRoomId(rid); + + const result = await Promise.allSettled([ + Messages.removeByRoomId(rid), + ReadReceipts.removeByRoomId(rid), + Subscriptions.removeByRoomId(rid, { + async onTrash(doc) { + void notifyOnSubscriptionChanged(doc, 'removed'); + }, + }), + LivechatInquiry.removeByRoomId(rid), + LivechatRooms.removeById(rid), + ]); + + if (result[3]?.status === 'fulfilled' && result[3].value?.deletedCount && inquiry) { + void notifyOnLivechatInquiryChanged(inquiry, 'removed'); + } + + for (const r of result) { + if (r.status === 'rejected') { + livechatLogger.error(`Error removing room ${rid}: ${r.reason}`); + throw new Meteor.Error('error-removing-room', 'Error removing room'); + } + } +} diff --git a/apps/meteor/app/livechat/server/lib/utils.ts b/apps/meteor/app/livechat/server/lib/utils.ts index ad1eb0a793033..a8399b8ecf681 100644 --- a/apps/meteor/app/livechat/server/lib/utils.ts +++ b/apps/meteor/app/livechat/server/lib/utils.ts @@ -1,7 +1,9 @@ -import type { ILivechatAgentStatus } from '@rocket.chat/core-typings'; +import type { ILivechatAgent, ILivechatAgentStatus, IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; +import type { Filter } from 'mongodb'; import { RoutingManager } from './RoutingManager'; +import type { AKeyOf } from './localTypes'; import { callbacks } from '../../../../lib/callbacks'; import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; @@ -11,6 +13,7 @@ export function showConnecting() { export async function setUserStatusLivechat(userId: string, status: ILivechatAgentStatus) { const user = await Users.setLivechatStatus(userId, status); + // TODO: shouldnt this callback run if the modified count is > 0 too? callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); if (user.modifiedCount > 0) { @@ -26,3 +29,24 @@ export async function setUserStatusLivechat(userId: string, status: ILivechatAge return user; } + +export async function setUserStatusLivechatIf( + userId: string, + status: ILivechatAgentStatus, + condition?: Filter, + fields?: AKeyOf, +) { + const result = await Users.setLivechatStatusIf(userId, status, condition, fields); + + if (result.modifiedCount > 0) { + void notifyOnUserChange({ + id: userId, + clientAction: 'updated', + diff: { ...fields, statusLivechat: status }, + }); + } + + // TODO: shouldnt this callback run if the modified count is > 0 too? + callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); + return result; +} diff --git a/apps/meteor/app/livechat/server/lib/validateRequiredCustomFields.ts b/apps/meteor/app/livechat/server/lib/validateRequiredCustomFields.ts deleted file mode 100644 index 008a4b13c7824..0000000000000 --- a/apps/meteor/app/livechat/server/lib/validateRequiredCustomFields.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ILivechatCustomField } from '@rocket.chat/core-typings'; - -export const validateRequiredCustomFields = (customFields: string[], livechatCustomFields: ILivechatCustomField[]) => { - const errors: string[] = []; - const requiredCustomFields = livechatCustomFields.filter((field) => field.required); - - requiredCustomFields.forEach((field) => { - if (!customFields.find((f) => f === field._id)) { - errors.push(field._id); - } - }); - - if (errors.length > 0) { - throw new Error(`Missing required custom fields: ${errors.join(', ')}`); - } -}; diff --git a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts index ba3939bb8573e..e154667c3c188 100644 --- a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts +++ b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts @@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../lib/callbacks'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Livechat } from '../lib/LivechatTyped'; +import { removeOmnichannelRoom } from '../lib/rooms'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -32,7 +32,7 @@ Meteor.methods({ const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); const promises: Promise[] = []; await LivechatRooms.findClosedRooms(departmentIds, {}, extraQuery).forEach(({ _id }: IOmnichannelRoom) => { - promises.push(Livechat.removeRoom(_id)); + promises.push(removeOmnichannelRoom(_id)); }); await Promise.all(promises); diff --git a/apps/meteor/app/livechat/server/methods/removeRoom.ts b/apps/meteor/app/livechat/server/methods/removeRoom.ts index 751d51d4f019f..7d659a15985e1 100644 --- a/apps/meteor/app/livechat/server/methods/removeRoom.ts +++ b/apps/meteor/app/livechat/server/methods/removeRoom.ts @@ -4,7 +4,7 @@ import { LivechatRooms } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Livechat } from '../lib/LivechatTyped'; +import { removeOmnichannelRoom } from '../lib/rooms'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -40,6 +40,6 @@ Meteor.methods({ }); } - await Livechat.removeRoom(rid); + await removeOmnichannelRoom(rid); }, }); diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index b41fc425bf066..1df1245655bab 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -7,7 +7,7 @@ import { Meteor } from 'meteor/meteor'; import { businessHourManager } from './business-hour'; import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper'; -import { Livechat as LivechatTyped } from './lib/LivechatTyped'; +import { setUserStatusLivechatIf } from './lib/utils'; import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor'; import { callbacks } from '../../../lib/callbacks'; import { beforeLeaveRoomCallback } from '../../../lib/callbacks/beforeLeaveRoomCallback'; @@ -88,13 +88,9 @@ Meteor.startup(async () => { return; } - void LivechatTyped.setUserStatusLivechatIf( - user._id, - ILivechatAgentStatus.NOT_AVAILABLE, - {}, - { livechatStatusSystemModified: true }, - ).catch(); + void setUserStatusLivechatIf(user._id, ILivechatAgentStatus.NOT_AVAILABLE, {}, { livechatStatusSystemModified: true }).catch(); + // TODO: Shouldn't this notifier be the same as the one inside setUserStatusLivechatIf? void notifyOnUserChange({ id: user._id, clientAction: 'updated', diff --git a/apps/meteor/server/services/omnichannel/service.ts b/apps/meteor/server/services/omnichannel/service.ts index e5b21f4aae97b..239a759aac5c4 100644 --- a/apps/meteor/server/services/omnichannel/service.ts +++ b/apps/meteor/server/services/omnichannel/service.ts @@ -5,8 +5,8 @@ import { License } from '@rocket.chat/license'; import moment from 'moment'; import { OmnichannelQueue } from './queue'; -import { Livechat } from '../../../app/livechat/server/lib/LivechatTyped'; import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager'; +import { notifyAgentStatusChanged } from '../../../app/livechat/server/lib/omni-users'; import { settings } from '../../../app/settings/server'; export class OmnichannelService extends ServiceClassInternal implements IOmnichannelService { @@ -27,7 +27,7 @@ export class OmnichannelService extends ServiceClassInternal implements IOmnicha const hasRole = user.roles.some((role) => ['livechat-manager', 'livechat-monitor', 'livechat-agent'].includes(role)); if (hasRole) { // TODO change `Livechat.notifyAgentStatusChanged` to a service call - await Livechat.notifyAgentStatusChanged(user._id, user.status); + await notifyAgentStatusChanged(user._id, user.status); } }); } diff --git a/apps/meteor/tests/unit/app/livechat/server/hooks/sendToCRM.tests.ts b/apps/meteor/tests/unit/app/livechat/server/hooks/sendToCRM.tests.ts index 70fbc42bc77dd..79651cea1a52c 100644 --- a/apps/meteor/tests/unit/app/livechat/server/hooks/sendToCRM.tests.ts +++ b/apps/meteor/tests/unit/app/livechat/server/hooks/sendToCRM.tests.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import p from 'proxyquire'; +import sinon from 'sinon'; const resultObj = { result: true, @@ -16,10 +17,10 @@ const { sendMessageType, isOmnichannelNavigationMessage, isOmnichannelClosingMes }, }, '../../../utils/server/functions/normalizeMessageFileUpload': { - normalizeMessageFileUpload: (data: any) => data, + normalizeMessageFileUpload: sinon.stub().returnsArg(0), }, '../lib/webhooks': {}, - '../lib/LivechatTyped': { Livechat: {} }, + '../lib/guests': { getLivechatRoomGuestInfo: sinon.stub() }, }); describe('[OC] Send TO CRM', () => { diff --git a/apps/meteor/tests/unit/app/livechat/server/lib/validateRequiredCustomFields.spec.ts b/apps/meteor/tests/unit/app/livechat/server/lib/validateRequiredCustomFields.spec.ts index a8ad3f083812e..1009c2b76b929 100644 --- a/apps/meteor/tests/unit/app/livechat/server/lib/validateRequiredCustomFields.spec.ts +++ b/apps/meteor/tests/unit/app/livechat/server/lib/validateRequiredCustomFields.spec.ts @@ -1,7 +1,7 @@ import type { ILivechatCustomField } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { validateRequiredCustomFields } from '../../../../../../app/livechat/server/lib/validateRequiredCustomFields'; +import { validateRequiredCustomFields } from '../../../../../../app/livechat/server/lib/custom-fields'; describe('validateRequiredCustomFields', () => { it('should throw an error if the required custom fields are not provided', async () => { From 49843c821906dab88d336c61d15f60fe63c7a70e Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 10 Apr 2025 18:42:26 -0300 Subject: [PATCH 098/187] regression: fix apps logs expire as `undefined` (#35774) --- packages/models/src/models/AppLogsModel.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/models/src/models/AppLogsModel.ts b/packages/models/src/models/AppLogsModel.ts index de7301e24743f..4be9cc9e4dcbc 100644 --- a/packages/models/src/models/AppLogsModel.ts +++ b/packages/models/src/models/AppLogsModel.ts @@ -4,10 +4,7 @@ import type { Db, DeleteResult, Filter } from 'mongodb'; import { BaseRaw } from './BaseRaw'; export class AppsLogsModel extends BaseRaw implements IAppLogsModel { - constructor( - db: Db, - private readonly expireAfterSeconds: number = 60 * 60 * 24 * 30, - ) { + constructor(db: Db) { super(db, 'apps_logs', undefined); } @@ -17,7 +14,7 @@ export class AppsLogsModel extends BaseRaw implements IAppLogsModel { key: { _updatedAt: 1, }, - expireAfterSeconds: this.expireAfterSeconds, + expireAfterSeconds: 60 * 60 * 24 * 30, }, ]; } From ca6870a493ee52c397e51701a5952b5528724f22 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 10 Apr 2025 19:42:39 -0300 Subject: [PATCH 099/187] fix: GUI crash when editing canned response with tags via contextual bar (#35679) --- .changeset/good-bobcats-argue.md | 5 ++ .../client/components/Omnichannel/Tags.tsx | 7 ++- .../additionalForms/CurrentChatTags.tsx | 4 +- .../CreateCannedResponseModal.tsx | 32 ++++-------- .../tags/AutoCompleteTagsMultiple.tsx | 3 ++ ...nichannel-canned-responses-sidebar.spec.ts | 52 +++++++++++++++---- .../fragments/home-omnichannel-content.ts | 6 +++ .../omnichannel-canned-responses.ts | 45 +++++++++++++++- 8 files changed, 119 insertions(+), 35 deletions(-) create mode 100644 .changeset/good-bobcats-argue.md diff --git a/.changeset/good-bobcats-argue.md b/.changeset/good-bobcats-argue.md new file mode 100644 index 0000000000000..e4603f8e99b0c --- /dev/null +++ b/.changeset/good-bobcats-argue.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes a GUI crash when editing a canned response with tags via room contextual bar. diff --git a/apps/meteor/client/components/Omnichannel/Tags.tsx b/apps/meteor/client/components/Omnichannel/Tags.tsx index 9a2a86500633e..15678390b323f 100644 --- a/apps/meteor/client/components/Omnichannel/Tags.tsx +++ b/apps/meteor/client/components/Omnichannel/Tags.tsx @@ -2,7 +2,7 @@ import { TextInput, Chip, Button, FieldLabel, FieldRow } from '@rocket.chat/fuse import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import type { ChangeEvent, ReactElement } from 'react'; -import { useMemo, useState } from 'react'; +import { useId, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FormSkeleton } from './Skeleton'; @@ -19,6 +19,7 @@ type TagsProps = { const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps): ReactElement => { const { t } = useTranslation(); + const tagsFieldId = useId(); const { data: tagsResult, isLoading } = useLivechatTags({ department, @@ -66,13 +67,14 @@ const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps) return ( <> - + {t('Tags')} {tagsResult?.tags && tagsResult?.tags.length ? ( { handler(tags.map((tag) => tag.label)); @@ -87,6 +89,7 @@ const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps) ): void => handleTagValue(currentTarget.value)} flexGrow={1} placeholder={t('Enter_a_tag')} diff --git a/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx b/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx index 08bb02b8fb0a2..b4c2dde91758a 100644 --- a/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx +++ b/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx @@ -2,13 +2,14 @@ import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; import AutoCompleteTagsMultiple from '../tags/AutoCompleteTagsMultiple'; type CurrentChatTagsProps = { + id?: string; value: Array<{ value: string; label: string }>; handler: (value: { label: string; value: string }[]) => void; department?: string; viewAll?: boolean; }; -const CurrentChatTags = ({ value, handler, department, viewAll }: CurrentChatTagsProps) => { +const CurrentChatTags = ({ id, value, handler, department, viewAll }: CurrentChatTagsProps) => { const hasLicense = useHasLicenseModule('livechat-enterprise'); if (!hasLicense) { @@ -17,6 +18,7 @@ const CurrentChatTags = ({ value, handler, department, viewAll }: CurrentChatTag return ( ({ _id: cannedResponseData?._id || '', shortcut: cannedResponseData?.shortcut || '', text: cannedResponseData?.text || '', - tags: - cannedResponseData?.tags && Array.isArray(cannedResponseData.tags) - ? cannedResponseData.tags.map((tag: string) => ({ label: tag, value: tag })) - : [], + tags: cannedResponseData?.tags || [], scope: cannedResponseData?.scope || 'user', departmentId: cannedResponseData?.departmentId || '', }); @@ -44,7 +32,7 @@ const CreateCannedResponseModal = ({ cannedResponseData, onClose, reloadCannedLi const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const methods = useForm({ defaultValues: getInitialData(cannedResponseData) }); + const methods = useForm({ defaultValues: getInitialData(cannedResponseData) }); const { handleSubmit, formState: { isDirty }, @@ -53,7 +41,7 @@ const CreateCannedResponseModal = ({ cannedResponseData, onClose, reloadCannedLi const saveCannedResponse = useEndpoint('POST', '/v1/canned-responses'); const handleCreate = useCallback( - async ({ departmentId, ...data }: CreateCannedResponseModalFormData) => { + async ({ departmentId, ...data }: CannedResponseEditFormData) => { try { await saveCannedResponse({ ...data, @@ -83,9 +71,11 @@ const CreateCannedResponseModal = ({ cannedResponseData, onClose, reloadCannedLi title={cannedResponseData?._id ? t('Edit_Canned_Response') : t('Create_canned_response')} wrapperFunction={(props) => } > - - - + }> + + + + ); }; diff --git a/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx b/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx index d80cc2b913fb1..6a980e6de6bfb 100644 --- a/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx +++ b/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx @@ -9,6 +9,7 @@ import { AsyncStatePhase } from '../../hooks/useAsyncState'; import { useTagsList } from '../../hooks/useTagsList'; type AutoCompleteTagsMultipleProps = { + id?: string; value?: PaginatedMultiSelectOption[]; onlyMyTags?: boolean; onChange?: (value: PaginatedMultiSelectOption[]) => void; @@ -17,6 +18,7 @@ type AutoCompleteTagsMultipleProps = { }; const AutoCompleteTagsMultiple = ({ + id, value = [], onlyMyTags = false, onChange = () => undefined, @@ -44,6 +46,7 @@ const AutoCompleteTagsMultiple = ({ return ( { +test.describe.serial('OC - Canned Responses Sidebar', () => { test.skip(!IS_EE, 'Enterprise Only'); let poLiveChat: OmnichannelLiveChat; let newVisitor: { email: string; name: string }; - let agent: { page: Page; poHomeChannel: HomeChannel }; + let agent: { page: Page; poHomeChannel: HomeOmnichannel }; + + const cannedResponseName = faker.string.uuid(); test.beforeAll(async ({ api, browser }) => { newVisitor = createFakeVisitor(); @@ -23,34 +26,65 @@ test.describe('Omnichannel Canned Responses Sidebar', () => { await api.post('/livechat/users/manager', { username: 'user1' }); const { page } = await createAuxContext(browser, Users.user1); - agent = { page, poHomeChannel: new HomeChannel(page) }; + agent = { page, poHomeChannel: new HomeOmnichannel(page) }; }); + test.beforeEach(async ({ page, api }) => { poLiveChat = new OmnichannelLiveChat(page, api); }); + test.afterAll('close livechat conversation', async () => { + await agent.poHomeChannel.content.closeChat(); + }); + test.afterAll(async ({ api }) => { await api.delete('/livechat/users/agent/user1'); await api.delete('/livechat/users/manager/user1'); + await poLiveChat.page.close(); await agent.page.close(); }); - test('Receiving a message from visitor', async ({ page }) => { - await test.step('Expect send a message as a visitor', async () => { + test('OC - Canned Responses Sidebar - Create', async ({ page }) => { + await test.step('expect send a message as a visitor', async () => { await page.goto('/livechat'); await poLiveChat.openLiveChat(); await poLiveChat.sendMessage(newVisitor, false); - await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); + await poLiveChat.onlineAgentMessage.fill('this_a_test_message_from_visitor'); await poLiveChat.btnSendMessageToOnlineAgent.click(); }); - await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { + await test.step('expect to have 1 omnichannel assigned to agent 1', async () => { await agent.poHomeChannel.sidenav.openChat(newVisitor.name); }); - await test.step('Expect to be able to open canned responses sidebar and creation', async () => { + await test.step('expect to be able to open canned responses sidebar and creation', async () => { await agent.poHomeChannel.content.btnCannedResponses.click(); + }); + + await test.step('expect to create new canned response', async () => { await agent.poHomeChannel.content.btnNewCannedResponse.click(); + await agent.poHomeChannel.cannedResponses.inputShortcut.fill(cannedResponseName); + await agent.poHomeChannel.cannedResponses.inputMessage.fill(faker.lorem.paragraph()); + await agent.poHomeChannel.cannedResponses.addTag(faker.commerce.department()); + await agent.poHomeChannel.cannedResponses.radioPublic.click(); + await agent.poHomeChannel.cannedResponses.btnSave.click(); + }); + }); + + test('OC - Canned Responses Sidebar - Edit', async () => { + await test.step('expect to have 1 omnichannel assigned to agent 1', async () => { + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); + }); + + await test.step('expect to be able to open canned responses sidebar and creation', async () => { + await agent.poHomeChannel.content.btnCannedResponses.click(); + }); + + await test.step('expect to edit canned response', async () => { + await agent.poHomeChannel.cannedResponses.listItem(cannedResponseName).click(); + await agent.poHomeChannel.cannedResponses.btnEdit.click(); + await agent.poHomeChannel.cannedResponses.radioPrivate.click(); + await agent.poHomeChannel.cannedResponses.btnSave.click(); }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts index 37f35b8202de8..d6c8cf9decf72 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts @@ -74,4 +74,10 @@ export class HomeOmnichannelContent extends HomeContent { get infoHeaderName(): Locator { return this.page.locator('.rcx-room-header').getByRole('heading'); } + + async closeChat() { + await this.btnCloseChat.click(); + await this.closeChatModal.inputComment.fill('any_comment'); + await this.closeChatModal.btnConfirm.click(); + } } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts index 0537090cc0c02..17713dd4b51e2 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts @@ -3,8 +3,49 @@ import type { Locator } from '@playwright/test'; import { OmnichannelAdministration } from './omnichannel-administration'; export class OmnichannelCannedResponses extends OmnichannelAdministration { - get radioPublic(): Locator { - return this.page.locator('[data-qa-id="canned-response-public-radio"]').first(); + get inputShortcut() { + return this.page.getByRole('textbox', { name: 'Shortcut', exact: true }); + } + + get inputMessage() { + return this.page.getByRole('textbox', { name: 'Message', exact: true }); + } + + get radioPublic() { + return this.page.locator('label', { has: this.page.getByRole('radio', { name: 'Public' }) }); + } + + get radioDepartment() { + return this.page.locator('label', { has: this.page.getByRole('radio', { name: 'Department' }) }); + } + + get radioPrivate() { + return this.page.locator('label', { has: this.page.getByRole('radio', { name: 'Private' }) }); + } + + get inputTags() { + return this.page.getByRole('textbox', { name: 'Tags', exact: true }); + } + + get btnAddTag() { + return this.page.getByRole('button', { name: 'Add', exact: true }); + } + + listItem(name: string) { + return this.page.getByText(`!${name}`, { exact: true }); + } + + async addTag(tag: string) { + await this.inputTags.fill(tag); + await this.btnAddTag.click(); + } + + get btnEdit() { + return this.page.getByRole('button', { name: 'Edit', exact: true }); + } + + get btnSave(): Locator { + return this.page.getByRole('button', { name: 'Save', exact: true }); } get btnNew(): Locator { From 895ea3fdbba1d0e3cf1bed03cb8d0abfcca5d351 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 10 Apr 2025 20:18:34 -0300 Subject: [PATCH 100/187] feat: Allow to dismiss contact unknown callout (#35703) --- .changeset/young-avocados-brake.md | 6 ++ .../ComposerOmnichannelCallout.spec.tsx | 75 ++++++++++++++++++ .../ComposerOmnichannelCallout.tsx | 41 ++++------ ...mnichannel-contact-unknown-callout.spec.ts | 69 ++++++++++++++++ .../page-objects/fragments/home-content.ts | 8 ++ apps/meteor/tests/mocks/data.ts | 78 +++++++++++++++---- packages/i18n/src/locales/en.i18n.json | 5 +- packages/i18n/src/locales/nb.i18n.json | 3 - packages/i18n/src/locales/nn.i18n.json | 3 - packages/i18n/src/locales/pt-BR.i18n.json | 5 +- 10 files changed, 238 insertions(+), 55 deletions(-) create mode 100644 .changeset/young-avocados-brake.md create mode 100644 apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.spec.tsx create mode 100644 apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts diff --git a/.changeset/young-avocados-brake.md b/.changeset/young-avocados-brake.md new file mode 100644 index 0000000000000..a20df69c72426 --- /dev/null +++ b/.changeset/young-avocados-brake.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Adds close action to contact unknown callout displayed within Livechat rooms diff --git a/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.spec.tsx b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.spec.tsx new file mode 100644 index 0000000000000..f246478e3b4b8 --- /dev/null +++ b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.spec.tsx @@ -0,0 +1,75 @@ +import { faker } from '@faker-js/faker/locale/af_ZA'; +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import ComposerOmnichannelCallout from './ComposerOmnichannelCallout'; +import FakeRoomProvider from '../../../../../tests/mocks/client/FakeRoomProvider'; +import { createFakeContact, createFakeRoom } from '../../../../../tests/mocks/data'; + +jest.mock('../../../omnichannel/contactInfo/tabs/ContactInfoChannels/useBlockChannel', () => ({ + useBlockChannel: () => jest.fn(), +})); + +const fakeVisitor = { + _id: faker.string.uuid(), + token: faker.string.uuid(), + username: faker.internet.userName(), +}; + +const fakeRoom = createFakeRoom({ t: 'l', v: fakeVisitor }); +const fakeContact = createFakeContact(); + +it('should be displayed if contact is unknown', async () => { + const getContactMockFn = jest.fn().mockResolvedValue({ contact: fakeContact }); + const wrapper = mockAppRoot().withEndpoint('GET', '/v1/omnichannel/contacts.get', getContactMockFn).withRoom(fakeRoom); + + render( + + + , + { wrapper: wrapper.build() }, + ); + + await waitFor(() => expect(getContactMockFn).toHaveBeenCalled()); + expect(screen.getByText('Unknown_contact_callout_description')).toBeVisible(); + expect(screen.getByRole('button', { name: 'Add_contact' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Block' })).toBeVisible(); + expect(screen.getByRole('button', { name: 'Dismiss' })).toBeVisible(); +}); + +it('should not be displayed if contact is known', async () => { + const getContactMockFn = jest.fn().mockResolvedValue({ contact: createFakeContact({ unknown: false }) }); + const wrapper = mockAppRoot().withEndpoint('GET', '/v1/omnichannel/contacts.get', getContactMockFn).withRoom(fakeRoom); + + render( + + + , + { wrapper: wrapper.build() }, + ); + + await waitFor(() => expect(getContactMockFn).toHaveBeenCalled()); + expect(screen.queryByText('Unknown_contact_callout_description')).not.toBeInTheDocument(); +}); + +it('should hide callout on dismiss', async () => { + const getContactMockFn = jest.fn().mockResolvedValue({ contact: fakeContact }); + const wrapper = mockAppRoot().withEndpoint('GET', '/v1/omnichannel/contacts.get', getContactMockFn).withRoom(fakeRoom); + + render( + + + , + { wrapper: wrapper.build() }, + ); + + await waitFor(() => expect(getContactMockFn).toHaveBeenCalled()); + expect(screen.getByText('Unknown_contact_callout_description')).toBeVisible(); + + const btnDismiss = screen.getByRole('button', { name: 'Dismiss' }); + await userEvent.click(btnDismiss); + + expect(screen.queryByText('Unknown_contact_callout_description')).not.toBeInTheDocument(); +}); diff --git a/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx index b1ce6cc244d3d..23a42b0546ad3 100644 --- a/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx +++ b/apps/meteor/client/views/room/composer/ComposerOmnichannel/ComposerOmnichannelCallout.tsx @@ -1,26 +1,18 @@ -import { Box, Button, ButtonGroup, Callout } from '@rocket.chat/fuselage'; -import { useAtLeastOnePermission, useEndpoint, useRouter, useSetting } from '@rocket.chat/ui-contexts'; +import { Button, ButtonGroup, Callout, IconButton } from '@rocket.chat/fuselage'; +import { useSessionStorage } from '@rocket.chat/fuselage-hooks'; +import { useEndpoint, useRouter } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import { Trans, useTranslation } from 'react-i18next'; +import { useId } from 'react'; +import { useTranslation } from 'react-i18next'; import { isSameChannel } from '../../../../../app/livechat/lib/isSameChannel'; -import { useHasLicenseModule } from '../../../../hooks/useHasLicenseModule'; import { useBlockChannel } from '../../../omnichannel/contactInfo/tabs/ContactInfoChannels/useBlockChannel'; import { useOmnichannelRoom } from '../../contexts/RoomContext'; const ComposerOmnichannelCallout = () => { const { t } = useTranslation(); const room = useOmnichannelRoom(); - const { navigate, buildRoutePath } = useRouter(); - const hasLicense = useHasLicenseModule('contact-id-verification'); - const securityPrivacyRoute = buildRoutePath('/omnichannel/security-privacy'); - const shouldShowSecurityRoute = useSetting('Livechat_Require_Contact_Verification') !== 'never' || !hasLicense; - - const canViewSecurityPrivacy = useAtLeastOnePermission([ - 'view-privileged-setting', - 'edit-privileged-setting', - 'manage-selected-settings', - ]); + const { navigate } = useRouter(); const { _id, @@ -29,6 +21,9 @@ const ComposerOmnichannelCallout = () => { contactId, } = room; + const calloutDescriptionId = useId(); + const [dismissed, setDismissed] = useSessionStorage(`contact-unknown-callout-${contactId}`, false); + const getContactById = useEndpoint('GET', '/v1/omnichannel/contacts.get'); const { data } = useQuery({ queryKey: ['getContactById', contactId], queryFn: () => getContactById({ contactId }) }); @@ -37,14 +32,15 @@ const ComposerOmnichannelCallout = () => { const handleBlock = useBlockChannel({ blocked: currentChannel?.blocked || false, association }); - if (!data?.contact?.unknown) { + if (dismissed || !data?.contact?.unknown) { return null; } return ( + setDismissed(true)} /> } > - {shouldShowSecurityRoute ? ( - - Add to contact list manually and - - enable verification - - using multi-factor authentication. - - ) : ( - t('Add_to_contact_list_manually') - )} +

{t('Unknown_contact_callout_description')}

); }; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts new file mode 100644 index 0000000000000..44335abad6cab --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-unknown-callout.spec.ts @@ -0,0 +1,69 @@ +import type { Page } from '@playwright/test'; + +import { createFakeVisitor } from '../../mocks/data'; +import { IS_EE } from '../config/constants'; +import { createAuxContext } from '../fixtures/createAuxContext'; +import { Users } from '../fixtures/userStates'; +import { OmnichannelLiveChat, HomeChannel } from '../page-objects'; +import { expect, test } from '../utils/test'; + +test.describe('OC - Contact Unknown Callout', () => { + test.skip(!IS_EE, 'Enterprise Only'); + + let poLiveChat: OmnichannelLiveChat; + let newVisitor: { email: string; name: string }; + + let agent: { page: Page; poHomeChannel: HomeChannel }; + + test.beforeAll(async ({ api, browser }) => { + newVisitor = createFakeVisitor(); + + await api.post('/livechat/users/agent', { username: 'user1' }); + await api.post('/livechat/users/manager', { username: 'user1' }); + + const { page } = await createAuxContext(browser, Users.user1); + agent = { page, poHomeChannel: new HomeChannel(page) }; + }); + test.beforeEach(async ({ page, api }) => { + poLiveChat = new OmnichannelLiveChat(page, api); + }); + + test.beforeEach('create livechat conversation', async ({ page }) => { + await page.goto('/livechat'); + await poLiveChat.openLiveChat(); + await poLiveChat.sendMessage(newVisitor, false); + await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + }); + + test.afterEach('close livechat conversation', async () => { + await poLiveChat.closeChat(); + }); + + test.afterAll(async ({ api }) => { + await api.delete('/livechat/users/agent/user1'); + await api.delete('/livechat/users/manager/user1'); + await agent.page.close(); + }); + + test('OC - Contact Unknown Callout - Dismiss callout', async () => { + await test.step('expect to open conversation', async () => { + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); + }); + + await test.step('expect contact unknown callout to be visible', async () => { + await expect(agent.poHomeChannel.content.contactUnknownCallout).toBeVisible(); + }); + + await test.step('expect to hide callout when dismiss is clicked', async () => { + await agent.poHomeChannel.content.btnDismissContactUnknownCallout.click(); + await expect(agent.poHomeChannel.content.contactUnknownCallout).not.toBeVisible(); + }); + + await test.step('expect keep callout hidden after changing pages', async () => { + await agent.poHomeChannel.sidenav.sidebarHomeAction.click(); + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); + await expect(agent.poHomeChannel.content.contactUnknownCallout).not.toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 08806275ef42e..cab59e8430693 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -463,4 +463,12 @@ export class HomeContent { get btnJoinChannel() { return this.page.getByRole('button', { name: 'Join channel' }); } + + get contactUnknownCallout() { + return this.page.getByRole('status', { name: 'Unknown contact. This contact is not on the contact list.' }); + } + + get btnDismissContactUnknownCallout() { + return this.contactUnknownCallout.getByRole('button', { name: 'Dismiss' }); + } } diff --git a/apps/meteor/tests/mocks/data.ts b/apps/meteor/tests/mocks/data.ts index d059d941abd6f..a651c8494335b 100644 --- a/apps/meteor/tests/mocks/data.ts +++ b/apps/meteor/tests/mocks/data.ts @@ -1,7 +1,17 @@ import { faker } from '@faker-js/faker'; import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '@rocket.chat/apps-engine/client/definition'; -import { AppSubscriptionStatus } from '@rocket.chat/core-typings'; -import type { LicenseInfo, App, IMessage, IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { ILivechatContact } from '@rocket.chat/apps-engine/definition/livechat'; +import { AppSubscriptionStatus, OmnichannelSourceType } from '@rocket.chat/core-typings'; +import type { + LicenseInfo, + App, + IMessage, + IRoom, + ISubscription, + IUser, + ILivechatContactChannel, + Serialized, +} from '@rocket.chat/core-typings'; import { parse } from '@rocket.chat/message-parser'; import type { MessageWithMdEnforced } from '../../client/lib/parseMessageTextToAstMarkdown'; @@ -21,21 +31,22 @@ export function createFakeUser(overrides?: Partial): IUser { }; } -export const createFakeRoom = (overrides?: Partial): IRoom => ({ - _id: faker.database.mongodbObjectId(), - _updatedAt: faker.date.recent(), - t: faker.helpers.arrayElement(['c', 'p', 'd']), - msgs: faker.number.int({ min: 0 }), - u: { +export const createFakeRoom = (overrides?: Partial): T => + ({ _id: faker.database.mongodbObjectId(), - username: faker.internet.userName(), - name: faker.person.fullName(), - ...overrides?.u, - }, - usersCount: faker.number.int({ min: 0 }), - autoTranslateLanguage: faker.helpers.arrayElement(['en', 'es', 'pt', 'ar', 'it', 'ru', 'fr']), - ...overrides, -}); + _updatedAt: faker.date.recent(), + t: faker.helpers.arrayElement(['c', 'p', 'd']), + msgs: faker.number.int({ min: 0 }), + u: { + _id: faker.database.mongodbObjectId(), + username: faker.internet.userName(), + name: faker.person.fullName(), + ...overrides?.u, + }, + usersCount: faker.number.int({ min: 0 }), + autoTranslateLanguage: faker.helpers.arrayElement(['en', 'es', 'pt', 'ar', 'it', 'ru', 'fr']), + ...overrides, + }) as T; export const createFakeSubscription = (overrides?: Partial): ISubscription => ({ _id: faker.database.mongodbObjectId(), @@ -284,3 +295,38 @@ export function createFakeVisitor() { email: faker.internet.email(), } as const; } + +export function createFakeContactChannel(overrides?: Partial>): Serialized { + return { + name: 'widget', + blocked: false, + verified: false, + ...overrides, + visitor: { + visitorId: faker.string.uuid(), + source: { + type: OmnichannelSourceType.WIDGET, + }, + ...overrides?.visitor, + }, + details: { + type: OmnichannelSourceType.WIDGET, + destination: '', + ...overrides?.details, + }, + }; +} + +export function createFakeContact(overrides?: Partial>): Serialized { + return { + _id: faker.string.uuid(), + _updatedAt: new Date().toISOString(), + name: pullNextVisitorName(), + phones: [{ phoneNumber: faker.phone.number() }], + emails: [{ address: faker.internet.email() }], + unknown: true, + channels: [createFakeContactChannel()], + createdAt: new Date().toISOString(), + ...overrides, + }; +} diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 4f3c109638bc1..fee5d3ae26462 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1717,6 +1717,7 @@ "Daily_Active_Users": "Daily Active Users", "Display_unread_counter": "Display room as unread when there are unread messages", "Displays_action_text": "Displays action text", + "Dismiss": "Dismiss", "Data_modified": "Data Modified", "Do_not_display_unread_counter": "Do not display any counter of this channel", "Do_you_want_to_accept": "Do you want to accept?", @@ -5999,6 +6000,7 @@ "Unique_ID_change_detected": "Unique ID change detected", "Unknown_Import_State": "Unknown Import State", "Unknown_User": "Unknown User", + "Unknown_contact_callout_description": "Unknown contact. This contact is not on the contact list.", "Unlimited": "Unlimited", "Unmute": "Unmute", "unpinning-not-allowed": "Unpinning is not allowed", @@ -6778,11 +6780,8 @@ "Advanced_contact_profile": "Advanced contact profile", "Advanced_contact_profile_description": "Manage multiple emails and phone numbers for a single contact, enabling a comprehensive multi-channel history that keeps you well-informed and improves communication efficiency.", "Add_contact": "Add contact", - "Add_to_contact_list_manually": "Add to contact list manually", - "Add_to_contact_and_enable_verification_description": "Add to contact list manually and <1>enable verification using multi-factor authentication.", "Ask_enable_advanced_contact_profile": "Ask your workspace admin to enable advanced contact profile", "close-blocked-room-comment": "This channel has been blocked", - "Contact_unknown": "Contact unknown", "Review_contact": "Review contact", "See_conflicts": "See conflicts", "Conflicts_found": "Conflicts found", diff --git a/packages/i18n/src/locales/nb.i18n.json b/packages/i18n/src/locales/nb.i18n.json index 6c425ba6327c9..dda09ad8d3dd7 100644 --- a/packages/i18n/src/locales/nb.i18n.json +++ b/packages/i18n/src/locales/nb.i18n.json @@ -6185,11 +6185,8 @@ "Advanced_contact_profile": "Avansert kontaktprofil", "Advanced_contact_profile_description": "Administrer flere e-poster og telefonnumre for en enkelt kontakt, noe som muliggjør en omfattende flerkanalshistorikk som holder deg godt informert og forbedrer kommunikasjonseffektiviteten.", "Add_contact": "Legg til kontakt", - "Add_to_contact_list_manually": "Legg til i kontaktlisten manuelt", - "Add_to_contact_and_enable_verification_description": "Legg til i kontaktlisten manuelt og <1>aktiver verifisering ved hjelp av multifaktorautentisering.", "Ask_enable_advanced_contact_profile": "Be arbeidsområdeadministratoren din om å aktivere avansert kontaktprofil", "close-blocked-room-comment": "Denne kanalen er blokkert", - "Contact_unknown": "Ukjent kontakt", "Review_contact": "Gjennomgå kontakt", "See_conflicts": "Se konflikter", "Conflicts_found": "Konflikter funnet", diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index 1e73ad4e738d2..14c86c4f6c25c 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -6185,11 +6185,8 @@ "Advanced_contact_profile": "Avansert kontaktprofil", "Advanced_contact_profile_description": "Administrer flere e-poster og telefonnumre for en enkelt kontakt, noe som muliggjør en omfattende flerkanalshistorikk som holder deg godt informert og forbedrer kommunikasjonseffektiviteten.", "Add_contact": "Legg til kontakt", - "Add_to_contact_list_manually": "Legg til i kontaktlisten manuelt", - "Add_to_contact_and_enable_verification_description": "Legg til i kontaktlisten manuelt og <1>aktiver verifisering ved hjelp av multifaktorautentisering.", "Ask_enable_advanced_contact_profile": "Be arbeidsområdeadministratoren din om å aktivere avansert kontaktprofil", "close-blocked-room-comment": "Denne kanalen er blokkert", - "Contact_unknown": "Ukjent kontakt", "Review_contact": "Gjennomgå kontakt", "See_conflicts": "Se konflikter", "Conflicts_found": "Konflikter funnet", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index fed94c6851d0f..67f050a51caba 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -324,8 +324,6 @@ "Add_members": "Adicionar membros", "Add_monitor": "Adicionar monitor", "Add_phone": "Adicionar número de telefone", - "Add_to_contact_and_enable_verification_description": "Adicione o contato na lista manualmente e <1>habilite a verificação usando autenticação de múltiplos fatores.", - "Add_to_contact_list_manually": "Adicione o contato na lista manualmente", "Add_user": "Adicionar usuário", "Add_users": "Adicionar usuários", "Added__username__to_team": "@{{user_added}} adicionado a esta equipe", @@ -927,7 +925,6 @@ "Contact_identification": "Identificação de contato", "Contact_not_found": "Contato não encontrado", "Contact_unblocked": "Contato desbloqueado", - "Contact_unknown": "Contato desconhecido", "Contacts": "Contatos", "Contains_Security_Fixes": "Contém correções de segurança", "Content": "Conteúdo", @@ -1402,6 +1399,7 @@ "Display_setting_permissions": "Exibir permissões para alterar configurações", "Display_unread_counter": "Exibir número de mensagens não lidas", "Displays_action_text": "Exibe texto da ação", + "Dismiss": "Dispensar", "Do_It_Later": "Fazer depois", "Do_Nothing": "Não fazer nada", "Do_not_display_unread_counter": "Não exibir nenhum contador desse canal", @@ -3971,6 +3969,7 @@ "Uninstall": "Desinstalar", "Unit_removed": "Unidade removida", "Unknown_Import_State": "Estado de importação desconhecido", + "Unknown_contact_callout_description": "Contato desconhecido. Este contato não está na lista de contatos.", "Unlimited": "Ilimitado", "Unmute": "Ativar o som", "Unmute_microphone": "Ativar o som do microfone", From eabc50d3b18baa10aed579eaa7316ea9d1db9fc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:58:31 -0300 Subject: [PATCH 101/187] chore: create `LoggedInArea` (#35741) Co-authored-by: Guilherme Gazzo --- .../useNotificationUserCalendar.ts | 9 +++--- .../hooks/notification/useNotifyUser.ts | 15 ++++------ apps/meteor/client/views/root/AppLayout.tsx | 14 --------- .../root/MainLayout/AuthenticationCheck.tsx | 13 +++++--- .../views/root/MainLayout/LoggedInArea.tsx | 30 +++++++++++++++++++ .../client/views/root/hooks/useForceLogout.ts | 9 ++---- .../views/root/hooks/useOTRMessaging.ts | 9 ++---- .../root/hooks/useStoreCookiesOnLogin.ts | 7 ++--- .../root/hooks/useUpdateVideoConfUser.ts | 5 ++-- .../client/views/root/hooks/useWebRTC.ts | 7 ++--- 10 files changed, 59 insertions(+), 59 deletions(-) create mode 100644 apps/meteor/client/views/root/MainLayout/LoggedInArea.tsx diff --git a/apps/meteor/client/hooks/notification/useNotificationUserCalendar.ts b/apps/meteor/client/hooks/notification/useNotificationUserCalendar.ts index bb7d497194d1d..47af5ae6e06eb 100644 --- a/apps/meteor/client/hooks/notification/useNotificationUserCalendar.ts +++ b/apps/meteor/client/hooks/notification/useNotificationUserCalendar.ts @@ -1,19 +1,18 @@ -import type { ICalendarNotification } from '@rocket.chat/core-typings'; +import type { ICalendarNotification, IUser } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useSetting, useStream, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useSetting, useStream, useUserPreference } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { imperativeModal } from '../../lib/imperativeModal'; import OutlookCalendarEventModal from '../../views/outlookCalendar/OutlookCalendarEventModal'; -export const useNotificationUserCalendar = () => { - const user = useUser(); +export const useNotificationUserCalendar = (user: IUser) => { const requireInteraction = useUserPreference('desktopNotificationRequireInteraction'); const outLookEnabled = useSetting('Outlook_Calendar_Enabled'); const notifyUserStream = useStream('notify-user'); const notifyUserCalendar = useEffectEvent(async (notification: ICalendarNotification) => { - if (!user || user.status === 'busy') { + if (user.status === 'busy') { return; } diff --git a/apps/meteor/client/hooks/notification/useNotifyUser.ts b/apps/meteor/client/hooks/notification/useNotifyUser.ts index d2762144e94ba..3ac30732dc8a4 100644 --- a/apps/meteor/client/hooks/notification/useNotifyUser.ts +++ b/apps/meteor/client/hooks/notification/useNotifyUser.ts @@ -1,6 +1,6 @@ -import type { AtLeast, INotificationDesktop, ISubscription } from '@rocket.chat/core-typings'; +import type { AtLeast, INotificationDesktop, ISubscription, IUser } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { useCustomSound, useRouter, useStream, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useCustomSound, useRouter, useStream, useUserPreference } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { useEmbeddedLayout } from '../useEmbeddedLayout'; @@ -9,8 +9,7 @@ import { useNewMessageNotification } from './useNewMessageNotification'; import { RoomManager } from '../../lib/RoomManager'; import { fireGlobalEvent } from '../../lib/utils/fireGlobalEvent'; -export const useNotifyUser = () => { - const user = useUser(); +export const useNotifyUser = (user: IUser) => { const router = useRouter(); const isLayoutEmbedded = useEmbeddedLayout(); const notifyUserStream = useStream('notify-user'); @@ -20,7 +19,7 @@ export const useNotifyUser = () => { const showDesktopNotification = useDesktopNotification(); const notifyNewRoom = useEffectEvent(async (sub: AtLeast): Promise => { - if (!user || user.status === 'busy') { + if (user.status === 'busy') { return; } @@ -57,10 +56,6 @@ export const useNotifyUser = () => { }); useEffect(() => { - if (!user?._id) { - return; - } - const unsubNotification = notifyUserStream(`${user._id}/notification`, notifyNewMessageAudioAndDesktop); const unsubSubs = notifyUserStream(`${user._id}/subscriptions-changed`, (action, sub) => { @@ -75,7 +70,7 @@ export const useNotifyUser = () => { unsubNotification(); unsubSubs(); }; - }, [notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user?._id]); + }, [notifyNewMessageAudioAndDesktop, notifyNewRoom, notifyUserStream, router, user._id]); useEffect(() => () => notificationSounds.stopNewRoom(), [notificationSounds]); }; diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 7cad62d46c6fc..68b46e0e7d94b 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -4,14 +4,9 @@ import DocumentTitleWrapper from './DocumentTitleWrapper'; import PageLoading from './PageLoading'; import { useCodeHighlight } from './hooks/useCodeHighlight'; import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke'; -import { useForceLogout } from './hooks/useForceLogout'; import { useGoogleTagManager } from './hooks/useGoogleTagManager'; import { useMessageLinkClicks } from './hooks/useMessageLinkClicks'; -import { useOTRMessaging } from './hooks/useOTRMessaging'; import { useSettingsOnLoadSiteUrl } from './hooks/useSettingsOnLoadSiteUrl'; -import { useStoreCookiesOnLogin } from './hooks/useStoreCookiesOnLogin'; -import { useUpdateVideoConfUser } from './hooks/useUpdateVideoConfUser'; -import { useWebRTC } from './hooks/useWebRTC'; import { useWordPressOAuth } from './hooks/useWordPressOAuth'; import { useCorsSSLConfig } from '../../../app/cors/client/useCorsSSLConfig'; import { useDolphin } from '../../../app/dolphin/client/hooks/useDolphin'; @@ -23,8 +18,6 @@ import { useLivechatEnterprise } from '../../../app/livechat-enterprise/hooks/us import { useNextcloud } from '../../../app/nextcloud/client/useNextcloud'; import { useTokenPassAuth } from '../../../app/tokenpass/client/hooks/useTokenPassAuth'; import { useNotificationPermission } from '../../hooks/notification/useNotificationPermission'; -import { useNotificationUserCalendar } from '../../hooks/notification/useNotificationUserCalendar'; -import { useNotifyUser } from '../../hooks/notification/useNotifyUser'; import { useAnalytics } from '../../hooks/useAnalytics'; import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking'; import { useAutoupdate } from '../../hooks/useAutoupdate'; @@ -48,7 +41,6 @@ const AppLayout = () => { useEscapeKeyStroke(); useAnalyticsEventTracking(); useLoadRoomForAllowedAnonymousRead(); - useNotifyUser(); useNotificationPermission(); useEmojiOne(); useRedirectToSetupWizard(); @@ -63,14 +55,8 @@ const AppLayout = () => { useWordPressOAuth(); useCustomOAuth(); useCorsSSLConfig(); - useOTRMessaging(); - useUpdateVideoConfUser(); - useWebRTC(); - useStoreCookiesOnLogin(); useAutoupdate(); - useForceLogout(); useCodeHighlight(); - useNotificationUserCalendar(); const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot); diff --git a/apps/meteor/client/views/root/MainLayout/AuthenticationCheck.tsx b/apps/meteor/client/views/root/MainLayout/AuthenticationCheck.tsx index a10d59eff67b8..822acfc22562b 100644 --- a/apps/meteor/client/views/root/MainLayout/AuthenticationCheck.tsx +++ b/apps/meteor/client/views/root/MainLayout/AuthenticationCheck.tsx @@ -1,7 +1,8 @@ -import { useSession, useUserId, useSetting } from '@rocket.chat/ui-contexts'; +import { useSession, useUser, useSetting } from '@rocket.chat/ui-contexts'; import RegistrationRoute from '@rocket.chat/web-ui-registration'; import type { ReactElement, ReactNode } from 'react'; +import LoggedInArea from './LoggedInArea'; import LoginPage from './LoginPage'; import UsernameCheck from './UsernameCheck'; @@ -15,12 +16,16 @@ import UsernameCheck from './UsernameCheck'; * renders the page, without creating an user (not even an anonymous user) */ const AuthenticationCheck = ({ children, guest }: { children: ReactNode; guest?: boolean }): ReactElement => { - const uid = useUserId(); + const user = useUser(); const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead'); const forceLogin = useSession('forceLogin'); - if (uid) { - return {children}; + if (user) { + return ( + + {children} + + ); } if (!forceLogin && guest) { diff --git a/apps/meteor/client/views/root/MainLayout/LoggedInArea.tsx b/apps/meteor/client/views/root/MainLayout/LoggedInArea.tsx new file mode 100644 index 0000000000000..298da34a0f8df --- /dev/null +++ b/apps/meteor/client/views/root/MainLayout/LoggedInArea.tsx @@ -0,0 +1,30 @@ +import { useUser } from '@rocket.chat/ui-contexts'; +import type { ReactNode } from 'react'; + +import { useNotificationUserCalendar } from '../../../hooks/notification/useNotificationUserCalendar'; +import { useNotifyUser } from '../../../hooks/notification/useNotifyUser'; +import { useForceLogout } from '../hooks/useForceLogout'; +import { useOTRMessaging } from '../hooks/useOTRMessaging'; +import { useStoreCookiesOnLogin } from '../hooks/useStoreCookiesOnLogin'; +import { useUpdateVideoConfUser } from '../hooks/useUpdateVideoConfUser'; +import { useWebRTC } from '../hooks/useWebRTC'; + +const LoggedInArea = ({ children }: { children: ReactNode }) => { + const user = useUser(); + + if (!user) { + throw new Error('User not logged'); + } + + useNotifyUser(user); + useUpdateVideoConfUser(user._id); + useWebRTC(user._id); + useOTRMessaging(user._id); + useNotificationUserCalendar(user); + useForceLogout(user._id); + useStoreCookiesOnLogin(user._id); + + return children; +}; + +export default LoggedInArea; diff --git a/apps/meteor/client/views/root/hooks/useForceLogout.ts b/apps/meteor/client/views/root/hooks/useForceLogout.ts index 080dcb9a42377..07390ddb0d4aa 100644 --- a/apps/meteor/client/views/root/hooks/useForceLogout.ts +++ b/apps/meteor/client/views/root/hooks/useForceLogout.ts @@ -1,16 +1,11 @@ -import { useUserId, useStream, useSessionDispatch } from '@rocket.chat/ui-contexts'; +import { useStream, useSessionDispatch } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -export const useForceLogout = () => { - const userId = useUserId(); +export const useForceLogout = (userId: string) => { const getNotifyUserStream = useStream('notify-user'); const setForceLogout = useSessionDispatch('forceLogout'); useEffect(() => { - if (!userId) { - return; - } - setForceLogout(false); const unsubscribe = getNotifyUserStream(`${userId}/force_logout`, () => { diff --git a/apps/meteor/client/views/root/hooks/useOTRMessaging.ts b/apps/meteor/client/views/root/hooks/useOTRMessaging.ts index 3b09e84b86692..b3195326fe8c6 100644 --- a/apps/meteor/client/views/root/hooks/useOTRMessaging.ts +++ b/apps/meteor/client/views/root/hooks/useOTRMessaging.ts @@ -1,6 +1,6 @@ import type { AtLeast, IMessage } from '@rocket.chat/core-typings'; import { isOTRMessage } from '@rocket.chat/core-typings'; -import { useMethod, useStream, useUserId } from '@rocket.chat/ui-contexts'; +import { useMethod, useStream } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import OTR from '../../../../app/otr/client/OTR'; @@ -9,16 +9,11 @@ import { t } from '../../../../app/utils/lib/i18n'; import { onClientBeforeSendMessage } from '../../../lib/onClientBeforeSendMessage'; import { onClientMessageReceived } from '../../../lib/onClientMessageReceived'; -export const useOTRMessaging = () => { - const uid = useUserId(); +export const useOTRMessaging = (uid: string) => { const updateOTRAck = useMethod('updateOTRAck'); const notifyUser = useStream('notify-user'); useEffect(() => { - if (!uid) { - return; - } - const handleNotifyUser = (type: 'handshake' | 'acknowledge' | 'deny' | 'end', data: { roomId: string; userId: string }) => { if (!data.roomId || !data.userId || data.userId === uid) { return; diff --git a/apps/meteor/client/views/root/hooks/useStoreCookiesOnLogin.ts b/apps/meteor/client/views/root/hooks/useStoreCookiesOnLogin.ts index 1d43b408b81f6..712726bd9a063 100644 --- a/apps/meteor/client/views/root/hooks/useStoreCookiesOnLogin.ts +++ b/apps/meteor/client/views/root/hooks/useStoreCookiesOnLogin.ts @@ -1,15 +1,14 @@ -import { useConnectionStatus, useUserId } from '@rocket.chat/ui-contexts'; +import { useConnectionStatus } from '@rocket.chat/ui-contexts'; import { Accounts } from 'meteor/accounts-base'; import { useEffect } from 'react'; -export const useStoreCookiesOnLogin = () => { - const userId = useUserId(); +export const useStoreCookiesOnLogin = (userId: string) => { const { isLoggingIn } = useConnectionStatus(); useEffect(() => { // Check for isLoggingIn to be reactive and ensure it will process only after login finishes // preventing race condition setting the rc_token as null forever - if (userId && isLoggingIn === false) { + if (isLoggingIn === false) { const secure = location.protocol === 'https:' ? '; secure' : ''; document.cookie = `rc_uid=${encodeURI(userId)}; path=/${secure}`; diff --git a/apps/meteor/client/views/root/hooks/useUpdateVideoConfUser.ts b/apps/meteor/client/views/root/hooks/useUpdateVideoConfUser.ts index bc3113eeb7fc5..67e3e72dd3012 100644 --- a/apps/meteor/client/views/root/hooks/useUpdateVideoConfUser.ts +++ b/apps/meteor/client/views/root/hooks/useUpdateVideoConfUser.ts @@ -1,10 +1,9 @@ -import { useConnectionStatus, useUserId } from '@rocket.chat/ui-contexts'; +import { useConnectionStatus } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { VideoConfManager } from '../../../lib/VideoConfManager'; -export const useUpdateVideoConfUser = () => { - const userId = useUserId(); +export const useUpdateVideoConfUser = (userId: string) => { const { connected, isLoggingIn } = useConnectionStatus(); useEffect(() => { diff --git a/apps/meteor/client/views/root/hooks/useWebRTC.ts b/apps/meteor/client/views/root/hooks/useWebRTC.ts index bb6e66d4f02cd..a6182f985d406 100644 --- a/apps/meteor/client/views/root/hooks/useWebRTC.ts +++ b/apps/meteor/client/views/root/hooks/useWebRTC.ts @@ -1,17 +1,14 @@ -import { useStream, useUserId } from '@rocket.chat/ui-contexts'; +import { useStream } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import type { CandidateData, DescriptionData, JoinData } from '../../../../app/webrtc/client/WebRTCClass'; import { WebRTC } from '../../../../app/webrtc/client/WebRTCClass'; import { WEB_RTC_EVENTS } from '../../../../app/webrtc/lib/constants'; -export const useWebRTC = () => { - const uid = useUserId(); +export const useWebRTC = (uid: string) => { const notifyUser = useStream('notify-user'); useEffect(() => { - if (!uid) return; - const handleNotifyUser = (type: 'candidate' | 'description' | 'join', data: CandidateData | DescriptionData | JoinData) => { if (data.room == null) return; From f8ce8e5d1d20e4bf4d94c6cebd11810729b4d521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:08:32 -0300 Subject: [PATCH 102/187] chore: Remove meteor functions from `loginViaQuery.ts` (#35730) --- apps/meteor/client/startup/index.ts | 1 - apps/meteor/client/startup/loginViaQuery.ts | 31 -------------- apps/meteor/client/views/root/AppLayout.tsx | 2 + .../views/root/hooks/useLoginViaQuery.ts | 41 +++++++++++++++++++ 4 files changed, 43 insertions(+), 32 deletions(-) delete mode 100644 apps/meteor/client/startup/loginViaQuery.ts create mode 100644 apps/meteor/client/views/root/hooks/useLoginViaQuery.ts diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 4089326d356f5..85c526efbba39 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -9,7 +9,6 @@ import './e2e'; import './iframeCommands'; import './incomingMessages'; import './loadMissedMessages'; -import './loginViaQuery'; import './messageObserve'; import './messageTypes'; import './reloadRoomAfterLogin'; diff --git a/apps/meteor/client/startup/loginViaQuery.ts b/apps/meteor/client/startup/loginViaQuery.ts deleted file mode 100644 index f22fb80d1c713..0000000000000 --- a/apps/meteor/client/startup/loginViaQuery.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { router } from '../providers/RouterProvider'; - -Meteor.startup(() => { - Tracker.afterFlush(() => { - const { resumeToken } = router.getSearchParameters(); - if (!resumeToken) { - return; - } - - Meteor.loginWithToken(resumeToken, () => { - const routeName = router.getRouteName(); - - if (!routeName) { - router.navigate('/home'); - } - - const { resumeToken: _, userId: __, ...search } = router.getSearchParameters(); - - router.navigate( - { - pathname: router.getLocationPathname(), - search, - }, - { replace: true }, - ); - }); - }); -}); diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 68b46e0e7d94b..64f2630ed8527 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -5,6 +5,7 @@ import PageLoading from './PageLoading'; import { useCodeHighlight } from './hooks/useCodeHighlight'; import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke'; import { useGoogleTagManager } from './hooks/useGoogleTagManager'; +import { useLoginViaQuery } from './hooks/useLoginViaQuery'; import { useMessageLinkClicks } from './hooks/useMessageLinkClicks'; import { useSettingsOnLoadSiteUrl } from './hooks/useSettingsOnLoadSiteUrl'; import { useWordPressOAuth } from './hooks/useWordPressOAuth'; @@ -57,6 +58,7 @@ const AppLayout = () => { useCorsSSLConfig(); useAutoupdate(); useCodeHighlight(); + useLoginViaQuery(); const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot); diff --git a/apps/meteor/client/views/root/hooks/useLoginViaQuery.ts b/apps/meteor/client/views/root/hooks/useLoginViaQuery.ts new file mode 100644 index 0000000000000..a67739eb7b025 --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useLoginViaQuery.ts @@ -0,0 +1,41 @@ +import { useRouter, useLoginWithToken } from '@rocket.chat/ui-contexts'; +import { useEffect } from 'react'; + +export const useLoginViaQuery = () => { + const router = useRouter(); + const loginWithToken = useLoginWithToken(); + + useEffect(() => { + const handleLogin = async () => { + const { resumeToken } = router.getSearchParameters(); + + if (!resumeToken) { + return; + } + + try { + await loginWithToken(resumeToken); + + const routeName = router.getRouteName(); + + if (!routeName) { + router.navigate('/home'); + } + + const { resumeToken: _, userId: __, ...search } = router.getSearchParameters(); + + router.navigate( + { + pathname: router.getLocationPathname(), + search, + }, + { replace: true }, + ); + } catch (error) { + console.error('Failed to login with token', error); + } + }; + + handleLogin(); + }, [loginWithToken, router]); +}; From d8ba07aad9ddfebe016975c2e05037383879893c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Thu, 10 Apr 2025 21:58:15 -0300 Subject: [PATCH 103/187] chore: Remove meteor functions from `loadMissedMessages.ts` (#35695) --- apps/meteor/client/startup/index.ts | 1 - .../client/startup/loadMissedMessages.ts | 42 ---------------- apps/meteor/client/views/root/AppLayout.tsx | 2 + .../views/root/hooks/useLoadMissedMessages.ts | 50 +++++++++++++++++++ 4 files changed, 52 insertions(+), 43 deletions(-) delete mode 100644 apps/meteor/client/startup/loadMissedMessages.ts create mode 100644 apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 85c526efbba39..dc217a109b80c 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -8,7 +8,6 @@ import './deviceManagement'; import './e2e'; import './iframeCommands'; import './incomingMessages'; -import './loadMissedMessages'; import './messageObserve'; import './messageTypes'; import './reloadRoomAfterLogin'; diff --git a/apps/meteor/client/startup/loadMissedMessages.ts b/apps/meteor/client/startup/loadMissedMessages.ts deleted file mode 100644 index 37bd437d8af27..0000000000000 --- a/apps/meteor/client/startup/loadMissedMessages.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { Messages, Subscriptions } from '../../app/models/client'; -import { LegacyRoomManager, upsertMessage } from '../../app/ui-utils/client'; -import { callWithErrorHandling } from '../lib/utils/callWithErrorHandling'; - -const loadMissedMessages = async function (rid: IRoom['_id']): Promise { - const lastMessage = Messages.findOne({ rid, _hidden: { $ne: true }, temp: { $exists: false } }, { sort: { ts: -1 }, limit: 1 }); - - if (!lastMessage) { - return; - } - - try { - const result = await callWithErrorHandling('loadMissedMessages', rid, lastMessage.ts); - if (result) { - const subscription = Subscriptions.findOne({ rid }); - await Promise.all(Array.from(result).map((msg) => upsertMessage({ msg, subscription }))); - } - } catch (error) { - console.error(error); - } -}; - -Meteor.startup(() => { - let connectionWasOnline = true; - Tracker.autorun(() => { - const { connected } = Meteor.connection.status(); - - if (connected === true && connectionWasOnline === false && LegacyRoomManager.openedRooms) { - Object.keys(LegacyRoomManager.openedRooms).forEach((key) => { - const value = LegacyRoomManager.openedRooms[key]; - if (value.rid) { - loadMissedMessages(value.rid); - } - }); - } - connectionWasOnline = connected; - }); -}); diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 64f2630ed8527..e7afa4be398da 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -5,6 +5,7 @@ import PageLoading from './PageLoading'; import { useCodeHighlight } from './hooks/useCodeHighlight'; import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke'; import { useGoogleTagManager } from './hooks/useGoogleTagManager'; +import { useLoadMissedMessages } from './hooks/useLoadMissedMessages'; import { useLoginViaQuery } from './hooks/useLoginViaQuery'; import { useMessageLinkClicks } from './hooks/useMessageLinkClicks'; import { useSettingsOnLoadSiteUrl } from './hooks/useSettingsOnLoadSiteUrl'; @@ -59,6 +60,7 @@ const AppLayout = () => { useAutoupdate(); useCodeHighlight(); useLoginViaQuery(); + useLoadMissedMessages(); const layout = useSyncExternalStore(appLayout.subscribe, appLayout.getSnapshot); diff --git a/apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts b/apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts new file mode 100644 index 0000000000000..976b1b2bafbbc --- /dev/null +++ b/apps/meteor/client/views/root/hooks/useLoadMissedMessages.ts @@ -0,0 +1,50 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useConnectionStatus } from '@rocket.chat/ui-contexts'; +import { useEffect, useRef } from 'react'; + +import { Messages, Subscriptions } from '../../../../app/models/client'; +import { LegacyRoomManager, upsertMessage } from '../../../../app/ui-utils/client'; +import { callWithErrorHandling } from '../../../lib/utils/callWithErrorHandling'; + +/** + * Loads missed messages for a room + * @param rid - Room ID + */ +const loadMissedMessages = async (rid: IRoom['_id']): Promise => { + const lastMessage = Messages.findOne({ rid, _hidden: { $ne: true }, temp: { $exists: false } }, { sort: { ts: -1 }, limit: 1 }); + + if (!lastMessage) { + return; + } + + try { + const result = await callWithErrorHandling('loadMissedMessages', rid, lastMessage.ts); + if (result) { + const subscription = Subscriptions.findOne({ rid }); + await Promise.all(Array.from(result).map((msg) => upsertMessage({ msg, subscription }))); + } + } catch (error) { + console.error('Error loading missed messages:', error); + } +}; + +/** + * React hook that loads missed messages when connection is restored + */ +export const useLoadMissedMessages = (): void => { + const { connected } = useConnectionStatus(); + const connectionWasOnlineRef = useRef(connected); + + useEffect(() => { + if (connected === true && connectionWasOnlineRef.current === false && LegacyRoomManager.openedRooms) { + Object.keys(LegacyRoomManager.openedRooms).forEach((key) => { + const value = LegacyRoomManager.openedRooms[key]; + if (value.rid) { + loadMissedMessages(value.rid); + } + }); + } + + connectionWasOnlineRef.current = connected; + }, [connected]); +}; From 3e34228268b10db5ab857f4ab108fdfc055c350f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 11 Apr 2025 11:18:48 -0600 Subject: [PATCH 104/187] chore: Remove `livechatTyped` in favor of smaller components (#35785) --- .../app/apps/server/bridges/livechat.ts | 6 +- .../app/livechat/imports/server/rest/users.ts | 10 +- .../app/livechat/server/api/lib/livechat.ts | 5 - .../app/livechat/server/api/v1/agent.ts | 5 +- .../app/livechat/server/api/v1/config.ts | 4 +- .../app/livechat/server/api/v1/videoCall.ts | 5 +- .../app/livechat/server/api/v1/visitor.ts | 13 +- apps/meteor/app/livechat/server/lib/Helper.ts | 5 +- .../app/livechat/server/lib/LivechatTyped.ts | 221 ------------------ .../app/livechat/server/lib/QueueManager.ts | 5 +- .../lib/contacts/registerContact.spec.ts | 7 - .../livechat/server/lib/getOnlineAgents.ts | 26 --- apps/meteor/app/livechat/server/lib/guests.ts | 19 +- .../app/livechat/server/lib/omni-users.ts | 111 ++++++++- .../app/livechat/server/lib/service-status.ts | 83 +++++++ apps/meteor/app/livechat/server/lib/utils.ts | 27 ++- .../server/methods/changeLivechatStatus.ts | 5 +- .../livechat/server/methods/saveAgentInfo.ts | 4 +- .../server/methods/setUpConnection.ts | 4 +- .../server/hooks/beforeRoutingChat.ts | 2 +- .../hooks/checkAgentBeforeTakeInquiry.ts | 4 +- apps/meteor/server/services/meteor/service.ts | 4 +- 22 files changed, 272 insertions(+), 303 deletions(-) delete mode 100644 apps/meteor/app/livechat/server/lib/LivechatTyped.ts delete mode 100644 apps/meteor/app/livechat/server/lib/getOnlineAgents.ts create mode 100644 apps/meteor/app/livechat/server/lib/service-status.ts diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 73397b728f034..6c1152e678b3d 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -10,7 +10,6 @@ import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@roc import { callbacks } from '../../../../lib/callbacks'; import { deasyncPromise } from '../../../../server/deasync/deasync'; -import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; import { closeRoom } from '../../../livechat/server/lib/closeRoom'; import { setCustomFields } from '../../../livechat/server/lib/custom-fields'; import { getRoomMessages } from '../../../livechat/server/lib/getRoomMessages'; @@ -18,6 +17,7 @@ import { registerGuest } from '../../../livechat/server/lib/guests'; import type { ILivechatMessage } from '../../../livechat/server/lib/localTypes'; import { updateMessage, sendMessage } from '../../../livechat/server/lib/messages'; import { createRoom } from '../../../livechat/server/lib/rooms'; +import { online } from '../../../livechat/server/lib/service-status'; import { transfer } from '../../../livechat/server/lib/transfer'; import { settings } from '../../../settings/server'; @@ -41,11 +41,11 @@ export class AppLivechatBridge extends LivechatBridge { protected isOnline(departmentId?: string): boolean { // This function will be converted to sync inside the apps-engine code // TODO: Track Deprecation - return deasyncPromise(LivechatTyped.online(departmentId)); + return deasyncPromise(online(departmentId)); } protected async isOnlineAsync(departmentId?: string): Promise { - return LivechatTyped.online(departmentId); + return online(departmentId); } protected async createMessage(message: IAppsLivechatMessage, appId: string): Promise { diff --git a/apps/meteor/app/livechat/imports/server/rest/users.ts b/apps/meteor/app/livechat/imports/server/rest/users.ts index 85ceb5103687a..b8d5088d80418 100644 --- a/apps/meteor/app/livechat/imports/server/rest/users.ts +++ b/apps/meteor/app/livechat/imports/server/rest/users.ts @@ -7,7 +7,7 @@ import { API } from '../../../../api/server'; import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems'; import { hasAtLeastOnePermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { findAgents, findManagers } from '../../../server/api/lib/users'; -import { Livechat } from '../../../server/lib/LivechatTyped'; +import { addManager, addAgent, removeAgent, removeManager } from '../../../server/lib/omni-users'; const emptyStringArray: string[] = []; @@ -73,12 +73,12 @@ API.v1.addRoute( }, async post() { if (this.urlParams.type === 'agent') { - const user = await Livechat.addAgent(this.bodyParams.username); + const user = await addAgent(this.bodyParams.username); if (user) { return API.v1.success({ user }); } } else if (this.urlParams.type === 'manager') { - const user = await Livechat.addManager(this.bodyParams.username); + const user = await addManager(this.bodyParams.username); if (user) { return API.v1.success({ user }); } @@ -130,11 +130,11 @@ API.v1.addRoute( } if (this.urlParams.type === 'agent') { - if (await Livechat.removeAgent(user.username)) { + if (await removeAgent(user.username)) { return API.v1.success(); } } else if (this.urlParams.type === 'manager') { - if (await Livechat.removeManager(user.username)) { + if (await removeManager(user.username)) { return API.v1.success(); } } else { diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.ts b/apps/meteor/app/livechat/server/api/lib/livechat.ts index bd8df571884ec..df43140b28f5b 100644 --- a/apps/meteor/app/livechat/server/api/lib/livechat.ts +++ b/apps/meteor/app/livechat/server/api/lib/livechat.ts @@ -6,13 +6,8 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { normalizeAgent } from '../../lib/Helper'; -import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { getInitSettings } from '../../lib/settings'; -export function online(department: string, skipSettingCheck = false, skipFallbackCheck = false): Promise { - return LivechatTyped.online(department, skipSettingCheck, skipFallbackCheck); -} - async function findTriggers(): Promise[]> { const triggers = await LivechatTrigger.findEnabled().toArray(); const hasLicense = License.hasModule('livechat-enterprise'); diff --git a/apps/meteor/app/livechat/server/api/v1/agent.ts b/apps/meteor/app/livechat/server/api/v1/agent.ts index 0acedc924b0c3..e5b59bde2d01c 100644 --- a/apps/meteor/app/livechat/server/api/v1/agent.ts +++ b/apps/meteor/app/livechat/server/api/v1/agent.ts @@ -5,10 +5,9 @@ import { isGETAgentNextToken, isPOSTLivechatAgentStatusProps } from '@rocket.cha import { API } from '../../../../api/server'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { RoutingManager } from '../../lib/RoutingManager'; import { getRequiredDepartment } from '../../lib/departmentsLib'; -import { setUserStatusLivechat } from '../../lib/utils'; +import { setUserStatusLivechat, allowAgentChangeServiceStatus } from '../../lib/utils'; import { findRoom, findGuest, findAgent, findOpenRoom } from '../lib/livechat'; API.v1.addRoute('livechat/agent.info/:rid/:token', { @@ -97,7 +96,7 @@ API.v1.addRoute( return API.v1.success({ status: agent.statusLivechat }); } - const canChangeStatus = await LivechatTyped.allowAgentChangeServiceStatus(newStatus, agentId); + const canChangeStatus = await allowAgentChangeServiceStatus(newStatus, agentId); if (agentId !== this.userId) { if (!(await hasPermissionAsync(this.userId, 'manage-livechat-agents'))) { diff --git a/apps/meteor/app/livechat/server/api/v1/config.ts b/apps/meteor/app/livechat/server/api/v1/config.ts index 17a2945e75def..55afdfc00e6ad 100644 --- a/apps/meteor/app/livechat/server/api/v1/config.ts +++ b/apps/meteor/app/livechat/server/api/v1/config.ts @@ -3,7 +3,7 @@ import mem from 'mem'; import { API } from '../../../../api/server'; import { settings as serverSettings } from '../../../../settings/server'; -import { Livechat } from '../../lib/LivechatTyped'; +import { online } from '../../lib/service-status'; import { settings, findOpenRoom, getExtraConfigInfo, findAgent, findGuestWithoutActivity } from '../lib/livechat'; const cachedSettings = mem(settings, { maxAge: process.env.TEST_MODE === 'true' ? 1 : 1000, cacheKey: JSON.stringify }); @@ -23,7 +23,7 @@ API.v1.addRoute( const config = await cachedSettings({ businessUnit }); - const status = await Livechat.online(department); + const status = await online(department); const guest = token ? await findGuestWithoutActivity(token) : null; const room = guest ? await findOpenRoom(guest.token) : undefined; diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.ts b/apps/meteor/app/livechat/server/api/v1/videoCall.ts index 01bda33724d11..ee71543b75aba 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.ts +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.ts @@ -8,7 +8,7 @@ import { API } from '../../../../api/server'; import { canSendMessageAsync } from '../../../../authorization/server/functions/canSendMessage'; import { notifyOnRoomChangedById, notifyOnSettingChanged } from '../../../../lib/server/lib/notifyListener'; import { settings as rcSettings } from '../../../../settings/server'; -import { Livechat } from '../../lib/LivechatTyped'; +import { updateCallStatus } from '../../lib/utils'; import { settings } from '../lib/livechat'; API.v1.addRoute( @@ -70,6 +70,7 @@ API.v1.addRoute( }, ); +// TODO: investigate if we can deprecate this functionality API.v1.addRoute( 'livechat/webrtc.call/:callId', { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPUTWebRTCCallId }, @@ -100,7 +101,7 @@ API.v1.addRoute( throw new Error('invalid-callId'); } - await Livechat.updateCallStatus(callId, rid, status, this.user); + await updateCallStatus(callId, rid, status, this.user); return API.v1.success({ status }); }, diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index 19f3aca1fc535..d685f3e59757a 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -6,10 +6,11 @@ import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../lib/callbacks'; import { API } from '../../../../api/server'; import { settings } from '../../../../settings/server'; -import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { validateRequiredCustomFields } from '../../lib/custom-fields'; -import { registerGuest, removeGuest } from '../../lib/guests'; +import { registerGuest, removeGuest, notifyGuestStatusChanged } from '../../lib/guests'; +import { livechatLogger } from '../../lib/logger'; import { saveRoomInfo } from '../../lib/rooms'; +import { updateCallStatus } from '../../lib/utils'; import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; API.v1.addRoute( @@ -111,7 +112,7 @@ API.v1.addRoute( ); if (processedKeys.length !== keys.length) { - LivechatTyped.logger.warn({ + livechatLogger.warn({ msg: 'Some custom fields were not processed', visitorId: visitor._id, missingKeys: keys.filter((key) => !processedKeys.includes(key)), @@ -119,7 +120,7 @@ API.v1.addRoute( } if (errors.length > 0) { - LivechatTyped.logger.error({ + livechatLogger.error({ msg: 'Error updating custom fields', visitorId: visitor._id, errors, @@ -237,7 +238,7 @@ API.v1.addRoute('livechat/visitor.callStatus', { if (!guest) { throw new Meteor.Error('invalid-token'); } - await LivechatTyped.updateCallStatus(callId, rid, callStatus, guest); + await updateCallStatus(callId, rid, callStatus, guest); return API.v1.success({ token, callStatus }); }, }); @@ -256,7 +257,7 @@ API.v1.addRoute('livechat/visitor.status', { throw new Meteor.Error('invalid-token'); } - await LivechatTyped.notifyGuestStatusChanged(token, status); + await notifyGuestStatusChanged(token, status); return API.v1.success({ token, status }); }, diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index b8b993d19dd9d..6db7408aeef0d 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -37,12 +37,11 @@ import { Meteor } from 'meteor/meteor'; import type { ClientSession } from 'mongodb'; import { ObjectId } from 'mongodb'; -import { Livechat as LivechatTyped } from './LivechatTyped'; import { queueInquiry, saveQueueInquiry } from './QueueManager'; import { RoutingManager } from './RoutingManager'; import { isVerifiedChannelInSource } from './contacts/isVerifiedChannelInSource'; import { migrateVisitorIfMissingContact } from './contacts/migrateVisitorIfMissingContact'; -import { getOnlineAgents } from './getOnlineAgents'; +import { checkOnlineAgents, getOnlineAgents } from './service-status'; import { saveTransferHistory } from './transfer'; import { callbacks } from '../../../../lib/callbacks'; import { validateEmail as validatorFunc } from '../../../../lib/emailValidator'; @@ -630,7 +629,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi if ( !RoutingManager.getConfig()?.autoAssignAgent || !(await Omnichannel.isWithinMACLimit(room)) || - (department?.allowReceiveForwardOffline && !(await LivechatTyped.checkOnlineAgents(departmentId))) + (department?.allowReceiveForwardOffline && !(await checkOnlineAgents(departmentId))) ) { logger.debug(`Room ${room._id} will be on department queue`); await saveTransferHistory(room, transferData); diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts deleted file mode 100644 index 5f7c7c9dcd58c..0000000000000 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { VideoConf } from '@rocket.chat/core-services'; -import type { IUser, ILivechatVisitor, ILivechatDepartment, UserStatus } from '@rocket.chat/core-typings'; -import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import { Logger } from '@rocket.chat/logger'; -import { LivechatDepartment, LivechatInquiry, LivechatRooms, Users, LivechatDepartmentAgents, Rooms } from '@rocket.chat/models'; -import { removeEmpty } from '@rocket.chat/tools'; -import { check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; - -import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; -import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; -import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; -import { updateMessage } from '../../../lib/server/functions/updateMessage'; -import { notifyOnLivechatInquiryChangedByToken } from '../../../lib/server/lib/notifyListener'; -import { settings } from '../../../settings/server'; -import { businessHourManager } from '../business-hour'; -import { updateDepartmentAgents } from './Helper'; -import { afterAgentAdded, afterRemoveAgent } from './hooks'; - -class LivechatClass { - logger: Logger; - - constructor() { - this.logger = new Logger('Livechat'); - } - - async online(department?: string, skipNoAgentSetting = false, skipFallbackCheck = false): Promise { - Livechat.logger.debug(`Checking online agents ${department ? `for department ${department}` : ''}`); - if (!skipNoAgentSetting && settings.get('Livechat_accept_chats_with_no_agents')) { - Livechat.logger.debug('Can accept without online agents: true'); - return true; - } - - if (settings.get('Livechat_assign_new_conversation_to_bot')) { - Livechat.logger.debug(`Fetching online bot agents for department ${department}`); - const botAgents = await Livechat.getBotAgents(department); - if (botAgents) { - const onlineBots = await Livechat.countBotAgents(department); - this.logger.debug(`Found ${onlineBots} online`); - if (onlineBots > 0) { - return true; - } - } - } - - const agentsOnline = await this.checkOnlineAgents(department, undefined, skipFallbackCheck); - Livechat.logger.debug(`Are online agents ${department ? `for department ${department}` : ''}?: ${agentsOnline}`); - return agentsOnline; - } - - async checkOnlineAgents(department?: string, agent?: { agentId: string }, skipFallbackCheck = false): Promise { - if (agent?.agentId) { - return Users.checkOnlineAgents(agent.agentId, settings.get('Livechat_enabled_when_agent_idle')); - } - - if (department) { - const onlineForDep = await LivechatDepartmentAgents.checkOnlineForDepartment(department); - if (onlineForDep || skipFallbackCheck) { - return onlineForDep; - } - - const dep = await LivechatDepartment.findOneById>(department, { - projection: { fallbackForwardDepartment: 1 }, - }); - if (!dep?.fallbackForwardDepartment) { - return onlineForDep; - } - - return this.checkOnlineAgents(dep?.fallbackForwardDepartment); - } - - return Users.checkOnlineAgents(undefined, settings.get('Livechat_enabled_when_agent_idle')); - } - - private async getBotAgents(department?: string) { - if (department) { - return LivechatDepartmentAgents.getBotsForDepartment(department); - } - - return Users.findBotAgents(); - } - - private async countBotAgents(department?: string) { - if (department) { - return LivechatDepartmentAgents.countBotsForDepartment(department); - } - - return Users.countBotAgents(); - } - - async saveAgentInfo(_id: string, agentData: any, agentDepartments: string[]) { - check(_id, String); - check(agentData, Object); - check(agentDepartments, [String]); - - const user = await Users.findOneById(_id); - if (!user || !(await hasRoleAsync(_id, 'livechat-agent'))) { - throw new Meteor.Error('error-user-is-not-agent', 'User is not a livechat agent'); - } - - await Users.setLivechatData(_id, removeEmpty(agentData)); - - const currentDepartmentsForAgent = await LivechatDepartmentAgents.findByAgentId(_id).toArray(); - - const toRemoveIds = currentDepartmentsForAgent - .filter((dept) => !agentDepartments.includes(dept.departmentId)) - .map((dept) => dept.departmentId); - const toAddIds = agentDepartments.filter((d) => !currentDepartmentsForAgent.some((c) => c.departmentId === d)); - - await Promise.all( - await LivechatDepartment.findInIds([...toRemoveIds, ...toAddIds], { - projection: { - _id: 1, - enabled: 1, - }, - }) - .map((dep) => { - return updateDepartmentAgents( - dep._id, - { - ...(toRemoveIds.includes(dep._id) ? { remove: [{ agentId: _id }] } : { upsert: [{ agentId: _id, count: 0, order: 0 }] }), - }, - dep.enabled, - ); - }) - .toArray(), - ); - - return true; - } - - async updateCallStatus(callId: string, rid: string, status: 'ended' | 'declined', user: IUser | ILivechatVisitor) { - await Rooms.setCallStatus(rid, status); - if (status === 'ended' || status === 'declined') { - if (await VideoConf.declineLivechatCall(callId)) { - return; - } - - return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date(), rid }, user as unknown as IUser); - } - } - - async removeAgent(username: string) { - const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); - - if (!user) { - throw new Error('error-invalid-user'); - } - - const { _id } = user; - - if (await removeUserFromRolesAsync(_id, ['livechat-agent'])) { - return afterRemoveAgent(user); - } - - return false; - } - - async removeManager(username: string) { - // TODO: we already validated user exists at this point, remove this check - const user = await Users.findOneByUsername(username, { projection: { _id: 1 } }); - - if (!user) { - throw new Error('error-invalid-user'); - } - - return removeUserFromRolesAsync(user._id, ['livechat-manager']); - } - - async allowAgentChangeServiceStatus(statusLivechat: ILivechatAgentStatus, agentId: string) { - if (statusLivechat !== ILivechatAgentStatus.AVAILABLE) { - return true; - } - - return businessHourManager.allowAgentChangeServiceStatus(agentId); - } - - async notifyGuestStatusChanged(token: string, status: UserStatus) { - await LivechatRooms.updateVisitorStatus(token, status); - - const inquiryVisitorStatus = await LivechatInquiry.updateVisitorStatus(token, status); - - if (inquiryVisitorStatus.modifiedCount) { - void notifyOnLivechatInquiryChangedByToken(token, 'updated', { v: { status } }); - } - } - - async addAgent(username: string) { - check(username, String); - - const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); - - if (!user) { - throw new Meteor.Error('error-invalid-user'); - } - - if (await addUserRolesAsync(user._id, ['livechat-agent'])) { - return afterAgentAdded(user); - } - - return false; - } - - async addManager(username: string) { - check(username, String); - - const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); - - if (!user) { - throw new Meteor.Error('error-invalid-user'); - } - - if (await addUserRolesAsync(user._id, ['livechat-manager'])) { - return user; - } - - return false; - } -} - -export const Livechat = new LivechatClass(); diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index 280836d1fea86..7460bc6bc8247 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -20,10 +20,9 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { createLivechatRoom, createLivechatInquiry, allowAgentSkipQueue, prepareLivechatRoom } from './Helper'; -import { Livechat } from './LivechatTyped'; import { RoutingManager } from './RoutingManager'; import { isVerifiedChannelInSource } from './contacts/isVerifiedChannelInSource'; -import { getOnlineAgents } from './getOnlineAgents'; +import { checkOnlineAgents, getOnlineAgents } from './service-status'; import { getInquirySortMechanismSetting } from './settings'; import { dispatchInquiryPosition } from '../../../../ee/app/livechat-enterprise/server/lib/Helper'; import { callbacks } from '../../../../lib/callbacks'; @@ -328,7 +327,7 @@ export class QueueManager { throw new Meteor.Error('no-agent-online', 'Sorry, no online agents'); } - if (!agent && !guest.department && !(await Livechat.checkOnlineAgents())) { + if (!agent && !guest.department && !(await checkOnlineAgents())) { throw new Meteor.Error('no-agent-online', 'Sorry, no online agents'); } } diff --git a/apps/meteor/app/livechat/server/lib/contacts/registerContact.spec.ts b/apps/meteor/app/livechat/server/lib/contacts/registerContact.spec.ts index 03562a881a9e4..9eb3499a8b175 100644 --- a/apps/meteor/app/livechat/server/lib/contacts/registerContact.spec.ts +++ b/apps/meteor/app/livechat/server/lib/contacts/registerContact.spec.ts @@ -37,13 +37,6 @@ const { registerContact } = proxyquire.noCallThru().load('./registerContact', { '@rocket.chat/models': modelsMock, '@rocket.chat/tools': { wrapExceptions: sinon.stub() }, './Helper': { validateEmail: sinon.stub() }, - './LivechatTyped': { - Livechat: { - logger: { - debug: sinon.stub(), - }, - }, - }, }); describe('registerContact', () => { diff --git a/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts b/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts deleted file mode 100644 index 69b89cdc1c907..0000000000000 --- a/apps/meteor/app/livechat/server/lib/getOnlineAgents.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { ILivechatAgent, SelectedAgent } from '@rocket.chat/core-typings'; -import { Users, LivechatDepartmentAgents } from '@rocket.chat/models'; -import type { FindCursor } from 'mongodb'; - -import { settings } from '../../../settings/server'; - -export async function getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> { - if (agent?.agentId) { - return Users.findOnlineAgents(agent.agentId, settings.get('Livechat_enabled_when_agent_idle')); - } - - if (department) { - const departmentAgents = await LivechatDepartmentAgents.getOnlineForDepartment(department); - if (!departmentAgents) { - return; - } - - const agentIds = await departmentAgents.map(({ agentId }) => agentId).toArray(); - if (!agentIds.length) { - return; - } - - return Users.findByIds([...new Set(agentIds)]); - } - return Users.findOnlineAgents(undefined, settings.get('Livechat_enabled_when_agent_idle')); -} diff --git a/apps/meteor/app/livechat/server/lib/guests.ts b/apps/meteor/app/livechat/server/lib/guests.ts index 1ab2a75f86a0a..92a6dbf9b7c15 100644 --- a/apps/meteor/app/livechat/server/lib/guests.ts +++ b/apps/meteor/app/livechat/server/lib/guests.ts @@ -1,5 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; -import type { ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { ILivechatVisitor, IOmnichannelRoom, UserStatus } from '@rocket.chat/core-typings'; import { LivechatVisitors, LivechatCustomField, @@ -24,7 +24,11 @@ import { trim } from '../../../../lib/utils/stringUtils'; import { i18n } from '../../../../server/lib/i18n'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; -import { notifyOnSubscriptionChanged, notifyOnLivechatInquiryChanged } from '../../../lib/server/lib/notifyListener'; +import { + notifyOnSubscriptionChanged, + notifyOnLivechatInquiryChanged, + notifyOnLivechatInquiryChangedByToken, +} from '../../../lib/server/lib/notifyListener'; export async function saveGuest( guestData: Pick & { email?: string; phone?: string }, @@ -224,3 +228,14 @@ export async function getLivechatRoomGuestInfo(room: IOmnichannelRoom) { return postData; } + +export async function notifyGuestStatusChanged(token: string, status: UserStatus) { + // TODO: a promise.all maybe? + await LivechatRooms.updateVisitorStatus(token, status); + + const inquiryVisitorStatus = await LivechatInquiry.updateVisitorStatus(token, status); + + if (inquiryVisitorStatus.modifiedCount) { + void notifyOnLivechatInquiryChangedByToken(token, 'updated', { v: { status } }); + } +} diff --git a/apps/meteor/app/livechat/server/lib/omni-users.ts b/apps/meteor/app/livechat/server/lib/omni-users.ts index c96bc84d3825f..3b144196b7b1e 100644 --- a/apps/meteor/app/livechat/server/lib/omni-users.ts +++ b/apps/meteor/app/livechat/server/lib/omni-users.ts @@ -1,8 +1,14 @@ import { api } from '@rocket.chat/core-services'; import type { UserStatus } from '@rocket.chat/core-typings'; -import { LivechatRooms } from '@rocket.chat/models'; +import { LivechatDepartment, LivechatDepartmentAgents, LivechatRooms, Users } from '@rocket.chat/models'; +import { removeEmpty } from '@rocket.chat/tools'; +import { updateDepartmentAgents } from './Helper'; +import { afterAgentAdded, afterRemoveAgent } from './hooks'; import { callbacks } from '../../../../lib/callbacks'; +import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; +import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles'; +import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { settings } from '../../../settings/server'; export async function notifyAgentStatusChanged(userId: string, status?: UserStatus) { @@ -22,3 +28,106 @@ export async function notifyAgentStatusChanged(userId: string, status?: UserStat }); }); } + +export async function addManager(username: string) { + // TODO: remove 'check' function call + check(username, String); + + const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); + + if (!user) { + throw new Meteor.Error('error-invalid-user'); + } + + if (await addUserRolesAsync(user._id, ['livechat-manager'])) { + return user; + } + + return false; +} + +export async function addAgent(username: string) { + check(username, String); + + const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); + + if (!user) { + throw new Meteor.Error('error-invalid-user'); + } + + if (await addUserRolesAsync(user._id, ['livechat-agent'])) { + return afterAgentAdded(user); + } + + return false; +} + +export async function removeAgent(username: string) { + // TODO: we already validated user exists at this point, remove this check + const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } }); + + if (!user) { + throw new Error('error-invalid-user'); + } + + const { _id } = user; + + if (await removeUserFromRolesAsync(_id, ['livechat-agent'])) { + return afterRemoveAgent(user); + } + + return false; +} + +export async function removeManager(username: string) { + // TODO: we already validated user exists at this point, remove this check + const user = await Users.findOneByUsername(username, { projection: { _id: 1 } }); + + if (!user) { + throw new Error('error-invalid-user'); + } + + return removeUserFromRolesAsync(user._id, ['livechat-manager']); +} + +export async function saveAgentInfo(_id: string, agentData: any, agentDepartments: string[]) { + // TODO: check if these 'check' functions are necessary + check(_id, String); + check(agentData, Object); + check(agentDepartments, [String]); + + const user = await Users.findOneById(_id); + if (!user || !(await hasRoleAsync(_id, 'livechat-agent'))) { + throw new Meteor.Error('error-user-is-not-agent', 'User is not a livechat agent'); + } + + await Users.setLivechatData(_id, removeEmpty(agentData)); + + const currentDepartmentsForAgent = await LivechatDepartmentAgents.findByAgentId(_id).toArray(); + + const toRemoveIds = currentDepartmentsForAgent + .filter((dept) => !agentDepartments.includes(dept.departmentId)) + .map((dept) => dept.departmentId); + const toAddIds = agentDepartments.filter((d) => !currentDepartmentsForAgent.some((c) => c.departmentId === d)); + + await Promise.all( + await LivechatDepartment.findInIds([...toRemoveIds, ...toAddIds], { + projection: { + _id: 1, + enabled: 1, + }, + }) + .map((dep) => { + return updateDepartmentAgents( + dep._id, + { + ...(toRemoveIds.includes(dep._id) ? { remove: [{ agentId: _id }] } : { upsert: [{ agentId: _id, count: 0, order: 0 }] }), + }, + dep.enabled, + ); + }) + .toArray(), + ); + + return true; +} diff --git a/apps/meteor/app/livechat/server/lib/service-status.ts b/apps/meteor/app/livechat/server/lib/service-status.ts new file mode 100644 index 0000000000000..65b9b606d6713 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/service-status.ts @@ -0,0 +1,83 @@ +import type { ILivechatAgent, ILivechatDepartment, SelectedAgent } from '@rocket.chat/core-typings'; +import { Users, LivechatDepartmentAgents, LivechatDepartment } from '@rocket.chat/models'; +import type { FindCursor } from 'mongodb'; + +import { livechatLogger } from './logger'; +import { settings } from '../../../settings/server'; + +export async function getOnlineAgents(department?: string, agent?: SelectedAgent | null): Promise | undefined> { + if (agent?.agentId) { + return Users.findOnlineAgents(agent.agentId, settings.get('Livechat_enabled_when_agent_idle')); + } + + if (department) { + const departmentAgents = await LivechatDepartmentAgents.getOnlineForDepartment(department); + if (!departmentAgents) { + return; + } + + const agentIds = await departmentAgents.map(({ agentId }) => agentId).toArray(); + if (!agentIds.length) { + return; + } + + return Users.findByIds([...new Set(agentIds)]); + } + return Users.findOnlineAgents(undefined, settings.get('Livechat_enabled_when_agent_idle')); +} + +export async function online(department?: string, skipNoAgentSetting = false, skipFallbackCheck = false): Promise { + livechatLogger.debug(`Checking online agents ${department ? `for department ${department}` : ''}`); + if (!skipNoAgentSetting && settings.get('Livechat_accept_chats_with_no_agents')) { + livechatLogger.debug('Can accept without online agents: true'); + return true; + } + + if (settings.get('Livechat_assign_new_conversation_to_bot')) { + livechatLogger.debug(`Fetching online bot agents for department ${department}`); + // get & count where doing the same, but get was getting data, while count was only counting. We only need the count here + const botAgents = await countBotAgents(department); + if (botAgents) { + livechatLogger.debug(`Found ${botAgents} online`); + if (botAgents > 0) { + return true; + } + } + } + + const agentsOnline = await checkOnlineAgents(department, undefined, skipFallbackCheck); + livechatLogger.debug(`Are online agents ${department ? `for department ${department}` : ''}?: ${agentsOnline}`); + return agentsOnline; +} + +export async function checkOnlineAgents(department?: string, agent?: { agentId: string }, skipFallbackCheck = false): Promise { + if (agent?.agentId) { + return Users.checkOnlineAgents(agent.agentId, settings.get('Livechat_enabled_when_agent_idle')); + } + + if (department) { + const onlineForDep = await LivechatDepartmentAgents.checkOnlineForDepartment(department); + if (onlineForDep || skipFallbackCheck) { + return onlineForDep; + } + + const dep = await LivechatDepartment.findOneById>(department, { + projection: { fallbackForwardDepartment: 1 }, + }); + if (!dep?.fallbackForwardDepartment) { + return onlineForDep; + } + + return checkOnlineAgents(dep?.fallbackForwardDepartment); + } + + return Users.checkOnlineAgents(undefined, settings.get('Livechat_enabled_when_agent_idle')); +} + +async function countBotAgents(department?: string) { + if (department) { + return LivechatDepartmentAgents.countBotsForDepartment(department); + } + + return Users.countBotAgents(); +} diff --git a/apps/meteor/app/livechat/server/lib/utils.ts b/apps/meteor/app/livechat/server/lib/utils.ts index a8399b8ecf681..dd03d1e3c9c46 100644 --- a/apps/meteor/app/livechat/server/lib/utils.ts +++ b/apps/meteor/app/livechat/server/lib/utils.ts @@ -1,11 +1,15 @@ -import type { ILivechatAgent, ILivechatAgentStatus, IUser } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; +import { VideoConf } from '@rocket.chat/core-services'; +import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; +import type { ILivechatAgent, ILivechatVisitor, IUser } from '@rocket.chat/core-typings'; +import { Rooms, Users } from '@rocket.chat/models'; import type { Filter } from 'mongodb'; import { RoutingManager } from './RoutingManager'; import type { AKeyOf } from './localTypes'; import { callbacks } from '../../../../lib/callbacks'; +import { updateMessage } from '../../../lib/server/functions/updateMessage'; import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener'; +import { businessHourManager } from '../business-hour'; export function showConnecting() { return RoutingManager.getConfig()?.showConnecting || false; @@ -50,3 +54,22 @@ export async function setUserStatusLivechatIf( callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); return result; } + +export async function allowAgentChangeServiceStatus(statusLivechat: ILivechatAgentStatus, agentId: string) { + if (statusLivechat !== ILivechatAgentStatus.AVAILABLE) { + return true; + } + + return businessHourManager.allowAgentChangeServiceStatus(agentId); +} + +export async function updateCallStatus(callId: string, rid: string, status: 'ended' | 'declined', user: IUser | ILivechatVisitor) { + await Rooms.setCallStatus(rid, status); + if (status === 'ended' || status === 'declined') { + if (await VideoConf.declineLivechatCall(callId)) { + return; + } + + return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date(), rid }, user as unknown as IUser); + } +} diff --git a/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts b/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts index 816bd77266d9c..e82b2e4d24687 100644 --- a/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts +++ b/apps/meteor/app/livechat/server/methods/changeLivechatStatus.ts @@ -5,8 +5,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { Livechat as LivechatTS } from '../lib/LivechatTyped'; -import { setUserStatusLivechat } from '../lib/utils'; +import { setUserStatusLivechat, allowAgentChangeServiceStatus } from '../lib/utils'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -63,7 +62,7 @@ Meteor.methods({ return setUserStatusLivechat(agentId, newStatus); } - if (!(await LivechatTS.allowAgentChangeServiceStatus(newStatus, agentId))) { + if (!(await allowAgentChangeServiceStatus(newStatus, agentId))) { throw new Meteor.Error('error-business-hours-are-closed', 'Not allowed', { method: 'livechat:changeLivechatStatus', }); diff --git a/apps/meteor/app/livechat/server/methods/saveAgentInfo.ts b/apps/meteor/app/livechat/server/methods/saveAgentInfo.ts index fe542b67156e7..215cb2b891a45 100644 --- a/apps/meteor/app/livechat/server/methods/saveAgentInfo.ts +++ b/apps/meteor/app/livechat/server/methods/saveAgentInfo.ts @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; -import { Livechat } from '../lib/LivechatTyped'; +import { saveAgentInfo } from '../lib/omni-users'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -29,6 +29,6 @@ Meteor.methods({ }); } - return Livechat.saveAgentInfo(_id, agentData, agentDepartments); + return saveAgentInfo(_id, agentData, agentDepartments); }, }); diff --git a/apps/meteor/app/livechat/server/methods/setUpConnection.ts b/apps/meteor/app/livechat/server/methods/setUpConnection.ts index 21ce09acaa229..d6f05c9af03f1 100644 --- a/apps/meteor/app/livechat/server/methods/setUpConnection.ts +++ b/apps/meteor/app/livechat/server/methods/setUpConnection.ts @@ -3,7 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { Livechat } from '../lib/LivechatTyped'; +import { notifyGuestStatusChanged } from '../lib/guests'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -33,7 +33,7 @@ Meteor.methods({ if (this.connection && !this.connection.livechatToken) { this.connection.livechatToken = token; this.connection.onClose(async () => { - await Livechat.notifyGuestStatusChanged(token, UserStatus.OFFLINE); + await notifyGuestStatusChanged(token, UserStatus.OFFLINE); }); } }, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts index 444d310b3c5f2..d2782ddee00e9 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.ts @@ -2,10 +2,10 @@ import { type ILivechatDepartment } from '@rocket.chat/core-typings'; import { LivechatDepartment, LivechatInquiry, LivechatRooms } from '@rocket.chat/models'; import { notifyOnLivechatInquiryChanged } from '../../../../../app/lib/server/lib/notifyListener'; -import { online } from '../../../../../app/livechat/server/api/lib/livechat'; import { allowAgentSkipQueue } from '../../../../../app/livechat/server/lib/Helper'; import { saveQueueInquiry } from '../../../../../app/livechat/server/lib/QueueManager'; import { setDepartmentForGuest } from '../../../../../app/livechat/server/lib/departmentsLib'; +import { online } from '../../../../../app/livechat/server/lib/service-status'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; import { cbLogger } from '../lib/logger'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts index 9722cf25b2009..2abdaf88957b8 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts @@ -1,7 +1,7 @@ import { Users } from '@rocket.chat/models'; import { allowAgentSkipQueue } from '../../../../../app/livechat/server/lib/Helper'; -import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped'; +import { checkOnlineAgents } from '../../../../../app/livechat/server/lib/service-status'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; import { getMaxNumberSimultaneousChat } from '../lib/Helper'; @@ -32,7 +32,7 @@ const validateMaxChats = async ({ } const { agentId } = agent; - if (!(await Livechat.checkOnlineAgents(undefined, agent))) { + if (!(await checkOnlineAgents(undefined, agent))) { cbLogger.debug('Provided agent is not online'); throw new Error('Provided agent is not online'); } diff --git a/apps/meteor/server/services/meteor/service.ts b/apps/meteor/server/services/meteor/service.ts index 1378e9d41466d..99fdb9b49dfb5 100644 --- a/apps/meteor/server/services/meteor/service.ts +++ b/apps/meteor/server/services/meteor/service.ts @@ -8,7 +8,7 @@ import { Meteor } from 'meteor/meteor'; import { MongoInternals } from 'meteor/mongo'; import { triggerHandler } from '../../../app/integrations/server/lib/triggerHandler'; -import { Livechat } from '../../../app/livechat/server/lib/LivechatTyped'; +import { notifyGuestStatusChanged } from '../../../app/livechat/server/lib/guests'; import { onlineAgents, monitorAgents } from '../../../app/livechat/server/lib/stream/agentStatus'; import { metrics } from '../../../app/metrics/server'; import notifications from '../../../app/notifications/server/lib/Notifications'; @@ -285,7 +285,7 @@ export class MeteorService extends ServiceClassInternal implements IMeteor { } async notifyGuestStatusChanged(token: string, status: UserStatus): Promise { - return Livechat.notifyGuestStatusChanged(token, status); + return notifyGuestStatusChanged(token, status); } async getURL(path: string, params: Record = {}, cloudDeepLinkUrl?: string): Promise { From 1eeb139158fcd621a2b8d3a7de5bb512e659261d Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Fri, 11 Apr 2025 15:13:53 -0300 Subject: [PATCH 105/187] feat: Security Logs Page (#35218) --- .changeset/lovely-waves-sniff.md | 9 + .../client/providers/SettingsProvider.tsx | 2 + .../header/actions/hooks/useAuditItems.tsx | 12 +- apps/meteor/client/startup/audit.tsx | 19 + .../client/views/audit/SecurityLogsPage.tsx | 20 + .../audit/components/AppInfoField.spec.tsx | 21 + .../audit/components/AppInfoField.stories.tsx | 89 ++++ .../views/audit/components/AppInfoField.tsx | 40 ++ .../audit/components/AuditModalField.tsx | 8 + .../audit/components/AuditModalLabel.tsx | 8 + .../views/audit/components/AuditModalText.tsx | 13 + .../SecurityLogDisplayModal.spec.tsx | 21 + .../SecurityLogDisplayModal.stories.tsx | 31 ++ .../components/SecurityLogDisplayModal.tsx | 85 ++++ .../audit/components/SecurityLogsTable.tsx | 219 ++++++++++ .../views/audit/components/SettingSelect.tsx | 34 ++ .../__snapshots__/AppInfoField.spec.tsx.snap | 77 ++++ .../SecurityLogDisplayModal.spec.tsx.snap | 412 ++++++++++++++++++ .../hooks/useSettingSelectOptions.spec.ts | 78 ++++ .../audit/hooks/useSettingSelectOptions.ts | 41 ++ .../audit/utils/getAppTypeTranslation.ts | 1 + packages/core-typings/src/IServerEvent.ts | 2 +- packages/i18n/src/locales/en.i18n.json | 17 + packages/i18n/src/locales/pt-BR.i18n.json | 7 + .../src/MockedAppRootBuilder.tsx | 10 +- packages/ui-contexts/src/SettingsContext.ts | 2 + 26 files changed, 1267 insertions(+), 11 deletions(-) create mode 100644 .changeset/lovely-waves-sniff.md create mode 100644 apps/meteor/client/views/audit/SecurityLogsPage.tsx create mode 100644 apps/meteor/client/views/audit/components/AppInfoField.spec.tsx create mode 100644 apps/meteor/client/views/audit/components/AppInfoField.stories.tsx create mode 100644 apps/meteor/client/views/audit/components/AppInfoField.tsx create mode 100644 apps/meteor/client/views/audit/components/AuditModalField.tsx create mode 100644 apps/meteor/client/views/audit/components/AuditModalLabel.tsx create mode 100644 apps/meteor/client/views/audit/components/AuditModalText.tsx create mode 100644 apps/meteor/client/views/audit/components/SecurityLogDisplayModal.spec.tsx create mode 100644 apps/meteor/client/views/audit/components/SecurityLogDisplayModal.stories.tsx create mode 100644 apps/meteor/client/views/audit/components/SecurityLogDisplayModal.tsx create mode 100644 apps/meteor/client/views/audit/components/SecurityLogsTable.tsx create mode 100644 apps/meteor/client/views/audit/components/SettingSelect.tsx create mode 100644 apps/meteor/client/views/audit/components/__snapshots__/AppInfoField.spec.tsx.snap create mode 100644 apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap create mode 100644 apps/meteor/client/views/audit/hooks/useSettingSelectOptions.spec.ts create mode 100644 apps/meteor/client/views/audit/hooks/useSettingSelectOptions.ts create mode 100644 apps/meteor/client/views/audit/utils/getAppTypeTranslation.ts diff --git a/.changeset/lovely-waves-sniff.md b/.changeset/lovely-waves-sniff.md new file mode 100644 index 0000000000000..33b01c482b78b --- /dev/null +++ b/.changeset/lovely-waves-sniff.md @@ -0,0 +1,9 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +"@rocket.chat/mock-providers": minor +"@rocket.chat/ui-client": minor +"@rocket.chat/ui-contexts": minor +--- + +Adds a new admin page to audit settings changes in a server diff --git a/apps/meteor/client/providers/SettingsProvider.tsx b/apps/meteor/client/providers/SettingsProvider.tsx index 06bce25bc7481..96f7c9c970fed 100644 --- a/apps/meteor/client/providers/SettingsProvider.tsx +++ b/apps/meteor/client/providers/SettingsProvider.tsx @@ -60,6 +60,8 @@ const SettingsProvider = ({ children }: SettingsProviderProps) => { sorter: 1, i18nLabel: 1, }, + ...('skip' in query && typeof query.skip === 'number' && { skip: query.skip }), + ...('limit' in query && typeof query.limit === 'number' && { limit: query.limit }), }, ) .fetch(), diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx index 1f02f3da31fd5..e284c147ba4c4 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx @@ -13,6 +13,7 @@ export const useAuditItems = (): GenericMenuItemProps[] => { const auditHomeRoute = useRoute('audit-home'); const auditSettingsRoute = useRoute('audit-log'); + const securityLogsRoute = useRoute('security-logs'); if (!hasAuditPermission && !hasAuditLogPermission) { return []; @@ -31,5 +32,14 @@ export const useAuditItems = (): GenericMenuItemProps[] => { onClick: () => auditSettingsRoute.push(), }; - return [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem].filter(Boolean) as GenericMenuItemProps[]; + const securityLogItem: GenericMenuItemProps = { + id: 'securityLog', + icon: 'document-eye', + content: t('Security_logs'), + onClick: () => securityLogsRoute.push(), + }; + + return [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem, hasAuditLogPermission && securityLogItem].filter( + Boolean, + ) as GenericMenuItemProps[]; }; diff --git a/apps/meteor/client/startup/audit.tsx b/apps/meteor/client/startup/audit.tsx index a0efee0c45d0f..30031114f2562 100644 --- a/apps/meteor/client/startup/audit.tsx +++ b/apps/meteor/client/startup/audit.tsx @@ -5,11 +5,13 @@ import { hasAllPermission } from '../../app/authorization/client'; import { appLayout } from '../lib/appLayout'; import { onToggledFeature } from '../lib/onToggledFeature'; import { router } from '../providers/RouterProvider'; +import SettingsProvider from '../providers/SettingsProvider'; import NotAuthorizedPage from '../views/notAuthorized/NotAuthorizedPage'; import MainLayout from '../views/root/MainLayout'; const AuditPage = lazy(() => import('../views/audit/AuditPage')); const AuditLogPage = lazy(() => import('../views/audit/AuditLogPage')); +const SecurityLogsPage = lazy(() => import('../views/audit/SecurityLogsPage')); declare module '@rocket.chat/ui-contexts' { interface IRouterPaths { @@ -21,6 +23,10 @@ declare module '@rocket.chat/ui-contexts' { pathname: '/audit-log'; pattern: '/audit-log'; }; + 'security-logs': { + pathname: '/security-logs'; + pattern: '/security-logs'; + }; } } @@ -57,6 +63,19 @@ onToggledFeature('auditing', { , ), }, + { + path: '/security-logs', + id: 'security-logs', + element: appLayout.wrap( + + + + + + + , + ), + }, ]); }, down: () => { diff --git a/apps/meteor/client/views/audit/SecurityLogsPage.tsx b/apps/meteor/client/views/audit/SecurityLogsPage.tsx new file mode 100644 index 0000000000000..ae6a67db259ca --- /dev/null +++ b/apps/meteor/client/views/audit/SecurityLogsPage.tsx @@ -0,0 +1,20 @@ +import { type ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import SecurityLogsTable from './components/SecurityLogsTable'; +import { Page, PageHeader, PageContent } from '../../components/Page'; + +const SecurityLogsPage = (): ReactElement => { + const { t } = useTranslation(); + + return ( + + + + + + + ); +}; + +export default SecurityLogsPage; diff --git a/apps/meteor/client/views/audit/components/AppInfoField.spec.tsx b/apps/meteor/client/views/audit/components/AppInfoField.spec.tsx new file mode 100644 index 0000000000000..cfa92559fdcfc --- /dev/null +++ b/apps/meteor/client/views/audit/components/AppInfoField.spec.tsx @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './AppInfoField.stories'; + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); + +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const view = render(, { wrapper: mockAppRoot().build() }); + expect(view.baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(, { wrapper: mockAppRoot().build() }); + + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); diff --git a/apps/meteor/client/views/audit/components/AppInfoField.stories.tsx b/apps/meteor/client/views/audit/components/AppInfoField.stories.tsx new file mode 100644 index 0000000000000..b5a29e47427be --- /dev/null +++ b/apps/meteor/client/views/audit/components/AppInfoField.stories.tsx @@ -0,0 +1,89 @@ +import type { AppSubscriptionStatus } from '@rocket.chat/core-typings'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import type { Meta, StoryFn } from '@storybook/react'; + +import { AppInfoField } from './AppInfoField'; + +export default { + title: 'views/Audit/AppInfoField', + component: AppInfoField, + args: { + appId: 'app-id', + }, + decorators: [ + mockAppRoot() + .withEndpoint('GET', '/apps/:id', () => ({ + app: { + name: 'App Name', + id: '', + iconFileData: '', + appRequestStats: { + appId: '', + totalSeen: 0, + totalUnseen: 0, + }, + author: { + name: '', + homepage: '', + support: '', + }, + description: '', + privacyPolicySummary: '', + detailedDescription: { + raw: '', + rendered: '', + }, + detailedChangelog: { + raw: '', + rendered: '', + }, + categories: [], + version: '', + price: 0, + purchaseType: 'buy' as const, + pricingPlans: [], + iconFileContent: '', + isSubscribed: false, + bundledIn: [], + marketplaceVersion: '', + // Recursive typem expect an App type here + latest: undefined as any, + subscriptionInfo: { + typeOf: '', + status: 'Active' as AppSubscriptionStatus, + statusFromBilling: false, + isSeatBased: false, + seats: 0, + maxSeats: 0, + license: { + license: '', + version: 0, + expireDate: '', + }, + startDate: '', + periodEnd: '', + endDate: '', + externallyManaged: false, + isSubscribedViaBundle: false, + }, + tosLink: '', + privacyLink: '', + modifiedAt: '', + permissions: [], + languages: [], + createdDate: '', + private: false, + documentationUrl: '', + migrated: false, + }, + })) + .withTranslations('en', 'core', { App_name: 'App Name' }) + .buildStoryDecorator(), + ], +} satisfies Meta; + +export const Default: StoryFn = (args) => ; + +export const NoAppInfo: StoryFn = (args) => ; + +NoAppInfo.decorators = [mockAppRoot().withTranslations('en', 'core', { App_id: 'App Id' }).buildStoryDecorator()]; diff --git a/apps/meteor/client/views/audit/components/AppInfoField.tsx b/apps/meteor/client/views/audit/components/AppInfoField.tsx new file mode 100644 index 0000000000000..18541b31fa002 --- /dev/null +++ b/apps/meteor/client/views/audit/components/AppInfoField.tsx @@ -0,0 +1,40 @@ +import { Skeleton } from '@rocket.chat/fuselage'; +import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; + +import AuditModalField from './AuditModalField'; +import AuditModalLabel from './AuditModalLabel'; +import AuditModalText from './AuditModalText'; + +type AppInfoFieldProps = { + appId: string; +}; + +// This is a separate component to encapsulate its logic and in the future expand it to a field that shows more info on the App +export const AppInfoField = ({ appId }: AppInfoFieldProps) => { + const t = useTranslation(); + + const getAppInfo = useEndpoint('GET', `/apps/:id`, { id: appId }); + + const { data, isLoading, isSuccess } = useQuery({ + queryKey: ['getAppInfo', appId], + + queryFn: async () => { + return getAppInfo(); + }, + }); + + return ( + <> + + {t('Actor')} + {t('App')} + + + {isSuccess && data ? t('App_name') : t('App_id')} + {isLoading && } + {isSuccess && data ? {data.app.name} : {appId}} + + + ); +}; diff --git a/apps/meteor/client/views/audit/components/AuditModalField.tsx b/apps/meteor/client/views/audit/components/AuditModalField.tsx new file mode 100644 index 0000000000000..178260d432f20 --- /dev/null +++ b/apps/meteor/client/views/audit/components/AuditModalField.tsx @@ -0,0 +1,8 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +type AuditModalFieldProps = ComponentPropsWithoutRef; + +const AuditModalField = (props: AuditModalFieldProps) => ; + +export default AuditModalField; diff --git a/apps/meteor/client/views/audit/components/AuditModalLabel.tsx b/apps/meteor/client/views/audit/components/AuditModalLabel.tsx new file mode 100644 index 0000000000000..3b2c58cffeb85 --- /dev/null +++ b/apps/meteor/client/views/audit/components/AuditModalLabel.tsx @@ -0,0 +1,8 @@ +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +type AuditModalLabelProps = ComponentPropsWithoutRef; + +const AuditModalLabel = (props: AuditModalLabelProps) => ; + +export default AuditModalLabel; diff --git a/apps/meteor/client/views/audit/components/AuditModalText.tsx b/apps/meteor/client/views/audit/components/AuditModalText.tsx new file mode 100644 index 0000000000000..04f898d578d81 --- /dev/null +++ b/apps/meteor/client/views/audit/components/AuditModalText.tsx @@ -0,0 +1,13 @@ +import { css } from '@rocket.chat/css-in-js'; +import { Box } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; + +const wordBreak = css` + word-break: break-word; +`; + +type AuditModalTextProps = ComponentPropsWithoutRef; + +const AuditModalText = (props: AuditModalTextProps) => ; + +export default AuditModalText; diff --git a/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.spec.tsx b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.spec.tsx new file mode 100644 index 0000000000000..1a3898ee9b8c2 --- /dev/null +++ b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.spec.tsx @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './SecurityLogDisplayModal.stories'; + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); + +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const view = render(, { wrapper: mockAppRoot().build() }); + expect(view.baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(, { wrapper: mockAppRoot().build() }); + + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); diff --git a/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.stories.tsx b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.stories.tsx new file mode 100644 index 0000000000000..bb9a7b952706c --- /dev/null +++ b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.stories.tsx @@ -0,0 +1,31 @@ +import type { Meta, StoryFn } from '@storybook/react'; + +import SecurityLogDisplayModal from './SecurityLogDisplayModal'; + +export default { + title: 'views/Audit/SecurityLogDisplay', + component: SecurityLogDisplayModal, + args: { + timestamp: 'Thursday, 20-Mar-25 17:17:46', + actor: { + type: 'user', + _id: 'user-id', + username: 'username', + useragent: 'useragent', + ip: '127.0.0.1', + }, + setting: 'Show_message_in_email_notification', + changedFrom: 'false', + changedTo: 'true', + }, +} satisfies Meta; + +export const Default: StoryFn = (args) => ; + +export const system: StoryFn = (args) => ( + +); + +export const app: StoryFn = (args) => ( + +); diff --git a/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.tsx b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.tsx new file mode 100644 index 0000000000000..ae7d23ff9b401 --- /dev/null +++ b/apps/meteor/client/views/audit/components/SecurityLogDisplayModal.tsx @@ -0,0 +1,85 @@ +import type { IAuditServerUserActor, IAuditServerSystemActor, IAuditServerAppActor } from '@rocket.chat/core-typings'; +import { Box } from '@rocket.chat/fuselage'; +import { UserAvatar } from '@rocket.chat/ui-avatar'; +import { format } from 'date-fns'; +import { useTranslation } from 'react-i18next'; + +import { AppInfoField } from './AppInfoField'; +import AuditModalField from './AuditModalField'; +import AuditModalLabel from './AuditModalLabel'; +import AuditModalText from './AuditModalText'; +import GenericModal from '../../../components/GenericModal'; + +type SecurityLogDisplayProps = { + timestamp: string; + actor: IAuditServerUserActor | IAuditServerSystemActor | IAuditServerAppActor; + setting: string; + changedFrom: string; + changedTo: string; + onCancel: () => void; +}; + +const SecurityLogDisplayModal = ({ timestamp, actor, setting, changedFrom, changedTo, onCancel }: SecurityLogDisplayProps) => { + const { t } = useTranslation(); + + return ( + + + {t('Timestamp')} + {format(new Date(timestamp), 'MMMM d yyyy, h:mm:ss a')} + + + {actor.type === 'user' && ( + + {t('Actor')} + + {actor.type === 'user' && } + + {actor.username} + + + + )} + + {actor.type === 'app' && } + + {actor.type === 'system' && ( + <> + + {t('Actor')} + {t('System')} + + + + {t('Reason')} + {actor.reason} + + + )} + + + {t('Setting')} + {t(setting)} + + + + {t('Changed_from')} + {changedFrom} + + + + {t('Changed_to')} + {changedTo} + + + ); +}; + +export default SecurityLogDisplayModal; diff --git a/apps/meteor/client/views/audit/components/SecurityLogsTable.tsx b/apps/meteor/client/views/audit/components/SecurityLogsTable.tsx new file mode 100644 index 0000000000000..f22fa54a7e709 --- /dev/null +++ b/apps/meteor/client/views/audit/components/SecurityLogsTable.tsx @@ -0,0 +1,219 @@ +import type { IAuditServerAppActor, IAuditServerSystemActor, IAuditServerUserActor } from '@rocket.chat/core-typings'; +import { Box, Button, ButtonGroup, Field, FieldLabel, Margins, Pagination } from '@rocket.chat/fuselage'; +import { UserAvatar } from '@rocket.chat/ui-avatar'; +import { useEndpoint, useSetModal } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import { format } from 'date-fns'; +import { useState, type ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import SecurityLogDisplayModal from './SecurityLogDisplayModal'; +import { SettingSelect } from './SettingSelect'; +import DateRangePicker from './forms/DateRangePicker'; +import GenericNoResults from '../../../components/GenericNoResults'; +import { + GenericTable, + GenericTableBody, + GenericTableCell, + GenericTableHeader, + GenericTableHeaderCell, + GenericTableLoadingRow, + GenericTableRow, +} from '../../../components/GenericTable'; +import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; +import type { DateRange } from '../utils/dateRange'; +import { getTypeTranslation } from '../utils/getAppTypeTranslation'; + +const SecurityLogsTable = (): ReactElement => { + const { t } = useTranslation(); + const [setting, setSetting] = useState(''); + + const setModal = useSetModal(); + + const [dateRange, setDateRange] = useState(() => ({ + start: undefined, + end: undefined, + })); + + const [query, setQuery] = useState({ + start: new Date(0).toISOString(), + end: new Date().toISOString(), + settingId: '', + }); + + const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); + + const handleClearFilters = () => { + setSetting(''); + setDateRange({ start: undefined, end: undefined }); + setQuery({ + start: new Date(0).toISOString(), + end: new Date().toISOString(), + settingId: '', + }); + onSetCurrent(0); + }; + + const handleApplyFilters = () => { + const { start, end } = dateRange; + setQuery({ + start: start?.toISOString() ?? new Date(0).toISOString(), + end: end?.toISOString() ?? new Date().toISOString(), + settingId: setting, + }); + onSetCurrent(0); + }; + + const handleItemClick = ({ + actor, + timestamp, + setting, + changedFrom, + changedTo, + }: { + actor: IAuditServerUserActor | IAuditServerSystemActor | IAuditServerAppActor; + timestamp: string; + setting: unknown; + changedFrom: string; + changedTo: string; + }) => { + setModal( + setModal(null)} + />, + ); + }; + + const getAudits = useEndpoint('GET', '/v1/audit.settings'); + + const { data, isLoading, isSuccess } = useQuery({ + queryKey: ['audit.settings', query, itemsPerPage, current], + + queryFn: async () => { + return getAudits({ ...query, ...(itemsPerPage && { count: itemsPerPage }), ...(current && { offset: current }) }); + }, + }); + + return ( + <> + + + + {t('Date')} + + + + + + {t('Setting')} + + + + + + + + + + + + {isLoading && ( + + + {t('Actor')} + {t('Timestamp')} + {t('Setting')} + {t('Changed_from')} + {t('Changed_to')} + + + + + + )} + {isSuccess && data.total === 0 && ( + + )} + {isSuccess && data.total > 0 && ( + + + {t('Actor')} + {t('Timestamp')} + {t('Setting')} + {t('Changed_from')} + {t('Changed_to')} + + + {data.events.map((item) => { + const setting = item.data.find((item) => item.key === 'id')?.value; + const previous = item.data.find((item) => item.key === 'previous')?.value || t('Empty'); + const current = item.data.find((item) => item.key === 'current')?.value || t('Empty'); + return ( + + handleItemClick({ + actor: item.actor, + timestamp: new Date(item.ts).toDateString(), + setting, + changedFrom: String(previous), + changedTo: String(current), + }) + } + > + + + {item.actor.type === 'user' && ( + + + + )} + + {item.actor.type === 'user' ? item.actor.username : t(getTypeTranslation(item.actor.type))} + + + + {format(new Date(item.ts), 'MMMM d yyyy, h:mm:ss a')} + + {setting && String(setting)} + + {String(previous)} + {String(current)} + + ); + })} + + + )} + + + ); +}; + +export default SecurityLogsTable; diff --git a/apps/meteor/client/views/audit/components/SettingSelect.tsx b/apps/meteor/client/views/audit/components/SettingSelect.tsx new file mode 100644 index 0000000000000..2baa9c4958d75 --- /dev/null +++ b/apps/meteor/client/views/audit/components/SettingSelect.tsx @@ -0,0 +1,34 @@ +import { Option, PaginatedSelectFiltered } from '@rocket.chat/fuselage'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useSettingSelectOptions } from '../hooks/useSettingSelectOptions'; + +export const SettingSelect = ({ value, onChange }: { value: string; onChange: (value: string) => void }) => { + const { t } = useTranslation(); + const [filter, setFilter] = useState(''); + + const debouncedFilter = useDebouncedValue(filter, 500); + + const { data, fetchNextPage, isFetchingNextPage } = useSettingSelectOptions(debouncedFilter); + const flattenedData = data?.pages.flatMap((page) => page) || []; + + return ( + onChange(val)} + placeholder={t('All_Settings')} + filter={filter} + setFilter={setFilter as (value: string | number | undefined) => void} + options={flattenedData} + endReached={() => !isFetchingNextPage && fetchNextPage({ cancelRefetch: true })} + renderItem={({ label, ...props }) => ( + + )} + /> + ); +}; diff --git a/apps/meteor/client/views/audit/components/__snapshots__/AppInfoField.spec.tsx.snap b/apps/meteor/client/views/audit/components/__snapshots__/AppInfoField.spec.tsx.snap new file mode 100644 index 0000000000000..872c71313ed24 --- /dev/null +++ b/apps/meteor/client/views/audit/components/__snapshots__/AppInfoField.spec.tsx.snap @@ -0,0 +1,77 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders Default without crashing 1`] = ` + +
+
+
+ Actor +
+
+ App +
+
+
+
+ App_id +
+ +
+ app-id +
+
+
+ +`; + +exports[`renders NoAppInfo without crashing 1`] = ` + +
+
+
+ Actor +
+
+ App +
+
+
+
+ App Id +
+ +
+ app-id +
+
+
+ +`; diff --git a/apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap b/apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap new file mode 100644 index 0000000000000..50fc75fba27a3 --- /dev/null +++ b/apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap @@ -0,0 +1,412 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders Default without crashing 1`] = ` + +
+ +
+
+
+
+

+ Setting_change +

+
+
+
+
+
+
+
+ Timestamp +
+
+ March 20 2025, 5:17:46 PM +
+
+
+
+ Actor +
+
+
+ +
+
+ username +
+
+
+
+
+ Setting +
+
+ Show_message_in_email_notification +
+
+
+
+ Changed_from +
+
+ false +
+
+
+
+ Changed_to +
+
+ true +
+
+
+
+ +
+
+ +`; + +exports[`renders app without crashing 1`] = ` + +
+ +
+
+
+
+

+ Setting_change +

+
+
+
+
+
+
+
+ Timestamp +
+
+ March 20 2025, 5:17:46 PM +
+
+
+
+ Actor +
+
+ App +
+
+
+
+ App_id +
+ +
+ app-id +
+
+
+
+ Setting +
+
+ Show_message_in_email_notification +
+
+
+
+ Changed_from +
+
+ false +
+
+
+
+ Changed_to +
+
+ true +
+
+
+
+ +
+
+ +`; + +exports[`renders system without crashing 1`] = ` + +
+ +
+
+
+
+

+ Setting_change +

+
+
+
+
+
+
+
+ Timestamp +
+
+ March 20 2025, 5:17:46 PM +
+
+
+
+ Actor +
+
+ System +
+
+
+
+ Reason +
+
+ update +
+
+
+
+ Setting +
+
+ Show_message_in_email_notification +
+
+
+
+ Changed_from +
+
+ false +
+
+
+
+ Changed_to +
+
+ true +
+
+
+
+ +
+
+ +`; diff --git a/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.spec.ts b/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.spec.ts new file mode 100644 index 0000000000000..3a72f659c91e3 --- /dev/null +++ b/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.spec.ts @@ -0,0 +1,78 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook, waitFor } from '@testing-library/react'; + +import { useSettingSelectOptions } from './useSettingSelectOptions'; + +// TODO: check if the return of items matches the settings we mocked +describe('useSettingSelectOptions', () => { + it('should return the ordered list of options', async () => { + const { result } = renderHook(() => useSettingSelectOptions(), { + wrapper: mockAppRoot() + .withSetting('test1', true) + .withSetting('test2', true) + .withSetting('test3', true) + .withSetting('test4', true) + .withSetting('test5', true) + .withSetting('test6', true) + .withSetting('test7', true) + .withSetting('test8', true) + .withSetting('test9', true) + .withSetting('test10', true) + .withSetting('test11', true) + .withSetting('test12', true) + .withSetting('test13', true) + .withSetting('test14', true) + .withSetting('test15', true) + .build(), + }); + + await waitFor(() => expect(result.current?.data?.pages[0][0]).toEqual({ _id: 'test1', label: 'test1', value: 'test1' })); + await waitFor(() => expect(result.current?.data?.pages[0][1]).toEqual({ _id: 'test2', label: 'test2', value: 'test2' })); + await waitFor(() => expect(result.current?.data?.pages[0][2]).toEqual({ _id: 'test3', label: 'test3', value: 'test3' })); + await waitFor(() => expect(result.current?.data?.pages[0][3]).toEqual({ _id: 'test4', label: 'test4', value: 'test4' })); + await waitFor(() => expect(result.current?.data?.pages[0][4]).toEqual({ _id: 'test5', label: 'test5', value: 'test5' })); + await waitFor(() => expect(result.current?.data?.pages[0][5]).toEqual({ _id: 'test6', label: 'test6', value: 'test6' })); + await waitFor(() => expect(result.current?.data?.pages[0][6]).toEqual({ _id: 'test7', label: 'test7', value: 'test7' })); + await waitFor(() => expect(result.current?.data?.pages[0][7]).toEqual({ _id: 'test8', label: 'test8', value: 'test8' })); + await waitFor(() => expect(result.current?.data?.pages[0][8]).toEqual({ _id: 'test9', label: 'test9', value: 'test9' })); + await waitFor(() => expect(result.current?.data?.pages[0][9]).toEqual({ _id: 'test10', label: 'test10', value: 'test10' })); + await waitFor(() => expect(result.current?.data?.pages[0][10]).toEqual({ _id: 'test11', label: 'test11', value: 'test11' })); + await waitFor(() => expect(result.current?.data?.pages[0][11]).toEqual({ _id: 'test12', label: 'test12', value: 'test12' })); + await waitFor(() => expect(result.current?.data?.pages[0][12]).toEqual({ _id: 'test13', label: 'test13', value: 'test13' })); + await waitFor(() => expect(result.current?.data?.pages[0][13]).toEqual({ _id: 'test14', label: 'test14', value: 'test14' })); + await waitFor(() => expect(result.current?.data?.pages[0][14]).toEqual({ _id: 'test15', label: 'test15', value: 'test15' })); + + await waitFor(() => expect(result.current?.data?.pages[0]).toHaveLength(15)); + }); + + it('should return the list of filtered options', async () => { + const { result } = renderHook(() => useSettingSelectOptions('TeSt1'), { + wrapper: mockAppRoot() + .withSetting('test1', true) + .withSetting('test2', true) + .withSetting('test3', true) + .withSetting('test4', true) + .withSetting('test5', true) + .withSetting('test6', true) + .withSetting('test7', true) + .withSetting('test8', true) + .withSetting('test9', true) + .withSetting('test10', true) + .withSetting('test11', true) + .withSetting('test12', true) + .withSetting('test13', true) + .withSetting('test14', true) + .withSetting('test15', true) + .build(), + }); + + await waitFor(() => expect(result.current?.data?.pages[0][0]).toEqual({ _id: 'test1', label: 'test1', value: 'test1' })); + await waitFor(() => expect(result.current?.data?.pages[0][1]).toEqual({ _id: 'test10', label: 'test10', value: 'test10' })); + await waitFor(() => expect(result.current?.data?.pages[0][2]).toEqual({ _id: 'test11', label: 'test11', value: 'test11' })); + await waitFor(() => expect(result.current?.data?.pages[0][3]).toEqual({ _id: 'test12', label: 'test12', value: 'test12' })); + await waitFor(() => expect(result.current?.data?.pages[0][4]).toEqual({ _id: 'test13', label: 'test13', value: 'test13' })); + await waitFor(() => expect(result.current?.data?.pages[0][5]).toEqual({ _id: 'test14', label: 'test14', value: 'test14' })); + await waitFor(() => expect(result.current?.data?.pages[0][6]).toEqual({ _id: 'test15', label: 'test15', value: 'test15' })); + await waitFor(() => expect(result.current?.data?.pages[0]).toHaveLength(7)); + }); +}); diff --git a/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.ts b/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.ts new file mode 100644 index 0000000000000..e7342690b13a7 --- /dev/null +++ b/apps/meteor/client/views/audit/hooks/useSettingSelectOptions.ts @@ -0,0 +1,41 @@ +import { useSettings } from '@rocket.chat/ui-contexts'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +type SettingSelectOption = { + label: string; + value: string; + _id: string; +}; + +export const useSettingSelectOptions = (filter = '') => { + const settings = useSettings(); + + const fetchData = useCallback( + async (start = 0): Promise => { + return settings + .map(({ _id }) => ({ label: _id, value: _id, _id })) + .filter(({ label }) => label.toUpperCase().includes(filter.toUpperCase())) + .slice(start, start + 50); + }, + [filter, settings], + ); + + return useInfiniteQuery({ + queryKey: ['settings', filter], + queryFn: ({ pageParam }) => fetchData(pageParam), + getNextPageParam: (lastPage, _allPages, lastPageParam) => { + if (lastPage.length === 0) { + return undefined; + } + return lastPageParam + 1; + }, + getPreviousPageParam: (_firstPage, _allPages, firstPageParam) => { + if (firstPageParam <= 1) { + return undefined; + } + return firstPageParam - 1; + }, + initialPageParam: 0, + }); +}; diff --git a/apps/meteor/client/views/audit/utils/getAppTypeTranslation.ts b/apps/meteor/client/views/audit/utils/getAppTypeTranslation.ts new file mode 100644 index 0000000000000..e225053a8afdc --- /dev/null +++ b/apps/meteor/client/views/audit/utils/getAppTypeTranslation.ts @@ -0,0 +1 @@ +export const getTypeTranslation = (type: 'app' | 'system') => (type === 'app' ? 'App' : 'System'); diff --git a/packages/core-typings/src/IServerEvent.ts b/packages/core-typings/src/IServerEvent.ts index 2f4348260ae81..4bf19d1236193 100644 --- a/packages/core-typings/src/IServerEvent.ts +++ b/packages/core-typings/src/IServerEvent.ts @@ -42,9 +42,9 @@ export interface IAuditServerAppActor { export type IAuditServerActor = IAuditServerUserActor | IAuditServerSystemActor | IAuditServerAppActor; interface IAuditServerEvent { + _id: string; t: string; ts: Date; - actor: IAuditServerActor; } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index fee5d3ae26462..a524a44b339ca 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -553,6 +553,7 @@ "Application_updated": "Application updated", "Apply": "Apply", "Apply_and_refresh_all_clients": "Apply and refresh all clients", + "Apply_filters": "Apply filters", "Apps": "Apps", "Apps_Engine_Version": "Apps Engine Version", "API_Enable_Rate_Limiter": "Enable Rate Limiter", @@ -1095,6 +1096,8 @@ "color": "Color", "changed_room_announcement_to__room_announcement_": "changed room announcement to: {{room_announcement}}", "changed_room_description_to__room_description_": "changed room description to: {{room_description}}", + "Changed_from": "Changed from", + "Changed_to": "Changed to", "Color": "Color", "Colors": "Colors", "change-livechat-room-visitor": "Change Livechat Room Visitors", @@ -4147,6 +4150,7 @@ "Microphone": "Microphone", "Microphone_access_not_allowed": "Microphone access was not allowed, please check your browser settings.", "RealName_Change_Disabled": "Your Rocket.Chat administrator has disabled the changing of names", + "Reason": "Reason", "Reason_To_Join": "Reason to Join", "Mic_off": "Mic Off", "Receive_alerts": "Receive alerts", @@ -4675,7 +4679,9 @@ "Pages": "Pages", "set-readonly_description": "Permission to set a channel to read only channel", "Settings": "Settings", + "Setting": "Setting", "Parent_channel_or_team": "Parent channel or team", + "Setting_change": "Setting change", "Settings_updated": "Settings updated", "Participants": "Participants", "Setup_Wizard": "Setup Wizard", @@ -4913,6 +4919,7 @@ "Queue_delay_timeout": "Queue processing delay timeout", "Queue_Time": "Queue Time", "System_messages": "System Messages", + "System": "System", "Queue_management": "Queue Management", "Tag": "Tag", "Quick_reactions": "Quick reactions", @@ -5579,6 +5586,7 @@ "Show_mentions": "Show badge for mentions", "Accept_receive_inquiry_no_online_agents": "Allow department to receive forwarded inquiries even when there's no available agents", "Accept_receive_inquiry_no_online_agents_Hint": "This method is effective only with automatic assignment routing methods, and does not apply to Manual Selection.", + "Actor": "Actor", "view-livechat-manager": "View Omnichannel Manager", "Show_Only_This_Content": "Show only this content", "view-livechat-manager_description": "Permission to view other Omnichannel managers", @@ -5919,6 +5927,7 @@ "Timeout_in_miliseconds": "Timeout (in miliseconds)", "Timeout_in_miliseconds_cant_be_negative_number": "Timeout (in miliseconds) can't a negative number", "Timeout_in_miliseconds_hint": "The time in milliseconds to wait for an external service to respond before canceling the request.", + "Timestamp": "Timestamp", "Timezone": "Timezone", "To_prevent_seeing_this_message_again_allow_popups_from_workspace_URL": "To prevent seeing this message again, make sure your browser settings allow pop-ups to be opened from the workspace URL: ", "toggle-room-e2e-encryption": "Toggle Room E2E Encryption", @@ -5963,6 +5972,7 @@ "Troubleshoot_Force_Caching_Version": "Force browsers to clear networking cache based on version change", "Troubleshoot_Force_Caching_Version_Alert": "If the value provided is not empty and different from previous one the browsers will try to clear the cache. This setting should not be set for a long period since it affects the browser performance, please clear it as soon as possible.", "Try_now": "Try now", + "Try_different_filters": "Try different filters", "Try_searching_in_the_marketplace_instead": "Try searching in the Marketplace instead", "Turn_on_video": "Turn on video", "Turn_on_answer_chats": "Turn on answer chats", @@ -6075,6 +6085,9 @@ "Value_messages": "{{value}} messages", "Value_users": "{{value}} users", "Version_version": "Version {{version}}", + "App": "App", + "App_id": "App Id", + "App_name": "App name", "App_Request_Admin_Message": "Hi {{admin_name}}, {{user_name}} submitted a request to install {{app_name}} app on this workspace. \n \n This is the message they included: \n>{{message}} \n \n To learn more and install the {{app_name}} app, [click here]({{learn_more}}).", "App_version_incompatible_tooltip": "App incompatible with Rocket.Chat version", "App_request_enduser_message": "The app you requested, {{appName}}, has just been installed on this workspace. \n [Click here]({{learnmore}}) to learn about the app.", @@ -6571,6 +6584,7 @@ "Disconnect_workspace": "Disconnect workspace", "Awaiting_confirmation": "Awaiting confirmation", "Security_code": "Security code", + "Security_logs": "Security logs", "Registration_Token": "Registration Token", "RegisterWorkspace_Button": "Register workspace", "ConnectWorkspace_Button": "Connect workspace", @@ -6621,6 +6635,7 @@ "App_will_lose_grandfathered_status": "**This app will lose its app limit policy exemption.** \n \nWorkspaces on Community can have up to {{limit}} apps enabled. Uninstalling this app will cause it to lose its exemption policy.", "App_will_lose_grandfathered_status_private": "**This app will lose its app limit policy exemption.** \n \nBecause Community workspaces cannot enable private apps, this workspace will require a premium plan in order to enable this app again in future.", "All_rooms": "All rooms", + "All_Settings": "All Settings", "All_visible": "All visible", "all": "all", "Filter_by_room": "Filter by room type", @@ -6767,6 +6782,8 @@ "Advanced_settings": "Advanced settings", "Security_and_permissions": "Security and permissions", "Security_and_privacy": "Security and privacy", + "Security_Log_App": "App ( {{appId}} )", + "Security_Log_System": "System ( {{reason}} )", "Sidepanel_navigation": "Secondary navigation for teams", "Sidepanel_navigation_description": "Display channels and/or discussions associated with teams by default. This allows team owners to customize communication methods to best meet their team’s needs. This is currently in feature preview and will be a premium capability once fully released.", "Show_channels_description": "Show team channels in second sidebar", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index 67f050a51caba..006928d91cb2f 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -72,6 +72,7 @@ "A_new_owner_will_be_assigned_automatically_to_those__count__rooms__rooms__": "Um novo proprietário será atribuído automaticamente a estas {{count}} salas:
{{rooms}}.", "A_secure_and_highly_private_self-managed_solution_for_conference_calls": "Uma solução autogerenciada segura e altamente privada para chamadas em conferência.", "A_workspace_admin_needs_to_install_and_configure_a_conference_call_app": "Um administrador do workspace precisa instalar e configurar um aplicativo de chamada de vídeo.", + "Actor": "Autor", "Accept": "Aceitar", "Accept_Call": "Aceitar chamada", "Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Aceitar solicitações de omnichannel de entrada mesmo que não tenham agentes online", @@ -400,6 +401,7 @@ "Answer_call": "Receber chamada", "Apiai_Key": "Api.ai Key", "Apiai_Language": "Idioma Api.ai", + "App": "Aplicativo", "App_Details": "Detalhes do aplicativo", "App_Info": "Informação do aplicativo", "App_Information": "Informações do aplicativo", @@ -758,6 +760,8 @@ "Categories*": "Categorias*", "Certificates_and_Keys": "Certificados e chaves", "Change_Room_Type": "Mudando o Tipo de Sala", + "Changed_from": "Mudou de", + "Changed_to": "Mudou para", "Changing_email": "Alterando e-mail", "Channel": "Canal", "Channel_Archived": "Canal com o nome `#%s` foi arquivado com sucesso", @@ -3528,7 +3532,9 @@ "Set_as_owner": "Definir como proprietário", "Set_random_password_and_send_by_email": "Definir senha aleatória e enviar por e-mail", "Settings": "Configurações", + "Setting": "Configuração", "Settings_updated": "Configurações atualizadas", + "Setting_change": "Configuração alterada", "Setup_Wizard": "Assistente de configuração", "Setup_Wizard_Description": "Informações básicas do seu workspace como organização, nome e país.", "Setup_Wizard_Info": "Vamos apoiar na configuração do seu primeiro usuário administrador, na configuração da sua organização e no registro do servidor, para que possa receber notificações push gratuitas e muito mais.", @@ -3699,6 +3705,7 @@ "Sync_in_progress": "Sincronização em andamento", "Sync_success": "Sincronizado com sucesso", "System_messages": "Mensagens do sistema", + "System": "Sistema", "TOTP Invalid [totp-invalid]": "Código ou senha invalida", "TOTP_Reset_Other_Key_Warning": "Redefinir o TOTP de dois fatores atual vai desconectar o usuário. O usuário poderá definir os dois fatores mais tarde novamente.", "TOTP_reset_email": "Notificação de redefinição TOTP de dois fatores", diff --git a/packages/mock-providers/src/MockedAppRootBuilder.tsx b/packages/mock-providers/src/MockedAppRootBuilder.tsx index 4b557b392f650..9446f29c4251f 100644 --- a/packages/mock-providers/src/MockedAppRootBuilder.tsx +++ b/packages/mock-providers/src/MockedAppRootBuilder.tsx @@ -13,7 +13,7 @@ import { Emitter } from '@rocket.chat/emitter'; import languages from '@rocket.chat/i18n/dist/languages'; import { createFilterFromQuery } from '@rocket.chat/mongo-adapter'; import type { Method, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings'; -import type { Device, ModalContextValue, SubscriptionWithRoom, TranslationKey } from '@rocket.chat/ui-contexts'; +import type { Device, ModalContextValue, SettingsContextQuery, SubscriptionWithRoom, TranslationKey } from '@rocket.chat/ui-contexts'; import { AuthorizationContext, ConnectionStatusContext, @@ -42,13 +42,6 @@ type Mutable = { -readonly [P in keyof T]: T[P]; }; -export type SettingsContextQuery = { - readonly _id?: ISetting['_id'][] | RegExp; - readonly group?: ISetting['_id']; - readonly section?: string; - readonly tab?: ISetting['_id']; -}; - // eslint-disable-next-line @typescript-eslint/naming-convention interface MockedAppRootEvents { 'update-modal': void; @@ -417,7 +410,6 @@ export class MockedAppRootBuilder { const filter = cache.get(query) ?? createFilterFromQuery({ - ...query, ...(query._id ? { _id: { $in: query._id } } : {}), } as any); cache.set(query, filter); diff --git a/packages/ui-contexts/src/SettingsContext.ts b/packages/ui-contexts/src/SettingsContext.ts index a4f11b3235f6a..95e364de2c748 100644 --- a/packages/ui-contexts/src/SettingsContext.ts +++ b/packages/ui-contexts/src/SettingsContext.ts @@ -6,6 +6,8 @@ export type SettingsContextQuery = { readonly group?: ISetting['_id']; readonly section?: string; readonly tab?: ISetting['_id']; + readonly skip?: number; + readonly limit?: number; }; export type SettingsContextValue = { From 4b28126ac94cf1d3312b30ad9863ca02673f49d4 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Fri, 11 Apr 2025 15:58:59 -0300 Subject: [PATCH 106/187] feat: sync federated users from LDAP (#35718) Co-authored-by: Marcos Spessatto Defendi <15324204+MarcosSpessatto@users.noreply.github.com> --- .changeset/stupid-rabbits-hide.md | 7 +++ .../classes/converters/UserConverter.ts | 10 +++- apps/meteor/server/lib/ldap/Manager.ts | 48 +++++++++++++++++++ apps/meteor/server/settings/ldap.ts | 5 ++ .../core-typings/src/import/IImportUser.ts | 1 + packages/i18n/src/locales/en.i18n.json | 2 + 6 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 .changeset/stupid-rabbits-hide.md diff --git a/.changeset/stupid-rabbits-hide.md b/.changeset/stupid-rabbits-hide.md new file mode 100644 index 0000000000000..341fea6af5a5d --- /dev/null +++ b/.changeset/stupid-rabbits-hide.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/core-typings': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Adds a new setting to allow syncing federated users data through LDAP diff --git a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts index 03f853832ef51..0ff4f72a6f1c8 100644 --- a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts @@ -259,6 +259,10 @@ export class UserConverter extends RecordConverter[0]); + const localUsername = userData.federated ? undefined : userData.username; + if (userData.name || localUsername) { + await saveUserIdentity({ _id, name: userData.name, username: localUsername } as Parameters[0]); } if (userData.importIds.length) { @@ -347,6 +352,7 @@ export class UserConverter extends RecordConverter email.trim()); const name = this.getLdapName(ldapUser) || undefined; const voipExtension = this.getLdapExtension(ldapUser); @@ -182,6 +193,10 @@ export class LDAPManager { id, }, }, + ...(homeServer && { + username: `${username}:${homeServer}`, + federated: true, + }), }; this.onMapUserData(ldapUser, userData); @@ -500,6 +515,39 @@ export class LDAPManager { return this.getLdapDynamicValue(ldapUser, usernameField); } + protected static getFederationHomeServer(ldapUser: ILDAPEntry): string | undefined { + if (!settings.get('Federation_Matrix_enabled')) { + return; + } + + const homeServerField = settings.get('LDAP_FederationHomeServer_Field'); + const homeServer = this.getLdapDynamicValue(ldapUser, homeServerField); + + if (!homeServer) { + return; + } + + logger.debug({ msg: 'User has a federation home server', homeServer }); + + const localServer = settings.get('Federation_Matrix_homeserver_domain'); + if (localServer === homeServer) { + return; + } + + return homeServer; + } + + protected static getFederatedUsername(ldapUser: ILDAPEntry, requestUsername: string): string { + const username = this.slugifyUsername(ldapUser, requestUsername); + const homeServer = this.getFederationHomeServer(ldapUser); + + if (homeServer) { + return `${username}:${homeServer}`; + } + + return username; + } + // This method will find existing users by LDAP id or by username. private static async findExistingUser(ldapUser: ILDAPEntry, slugifiedUsername: string): Promise { const user = await this.findExistingLDAPUser(ldapUser); diff --git a/apps/meteor/server/settings/ldap.ts b/apps/meteor/server/settings/ldap.ts index d0d77d4ec3456..c492c94b69504 100644 --- a/apps/meteor/server/settings/ldap.ts +++ b/apps/meteor/server/settings/ldap.ts @@ -214,6 +214,11 @@ export const createLdapSettings = () => type: 'string', enableQuery, }); + + await this.add('LDAP_FederationHomeServer_Field', '', { + type: 'string', + enableQuery, + }); }); await this.section('LDAP_DataSync_Avatar', async function () { diff --git a/packages/core-typings/src/import/IImportUser.ts b/packages/core-typings/src/import/IImportUser.ts index de3b7806a300d..66841937cd90b 100644 --- a/packages/core-typings/src/import/IImportUser.ts +++ b/packages/core-typings/src/import/IImportUser.ts @@ -19,4 +19,5 @@ export interface IImportUser { password?: string; voipExtension?: string; + federated?: boolean; } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index a524a44b339ca..6b62f7aef1025 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3014,6 +3014,8 @@ "LDAP_Enable_Description": "Attempt to utilize LDAP for authentication.", "LDAP_Encryption": "Encryption", "LDAP_Encryption_Description": "The encryption method used to secure communications to the LDAP server. Examples include `plain` (no encryption), `SSL/LDAPS` (encrypted from the start), and `StartTLS` (upgrade to encrypted communication once connected).", + "LDAP_FederationHomeServer_Field": "Federation Home Server field", + "LDAP_FederationHomeServer_Field_Description": "The Home Server can only be assigned on user creation. Changing this will have no effect on users that were already synced.", "If_you_didnt_try_to_login_in_your_account_please_ignore_this_email": "If you didn't try to login in your account please ignore this email.", "LDAP_Find_User_After_Login": "Find user after login", "LDAP_Find_User_After_Login_Description": "Will perform a search of the user's DN after bind to ensure the bind was successful preventing login with empty passwords when allowed by the AD configuration.", From 282bfdb2889b76050b26c7388360c33323da3620 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Fri, 11 Apr 2025 16:42:10 -0300 Subject: [PATCH 107/187] chore: remove useragent-ng workaround (#35786) --- apps/meteor/.docker/Dockerfile.alpine | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index e098d21722a5d..ce2e8163caf14 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -48,13 +48,6 @@ RUN cd /app/bundle/programs/server \ # && npm install isolated-vm@4.6.0 \ # && mv node_modules/isolated-vm npm/node_modules/isolated-vm \ # # End hack for isolated-vm - # TODO: remove with meteor 3.1.2 - && cd /tmp \ - && npm install useragent-ng@2.4.4 --no-save \ - && rm -rf /app/bundle/programs/server/npm/node_modules/meteor/webapp/node_modules/useragent-ng \ - && mv node_modules/useragent-ng /app/bundle/programs/server/npm/node_modules/meteor/webapp/node_modules/useragent-ng \ - && rm -rf /tmp/node_modules \ - # end workaround for useragent-ng/meteor<3.1.2 && cd /app/bundle/programs/server/npm \ && npm rebuild bcrypt --build-from-source \ && npm cache clear --force From a585b308ae4e1b6df0cc19d10b6861f70e962a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Fri, 11 Apr 2025 17:03:36 -0300 Subject: [PATCH 108/187] refactor: remove emoji-custom from meteor (#35737) --- apps/meteor/app/emoji-custom/client/index.ts | 1 - .../hooks/customEmoji/useCustomEmoji.ts | 55 +++++++++ apps/meteor/client/importPackages.ts | 1 - .../lib/customEmoji.ts} | 112 ++++++------------ .../EmojiPickerProvider.tsx | 2 + .../useUpdateCustomEmoji.ts | 2 +- 6 files changed, 93 insertions(+), 80 deletions(-) delete mode 100644 apps/meteor/app/emoji-custom/client/index.ts create mode 100644 apps/meteor/client/hooks/customEmoji/useCustomEmoji.ts rename apps/meteor/{app/emoji-custom/client/lib/emojiCustom.ts => client/lib/customEmoji.ts} (76%) diff --git a/apps/meteor/app/emoji-custom/client/index.ts b/apps/meteor/app/emoji-custom/client/index.ts deleted file mode 100644 index 780a12a3898f5..0000000000000 --- a/apps/meteor/app/emoji-custom/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './lib/emojiCustom'; diff --git a/apps/meteor/client/hooks/customEmoji/useCustomEmoji.ts b/apps/meteor/client/hooks/customEmoji/useCustomEmoji.ts new file mode 100644 index 0000000000000..8b8400145d97f --- /dev/null +++ b/apps/meteor/client/hooks/customEmoji/useCustomEmoji.ts @@ -0,0 +1,55 @@ +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import { useEffect } from 'react'; + +import { emoji } from '../../../app/emoji/client'; +import { customRender } from '../../lib/customEmoji'; + +export const useCustomEmoji = () => { + const getCustomEmojis = useEndpoint('GET', '/v1/emoji-custom.list'); + const result = useQuery({ + queryKey: ['emoji-custom.list'], + queryFn: () => getCustomEmojis({ query: '' }), + }); + + useEffect(() => { + emoji.packages.emojiCustom = { + emojiCategories: [{ key: 'rocket', i18n: 'Custom' }], + categoryIndex: 1, + toneList: {}, + list: [], + _regexpSignature: null, + _regexp: null, + emojisByCategory: { rocket: [] }, + render: customRender, + renderPicker: customRender, + }; + + if (result.isError) { + console.error('Error getting custom emoji ', result.error); + } + + if (result.isSuccess) { + const { + emojis: { update: customEmojis }, + } = result.data; + + const addCustomEmojis = () => { + for (const currentEmoji of customEmojis) { + emoji.packages.emojiCustom.emojisByCategory.rocket.push(currentEmoji.name); + emoji.packages.emojiCustom.list?.push(`:${currentEmoji.name}:`); + emoji.list[`:${currentEmoji.name}:`] = { ...currentEmoji, emojiPackage: 'emojiCustom' } as any; + for (const alias of currentEmoji.aliases) { + emoji.packages.emojiCustom.list?.push(`:${alias}:`); + emoji.list[`:${alias}:`] = { + emojiPackage: 'emojiCustom', + aliasOf: currentEmoji.name, + }; + } + } + emoji.dispatchUpdate(); + }; + addCustomEmojis(); + } + }, [result.data, result.error, result.isError, result.isSuccess]); +}; diff --git a/apps/meteor/client/importPackages.ts b/apps/meteor/client/importPackages.ts index d82f6460d024f..556f77a4146ae 100644 --- a/apps/meteor/client/importPackages.ts +++ b/apps/meteor/client/importPackages.ts @@ -3,7 +3,6 @@ import '../app/authorization/client'; import '../app/autotranslate/client'; import '../app/emoji/client'; import '../app/emoji-emojione/client'; -import '../app/emoji-custom/client'; import '../app/gitlab/client'; import '../app/iframe-login/client'; import '../app/license/client'; diff --git a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts b/apps/meteor/client/lib/customEmoji.ts similarity index 76% rename from apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts rename to apps/meteor/client/lib/customEmoji.ts index bb76f7388c179..e1f040af64912 100644 --- a/apps/meteor/app/emoji-custom/client/lib/emojiCustom.ts +++ b/apps/meteor/client/lib/customEmoji.ts @@ -1,11 +1,8 @@ import type { IEmoji } from '@rocket.chat/core-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { Meteor } from 'meteor/meteor'; -import { onLoggedIn } from '../../../../client/lib/loggedIn'; -import { emoji, removeFromRecent, replaceEmojiInRecent } from '../../../emoji/client'; -import { getURL } from '../../../utils/client'; -import { sdk } from '../../../utils/client/lib/SDKClient'; +import { emoji, removeFromRecent, replaceEmojiInRecent } from '../../app/emoji/client'; +import { getURL } from '../../app/utils/client'; const isSetNotNull = (fn: () => unknown) => { let value; @@ -17,38 +14,6 @@ const isSetNotNull = (fn: () => unknown) => { return value !== null && value !== undefined; }; -const getEmojiUrlFromName = (name: string, extension: string, etag?: string) => { - if (!name) { - return; - } - - return getURL(`/emoji-custom/${encodeURIComponent(name)}.${extension}${etag ? `?etag=${etag}` : ''}`); -}; - -export const deleteEmojiCustom = (emojiData: IEmoji) => { - delete emoji.list[`:${emojiData.name}:`]; - const arrayIndex = emoji.packages.emojiCustom.emojisByCategory.rocket.indexOf(emojiData.name); - if (arrayIndex !== -1) { - emoji.packages.emojiCustom.emojisByCategory.rocket.splice(arrayIndex, 1); - } - const arrayIndexList = emoji.packages.emojiCustom.list?.indexOf(`:${emojiData.name}:`) ?? -1; - if (arrayIndexList !== -1) { - emoji.packages.emojiCustom.list?.splice(arrayIndexList, 1); - } - if (emojiData.aliases) { - for (const alias of emojiData.aliases) { - delete emoji.list[`:${alias}:`]; - const aliasIndex = emoji.packages.emojiCustom.list?.indexOf(`:${alias}:`) ?? -1; - if (aliasIndex !== -1) { - emoji.packages.emojiCustom.list?.splice(aliasIndex, 1); - } - } - } - - removeFromRecent(emojiData.name, emoji.packages.base.emojisByCategory.recent); - emoji.dispatchUpdate(); -}; - export const updateEmojiCustom = (emojiData: IEmoji) => { const previousExists = isSetNotNull(() => emojiData.previousName); const currentAliases = isSetNotNull(() => emojiData.aliases); @@ -98,7 +63,39 @@ export const updateEmojiCustom = (emojiData: IEmoji) => { emoji.dispatchUpdate(); }; -const customRender = (html: string) => { +export const deleteEmojiCustom = (emojiData: IEmoji) => { + delete emoji.list[`:${emojiData.name}:`]; + const arrayIndex = emoji.packages.emojiCustom.emojisByCategory.rocket.indexOf(emojiData.name); + if (arrayIndex !== -1) { + emoji.packages.emojiCustom.emojisByCategory.rocket.splice(arrayIndex, 1); + } + const arrayIndexList = emoji.packages.emojiCustom.list?.indexOf(`:${emojiData.name}:`) ?? -1; + if (arrayIndexList !== -1) { + emoji.packages.emojiCustom.list?.splice(arrayIndexList, 1); + } + if (emojiData.aliases) { + for (const alias of emojiData.aliases) { + delete emoji.list[`:${alias}:`]; + const aliasIndex = emoji.packages.emojiCustom.list?.indexOf(`:${alias}:`) ?? -1; + if (aliasIndex !== -1) { + emoji.packages.emojiCustom.list?.splice(aliasIndex, 1); + } + } + } + + removeFromRecent(emojiData.name, emoji.packages.base.emojisByCategory.recent); + emoji.dispatchUpdate(); +}; + +const getEmojiUrlFromName = (name: string, extension: string, etag?: string) => { + if (!name) { + return; + } + + return getURL(`/emoji-custom/${encodeURIComponent(name)}.${extension}${etag ? `?etag=${etag}` : ''}`); +}; + +export const customRender = (html: string) => { const emojisMatchGroup = emoji.packages.emojiCustom.list?.map(escapeRegExp).join('|'); if (emojisMatchGroup !== emoji.packages.emojiCustom._regexpSignature) { emoji.packages.emojiCustom._regexpSignature = emojisMatchGroup; @@ -131,42 +128,3 @@ const customRender = (html: string) => { return html; }; - -emoji.packages.emojiCustom = { - emojiCategories: [{ key: 'rocket', i18n: 'Custom' }], - categoryIndex: 1, - toneList: {}, - list: [], - _regexpSignature: null, - _regexp: null, - emojisByCategory: {}, - render: customRender, - renderPicker: customRender, -}; - -Meteor.startup(() => { - onLoggedIn(async () => { - try { - const { - emojis: { update: emojis }, - } = await sdk.rest.get('/v1/emoji-custom.list', { query: '' }); - - emoji.packages.emojiCustom.emojisByCategory = { rocket: [] }; - for (const currentEmoji of emojis) { - emoji.packages.emojiCustom.emojisByCategory.rocket.push(currentEmoji.name); - emoji.packages.emojiCustom.list?.push(`:${currentEmoji.name}:`); - emoji.list[`:${currentEmoji.name}:`] = { ...currentEmoji, emojiPackage: 'emojiCustom' } as any; - for (const alias of currentEmoji.aliases) { - emoji.packages.emojiCustom.list?.push(`:${alias}:`); - emoji.list[`:${alias}:`] = { - emojiPackage: 'emojiCustom', - aliasOf: currentEmoji.name, - }; - } - } - emoji.dispatchUpdate(); - } catch (e) { - console.error('Error getting custom emoji', e); - } - }); -}); diff --git a/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx index 70ed30697649b..d574fbff845bb 100644 --- a/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx +++ b/apps/meteor/client/providers/EmojiPickerProvider/EmojiPickerProvider.tsx @@ -5,6 +5,7 @@ import { useState, useCallback, useMemo, useSyncExternalStore } from 'react'; import { useUpdateCustomEmoji } from './useUpdateCustomEmoji'; import { emoji, getFrequentEmoji, createEmojiListByCategorySubscription } from '../../../app/emoji/client'; import { EmojiPickerContext } from '../../contexts/EmojiPickerContext'; +import { useCustomEmoji } from '../../hooks/customEmoji/useCustomEmoji'; import EmojiPicker from '../../views/composer/EmojiPicker/EmojiPicker'; const DEFAULT_ITEMS_LIMIT = 90; @@ -34,6 +35,7 @@ const EmojiPickerProvider = ({ children }: { children: ReactNode }): ReactElemen const [emojiListByCategory, categoriesIndexes] = useSyncExternalStore(sub, getSnapshot); + useCustomEmoji(); useUpdateCustomEmoji(); const addFrequentEmojis = useCallback( diff --git a/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts b/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts index 67d9f5bd20780..19c13fbce33c2 100644 --- a/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts +++ b/apps/meteor/client/providers/EmojiPickerProvider/useUpdateCustomEmoji.ts @@ -1,7 +1,7 @@ import { useStream, useUserId } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { updateEmojiCustom, deleteEmojiCustom } from '../../../app/emoji-custom/client/lib/emojiCustom'; +import { updateEmojiCustom, deleteEmojiCustom } from '../../lib/customEmoji'; export const useUpdateCustomEmoji = () => { const notify = useStream('notify-logged'); From dd87ca90cda5f7cab7822c8f404cbc1fc9a6af50 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 11 Apr 2025 17:51:05 -0300 Subject: [PATCH 109/187] fix: await api startup to prevent apps-engine startup problems (#35762) --- .changeset/dirty-seas-explode.md | 5 +++++ apps/meteor/ee/server/startup/index.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/dirty-seas-explode.md diff --git a/.changeset/dirty-seas-explode.md b/.changeset/dirty-seas-explode.md new file mode 100644 index 0000000000000..3b7a205ff7a94 --- /dev/null +++ b/.changeset/dirty-seas-explode.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue with apps loading in microservices in some cases by ensuring all services have started before trying to load apps diff --git a/apps/meteor/ee/server/startup/index.ts b/apps/meteor/ee/server/startup/index.ts index 07fbab3961937..bb65bac500fca 100644 --- a/apps/meteor/ee/server/startup/index.ts +++ b/apps/meteor/ee/server/startup/index.ts @@ -14,7 +14,7 @@ export const registerEEBroker = async (): Promise => { const { startBroker } = await import('@rocket.chat/network-broker'); api.setBroker(startBroker()); - void api.start(); + await api.start(); } else { require('./presence'); } From 35384ef95b0721d0d0d11bf111f6f70e31e42c8d Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 11 Apr 2025 18:49:32 -0300 Subject: [PATCH 110/187] chore(sdk): add methods to connect and disconnect (#35788) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Júlia Jaeger Foresti <60678893+juliajforesti@users.noreply.github.com> --- apps/meteor/app/utils/client/lib/SDKClient.ts | 10 ++++++++++ apps/meteor/client/providers/ServerProvider.tsx | 2 ++ apps/meteor/tests/mocks/client/ServerProviderMock.tsx | 2 ++ packages/mock-providers/src/MockedAppRootBuilder.tsx | 4 +++- .../ui-contexts/src/ServerContext/ServerContext.ts | 8 ++++++++ 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/utils/client/lib/SDKClient.ts b/apps/meteor/app/utils/client/lib/SDKClient.ts index 9113976435377..d064f52e9ef74 100644 --- a/apps/meteor/app/utils/client/lib/SDKClient.ts +++ b/apps/meteor/app/utils/client/lib/SDKClient.ts @@ -249,12 +249,22 @@ export const createSDK = (rest: RestClientInterface) => { return Meteor.callAsync(method, ...args); }; + const disconnect = () => { + Meteor.disconnect(); + }; + + const reconnect = () => { + Meteor.reconnect(); + }; + return { rest, stop: stopAll, stream, publish, call, + disconnect, + reconnect, }; }; diff --git a/apps/meteor/client/providers/ServerProvider.tsx b/apps/meteor/client/providers/ServerProvider.tsx index d6f6920016c43..85df8de5f6e51 100644 --- a/apps/meteor/client/providers/ServerProvider.tsx +++ b/apps/meteor/client/providers/ServerProvider.tsx @@ -75,6 +75,8 @@ const contextValue = { callEndpoint, uploadToEndpoint, getStream, + disconnect: () => Meteor.disconnect(), + reconnect: () => Meteor.reconnect(), }; type ServerProviderProps = { children?: ReactNode }; diff --git a/apps/meteor/tests/mocks/client/ServerProviderMock.tsx b/apps/meteor/tests/mocks/client/ServerProviderMock.tsx index 5638bc39160de..5c04ed43d4d25 100644 --- a/apps/meteor/tests/mocks/client/ServerProviderMock.tsx +++ b/apps/meteor/tests/mocks/client/ServerProviderMock.tsx @@ -67,6 +67,8 @@ const contextValue = { callEndpoint, uploadToEndpoint, getStream, + reconnect: () => undefined, + disconnect: () => undefined, }; type ServerProviderMockProps = { diff --git a/packages/mock-providers/src/MockedAppRootBuilder.tsx b/packages/mock-providers/src/MockedAppRootBuilder.tsx index 9446f29c4251f..50815c1f692fa 100644 --- a/packages/mock-providers/src/MockedAppRootBuilder.tsx +++ b/packages/mock-providers/src/MockedAppRootBuilder.tsx @@ -78,6 +78,8 @@ export class MockedAppRootBuilder { getStream: () => () => () => undefined, uploadToEndpoint: () => Promise.reject(new Error('not implemented')), callMethod: () => Promise.reject(new Error('not implemented')), + disconnect: () => Promise.reject(new Error('not implemented')), + reconnect: () => Promise.reject(new Error('not implemented')), info: undefined, }; @@ -646,7 +648,7 @@ export class MockedAppRootBuilder { - {/* + {/* */} diff --git a/packages/ui-contexts/src/ServerContext/ServerContext.ts b/packages/ui-contexts/src/ServerContext/ServerContext.ts index e5b7fd63c1c5d..c4ae8c3ebcde3 100644 --- a/packages/ui-contexts/src/ServerContext/ServerContext.ts +++ b/packages/ui-contexts/src/ServerContext/ServerContext.ts @@ -44,6 +44,8 @@ export type ServerContextValue = { retransmitToSelf?: boolean | undefined; }, ) => (eventName: K, callback: (...args: StreamerCallbackArgs) => void) => () => void; + disconnect: () => void; + reconnect: () => void; }; export const ServerContext = createContext({ @@ -56,4 +58,10 @@ export const ServerContext = createContext({ throw new Error('not implemented'); }, getStream: () => () => (): void => undefined, + disconnect: () => { + throw new Error('not implemented'); + }, + reconnect: () => { + throw new Error('not implemented'); + }, }); From 2ee1a81de770a682f6e7a8590a896e76a32f4e3c Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 14 Apr 2025 11:18:03 -0600 Subject: [PATCH 111/187] fix: PDF Worker markdown processing (#35705) --- .changeset/poor-spies-hug.md | 5 + .../ChatTranscript/components/Message.tsx | 39 +- .../ChatTranscript/components/utils.spec.ts | 48 ++ .../ChatTranscript/components/utils.ts | 19 + .../src/templates/ChatTranscript/index.tsx | 2 +- .../markup/blocks/CodeBlock.tsx | 2 +- .../markup/blocks/OrderedListBlock.tsx | 2 +- .../markup/blocks/UnorderedListBlock.tsx | 2 +- ee/packages/pdf-worker/src/worker.fixtures.ts | 673 ++++++++++++++++++ ee/packages/pdf-worker/src/worker.spec.ts | 8 + 10 files changed, 787 insertions(+), 13 deletions(-) create mode 100644 .changeset/poor-spies-hug.md create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.spec.ts create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.ts diff --git a/.changeset/poor-spies-hug.md b/.changeset/poor-spies-hug.md new file mode 100644 index 0000000000000..59b57b1f2dc28 --- /dev/null +++ b/.changeset/poor-spies-hug.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/pdf-worker": patch +--- + +Fixes an issue with PDF generation process that caused the server to hang when a single message consisted of too many (+30) markdown elements and was followed and preceded by more messages. diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Message.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Message.tsx index 5b6bf2fb6bd92..78338f8ab55f2 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Message.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Message.tsx @@ -7,6 +7,7 @@ import { Divider } from './Divider'; import { Files } from './Files'; import { MessageHeader } from './MessageHeader'; import { Quotes } from './Quotes'; +import { isSystemMessage, markupEntriesGreaterThan10, messageLongerThanPage, splitByTens } from './utils'; const styles = StyleSheet.create({ wrapper: { @@ -23,20 +24,40 @@ const styles = StyleSheet.create({ }, }); -const messageLongerThanPage = (message: string | undefined) => (message?.length ?? 0) > 1200; +const processMd = (message: PDFMessage) => + splitByTens(message.md).map((chunk, index) => ( + + } /> + + )); -const isSystemMessage = (message: PDFMessage) => !!message.t; +const processMessage = (message: PDFMessage) => { + if (message.md) { + if (markupEntriesGreaterThan10(message.md)) { + return processMd(message); + } -const Message = ({ message, invalidFileMessage }: { message: PDFMessage; invalidFileMessage: string }) => ( - - - {message.divider && } - + return ( - {message.md ? : {message.msg}} + - {message.quotes && } + ); + } + + return ( + + {message.msg} + ); +}; + +const Message = ({ message, invalidFileMessage }: { message: PDFMessage; invalidFileMessage: string }) => ( + + {message.divider && } + + + {processMessage(message)} + {message.quotes && } {message.files && } diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.spec.ts b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.spec.ts new file mode 100644 index 0000000000000..0b5ceb101f4d1 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.spec.ts @@ -0,0 +1,48 @@ +import { messageLongerThanPage, splitByTens, isSystemMessage, markupEntriesGreaterThan10 } from './utils'; + +describe('utils', () => { + it('should return true if message is longer than MAX_MSG_SIZE', () => { + const message = 'f'.repeat(10000); + expect(messageLongerThanPage(message)).toBe(true); + }); + it('should return false if message is shorter than MAX_MSG_SIZE', () => { + const message = 'f'.repeat(1000); + expect(messageLongerThanPage(message)).toBe(false); + }); + it('should return false if message is exactly MAX_MSG_SIZE', () => { + const message = 'f'.repeat(1200); + expect(messageLongerThanPage(message)).toBe(false); + }); + it('should return true if message has more markdown elements than MAX_MD_ELEMENTS_PER_VIEW', () => { + const message = Array(11).fill({}); + expect(markupEntriesGreaterThan10(message)).toBe(true); + }); + it('should return false if message has less markdown elements than MAX_MD_ELEMENTS_PER_VIEW', () => { + const message = Array(10).fill({}); + expect(markupEntriesGreaterThan10(message)).toBe(false); + }); + it('should return false if message has exactly MAX_MD_ELEMENTS_PER_VIEW markdown elements', () => { + const message = Array(10).fill({}); + expect(markupEntriesGreaterThan10(message)).toBe(false); + }); + it('should split an array by groups of 10', () => { + const message = Array(11).fill({}); + expect(splitByTens(message)).toEqual([Array(10).fill({}), Array(1).fill({})]); + }); + it('should split an array by groups of 10', () => { + const message = Array(21).fill({}); + expect(splitByTens(message)).toEqual([Array(10).fill({}), Array(10).fill({}), Array(1).fill({})]); + }); + it('should split an array by groups of 10', () => { + const message = Array(8).fill({}); + expect(splitByTens(message)).toEqual([Array(8).fill({})]); + }); + it('should return true if message is a system message', () => { + const message = { t: 'system' }; + expect(isSystemMessage(message as any)).toBe(true); + }); + it('should return false if message is not a system message', () => { + const message = { msg: 'text' }; + expect(isSystemMessage(message as any)).toBe(false); + }); +}); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.ts b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.ts new file mode 100644 index 0000000000000..099d61951465a --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/utils.ts @@ -0,0 +1,19 @@ +import type { PDFMessage } from '..'; + +const MAX_MD_ELEMENTS_PER_VIEW = 10; +const MAX_MSG_SIZE = 1200; + +export const messageLongerThanPage = (message: string | undefined) => (message?.length ?? 0) > MAX_MSG_SIZE; + +// When a markup list is greater than 10 (magic number, but a reasonable small/big number) we're gonna split the markdown into multiple element +// So react-pdf can split them evenly across pages +export const markupEntriesGreaterThan10 = (messageMd: unknown[] = []) => messageMd.length > MAX_MD_ELEMENTS_PER_VIEW; +export const splitByTens = (array: unknown[] = []): unknown[][] => { + const result = []; + for (let i = 0; i < array.length; i += 10) { + result.push(array.slice(i, i + 10)); + } + return result; +}; + +export const isSystemMessage = (message: PDFMessage) => !!message.t; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx index 36dc77fe4f1c3..8c0a8085c344b 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx @@ -65,7 +65,7 @@ export const ChatTranscriptPDF = ({ header, messages, t }: ChatTranscriptData) = return ( - +
( - + {lines.map((line, index) => ( {line.value?.value || ' '} diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx index c394e0a72ebd7..fbfecc379651f 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx @@ -21,7 +21,7 @@ type OrderedListBlockProps = { }; const OrderedListBlock = ({ items }: OrderedListBlockProps) => ( - + {items.map(({ value, number }, index) => ( {number}. diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx index dbedb8a0b5e02..ec074fc77d9fb 100644 --- a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx @@ -20,7 +20,7 @@ type UnorderedListBlockProps = { items: MessageParser.ListItem[]; }; const UnorderedListBlock = ({ items }: UnorderedListBlockProps) => ( - + {items.map(({ value }, index) => ( diff --git a/ee/packages/pdf-worker/src/worker.fixtures.ts b/ee/packages/pdf-worker/src/worker.fixtures.ts index a1a6cf388e4c5..2ccf74ed0ac18 100644 --- a/ee/packages/pdf-worker/src/worker.fixtures.ts +++ b/ee/packages/pdf-worker/src/worker.fixtures.ts @@ -770,3 +770,676 @@ export const dataWithASingleSystemMessage = { }, ], }; + +export const dataWith2ReallyBigMessages = { + agent: { + name: 'Juanito De Ponce', + username: 'juanito.ponce', + }, + visitor: { + name: 'Christian Castro', + username: 'christian.castro', + }, + siteName: 'Rocket.Chat', + closedAt: '2022-11-21T00:00:00.000Z', + dateFormat: 'MMM D, YYYY', + timeAndDateFormat: 'MMM D, YYYY H:mm:ss', + timezone: 'Etc/GMT+1', + translations: [ + { + key: 'Agent', + value: 'Agent', + }, + { + key: 'Date', + value: 'Date', + }, + { + key: 'Customer', + value: 'Customer', + }, + { + key: 'Chat_transcript', + value: 'Chat transcript', + }, + { + key: 'Time', + value: 'Time', + }, + { + key: 'This_attachment_is_not_supported', + value: 'Attachment format not supported', + }, + ], + messages: [ + { + _id: 'nDYb7NKuL3T7RL6Wg', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Guten Tag alle! Ich brauche eine kleine Hilfe mit der TechSuiteX Anwendung.', + ts: '2025-04-02T12:55:06.279Z', + u: { + _id: '67e6671d9ddc2fe11b73ec5b', + username: 'Guest', + name: 'Anonymous User', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Guten Tag alle! ', + }, + { + type: 'EMOJI', + value: { + type: 'PLAIN_TEXT', + value: ':)', + }, + shortCode: 'slight_smile', + }, + { + type: 'PLAIN_TEXT', + value: ' Ich brauche eine kleine Hilfe mit der TechSuiteX Anwendung.', + }, + ], + }, + ], + files: [], + quotes: [], + }, + { + _id: 'vM2j9MFa4aXQukWJG', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Könntet ihr mir die Mindestvoraussetzungen für V2.0 mitteilen? Und ebenso die Spezifikationen für V2.3? Wir planen die Anschaffung eines stärkeren Computers, und unser IT-Team hat nach den Details gefragt.', + ts: '2025-04-02T12:56:32.098Z', + u: { + _id: '67e6671d9ddc2fe11b73ec5b', + username: 'Guest', + name: 'Anonymous User', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: + 'Könntet ihr mir die Mindestvoraussetzungen für V2.0 mitteilen? Und ebenso die Spezifikationen für V2.3? Wir planen die Anschaffung eines stärkeren Computers, und unser IT-Team hat nach den Details gefragt.', + }, + ], + }, + ], + files: [], + quotes: [], + }, + { + _id: 'T8nHTGt6TnuoSJqCj', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Willkommen bei der TechSupport Kundenhotline!', + ts: '2025-04-02T12:58:36.380Z', + u: { + _id: 'K4hFYDc2aFXhcRPGj', + username: 'User123', + name: 'Anonymous User', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Willkommen bei der TechSupport Kundenhotline!', + }, + ], + }, + ], + files: [], + quotes: [], + }, + { + _id: 'YCXWJ32cFSPdxwuX8', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Guten Tag, danke für Ihre Nachricht. Ich stehe Ihnen im Support-Chat zur Verfügung.', + ts: '2025-04-02T12:58:50.921Z', + u: { + _id: 'K4hFYDc2aFXhcRPGj', + username: 'KosuchK', + name: 'Kosuch, Karl-Heinz', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Guten Tag, danke für Ihre Nachricht. Ich stehe Ihnen im Support-Chat zur Verfügung.', + }, + ], + }, + ], + files: [], + quotes: [], + }, + { + _id: 'QvoAfRg4AAXCCgFuE', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Here are the system requirements for the application:\n\n1. Hardware Requirements\n\nMinimum system requirements:\n• Standard PC with Intel processor, at least 3.1 GHz\n• 4 GB RAM\n• At least 10 GB of free disk space\n• Screen resolution of at least 1280 x 768 pixels and 65k colors\n• DVD drive for installation (USB installation possible)\n• Required interfaces for peripherals: RS-232, Ethernet, USB 2.0\n• Printer: Any OS-supported printer\n\n¹ Adapter required if no free RS-232 port is available.\n² Ethernet adapter required if no free port is available.\n\nRecommended system:\n• Intel Core i5, 3.4 GHz\n• 8 GB (preferably 16 GB) RAM\n• 500 GB SSD storage\n• Screen resolution of 1920 x 1080 pixels\n• DVD drive\n• 1 serial RS-232 interface\n• 2 × 1-Gbit Ethernet interfaces\n\nSpecial requirements apply for advanced peripherals.\n\n2. Software Requirements\n\n• Operating Systems:\n - Windows 7 / 8 / 8.1 / 10 (latest service pack recommended)\n - Future versions will support only Windows 10.\n - Graphics driver must support OpenGL V2.1 or higher.\n\n• Media Player:\n - Some OS versions do not include the default media player.\n\n• Office Integration:\n - Spreadsheet and document software must be installed to use export features.', + ts: '2025-04-02T13:01:04.324Z', + u: { + _id: 'K4hFYDc2aFXhcRPGj', + username: 'User123', + name: 'Anonymous User', + }, + md: [ + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'Here are the system requirements for the application' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '1. Hardware Requirements' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'Minimum system requirements:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Standard PC with Intel processor, at least 3.1 GHz' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• 4 GB RAM' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• At least 10 GB of free disk space' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Screen resolution of at least 1280 x 768 pixels and 65k colors' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• DVD drive for installation (USB installation possible)' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Required interfaces: RS-232, Ethernet, USB 2.0' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Printer: Any OS-supported printer' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '¹ Adapter required if no free RS-232 port is available.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '² Ethernet adapter required if no free port is available.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'Recommended system:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Intel Core i5, 3.4 GHz' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• 8 GB (preferably 16 GB) RAM' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• 500 GB SSD storage' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Screen resolution of 1920 x 1080 pixels' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• DVD drive' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• 1 serial RS-232 interface' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• 2 × 1-Gbit Ethernet interfaces' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: 'Special requirements apply for advanced peripherals.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '2. Software Requirements' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Operating Systems:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: ' - Windows 7 / 8 / 8.1 / 10 (latest service pack recommended)' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: ' - Future versions will support only Windows 10.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: ' - Graphics driver must support OpenGL V2.1 or higher.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Media Player:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: ' - Some OS versions do not include the default media player.' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { type: 'PARAGRAPH', value: [{ type: 'PLAIN_TEXT', value: '• Office Integration:' }] }, + { + type: 'PARAGRAPH', + value: [{ type: 'PLAIN_TEXT', value: ' - Spreadsheet and document software must be installed to use export features.' }], + }, + ], + }, + { + _id: 'b8mMFBNoDe6umGP6e', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Here are the system requirements for Application X V1.91\n1. Hardware Requirements\n\nRecommended System:\n• Intel Core-i5, 3.4 GHz (Turbo > 4 GHz)\n• 16 GB RAM\n• 500 GB SSD\n• Screen resolution 1920 x 1080\n• 2 * 1-Gbit Ethernet interfaces (Communication with test machine and company network)\n(• USB 2.0 interfaces when using USB devices)\n(• RS-232 interfaces when using RS-232 devices; USB-RS-232 adapter possible)\n\nSpecial requirements apply when using additional peripherals or starting multiple devices at once.\nSince 1.9.2024, Application X is available as a download from the customer portal and can be downloaded. No DVD is included by default.\n(https://www.example.com/)\n\n2. Software Requirements\n\n• Operating Systems:\n - Microsoft Windows 11 from Application X V1.6\n - Microsoft Windows 7 up to Application X V1.5\n - Microsoft Windows 10 for all Application X versions\n\nIt is generally recommended to install the latest service pack for the operating system.\n\n• Required Programs:\n - Media Player\n - Microsoft Excel or Word if using optional export interfaces for these types', + ts: '2025-04-02T13:03:06.045Z', + u: { + _id: 'K4hFYDc2aFXhcRPGj', + username: 'User123', + name: 'Anonymous User', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Here are the system requirements for Application X V1.91', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '1. Hardware Requirements', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Recommended System:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Intel Core-i5, 3.4 GHz (Turbo > 4 GHz)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 16 GB RAM', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 500 GB SSD', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Screen resolution 1920 x 1080', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 2 * 1-Gbit Ethernet interfaces (Communication with test machine and company network)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(• USB 2.0 interfaces when using USB devices)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(• RS-232 interfaces when using RS-232 devices; USB-RS-232 adapter possible)', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Special requirements apply when using additional peripherals or starting multiple devices at once.', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: + 'Since 1.9.2024, Application X is available as a download from the customer portal and can be downloaded. No DVD is included by default.', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(https://www.example.com/)', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '2. Software Requirements', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Operating Systems:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 11 from Application X V1.6', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 7 up to Application X V1.5', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 10 for all Application X versions', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'It is generally recommended to install the latest service pack for the operating system.', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Required Programs:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Media Player', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '( - Microsoft Excel or Word if using optional export interfaces for these types)', + }, + ], + }, + ], + files: [], + quotes: [], + }, + { + _id: 'b8mMFBNoDe6umGP6e', + rid: 'Zyutf8db4pSn3qbW4', + msg: 'Here are the system requirements for Application X V1.91\n1. Hardware Requirements\n\nRecommended System:\n• Intel Core-i5, 3.4 GHz (Turbo > 4 GHz)\n• 16 GB RAM\n• 500 GB SSD\n• Screen resolution 1920 x 1080\n• 2 * 1-Gbit Ethernet interfaces (Communication with test machine and company network)\n(• USB 2.0 interfaces when using USB devices)\n(• RS-232 interfaces when using RS-232 devices; USB-RS-232 adapter possible)\n\nSpecial requirements apply when using additional peripherals or starting multiple devices at once.\nSince 1.9.2024, Application X is available as a download from the customer portal and can be downloaded. No DVD is included by default.\n(https://www.example.com/)\n\n2. Software Requirements\n\n• Operating Systems:\n - Microsoft Windows 11 from Application X V1.6\n - Microsoft Windows 7 up to Application X V1.5\n - Microsoft Windows 10 for all Application X versions\n\nIt is generally recommended to install the latest service pack for the operating system.\n\n• Required Programs:\n - Media Player\n - Microsoft Excel or Word if using optional export interfaces for these types', + ts: '2025-04-02T13:03:06.045Z', + u: { + _id: 'K4hFYDc2aFXhcRPGj', + username: 'User123', + name: 'Anonymous User', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Here are the system requirements for Application X V1.91', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '1. Hardware Requirements', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Recommended System:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Intel Core-i5, 3.4 GHz (Turbo > 4 GHz)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 16 GB RAM', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 500 GB SSD', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Screen resolution 1920 x 1080', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• 2 * 1-Gbit Ethernet interfaces (Communication with test machine and company network)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(• USB 2.0 interfaces when using USB devices)', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(• RS-232 interfaces when using RS-232 devices; USB-RS-232 adapter possible)', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Special requirements apply when using additional peripherals or starting multiple devices at once.', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: + 'Since 1.9.2024, Application X is available as a download from the customer portal and can be downloaded. No DVD is included by default.', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '(https://www.example.com/)', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '2. Software Requirements', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Operating Systems:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 11 from Application X V1.6', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 7 up to Application X V1.5', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Microsoft Windows 10 for all Application X versions', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'It is generally recommended to install the latest service pack for the operating system.', + }, + ], + }, + { + type: 'LINE_BREAK', + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '• Required Programs:', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: ' - Media Player', + }, + ], + }, + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: '( - Microsoft Excel or Word if using optional export interfaces for these types)', + }, + ], + }, + ], + files: [], + quotes: [], + }, + ], +}; diff --git a/ee/packages/pdf-worker/src/worker.spec.ts b/ee/packages/pdf-worker/src/worker.spec.ts index d880700194beb..02c8af09a28b5 100644 --- a/ee/packages/pdf-worker/src/worker.spec.ts +++ b/ee/packages/pdf-worker/src/worker.spec.ts @@ -9,6 +9,7 @@ import { dataWithMultipleMessagesAndABigMessage, dataWithASingleMessageAndAnImage, dataWithASingleSystemMessage, + dataWith2ReallyBigMessages, } from './worker.fixtures'; const streamToBuffer = async (stream: NodeJS.ReadableStream) => { @@ -79,6 +80,13 @@ it('should generate a pdf transcript for a single system message', async () => { expect(buffer).toBeTruthy(); }); +it('should generate a pdf transcript for rooms with messages consisting of tons of markdown elements', async () => { + const stream = await pdfWorker.renderToStream({ data: dataWith2ReallyBigMessages }); + const buffer = await streamToBuffer(stream); + + expect(buffer).toBeTruthy(); +}); + describe('isMimeTypeValid', () => { it('should return true if mimeType is valid', () => { expect(pdfWorker.isMimeTypeValid('image/png')).toBe(true); From ba78cbe2f23bf32c8a1cd6be27ac02e98feb2d4d Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Mon, 14 Apr 2025 15:44:30 -0300 Subject: [PATCH 112/187] chore: Livechat department incorrect param name (#35455) --- .../end-to-end/api/livechat/10-departments.ts | 29 +++++++++++++++++-- packages/rest-typings/src/v1/omnichannel.ts | 2 +- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts index 9a92b6332d92e..52ae177471c6a 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts @@ -222,7 +222,7 @@ import { IS_EE } from '../../../e2e/config/constants'; .expect(400); }); - it('should return an error if requestTagsBeforeClosing is true but no tags are provided', async () => { + it('should return an error if requestTagBeforeClosing is true but no tags are provided', async () => { await request .post(api('livechat/department')) .set(credentials) @@ -240,7 +240,7 @@ import { IS_EE } from '../../../e2e/config/constants'; .expect(400); }); - it('should return an error if requestTagsBeforeClosing is true but tags are not an array', async () => { + it('should return an error if requestTagBeforeClosing is true but tags are not an array', async () => { await request .post(api('livechat/department')) .set(credentials) @@ -259,6 +259,31 @@ import { IS_EE } from '../../../e2e/config/constants'; .expect(400); }); + it('should create department if requestTagBeforeClosing is true and tags are an array', async () => { + const chatClosingTags = ['tagA', 'tagB']; + const { body } = await request + .post(api('livechat/department')) + .set(credentials) + .send({ + department: { + name: 'Test', + enabled: true, + showOnOfflineForm: true, + showOnRegistration: true, + email: 'bla@bla', + requestTagBeforeClosingChat: true, + chatClosingTags, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body.department).to.have.property('requestTagBeforeClosingChat', true); + expect(body.department.chatClosingTags).to.deep.equal(chatClosingTags); + + await deleteDepartment(body.department._id); + }); + it('should return an error if fallbackForwardDepartment is present but is not a department id', async () => { await request .post(api('livechat/department')) diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 834dc8efc459d..51e5934864c0f 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -590,7 +590,7 @@ const POSTLivechatDepartmentSchema = { showOnOfflineForm: { type: 'boolean', }, - requestTagsBeforeClosingChat: { + requestTagBeforeClosingChat: { type: 'boolean', nullable: true, }, From 56a15e820804b5cbb1bedfeb4edd748bfe483998 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Mon, 14 Apr 2025 16:26:09 -0300 Subject: [PATCH 113/187] fix: federated users overwriting local display name with their username (#35716) --- .changeset/tall-hornets-live.md | 5 +++++ .../federation/domain/FederatedUser.ts | 18 +++++++++++++++++- .../user/sender/UserServiceSender.spec.ts | 4 ++-- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 .changeset/tall-hornets-live.md diff --git a/.changeset/tall-hornets-live.md b/.changeset/tall-hornets-live.md new file mode 100644 index 0000000000000..522c6bdc00d1f --- /dev/null +++ b/.changeset/tall-hornets-live.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where a federated user's display name would be overwritten by its username diff --git a/apps/meteor/server/services/federation/domain/FederatedUser.ts b/apps/meteor/server/services/federation/domain/FederatedUser.ts index 02f08708d9db1..7d5818aa3cedb 100644 --- a/apps/meteor/server/services/federation/domain/FederatedUser.ts +++ b/apps/meteor/server/services/federation/domain/FederatedUser.ts @@ -115,7 +115,23 @@ export class FederatedUser { } public shouldUpdateDisplayName(displayName: string): boolean { - return this.internalReference.name !== displayName; + // If there is no change, then we don't need to update + if (this.internalReference.name === displayName) { + return false; + } + + // If we don't have a name yet, then use whatever we're receiving + if (!this.internalReference.name) { + return true; + } + + // If the displayName received is based on the username, then ignore it and keep the existing name instead + if (this.internalReference.username?.includes(displayName) || this.externalId.includes(displayName)) { + return false; + } + + // It's a new value and an actual display name + return true; } public getInternalId(): string { diff --git a/apps/meteor/tests/unit/server/federation/application/user/sender/UserServiceSender.spec.ts b/apps/meteor/tests/unit/server/federation/application/user/sender/UserServiceSender.spec.ts index 1c5213c91acb9..c94758214992a 100644 --- a/apps/meteor/tests/unit/server/federation/application/user/sender/UserServiceSender.spec.ts +++ b/apps/meteor/tests/unit/server/federation/application/user/sender/UserServiceSender.spec.ts @@ -238,7 +238,7 @@ describe('Federation - Application - FederationUserServiceSender', () => { existsOnlyOnProxyServer: false, }), ); - bridge.getUserProfileInformation.resolves({ displayname: 'normalizedInviterId' }); + bridge.getUserProfileInformation.resolves({ displayName: 'normalizedInviterId' }); await service.afterUserRealNameChanged('id', 'name'); expect(bridge.setUserDisplayName.called).to.be.false; @@ -252,7 +252,7 @@ describe('Federation - Application - FederationUserServiceSender', () => { _id: '_id', }), ); - bridge.getUserProfileInformation.resolves({ displayname: 'different' }); + bridge.getUserProfileInformation.resolves({ displayName: 'different' }); await service.afterUserRealNameChanged('id', 'name'); expect(bridge.setUserDisplayName.calledWith('externalInviterId', 'name')).to.be.true; From c24da0bfb76dc792416d8ce53f45c59e74949187 Mon Sep 17 00:00:00 2001 From: Rafael Tapia Date: Tue, 15 Apr 2025 09:15:10 -0300 Subject: [PATCH 114/187] fix: check if new LDAP users will exceed the license (#35244) Co-authored-by: Ricardo Garim <9621276+ricardogarim@users.noreply.github.com> --- .changeset/famous-falcons-laugh.md | 5 +++++ apps/meteor/server/lib/ldap/UserConverter.ts | 14 ++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 .changeset/famous-falcons-laugh.md diff --git a/.changeset/famous-falcons-laugh.md b/.changeset/famous-falcons-laugh.md new file mode 100644 index 0000000000000..e7008a8d55da9 --- /dev/null +++ b/.changeset/famous-falcons-laugh.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Ensures seat limit validation in LDAP sync, preventing activations beyond license restrictions. \ No newline at end of file diff --git a/apps/meteor/server/lib/ldap/UserConverter.ts b/apps/meteor/server/lib/ldap/UserConverter.ts index 4f76c61cefe62..bb6087a971345 100644 --- a/apps/meteor/server/lib/ldap/UserConverter.ts +++ b/apps/meteor/server/lib/ldap/UserConverter.ts @@ -1,7 +1,9 @@ import type { IImportUser, IUser } from '@rocket.chat/core-typings'; +import { License } from '@rocket.chat/license'; import type { Logger } from '@rocket.chat/logger'; import { Users } from '@rocket.chat/models'; +import { logger } from './Logger'; import type { ConverterCache } from '../../../app/importer/server/classes/converters/ConverterCache'; import { type RecordConverterOptions } from '../../../app/importer/server/classes/converters/RecordConverter'; import { UserConverter, type UserConverterOptions } from '../../../app/importer/server/classes/converters/UserConverter'; @@ -45,6 +47,18 @@ export class LDAPUserConverter extends UserConverter { } } + async insertUser(userData: IImportUser): Promise { + if (!userData.deleted) { + // #TODO: Change the LDAP sync process to split the inserts and updates into two stages so that we can validate this only once for all insertions + if (await License.shouldPreventAction('activeUsers')) { + logger.warn({ msg: 'Max users allowed reached, creating new LDAP users in inactive state ', username: userData.username }); + userData.deleted = true; + } + } + + return super.insertUser(userData); + } + static async convertSingleUser(userData: IImportUser, options?: UserConverterOptions): Promise { const converter = new LDAPUserConverter(options); await converter.addObject(userData); From f22a6db120e5e165bb6d2211db60f908a62b2118 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 15 Apr 2025 09:46:32 -0300 Subject: [PATCH 115/187] test: bring back coverage e2e (#35803) --- .../.npm/package/npm-shrinkwrap.json | 39 ++++++++----------- .../packages/rocketchat-coverage/package.js | 6 +-- turbo.json | 1 + 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json b/apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json index 526c4cbd2d5c5..a7aac7b28101d 100644 --- a/apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json +++ b/apps/meteor/packages/rocketchat-coverage/.npm/package/npm-shrinkwrap.json @@ -1,5 +1,5 @@ { - "lockfileVersion": 1, + "lockfileVersion": 4, "dependencies": { "has-flag": { "version": "4.0.0", @@ -12,36 +12,29 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" }, "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==" }, "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dependencies": { - "istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==" - } - } + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==" }, "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==" + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==" }, "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==" }, "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==" }, "supports-color": { "version": "7.2.0", diff --git a/apps/meteor/packages/rocketchat-coverage/package.js b/apps/meteor/packages/rocketchat-coverage/package.js index 55dde32fec3d5..3e1a77ece13c3 100644 --- a/apps/meteor/packages/rocketchat-coverage/package.js +++ b/apps/meteor/packages/rocketchat-coverage/package.js @@ -12,7 +12,7 @@ Package.onUse(function (api) { }); Npm.depends({ - 'istanbul-lib-report': '3.0.0', - 'istanbul-reports': '3.0.2', - 'istanbul-lib-coverage': '3.0.0', + 'istanbul-lib-report': '3.0.1', + 'istanbul-reports': '3.1.7', + 'istanbul-lib-coverage': '3.2.2', }); diff --git a/turbo.json b/turbo.json index 3211478850242..e47124a557988 100644 --- a/turbo.json +++ b/turbo.json @@ -46,6 +46,7 @@ }, "@rocket.chat/meteor#build:ci": { "dependsOn": ["^build"], + "env": ["BABEL_ENV"], "cache": false } } From d165f0c28044c5f14aa8e332cb2400f9056a9b63 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Tue, 15 Apr 2025 11:58:00 -0300 Subject: [PATCH 116/187] regression: incoming integration not working with new hono router (#35772) --- apps/meteor/app/api/server/router.ts | 8 ++- .../meteor/app/integrations/server/api/api.js | 24 ++++--- .../end-to-end/api/incoming-integrations.ts | 65 +++++++++++++++++++ 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index 3d06278d9d9eb..b11b3e603e54c 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -138,6 +138,10 @@ export class Router< } private async parseBodyParams(request: HonoRequest, overrideBodyParams: Record = {}) { + if (Object.keys(overrideBodyParams).length !== 0) { + return overrideBodyParams; + } + try { let parsedBody = {}; const contentType = request.header('content-type'); @@ -151,14 +155,14 @@ export class Router< } // This is necessary to keep the compatibility with the previous version, otherwise the bodyParams will be an empty string when no content-type is sent if (parsedBody === '') { - return { ...overrideBodyParams }; + return {}; } if (Array.isArray(parsedBody)) { return parsedBody; } - return { ...parsedBody, ...overrideBodyParams }; + return { ...parsedBody }; // eslint-disable-next-line no-empty } catch {} diff --git a/apps/meteor/app/integrations/server/api/api.js b/apps/meteor/app/integrations/server/api/api.js index 5541616c7cbe9..451a125e1b0bf 100644 --- a/apps/meteor/app/integrations/server/api/api.js +++ b/apps/meteor/app/integrations/server/api/api.js @@ -87,25 +87,29 @@ async function executeIntegrationRest() { }; const scriptEngine = getEngine(this.request.integration); - if (scriptEngine.integrationHasValidScript(this.request.integration)) { - this.request.setEncoding('utf8'); - const content_raw = this.request.read(); + const buffers = []; + for await (const chunk of this.request.body) { + buffers.push(chunk); + } + const content_raw = Buffer.concat(buffers).toString('utf8'); + const protocol = `${this.request.headers.get('x-forwarded-proto')}:` || 'http:'; + const url = new URL(this.request.url, `${protocol}//${this.request.headers.host}`); const request = { url: { - hash: this.request._parsedUrl.hash, - search: this.request._parsedUrl.search, + hash: url.hash, + search: url.search, query: this.queryParams, - pathname: this.request._parsedUrl.pathname, - path: this.request._parsedUrl.path, + pathname: url.pathname, + path: url.path, }, url_raw: this.request.url, url_params: this.urlParams, content: this.bodyParams, content_raw, - headers: this.request.headers, - body: this.request.body, + headers: Object.fromEntries(this.request.headers.entries()), + body: this.bodyParams, user: { _id: this.user._id, name: this.user.name, @@ -321,7 +325,7 @@ const middleware = async (c, next) => { } try { - const body = await (req.header('content-type')?.includes('application/json') ? req.raw.clone().json() : req.raw.clone().text()); + const body = Object.fromEntries(new URLSearchParams(await req.raw.clone().text())); if (!body || typeof body !== 'object' || !('payload' in body) || Object.keys(body).length !== 1) { return next(); } diff --git a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts index 952d21b386843..f745bf34cc356 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -330,6 +330,71 @@ describe('[Incoming Integrations]', () => { }); }); }); + + it('should send a message if the payload is a application/x-www-form-urlencoded JSON AND the integration has a valid script', async () => { + const payload = { msg: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; + let withScript: IIntegration | undefined; + + await updatePermission('manage-incoming-integrations', ['admin']); + await request + .post(api('integrations.create')) + .set(credentials) + .send({ + type: 'webhook-incoming', + name: 'Incoming test with script', + enabled: true, + alias: 'test', + username: 'rocket.cat', + scriptEnabled: true, + overrideDestinationChannelEnabled: false, + channel: '#general', + script: ` + class Script { + process_incoming_request({ request }) { + return { + content:{ + text: request.content.text + } + }; + } + } + `, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('integration').and.to.be.an('object'); + withScript = res.body.integration; + }); + + if (!withScript) { + throw new Error('Integration not created'); + } + + await request + .post(`/hooks/${withScript._id}/${withScript.token}`) + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(`payload=${JSON.stringify(payload)}`) + .expect(200) + .expect(async () => { + return request + .get(api('channels.messages')) + .set(credentials) + .query({ + roomId: 'GENERAL', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').and.to.be.an('array'); + expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; + }); + }); + + await removeIntegration(withScript._id, 'incoming'); + }); }); describe('[/integrations.history]', () => { From ec7894139c56d0e29ac696c448cc932efb6cb0f0 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Tue, 15 Apr 2025 14:46:45 -0300 Subject: [PATCH 117/187] fix: voip calls not ringing after temporary disconnection (#35765) --- .changeset/hot-beers-glow.md | 6 +++ .../UserMenu/hooks/useVoipItemsSection.tsx | 12 +++-- .../header/hooks/useVoipItemsSection.tsx | 12 +++-- packages/i18n/src/locales/en.i18n.json | 1 + packages/ui-voip/src/hooks/useVoipState.tsx | 4 +- packages/ui-voip/src/lib/VoipClient.ts | 51 +++++++++++++++++++ 6 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 .changeset/hot-beers-glow.md diff --git a/.changeset/hot-beers-glow.md b/.changeset/hot-beers-glow.md new file mode 100644 index 0000000000000..0cc220e20a7af --- /dev/null +++ b/.changeset/hot-beers-glow.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-voip': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue causing VoIP calls to no longer reach the client after a temporary disconnection diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx index 9a2584869b13c..6025affc42f99 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useVoipItemsSection.tsx @@ -10,7 +10,7 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const { clientError, isEnabled, isReady, isRegistered } = useVoipState(); + const { clientError, isEnabled, isReady, isRegistered, isReconnecting } = useVoipState(); const { register, unregister, onRegisteredOnce, onUnregisteredOnce } = useVoipAPI(); const toggleVoip = useMutation({ @@ -44,8 +44,12 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef return t('Loading'); } + if (isReconnecting) { + return t('Reconnecting'); + } + return ''; - }, [clientError, isReady, toggleVoip.isPending, t]); + }, [clientError, isReady, toggleVoip.isPending, t, isReconnecting]); return useMemo(() => { if (!isEnabled) { @@ -57,7 +61,7 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef { id: 'toggle-voip', icon: isRegistered ? 'phone-disabled' : 'phone', - disabled: !isReady || toggleVoip.isPending, + disabled: !isReady || toggleVoip.isPending || isReconnecting, onClick: () => toggleVoip.mutate(), content: ( @@ -67,7 +71,7 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef }, ], }; - }, [isEnabled, isRegistered, isReady, tooltip, t, toggleVoip]); + }, [isEnabled, isRegistered, isReady, tooltip, t, toggleVoip, isReconnecting]); }; export default useVoipItemsSection; diff --git a/apps/meteor/client/sidebar/header/hooks/useVoipItemsSection.tsx b/apps/meteor/client/sidebar/header/hooks/useVoipItemsSection.tsx index c8b7e89dd2298..d77a78deb457c 100644 --- a/apps/meteor/client/sidebar/header/hooks/useVoipItemsSection.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useVoipItemsSection.tsx @@ -10,7 +10,7 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const { clientError, isEnabled, isReady, isRegistered } = useVoipState(); + const { clientError, isEnabled, isReady, isRegistered, isReconnecting } = useVoipState(); const { register, unregister, onRegisteredOnce, onUnregisteredOnce } = useVoipAPI(); const toggleVoip = useMutation({ @@ -44,8 +44,12 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef return t('Loading'); } + if (isReconnecting) { + return t('Reconnecting'); + } + return ''; - }, [clientError, isReady, toggleVoip.isPending, t]); + }, [clientError, isReady, toggleVoip.isPending, t, isReconnecting]); return useMemo(() => { if (!isEnabled) { @@ -57,7 +61,7 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef { id: 'toggle-voip', icon: isRegistered ? 'phone-disabled' : 'phone', - disabled: !isReady || toggleVoip.isPending, + disabled: !isReady || toggleVoip.isPending || isReconnecting, onClick: () => toggleVoip.mutate(), content: ( @@ -67,5 +71,5 @@ export const useVoipItemsSection = (): { items: GenericMenuItemProps[] } | undef }, ], }; - }, [isEnabled, isRegistered, isReady, tooltip, t, toggleVoip]); + }, [isEnabled, isRegistered, isReady, tooltip, t, toggleVoip, isReconnecting]); }; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 6b62f7aef1025..2c8b13028198a 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -4162,6 +4162,7 @@ "Missing_configuration": "Missing configuration", "Recent_Import_History": "Recent Import History", "Record": "Record", + "Reconnecting": "Reconnecting", "Mobex_sms_gateway_address": "Mobex SMS Gateway Address", "Mobex_sms_gateway_address_desc": "IP or Host of your Mobex service with specified port. E.g. `http://192.168.1.1:1401` or `https://www.example.com:1401`", "Mobex_sms_gateway_from_number": "From", diff --git a/packages/ui-voip/src/hooks/useVoipState.tsx b/packages/ui-voip/src/hooks/useVoipState.tsx index 66e1ea7538a06..b55f679dcef42 100644 --- a/packages/ui-voip/src/hooks/useVoipState.tsx +++ b/packages/ui-voip/src/hooks/useVoipState.tsx @@ -1,7 +1,7 @@ import { useContext, useMemo } from 'react'; -import { VoipContext } from '../contexts/VoipContext'; import { useVoipEffect } from './useVoipEffect'; +import { VoipContext } from '../contexts/VoipContext'; export type VoipState = { isEnabled: boolean; @@ -15,6 +15,7 @@ export type VoipState = { isError: boolean; error?: Error | null; clientError?: Error | null; + isReconnecting: boolean; }; const DEFAULT_STATE = { @@ -26,6 +27,7 @@ const DEFAULT_STATE = { isOngoing: false, isOutgoing: false, isError: false, + isReconnecting: false, }; export const useVoipState = (): VoipState => { diff --git a/packages/ui-voip/src/lib/VoipClient.ts b/packages/ui-voip/src/lib/VoipClient.ts index 86850aa9c93fb..12af06ad3bf6f 100644 --- a/packages/ui-voip/src/lib/VoipClient.ts +++ b/packages/ui-voip/src/lib/VoipClient.ts @@ -46,6 +46,8 @@ class VoipClient extends Emitter { private contactInfo: ContactInfo | null = null; + private reconnecting = false; + constructor(private readonly config: VoIPUserConfiguration) { super(); @@ -400,9 +402,17 @@ class VoipClient extends Emitter { } if (connectionRetryCount !== -1 && reconnectionAttempt > connectionRetryCount) { + console.error('VoIP reconnection limit reached.'); + this.reconnecting = false; + this.emit('stateChanged'); return; } + if (!this.reconnecting) { + this.reconnecting = true; + this.emit('stateChanged'); + } + const reconnectionDelay = Math.pow(2, reconnectionAttempt % 4); console.error(`Attempting to reconnect with backoff due to network loss. Backoff time [${reconnectionDelay}]`); @@ -521,6 +531,10 @@ class VoipClient extends Emitter { return !!this.error; } + public isReconnecting(): boolean { + return this.reconnecting; + } + public isOnline(): boolean { return this.online; } @@ -604,6 +618,7 @@ class VoipClient extends Emitter { isOutgoing: this.isOutgoing(), isInCall: this.isInCall(), isError: this.isError(), + isReconnecting: this.isReconnecting(), }; } @@ -760,15 +775,51 @@ class VoipClient extends Emitter { } private onUserAgentConnected = (): void => { + console.log('VoIP user agent connected.'); + + const wasReconnecting = this.reconnecting; + + this.reconnecting = false; this.networkEmitter.emit('connected'); this.emit('stateChanged'); + + if (!this.isReady() || !wasReconnecting) { + return; + } + + this.register() + .then(() => { + this.emit('stateChanged'); + }) + .catch((error?: any) => { + console.error('VoIP failed to register after user agent connection.'); + if (error) { + console.error(error); + } + }); }; private onUserAgentDisconnected = (error: any): void => { + console.log('VoIP user agent disconnected.'); + + this.reconnecting = !!error; this.networkEmitter.emit('disconnected'); this.emit('stateChanged'); if (error) { + if (this.isRegistered()) { + this.unregister() + .then(() => { + this.emit('stateChanged'); + }) + .catch((error?: any) => { + console.error('VoIP failed to unregister after user agent disconnection.'); + if (error) { + console.error(error); + } + }); + } + this.networkEmitter.emit('connectionerror', error); this.attemptReconnection(); } From 8715b7e5a33e10a838e1a2fbac41e44fdc2c29bf Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 16 Apr 2025 07:56:08 -0300 Subject: [PATCH 118/187] fix: infinite loop in useAgentsList after first fetch (#35779) --- .changeset/gold-laws-wink.md | 5 +++ .../client/components/AutoCompleteAgent.tsx | 6 ++- .../Omnichannel/hooks/useAgentsList.ts | 2 +- .../DepartmentAgentsTable/AddAgent.tsx | 10 ++++- .../DepartmentAgentsTable.tsx | 7 ++-- .../departments/EditDepartment.tsx | 7 +++- .../omnichannel-departaments.spec.ts | 38 +++++++++++++++++-- .../page-objects/omnichannel-departments.ts | 16 ++++++++ 8 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 .changeset/gold-laws-wink.md diff --git a/.changeset/gold-laws-wink.md b/.changeset/gold-laws-wink.md new file mode 100644 index 0000000000000..4af030b274cf0 --- /dev/null +++ b/.changeset/gold-laws-wink.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes a fetch infinite loop when adding agents to a department diff --git a/apps/meteor/client/components/AutoCompleteAgent.tsx b/apps/meteor/client/components/AutoCompleteAgent.tsx index fad5b1a15d575..f20c688ef169f 100644 --- a/apps/meteor/client/components/AutoCompleteAgent.tsx +++ b/apps/meteor/client/components/AutoCompleteAgent.tsx @@ -1,13 +1,13 @@ import { PaginatedSelectFiltered } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import type { ReactElement } from 'react'; +import type { AriaAttributes, ReactElement } from 'react'; import { memo, useMemo, useState } from 'react'; import { useRecordList } from '../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../lib/asyncState'; import { useAgentsList } from './Omnichannel/hooks/useAgentsList'; -type AutoCompleteAgentProps = { +type AutoCompleteAgentProps = Pick & { value: string; error?: string; placeholder?: string; @@ -31,6 +31,7 @@ const AutoCompleteAgent = ({ onlyAvailable = false, withTitle = false, onChange, + 'aria-labelledby': ariaLabelledBy, }: AutoCompleteAgentProps): ReactElement => { const [agentsFilter, setAgentsFilter] = useState(''); @@ -57,6 +58,7 @@ const AutoCompleteAgent = ({ setFilter={setAgentsFilter as (value: string | number | undefined) => void} options={agentsItems} data-qa='autocomplete-agent' + aria-labelledby={ariaLabelledBy} endReached={ agentsPhase === AsyncStatePhase.LOADING ? (): void => undefined : (start): void => loadMoreAgents(start, Math.min(50, agentsTotal)) } diff --git a/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts index d81f1b1f3e5a7..0c40ddf0d5826 100644 --- a/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts +++ b/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts @@ -76,7 +76,7 @@ export const useAgentsList = ( return { items, - itemCount: total + 1, + itemCount: total, }; }, [excludeId, getAgents, haveAll, haveNoAgentsSelectedOption, onlyAvailable, showIdleAgents, t, text], diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AddAgent.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AddAgent.tsx index 86cb80c7c4d5c..7ddbdb66a6b41 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AddAgent.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/AddAgent.tsx @@ -1,6 +1,7 @@ import { Box, Button } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import type { AriaAttributes } from 'react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,7 +9,12 @@ import AutoCompleteAgent from '../../../../components/AutoCompleteAgent'; import { useEndpointAction } from '../../../../hooks/useEndpointAction'; import type { IDepartmentAgent } from '../definitions'; -function AddAgent({ agentList, onAdd }: { agentList: IDepartmentAgent[]; onAdd: (agent: IDepartmentAgent) => void }) { +type AddAgentProps = Pick & { + agentList: IDepartmentAgent[]; + onAdd: (agent: IDepartmentAgent) => void; +}; + +function AddAgent({ agentList, onAdd, 'aria-labelledby': ariaLabelledBy }: AddAgentProps) { const { t } = useTranslation(); const [userId, setUserId] = useState(''); @@ -37,7 +43,7 @@ function AddAgent({ agentList, onAdd }: { agentList: IDepartmentAgent[]; onAdd: }); return ( - + - - - - - )} - - {(authEnabled || !hasOutlookMethods) && ( - <> - - {(total === 0 || calendarListResult.isError) && ( - - {calendarListResult.isError && ( - - - {t('Something_went_wrong')} - {getErrorMessage(calendarListResult.error)} - - )} - {!calendarListResult.isError && total === 0 && ( - - - {t('No_history')} - - )} - - )} - {calendarListResult.isSuccess && calendarListResult.data.length > 0 && ( - - - } - /> - - - )} - - + + + {calendarListResult.isPending && } + {calendarListResult.isError && ( + + + {t('Something_went_wrong')} + {getErrorMessage(calendarListResult.error)} + + )} + {!calendarListResult.isPending && total === 0 && ( + + + {t('No_history')} + + )} + {calendarListResult.isSuccess && calendarListResult.data.length > 0 && ( + + } + /> + + )} + + + + + {authEnabled && } + {outlookUrl && ( + + )} + + {hasOutlookMethods && ( + - {authEnabled && } - {outlookUrl && ( - - )} + - {hasOutlookMethods && ( - - - - - - )} - - - )} + + )} + ); }; From 6401f29613eea781237901875abb00aa2a23e89d Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Thu, 17 Apr 2025 21:36:18 -0300 Subject: [PATCH 140/187] fix: LDAP Sync stops processing users if any entry can't be mapped to a Rocket.Chat user (#35810) --- .changeset/yellow-comics-cheer.md | 5 +++++ apps/meteor/server/lib/ldap/Connection.ts | 3 --- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 .changeset/yellow-comics-cheer.md diff --git a/.changeset/yellow-comics-cheer.md b/.changeset/yellow-comics-cheer.md new file mode 100644 index 0000000000000..1ebf8920c7a3d --- /dev/null +++ b/.changeset/yellow-comics-cheer.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue on the LDAP Background Sync where the process would stop syncing users if any of the LDAP users couldn't be properly mapped to a Rocket.Chat User (for example, by not having an email address) diff --git a/apps/meteor/server/lib/ldap/Connection.ts b/apps/meteor/server/lib/ldap/Connection.ts index 44166b445a7bb..10dc2a82a46d2 100644 --- a/apps/meteor/server/lib/ldap/Connection.ts +++ b/apps/meteor/server/lib/ldap/Connection.ts @@ -372,7 +372,6 @@ export class LDAPConnection { realEntries++; } catch (e) { searchLogger.error(e); - throw e; } }); @@ -544,7 +543,6 @@ export class LDAPConnection { entries.push(result as T); } catch (e) { searchLogger.error(e); - throw e; } }); @@ -622,7 +620,6 @@ export class LDAPConnection { } } catch (e) { searchLogger.error(e); - throw e; } }); From bbd0b0d9ed181a156430e2a446d3b56092e3f645 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Thu, 17 Apr 2025 22:33:29 -0300 Subject: [PATCH 141/187] fix: VoIP calls not gathering Ice Servers (#35832) --- .changeset/old-readers-battle.md | 8 +++++++ apps/meteor/ee/server/settings/voip.ts | 21 +++++++++++++++++++ .../src/voip/VoIPUserConfiguration.ts | 6 ++++++ packages/i18n/src/locales/en.i18n.json | 4 ++++ .../{useWebRtcServers.ts => useIceServers.ts} | 4 ++-- packages/ui-voip/src/hooks/useVoipClient.tsx | 8 ++++--- packages/ui-voip/src/lib/VoipClient.ts | 4 ++-- 7 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 .changeset/old-readers-battle.md rename packages/ui-voip/src/hooks/{useWebRtcServers.ts => useIceServers.ts} (77%) diff --git a/.changeset/old-readers-battle.md b/.changeset/old-readers-battle.md new file mode 100644 index 0000000000000..fb00619ef9325 --- /dev/null +++ b/.changeset/old-readers-battle.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/core-typings': patch +'@rocket.chat/ui-voip': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes an issue where Voice Calls were unable to gather Ice Servers diff --git a/apps/meteor/ee/server/settings/voip.ts b/apps/meteor/ee/server/settings/voip.ts index e63fc1f13df26..50f6e4dbafe95 100644 --- a/apps/meteor/ee/server/settings/voip.ts +++ b/apps/meteor/ee/server/settings/voip.ts @@ -8,6 +8,8 @@ export function addSettings(): Promise { modules: ['teams-voip'], }, async function () { + const enableQuery = { _id: 'VoIP_TeamCollab_Enabled', value: true }; + await this.add('VoIP_TeamCollab_Enabled', false, { type: 'boolean', public: true, @@ -19,30 +21,49 @@ export function addSettings(): Promise { type: 'string', public: true, invalidValue: '', + enableQuery, }); await this.add('VoIP_TeamCollab_FreeSwitch_Port', 8021, { type: 'int', public: true, invalidValue: 8021, + enableQuery, }); await this.add('VoIP_TeamCollab_FreeSwitch_Password', '', { type: 'password', secret: true, invalidValue: '', + enableQuery, }); await this.add('VoIP_TeamCollab_FreeSwitch_Timeout', 3000, { type: 'int', public: true, invalidValue: 3000, + enableQuery, }); await this.add('VoIP_TeamCollab_FreeSwitch_WebSocket_Path', '', { type: 'string', public: true, invalidValue: '', + enableQuery, + }); + + await this.add('VoIP_TeamCollab_Ice_Servers', 'stun:stun.l.google.com:19302', { + type: 'string', + public: true, + invalidValue: '', + enableQuery, + }); + + await this.add('VoIP_TeamCollab_Ice_Gathering_Timeout', 5000, { + type: 'int', + public: true, + invalidValue: 5000, + enableQuery, }); }, ); diff --git a/packages/core-typings/src/voip/VoIPUserConfiguration.ts b/packages/core-typings/src/voip/VoIPUserConfiguration.ts index 01bca0a5409cb..7bade79310cad 100644 --- a/packages/core-typings/src/voip/VoIPUserConfiguration.ts +++ b/packages/core-typings/src/voip/VoIPUserConfiguration.ts @@ -45,6 +45,12 @@ export interface VoIPUserConfiguration { * @defaultValue undefined */ enableKeepAliveUsingOptionsForUnstableNetworks: boolean; + + /** + * Time to wait for Ice Gathering to complete + * @defaultValue 5000 + */ + iceGatheringTimeout?: number; } export interface IMediaStreamRenderer { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 4e5cc01e43fbd..d3fb8e9457146 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6270,6 +6270,10 @@ "VoIP_TeamCollab_FreeSwitch_Timeout": "FreeSwitch Request Timeout", "VoIP_TeamCollab_FreeSwitch_WebSocket_Path": "WebSocket Path", "VoIP_TeamCollab_Beta_Alert": "This feature is currently in Beta, please report any issues to Rocket.Chat support", + "VoIP_TeamCollab_Ice_Servers": "Ice Servers", + "VoIP_TeamCollab_Ice_Servers_Description": "A list of Ice Servers (STUN and/or TURN), separated by comma. \n Username, password and port are allowed in the format `username:password@stun:host:port` or `username:password@turn:host:port`. \n Both username and password may be html-encoded.", + "VoIP_TeamCollab_Ice_Gathering_Timeout": "Ice Gathering Timeout", + "VoIP_TeamCollab_Ice_Gathering_Timeout_Description": "Time to wait for Ice Gathering to complete before sending. Low values may prevent Ice Servers from being used, while high values may delay the start of VoIP calls if an invalid Ice Server is specified.", "VoIP_Toggle": "Enable/Disable VoIP", "Chat_opened_by_visitor": "Chat opened by the visitor", "Waiting_for_answer": "Waiting for answer", diff --git a/packages/ui-voip/src/hooks/useWebRtcServers.ts b/packages/ui-voip/src/hooks/useIceServers.ts similarity index 77% rename from packages/ui-voip/src/hooks/useWebRtcServers.ts rename to packages/ui-voip/src/hooks/useIceServers.ts index 9753098c0a659..1c32343a9246a 100644 --- a/packages/ui-voip/src/hooks/useWebRtcServers.ts +++ b/packages/ui-voip/src/hooks/useIceServers.ts @@ -4,8 +4,8 @@ import { useMemo } from 'react'; import type { IceServer } from '../definitions'; import { parseStringToIceServers } from '../utils/parseStringToIceServers'; -export const useWebRtcServers = (): IceServer[] => { - const servers = useSetting('WebRTC_Servers'); +export const useIceServers = (): IceServer[] => { + const servers = useSetting('VoIP_TeamCollab_Ice_Servers'); return useMemo(() => { if (typeof servers !== 'string' || !servers.trim()) { diff --git a/packages/ui-voip/src/hooks/useVoipClient.tsx b/packages/ui-voip/src/hooks/useVoipClient.tsx index 589f76d7952c0..fdda0587b7d52 100644 --- a/packages/ui-voip/src/hooks/useVoipClient.tsx +++ b/packages/ui-voip/src/hooks/useVoipClient.tsx @@ -1,8 +1,8 @@ -import { useUser, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useUser, useEndpoint, useSetting } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import { useEffect, useRef } from 'react'; -import { useWebRtcServers } from './useWebRtcServers'; +import { useIceServers } from './useIceServers'; import VoipClient from '../lib/VoipClient'; type VoipClientParams = { @@ -20,8 +20,9 @@ export const useVoipClient = ({ enabled = true, autoRegister = true }: VoipClien const voipClientRef = useRef(null); const getRegistrationInfo = useEndpoint('GET', '/v1/voip-freeswitch.extension.getRegistrationInfoByUserId'); + const iceGatheringTimeout = useSetting('VoIP_TeamCollab_Ice_Gathering_Timeout', 5000); - const iceServers = useWebRtcServers(); + const iceServers = useIceServers(); const { data: voipClient, error } = useQuery({ queryKey: ['voip-client', enabled, userId, iceServers], @@ -59,6 +60,7 @@ export const useVoipClient = ({ enabled = true, autoRegister = true }: VoipClien webSocketURI: websocketPath, connectionRetryCount: Number(10), // TODO: get from settings enableKeepAliveUsingOptionsForUnstableNetworks: true, // TODO: get from settings + iceGatheringTimeout, }; const voipClient = await VoipClient.create(config); diff --git a/packages/ui-voip/src/lib/VoipClient.ts b/packages/ui-voip/src/lib/VoipClient.ts index 12af06ad3bf6f..7ab1fe3906eea 100644 --- a/packages/ui-voip/src/lib/VoipClient.ts +++ b/packages/ui-voip/src/lib/VoipClient.ts @@ -55,7 +55,7 @@ class VoipClient extends Emitter { } public async init() { - const { authPassword, authUserName, sipRegistrarHostnameOrIP, iceServers, webSocketURI } = this.config; + const { authPassword, authUserName, sipRegistrarHostnameOrIP, iceServers, webSocketURI, iceGatheringTimeout } = this.config; const transportOptions = { server: webSocketURI, @@ -64,7 +64,7 @@ class VoipClient extends Emitter { }; const sdpFactoryOptions = { - iceGatheringTimeout: 10, + ...(typeof iceGatheringTimeout === 'number' && { iceGatheringTimeout }), peerConnectionConfiguration: { iceServers }, }; From 1c839a3ae5eb694d044c2dd7e441bf2de85c44d7 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 17 Apr 2025 20:41:06 -0600 Subject: [PATCH 142/187] fix: Apply chat limits on enterprise Routing Algorithms (#35831) Co-authored-by: Diego Sampaio <8591547+sampaiodiego@users.noreply.github.com> --- .changeset/purple-hairs-hang.md | 5 + .../server/lib/routing/AutoSelection.ts | 1 - .../applySimultaneousChatsRestrictions.ts | 52 ++-- .../server/lib/routing/LoadBalancing.ts | 4 + .../server/lib/routing/LoadRotation.ts | 4 + apps/meteor/ee/server/models/raw/Users.ts | 20 +- .../tests/end-to-end/api/livechat/07-queue.ts | 271 +++++++++++++++++- packages/core-typings/src/ILivechatAgent.ts | 7 + .../models/ILivechatDepartmentAgentsModel.ts | 4 +- .../model-typings/src/models/IUsersModel.ts | 14 +- .../src/models/LivechatDepartmentAgents.ts | 4 +- packages/models/src/models/Users.ts | 109 ++++--- 12 files changed, 414 insertions(+), 81 deletions(-) create mode 100644 .changeset/purple-hairs-hang.md diff --git a/.changeset/purple-hairs-hang.md b/.changeset/purple-hairs-hang.md new file mode 100644 index 0000000000000..bb7afddddc693 --- /dev/null +++ b/.changeset/purple-hairs-hang.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue where enterprise routing algorithms could get stuck on selecting the same agent due to chat limits being applied after agent selection, but before agent assignment diff --git a/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts b/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts index 6b406bb511933..ee3385879cc4c 100644 --- a/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts +++ b/apps/meteor/app/livechat/server/lib/routing/AutoSelection.ts @@ -26,7 +26,6 @@ class AutoSelection implements IRoutingMethod { } async getNextAgent(department?: string, ignoreAgentId?: string): Promise { - // TODO: apply this extra query to other routing algorithms const extraQuery = await callbacks.run('livechat.applySimultaneousChatRestrictions', undefined, { ...(department ? { departmentId: department } : {}), }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts index c0f07b566444d..2de13028e12e4 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/applySimultaneousChatsRestrictions.ts @@ -1,39 +1,43 @@ -import type { ILivechatDepartment } from '@rocket.chat/core-typings'; +import type { ILivechatDepartment, AvailableAgentsAggregation } from '@rocket.chat/core-typings'; import { LivechatDepartment } from '@rocket.chat/models'; -import type { Document } from 'mongodb'; +import type { Filter } from 'mongodb'; import { settings } from '../../../../../app/settings/server'; import { callbacks } from '../../../../../lib/callbacks'; -callbacks.add( - 'livechat.applySimultaneousChatRestrictions', - async (_: any, { departmentId }: { departmentId?: string } = {}) => { - const limitFilter: Document[] = []; +export async function getChatLimitsQuery(departmentId?: string): Promise> { + const limitFilter: Filter = []; - if (departmentId) { - const departmentLimit = - ( - await LivechatDepartment.findOneById>(departmentId, { - projection: { maxNumberSimultaneousChat: 1 }, - }) - )?.maxNumberSimultaneousChat || 0; - if (departmentLimit > 0) { - limitFilter.push({ 'queueInfo.chatsForDepartment': { $gte: Number(departmentLimit) } }); - } + if (departmentId) { + const departmentLimit = + ( + await LivechatDepartment.findOneById>(departmentId, { + projection: { maxNumberSimultaneousChat: 1 }, + }) + )?.maxNumberSimultaneousChat || 0; + if (departmentLimit > 0) { + limitFilter.push({ 'queueInfo.chatsForDepartment': { $gte: Number(departmentLimit) } }); } + } + + limitFilter.push({ + $and: [{ maxChatsForAgent: { $gt: 0 } }, { $expr: { $gte: ['$queueInfo.chats', '$maxChatsForAgent'] } }], + }); + const maxChatsPerSetting = settings.get('Livechat_maximum_chats_per_agent'); + if (maxChatsPerSetting > 0) { limitFilter.push({ - $and: [{ maxChatsForAgent: { $gt: 0 } }, { $expr: { $gte: ['$queueInfo.chats', '$maxChatsForAgent'] } }], + $and: [{ maxChatsForAgent: { $eq: 0 } }, { 'queueInfo.chats': { $gte: maxChatsPerSetting } }], }); + } - const maxChatsPerSetting = settings.get('Livechat_maximum_chats_per_agent'); - if (maxChatsPerSetting > 0) { - limitFilter.push({ - $and: [{ maxChatsForAgent: { $eq: 0 } }, { 'queueInfo.chats': { $gte: maxChatsPerSetting } }], - }); - } + return { $match: { $or: limitFilter } }; +} - return { $match: { $or: limitFilter } }; +callbacks.add( + 'livechat.applySimultaneousChatRestrictions', + async (_: any, { departmentId }: { departmentId?: string } = {}) => { + return getChatLimitsQuery(departmentId); }, callbacks.priority.HIGH, 'livechat-apply-simultaneous-restrictions', diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadBalancing.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadBalancing.ts index b909e00b27a54..ded9f383fe30f 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadBalancing.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadBalancing.ts @@ -3,6 +3,7 @@ import { Users } from '@rocket.chat/models'; import { RoutingManager } from '../../../../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../../../../app/settings/server'; import type { IRoutingManagerConfig } from '../../../../../../definition/IRoutingManagerConfig'; +import { getChatLimitsQuery } from '../../hooks/applySimultaneousChatsRestrictions'; /* Load Balancing Queuing method: * @@ -29,10 +30,13 @@ class LoadBalancing { } async getNextAgent(department?: string, ignoreAgentId?: string) { + const extraQuery = await getChatLimitsQuery(department); + const unavailableUsers = await Users.getUnavailableAgents(department, extraQuery); const nextAgent = await Users.getNextLeastBusyAgent( department, ignoreAgentId, settings.get('Livechat_enabled_when_agent_idle'), + unavailableUsers.map((u) => u.username), ); if (!nextAgent) { return; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts index 7c915c65b8812..dda0ac5c98861 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/routing/LoadRotation.ts @@ -4,6 +4,7 @@ import { Users } from '@rocket.chat/models'; import { RoutingManager } from '../../../../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../../../../app/settings/server'; import type { IRoutingManagerConfig } from '../../../../../../definition/IRoutingManagerConfig'; +import { getChatLimitsQuery } from '../../hooks/applySimultaneousChatsRestrictions'; /* Load Rotation Queuing method: * Routing method where the agent with the oldest routing time is the next agent to serve incoming chats @@ -28,10 +29,13 @@ class LoadRotation { } public async getNextAgent(department?: string, ignoreAgentId?: string): Promise { + const extraQuery = await getChatLimitsQuery(department); + const unavailableUsers = await Users.getUnavailableAgents(department, extraQuery); const nextAgent = await Users.getLastAvailableAgentRouted( department, ignoreAgentId, settings.get('Livechat_enabled_when_agent_idle'), + unavailableUsers.map((user) => user.username), ); if (!nextAgent?.username) { return; diff --git a/apps/meteor/ee/server/models/raw/Users.ts b/apps/meteor/ee/server/models/raw/Users.ts index 31f71b5349499..d7324f7f3a08e 100644 --- a/apps/meteor/ee/server/models/raw/Users.ts +++ b/apps/meteor/ee/server/models/raw/Users.ts @@ -1,16 +1,15 @@ -import type { RocketChatRecordDeleted, IUser } from '@rocket.chat/core-typings'; +import type { RocketChatRecordDeleted, IUser, AvailableAgentsAggregation } from '@rocket.chat/core-typings'; import { UsersRaw } from '@rocket.chat/models'; -import type { Db, Collection } from 'mongodb'; +import type { Db, Collection, Filter } from 'mongodb'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; -type AgentMetadata = { - username: string; -}; - declare module '@rocket.chat/model-typings' { interface IUsersModel { - getUnavailableAgents(departmentId: string, customFilter: { [k: string]: any }[]): Promise; + getUnavailableAgents( + departmentId: string, + customFilter: Filter, + ): Promise[]>; } } @@ -19,7 +18,10 @@ export class UsersEE extends UsersRaw { super(db, trash); } - getUnavailableAgents(departmentId: string, customFilter: { [k: string]: any }[]): Promise { + getUnavailableAgents( + departmentId: string, + customFilter: Filter, + ): Promise[]> { // if department is provided, remove the agents that are not from the selected department const departmentFilter = departmentId ? [ @@ -46,7 +48,7 @@ export class UsersEE extends UsersRaw { : []; return this.col - .aggregate( + .aggregate( [ { $match: { diff --git a/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts b/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts index 3a07b5ec28b82..b9ba85bc7825c 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/07-queue.ts @@ -386,7 +386,7 @@ describe('LIVECHAT - Queue', () => { }); }); -(IS_EE ? describe : describe.skip)('Livechat - Chat limits', () => { +(IS_EE ? describe : describe.skip)('Livechat - Chat limits - AutoSelection', () => { let testUser: { user: IUser; credentials: Credentials }; let testDepartment: ILivechatDepartment; let testDepartment2: ILivechatDepartment; @@ -609,3 +609,272 @@ describe('LIVECHAT - Queue', () => { expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); }); }); + +// Note: didn't add for LoadRotation as everything that changes is how the agent is selected +// but the limits applicable are the same as load balance and autoselection +(IS_EE ? describe : describe.skip)('Livechat - Chat limits - LoadBalance', () => { + let testUser: { user: IUser; credentials: Credentials }; + let testUser2: { user: IUser; credentials: Credentials }; + let testDepartment: ILivechatDepartment; + let testDepartment2: ILivechatDepartment; + let testDepartment3: ILivechatDepartment; + + before((done) => getCredentials(done)); + + before(async () => + Promise.all([ + updateSetting('Livechat_enabled', true), + updateSetting('Livechat_Routing_Method', 'Load_Balancing'), + updateSetting('Omnichannel_enable_department_removal', true), + updateEESetting('Livechat_maximum_chats_per_agent', 0), + updateEESetting('Livechat_waiting_queue', true), + ]), + ); + + before(async () => { + const user = await createUser(); + const user2 = await createUser(); + await createAgent(user.username); + await createAgent(user2.username); + const credentials3 = await login(user.username, password); + const credentials4 = await login(user2.username, password); + await makeAgentAvailable(credentials3); + await makeAgentAvailable(credentials4); + + testUser = { + user, + credentials: credentials3, + }; + testUser2 = { + user: user2, + credentials: credentials4, + }; + }); + + before(async () => { + testDepartment = await createDepartment([{ agentId: testUser.user._id }], `${new Date().toISOString()}-department`, true, { + maxNumberSimultaneousChat: 2, + }); + testDepartment2 = await createDepartment([{ agentId: testUser.user._id }], `${new Date().toISOString()}-department2`, true, { + maxNumberSimultaneousChat: 2, + }); + testDepartment3 = await createDepartment( + [{ agentId: testUser.user._id }, { agentId: testUser2.user._id }], + `${new Date().toISOString()}-department3`, + true, + { + maxNumberSimultaneousChat: 2, + }, + ); + + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 1 }, [ + testDepartment._id, + testDepartment2._id, + testDepartment3._id, + ]); + await updateLivechatSettingsForUser(testUser2.user._id, { maxNumberSimultaneousChat: 1 }, [testDepartment3._id]); + }); + + after(async () => { + await Promise.all([ + deleteUser(testUser.user), + updateEESetting('Livechat_maximum_chats_per_agent', 0), + updateEESetting('Livechat_waiting_queue', false), + updateSetting('Livechat_Routing_Method', 'Auto_Selection'), + deleteDepartment(testDepartment._id), + deleteDepartment(testDepartment2._id), + deleteDepartment(testDepartment3._id), + ]); + await updateSetting('Omnichannel_enable_department_removal', false); + }); + + it('should allow a user to take a chat on a department since agent limit is set to 1 and department limit is set to 2 (agent has 0 chats)', async () => { + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + let previousChat: string; + it('should not allow a user to take a chat on a department since agent limit is set to 1 and department limit is set to 2 (agent has 1 chat)', async () => { + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should allow a user to take a chat on a department when agent limit is increased to 2 and department limit is set to 2 (agent has 1 chat)', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 2 }, [testDepartment._id, testDepartment2._id]); + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take chat on department B when agent limit is 2 and already has 2 chats', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should allow user to take a chat on department B when agent limit is increased to 3 and user has 2 chats on department A', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 3 }, [testDepartment._id, testDepartment2._id]); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should allow a user to take a chat on department B when agent limit is 0 and department B limit is 2 (user has 3 chats)', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 0 }, [testDepartment._id, testDepartment2._id]); + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take a chat on department B when is on the limit (user has 4 chats, 2 chats on department B)', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should not allow user to take a chat on department B even if global limit allows it (user has 4 chats)', async () => { + await updateEESetting('Livechat_maximum_chats_per_agent', 6); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.undefined; + }); + it('should allow user to take a chat when department B limit is removed and its below global limit (user has 4 chats)', async () => { + await updateDepartment({ departmentId: testDepartment2._id, opts: { maxNumberSimultaneousChat: 0 }, userCredentials: credentials }); + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should allow user to take a chat on department B (user has 5 chats, global limit is 6, department limit is 0)', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take a chat on department B (user has 6 chats, global limit is 6, department limit is 0)', async () => { + const visitor = await createVisitor(testDepartment2._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + it('should allow user to take chat once the global limit is removed (user has 7 chats)', async () => { + await updateEESetting('Livechat_maximum_chats_per_agent', 0); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should not allow user to take chat on department A (as limit for it hasnt changed)', async () => { + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + }); + + it('should allow user to take a chat on department A when its limit gets removed (no agent, global or department filter are applied)', async () => { + await updateDepartment({ departmentId: testDepartment._id, opts: { maxNumberSimultaneousChat: 0 }, userCredentials: credentials }); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should honor agent limit over global limit when both are set (user has 8 chats)', async () => { + await updateEESetting('Livechat_maximum_chats_per_agent', 100000); + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 4 }, [testDepartment._id, testDepartment2._id]); + + const visitor = await createVisitor(testDepartment._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.undefined; + previousChat = room._id; + }); + + // We already tested this case but this way the queue ends up empty :) + it('should receive the chat after agent limit is removed', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 0 }, [testDepartment._id, testDepartment2._id]); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(previousChat); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser.user._id); + }); + + it('should route the chat to another agent if limit for agent A is reached and agent B is available', async () => { + await updateLivechatSettingsForUser(testUser.user._id, { maxNumberSimultaneousChat: 1 }, [ + testDepartment._id, + testDepartment2._id, + testDepartment3._id, + ]); + + const visitor = await createVisitor(testDepartment3._id); + const room = await createLivechatRoom(visitor.token); + + await sleep(5000); + const roomInfo = await getLivechatRoomInfo(room._id); + + expect(roomInfo.servedBy).to.be.an('object'); + expect(roomInfo.servedBy?._id).to.be.equal(testUser2.user._id); + }); +}); diff --git a/packages/core-typings/src/ILivechatAgent.ts b/packages/core-typings/src/ILivechatAgent.ts index f106239400b17..4a67930942b2d 100644 --- a/packages/core-typings/src/ILivechatAgent.ts +++ b/packages/core-typings/src/ILivechatAgent.ts @@ -15,3 +15,10 @@ export interface ILivechatAgent extends IUser { livechatStatusSystemModified?: boolean; openBusinessHours?: string[]; } + +export type AvailableAgentsAggregation = { + agentId: string; + username: string; + maxChatsForAgent: number; + queueInfo: { chats: number; chatsForDepartment?: number }; +}; diff --git a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts index 827b266421b32..5435f1ea4458f 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentAgentsModel.ts @@ -1,4 +1,4 @@ -import type { ILivechatDepartmentAgents, IUser } from '@rocket.chat/core-typings'; +import type { AvailableAgentsAggregation, ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; import type { DeleteResult, FindCursor, FindOptions, Document, UpdateResult, Filter, AggregationCursor } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; @@ -57,7 +57,7 @@ export interface ILivechatDepartmentAgentsModel extends IBaseModel, + extraQuery?: Filter, ): Promise | null | undefined>; checkOnlineForDepartment(departmentId: string): Promise; getOnlineForDepartment( diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index f9089d90132e2..63039ab40d025 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -1,4 +1,5 @@ import type { + AvailableAgentsAggregation, IUser, IRole, ILivechatAgent, @@ -100,12 +101,14 @@ export interface IUsersModel extends IBaseModel { department?: string, ignoreAgentId?: string, isEnabledWhenAgentIdle?: boolean, - ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; count: number; departments?: any[] }>; + ignoreUsernames?: string[], + ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; count: number }>; getLastAvailableAgentRouted( department?: string, ignoreAgentId?: string, isEnabledWhenAgentIdle?: boolean, - ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; departments?: any[] }>; + ignoreUsernames?: string[], + ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date }>; setLastRoutingTime(userId: IUser['_id']): Promise | null>; @@ -251,7 +254,10 @@ export interface IUsersModel extends IBaseModel { isLivechatEnabledWhenAgentIdle?: boolean, ): FindCursor; countOnlineUserFromList(userList: string | string[], isLivechatEnabledWhenAgentIdle?: boolean): Promise; - getUnavailableAgents(departmentId?: string, extraQuery?: Document): Promise<{ username: string }[]>; + getUnavailableAgents( + departmentId?: string, + extraQuery?: Filter, + ): Promise[]>; findOneOnlineAgentByUserList( userList: string[] | string, options?: FindOptions, @@ -290,7 +296,7 @@ export interface IUsersModel extends IBaseModel { countAgents(): Promise; getNextAgent( ignoreAgentId?: string, - extraQuery?: Filter, + extraQuery?: Filter, enabledWhenAgentIdle?: boolean, ): Promise<{ agentId: string; username?: string } | null>; getNextBotAgent(ignoreAgentId?: string): Promise<{ agentId: string; username?: string } | null>; diff --git a/packages/models/src/models/LivechatDepartmentAgents.ts b/packages/models/src/models/LivechatDepartmentAgents.ts index 064a59704154c..ddb5b7f40e518 100644 --- a/packages/models/src/models/LivechatDepartmentAgents.ts +++ b/packages/models/src/models/LivechatDepartmentAgents.ts @@ -1,4 +1,4 @@ -import type { ILivechatDepartmentAgents, RocketChatRecordDeleted, IUser } from '@rocket.chat/core-typings'; +import type { AvailableAgentsAggregation, ILivechatDepartmentAgents, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { FindPaginated, ILivechatDepartmentAgentsModel } from '@rocket.chat/model-typings'; import type { Collection, @@ -177,7 +177,7 @@ export class LivechatDepartmentAgentsRaw extends BaseRaw, + extraQuery?: Filter, ): Promise | null | undefined> { const agents = await this.findByDepartmentId(departmentId).toArray(); diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index aeadbcf907ffc..9fa764fd0e9da 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -1,4 +1,5 @@ import type { + AvailableAgentsAggregation, AtLeast, DeepWritable, ILivechatAgent, @@ -544,10 +545,40 @@ export class UsersRaw extends BaseRaw> implements IU department?: string, ignoreAgentId?: string, isEnabledWhenAgentIdle?: boolean, + ignoreUsernames?: string[], ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; count: number; departments?: any[] }> { - const match = queryStatusAgentOnline({ ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }) }, isEnabledWhenAgentIdle); + const match = queryStatusAgentOnline( + { ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }), ...(ignoreUsernames?.length && { username: { $nin: ignoreUsernames } }) }, + isEnabledWhenAgentIdle, + ); + + const departmentFilter = department + ? [ + { + $lookup: { + from: 'rocketchat_livechat_department_agents', + let: { userId: '$_id' }, + pipeline: [ + { + $match: { + $expr: { + $and: [{ $eq: ['$$userId', '$agentId'] }, { $eq: ['$departmentId', department] }], + }, + }, + }, + ], + as: 'department', + }, + }, + { + $match: { department: { $size: 1 } }, + }, + ] + : []; + const aggregate: Document[] = [ { $match: match }, + ...departmentFilter, { $lookup: { from: 'rocketchat_subscription', @@ -569,35 +600,20 @@ export class UsersRaw extends BaseRaw> implements IU as: 'subs', }, }, - { - $lookup: { - from: 'rocketchat_livechat_department_agents', - localField: '_id', - foreignField: 'agentId', - as: 'departments', - }, - }, { $project: { agentId: '$_id', username: 1, lastRoutingTime: 1, - departments: 1, count: { $size: '$subs' }, }, }, { $sort: { count: 1, lastRoutingTime: 1, username: 1 } }, + { $limit: 1 }, ]; - if (department) { - aggregate.push({ $unwind: '$departments' }); - aggregate.push({ $match: { 'departments.departmentId': department } }); - } - - aggregate.push({ $limit: 1 }); - const [agent] = await this.col - .aggregate<{ agentId: string; username?: string; lastRoutingTime?: Date; count: number; departments?: any[] }>(aggregate) + .aggregate<{ agentId: string; username?: string; lastRoutingTime?: Date; count: number }>(aggregate) .toArray(); if (agent) { await this.setLastRoutingTime(agent.agentId); @@ -610,32 +626,46 @@ export class UsersRaw extends BaseRaw> implements IU department?: string, ignoreAgentId?: string, isEnabledWhenAgentIdle?: boolean, + ignoreUsernames?: string[], ): Promise<{ agentId: string; username?: string; lastRoutingTime?: Date; departments?: any[] }> { - const match = queryStatusAgentOnline({ ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }) }, isEnabledWhenAgentIdle); + const match = queryStatusAgentOnline( + { ...(ignoreAgentId && { _id: { $ne: ignoreAgentId } }), ...(ignoreUsernames?.length && { username: { $nin: ignoreUsernames } }) }, + isEnabledWhenAgentIdle, + ); + const departmentFilter = department + ? [ + { + $lookup: { + from: 'rocketchat_livechat_department_agents', + let: { userId: '$_id' }, + pipeline: [ + { + $match: { + $expr: { + $and: [{ $eq: ['$$userId', '$agentId'] }, { $eq: ['$departmentId', department] }], + }, + }, + }, + ], + as: 'department', + }, + }, + { + $match: { department: { $size: 1 } }, + }, + ] + : []; + const aggregate: Document[] = [ { $match: match }, - { - $lookup: { - from: 'rocketchat_livechat_department_agents', - localField: '_id', - foreignField: 'agentId', - as: 'departments', - }, - }, - { $project: { agentId: '$_id', username: 1, lastRoutingTime: 1, departments: 1 } }, + ...departmentFilter, + { $project: { agentId: '$_id', username: 1, lastRoutingTime: 1 } }, { $sort: { lastRoutingTime: 1, username: 1 } }, ]; - if (department) { - aggregate.push({ $unwind: '$departments' }); - aggregate.push({ $match: { 'departments.departmentId': department } }); - } - aggregate.push({ $limit: 1 }); - const [agent] = await this.col - .aggregate<{ agentId: string; username?: string; lastRoutingTime?: Date; departments?: any[] }>(aggregate) - .toArray(); + const [agent] = await this.col.aggregate<{ agentId: string; username?: string; lastRoutingTime?: Date }>(aggregate).toArray(); if (agent) { await this.setLastRoutingTime(agent.agentId); } @@ -1628,7 +1658,10 @@ export class UsersRaw extends BaseRaw> implements IU return this.findOne(query, options); } - async getUnavailableAgents(_departmentId?: string, _extraQuery?: Document): Promise<{ username: string }[]> { + async getUnavailableAgents( + _departmentId?: string, + _extraQuery?: Filter, + ): Promise[]> { return []; } @@ -1910,7 +1943,7 @@ export class UsersRaw extends BaseRaw> implements IU } // 2 - async getNextAgent(ignoreAgentId?: string, extraQuery?: Filter, enabledWhenAgentIdle?: boolean) { + async getNextAgent(ignoreAgentId?: string, extraQuery?: Filter, enabledWhenAgentIdle?: boolean) { // TODO: Create class Agent // fetch all unavailable agents, and exclude them from the selection const unavailableAgents = (await this.getUnavailableAgents(undefined, extraQuery)).map((u) => u.username); From 3309dac86aaa331af0eeaba4a9a2620d8bd74423 Mon Sep 17 00:00:00 2001 From: Lucas Pelegrino Date: Fri, 18 Apr 2025 02:05:08 -0300 Subject: [PATCH 143/187] fix: Update Contact's custom fields when updating visitor custom fields (#35580) Co-authored-by: Diego Sampaio <8591547+sampaiodiego@users.noreply.github.com> --- .changeset/young-kiwis-fly.md | 5 + .../app/livechat/server/api/v1/customField.ts | 9 +- .../app/livechat/server/api/v1/visitor.ts | 16 +- .../app/livechat/server/lib/custom-fields.ts | 38 ++- .../api/livechat/03-custom-fields.ts | 246 +++++++++++++++++- .../src/models/ILivechatContactsModel.ts | 1 + .../models/src/models/LivechatContacts.ts | 4 + 7 files changed, 301 insertions(+), 18 deletions(-) create mode 100644 .changeset/young-kiwis-fly.md diff --git a/.changeset/young-kiwis-fly.md b/.changeset/young-kiwis-fly.md new file mode 100644 index 0000000000000..468668c9a9eaa --- /dev/null +++ b/.changeset/young-kiwis-fly.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes contact custom fields not being updated when updating a visitor's custom field diff --git a/apps/meteor/app/livechat/server/api/v1/customField.ts b/apps/meteor/app/livechat/server/api/v1/customField.ts index a4de976407871..c678824d905e1 100644 --- a/apps/meteor/app/livechat/server/api/v1/customField.ts +++ b/apps/meteor/app/livechat/server/api/v1/customField.ts @@ -12,15 +12,12 @@ API.v1.addRoute( { async post() { const { token, key, value, overwrite } = this.bodyParams; - const guest = await findGuest(token); if (!guest) { throw new Error('invalid-token'); } - if (!(await setCustomFields({ token, key, value, overwrite }))) { - return API.v1.failure(); - } + await setCustomFields({ token, key, value, overwrite }); return API.v1.success({ field: { key, value, overwrite } }); }, @@ -46,9 +43,7 @@ API.v1.addRoute( overwrite: boolean; }): Promise<{ Key: string; value: string; overwrite: boolean }> => { const data = Object.assign({ token }, customField); - if (!(await setCustomFields(data))) { - throw new Error('error-setting-custom-field'); - } + await setCustomFields(data); return { Key: customField.key, value: customField.value, overwrite: customField.overwrite }; }, diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index d685f3e59757a..e2195738966b0 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -1,12 +1,12 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { LivechatVisitors as VisitorsRaw, LivechatCustomField, LivechatRooms } from '@rocket.chat/models'; +import type { IRoom, ILivechatCustomField } from '@rocket.chat/core-typings'; +import { LivechatVisitors as VisitorsRaw, LivechatCustomField, LivechatRooms, LivechatContacts } from '@rocket.chat/models'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../../../lib/callbacks'; import { API } from '../../../../api/server'; import { settings } from '../../../../settings/server'; -import { validateRequiredCustomFields } from '../../lib/custom-fields'; +import { updateContactsCustomFields, validateRequiredCustomFields } from '../../lib/custom-fields'; import { registerGuest, removeGuest, notifyGuestStatusChanged } from '../../lib/guests'; import { livechatLogger } from '../../lib/logger'; import { saveRoomInfo } from '../../lib/rooms'; @@ -93,9 +93,9 @@ API.v1.addRoute( ).toArray(); validateRequiredCustomFields(keys, livechatCustomFields); - const matchingCustomFields = livechatCustomFields.filter((field) => keys.includes(field._id)); + const matchingCustomFields = livechatCustomFields.filter((field: ILivechatCustomField) => keys.includes(field._id)); const processedKeys = await Promise.all( - matchingCustomFields.map(async (field) => { + matchingCustomFields.map(async (field: ILivechatCustomField) => { const customField = customFields.find((f) => f.key === field._id); if (!customField) { return; @@ -107,6 +107,12 @@ API.v1.addRoute( errors.push(key); } + // TODO deduplicate this code and the one at the function setCustomFields (apps/meteor/app/livechat/server/lib/custom-fields.ts) + const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray(); + if (contacts.length > 0) { + await Promise.all(contacts.map((contact) => updateContactsCustomFields(contact, key, value, overwrite))); + } + return key; }), ); diff --git a/apps/meteor/app/livechat/server/lib/custom-fields.ts b/apps/meteor/app/livechat/server/lib/custom-fields.ts index c52781c3c4602..e63fb2e43c6dc 100644 --- a/apps/meteor/app/livechat/server/lib/custom-fields.ts +++ b/apps/meteor/app/livechat/server/lib/custom-fields.ts @@ -1,5 +1,5 @@ -import type { ILivechatCustomField } from '@rocket.chat/core-typings'; -import { LivechatCustomField, LivechatRooms, LivechatVisitors } from '@rocket.chat/models'; +import type { ILivechatContact, ILivechatCustomField } from '@rocket.chat/core-typings'; +import { LivechatContacts, LivechatCustomField, LivechatRooms, LivechatVisitors } from '@rocket.chat/models'; import { livechatLogger } from './logger'; import { i18n } from '../../../utils/lib/i18n'; @@ -19,7 +19,31 @@ export const validateRequiredCustomFields = (customFields: string[], livechatCus } }; -export async function setCustomFields({ token, key, value, overwrite }: { key: string; value: string; overwrite: boolean; token: string }) { +export async function updateContactsCustomFields(contact: ILivechatContact, key: string, value: string, overwrite: boolean): Promise { + if (overwrite || !contact.customFields || !contact.customFields[key]) { + contact.customFields ??= {}; + contact.customFields[key] = value; + } else { + contact.conflictingFields ??= []; + contact.conflictingFields.push({ field: `customFields.${key}`, value }); + } + + await LivechatContacts.updateContact(contact._id, { customFields: contact.customFields, conflictingFields: contact.conflictingFields }); + + livechatLogger.debug({ msg: `Contact ${contact._id} updated with custom fields` }); +} + +export async function setCustomFields({ + token, + key, + value, + overwrite, +}: { + key: string; + value: string; + overwrite: boolean; + token: string; +}): Promise { livechatLogger.debug(`Setting custom fields data for visitor with token ${token}`); const customField = await LivechatCustomField.findOneById(key); @@ -39,6 +63,14 @@ export async function setCustomFields({ token, key, value, overwrite }: { key: s result = await LivechatRooms.updateDataByToken(token, key, value, overwrite); } else { result = await LivechatVisitors.updateLivechatDataByToken(token, key, value, overwrite); + + const visitor = await LivechatVisitors.getVisitorByToken(token, { projection: { _id: 1 } }); + if (visitor) { + const contacts = await LivechatContacts.findAllByVisitorId(visitor._id).toArray(); + if (contacts.length > 0) { + await Promise.all(contacts.map((contact) => updateContactsCustomFields(contact, key, value, overwrite))); + } + } } if (typeof result === 'boolean') { diff --git a/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts b/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts index 75c1eaa622906..d18e1aa4974ad 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.ts @@ -1,3 +1,4 @@ +import type { ILivechatVisitor } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { before, describe, it } from 'mocha'; import type { Response } from 'supertest'; @@ -124,7 +125,13 @@ describe('LIVECHAT - custom fields', () => { await request.post(api('livechat/custom.fields')).send({ token: 'invalid-token' }).expect(400); }); it('should fail if customFields is not an array', async () => { - await request.post(api('livechat/custom.fields')).send({ token: 'invalid-token', customFields: 'invalid-custom-fields' }).expect(400); + await request + .post(api('livechat/custom.fields')) + .send({ + token: 'invalid-token', + customFields: 'invalid-custom-fields', + }) + .expect(400); }); it('should fail if customFields is an empty array', async () => { await request.post(api('livechat/custom.fields')).send({ token: 'invalid-token', customFields: [] }).expect(400); @@ -138,7 +145,10 @@ describe('LIVECHAT - custom fields', () => { it('should fail if token is not a valid token', async () => { await request .post(api('livechat/custom.fields')) - .send({ token: 'invalid-token', customFields: [{ key: 'invalid-key', value: 'invalid-value', overwrite: true }] }) + .send({ + token: 'invalid-token', + customFields: [{ key: 'invalid-key', value: 'invalid-value', overwrite: true }], + }) .expect(400); }); it('should fail when customFields.key is invalid', async () => { @@ -166,7 +176,10 @@ describe('LIVECHAT - custom fields', () => { const { body } = await request .post(api('livechat/custom.fields')) - .send({ token: visitor.token, customFields: [{ key: customFieldName, value: 'test_address', overwrite: true }] }) + .send({ + token: visitor.token, + customFields: [{ key: customFieldName, value: 'test_address', overwrite: true }], + }) .expect(200); expect(body).to.have.property('success', true); @@ -176,4 +189,231 @@ describe('LIVECHAT - custom fields', () => { expect(body.fields[0]).to.have.property('value', 'test_address'); }); }); + + describe('livechat/custom.field [with Contacts]', () => { + let visitor: ILivechatVisitor; + let contactId: string; + + const customFieldName = `custom_field_${Date.now()}`; + const customFieldValue = 'custom-field-value'; + + before(async () => { + await updatePermission('create-livechat-contact', ['admin']); + await updatePermission('view-livechat-contact', ['admin']); + + // Create a Visitor + visitor = await createVisitor(); + + // Create a Contact and store id on var + await request + .post(api('omnichannel/contacts')) + .set(credentials) + .send({ + name: visitor.name, + emails: [visitor.visitorEmails?.[0].address], + phones: [visitor.phone?.[0].phoneNumber], + }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contactId'); + + contactId = res.body.contactId; + }); + + await request.get(api('livechat/room')).query({ token: visitor.token }); + + // Create Custom Field + await createCustomField({ + searchable: true, + field: customFieldName, + label: customFieldName, + defaultValue: 'test_default_address', + scope: 'visitor', + visibility: 'public', + regexp: '', + }); + + await createCustomField({ + searchable: true, + field: `${customFieldName}_2`, + label: `${customFieldName}_2`, + defaultValue: 'test_default_address', + scope: 'visitor', + visibility: 'public', + regexp: '', + }); + }); + + it('should save the custom field on Contact when available', async () => { + // Save the custom field on Visitor/Contact + await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: customFieldName, value: customFieldValue, overwrite: true }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + // Fetch the visitor to validate custom fields are properly set. + await request + .get(api(`livechat/visitor/${visitor.token}`)) + .set(credentials) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.visitor).to.have.property('livechatData'); + expect(res.body.visitor.livechatData).to.have.property(customFieldName, customFieldValue); + }); + + // Fetch the visitor's contact to validate custom fields are properly set. + await request + .get(api(`omnichannel/contacts.get`)) + .set(credentials) + .query({ contactId }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.have.property('customFields'); + expect(res.body.contact.customFields).to.have.property(customFieldName, customFieldValue); + }); + }); + + it('Should save multiple custom fields on Contact when available', async () => { + // Save the custom field on Visitor/Contact + await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: customFieldName, value: customFieldValue, overwrite: true }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: `${customFieldName}_2`, value: customFieldValue, overwrite: true }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + // Fetch the visitor to validate custom fields are properly set. + await request + .get(api(`livechat/visitor/${visitor.token}`)) + .set(credentials) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.visitor).to.have.property('livechatData'); + expect(res.body.visitor.livechatData).to.have.property(customFieldName, customFieldValue); + expect(res.body.visitor.livechatData).to.have.property(`${customFieldName}_2`, customFieldValue); + }); + + // Fetch the visitor's contact to validate custom fields are properly set. + await request + .get(api(`omnichannel/contacts.get`)) + .set(credentials) + .query({ contactId }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.have.property('customFields'); + expect(res.body.contact.customFields).to.have.property(customFieldName, customFieldValue); + expect(res.body.contact.customFields).to.have.property(`${customFieldName}_2`, customFieldValue); + }); + }); + + it('should add the custom field as conflict on Contact when overwrite is false', async () => { + const conflictingFieldValue = 'conflicting-custom-field-value'; + + // Save the custom field on Contact + await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: customFieldName, value: conflictingFieldValue, overwrite: false }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + // Fetch the visitor to validate custom fields are properly set. + await request + .get(api(`livechat/visitor/${visitor.token}`)) + .set(credentials) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.visitor).to.have.property('livechatData'); + expect(res.body.visitor.livechatData).to.have.property(customFieldName, customFieldValue); + }); + + // Fetch the visitor's contact to validate custom fields are properly set. + await request + .get(api(`omnichannel/contacts.get`)) + .set(credentials) + .query({ contactId }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.have.property('customFields'); + expect(res.body.contact.customFields).to.have.property(customFieldName, customFieldValue); + + // Validate custom fields contain both entries, indicating conflict criteria + expect(res.body.contact.conflictingFields).to.have.lengthOf(1); + expect(res.body.contact.conflictingFields[0]).to.have.property('field', `customFields.${customFieldName}`); + expect(res.body.contact.conflictingFields[0]).to.have.property('value', conflictingFieldValue); + }); + }); + + it('should not save the custom field as a conflict on Contact when overwrite is false but custom field is not registered yet', async () => { + await createCustomField({ + searchable: true, + field: `${customFieldName}_3`, + label: `${customFieldName}_3`, + defaultValue: 'test_default_address', + scope: 'visitor', + visibility: 'public', + regexp: '', + }); + + // Save the custom field on Contact + await request + .post(api('livechat/custom.field')) + .send({ token: visitor.token, key: `${customFieldName}_3`, value: customFieldValue, overwrite: false }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + // Fetch the visitor to validate custom fields are properly set. + await request + .get(api(`livechat/visitor/${visitor.token}`)) + .set(credentials) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body.visitor).to.have.property('livechatData'); + expect(res.body.visitor.livechatData).to.have.property(`${customFieldName}_3`, customFieldValue); + }); + + // Fetch the visitor's contact to validate custom fields are properly set. + await request + .get(api(`omnichannel/contacts.get`)) + .set(credentials) + .query({ contactId }) + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('contact'); + expect(res.body.contact).to.have.property('customFields'); + expect(res.body.contact.customFields).to.have.property(`${customFieldName}_3`, customFieldValue); + + // Validate custom fields contain both entries, indicating conflict criteria + expect(res.body.contact.conflictingFields).to.have.lengthOf(1); + expect(res.body.contact.conflictingFields[0]).to.not.have.property('field', `customFields.${customFieldName}_3`); + }); + }); + }); }); diff --git a/packages/model-typings/src/models/ILivechatContactsModel.ts b/packages/model-typings/src/models/ILivechatContactsModel.ts index a61e731a79b30..b93ec3b304bef 100644 --- a/packages/model-typings/src/models/ILivechatContactsModel.ts +++ b/packages/model-typings/src/models/ILivechatContactsModel.ts @@ -68,4 +68,5 @@ export interface ILivechatContactsModel extends IBaseModel { countVerified(): Promise; countContactsWithoutChannels(): Promise; getStatistics(): AggregationCursor<{ totalConflicts: number; avgChannelsPerContact: number }>; + updateByVisitorId(visitorId: string, update: UpdateFilter, options?: UpdateOptions): Promise; } diff --git a/packages/models/src/models/LivechatContacts.ts b/packages/models/src/models/LivechatContacts.ts index 5e0a30c8dd81b..927d7b82a6bef 100644 --- a/packages/models/src/models/LivechatContacts.ts +++ b/packages/models/src/models/LivechatContacts.ts @@ -374,4 +374,8 @@ export class LivechatContactsRaw extends BaseRaw implements IL { allowDiskUse: true, readPreference: readSecondaryPreferred() }, ); } + + updateByVisitorId(visitorId: string, update: UpdateFilter, options?: UpdateOptions): Promise { + return this.updateOne({ 'channels.visitor.visitorId': visitorId }, update, options); + } } From cc344bea08c08501f50e9cee620b2926a322a4ee Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Fri, 18 Apr 2025 03:48:50 -0300 Subject: [PATCH 144/187] feat: improve UX for logged users with mandatory 2FA roles (#35709) --- .changeset/ten-schools-collect.md | 6 ++ apps/meteor/client/components/TextCopy.tsx | 2 +- .../account/security/AccountSecurityPage.tsx | 14 +++- .../views/account/security/TwoFactorEmail.tsx | 43 +++++------ .../views/account/security/TwoFactorTOTP.tsx | 77 ++++++++++++------- .../client/views/hooks/useRequire2faSetup.ts | 22 ++++++ .../MainLayout/TwoFactorAuthSetupCheck.tsx | 27 +++---- .../MainLayout/TwoFactorRequiredModal.tsx | 31 ++++++++ apps/meteor/tests/e2e/account-profile.spec.ts | 17 ++-- apps/meteor/tests/e2e/enforce-2FA.spec.ts | 8 +- .../tests/e2e/page-objects/account-profile.ts | 12 ++- packages/i18n/src/locales/af.i18n.json | 5 +- packages/i18n/src/locales/ar.i18n.json | 8 +- packages/i18n/src/locales/az.i18n.json | 5 +- packages/i18n/src/locales/be-BY.i18n.json | 5 +- packages/i18n/src/locales/bg.i18n.json | 5 +- packages/i18n/src/locales/bs.i18n.json | 5 +- packages/i18n/src/locales/ca.i18n.json | 8 +- packages/i18n/src/locales/cs.i18n.json | 8 +- packages/i18n/src/locales/cy.i18n.json | 5 +- packages/i18n/src/locales/da.i18n.json | 8 +- packages/i18n/src/locales/de-AT.i18n.json | 5 +- packages/i18n/src/locales/de-IN.i18n.json | 5 +- packages/i18n/src/locales/de.i18n.json | 8 +- packages/i18n/src/locales/el.i18n.json | 5 +- packages/i18n/src/locales/en.i18n.json | 15 ++-- packages/i18n/src/locales/eo.i18n.json | 5 +- packages/i18n/src/locales/es.i18n.json | 8 +- packages/i18n/src/locales/fa.i18n.json | 5 +- packages/i18n/src/locales/fi.i18n.json | 8 +- packages/i18n/src/locales/fr.i18n.json | 8 +- packages/i18n/src/locales/hi-IN.i18n.json | 8 +- packages/i18n/src/locales/hr.i18n.json | 5 +- packages/i18n/src/locales/hu.i18n.json | 8 +- packages/i18n/src/locales/id.i18n.json | 5 +- packages/i18n/src/locales/it.i18n.json | 5 +- packages/i18n/src/locales/ja.i18n.json | 8 +- packages/i18n/src/locales/ka-GE.i18n.json | 8 +- packages/i18n/src/locales/km.i18n.json | 5 +- packages/i18n/src/locales/ko.i18n.json | 8 +- packages/i18n/src/locales/ku.i18n.json | 5 +- packages/i18n/src/locales/lo.i18n.json | 5 +- packages/i18n/src/locales/lt.i18n.json | 5 +- packages/i18n/src/locales/lv.i18n.json | 5 +- packages/i18n/src/locales/mn.i18n.json | 5 +- packages/i18n/src/locales/ms-MY.i18n.json | 5 +- packages/i18n/src/locales/nb.i18n.json | 8 +- packages/i18n/src/locales/nl.i18n.json | 8 +- packages/i18n/src/locales/nn.i18n.json | 8 +- packages/i18n/src/locales/pl.i18n.json | 8 +- packages/i18n/src/locales/pt-BR.i18n.json | 8 +- packages/i18n/src/locales/pt.i18n.json | 5 +- packages/i18n/src/locales/ro.i18n.json | 5 +- packages/i18n/src/locales/ru.i18n.json | 8 +- packages/i18n/src/locales/sk-SK.i18n.json | 5 +- packages/i18n/src/locales/sl-SI.i18n.json | 5 +- packages/i18n/src/locales/sq.i18n.json | 5 +- packages/i18n/src/locales/sr.i18n.json | 4 +- packages/i18n/src/locales/sv.i18n.json | 8 +- packages/i18n/src/locales/ta-IN.i18n.json | 5 +- packages/i18n/src/locales/th-TH.i18n.json | 5 +- packages/i18n/src/locales/tr.i18n.json | 5 +- packages/i18n/src/locales/uk.i18n.json | 7 +- packages/i18n/src/locales/vi-VN.i18n.json | 5 +- packages/i18n/src/locales/zh-HK.i18n.json | 5 +- packages/i18n/src/locales/zh-TW.i18n.json | 8 +- packages/i18n/src/locales/zh.i18n.json | 8 +- 67 files changed, 230 insertions(+), 386 deletions(-) create mode 100644 .changeset/ten-schools-collect.md create mode 100644 apps/meteor/client/views/hooks/useRequire2faSetup.ts create mode 100644 apps/meteor/client/views/root/MainLayout/TwoFactorRequiredModal.tsx diff --git a/.changeset/ten-schools-collect.md b/.changeset/ten-schools-collect.md new file mode 100644 index 0000000000000..d67233ddb4a53 --- /dev/null +++ b/.changeset/ten-schools-collect.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/i18n": patch +--- + +Improves UX for users with mandatory 2FA roles by clarifying required actions diff --git a/apps/meteor/client/components/TextCopy.tsx b/apps/meteor/client/components/TextCopy.tsx index f2cddcd85d7dd..3a6481ec7ce43 100644 --- a/apps/meteor/client/components/TextCopy.tsx +++ b/apps/meteor/client/components/TextCopy.tsx @@ -31,7 +31,7 @@ const TextCopy = ({ text, wrapper = defaultWrapperRenderer, ...props }: TextCopy justifyContent='stretch' alignItems='flex-start' flexGrow={1} - padding={16} + pb={16} backgroundColor='surface' width='full' {...props} diff --git a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx index db79ea5b22e6e..3033161d1aca5 100644 --- a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx +++ b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx @@ -1,4 +1,4 @@ -import { Box, Accordion, AccordionItem, ButtonGroup, Button } from '@rocket.chat/fuselage'; +import { Box, Accordion, AccordionItem, ButtonGroup, Button, Callout } from '@rocket.chat/fuselage'; import { useSetting, useTranslation, useUser } from '@rocket.chat/ui-contexts'; import { useId } from 'react'; import type { ReactElement } from 'react'; @@ -9,6 +9,7 @@ import EndToEnd from './EndToEnd'; import TwoFactorEmail from './TwoFactorEmail'; import TwoFactorTOTP from './TwoFactorTOTP'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useRequire2faSetup } from '../../hooks/useRequire2faSetup'; const passwordDefaultValues = { password: '', confirmationPassword: '' }; @@ -38,6 +39,8 @@ const AccountSecurityPage = (): ReactElement => { const passwordFormId = useId(); + const require2faSetup = useRequire2faSetup(); + return ( @@ -46,7 +49,7 @@ const AccountSecurityPage = (): ReactElement => { {allowPasswordChange && ( - + @@ -54,7 +57,12 @@ const AccountSecurityPage = (): ReactElement => { )} {(twoFactorTOTP || showEmailTwoFactor) && twoFactorEnabled && ( - + + {require2faSetup && ( + + {t('Enable_two-factor_authentication_callout_description')} + + )} {twoFactorTOTP && } {showEmailTwoFactor && } diff --git a/apps/meteor/client/views/account/security/TwoFactorEmail.tsx b/apps/meteor/client/views/account/security/TwoFactorEmail.tsx index c77c395cdefc2..5ff4e6d063102 100644 --- a/apps/meteor/client/views/account/security/TwoFactorEmail.tsx +++ b/apps/meteor/client/views/account/security/TwoFactorEmail.tsx @@ -1,7 +1,7 @@ -import { Box, Button, Margins } from '@rocket.chat/fuselage'; +import { Box, Field, FieldLabel, FieldRow, Margins, ToggleSwitch } from '@rocket.chat/fuselage'; import { useUser } from '@rocket.chat/ui-contexts'; -import type { ComponentProps } from 'react'; -import { useCallback } from 'react'; +import type { ComponentProps, FormEvent } from 'react'; +import { useCallback, useId } from 'react'; import { useTranslation } from 'react-i18next'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; @@ -9,6 +9,7 @@ import { useEndpointAction } from '../../../hooks/useEndpointAction'; const TwoFactorEmail = (props: ComponentProps) => { const { t } = useTranslation(); const user = useUser(); + const emailId = useId(); const isEnabled = user?.services?.email2fa?.enabled; @@ -19,30 +20,26 @@ const TwoFactorEmail = (props: ComponentProps) => { successMessage: t('Two-factor_authentication_disabled'), }); - const handleEnable = useCallback(async () => { - await enable2faAction(); - }, [enable2faAction]); - const handleDisable = useCallback(async () => { - await disable2faAction(); - }, [disable2faAction]); + const handleEnable = useCallback( + async (e: FormEvent) => { + if (e.currentTarget.checked) { + await enable2faAction(); + } else { + await disable2faAction(); + } + }, + [disable2faAction, enable2faAction], + ); return ( - {t('Two-factor_authentication_email')} - {isEnabled && ( - - )} - {!isEnabled && ( - <> - {t('Two-factor_authentication_email_is_currently_disabled')} - - - )} + + + {t('Two-factor_authentication_email')} + + + ); diff --git a/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx b/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx index a8d2230eec62e..1c7bac0937d0a 100644 --- a/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx +++ b/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx @@ -1,8 +1,8 @@ -import { Box, Button, TextInput, Margins } from '@rocket.chat/fuselage'; -import { useSafely } from '@rocket.chat/fuselage-hooks'; +import { Box, Button, TextInput, Margins, Field, FieldRow, FieldLabel, ToggleSwitch } from '@rocket.chat/fuselage'; +import { useEffectEvent, useSafely } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useUser, useMethod } from '@rocket.chat/ui-contexts'; -import type { ReactElement, ComponentPropsWithoutRef } from 'react'; -import { useState, useCallback, useEffect } from 'react'; +import type { ReactElement, ComponentPropsWithoutRef, FormEvent } from 'react'; +import { useState, useCallback, useEffect, useId } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import qrcode from 'yaqrcode'; @@ -51,7 +51,7 @@ const TwoFactorTOTP = (props: TwoFactorTOTPProps): ReactElement => { updateCodesRemaining(); }, [checkCodesRemainingFn, setCodesRemaining, totpEnabled]); - const handleEnableTotp = useCallback(async () => { + const enableTotp = useEffectEvent(async () => { try { const result = await enableTotpFn(); @@ -62,26 +62,46 @@ const TwoFactorTOTP = (props: TwoFactorTOTPProps): ReactElement => { } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } - }, [dispatchToastMessage, enableTotpFn, setQrCode, setRegisteringTotp, setTotpSecret]); + }); + + const disableTotp = useEffectEvent(async () => { + if (!totpEnabled) { + setRegisteringTotp(false); + + return; + } - const handleDisableTotp = useCallback(async () => { const onDisable = async (authCode: string): Promise => { try { const result = await disableTotpFn(authCode); if (!result) { - return dispatchToastMessage({ type: 'error', message: t('Invalid_two_factor_code') }); + dispatchToastMessage({ type: 'error', message: t('Invalid_two_factor_code') }); + + return; } dispatchToastMessage({ type: 'success', message: t('Two-factor_authentication_disabled') }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } + closeModal(); }; setModal(); - }, [closeModal, disableTotpFn, dispatchToastMessage, setModal, t]); + }); + + const handleToggleTotp = useEffectEvent(async (e: FormEvent) => { + if (e.currentTarget?.checked) { + void enableTotp(); + } else { + void disableTotp(); + } + }); + + const totpId = useId(); + const totpCodeId = useId(); const handleVerifyCode = useCallback( async ({ authCode }: TwoFactorTOTPFormData) => { @@ -94,6 +114,8 @@ const TwoFactorTOTP = (props: TwoFactorTOTPProps): ReactElement => { setRegisteringTotp(false); setModal(); + + dispatchToastMessage({ type: 'success', message: t('Two-factor_authentication_enabled') }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } @@ -121,38 +143,35 @@ const TwoFactorTOTP = (props: TwoFactorTOTPProps): ReactElement => { return ( - {t('Two-factor_authentication_via_TOTP')} - {!totpEnabled && !registeringTotp && ( - <> - {t('Two-factor_authentication_is_currently_disabled')} - - - )} + + + {t('Two-factor_authentication_via_TOTP')} + + + {!totpEnabled && registeringTotp && ( <> {t('Scan_QR_code')} {t('Scan_QR_code_alternative_s')} -
+ + +
+ + + +
+
+ {null} +
) } body={ diff --git a/apps/meteor/client/views/room/RoomNotFound.tsx b/apps/meteor/client/views/room/RoomNotFound.tsx index 6c3b849a60b35..3d13b1a5e930f 100644 --- a/apps/meteor/client/views/room/RoomNotFound.tsx +++ b/apps/meteor/client/views/room/RoomNotFound.tsx @@ -1,12 +1,12 @@ import { Box } from '@rocket.chat/fuselage'; -import { Header, HeaderToolbar } from '@rocket.chat/ui-client'; +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn, Header, HeaderToolbar } from '@rocket.chat/ui-client'; import { useLayout } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import RoomLayout from './layout/RoomLayout'; import NotFoundState from '../../components/NotFoundState'; -import SidebarToggler from '../../components/SidebarToggler'; +import { SidebarTogglerV2 } from '../../components/SidebarTogglerV2'; const RoomNotFound = (): ReactElement => { const { t } = useTranslation(); @@ -16,11 +16,16 @@ const RoomNotFound = (): ReactElement => { - - - -
+ + +
+ + + +
+
+ {null} +
) } body={ diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index a8b105222e4e3..b477cf664ac2b 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -90,11 +90,40 @@ test.describe.serial('feature preview', () => { await expect(poHomeChannel.navbar.navbar).toBeVisible(); }); - test('should display "Recent" button on sidebar search section, and display recent chats when clicked', async ({ page }) => { + test('should render global header navigation', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidebar.btnRecent.click(); - await expect(poHomeChannel.sidebar.sidebar.getByRole('heading', { name: 'Recent' })).toBeVisible(); + await test.step('should display recent chats when navbar search is clicked', async () => { + await poHomeChannel.navbar.searchInput.click(); + await expect(poHomeChannel.navbar.searchList).toBeVisible(); + await poHomeChannel.navbar.searchInput.blur(); + }); + + await test.step('should display home and directory button', async () => { + await expect(poHomeChannel.navbar.homeButton).toBeVisible(); + await expect(poHomeChannel.navbar.btnDirectory).toBeVisible(); + }); + + await test.step('should display home and directory inside a menu and sidebar toggler in tablet view', async () => { + await page.setViewportSize({ width: 1023, height: 767 }); + await expect(poHomeChannel.navbar.btnMenuPages).toBeVisible(); + await expect(poHomeChannel.navbar.btnSidebarToggler).toBeVisible(); + }); + + await test.step('should display voice and omnichannel items inside a menu in mobile view', async () => { + await page.setViewportSize({ width: 767, height: 510 }); + await expect(poHomeChannel.navbar.btnVoiceAndOmnichannel).toBeVisible(); + }); + + await test.step('should hide everything else when navbar search is focused in mobile view', async () => { + await page.setViewportSize({ width: 767, height: 510 }); + await poHomeChannel.navbar.searchInput.click(); + + await expect(poHomeChannel.navbar.btnMenuPages).not.toBeVisible(); + await expect(poHomeChannel.navbar.btnSidebarToggler).not.toBeVisible(); + await expect(poHomeChannel.navbar.btnVoiceAndOmnichannel).not.toBeVisible(); + await expect(poHomeChannel.navbar.groupHistoryNavigation).not.toBeVisible(); + }); }); test('should not display room topic in direct message', async ({ page }) => { @@ -169,10 +198,9 @@ test.describe.serial('feature preview', () => { test('should show unread badge on collapser when group is collapsed and has unread items', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidebar.openChat(targetChannel); + await poHomeChannel.navbar.openChat(targetChannel); await poHomeChannel.content.sendMessage('hello world'); - await poHomeChannel.sidebar.typeSearch(targetChannel); const item = poHomeChannel.sidebar.getSearchRoomByName(targetChannel); await poHomeChannel.sidebar.markItemAsUnread(item); await poHomeChannel.sidebar.escSearch(); @@ -185,7 +213,7 @@ test.describe.serial('feature preview', () => { test('should not show NavBar in embedded layout', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidebar.openChat(targetChannel); + await poHomeChannel.navbar.openChat(targetChannel); await expect(page.locator('role=navigation[name="header"]')).toBeVisible(); const embeddedLayoutURL = `${page.url()}?layout=embedded`; await page.goto(embeddedLayoutURL); @@ -194,7 +222,7 @@ test.describe.serial('feature preview', () => { test('should display the room header properly', async ({ page }) => { await page.goto('/home'); - await poHomeChannel.sidebar.openChat(targetDiscussion.fname); + await poHomeChannel.navbar.openChat(targetDiscussion.fname); await test.step('should not display avatar in room header', async () => { await expect(page.locator('main').locator('header').getByRole('figure')).not.toBeVisible(); @@ -330,8 +358,8 @@ test.describe.serial('feature preview', () => { await page.goto('/home'); const message = 'hello world'; - await poHomeChannel.sidebar.setDisplayMode('Extended'); - await poHomeChannel.sidebar.openChat(sidepanelTeam); + await poHomeChannel.navbar.setDisplayMode('Extended'); + await poHomeChannel.navbar.openChat(sidepanelTeam); await poHomeChannel.content.sendMessage(message); await expect(poHomeChannel.sidepanel.getExtendedItem(sidepanelTeam, message)).toBeVisible(); }); @@ -341,8 +369,8 @@ test.describe.serial('feature preview', () => { const message = 'hello > world'; const parsedWrong = 'hello > world'; - await poHomeChannel.sidebar.setDisplayMode('Extended'); - await poHomeChannel.sidebar.openChat(sidepanelTeam); + await poHomeChannel.navbar.setDisplayMode('Extended'); + await poHomeChannel.navbar.openChat(sidepanelTeam); await poHomeChannel.content.sendMessage(message); await expect(poHomeChannel.sidepanel.getExtendedItem(sidepanelTeam, message)).toBeVisible(); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts index 55cda22ed09ef..c9e432527d0b8 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts @@ -1,5 +1,7 @@ import type { Locator, Page } from '@playwright/test'; +import { expect } from '../../utils/test'; + export class Navbar { private readonly page: Page; @@ -11,11 +13,72 @@ export class Navbar { return this.page.getByRole('navigation', { name: 'header' }); } - get pagesToolbar(): Locator { - return this.navbar.getByRole('toolbar', { name: 'Pages' }); + get btnSidebarToggler(): Locator { + return this.navbar.getByRole('button', { name: 'Open sidebar' }); + } + + get btnVoiceAndOmnichannel(): Locator { + return this.navbar.getByRole('button', { name: 'Voice and omnichannel' }); + } + + get groupHistoryNavigation(): Locator { + return this.navbar.getByRole('group', { name: 'History navigation' }); + } + + get pagesGroup(): Locator { + return this.navbar.getByRole('group', { name: 'Pages and actions' }); } get homeButton(): Locator { - return this.pagesToolbar.getByRole('button', { name: 'Home' }); + return this.pagesGroup.getByRole('button', { name: 'Home' }); + } + + get btnDirectory(): Locator { + return this.pagesGroup.getByRole('button', { name: 'Directory' }); + } + + get btnMenuPages(): Locator { + return this.pagesGroup.getByRole('button', { name: 'Pages' }); + } + + get navbarSearchSection(): Locator { + return this.navbar.getByRole('search'); + } + + get searchInput(): Locator { + return this.navbarSearchSection.getByRole('combobox'); + } + + get searchList(): Locator { + return this.navbarSearchSection.getByRole('listbox', { name: 'Channels' }); + } + + async typeSearch(name: string): Promise { + return this.searchInput.fill(name); + } + + async waitForChannel(): Promise { + await this.page.locator('role=main').waitFor(); + await this.page.locator('role=main >> role=heading[level=1]').waitFor(); + const messageList = this.page.getByRole('main').getByRole('list', { name: 'Message list', exact: true }); + await messageList.waitFor(); + + await expect(messageList).not.toHaveAttribute('aria-busy', 'true'); + } + + getSearchRoomByName(name: string): Locator { + return this.searchList.getByRole('option', { name }); + } + + async openChat(name: string): Promise { + await this.typeSearch(name); + await this.getSearchRoomByName(name).click(); + await this.waitForChannel(); + } + + async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { + await this.pagesGroup.getByRole('button', { name: 'Display', exact: true }).click(); + await this.pagesGroup.getByRole('menuitemcheckbox', { name: mode }).click(); + await this.pagesGroup.click(); } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts index 6c546b2fc9368..27ae799b4e4d9 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts @@ -1,7 +1,5 @@ import type { Locator, Page } from '@playwright/test'; -import { expect } from '../../utils/test'; - export class Sidebar { private readonly page: Page; @@ -14,20 +12,12 @@ export class Sidebar { return this.page.getByRole('navigation', { name: 'sidebar' }); } - get sidebarSearchSection(): Locator { - return this.sidebar.getByRole('search'); - } - - get btnRecent(): Locator { - return this.sidebarSearchSection.getByRole('button', { name: 'Recent' }); - } - get channelsList(): Locator { return this.sidebar.getByRole('list', { name: 'Channels' }); } - get searchList(): Locator { - return this.sidebar.getByRole('search').getByRole('list', { name: 'Channels' }); + getSearchRoomByName(name: string) { + return this.channelsList.getByRole('link', { name }); } get firstCollapser(): Locator { @@ -38,43 +28,10 @@ export class Sidebar { return this.channelsList.getByRole('listitem').first(); } - get searchInput(): Locator { - return this.sidebarSearchSection.getByRole('searchbox'); - } - - async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { - await this.sidebarSearchSection.getByRole('button', { name: 'Display', exact: true }).click(); - await this.sidebarSearchSection.getByRole('menuitemcheckbox', { name: mode }).click(); - await this.sidebarSearchSection.click(); - } - async escSearch(): Promise { await this.page.keyboard.press('Escape'); } - async waitForChannel(): Promise { - await this.page.locator('role=main').waitFor(); - await this.page.locator('role=main >> role=heading[level=1]').waitFor(); - const messageList = this.page.getByRole('main').getByRole('list', { name: 'Message list', exact: true }); - await messageList.waitFor(); - - await expect(messageList).not.toHaveAttribute('aria-busy', 'true'); - } - - async typeSearch(name: string): Promise { - return this.searchInput.fill(name); - } - - getSearchRoomByName(name: string): Locator { - return this.searchList.getByRole('link', { name }); - } - - async openChat(name: string): Promise { - await this.typeSearch(name); - await this.getSearchRoomByName(name).click(); - await this.waitForChannel(); - } - async markItemAsUnread(item: Locator): Promise { await item.hover(); await item.focus(); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 31495c3ea1dd7..d20ebda4f568f 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -761,6 +761,7 @@ "away": "away", "Away": "Away", "Back": "Back", + "Back_in_history": "Back in history", "Back_to_applications": "Back to applications", "Back_to_chat": "Back to chat", "Back_to_integration_detail": "Back to the integration detail", @@ -2396,6 +2397,7 @@ "Estimated_due_time": "Estimated due time", "error-password-in-history": "Entered password has been previously used", "Forward": "Forward", + "Forward_in_history": "Forward in history", "Estimated_due_time_in_minutes": "Estimated due time (time in minutes)", "Forward_chat": "Forward chat", "Forward_to_department": "Forward to department", @@ -2510,6 +2512,7 @@ "Highlights_How_To": "To be notified when someone mentions a word or phrase, add it here. You can separate words or phrases with commas. Highlight Words are not case sensitive.", "Highlights_List": "Highlight words", "History": "History", + "History_navigation": "History navigation", "every_12_hours": "Once every 12 hours", "every_24_hours": "Once every 24 hours", "every_48_hours": "Once every 48 hours", @@ -4684,6 +4687,7 @@ "set-react-when-readonly_description": "Permission to set the ability to react to messages in a read only channel", "set-readonly": "Set ReadOnly", "Pages": "Pages", + "Pages_and_actions": "Pages and actions", "set-readonly_description": "Permission to set a channel to read only channel", "Settings": "Settings", "Setting": "Setting", @@ -5978,6 +5982,7 @@ "Troubleshoot_Force_Caching_Version": "Force browsers to clear networking cache based on version change", "Troubleshoot_Force_Caching_Version_Alert": "If the value provided is not empty and different from previous one the browsers will try to clear the cache. This setting should not be set for a long period since it affects the browser performance, please clear it as soon as possible.", "Try_now": "Try now", + "Try_entering_a_different_search_term": "Try entering a different search term.", "Try_different_filters": "Try different filters", "Try_searching_in_the_marketplace_instead": "Try searching in the Marketplace instead", "Turn_on_video": "Turn on video", @@ -6217,6 +6222,7 @@ "Visitor_Name_Placeholder": "Please enter a visitor name...", "Visitor_not_found": "Visitor not found", "Visitor_does_not_exist": "Visitor does not exist!", + "Voice_and_omnichannel": "Voice and omnichannel", "Voice_Call": "Voice Call", "Voice_call": "Voice call", "Voice_call_extension": "Voice call extension", @@ -6775,6 +6781,7 @@ "Zoom_out": "Zoom out", "Zoom_in": "Zoom in", "Close_gallery": "Close gallery", + "Close_sidebar": "Close sidebar", "Next_image": "Next Image", "Previous_image": "Previous image", "Image_gallery": "Image gallery", @@ -6784,7 +6791,7 @@ "You_cant_take_chats_offline": "You cannot take new conversations because you're offline", "New_navigation": "Enhanced navigation experience", "New_navigation_description": "Explore our improved navigation, designed with clear scopes for easy access to what you need. This change serves as the foundation for future advancements in navigation management.", - "Workspace_and_user_settings": "Workspace and user settings", + "Workspace_and_user_preferences": "Workspace and user preferences", "Sidebar_Sections_Order": "Sidebar sections order", "Sidebar_Sections_Order_Description": "Select the categories in your preferred order", "Incoming_Calls": "Incoming calls", diff --git a/packages/i18n/src/locales/nb.i18n.json b/packages/i18n/src/locales/nb.i18n.json index 6915b9d7bc663..e48ae441ee9cb 100644 --- a/packages/i18n/src/locales/nb.i18n.json +++ b/packages/i18n/src/locales/nb.i18n.json @@ -6159,7 +6159,6 @@ "You_cant_take_chats_offline": "Du kan ikke ta nye samtaler fordi du er frakoblet", "New_navigation": "Forbedret navigasjonsopplevelse", "New_navigation_description": "Utforsk vår forbedrede navigasjon, designet med ett klart omfang for enkel tilgang til det du trenger. Denne endringen fungerer som grunnlaget for fremtidige fremskritt innen navigasjonsadministrasjon.", - "Workspace_and_user_settings": "Arbeidsområde og brukerinnstillinger", "Sidebar_Sections_Order": "Rekkefølge på sidefeltseksjoner", "Sidebar_Sections_Order_Description": "Velg kategoriene i din foretrukne rekkefølge", "Incoming_Calls": "Innkommende anrop", diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index 42d0418e2ba94..7f79e1b825117 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -6159,7 +6159,6 @@ "You_cant_take_chats_offline": "Du kan ikke ta nye samtaler fordi du er frakoblet", "New_navigation": "Forbedret navigasjonsopplevelse", "New_navigation_description": "Utforsk vår forbedrede navigasjon, designet med ett klart omfang for enkel tilgang til det du trenger. Denne endringen fungerer som grunnlaget for fremtidige fremskritt innen navigasjonsadministrasjon.", - "Workspace_and_user_settings": "Arbeidsområde og brukerinnstillinger", "Sidebar_Sections_Order": "Rekkefølge på sidefeltseksjoner", "Sidebar_Sections_Order_Description": "Velg kategoriene i din foretrukne rekkefølge", "Incoming_Calls": "Innkommende anrop", diff --git a/packages/ui-client/src/components/HeaderV2/Header.tsx b/packages/ui-client/src/components/HeaderV2/Header.tsx index 4515a5fc6104c..fe36431541265 100644 --- a/packages/ui-client/src/components/HeaderV2/Header.tsx +++ b/packages/ui-client/src/components/HeaderV2/Header.tsx @@ -1,40 +1,35 @@ import { Box } from '@rocket.chat/fuselage'; -import { useLayout } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import HeaderDivider from './HeaderDivider'; type HeaderProps = ComponentPropsWithoutRef; -const Header = (props: HeaderProps) => { - const { isMobile } = useLayout(); - - return ( +const Header = (props: HeaderProps) => ( + - - - - ); -}; + flexDirection='row' + bg='room' + {...props} + /> + + +); export default Header; diff --git a/packages/ui-contexts/src/LayoutContext.ts b/packages/ui-contexts/src/LayoutContext.ts index 2d900b5a7612e..3e4c9a665a864 100644 --- a/packages/ui-contexts/src/LayoutContext.ts +++ b/packages/ui-contexts/src/LayoutContext.ts @@ -8,6 +8,7 @@ export type SizeLayout = { export type LayoutContextValue = { isEmbedded: boolean; showTopNavbarEmbeddedLayout: boolean; + isTablet: boolean; isMobile: boolean; roomToolboxExpanded: boolean; sidebar: { @@ -17,6 +18,11 @@ export type LayoutContextValue = { expand: () => void; close: () => void; }; + navbar: { + searchExpanded: boolean; + expandSearch?: () => void; + collapseSearch?: () => void; + }; size: SizeLayout; contextualBarExpanded: boolean; contextualBarPosition: 'absolute' | 'relative' | 'fixed'; @@ -31,8 +37,14 @@ export type LayoutContextValue = { export const LayoutContext = createContext({ isEmbedded: false, showTopNavbarEmbeddedLayout: false, + isTablet: false, isMobile: false, roomToolboxExpanded: true, + navbar: { + searchExpanded: false, + expandSearch: () => undefined, + collapseSearch: () => undefined, + }, sidebar: { isCollapsed: false, toggle: () => undefined, From 4a2b9f3d7c7a11d4c95c29faf68f49f5d26bbfb2 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Sat, 19 Apr 2025 12:47:27 -0300 Subject: [PATCH 148/187] fix: engagement dashboard message and hourly activity charts (#35019) --- .changeset/witty-buttons-greet.md | 4 + .../EngagementDashboardPage.tsx | 2 +- .../messages/MessagesSentSection.tsx | 53 ++++--- .../messages/MessagesTab.stories.tsx | 2 +- .../messages/MessagesTab.tsx | 8 +- .../messages/useMessagesSent.spec.ts | 132 ++++++++++++++++++ .../messages/useMessagesSent.ts | 8 +- .../users/NewUsersSection.tsx | 47 ++----- .../users/useHourlyChatActivity.spec.ts | 91 ++++++++++++ .../users/useHourlyChatActivity.ts | 9 ++ .../users/useNewUsers.spec.ts | 132 ++++++++++++++++++ 11 files changed, 425 insertions(+), 63 deletions(-) create mode 100644 .changeset/witty-buttons-greet.md create mode 100644 apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.spec.ts create mode 100644 apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.spec.ts create mode 100644 apps/meteor/client/views/admin/engagementDashboard/users/useNewUsers.spec.ts diff --git a/.changeset/witty-buttons-greet.md b/.changeset/witty-buttons-greet.md new file mode 100644 index 0000000000000..4a12544ed764c --- /dev/null +++ b/.changeset/witty-buttons-greet.md @@ -0,0 +1,4 @@ +--- +"@rocket.chat/meteor": patch +--- +Fixes issue with some charts on engagement dashboard not showing local time and missing data visualizers diff --git a/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx index 528192249774b..d12ffb8df461b 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx @@ -56,7 +56,7 @@ const EngagementDashboardPage = ({ tab = 'users', onSelectTab }: EngagementDashb {(tab === 'users' && ) || - (tab === 'messages' && ) || + (tab === 'messages' && ) || (tab === 'channels' && )} diff --git a/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx index 8627d1a3f4adb..2d4b63227eb4b 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesSentSection.tsx @@ -13,14 +13,21 @@ import { usePeriodSelectorState } from '../../../../components/dashboards/usePer import CounterSet from '../../../../components/dataView/CounterSet'; import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter'; import { useMessagesSent } from './useMessagesSent'; +import { useFormatDate } from '../../../../hooks/useFormatDate'; -const MessagesSentSection = (): ReactElement => { +type MessagesSentSectionProps = { + timezone: 'utc' | 'local'; +}; + +const MessagesSentSection = ({ timezone }: MessagesSentSectionProps): ReactElement => { const [period, periodSelectorProps] = usePeriodSelectorState('last 7 days', 'last 30 days', 'last 90 days'); const periodLabel = usePeriodLabel(period); const { t } = useTranslation(); + const utc = timezone === 'utc'; + const { data } = useMessagesSent({ period, utc }); - const { data } = useMessagesSent({ period }); + const formatDate = useFormatDate(); const [countFromPeriod, variatonFromPeriod, countFromYesterday, variationFromYesterday, values] = useMemo(() => { if (!data) { @@ -70,7 +77,7 @@ const MessagesSentSection = (): ReactElement => { /> {values ? ( - + { padding={0.25} margin={{ // TODO: Get it from theme - bottom: 20, + bottom: 50, + left: 20, + top: 20, }} colors={[ // TODO: Get it from theme @@ -98,17 +107,21 @@ const MessagesSentSection = (): ReactElement => { enableGridY={false} axisTop={null} axisRight={null} - axisBottom={ - (values.length === 7 && { - tickSize: 0, - // TODO: Get it from theme - tickPadding: 4, - tickRotation: 0, - format: (date): string => moment(date).format('dddd'), - }) || - null - } - axisLeft={null} + valueScale={{ type: 'linear' }} + axisBottom={{ + tickSize: values.length > 31 ? 4 : 0, + // TODO: Get it from theme + tickPadding: 8, + tickRotation: values.length > 31 ? 90 : 0, + truncateTickAt: 0, + format: (date): string => moment(date).format('DD/MM'), + }} + axisLeft={{ + tickSize: 0, + // TODO: Get it from theme + tickPadding: 4, + tickRotation: 0, + }} animate={true} motionConfig='stiff' theme={{ @@ -128,14 +141,20 @@ const MessagesSentSection = (): ReactElement => { }, }, }} - tooltip={({ value }) => {t('Value_messages', { value })}} + tooltip={({ value, indexValue }) => ( + + {t('Value_messages', { value })}, {formatDate(indexValue)} + + )} /> ) : ( - + + + )} diff --git a/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.stories.tsx b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.stories.tsx index 91cdb2bacccb7..17e31eb055e65 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.stories.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.stories.tsx @@ -9,5 +9,5 @@ export default { decorators: [(fn) => ], } satisfies Meta; -export const Default: StoryFn = () => ; +export const Default: StoryFn = () => ; Default.storyName = 'MessagesTab'; diff --git a/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.tsx b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.tsx index 05203df871ed0..f0f15eba8dea0 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/messages/MessagesTab.tsx @@ -5,13 +5,17 @@ import EngagementDashboardCard from '../EngagementDashboardCard'; import MessagesPerChannelSection from './MessagesPerChannelSection'; import MessagesSentSection from './MessagesSentSection'; -const MessagesTab = (): ReactElement => { +type MessagesTabProps = { + timezone: 'utc' | 'local'; +}; + +const MessagesTab = ({ timezone }: MessagesTabProps): ReactElement => { const { t } = useTranslation(); return ( <> - + diff --git a/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.spec.ts b/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.spec.ts new file mode 100644 index 0000000000000..906885b2ebe0b --- /dev/null +++ b/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.spec.ts @@ -0,0 +1,132 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook, waitFor } from '@testing-library/react'; + +import { useMessagesSent } from './useMessagesSent'; + +beforeAll(() => { + jest.useFakeTimers().setSystemTime(new Date(2025, 4, 13)); +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +it('should return utc time', async () => { + const expectedResult = { + days: [ + { + day: 'Monday', + messages: 15, + }, + { + day: 'Tuesday', + messages: 10, + }, + { + day: 'Wednesday', + messages: 5, + }, + { + day: 'Thursday', + messages: 0, + }, + { + day: 'Friday', + messages: 0, + }, + { + day: 'Saturday', + messages: 0, + }, + { + day: 'Sunday', + messages: 0, + }, + ], + end: new Date('2025-05-13T23:59:59.999Z'), + period: { + count: 15, + variation: 2, + }, + start: new Date('2025-05-11T00:00:00.000Z'), + success: true, + yesterday: { + count: 15, + variation: 3, + }, + }; + const { result } = renderHook(() => useMessagesSent({ period: 'this week', utc: true }), { + legacyRoot: true, + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/engagement-dashboard/messages/messages-sent', () => expectedResult) + .build(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + if (!result.current.data) { + throw new Error('Data is undefined'); + } + expect(result.current.data).toEqual(expectedResult); +}); + +// CI is currently running in UTC, so no local time is returned +// TODO: find a way to simulate local time properly in tests +it.skip('should return local time', async () => { + const expectedResult = { + days: [ + { + day: 'Monday', + messages: 15, + }, + { + day: 'Tuesday', + messages: 10, + }, + { + day: 'Wednesday', + messages: 5, + }, + { + day: 'Thursday', + messages: 0, + }, + { + day: 'Friday', + messages: 0, + }, + { + day: 'Saturday', + messages: 0, + }, + { + day: 'Sunday', + messages: 0, + }, + ], + end: new Date('2025-05-14T02:59:59.000Z'), + period: { + count: 15, + variation: 2, + }, + start: new Date('2025-05-11T03:00:00.000Z'), + success: true, + yesterday: { + count: 15, + variation: 3, + }, + }; + const { result } = renderHook(() => useMessagesSent({ period: 'this week', utc: false }), { + legacyRoot: true, + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/engagement-dashboard/messages/messages-sent', () => expectedResult) + .build(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + if (!result.current.data) { + throw new Error('Data is undefined'); + } + expect(result.current.data).toEqual(expectedResult); +}); diff --git a/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.ts b/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.ts index 2f3ab1647cfa4..a8e3bfe8bed1c 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/messages/useMessagesSent.ts @@ -4,17 +4,17 @@ import { useQuery } from '@tanstack/react-query'; import type { Period } from '../../../../components/dashboards/periods'; import { getPeriodRange } from '../../../../components/dashboards/periods'; -type UseMessagesSentOptions = { period: Period['key'] }; +type UseMessagesSentOptions = { period: Period['key']; utc: boolean }; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export const useMessagesSent = ({ period }: UseMessagesSentOptions) => { +export const useMessagesSent = ({ period, utc }: UseMessagesSentOptions) => { const getMessagesSent = useEndpoint('GET', '/v1/engagement-dashboard/messages/messages-sent'); return useQuery({ - queryKey: ['admin/engagement-dashboard/messages/messages-sent', { period }], + queryKey: ['admin/engagement-dashboard/messages/messages-sent', { period, utc }], queryFn: async () => { - const { start, end } = getPeriodRange(period); + const { start, end } = getPeriodRange(period, utc); const response = await getMessagesSent({ start: start.toISOString(), diff --git a/apps/meteor/client/views/admin/engagementDashboard/users/NewUsersSection.tsx b/apps/meteor/client/views/admin/engagementDashboard/users/NewUsersSection.tsx index 6fe95d8826f5f..0c3afbc0fdec9 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/users/NewUsersSection.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/users/NewUsersSection.tsx @@ -1,6 +1,5 @@ import { ResponsiveBar } from '@nivo/bar'; import { Box, Flex, Skeleton, Tooltip } from '@rocket.chat/fuselage'; -import { useResizeObserver } from '@rocket.chat/fuselage-hooks'; import colors from '@rocket.chat/fuselage-tokens/colors.json'; import moment from 'moment'; import type { ReactElement } from 'react'; @@ -16,8 +15,6 @@ import { useFormatDate } from '../../../../hooks/useFormatDate'; import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter'; import { useNewUsers } from './useNewUsers'; -const TICK_WIDTH = 45; - type NewUsersSectionProps = { timezone: 'utc' | 'local'; }; @@ -33,32 +30,6 @@ const NewUsersSection = ({ timezone }: NewUsersSectionProps): ReactElement => { const formatDate = useFormatDate(); - const { ref: sizeRef, contentBoxSize: { inlineSize = 600 } = {} } = useResizeObserver(); - - const maxTicks = Math.ceil(inlineSize / TICK_WIDTH); - - const tickValues = useMemo(() => { - if (!data) { - return undefined; - } - - const arrayLength = moment(data.end).diff(data.start, 'days') + 1; - if (arrayLength <= maxTicks || !maxTicks) { - return undefined; - } - - const values = Array.from({ length: arrayLength }, (_, i) => moment(data.start).add(i, 'days').format('YYYY-MM-DD')); - - const relation = Math.ceil(values.length / maxTicks); - - return values.reduce((acc, cur, i) => { - if ((i + 1) % relation === 0) { - acc = [...acc, cur]; - } - return acc; - }, [] as string[]); - }, [data, maxTicks]); - const [countFromPeriod, variatonFromPeriod, countFromYesterday, variationFromYesterday, values] = useMemo(() => { if (!data) { return []; @@ -105,9 +76,9 @@ const NewUsersSection = ({ timezone }: NewUsersSectionProps): ReactElement => { /> {values ? ( - + - + { padding={0.25} margin={{ // TODO: Get it from theme - bottom: 20, + bottom: 50, left: 20, top: 20, }} @@ -136,12 +107,12 @@ const NewUsersSection = ({ timezone }: NewUsersSectionProps): ReactElement => { axisTop={null} axisRight={null} axisBottom={{ - tickSize: 0, + tickSize: values.length > 31 ? 4 : 0, // TODO: Get it from theme - tickPadding: 4, - tickRotation: 0, - tickValues, - format: (date): string => moment(date).format(values?.length === 7 ? 'dddd' : 'DD/MM'), + tickPadding: 8, + tickRotation: values.length > 31 ? 90 : 0, + truncateTickAt: 0, + format: (date): string => moment(date).format('DD/MM'), }} axisLeft={{ tickSize: 0, @@ -179,7 +150,7 @@ const NewUsersSection = ({ timezone }: NewUsersSectionProps): ReactElement => { ) : ( - + )} diff --git a/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.spec.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.spec.ts new file mode 100644 index 0000000000000..e27e0fe6e2cd2 --- /dev/null +++ b/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.spec.ts @@ -0,0 +1,91 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook, waitFor } from '@testing-library/react'; + +import { useHourlyChatActivity } from './useHourlyChatActivity'; + +it('should return utc time', async () => { + const expectedResult = { + hours: [ + { hour: 0, users: 0 }, + { hour: 2, users: 0 }, + { hour: 4, users: 0 }, + { hour: 6, users: 0 }, + { hour: 8, users: 0 }, + { hour: 10, users: 0 }, + { hour: 12, users: 5 }, + { hour: 14, users: 0 }, + { hour: 16, users: 0 }, + { hour: 18, users: 0 }, + { hour: 20, users: 0 }, + { hour: 22, users: 0 }, + ], + success: true, + }; + const { result } = renderHook(() => useHourlyChatActivity({ displacement: 0, utc: true }), { + legacyRoot: true, + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/engagement-dashboard/users/chat-busier/hourly-data', () => expectedResult) + .build(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + if (!result.current.data) { + throw new Error('Data is undefined'); + } + + expect(result.current.data?.hours).toEqual(expectedResult.hours); +}); + +// CI is currently running in UTC, so no local time is returned +// TODO: find a way to simulate local time properly in tests +it.skip('should return local time', async () => { + const receivedData = { + hours: [ + { hour: 0, users: 0 }, + { hour: 2, users: 0 }, + { hour: 4, users: 0 }, + { hour: 6, users: 0 }, + { hour: 8, users: 0 }, + { hour: 10, users: 0 }, + { hour: 12, users: 5 }, + { hour: 14, users: 0 }, + { hour: 16, users: 0 }, + { hour: 18, users: 0 }, + { hour: 20, users: 0 }, + { hour: 22, users: 0 }, + ], + success: true, + }; + + const expectedResult = { + hours: [ + { hour: 0, users: 0 }, + { hour: 2, users: 0 }, + { hour: 4, users: 5 }, + { hour: 6, users: 0 }, + { hour: 8, users: 0 }, + { hour: 10, users: 0 }, + { hour: 12, users: 0 }, + { hour: 14, users: 0 }, + { hour: 16, users: 0 }, + { hour: 18, users: 0 }, + { hour: 20, users: 0 }, + { hour: 22, users: 0 }, + ], + }; + + const { result } = renderHook(() => useHourlyChatActivity({ displacement: 0, utc: false }), { + legacyRoot: true, + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/engagement-dashboard/users/chat-busier/hourly-data', () => receivedData) + .build(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + if (!result.current.data) { + throw new Error('Data is undefined'); + } + expect(result.current.data.hours.sort((a, b) => a.hour - b.hour)).toEqual(expectedResult.hours); +}); diff --git a/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts index 98cdcc6818f8b..7870cd8105c78 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts +++ b/apps/meteor/client/views/admin/engagementDashboard/users/useHourlyChatActivity.ts @@ -21,6 +21,15 @@ export const useHourlyChatActivity = ({ displacement, utc }: UseHourlyChatActivi start: day.toISOString(), }); + if (!utc) { + response.hours = response.hours.map((hours) => { + return { + hour: moment(moment.utc().set({ hour: hours.hour, minute: 0, second: 0 }).toISOString()).hour(), + users: hours.users, + }; + }); + } + return response ? { ...response, diff --git a/apps/meteor/client/views/admin/engagementDashboard/users/useNewUsers.spec.ts b/apps/meteor/client/views/admin/engagementDashboard/users/useNewUsers.spec.ts new file mode 100644 index 0000000000000..e1da141053849 --- /dev/null +++ b/apps/meteor/client/views/admin/engagementDashboard/users/useNewUsers.spec.ts @@ -0,0 +1,132 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook, waitFor } from '@testing-library/react'; + +import { useNewUsers } from './useNewUsers'; + +beforeAll(() => { + jest.useFakeTimers().setSystemTime(new Date(2025, 4, 13)); +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +it('should return utc time', async () => { + const expectedResult = { + days: [ + { + day: 'Monday', + users: 15, + }, + { + day: 'Tuesday', + users: 10, + }, + { + day: 'Wednesday', + users: 5, + }, + { + day: 'Thursday', + users: 0, + }, + { + day: 'Friday', + users: 0, + }, + { + day: 'Saturday', + users: 0, + }, + { + day: 'Sunday', + users: 0, + }, + ], + end: new Date('2025-05-13T23:59:59.999Z'), + period: { + count: 15, + variation: 2, + }, + start: new Date('2025-05-11T00:00:00.000Z'), + success: true, + yesterday: { + count: 15, + variation: 3, + }, + }; + const { result } = renderHook(() => useNewUsers({ period: 'this week', utc: true }), { + legacyRoot: true, + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/engagement-dashboard/users/new-users', () => expectedResult) + .build(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + if (!result.current.data) { + throw new Error('Data is undefined'); + } + expect(result.current.data).toEqual(expectedResult); +}); + +// CI is currently running in UTC, so no local time is returned +// TODO: find a way to simulate local time properly in tests +it.skip('should return local time', async () => { + const expectedResult = { + days: [ + { + day: 'Monday', + users: 15, + }, + { + day: 'Tuesday', + users: 10, + }, + { + day: 'Wednesday', + users: 5, + }, + { + day: 'Thursday', + users: 0, + }, + { + day: 'Friday', + users: 0, + }, + { + day: 'Saturday', + users: 0, + }, + { + day: 'Sunday', + users: 0, + }, + ], + end: new Date('2025-05-13T23:59:59.999Z'), + period: { + count: 15, + variation: 2, + }, + start: new Date('2025-05-11T00:00:00.000Z'), + success: true, + yesterday: { + count: 15, + variation: 3, + }, + }; + const { result } = renderHook(() => useNewUsers({ period: 'this week', utc: false }), { + legacyRoot: true, + wrapper: mockAppRoot() + .withEndpoint('GET', '/v1/engagement-dashboard/users/new-users', () => expectedResult) + .build(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + if (!result.current.data) { + throw new Error('Data is undefined'); + } + expect(result.current.data).toEqual(expectedResult); +}); From 79d00548b66f98752280ca855f1a4f6f7c357fdc Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Sat, 19 Apr 2025 14:27:59 -0300 Subject: [PATCH 149/187] regression: sound effect for Voice Call Ended is no longer playing (#35833) --- packages/ui-voip/src/providers/VoipProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui-voip/src/providers/VoipProvider.tsx b/packages/ui-voip/src/providers/VoipProvider.tsx index 46a0c8b552f87..f752162999fe6 100644 --- a/packages/ui-voip/src/providers/VoipProvider.tsx +++ b/packages/ui-voip/src/providers/VoipProvider.tsx @@ -78,7 +78,6 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { const onCallTerminated = (): void => { voipSounds.playCallEnded(); - voipSounds.stopCallEnded(); voipSounds.stopDialer(); voipSounds.stopRinger(); window.removeEventListener('beforeunload', onBeforeUnload); @@ -109,6 +108,7 @@ const VoipProvider = ({ children }: { children: ReactNode }) => { voipClient.networkEmitter.on('localnetworkoffline', onNetworkDisconnected); return (): void => { + voipSounds.stopCallEnded(); voipClient.off('incomingcall', onIncomingCallRinging); voipClient.off('outgoingcall', onOutgoingCallRinging); voipClient.off('callestablished', onCallEstablished); From d8eb824d242cbbeafb11b1c4a806860e4541ba79 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Sun, 20 Apr 2025 05:33:20 -0300 Subject: [PATCH 150/187] feat: multi-instance app status (#35721) --- .changeset/nine-paws-sit.md | 16 ++ .../ee/lib/misc/fetchAppsStatusFromCluster.ts | 12 + .../ee/lib/misc/formatAppInstanceForRest.ts | 9 +- .../ee/server/apps/communication/rest.ts | 34 ++- .../server/local-services/instance/service.ts | 49 +++- .../ee/server/sdk/types/IInstanceService.ts | 2 + .../server/services/apps-engine/service.ts | 67 ++++- .../services/apps-engine/service.tests.ts | 231 ++++++++++++++++++ .../server/services/instance/service.tests.ts | 122 +++++++++ .../network-broker/src/NetworkBroker.ts | 8 +- packages/core-services/src/LocalBroker.ts | 8 +- packages/core-services/src/index.ts | 3 +- packages/core-services/src/lib/Api.ts | 6 +- .../core-services/src/types/IApiService.ts | 4 +- .../src/types/IAppsEngineService.ts | 7 + packages/core-services/src/types/IBroker.ts | 16 +- packages/core-typings/src/Apps.ts | 5 + packages/rest-typings/src/apps/index.ts | 3 +- 18 files changed, 580 insertions(+), 22 deletions(-) create mode 100644 .changeset/nine-paws-sit.md create mode 100644 apps/meteor/ee/lib/misc/fetchAppsStatusFromCluster.ts create mode 100644 apps/meteor/tests/unit/server/services/apps-engine/service.tests.ts create mode 100644 apps/meteor/tests/unit/server/services/instance/service.tests.ts diff --git a/.changeset/nine-paws-sit.md b/.changeset/nine-paws-sit.md new file mode 100644 index 0000000000000..04b0112a8b8ad --- /dev/null +++ b/.changeset/nine-paws-sit.md @@ -0,0 +1,16 @@ +--- +'@rocket.chat/network-broker': minor +'@rocket.chat/mock-providers': minor +'@rocket.chat/pdf-worker': minor +'@rocket.chat/core-services': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ui-contexts': minor +'@rocket.chat/ui-voip': minor +'@rocket.chat/models': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments diff --git a/apps/meteor/ee/lib/misc/fetchAppsStatusFromCluster.ts b/apps/meteor/ee/lib/misc/fetchAppsStatusFromCluster.ts new file mode 100644 index 0000000000000..830971dceb3cd --- /dev/null +++ b/apps/meteor/ee/lib/misc/fetchAppsStatusFromCluster.ts @@ -0,0 +1,12 @@ +import { Apps } from '@rocket.chat/core-services'; + +import { isRunningMs } from '../../../server/lib/isRunningMs'; +import { Instance } from '../../server/sdk'; + +export async function fetchAppsStatusFromCluster() { + if (isRunningMs()) { + return Apps.getAppsStatusInNodes(); + } + + return Instance.getAppsStatusInInstances(); +} diff --git a/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts b/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts index bf096122c50fe..7819404525d5f 100644 --- a/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts +++ b/apps/meteor/ee/lib/misc/formatAppInstanceForRest.ts @@ -3,6 +3,8 @@ import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; import type { AppLicenseValidationResult } from '@rocket.chat/apps-engine/server/marketplace/license'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +import type { AppStatusReport } from '@rocket.chat/core-services'; +import type { App } from '@rocket.chat/core-typings'; import { getInstallationSourceFromAppStorageItem } from '../../../lib/apps/getInstallationSourceFromAppStorageItem'; @@ -12,9 +14,10 @@ interface IAppInfoRest extends IAppInfo { licenseValidation?: AppLicenseValidationResult; private: boolean; migrated: boolean; + clusterStatus?: App['clusterStatus']; } -export async function formatAppInstanceForRest(app: ProxiedApp): Promise { +export async function formatAppInstanceForRest(app: ProxiedApp, clusterStatus?: AppStatusReport): Promise { const appRest: IAppInfoRest = { ...app.getInfo(), status: await app.getStatus(), @@ -23,6 +26,10 @@ export async function formatAppInstanceForRest(app: ProxiedApp): Promise formatAppInstanceForRest(app, clusterStatus))); + return API.v1.success({ apps: formatted }); }, }, @@ -298,7 +307,7 @@ export class AppsRestApi { apiDeprecationLogger.endpoint(this.request.route, '7.0.0', this.response, 'Use /apps/installed to get the installed apps list.'); const proxiedApps = await manager.get(); - const apps = await Promise.all(proxiedApps.map(formatAppInstanceForRest)); + const apps = await Promise.all(proxiedApps.map((app) => formatAppInstanceForRest(app))); return API.v1.success({ apps }); }, @@ -1268,12 +1277,25 @@ export class AppsRestApi { { authRequired: true, permissionsRequired: ['manage-apps'] }, { async get() { - const prl = manager.getOneById(this.urlParams.id); + const app = manager.getOneById(this.urlParams.id); - if (prl) { - return API.v1.success({ status: await prl.getStatus() }); + if (!app) { + return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`); } - return API.v1.notFound(`No App found by the id of: ${this.urlParams.id}`); + + const response: { status: AppStatus; clusterStatus?: AppStatusReport[string] } = { status: await app.getStatus() }; + + try { + const clusterStatus = await fetchAppsStatusFromCluster(); + + if (clusterStatus?.[app.getID()]) { + response.clusterStatus = clusterStatus[app.getID()]; + } + } catch (e) { + orchestrator.getRocketChatLogger().warn('App status endpoint: could not fetch status across cluster', e); + } + + return API.v1.success(response); }, async post() { const { id: appId } = this.urlParams; diff --git a/apps/meteor/ee/server/local-services/instance/service.ts b/apps/meteor/ee/server/local-services/instance/service.ts index 93d6d0c45e980..0c6dfa8f1695d 100644 --- a/apps/meteor/ee/server/local-services/instance/service.ts +++ b/apps/meteor/ee/server/local-services/instance/service.ts @@ -1,6 +1,7 @@ import os from 'os'; -import { License, ServiceClassInternal } from '@rocket.chat/core-services'; +import type { AppStatusReport } from '@rocket.chat/core-services'; +import { Apps, License, ServiceClassInternal } from '@rocket.chat/core-services'; import { InstanceStatus, defaultPingInterval, indexExpire } from '@rocket.chat/instance-status'; import { InstanceStatus as InstanceStatusRaw } from '@rocket.chat/models'; import EJSON from 'ejson'; @@ -117,6 +118,11 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe } }, }, + actions: { + getAppsStatus(_ctx) { + return Apps.getAppsStatusLocal(); + }, + }, }); } @@ -176,4 +182,45 @@ export class InstanceService extends ServiceClassInternal implements IInstanceSe async getInstances(): Promise { return this.broker.call('$node.list', { onlyAvailable: true }); } + + async getAppsStatusInInstances(): Promise { + const instances = await this.getInstances(); + + const control: Promise[] = []; + const statusByApp: AppStatusReport = {}; + + instances.forEach((instance) => { + if (instance.local) { + return; + } + + const { id: instanceId } = instance; + + control.push( + (async () => { + const appsStatus = await this.broker.call>, null>( + 'matrix.getAppsStatus', + null, + { nodeID: instanceId }, + ); + + if (!appsStatus) { + throw new Error(`Failed to get apps status from instance ${instanceId}`); + } + + appsStatus.forEach(({ status, appId }) => { + if (!statusByApp[appId]) { + statusByApp[appId] = []; + } + + statusByApp[appId].push({ instanceId, status }); + }); + })(), + ); + }); + + await Promise.all(control); + + return statusByApp; + } } diff --git a/apps/meteor/ee/server/sdk/types/IInstanceService.ts b/apps/meteor/ee/server/sdk/types/IInstanceService.ts index b5c54349dfa16..7ce7b4be285a3 100644 --- a/apps/meteor/ee/server/sdk/types/IInstanceService.ts +++ b/apps/meteor/ee/server/sdk/types/IInstanceService.ts @@ -1,5 +1,7 @@ +import type { AppStatusReport } from '@rocket.chat/core-services'; import type { BrokerNode } from 'moleculer'; export interface IInstanceService { getInstances(): Promise; + getAppsStatusInInstances(): Promise; } diff --git a/apps/meteor/server/services/apps-engine/service.ts b/apps/meteor/server/services/apps-engine/service.ts index 486a788563946..a858eead4c430 100644 --- a/apps/meteor/server/services/apps-engine/service.ts +++ b/apps/meteor/server/services/apps-engine/service.ts @@ -4,9 +4,10 @@ import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; -import type { IAppsEngineService } from '@rocket.chat/core-services'; +import type { AppStatusReport, IAppsEngineService } from '@rocket.chat/core-services'; import { ServiceClassInternal } from '@rocket.chat/core-services'; +import { isRunningMs } from '../../lib/isRunningMs'; import { SystemLogger } from '../../lib/logger/system'; export class AppsEngineService extends ServiceClassInternal implements IAppsEngineService { @@ -133,4 +134,68 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi return app.getStorageItem(); } + + async getAppsStatusLocal(): Promise<{ status: AppStatus; appId: string }[]> { + const apps = await Apps.self?.getManager()?.get(); + + if (!apps) { + return []; + } + + return Promise.all( + apps.map(async (app) => ({ + status: await app.getStatus(), + appId: app.getID(), + })), + ); + } + + async getAppsStatusInNodes(): Promise { + if (!isRunningMs()) { + throw new Error('Getting apps status in cluster is only available in microservices mode'); + } + + if (!this.api) { + throw new Error('AppsEngineService is not initialized'); + } + + // If we are running MS AND this.api is defined, we KNOW there is a local node + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + const { id: localNodeId } = (await this.api.nodeList()).find((node) => node.local)!; + + const services: { name: string; nodes: string[] }[] = await this.api?.call('$node.services', { onlyActive: true }); + + // We can filter out the local node because we already know its status + const availableNodes = services?.find((service) => service.name === 'apps-engine')?.nodes.filter((node) => node !== localNodeId); + + if (!availableNodes || availableNodes.length < 1) { + throw new Error('Not enough Apps-Engine nodes in deployment'); + } + + const statusByApp: AppStatusReport = {}; + + const apps: Promise[] = availableNodes.map(async (nodeID) => { + const appsStatus: Awaited> | undefined = await this.api?.call( + 'apps-engine.getAppsStatusLocal', + [], + { nodeID }, + ); + + if (!appsStatus) { + throw new Error(`Failed to get apps status from node ${nodeID}`); + } + + appsStatus.forEach(({ status, appId }) => { + if (!statusByApp[appId]) { + statusByApp[appId] = []; + } + + statusByApp[appId].push({ instanceId: nodeID, status }); + }); + }); + + await Promise.all(apps); + + return statusByApp; + } } diff --git a/apps/meteor/tests/unit/server/services/apps-engine/service.tests.ts b/apps/meteor/tests/unit/server/services/apps-engine/service.tests.ts new file mode 100644 index 0000000000000..277b3011eddfe --- /dev/null +++ b/apps/meteor/tests/unit/server/services/apps-engine/service.tests.ts @@ -0,0 +1,231 @@ +import type { IAppsEngineService } from '@rocket.chat/core-services'; +import { expect } from 'chai'; +import { describe, it, beforeEach, afterEach } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const AppsMock = { + self: { + isInitialized: sinon.stub(), + getManager: sinon.stub(), + getStorage: sinon.stub(), + getAppSourceStorage: sinon.stub(), + getRocketChatLogger: sinon.stub(), + triggerEvent: sinon.stub(), + }, +}; + +const apiMock = { + call: sinon.stub(), + nodeList: sinon.stub(), +}; + +const isRunningMsMock = sinon.stub(); + +const serviceMocks = { + '@rocket.chat/apps': { Apps: AppsMock }, + '@rocket.chat/core-services': { + api: apiMock, + ServiceClassInternal: class { + onEvent = sinon.stub(); + }, + }, + '../../lib/isRunningMs': { isRunningMs: isRunningMsMock }, + '../../lib/logger/system': { SystemLogger: { error: sinon.stub() } }, +}; + +const { AppsEngineService } = proxyquire.noCallThru().load('../../../../../server/services/apps-engine/service', serviceMocks); + +describe('AppsEngineService', () => { + let service: IAppsEngineService; + + it('should instantiate properly', () => { + expect(new AppsEngineService()).to.be.instanceOf(AppsEngineService); + }); + + describe('#getAppsStatusInNode - part 1', () => { + it('should error if api is not available', async () => { + isRunningMsMock.returns(true); + + const service = new AppsEngineService(); + await expect(service.getAppsStatusInNodes()).to.be.rejectedWith('AppsEngineService is not initialized'); + }); + }); + + beforeEach(() => { + service = new AppsEngineService(); + (service as any).api = apiMock; + }); + + afterEach(() => { + apiMock.call.reset(); + apiMock.nodeList.reset(); + AppsMock.self.isInitialized.reset(); + AppsMock.self.getManager.reset(); + AppsMock.self.getStorage.reset(); + AppsMock.self.getAppSourceStorage.reset(); + AppsMock.self.getRocketChatLogger.reset(); + AppsMock.self.triggerEvent.reset(); + isRunningMsMock.reset(); + }); + + describe('#isInitialized', () => { + it('should return true when Apps is initialized', () => { + AppsMock.self.isInitialized.returns(true); + expect(service.isInitialized()).to.be.true; + }); + + it('should return false when Apps is not initialized', () => { + AppsMock.self.isInitialized.returns(false); + expect(service.isInitialized()).to.be.false; + }); + }); + + describe('#getApps', () => { + it('should return app info from manager', async () => { + const mockApps = [{ getInfo: () => ({ id: 'app1' }) }]; + const mockManager = { get: sinon.stub().resolves(mockApps) }; + AppsMock.self.getManager.returns(mockManager); + + const result = await service.getApps({}); + expect(result).to.deep.equal([{ id: 'app1' }]); + }); + + it('should return undefined when manager is not available', async () => { + AppsMock.self.getManager.returns(undefined); + const result = await service.getApps({}); + expect(result).to.be.undefined; + }); + }); + + describe('#getAppsStatusLocal', () => { + it('should return app status reports', async () => { + const mockApps = [ + { + getStatus: sinon.stub().resolves('enabled'), + getID: sinon.stub().returns('app1'), + }, + ]; + const mockManager = { get: sinon.stub().resolves(mockApps) }; + AppsMock.self.getManager.returns(mockManager); + + const result = await service.getAppsStatusLocal(); + expect(result).to.deep.equal([ + { + status: 'enabled', + appId: 'app1', + }, + ]); + }); + + it('should return empty array when manager is not available', async () => { + AppsMock.self.getManager.returns(undefined); + const result = await service.getAppsStatusLocal(); + expect(result).to.deep.equal([]); + }); + }); + + describe('#getAppStorageItemById', () => { + it('should return storage item for existing app', async () => { + const mockStorageItem = { id: 'app1' }; + const mockApp = { + getStorageItem: sinon.stub().returns(mockStorageItem), + }; + const mockManager = { getOneById: sinon.stub().returns(mockApp) }; + AppsMock.self.getManager.returns(mockManager); + + const result = await service.getAppStorageItemById('app1'); + expect(result).to.equal(mockStorageItem); + }); + + it('should return undefined for non-existent app', async () => { + const mockManager = { getOneById: sinon.stub().returns(undefined) }; + AppsMock.self.getManager.returns(mockManager); + + const result = await service.getAppStorageItemById('non-existent'); + expect(result).to.be.undefined; + }); + }); + + describe('#getAppsStatusInNode - part 2', () => { + it('should throw error when not in microservices mode', async () => { + isRunningMsMock.returns(false); + await expect(service.getAppsStatusInNodes()).to.be.rejectedWith( + 'Getting apps status in cluster is only available in microservices mode', + ); + }); + + it('should throw error when not enough apps-engine nodes are available', async () => { + isRunningMsMock.returns(true); + apiMock.nodeList.resolves([{ id: 'node1', local: true }]); + apiMock.call.resolves([{ name: 'apps-engine', nodes: ['node1'] }]); + + await expect(service.getAppsStatusInNodes()).to.be.rejectedWith('Not enough Apps-Engine nodes in deployment'); + }); + + it('should not call the service for the local node', async () => { + isRunningMsMock.returns(true); + apiMock.nodeList.resolves([{ id: 'node1', local: true }]); + apiMock.call + .onFirstCall() + .resolves([{ name: 'apps-engine', nodes: ['node1', 'node2'] }]) + .onSecondCall() + .resolves([ + { status: 'enabled', appId: 'app1' }, + { status: 'enabled', appId: 'app2' }, + ]) + .onThirdCall() + .rejects(new Error('Should not be called')); + + const result = await service.getAppsStatusInNodes(); + + expect(result).to.deep.equal({ + app1: [{ instanceId: 'node2', status: 'enabled' }], + app2: [{ instanceId: 'node2', status: 'enabled' }], + }); + }); + + it('should return status from all nodes', async () => { + isRunningMsMock.returns(true); + apiMock.nodeList.resolves([{ id: 'node1', local: true }]); + apiMock.call + .onFirstCall() + .resolves([{ name: 'apps-engine', nodes: ['node1', 'node2', 'node3'] }]) + .onSecondCall() + .resolves([ + { status: 'enabled', appId: 'app1' }, + { status: 'enabled', appId: 'app2' }, + ]) + .onThirdCall() + .resolves([ + { status: 'initialized', appId: 'app1' }, + { status: 'enabled', appId: 'app2' }, + ]); + + const result = await service.getAppsStatusInNodes(); + + expect(result).to.deep.equal({ + app1: [ + { instanceId: 'node2', status: 'enabled' }, + { instanceId: 'node3', status: 'initialized' }, + ], + app2: [ + { instanceId: 'node2', status: 'enabled' }, + { instanceId: 'node3', status: 'enabled' }, + ], + }); + }); + + it('should throw error when failed to get status from a node', async () => { + isRunningMsMock.returns(true); + apiMock.nodeList.resolves([{ id: 'node1', local: true }]); + apiMock.call + .onFirstCall() + .resolves([{ name: 'apps-engine', nodes: ['node1', 'node2'] }]) + .onSecondCall() + .resolves(undefined); + + await expect(service.getAppsStatusInNodes()).to.be.rejectedWith('Failed to get apps status from node node2'); + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/services/instance/service.tests.ts b/apps/meteor/tests/unit/server/services/instance/service.tests.ts new file mode 100644 index 0000000000000..dac15385514a5 --- /dev/null +++ b/apps/meteor/tests/unit/server/services/instance/service.tests.ts @@ -0,0 +1,122 @@ +import { expect } from 'chai'; +import { describe, it, beforeEach, afterEach } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +import type { IInstanceService } from '../../../../../ee/server/sdk/types/IInstanceService'; + +const ServiceBrokerMock = { + call: sinon.stub(), +}; + +const AppsMock = { + getAppsStatusLocal: sinon.stub(), +}; + +const serviceMocks = { + '@rocket.chat/core-services': { + ServiceClassInternal: class { + onEvent = sinon.stub(); + }, + Apps: AppsMock, + }, + 'moleculer': { + ServiceBroker: sinon.stub().returns(ServiceBrokerMock), + Serializers: { + Base: class {}, + }, + }, +}; + +const { InstanceService } = proxyquire + .noPreserveCache() + .noCallThru() + .load('../../../../../ee/server/local-services/instance/service', serviceMocks); + +describe('InstanceService', () => { + let service: IInstanceService; + + beforeEach(() => { + service = new InstanceService(); + (service as any).broker = ServiceBrokerMock; + }); + + afterEach(() => { + ServiceBrokerMock.call.reset(); + AppsMock.getAppsStatusLocal.reset(); + }); + + describe('#getInstances', () => { + it('should return list of instances', async () => { + const mockInstances = [{ id: 'node1' }]; + ServiceBrokerMock.call.resolves(mockInstances); + + const instances = await service.getInstances(); + + expect(instances).to.deep.equal(mockInstances); + expect(ServiceBrokerMock.call.calledWith('$node.list', { onlyAvailable: true })).to.be.true; + }); + + it('should handle empty instance list', async () => { + ServiceBrokerMock.call.resolves([]); + + const instances = await service.getInstances(); + + expect(instances).to.deep.equal([]); + expect(ServiceBrokerMock.call.calledWith('$node.list', { onlyAvailable: true })).to.be.true; + }); + }); + + describe('#getAppsStatusInInstances', () => { + it('should return app status from all non-local instances', async () => { + const mockInstances = [ + { id: 'node1', local: true }, + { id: 'node2', local: false }, + { id: 'node3', local: false }, + ]; + + ServiceBrokerMock.call + .onFirstCall() + .resolves(mockInstances) + .onSecondCall() + .resolves([{ status: 'enabled', appId: 'app1' }]) + .onThirdCall() + .resolves([{ status: 'disabled', appId: 'app2' }]); + + const result = await service.getAppsStatusInInstances(); + + expect(result).to.deep.equal({ + app1: [{ instanceId: 'node2', status: 'enabled' }], + app2: [{ instanceId: 'node3', status: 'disabled' }], + }); + + expect(ServiceBrokerMock.call.calledThrice).to.be.true; + }); + + it('should handle empty app status response', async () => { + const mockInstances = [ + { id: 'node1', local: true }, + { id: 'node2', local: false }, + ]; + + ServiceBrokerMock.call.onFirstCall().resolves(mockInstances).onSecondCall().resolves([]); + + const result = await service.getAppsStatusInInstances(); + + expect(result).to.deep.equal({}); + expect(ServiceBrokerMock.call.calledTwice).to.be.true; + }); + + it('should handle undefined app status response', async () => { + const mockInstances = [ + { id: 'node1', local: true }, + { id: 'node2', local: false }, + ]; + + ServiceBrokerMock.call.onFirstCall().resolves(mockInstances).onSecondCall().resolves(undefined); + + await expect(service.getAppsStatusInInstances()).to.be.rejectedWith(`Failed to get apps status from instance node2`); + expect(ServiceBrokerMock.call.calledTwice).to.be.true; + }); + }); +}); diff --git a/ee/packages/network-broker/src/NetworkBroker.ts b/ee/packages/network-broker/src/NetworkBroker.ts index c38650e9ad7de..99d8716222413 100644 --- a/ee/packages/network-broker/src/NetworkBroker.ts +++ b/ee/packages/network-broker/src/NetworkBroker.ts @@ -1,5 +1,5 @@ import { asyncLocalStorage } from '@rocket.chat/core-services'; -import type { IBroker, IBrokerNode, IServiceMetrics, IServiceClass, EventSignatures } from '@rocket.chat/core-services'; +import type { CallingOptions, IBroker, IBrokerNode, IServiceMetrics, IServiceClass, EventSignatures } from '@rocket.chat/core-services'; import { injectCurrentContext, tracerSpan } from '@rocket.chat/tracing'; import type { ServiceBroker, Context, ServiceSchema } from 'moleculer'; @@ -32,7 +32,7 @@ export class NetworkBroker implements IBroker { this.metrics = broker.metrics; } - async call(method: string, data: any): Promise { + async call(method: string, data: any, options?: CallingOptions): Promise { if (!(await this.started)) { return; } @@ -40,17 +40,19 @@ export class NetworkBroker implements IBroker { const context = asyncLocalStorage.getStore(); if (context?.ctx?.call) { - return context.ctx.call(method, data); + return context.ctx.call(method, data, options); } const services: { name: string }[] = await this.broker.call('$node.services', { onlyAvailable: true, }); + if (!services.find((service) => service.name === method.split('.')[0])) { return new Error('method-not-available'); } return this.broker.call(method, data, { + ...options, meta: { optl: injectCurrentContext(), }, diff --git a/packages/core-services/src/LocalBroker.ts b/packages/core-services/src/LocalBroker.ts index 9e319c4cdbdb0..be19791097658 100644 --- a/packages/core-services/src/LocalBroker.ts +++ b/packages/core-services/src/LocalBroker.ts @@ -6,7 +6,7 @@ import { injectCurrentContext, tracerActiveSpan } from '@rocket.chat/tracing'; import { asyncLocalStorage } from '.'; import type { EventSignatures } from './events/Events'; -import type { IBroker, IBrokerNode } from './types/IBroker'; +import type { CallingOptions, IBroker, IBrokerNode } from './types/IBroker'; import type { ServiceClass, IServiceClass } from './types/ServiceClass'; type ExtendedServiceClass = { instance: IServiceClass; dependencies: string[]; isStarted: boolean }; @@ -29,7 +29,11 @@ export class LocalBroker implements IBroker { private defaultDependencies = ['settings']; - async call(method: string, data: any): Promise { + async call(method: string, data: any, options?: CallingOptions): Promise { + if (options) { + logger.warn('Options are not supported in LocalBroker'); + } + return tracerActiveSpan( `action ${method}`, {}, diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index dedbe3571aaac..c59df5ee55e66 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -50,13 +50,14 @@ import type { IVideoConfService, VideoConferenceJoinOptions } from './types/IVid import type { IVoipFreeSwitchService } from './types/IVoipFreeSwitchService'; import type { IVoipService } from './types/IVoipService'; +export { AppStatusReport } from './types/IAppsEngineService'; export { asyncLocalStorage } from './lib/asyncLocalStorage'; export { MeteorError, isMeteorError } from './MeteorError'; export { api } from './api'; export { EventSignatures } from './events/Events'; export { LocalBroker } from './LocalBroker'; -export { IBroker, IBrokerNode, BaseMetricOptions, IServiceMetrics } from './types/IBroker'; +export { IBroker, IBrokerNode, BaseMetricOptions, CallingOptions, IServiceMetrics } from './types/IBroker'; export { IServiceContext, ServiceClass, IServiceClass, ServiceClassInternal } from './types/ServiceClass'; diff --git a/packages/core-services/src/lib/Api.ts b/packages/core-services/src/lib/Api.ts index de028dcafd1d5..86516e27b31e1 100644 --- a/packages/core-services/src/lib/Api.ts +++ b/packages/core-services/src/lib/Api.ts @@ -1,6 +1,6 @@ import type { EventSignatures } from '../events/Events'; import type { IApiService } from '../types/IApiService'; -import type { IBroker, IBrokerNode } from '../types/IBroker'; +import type { CallingOptions, IBroker, IBrokerNode } from '../types/IBroker'; import type { IServiceClass } from '../types/ServiceClass'; export class Api implements IApiService { @@ -37,8 +37,8 @@ export class Api implements IApiService { } } - async call(method: string, data?: unknown): Promise { - return this.broker?.call(method, data); + async call(method: string, data?: unknown, options?: CallingOptions): Promise { + return this.broker?.call(method, data, options); } async broadcast(event: T, ...args: Parameters): Promise { diff --git a/packages/core-services/src/types/IApiService.ts b/packages/core-services/src/types/IApiService.ts index bff4bc3a2d82a..ab01517f9a0eb 100644 --- a/packages/core-services/src/types/IApiService.ts +++ b/packages/core-services/src/types/IApiService.ts @@ -1,4 +1,4 @@ -import type { IBroker, IBrokerNode } from './IBroker'; +import type { CallingOptions, IBroker, IBrokerNode } from './IBroker'; import type { IServiceClass } from './ServiceClass'; import type { EventSignatures } from '../events/Events'; @@ -9,7 +9,7 @@ export interface IApiService { registerService(instance: IServiceClass): void; - call(method: string, data?: unknown): Promise; + call(method: string, data?: unknown, options?: CallingOptions): Promise; broadcast(event: T, ...args: Parameters): Promise; diff --git a/packages/core-services/src/types/IAppsEngineService.ts b/packages/core-services/src/types/IAppsEngineService.ts index 9158d2fe3b299..e3abbb350e934 100644 --- a/packages/core-services/src/types/IAppsEngineService.ts +++ b/packages/core-services/src/types/IAppsEngineService.ts @@ -1,9 +1,16 @@ +import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata'; import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter'; import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage'; +export type AppStatusReport = { + [appId: string]: { instanceId: string; status: AppStatus }[]; +}; + export interface IAppsEngineService { isInitialized(): boolean; getApps(query: IGetAppsFilter): Promise; getAppStorageItemById(appId: string): Promise; + getAppsStatusLocal(): Promise<{ appId: string; status: AppStatus }[]>; + getAppsStatusInNodes(): Promise; } diff --git a/packages/core-services/src/types/IBroker.ts b/packages/core-services/src/types/IBroker.ts index d72221a09cabe..9ce5b693eaa41 100644 --- a/packages/core-services/src/types/IBroker.ts +++ b/packages/core-services/src/types/IBroker.ts @@ -30,6 +30,20 @@ export type BaseMetricOptions = { [key: string]: unknown; }; +export type CallingOptions = { + nodeID?: string; + // timeout?: number; + // retries?: number; + // fallbackResponse?: FallbackResponse | FallbackResponse[] | FallbackResponseHandler; + // meta?: GenericObject; + // parentSpan?: ContextParentSpan; + // parentCtx?: Context; + // requestID?: string; + // tracking?: boolean; + // paramsCloning?: boolean; + // caller?: string; +}; + export interface IServiceMetrics { register(opts: BaseMetricOptions): void; @@ -50,7 +64,7 @@ export interface IBroker { metrics?: IServiceMetrics; destroyService(service: IServiceClass): Promise; createService(service: IServiceClass, serviceDependencies?: string[]): void; - call(method: string, data: any): Promise; + call(method: string, data: any, options?: CallingOptions): Promise; broadcastToServices( services: string[], event: T, diff --git a/packages/core-typings/src/Apps.ts b/packages/core-typings/src/Apps.ts index 456aa987bb19c..06a18581cd442 100644 --- a/packages/core-typings/src/Apps.ts +++ b/packages/core-typings/src/Apps.ts @@ -129,6 +129,11 @@ export type App = { private: boolean; documentationUrl: string; migrated: boolean; + // Status of the app across the cluster (when deployment includes multiple instances) + clusterStatus?: { + instanceId: string; + status: AppStatus; + }[]; }; export type AppCategory = { diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index 528a1cc224078..d5f9de98d0ca8 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -123,6 +123,7 @@ export type AppsEndpoints = { '/apps/:id/status': { GET: () => { status: string; + clusterStatus: App['clusterStatus']; }; POST: (params: { status: AppStatus }) => { status: AppStatus; @@ -173,7 +174,7 @@ export type AppsEndpoints = { }; '/apps/installed': { - GET: () => { apps: App[] }; + GET: (params: { includeClusterStatus?: 'true' | 'false' }) => { success: true; apps: App[] } | { success: false; error: string }; }; '/apps/buildExternalAppRequest': { From 28c8dcca4ea643769ed5ebf72b764989f458cdf8 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Sun, 20 Apr 2025 12:47:12 +0000 Subject: [PATCH 151/187] Release 7.6.0-rc.0 --- .changeset/pre.json | 122 +++++++++++++++ apps/meteor/CHANGELOG.md | 140 ++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 15 ++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 12 ++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 14 ++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 14 ++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 15 ++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 15 ++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 14 ++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 14 ++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 13 ++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 ++ ee/packages/license/package.json | 2 +- ee/packages/network-broker/CHANGELOG.md | 13 ++ ee/packages/network-broker/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 ++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 15 ++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 ++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 ++ packages/api-client/package.json | 2 +- packages/apps-engine/CHANGELOG.md | 8 + packages/apps-engine/package.json | 2 +- packages/apps/CHANGELOG.md | 15 ++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 17 +++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 18 +++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 ++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 ++ packages/ddp-client/package.json | 2 +- packages/freeswitch/CHANGELOG.md | 9 ++ packages/freeswitch/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 14 ++ packages/fuselage-ui-kit/package.json | 4 +- packages/gazzodown/CHANGELOG.md | 11 ++ packages/gazzodown/package.json | 2 +- packages/i18n/CHANGELOG.md | 28 ++++ packages/i18n/package.json | 2 +- packages/instance-status/CHANGELOG.md | 9 ++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 9 ++ packages/livechat/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 16 ++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 26 ++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 31 ++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 13 ++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 ++ packages/ui-avatar/package.json | 2 +- packages/ui-client/CHANGELOG.md | 25 ++++ packages/ui-client/package.json | 2 +- packages/ui-contexts/CHANGELOG.md | 21 +++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 10 ++ packages/ui-video-conf/package.json | 2 +- packages/ui-voip/CHANGELOG.md | 19 +++ packages/ui-voip/package.json | 2 +- packages/web-ui-registration/CHANGELOG.md | 9 ++ packages/web-ui-registration/package.json | 2 +- 79 files changed, 839 insertions(+), 41 deletions(-) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000000..23d91fc54465d --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,122 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@rocket.chat/meteor": "7.6.0-develop", + "rocketchat-services": "2.0.10", + "@rocket.chat/uikit-playground": "0.6.10", + "@rocket.chat/account-service": "0.4.19", + "@rocket.chat/authorization-service": "0.4.19", + "@rocket.chat/ddp-streamer": "0.3.19", + "@rocket.chat/omnichannel-transcript": "0.4.19", + "@rocket.chat/presence-service": "0.4.19", + "@rocket.chat/queue-worker": "0.4.19", + "@rocket.chat/stream-hub-service": "0.4.19", + "@rocket.chat/license": "1.0.10", + "@rocket.chat/network-broker": "0.1.11", + "@rocket.chat/omnichannel-services": "0.3.16", + "@rocket.chat/pdf-worker": "0.2.16", + "@rocket.chat/presence": "0.2.19", + "@rocket.chat/ui-theming": "0.4.3", + "@rocket.chat/account-utils": "0.0.2", + "@rocket.chat/agenda": "0.1.0", + "@rocket.chat/api-client": "0.2.19", + "@rocket.chat/apps": "0.4.0", + "@rocket.chat/apps-engine": "1.50.0", + "@rocket.chat/base64": "1.0.13", + "@rocket.chat/cas-validate": "0.0.3", + "@rocket.chat/core-services": "0.8.0", + "@rocket.chat/core-typings": "7.6.0-develop", + "@rocket.chat/cron": "0.1.19", + "@rocket.chat/ddp-client": "0.3.19", + "@rocket.chat/eslint-config": "0.7.0", + "@rocket.chat/favicon": "0.0.2", + "@rocket.chat/freeswitch": "1.2.6", + "@rocket.chat/fuselage-ui-kit": "17.0.0", + "@rocket.chat/gazzodown": "17.0.0", + "@rocket.chat/i18n": "1.5.0", + "@rocket.chat/instance-status": "0.1.19", + "@rocket.chat/jest-presets": "0.0.1", + "@rocket.chat/jwt": "0.1.1", + "@rocket.chat/livechat": "1.22.6", + "@rocket.chat/log-format": "0.0.2", + "@rocket.chat/logger": "0.0.2", + "@rocket.chat/message-parser": "0.31.32", + "@rocket.chat/mock-providers": "0.1.9", + "@rocket.chat/model-typings": "1.5.0", + "@rocket.chat/models": "1.4.0", + "@rocket.chat/mongo-adapter": "0.0.2", + "@rocket.chat/poplib": "0.0.2", + "@rocket.chat/password-policies": "0.0.2", + "@rocket.chat/patch-injection": "0.0.1", + "@rocket.chat/peggy-loader": "0.31.27", + "@rocket.chat/random": "1.2.2", + "@rocket.chat/release-action": "2.2.3", + "@rocket.chat/release-changelog": "0.1.0", + "@rocket.chat/rest-typings": "7.6.0-develop", + "@rocket.chat/server-cloud-communication": "0.0.2", + "@rocket.chat/server-fetch": "0.0.3", + "@rocket.chat/sha256": "1.0.12", + "@rocket.chat/tools": "0.2.2", + "@rocket.chat/tracing": "0.0.1", + "@rocket.chat/ui-avatar": "13.0.0", + "@rocket.chat/ui-client": "17.0.0", + "@rocket.chat/ui-composer": "0.5.2", + "@rocket.chat/ui-contexts": "17.0.0", + "@rocket.chat/ui-kit": "0.37.0", + "@rocket.chat/ui-video-conf": "17.0.0", + "@rocket.chat/ui-voip": "7.0.0", + "@rocket.chat/web-ui-registration": "17.0.0" + }, + "changesets": [ + "afraid-boxes-destroy", + "angry-poems-build", + "beige-days-push", + "big-tips-greet", + "bright-forks-drop", + "dirty-seas-explode", + "eighty-wombats-smile", + "eleven-laws-crash", + "famous-falcons-laugh", + "few-mangos-protect", + "fifty-moles-boil", + "fluffy-weeks-float", + "four-clocks-collect", + "four-dragons-warn", + "fuzzy-eyes-clean", + "gold-laws-wink", + "good-bobcats-argue", + "gorgeous-turtles-flow", + "healthy-insects-cheer", + "heavy-boats-mix", + "honest-toys-guess", + "hot-beers-glow", + "hungry-wasps-remember", + "lovely-waves-sniff", + "nine-paws-sit", + "odd-waves-destroy", + "old-readers-battle", + "plenty-baboons-kneel", + "polite-turkeys-fetch", + "poor-spies-hug", + "purple-hairs-hang", + "rotten-candles-train", + "serious-grapes-smell", + "slow-ravens-tie", + "small-snails-burn", + "strong-shoes-end", + "stupid-rabbits-hide", + "sweet-ravens-refuse", + "tall-hornets-live", + "tasty-pianos-bathe", + "ten-schools-collect", + "thin-cycles-return", + "tidy-cups-smoke", + "wet-penguins-end", + "witty-buttons-greet", + "witty-foxes-thank", + "yellow-comics-cheer", + "young-avocados-brake", + "young-kiwis-fly" + ] +} diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 23d3627aa6508..af6b910c6bb2e 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,145 @@ # @rocket.chat/meteor +## 7.6.0-rc.0 + +### Minor Changes + +- ([#35717](https://github.com/RocketChat/Rocket.Chat/pull/35717)) Adds new settings to allow configuring custom variables with string manipulation functions on the LDAP data mapper + +- ([#35280](https://github.com/RocketChat/Rocket.Chat/pull/35280)) Allows apps to react to department status changes. + +- ([#35644](https://github.com/RocketChat/Rocket.Chat/pull/35644)) Adds the ability to dynamically add and remove options from select/multi-select settings in the Apps Engine to support more flexible configuration scenarios by exposing two new methods on the settings API. + +- ([#34954](https://github.com/RocketChat/Rocket.Chat/pull/34954) by [@tapiarafael](https://github.com/tapiarafael)) Allows search omnichannel rooms by the exact visitor name using double quotes to have a faster response + +- ([#35613](https://github.com/RocketChat/Rocket.Chat/pull/35613)) Replaces the parent room tag in room header in favor of a button to back to the parent room + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35615](https://github.com/RocketChat/Rocket.Chat/pull/35615)) Removes the avatar in the room header + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35218](https://github.com/RocketChat/Rocket.Chat/pull/35218)) Adds a new admin page to audit settings changes in a server + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#35631](https://github.com/RocketChat/Rocket.Chat/pull/35631)) Places the room topic next to the room title + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#34494](https://github.com/RocketChat/Rocket.Chat/pull/34494)) Implements auditing events for `/v1/users.update` API endpoint + +- ([#35672](https://github.com/RocketChat/Rocket.Chat/pull/35672)) Restores the previous room announcement style + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35464](https://github.com/RocketChat/Rocket.Chat/pull/35464)) Allows deleting federated remote users in case they are not present in the homeserver. + +- ([#35718](https://github.com/RocketChat/Rocket.Chat/pull/35718)) Adds a new setting to allow syncing federated users data through LDAP + +- ([#35807](https://github.com/RocketChat/Rocket.Chat/pull/35807)) Moves the room search functionality from the sidebar to the navbar and reorganize their relative actions + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35700](https://github.com/RocketChat/Rocket.Chat/pull/35700)) Separates voice call and video call room actions on room header. + +- ([#35703](https://github.com/RocketChat/Rocket.Chat/pull/35703)) Adds close action to contact unknown callout displayed within Livechat rooms + +### Patch Changes + +- ([#35790](https://github.com/RocketChat/Rocket.Chat/pull/35790)) Fixes an issue in `Admin > Settings` page where sometimes settings guarded by a license module would not be editable despite having the required modules. + +- ([#35795](https://github.com/RocketChat/Rocket.Chat/pull/35795)) Fixes incorrect message sender for incoming webhooks when "Post As" field is updated by ensuring both username and userId are synced to reflect the selected user. + +- ([#35299](https://github.com/RocketChat/Rocket.Chat/pull/35299)) Fixes user email verification update not working on admin > users + +- ([#35762](https://github.com/RocketChat/Rocket.Chat/pull/35762)) Fixes an issue with apps loading in microservices in some cases by ensuring all services have started before trying to load apps + +- ([#35816](https://github.com/RocketChat/Rocket.Chat/pull/35816)) Fixes an issue where the outlook calendar items list is not rendering properly + +- ([#35244](https://github.com/RocketChat/Rocket.Chat/pull/35244) by [@tapiarafael](https://github.com/tapiarafael)) Ensures seat limit validation in LDAP sync, preventing activations beyond license restrictions. + +- ([#35497](https://github.com/RocketChat/Rocket.Chat/pull/35497)) Fixes an issue where the app's logs index was not being created by default sometimes, also set to be always 30 days + +- ([#35736](https://github.com/RocketChat/Rocket.Chat/pull/35736)) Fixes an error causing the server to throw an "unhandled promise rejection" when removing an agent from a department without a business hour when using `Multiple` business hours + +- ([#35722](https://github.com/RocketChat/Rocket.Chat/pull/35722)) Fixes the behavior of "Maximum number of simultaneous chats" settings, making them more predictable. Previously, we applied a single limit per operation, being the order: `Department > Agent > Global`. This caused the department limit to take prescedence over agent's specific limit, causing some unwanted side effects. + + The new way of applying the filter is as follows: + + - An agent can accept chats from multiple departments, respecting each department’s limit individually. + - The total number of active chats (across all departments) must not exceed the configured Agent-Level or Global limit. + - If neither the Agent-Level nor Global Limit is set, only department-specific limits apply. + - If no limits are set at any level, there is no restriction on the number of chats an agent can handle. + +- ([#35394](https://github.com/RocketChat/Rocket.Chat/pull/35394) by [@sushen123](https://github.com/sushen123)) Fixes the save button loading state in app settings, ensuring it resets properly after saving. + +- ([#35779](https://github.com/RocketChat/Rocket.Chat/pull/35779)) Fixes a fetch infinite loop when adding agents to a department + +- ([#35679](https://github.com/RocketChat/Rocket.Chat/pull/35679)) Fixes a GUI crash when editing a canned response with tags via room contextual bar. + +- ([#35568](https://github.com/RocketChat/Rocket.Chat/pull/35568)) Fixes an issue with the leave room confirmation modal not displaying the room's name. + +- ([#35782](https://github.com/RocketChat/Rocket.Chat/pull/35782)) Fixes an issue where the incoming webhooks integration allowed messages to be sent to public channels under private teams by users who were not members of the team. + +- ([#35618](https://github.com/RocketChat/Rocket.Chat/pull/35618)) Fixes an issue when sending a message on an omnichannel room would cause the web client to fetch the room data again. + +- ([#35273](https://github.com/RocketChat/Rocket.Chat/pull/35273)) Fixes an issue where the code input type in settings renders duplicate code text boxes. + +- ([#35765](https://github.com/RocketChat/Rocket.Chat/pull/35765)) Fixes an issue causing VoIP calls to no longer reach the client after a temporary disconnection + +- ([#35397](https://github.com/RocketChat/Rocket.Chat/pull/35397)) Fixes a race condition while federating that caused direct messages to not work + +- ([#35832](https://github.com/RocketChat/Rocket.Chat/pull/35832)) Fixes an issue where Voice Calls were unable to gather Ice Servers + +- ([#35757](https://github.com/RocketChat/Rocket.Chat/pull/35757)) Fixes an issue where the bypass to call methods over microservices always returns to `{}` + +- ([#35831](https://github.com/RocketChat/Rocket.Chat/pull/35831)) Fixes an issue where enterprise routing algorithms could get stuck on selecting the same agent due to chat limits being applied after agent selection, but before agent assignment + +- ([#35698](https://github.com/RocketChat/Rocket.Chat/pull/35698)) Fixes the "Enabled" toggle always being displayed as active when editing a business hour. + +- ([#35715](https://github.com/RocketChat/Rocket.Chat/pull/35715)) Fixes an issue with dynamic API routes requiring a server restart to be operable. + +- ([#35614](https://github.com/RocketChat/Rocket.Chat/pull/35614)) Fixes an issue where header toolbox items are missing the proper margin in e2ee setup room header + +- ([#35716](https://github.com/RocketChat/Rocket.Chat/pull/35716)) Fixes an issue where a federated user's display name would be overwritten by its username + +- ([#35766](https://github.com/RocketChat/Rocket.Chat/pull/35766)) Fixes an issue where quick reactions (Feature Preview) would be absent on first access and after page refreshes + +- ([#35709](https://github.com/RocketChat/Rocket.Chat/pull/35709)) Improves UX for users with mandatory 2FA roles by clarifying required actions + +- ([#35616](https://github.com/RocketChat/Rocket.Chat/pull/35616)) Fixes Omnichannel Contact Center's chats filter not working when "From" and "To" fields have the same date + +- ([#35019](https://github.com/RocketChat/Rocket.Chat/pull/35019)) Fixes issue with some charts on engagement dashboard not showing local time and missing data visualizers + +- ([#35733](https://github.com/RocketChat/Rocket.Chat/pull/35733)) Fixes a typo in the app update success toast + +- ([#35810](https://github.com/RocketChat/Rocket.Chat/pull/35810)) Fixes an issue on the LDAP Background Sync where the process would stop syncing users if any of the LDAP users couldn't be properly mapped to a Rocket.Chat User (for example, by not having an email address) + +- ([#35580](https://github.com/RocketChat/Rocket.Chat/pull/35580)) Fixes contact custom fields not being updated when updating a visitor's custom field + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d649a761edd71e1325a635b757ef1df2e5a778a4, bbd14f84214b4785f2b58cfeb8e9117bdfbf18e8, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, f545617c2ac3d67af533e64c2670d8d564a56d15, bffc49f426259925c415651c2b2a58083dac547a, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, ec7894139c56d0e29ac696c448cc932efb6cb0f0, 6bf386dcc2a560963cf719fbc2d96569ce23a2de, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 5e3ab1a07163cd22ad4c41502ef232845d26bdc2, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 2ee1a81de770a682f6e7a8590a896e76a32f4e3c, 47ae69912cd90743e7bf836fdee4be481a01bbba, 72725d391e79b44e7380ee2fe640e2e4426c77ca, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, cc344bea08c08501f50e9cee620b2926a322a4ee, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de, be67bb771294c337c28da5e61ae47ab4e32244d1, 895ea3fdbba1d0e3cf1bed03cb8d0abfcca5d351]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/i18n@1.6.0-rc.0 + - @rocket.chat/apps-engine@1.51.0-rc.0 + - @rocket.chat/apps@0.5.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/ui-client@18.0.0-rc.0 + - @rocket.chat/ui-voip@8.0.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/pdf-worker@0.3.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 + - @rocket.chat/license@1.0.11-rc.0 + - @rocket.chat/omnichannel-services@0.3.17-rc.0 + - @rocket.chat/presence@0.2.20-rc.0 + - @rocket.chat/api-client@0.2.20-rc.0 + - @rocket.chat/cron@0.1.20-rc.0 + - @rocket.chat/freeswitch@1.2.7-rc.0 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.0 + - @rocket.chat/gazzodown@18.0.0-rc.0 + - @rocket.chat/web-ui-registration@18.0.0-rc.0 + - @rocket.chat/instance-status@0.1.20-rc.0 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.0 + - @rocket.chat/ui-video-conf@18.0.0-rc.0 + - @rocket.chat/server-cloud-communication@0.0.2 +
+ ## 7.5.0 ### Minor Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index ce63ec6724133..2d1db6a381089 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.6.0-develop" + "version": "7.6.0-rc.0" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 445df25fd1c5b..4145f2b94e79a 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,20 @@ # rocketchat-services +## 2.0.11-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d649a761edd71e1325a635b757ef1df2e5a778a4, bbd14f84214b4785f2b58cfeb8e9117bdfbf18e8, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/apps-engine@1.51.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
+ ## 2.0.10 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index b5ddf9e0e9646..9b40d9c59a81a 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "2.0.10", + "version": "2.0.11-rc.0", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index c913ffb323134..a6d2a95f5c5ba 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.6.0-develop", + "version": "7.6.0-rc.0", "private": true, "type": "commonjs", "author": { diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index 1e173b694b5ec..afd5741629278 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/uikit-playground +## 0.6.11-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.0 + - @rocket.chat/ui-avatar@14.0.0-rc.0 +
+ ## 0.6.10 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 55c2091acb779..9c594b9c5ddaa 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.6.10", + "version": "0.6.11-rc.0", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 62fda2782d5c0..412f8c28beb54 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/account-service +## 0.4.20-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
+ ## 0.4.19 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index f1098ebe9468a..dc0afc9f7cdef 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.19", + "version": "0.4.20-rc.0", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index af0aa045f3c49..79aa40315c379 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/authorization-service +## 0.4.20-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
+ ## 0.4.19 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 984f88ccf866f..a3e7405229bc7 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.19", + "version": "0.4.20-rc.0", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 284e5b0754d2a..a7df6e1fb45c7 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/ddp-streamer +## 0.3.20-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 + - @rocket.chat/instance-status@0.1.20-rc.0 +
+ ## 0.3.19 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 67b55701635f6..42e023fd474c1 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.19", + "version": "0.3.20-rc.0", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 4eab8227ff88f..168148696b2aa 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/omnichannel-transcript +## 0.4.20-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 2ee1a81de770a682f6e7a8590a896e76a32f4e3c, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/pdf-worker@0.3.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.17-rc.0 +
+ ## 0.4.19 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 0d32ed948c2ca..6b3d4b427c977 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.19", + "version": "0.4.20-rc.0", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 97cfb75cc7e33..caaa63bf1e718 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/presence-service +## 0.4.20-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/presence@0.2.20-rc.0 +
+ ## 0.4.19 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 4ab915b1b734c..baccf9eb51fe0 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.19", + "version": "0.4.20-rc.0", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index 2b517452595c9..16655fb1ee542 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/queue-worker +## 0.4.20-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.17-rc.0 +
+ ## 0.4.19 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 9234794114607..1f02b9d5e9740 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.19", + "version": "0.4.20-rc.0", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index d30d087e19776..f0d6c20dcfef4 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/stream-hub-service +## 0.4.20-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/network-broker@0.2.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 +
+ ## 0.4.19 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 11f27590a3de2..dca644636492d 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.19", + "version": "0.4.20-rc.0", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index cedf001c15c59..932abea430458 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 1.0.11-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 +
+ ## 1.0.10 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index cfe74e3aea6f1..54405c1d963e4 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "1.0.10", + "version": "1.0.11-rc.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/network-broker/CHANGELOG.md b/ee/packages/network-broker/CHANGELOG.md index 7007e30376a3f..3f9ecb8242b9f 100644 --- a/ee/packages/network-broker/CHANGELOG.md +++ b/ee/packages/network-broker/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/network-broker +## 0.2.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +-
Updated dependencies [d8eb824d242cbbeafb11b1c4a806860e4541ba79, e868a6f6598b7eb2843ef79126d18abd1f604b4f]: + + - @rocket.chat/core-services@0.9.0-rc.0 +
+ ## 0.1.11 ### Patch Changes diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index 8fb49f4f1cfe2..be3a420081282 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/network-broker", - "version": "0.1.11", + "version": "0.2.0-rc.0", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 1fbcfe427bcaf..8e0ae583535a8 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.17-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 2ee1a81de770a682f6e7a8590a896e76a32f4e3c, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/pdf-worker@0.3.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
+ ## 0.3.16 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 50d4a2a5d493a..08b3b0e236621 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.16", + "version": "0.3.17-rc.0", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 0b2ca7d4e0df6..8b7ce7801a9f2 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/pdf-worker +## 0.3.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +- ([#35705](https://github.com/RocketChat/Rocket.Chat/pull/35705)) Fixes an issue with PDF generation process that caused the server to hang when a single message consisted of too many (+30) markdown elements and was followed and preceded by more messages. + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 +
+ ## 0.2.16 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index 46722ee717e3a..043296cd07244 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.2.16", + "version": "0.3.0-rc.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index b3ae01737e24c..82c53f1337c33 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.20-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, e868a6f6598b7eb2843ef79126d18abd1f604b4f, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/core-services@0.9.0-rc.0 +
+ ## 0.2.19 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index ad13401065684..d014a5de5eb4a 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.19", + "version": "0.2.20-rc.0", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/package.json b/package.json index 11e18c181b06b..ee78a8fe3c9f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.6.0-develop", + "version": "7.6.0-rc.0", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index b2aca891c8d65..6945c7a3bf0d8 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.20-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
+ ## 0.2.19 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 42a78935b771b..0fd816cbde408 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.19", + "version": "0.2.20-rc.0", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/apps-engine/CHANGELOG.md b/packages/apps-engine/CHANGELOG.md index 60377b01a9048..6762906013c60 100644 --- a/packages/apps-engine/CHANGELOG.md +++ b/packages/apps-engine/CHANGELOG.md @@ -1,5 +1,13 @@ # @rocket.chat/apps-engine +## 1.51.0-rc.0 + +### Minor Changes + +- ([#35280](https://github.com/RocketChat/Rocket.Chat/pull/35280)) Allows apps to react to department status changes. + +- ([#35644](https://github.com/RocketChat/Rocket.Chat/pull/35644)) Adds the ability to dynamically add and remove options from select/multi-select settings in the Apps Engine to support more flexible configuration scenarios by exposing two new methods on the settings API. + ## 1.50.0 ### Minor Changes diff --git a/packages/apps-engine/package.json b/packages/apps-engine/package.json index 0502ea8c9a082..625235b7a0bcd 100644 --- a/packages/apps-engine/package.json +++ b/packages/apps-engine/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps-engine", - "version": "1.50.0", + "version": "1.51.0-rc.0", "description": "The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.", "main": "index", "typings": "index", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index e0fd38fe11f68..0368b6d28e74f 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/apps +## 0.5.0-rc.0 + +### Minor Changes + +- ([#35280](https://github.com/RocketChat/Rocket.Chat/pull/35280)) Allows apps to react to department status changes. + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d649a761edd71e1325a635b757ef1df2e5a778a4, bbd14f84214b4785f2b58cfeb8e9117bdfbf18e8, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/apps-engine@1.51.0-rc.0 + - @rocket.chat/model-typings@1.6.0-rc.0 +
+ ## 0.4.0 ### Minor Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index 3207f699f4102..f679c69098794 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.4.0", + "version": "0.5.0-rc.0", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index aaeee90d37674..5581a2eb2af1e 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,22 @@ # @rocket.chat/core-services +## 0.9.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +- ([#35757](https://github.com/RocketChat/Rocket.Chat/pull/35757)) Fixes an issue where the bypass to call methods over microservices always returns to `{}` + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
+ ## 0.8.0 ### Minor Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 4747f3b13470d..0f864faccbc2c 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.8.0", + "version": "0.9.0-rc.0", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index 4ce7d05b6e447..b12ebac7d99b8 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,23 @@ # @rocket.chat/core-typings +## 7.6.0-rc.0 + +### Minor Changes + +- ([#35717](https://github.com/RocketChat/Rocket.Chat/pull/35717)) Adds new settings to allow configuring custom variables with string manipulation functions on the LDAP data mapper + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#34494](https://github.com/RocketChat/Rocket.Chat/pull/34494)) Implements auditing events for `/v1/users.update` API endpoint + +- ([#35718](https://github.com/RocketChat/Rocket.Chat/pull/35718)) Adds a new setting to allow syncing federated users data through LDAP + +### Patch Changes + +- ([#35790](https://github.com/RocketChat/Rocket.Chat/pull/35790)) Fixes an issue in `Admin > Settings` page where sometimes settings guarded by a license module would not be editable despite having the required modules. + +- ([#35832](https://github.com/RocketChat/Rocket.Chat/pull/35832)) Fixes an issue where Voice Calls were unable to gather Ice Servers + ## 7.5.0 ### Minor Changes diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index ed2933bdfe31c..2722de6ee1191 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.6.0-develop", + "version": "7.6.0-rc.0", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 1c1237d06cff7..277bac8c0b3fc 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.20-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, 3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/models@1.5.0-rc.0 +
+ ## 0.1.19 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index ab6b29e6e0d23..e1090b055ef26 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.19", + "version": "0.1.20-rc.0", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index f5cca9a91f535..89db5634faed7 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.20-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 + - @rocket.chat/api-client@0.2.20-rc.0 +
+ ## 0.3.19 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 734ced27eb2c8..46a990580e3bc 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.19", + "version": "0.3.20-rc.0", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/freeswitch/CHANGELOG.md b/packages/freeswitch/CHANGELOG.md index 316ef7e2fca43..500076690ddd4 100644 --- a/packages/freeswitch/CHANGELOG.md +++ b/packages/freeswitch/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/freeswitch +## 1.2.7-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 +
+ ## 1.2.6 ### Patch Changes diff --git a/packages/freeswitch/package.json b/packages/freeswitch/package.json index e58d0f3c9dd7a..6450f57a80738 100644 --- a/packages/freeswitch/package.json +++ b/packages/freeswitch/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/freeswitch", - "version": "1.2.6", + "version": "1.2.7-rc.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 47abea2564add..fea3de387902c 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log +## 18.0.0-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d649a761edd71e1325a635b757ef1df2e5a778a4, bbd14f84214b4785f2b58cfeb8e9117bdfbf18e8, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/apps-engine@1.51.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/gazzodown@18.0.0-rc.0 + - @rocket.chat/ui-avatar@14.0.0-rc.0 + - @rocket.chat/ui-video-conf@18.0.0-rc.0 +
+ ## 17.0.0 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index ccf2ecab1eeed..75f34f065a419 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/fuselage-ui-kit", - "version": "17.0.0", + "version": "18.0.0-rc.0", "private": true, "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", @@ -93,7 +93,7 @@ "typescript": "~5.7.2" }, "peerDependencies": { - "@rocket.chat/apps-engine": "1.50.0", + "@rocket.chat/apps-engine": "1.51.0-rc.0", "@rocket.chat/eslint-config": "0.7.0", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index b7248b5916595..82238143a6f47 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/gazzodown +## 18.0.0-rc.0 + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, f545617c2ac3d67af533e64c2670d8d564a56d15, 6bf386dcc2a560963cf719fbc2d96569ce23a2de, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 5e3ab1a07163cd22ad4c41502ef232845d26bdc2, 47ae69912cd90743e7bf836fdee4be481a01bbba, 72725d391e79b44e7380ee2fe640e2e4426c77ca, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/ui-client@18.0.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 +
+ ## 17.0.0 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 17c4e380d31f2..75e86731aba80 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "17.0.0", + "version": "18.0.0-rc.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 5315f9378448f..7fe2303223406 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,33 @@ # @rocket.chat/i18n +## 1.6.0-rc.0 + +### Minor Changes + +- ([#35717](https://github.com/RocketChat/Rocket.Chat/pull/35717)) Adds new settings to allow configuring custom variables with string manipulation functions on the LDAP data mapper + +- ([#35613](https://github.com/RocketChat/Rocket.Chat/pull/35613)) Replaces the parent room tag in room header in favor of a button to back to the parent room + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35218](https://github.com/RocketChat/Rocket.Chat/pull/35218)) Adds a new admin page to audit settings changes in a server + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#35718](https://github.com/RocketChat/Rocket.Chat/pull/35718)) Adds a new setting to allow syncing federated users data through LDAP + +- ([#35807](https://github.com/RocketChat/Rocket.Chat/pull/35807)) Moves the room search functionality from the sidebar to the navbar and reorganize their relative actions + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35703](https://github.com/RocketChat/Rocket.Chat/pull/35703)) Adds close action to contact unknown callout displayed within Livechat rooms + +### Patch Changes + +- ([#35568](https://github.com/RocketChat/Rocket.Chat/pull/35568)) Fixes an issue with the leave room confirmation modal not displaying the room's name. + +- ([#35832](https://github.com/RocketChat/Rocket.Chat/pull/35832)) Fixes an issue where Voice Calls were unable to gather Ice Servers + +- ([#35709](https://github.com/RocketChat/Rocket.Chat/pull/35709)) Improves UX for users with mandatory 2FA roles by clarifying required actions + +- ([#35733](https://github.com/RocketChat/Rocket.Chat/pull/35733)) Fixes a typo in the app update success toast + ## 1.5.0 ### Minor Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index c15599b47fc2c..f58bf8beb6d65 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/i18n", - "version": "1.5.0", + "version": "1.6.0-rc.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index 641e4a200e39f..5284e6793abe5 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.20-rc.0 + +### Patch Changes + +-
Updated dependencies [3f1cddac558a1edc68c94d635698e1245c7172e2, 45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, a8896a7ed96021f1c0d0b1eb44945ee3f69a080b, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 47ae69912cd90743e7bf836fdee4be481a01bbba]: + + - @rocket.chat/models@1.5.0-rc.0 +
+ ## 0.1.19 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 49c06fcc4b913..5b17e60aa4a06 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.19", + "version": "0.1.20-rc.0", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 53d08b7619646..e9877e9d978ac 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/livechat Change Log +## 1.22.7-rc.0 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.0 +
+ ## 1.22.6 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 77bfb3b1f1e51..28e247b4ca66a 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.22.6", + "version": "1.22.7-rc.0", "files": [ "/build" ], diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index f0f913141bc02..36be05c9fc06a 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,21 @@ # @rocket.chat/mock-providers +## 0.2.0-rc.0 + +### Minor Changes + +- ([#35218](https://github.com/RocketChat/Rocket.Chat/pull/35218)) Adds a new admin page to audit settings changes in a server + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +-
Updated dependencies [2c190740d0ff166a4cefe8e833b0b2682a41fab1, f545617c2ac3d67af533e64c2670d8d564a56d15, bffc49f426259925c415651c2b2a58083dac547a, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, cc344bea08c08501f50e9cee620b2926a322a4ee, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de, be67bb771294c337c28da5e61ae47ab4e32244d1, 895ea3fdbba1d0e3cf1bed03cb8d0abfcca5d351]: + + - @rocket.chat/i18n@1.6.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 +
+ ## 0.1.9 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 7a6e9c4907fc8..6a7042dc9b2a9 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.1.9", + "version": "0.2.0-rc.0", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index 0141f8c73ce90..de4bb1f6451d3 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,31 @@ # @rocket.chat/model-typings +## 1.6.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#34494](https://github.com/RocketChat/Rocket.Chat/pull/34494)) Implements auditing events for `/v1/users.update` API endpoint + +### Patch Changes + +- ([#35497](https://github.com/RocketChat/Rocket.Chat/pull/35497)) Fixes an issue where the app's logs index was not being created by default sometimes, also set to be always 30 days + +- ([#35722](https://github.com/RocketChat/Rocket.Chat/pull/35722)) Fixes the behavior of "Maximum number of simultaneous chats" settings, making them more predictable. Previously, we applied a single limit per operation, being the order: `Department > Agent > Global`. This caused the department limit to take prescedence over agent's specific limit, causing some unwanted side effects. + + The new way of applying the filter is as follows: + + - An agent can accept chats from multiple departments, respecting each department’s limit individually. + - The total number of active chats (across all departments) must not exceed the configured Agent-Level or Global limit. + - If neither the Agent-Level nor Global Limit is set, only department-specific limits apply. + - If no limits are set at any level, there is no restriction on the number of chats an agent can handle. + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 +
+ ## 1.5.0 ### Minor Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 5f511b59c1a9e..165c1bf2e1bbe 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "1.5.0", + "version": "1.6.0-rc.0", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 1dd8d87c12cd4..9077b8e77922d 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,36 @@ # @rocket.chat/models +## 1.5.0-rc.0 + +### Minor Changes + +- ([#34954](https://github.com/RocketChat/Rocket.Chat/pull/34954) by [@tapiarafael](https://github.com/tapiarafael)) Allows search omnichannel rooms by the exact visitor name using double quotes to have a faster response + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#34494](https://github.com/RocketChat/Rocket.Chat/pull/34494)) Implements auditing events for `/v1/users.update` API endpoint + +### Patch Changes + +- ([#35497](https://github.com/RocketChat/Rocket.Chat/pull/35497)) Fixes an issue where the app's logs index was not being created by default sometimes, also set to be always 30 days + +- ([#35722](https://github.com/RocketChat/Rocket.Chat/pull/35722)) Fixes the behavior of "Maximum number of simultaneous chats" settings, making them more predictable. Previously, we applied a single limit per operation, being the order: `Department > Agent > Global`. This caused the department limit to take prescedence over agent's specific limit, causing some unwanted side effects. + + The new way of applying the filter is as follows: + + - An agent can accept chats from multiple departments, respecting each department’s limit individually. + - The total number of active chats (across all departments) must not exceed the configured Agent-Level or Global limit. + - If neither the Agent-Level nor Global Limit is set, only department-specific limits apply. + - If no limits are set at any level, there is no restriction on the number of chats an agent can handle. + +- ([#35618](https://github.com/RocketChat/Rocket.Chat/pull/35618)) Fixes an issue when sending a message on an omnichannel room would cause the web client to fetch the room data again. + +-
Updated dependencies [45a93a7713546ed2e3e0b3988e1f989371ebf53a, 5f11fea4ab1dc149f82b7d8c5fc556a2cf09fa5e, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 47ae69912cd90743e7bf836fdee4be481a01bbba]: + + - @rocket.chat/model-typings@1.6.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 +
+ ## 1.4.0 ### Minor Changes diff --git a/packages/models/package.json b/packages/models/package.json index 37576c5c835ad..dfb35d962dd9c 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "1.4.0", + "version": "1.5.0-rc.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 81ff0ff25ea49..18610e388917b 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/rest-typings +## 7.6.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4]: + + - @rocket.chat/core-typings@7.6.0-rc.0 +
+ ## 7.5.0 ### Minor Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index fd077c48418b0..2ee9d4ac0b0c9 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.6.0-develop", + "version": "7.6.0-rc.0", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index 539ee39113aae..2f95497774081 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 14.0.0-rc.0 + +### Patch Changes + +-
Updated dependencies [1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/ui-contexts@18.0.0-rc.0 +
+ ## 13.0.0 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index b76e2202f62a6..ddfbf7d242e82 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "13.0.0", + "version": "14.0.0-rc.0", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index f647338ac99ed..73db61ec730d4 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,30 @@ # @rocket.chat/ui-client +## 18.0.0-rc.0 + +### Minor Changes + +- ([#35613](https://github.com/RocketChat/Rocket.Chat/pull/35613)) Replaces the parent room tag in room header in favor of a button to back to the parent room + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35615](https://github.com/RocketChat/Rocket.Chat/pull/35615)) Removes the avatar in the room header + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35218](https://github.com/RocketChat/Rocket.Chat/pull/35218)) Adds a new admin page to audit settings changes in a server + +- ([#35631](https://github.com/RocketChat/Rocket.Chat/pull/35631)) Places the room topic next to the room title + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35672](https://github.com/RocketChat/Rocket.Chat/pull/35672)) Restores the previous room announcement style + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it +- ([#35807](https://github.com/RocketChat/Rocket.Chat/pull/35807)) Moves the room search functionality from the sidebar to the navbar and reorganize their relative actions + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it + +### Patch Changes + +-
Updated dependencies [1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/ui-avatar@14.0.0-rc.0 +
+ ## 17.0.0 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index a1a175904b8ee..0fd2b19b4a1e1 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "17.0.0", + "version": "18.0.0-rc.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index fc8bccb1fba13..badc73df27087 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,26 @@ # @rocket.chat/ui-contexts +## 18.0.0-rc.0 + +### Minor Changes + +- ([#35218](https://github.com/RocketChat/Rocket.Chat/pull/35218)) Adds a new admin page to audit settings changes in a server + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +- ([#35807](https://github.com/RocketChat/Rocket.Chat/pull/35807)) Moves the room search functionality from the sidebar to the navbar and reorganize their relative actions + > This change is being tested under `Enhanced navigation experience` feature preview, in order to check it you need to enabled it + +### Patch Changes + +-
Updated dependencies [aec9eaa941fe9dad81f38d8d18d1b58edd700eb1, 2c190740d0ff166a4cefe8e833b0b2682a41fab1, f545617c2ac3d67af533e64c2670d8d564a56d15, bffc49f426259925c415651c2b2a58083dac547a, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, bbd0b0d9ed181a156430e2a446d3b56092e3f645, 47ae69912cd90743e7bf836fdee4be481a01bbba, 4b28126ac94cf1d3312b30ad9863ca02673f49d4, cc344bea08c08501f50e9cee620b2926a322a4ee, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de, be67bb771294c337c28da5e61ae47ab4e32244d1, 895ea3fdbba1d0e3cf1bed03cb8d0abfcca5d351]: + + - @rocket.chat/core-typings@7.6.0-rc.0 + - @rocket.chat/i18n@1.6.0-rc.0 + - @rocket.chat/rest-typings@7.6.0-rc.0 + - @rocket.chat/ddp-client@0.3.20-rc.0 +
+ ## 17.0.0 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 20a4a8d3f4dc5..d1155e4f3e6f7 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "17.0.0", + "version": "18.0.0-rc.0", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index c1a55ade47c81..2ffb47586ff63 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 18.0.0-rc.0 + +### Patch Changes + +-
Updated dependencies [1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/ui-avatar@14.0.0-rc.0 +
+ ## 17.0.0 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index c60a9fd9b1d21..fa6a0781fca55 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "17.0.0", + "version": "18.0.0-rc.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-voip/CHANGELOG.md b/packages/ui-voip/CHANGELOG.md index a62f933fd2a2b..04de5752c5a48 100644 --- a/packages/ui-voip/CHANGELOG.md +++ b/packages/ui-voip/CHANGELOG.md @@ -1,5 +1,24 @@ # @rocket.chat/ui-voip +## 8.0.0-rc.0 + +### Minor Changes + +- ([#35721](https://github.com/RocketChat/Rocket.Chat/pull/35721)) Enhances the `/api/apps/installed` and `/api/apps/:id/status` endpoints so they get apps' status across the cluster in High-Availability and Microservices deployments + +### Patch Changes + +- ([#35765](https://github.com/RocketChat/Rocket.Chat/pull/35765)) Fixes an issue causing VoIP calls to no longer reach the client after a temporary disconnection + +- ([#35832](https://github.com/RocketChat/Rocket.Chat/pull/35832)) Fixes an issue where Voice Calls were unable to gather Ice Servers + +-
Updated dependencies [f545617c2ac3d67af533e64c2670d8d564a56d15, 6bf386dcc2a560963cf719fbc2d96569ce23a2de, 1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 5e3ab1a07163cd22ad4c41502ef232845d26bdc2, 72725d391e79b44e7380ee2fe640e2e4426c77ca, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/ui-client@18.0.0-rc.0 + - @rocket.chat/ui-contexts@18.0.0-rc.0 + - @rocket.chat/ui-avatar@14.0.0-rc.0 +
+ ## 7.0.0 ### Patch Changes diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index c2c70760bd75a..3beec493c9022 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-voip", - "version": "7.0.0", + "version": "8.0.0-rc.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index bcd12c12a52d6..ea9f1f1db9e7f 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 18.0.0-rc.0 + +### Patch Changes + +-
Updated dependencies [1eeb139158fcd621a2b8d3a7de5bb512e659261d, d8eb824d242cbbeafb11b1c4a806860e4541ba79, 4690c55d8e379d0bd5dfa444f3e0a4175e88d8de]: + + - @rocket.chat/ui-contexts@18.0.0-rc.0 +
+ ## 17.0.0 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 92da0226f19c9..ed13bd45d807c 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "17.0.0", + "version": "18.0.0-rc.0", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", From 59e3541aa79a678369adfe66b703e65cbceeff88 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 22 Apr 2025 11:57:21 -0300 Subject: [PATCH 152/187] regression: fix notifications not being sent if user is idle or offline (#35849) --- apps/meteor/app/notification-queue/server/NotificationQueue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/notification-queue/server/NotificationQueue.ts b/apps/meteor/app/notification-queue/server/NotificationQueue.ts index 8bfcead0e47f9..6690bea41f521 100644 --- a/apps/meteor/app/notification-queue/server/NotificationQueue.ts +++ b/apps/meteor/app/notification-queue/server/NotificationQueue.ts @@ -169,7 +169,7 @@ class NotificationClass { rid, mid, ts: new Date(), - schedule, + ...(schedule && { schedule }), items, }); } From a083701a9609d46f021034ed29fe3558b7ebfb75 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Tue, 22 Apr 2025 20:49:02 +0000 Subject: [PATCH 153/187] Release 7.6.0-rc.1 [no ci] --- .changeset/bump-patch-1745354933374.md | 5 +++ .changeset/pre.json | 1 + apps/meteor/CHANGELOG.md | 35 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 ++++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 12 +++++++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 14 ++++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 14 ++++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 15 ++++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 15 ++++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 14 ++++++++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 14 ++++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 13 +++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 +++++ ee/packages/license/package.json | 2 +- ee/packages/network-broker/CHANGELOG.md | 9 +++++ ee/packages/network-broker/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 ++++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 9 +++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 ++++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 ++++++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 10 ++++++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 11 ++++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 2 ++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 ++++++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 ++++++ packages/ddp-client/package.json | 2 +- packages/freeswitch/CHANGELOG.md | 9 +++++ packages/freeswitch/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 13 +++++++ packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/CHANGELOG.md | 11 ++++++ packages/gazzodown/package.json | 2 +- packages/instance-status/CHANGELOG.md | 9 +++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 9 +++++ packages/livechat/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 +++++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 9 +++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 10 ++++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 9 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 +++++ packages/ui-avatar/package.json | 2 +- packages/ui-client/CHANGELOG.md | 10 ++++++ packages/ui-client/package.json | 2 +- packages/ui-contexts/CHANGELOG.md | 11 ++++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 10 ++++++ packages/ui-video-conf/package.json | 2 +- packages/ui-voip/CHANGELOG.md | 11 ++++++ packages/ui-voip/package.json | 2 +- packages/web-ui-registration/CHANGELOG.md | 9 +++++ packages/web-ui-registration/package.json | 2 +- yarn.lock | 2 +- 77 files changed, 459 insertions(+), 39 deletions(-) create mode 100644 .changeset/bump-patch-1745354933374.md diff --git a/.changeset/bump-patch-1745354933374.md b/.changeset/bump-patch-1745354933374.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1745354933374.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/pre.json b/.changeset/pre.json index 23d91fc54465d..c945c41fbb582 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -74,6 +74,7 @@ "beige-days-push", "big-tips-greet", "bright-forks-drop", + "bump-patch-1745354933374", "dirty-seas-explode", "eighty-wombats-smile", "eleven-laws-crash", diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index af6b910c6bb2e..fde3672ddddba 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,40 @@ # @rocket.chat/meteor +## 7.6.0-rc.1 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/license@1.0.11-rc.1 + - @rocket.chat/omnichannel-services@0.3.17-rc.1 + - @rocket.chat/pdf-worker@0.3.0-rc.1 + - @rocket.chat/presence@0.2.20-rc.1 + - @rocket.chat/api-client@0.2.20-rc.1 + - @rocket.chat/apps@0.5.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/cron@0.1.20-rc.1 + - @rocket.chat/freeswitch@1.2.7-rc.1 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.1 + - @rocket.chat/gazzodown@18.0.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.1 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.1 + - @rocket.chat/ui-client@18.0.0-rc.1 + - @rocket.chat/ui-video-conf@18.0.0-rc.1 + - @rocket.chat/ui-voip@8.0.0-rc.1 + - @rocket.chat/web-ui-registration@18.0.0-rc.1 + - @rocket.chat/instance-status@0.1.20-rc.1 +
+ ## 7.6.0-rc.0 ### Minor Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 2d1db6a381089..42d13a29b3869 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.6.0-rc.0" + "version": "7.6.0-rc.1" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 4145f2b94e79a..b4b5a9af9a5fe 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,19 @@ # rocketchat-services +## 2.0.11-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
+ ## 2.0.11-rc.0 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 9b40d9c59a81a..4a9687bc6857b 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "2.0.11-rc.0", + "version": "2.0.11-rc.1", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index a6d2a95f5c5ba..eca87533de12d 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.6.0-rc.0", + "version": "7.6.0-rc.1", "private": true, "type": "commonjs", "author": { diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index afd5741629278..82738508f69e8 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/uikit-playground +## 0.6.11-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.1 + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-avatar@14.0.0-rc.1 +
+ ## 0.6.11-rc.0 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 9c594b9c5ddaa..267036211523a 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.6.11-rc.0", + "version": "0.6.11-rc.1", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 412f8c28beb54..3f6888f67e75f 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/account-service +## 0.4.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
+ ## 0.4.20-rc.0 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index dc0afc9f7cdef..9b168880425c2 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.20-rc.0", + "version": "0.4.20-rc.1", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 79aa40315c379..b06b28dcc443f 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/authorization-service +## 0.4.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
+ ## 0.4.20-rc.0 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index a3e7405229bc7..33cd3ee8320fa 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.20-rc.0", + "version": "0.4.20-rc.1", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index a7df6e1fb45c7..dd0161ef3250d 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/ddp-streamer +## 0.3.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 + - @rocket.chat/instance-status@0.1.20-rc.1 +
+ ## 0.3.20-rc.0 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 42e023fd474c1..897a3a0ccc9c3 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.20-rc.0", + "version": "0.3.20-rc.1", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 168148696b2aa..a7153a86fc900 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/omnichannel-transcript +## 0.4.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/omnichannel-services@0.3.17-rc.1 + - @rocket.chat/pdf-worker@0.3.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
+ ## 0.4.20-rc.0 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 6b3d4b427c977..f83a57ceed7a5 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.20-rc.0", + "version": "0.4.20-rc.1", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index caaa63bf1e718..cd185076db450 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/presence-service +## 0.4.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/presence@0.2.20-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
+ ## 0.4.20-rc.0 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index baccf9eb51fe0..2968bbdb44159 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.20-rc.0", + "version": "0.4.20-rc.1", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index 16655fb1ee542..7a7d627f94eb2 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/queue-worker +## 0.4.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/omnichannel-services@0.3.17-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
+ ## 0.4.20-rc.0 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 1f02b9d5e9740..58dbbf83f1596 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.20-rc.0", + "version": "0.4.20-rc.1", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index f0d6c20dcfef4..e40004a2be9a2 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/stream-hub-service +## 0.4.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 + - @rocket.chat/network-broker@0.2.0-rc.1 +
+ ## 0.4.20-rc.0 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index dca644636492d..f8f2a7ca24971 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.20-rc.0", + "version": "0.4.20-rc.1", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 932abea430458..57902e99ebb07 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 1.0.11-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 +
+ ## 1.0.11-rc.0 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 54405c1d963e4..01dea22d083e8 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "1.0.11-rc.0", + "version": "1.0.11-rc.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/network-broker/CHANGELOG.md b/ee/packages/network-broker/CHANGELOG.md index 3f9ecb8242b9f..09495d4f60238 100644 --- a/ee/packages/network-broker/CHANGELOG.md +++ b/ee/packages/network-broker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/network-broker +## 0.2.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.1 +
+ ## 0.2.0-rc.0 ### Minor Changes diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index be3a420081282..304dbf33ceb28 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/network-broker", - "version": "0.2.0-rc.0", + "version": "0.2.0-rc.1", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 8e0ae583535a8..a665af4e9bedb 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.17-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/pdf-worker@0.3.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 +
+ ## 0.3.17-rc.0 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 08b3b0e236621..e3d08c8855db0 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.17-rc.0", + "version": "0.3.17-rc.1", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 8b7ce7801a9f2..e915f38aefdd1 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.3.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 +
+ ## 0.3.0-rc.0 ### Minor Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index 043296cd07244..6b0084e32a3e7 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.3.0-rc.0", + "version": "0.3.0-rc.1", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 82c53f1337c33..2f1596686bc32 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/core-services@0.9.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 +
+ ## 0.2.20-rc.0 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index d014a5de5eb4a..d8e7a3c1bdb03 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.20-rc.0", + "version": "0.2.20-rc.1", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/package.json b/package.json index ee78a8fe3c9f2..5cbfc7d97164c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.6.0-rc.0", + "version": "7.6.0-rc.1", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 6945c7a3bf0d8..8000bfb810f7b 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 +
+ ## 0.2.20-rc.0 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 0fd816cbde408..40296aedc0ef8 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.20-rc.0", + "version": "0.2.20-rc.1", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index 0368b6d28e74f..6d5c1d2727563 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.5.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 +
+ ## 0.5.0-rc.0 ### Minor Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index f679c69098794..ceba19657377e 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.5.0-rc.0", + "version": "0.5.0-rc.1", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index 5581a2eb2af1e..d04c4065fffcc 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/core-services +## 0.9.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 +
+ ## 0.9.0-rc.0 ### Minor Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 0f864faccbc2c..53717d2f01482 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.9.0-rc.0", + "version": "0.9.0-rc.1", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index b12ebac7d99b8..2538a2b224fa5 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,7 @@ # @rocket.chat/core-typings +## 7.6.0-rc.1 + ## 7.6.0-rc.0 ### Minor Changes diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 2722de6ee1191..989398727d367 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.6.0-rc.0", + "version": "7.6.0-rc.1", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 277bac8c0b3fc..3984009e387be 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/models@1.5.0-rc.1 +
+ ## 0.1.20-rc.0 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index e1090b055ef26..3d2e534ab1cc6 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.20-rc.0", + "version": "0.1.20-rc.1", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index 89db5634faed7..f7622469d6dfb 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/api-client@0.2.20-rc.1 +
+ ## 0.3.20-rc.0 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 46a990580e3bc..3dff543c37be9 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.20-rc.0", + "version": "0.3.20-rc.1", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/freeswitch/CHANGELOG.md b/packages/freeswitch/CHANGELOG.md index 500076690ddd4..86a481e755ebd 100644 --- a/packages/freeswitch/CHANGELOG.md +++ b/packages/freeswitch/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/freeswitch +## 1.2.7-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 +
+ ## 1.2.7-rc.0 ### Patch Changes diff --git a/packages/freeswitch/package.json b/packages/freeswitch/package.json index 6450f57a80738..e54989d8b4a09 100644 --- a/packages/freeswitch/package.json +++ b/packages/freeswitch/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/freeswitch", - "version": "1.2.7-rc.0", + "version": "1.2.7-rc.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index fea3de387902c..95edac646f5e6 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 18.0.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/gazzodown@18.0.0-rc.1 + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-avatar@14.0.0-rc.1 + - @rocket.chat/ui-video-conf@18.0.0-rc.1 +
+ ## 18.0.0-rc.0 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 75f34f065a419..69b9e94287b7e 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/fuselage-ui-kit", - "version": "18.0.0-rc.0", + "version": "18.0.0-rc.1", "private": true, "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index 82238143a6f47..3e957b197c0e6 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/gazzodown +## 18.0.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-client@18.0.0-rc.1 +
+ ## 18.0.0-rc.0 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 75e86731aba80..e7fbe6401cb15 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "18.0.0-rc.0", + "version": "18.0.0-rc.1", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index 5284e6793abe5..d9bdc15d98d0e 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.20-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.1 +
+ ## 0.1.20-rc.0 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 5b17e60aa4a06..c51b0d8a06f76 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.20-rc.0", + "version": "0.1.20-rc.1", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index e9877e9d978ac..9355443e68b3b 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/livechat Change Log +## 1.22.7-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.1 +
+ ## 1.22.7-rc.0 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 28e247b4ca66a..e36301bf59f86 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.22.7-rc.0", + "version": "1.22.7-rc.1", "files": [ "/build" ], diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index 36be05c9fc06a..b1c5ef8994005 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.2.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 +
+ ## 0.2.0-rc.0 ### Minor Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 6a7042dc9b2a9..08cacdecf79ea 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.2.0-rc.0", + "version": "0.2.0-rc.1", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index de4bb1f6451d3..826262a4377f6 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/model-typings +## 1.6.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 +
+ ## 1.6.0-rc.0 ### Minor Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 165c1bf2e1bbe..fd53b5088dcc4 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "1.6.0-rc.0", + "version": "1.6.0-rc.1", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 9077b8e77922d..6fdf03dee5da1 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/models +## 1.5.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/model-typings@1.6.0-rc.1 +
+ ## 1.5.0-rc.0 ### Minor Changes diff --git a/packages/models/package.json b/packages/models/package.json index dfb35d962dd9c..7f06e9368558a 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "1.5.0-rc.0", + "version": "1.5.0-rc.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 18610e388917b..23e8c5db5f240 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/rest-typings +## 7.6.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 +
+ ## 7.6.0-rc.0 ### Minor Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 2ee9d4ac0b0c9..d5ced2198cc2b 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.6.0-rc.0", + "version": "7.6.0-rc.1", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index 2f95497774081..2a5eaf7f7fec7 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 14.0.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 +
+ ## 14.0.0-rc.0 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index ddfbf7d242e82..e670f72c24378 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "14.0.0-rc.0", + "version": "14.0.0-rc.1", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 73db61ec730d4..4b57da5c66e35 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-client +## 18.0.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-avatar@14.0.0-rc.1 +
+ ## 18.0.0-rc.0 ### Minor Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 0fd2b19b4a1e1..a60bea19246da 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "18.0.0-rc.0", + "version": "18.0.0-rc.1", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index badc73df27087..8ff501d19cc34 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-contexts +## 18.0.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.1 + - @rocket.chat/rest-typings@7.6.0-rc.1 + - @rocket.chat/ddp-client@0.3.20-rc.1 +
+ ## 18.0.0-rc.0 ### Minor Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index d1155e4f3e6f7..85dd4974b681e 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "18.0.0-rc.0", + "version": "18.0.0-rc.1", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index 2ffb47586ff63..093311888158b 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 18.0.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-avatar@14.0.0-rc.1 +
+ ## 18.0.0-rc.0 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index fa6a0781fca55..ed2f1693b5836 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "18.0.0-rc.0", + "version": "18.0.0-rc.1", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-voip/CHANGELOG.md b/packages/ui-voip/CHANGELOG.md index 04de5752c5a48..8cef46aea8cd0 100644 --- a/packages/ui-voip/CHANGELOG.md +++ b/packages/ui-voip/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-voip +## 8.0.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 + - @rocket.chat/ui-avatar@14.0.0-rc.1 + - @rocket.chat/ui-client@18.0.0-rc.1 +
+ ## 8.0.0-rc.0 ### Minor Changes diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 3beec493c9022..6c991b1b10b47 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-voip", - "version": "8.0.0-rc.0", + "version": "8.0.0-rc.1", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index ea9f1f1db9e7f..a0aa1bd29cbd1 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 18.0.0-rc.1 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.1 +
+ ## 18.0.0-rc.0 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index ed13bd45d807c..2461a56642e35 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "18.0.0-rc.0", + "version": "18.0.0-rc.1", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", diff --git a/yarn.lock b/yarn.lock index c5571301a3671..3c66c7da3b9c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8224,7 +8224,7 @@ __metadata: storybook-dark-mode: "npm:^4.0.2" typescript: "npm:~5.7.2" peerDependencies: - "@rocket.chat/apps-engine": 1.50.0 + "@rocket.chat/apps-engine": 1.51.0-rc.0 "@rocket.chat/eslint-config": 0.7.0 "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" From 9b8fa2c30fb44e975815f63d439d36be9aa767cc Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 24 Apr 2025 15:45:32 -0300 Subject: [PATCH 154/187] regression: Increase debounce time and add missing margin in `NavBarSearch` (#35866) --- .../client/NavBarV2/NavBarSearch/NavBarSearchListbox.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchListbox.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchListbox.tsx index 4d5a326b37268..f99f1e2ab54e8 100644 --- a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchListbox.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearchListbox.tsx @@ -27,7 +27,7 @@ const NavBarSearchListBox = ({ state, overlayProps }: NavBarSearchListBoxProps) const { resetField, watch } = useFormContext(); const { filterText } = watch(); - const debouncedFilter = useDebouncedValue(filterText, 200); + const debouncedFilter = useDebouncedValue(filterText, 500); const handleSelect = useEffectEvent(() => { state.close(); @@ -43,6 +43,7 @@ const NavBarSearchListBox = ({ state, overlayProps }: NavBarSearchListBoxProps) zIndex={99} padding={0} pb={16} + mbs={4} minHeight='x52' maxHeight='50vh' display='flex' From 4d1fcde5645bb09f8e80ce78e2a58822e1173479 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Thu, 24 Apr 2025 16:08:23 -0300 Subject: [PATCH 155/187] regression: fix security logs timestamp discrepancy (#35865) (#35867) --- apps/meteor/client/views/audit/components/SecurityLogsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/audit/components/SecurityLogsTable.tsx b/apps/meteor/client/views/audit/components/SecurityLogsTable.tsx index f22fa54a7e709..2286bd81cf6f5 100644 --- a/apps/meteor/client/views/audit/components/SecurityLogsTable.tsx +++ b/apps/meteor/client/views/audit/components/SecurityLogsTable.tsx @@ -172,7 +172,7 @@ const SecurityLogsTable = (): ReactElement => { onClick={() => handleItemClick({ actor: item.actor, - timestamp: new Date(item.ts).toDateString(), + timestamp: item.ts, setting, changedFrom: String(previous), changedTo: String(current), From 527801ba48185ab70939905b351a29e2a4b06c48 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Fri, 25 Apr 2025 09:35:09 -0300 Subject: [PATCH 156/187] regression: bring back lazy ee endpoints (#35838) --- apps/meteor/ee/server/api/index.ts | 2 ++ apps/meteor/ee/server/startup/engagementDashboard.ts | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/ee/server/api/index.ts b/apps/meteor/ee/server/api/index.ts index 264cf37e329eb..93a4c99d8f1b8 100644 --- a/apps/meteor/ee/server/api/index.ts +++ b/apps/meteor/ee/server/api/index.ts @@ -5,3 +5,5 @@ import './sessions'; import './chat'; import './roles'; import '../apps/communication/uikit'; +import './engagementDashboard'; +import './audit'; diff --git a/apps/meteor/ee/server/startup/engagementDashboard.ts b/apps/meteor/ee/server/startup/engagementDashboard.ts index f7a18f1f347bf..359307365029e 100644 --- a/apps/meteor/ee/server/startup/engagementDashboard.ts +++ b/apps/meteor/ee/server/startup/engagementDashboard.ts @@ -5,7 +5,6 @@ License.onToggledFeature('engagement-dashboard', { const { prepareAnalytics, attachCallbacks } = await import('../lib/engagementDashboard/startup'); await prepareAnalytics(); attachCallbacks(); - await import('../api/engagementDashboard'); }, down: async () => { const { detachCallbacks } = await import('../lib/engagementDashboard/startup'); From 01cd736698e08537fc14bfee076058ce915c10d6 Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Fri, 25 Apr 2025 11:18:49 -0300 Subject: [PATCH 157/187] regression: wrong permission to display Security_logs item (#35869) --- .../client/sidebar/header/actions/hooks/useAuditItems.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx index e284c147ba4c4..5734ffc9cac8a 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx @@ -39,7 +39,7 @@ export const useAuditItems = (): GenericMenuItemProps[] => { onClick: () => securityLogsRoute.push(), }; - return [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem, hasAuditLogPermission && securityLogItem].filter( + return [hasAuditPermission && auditMessageItem, hasAuditLogPermission && auditLogItem, hasAuditPermission && securityLogItem].filter( Boolean, ) as GenericMenuItemProps[]; }; From a576a5c627896551f8237c1bac12d44c5d5c96f8 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Fri, 25 Apr 2025 18:30:51 -0300 Subject: [PATCH 158/187] regression: ldap sync creating federated users with email addresses (#35850) --- .../app/importer/server/classes/converters/UserConverter.ts | 4 +++- apps/meteor/server/lib/ldap/Manager.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts index 0ff4f72a6f1c8..5074443c8995a 100644 --- a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts @@ -286,7 +286,9 @@ export class UserConverter extends RecordConverter email.trim()); + const emails = homeServer ? [] : this.getLdapEmails(ldapUser, username).map((email) => email.trim()); const name = this.getLdapName(ldapUser) || undefined; const voipExtension = this.getLdapExtension(ldapUser); From 2caf06ad9d6ef63cbba0becea40264fba606a954 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Sat, 26 Apr 2025 01:42:00 +0000 Subject: [PATCH 159/187] Release 7.6.0-rc.2 [no ci] --- .changeset/bump-patch-1745631711629.md | 5 +++ .changeset/pre.json | 1 + apps/meteor/CHANGELOG.md | 35 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 15 +++++++- apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 13 ++++++- apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 14 ++++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 14 ++++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 15 ++++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 15 ++++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 15 +++++++- ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 14 ++++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 13 +++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 +++++ ee/packages/license/package.json | 2 +- ee/packages/network-broker/CHANGELOG.md | 9 +++++ ee/packages/network-broker/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 ++++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 9 +++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 ++++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 ++++++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 10 ++++++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 11 ++++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 2 ++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 ++++++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 ++++++ packages/ddp-client/package.json | 2 +- packages/freeswitch/CHANGELOG.md | 9 +++++ packages/freeswitch/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 13 +++++++ packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/CHANGELOG.md | 11 ++++++ packages/gazzodown/package.json | 2 +- packages/instance-status/CHANGELOG.md | 9 +++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 9 +++++ packages/livechat/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 +++++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 9 +++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 10 ++++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 9 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 +++++ packages/ui-avatar/package.json | 2 +- packages/ui-client/CHANGELOG.md | 10 ++++++ packages/ui-client/package.json | 2 +- packages/ui-contexts/CHANGELOG.md | 11 ++++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 10 ++++++ packages/ui-video-conf/package.json | 2 +- packages/ui-voip/CHANGELOG.md | 11 ++++++ packages/ui-voip/package.json | 2 +- packages/web-ui-registration/CHANGELOG.md | 9 +++++ packages/web-ui-registration/package.json | 2 +- 76 files changed, 458 insertions(+), 41 deletions(-) create mode 100644 .changeset/bump-patch-1745631711629.md diff --git a/.changeset/bump-patch-1745631711629.md b/.changeset/bump-patch-1745631711629.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1745631711629.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/pre.json b/.changeset/pre.json index c945c41fbb582..a6b3179fe07c6 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -75,6 +75,7 @@ "big-tips-greet", "bright-forks-drop", "bump-patch-1745354933374", + "bump-patch-1745631711629", "dirty-seas-explode", "eighty-wombats-smile", "eleven-laws-crash", diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 6680ab16132f5..28529ed343b14 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,40 @@ # @rocket.chat/meteor +## 7.6.0-rc.2 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/license@1.0.12-rc.2 + - @rocket.chat/omnichannel-services@0.3.18-rc.2 + - @rocket.chat/pdf-worker@0.3.0-rc.2 + - @rocket.chat/presence@0.2.21-rc.2 + - @rocket.chat/api-client@0.2.21-rc.2 + - @rocket.chat/apps@0.5.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/cron@0.1.21-rc.2 + - @rocket.chat/freeswitch@1.2.8-rc.2 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.2 + - @rocket.chat/gazzodown@18.0.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.2 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.2 + - @rocket.chat/ui-client@18.0.0-rc.2 + - @rocket.chat/ui-video-conf@18.0.0-rc.2 + - @rocket.chat/ui-voip@8.0.0-rc.2 + - @rocket.chat/web-ui-registration@18.0.0-rc.2 + - @rocket.chat/instance-status@0.1.21-rc.2 +
+ ## 7.6.0-rc.1 ### Patch Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 42d13a29b3869..033ae320af393 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.6.0-rc.1" + "version": "7.6.0-rc.2" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 79d70a345e4a3..158c05bc2a348 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,19 @@ # rocketchat-services +## 2.0.12-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
+ ## 2.0.12-rc.1 ### Patch Changes @@ -42,7 +56,6 @@ - @rocket.chat/network-broker@0.1.12 - ## 2.0.10 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index c207ce3940446..cea1bcdf2b220 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "2.0.12-rc.1", + "version": "2.0.12-rc.2", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index eca87533de12d..236aeffe895b8 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.6.0-rc.1", + "version": "7.6.0-rc.2", "private": true, "type": "commonjs", "author": { diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index b5d784e8c3f67..f0f14105a402c 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/uikit-playground +## 0.6.12-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.2 + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-avatar@14.0.0-rc.2 +
+ ## 0.6.12-rc.1 ### Patch Changes @@ -24,7 +36,6 @@ - @rocket.chat/ui-avatar@14.0.0-rc.0 - ## 0.6.11 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index d69955d931af0..43971ffa5d64d 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.6.12-rc.1", + "version": "0.6.12-rc.2", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 9c72cbdc2c2e6..655316c0c195a 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/account-service +## 0.4.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
+ ## 0.4.21-rc.1 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index c6091e5bc0274..5d390957804bd 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.21-rc.1", + "version": "0.4.21-rc.2", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index c271cc50dd81d..7f6cd148ab3a6 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/authorization-service +## 0.4.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
+ ## 0.4.21-rc.1 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 48f4c8f659d8c..5d89f4b256e8c 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.21-rc.1", + "version": "0.4.21-rc.2", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 900a167783e08..692daac916163 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/ddp-streamer +## 0.3.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 + - @rocket.chat/instance-status@0.1.21-rc.2 +
+ ## 0.3.21-rc.1 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 3a7857074d956..3d23da747e85d 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.21-rc.1", + "version": "0.3.21-rc.2", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 8e2c83eb3be9f..40dca780d9322 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/omnichannel-transcript +## 0.4.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/omnichannel-services@0.3.18-rc.2 + - @rocket.chat/pdf-worker@0.3.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
+ ## 0.4.21-rc.1 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 223de91d129da..94ef481760bb8 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.21-rc.1", + "version": "0.4.21-rc.2", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 5b2ea06d300da..ad1ac4c38eca8 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/presence-service +## 0.4.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/presence@0.2.21-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
+ ## 0.4.21-rc.1 ### Patch Changes @@ -28,7 +42,6 @@ - @rocket.chat/presence@0.2.20-rc.0 - ## 0.4.20 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index b7366fe9e04c3..da9d6a38b0844 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.21-rc.1", + "version": "0.4.21-rc.2", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index c866b1966eb97..e6669f5548fba 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/queue-worker +## 0.4.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/omnichannel-services@0.3.18-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
+ ## 0.4.21-rc.1 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 62f2cc7874c78..d263de9d37fdd 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.21-rc.1", + "version": "0.4.21-rc.2", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index 5dc437fd40eae..5f4a39ec01bcc 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/stream-hub-service +## 0.4.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 + - @rocket.chat/network-broker@0.2.0-rc.2 +
+ ## 0.4.21-rc.1 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 964d20b694b95..311561baab79d 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.21-rc.1", + "version": "0.4.21-rc.2", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index afded7bca75b5..86038701ed546 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 1.0.12-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 +
+ ## 1.0.12-rc.1 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index aaf5cbe180569..6af52de929518 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "1.0.12-rc.1", + "version": "1.0.12-rc.2", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/network-broker/CHANGELOG.md b/ee/packages/network-broker/CHANGELOG.md index 98cb3d8ccde5c..3d0639ef15d41 100644 --- a/ee/packages/network-broker/CHANGELOG.md +++ b/ee/packages/network-broker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/network-broker +## 0.2.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.2 +
+ ## 0.2.0-rc.1 ### Patch Changes diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index 304dbf33ceb28..50dfd34316842 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/network-broker", - "version": "0.2.0-rc.1", + "version": "0.2.0-rc.2", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 934ae3f967431..27cbeda56ac26 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.18-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/pdf-worker@0.3.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 +
+ ## 0.3.18-rc.1 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index cdaf93df1af91..191981120bd34 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.18-rc.1", + "version": "0.3.18-rc.2", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 0655078a786c2..1043cd53022b1 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.3.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 +
+ ## 0.3.0-rc.1 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index 6b0084e32a3e7..2244b0f0d6e73 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.3.0-rc.1", + "version": "0.3.0-rc.2", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index d3f7652e946eb..01eca6709894a 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/core-services@0.9.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 +
+ ## 0.2.21-rc.1 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 790fbff3641fc..1e900122bfb69 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.21-rc.1", + "version": "0.2.21-rc.2", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/package.json b/package.json index 5cbfc7d97164c..62322a7d61f25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.6.0-rc.1", + "version": "7.6.0-rc.2", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 824d0720c75ef..068b8634b12c6 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 +
+ ## 0.2.21-rc.1 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index a4287ec944cb4..5c757d5f10cd2 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.21-rc.1", + "version": "0.2.21-rc.2", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index de8b43d5182e9..b8acd52b9f750 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.5.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 +
+ ## 0.5.0-rc.1 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index ceba19657377e..102ae2f68083c 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.5.0-rc.1", + "version": "0.5.0-rc.2", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index 8b760fcb5472f..fb319a1fbef93 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/core-services +## 0.9.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 +
+ ## 0.9.0-rc.1 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 53717d2f01482..d8ca9112e8d03 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.9.0-rc.1", + "version": "0.9.0-rc.2", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index b4e8104d18876..2eb112a709fb2 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,7 @@ # @rocket.chat/core-typings +## 7.6.0-rc.2 + ## 7.6.0-rc.1 ## 7.6.0-rc.0 diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 989398727d367..701a7507f9633 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.6.0-rc.1", + "version": "7.6.0-rc.2", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 78fc506115742..14e1ce680526a 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/models@1.5.0-rc.2 +
+ ## 0.1.21-rc.1 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index 903d6f5f28a47..b99adef14d846 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.21-rc.1", + "version": "0.1.21-rc.2", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index e290de541dda5..ee0d2d7bb606a 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/api-client@0.2.21-rc.2 +
+ ## 0.3.21-rc.1 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 5bcb067fd5e89..cc18b23c05c09 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.21-rc.1", + "version": "0.3.21-rc.2", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/freeswitch/CHANGELOG.md b/packages/freeswitch/CHANGELOG.md index 94919e77307bc..5a32d81d9abd4 100644 --- a/packages/freeswitch/CHANGELOG.md +++ b/packages/freeswitch/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/freeswitch +## 1.2.8-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 +
+ ## 1.2.8-rc.1 ### Patch Changes diff --git a/packages/freeswitch/package.json b/packages/freeswitch/package.json index dd73cac6550fa..f2e5812975a2f 100644 --- a/packages/freeswitch/package.json +++ b/packages/freeswitch/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/freeswitch", - "version": "1.2.8-rc.1", + "version": "1.2.8-rc.2", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 05e450c160739..d9b7eb66a1e9a 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 18.0.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/gazzodown@18.0.0-rc.2 + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-avatar@14.0.0-rc.2 + - @rocket.chat/ui-video-conf@18.0.0-rc.2 +
+ ## 18.0.0-rc.1 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 69b9e94287b7e..9f0b64625c76b 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/fuselage-ui-kit", - "version": "18.0.0-rc.1", + "version": "18.0.0-rc.2", "private": true, "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index e6fc07515ac27..19229def82ba8 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/gazzodown +## 18.0.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-client@18.0.0-rc.2 +
+ ## 18.0.0-rc.1 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index e7fbe6401cb15..ba4f5e96d27c6 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "18.0.0-rc.1", + "version": "18.0.0-rc.2", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index ce5d0122b1153..15b7e1f9b0642 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.21-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.2 +
+ ## 0.1.21-rc.1 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index fc5f6798077e4..74a2607ac34c5 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.21-rc.1", + "version": "0.1.21-rc.2", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 1e699d2eb6944..327d3411e9494 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/livechat Change Log +## 1.22.8-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.2 +
+ ## 1.22.8-rc.1 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index b4a23ed0bc39f..e66380aeb1d22 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.22.8-rc.1", + "version": "1.22.8-rc.2", "files": [ "/build" ], diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index b1c5ef8994005..8ce85aabcc5b2 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.2.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 +
+ ## 0.2.0-rc.1 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 08cacdecf79ea..96f0d043604ab 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.2.0-rc.1", + "version": "0.2.0-rc.2", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index ce2203fed5055..0e2411fa6bea3 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/model-typings +## 1.6.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 +
+ ## 1.6.0-rc.1 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index fd53b5088dcc4..6742dddeff1d9 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "1.6.0-rc.1", + "version": "1.6.0-rc.2", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 7af286092b7cc..b99f73cb61ec1 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/models +## 1.5.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/model-typings@1.6.0-rc.2 +
+ ## 1.5.0-rc.1 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index 7f06e9368558a..4223155b7080b 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "1.5.0-rc.1", + "version": "1.5.0-rc.2", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index e10d3f746e541..6ee2961a5a5da 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/rest-typings +## 7.6.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 +
+ ## 7.6.0-rc.1 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index d5ced2198cc2b..fd9e090619bcf 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.6.0-rc.1", + "version": "7.6.0-rc.2", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index d986261ff7b71..abc0906577def 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 14.0.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 +
+ ## 14.0.0-rc.1 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index e670f72c24378..dd995c12b1251 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "14.0.0-rc.1", + "version": "14.0.0-rc.2", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 757359e44e41f..3ea7ec75e8063 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-client +## 18.0.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-avatar@14.0.0-rc.2 +
+ ## 18.0.0-rc.1 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index a60bea19246da..e285029bbe7e0 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "18.0.0-rc.1", + "version": "18.0.0-rc.2", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index e08c9226e7061..3edc745cdd828 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-contexts +## 18.0.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.2 + - @rocket.chat/rest-typings@7.6.0-rc.2 + - @rocket.chat/ddp-client@0.3.21-rc.2 +
+ ## 18.0.0-rc.1 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 85dd4974b681e..ad0a092fb0df0 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "18.0.0-rc.1", + "version": "18.0.0-rc.2", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index ea3416d7d5d73..4f3376e0097fd 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 18.0.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-avatar@14.0.0-rc.2 +
+ ## 18.0.0-rc.1 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index ed2f1693b5836..9264b5e0a9316 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "18.0.0-rc.1", + "version": "18.0.0-rc.2", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-voip/CHANGELOG.md b/packages/ui-voip/CHANGELOG.md index f185447115ab3..d657134fb54d1 100644 --- a/packages/ui-voip/CHANGELOG.md +++ b/packages/ui-voip/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-voip +## 8.0.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 + - @rocket.chat/ui-avatar@14.0.0-rc.2 + - @rocket.chat/ui-client@18.0.0-rc.2 +
+ ## 8.0.0-rc.1 ### Patch Changes diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 6c991b1b10b47..b27ab094a792f 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-voip", - "version": "8.0.0-rc.1", + "version": "8.0.0-rc.2", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index beed440fd995f..2628163e51a76 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 18.0.0-rc.2 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.2 +
+ ## 18.0.0-rc.1 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 2461a56642e35..1c08596493fce 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "18.0.0-rc.1", + "version": "18.0.0-rc.2", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", From d4eef32b9a3cd68744dfa9da6098a1c947e0f70d Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 28 Apr 2025 11:25:31 -0600 Subject: [PATCH 160/187] regression: Room update via apps engine failing with `Duplicated index key` error (#35878) --- .../app/apps/server/converters/rooms.js | 10 ++-- .../apps/server/mocks/models/Rooms.mock.js | 13 +++++ .../tests/unit/app/apps/server/rooms.tests.ts | 57 ++++++++++++++++++- 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 5fc1c5ab57bf8..045c341186810 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -137,12 +137,12 @@ export class AppRoomsConverter { const newRoom = { ...(room.id && { _id: room.id }), - t: room.type, - ts: room.createdAt, - msgs: room.messageCount || 0, - _updatedAt: room.updatedAt, + ...(typeof room.type !== 'undefined' && { t: room.type }), + ...(typeof room.createdAt !== 'undefined' && { ts: room.createdAt }), + ...(typeof room.messageCount !== 'undefined' && { msgs: room.messageCount || 0 }), + ...(typeof room.updatedAt !== 'undefined' && { _updatedAt: room.updatedAt }), ...(room.displayName && { fname: room.displayName }), - ...(room.type !== 'd' && { name: room.slugifiedName }), + ...(room.type !== 'd' && room.slugifiedName && { name: room.slugifiedName }), ...(room.members && { members: room.members }), ...(typeof room.isDefault !== 'undefined' && { default: room.isDefault }), ...(typeof room.isReadOnly !== 'undefined' && { ro: room.isReadOnly }), diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js b/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js index 6cf0370f1754e..de59997324c67 100644 --- a/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js +++ b/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js @@ -115,6 +115,19 @@ export class RoomsMock extends BaseModelMock { updatedAt: new Date('2019-04-10T17:44:34.931Z'), }, + GENERALPartialWithOptionalProps: { + id: 'GENERAL', + slugifiedName: 'general', + displaySystemMessages: true, + updatedAt: new Date('2019-04-10T17:44:34.931Z'), + messageCount: 40, + type: 'c', + }, + + UpdatedRoom: { + customFields: { custom: 'field' }, + }, + LivechatRoom: { id: 'LivechatRoom', slugifiedName: undefined, diff --git a/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts b/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts index 0e7508556cf74..fa2dabcb20313 100644 --- a/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts +++ b/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts @@ -112,11 +112,66 @@ describe('The AppMessagesConverter instance', () => { expect(rocketchatRoom).to.have.property('_id', appRoom.id); expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName); expect(rocketchatRoom).to.have.property('sysMes', appRoom.displaySystemMessages); - expect(rocketchatRoom).to.have.property('msgs', 0); expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt); + expect(rocketchatRoom).to.not.have.property('msgs'); expect(rocketchatRoom).to.not.have.property('ro'); expect(rocketchatRoom).to.not.have.property('default'); + expect(rocketchatRoom).to.not.have.property('t'); + }); + + it('should return a proper schema when receiving a partial object', async () => { + const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom; + const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); + + expect(rocketchatRoom).to.have.property('_id', appRoom.id); + expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName); + expect(rocketchatRoom).to.have.property('sysMes', appRoom.displaySystemMessages); + expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt); + expect(rocketchatRoom).to.have.property('msgs', appRoom.messageCount); + expect(rocketchatRoom).to.have.property('t', 'c'); + + expect(rocketchatRoom).to.not.have.property('ro'); + expect(rocketchatRoom).to.not.have.property('default'); + }); + + it('should not include properties that are not present in the app room', async () => { + const appRoom = RoomsMock.convertedData.UpdatedRoom as unknown as IAppsRoom; + const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); + + expect(rocketchatRoom).to.have.property('customFields'); + expect(rocketchatRoom).to.not.have.property('_id'); + expect(rocketchatRoom).to.not.have.property('t'); + }); + + it('should not include name as undefined if the room doesnt have a name property', async () => { + const appRoom = RoomsMock.convertedData.UpdatedRoom as unknown as IAppsRoom; + const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); + + expect(rocketchatRoom.name).to.be.undefined; + }); + + it('should include a name if the source room has slugifiedName property', async () => { + const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom; + const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); + + expect(rocketchatRoom.name).to.equal(appRoom.slugifiedName); + }); + + it('should not use _unmappedProperties when the room is a partial object', async () => { + const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom; + // @ts-expect-error - _unmappedProperties + const rocketchatRoom = await roomConverter.convertAppRoom({ ...appRoom, _unmappedProperties_: { unmapped: 'property' } }, true); + + expect(rocketchatRoom).to.not.have.property('unmapped'); + }); + + it('should use _unmappedProperties when the room is a partial object', async () => { + const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom; + // @ts-expect-error - _unmappedProperties + const rocketchatRoom = await roomConverter.convertAppRoom({ ...appRoom, _unmappedProperties_: { unmapped: 'property' } }, false); + + expect(rocketchatRoom).to.have.property('unmapped', 'property'); }); }); }); From 6f70529494dea850404aaca5802e6e183c785b94 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 28 Apr 2025 15:15:23 -0300 Subject: [PATCH 161/187] regression: Sidebar should collapse in tablet view under feature preview only (#35872) --- .../client/components/Page/PageHeaderNoShadow.tsx | 4 ++-- apps/meteor/client/providers/LayoutProvider.tsx | 4 +++- apps/meteor/client/providers/MeteorProvider.tsx | 12 ++++++------ apps/meteor/client/views/room/NotSubscribedRoom.tsx | 4 ++-- apps/meteor/client/views/room/RoomNotFound.tsx | 4 ++-- .../tests/e2e/page-objects/fragments/home-sidenav.ts | 12 ++++++++++++ apps/meteor/tests/e2e/sidebar.spec.ts | 8 ++++++++ 7 files changed, 35 insertions(+), 13 deletions(-) diff --git a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx index 2d9aefb14ccef..58188d1346508 100644 --- a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx +++ b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx @@ -5,7 +5,7 @@ import type { ComponentPropsWithoutRef, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { HeaderToolbar } from '../Header'; -import { SidebarTogglerV2 } from '../SidebarTogglerV2'; +import SidebarToggler from '../SidebarToggler'; type PageHeaderProps = { title: ReactNode; @@ -36,7 +36,7 @@ const PageHeaderNoShadow = ({ children = undefined, title, onClickBack, ...props - + {null} diff --git a/apps/meteor/client/providers/LayoutProvider.tsx b/apps/meteor/client/providers/LayoutProvider.tsx index 391c9228dcb25..92b5b5d7871ff 100644 --- a/apps/meteor/client/providers/LayoutProvider.tsx +++ b/apps/meteor/client/providers/LayoutProvider.tsx @@ -1,4 +1,5 @@ import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; +import { useFeaturePreview } from '@rocket.chat/ui-client'; import { LayoutContext, useRouter, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import { useMemo, useState, useEffect } from 'react'; @@ -20,6 +21,7 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => { const [navBarSearchExpanded, setNavBarSearchExpanded] = useState(false); const breakpoints = useBreakpoints(); // ["xs", "sm", "md", "lg", "xl", xxl"] const [hiddenActions, setHiddenActions] = useState(hiddenActionsDefaultValue); + const enhancedNavigationEnabled = useFeaturePreview('newNavigation'); const router = useRouter(); // Once the layout is embedded, it can't be changed @@ -28,7 +30,7 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => { const isMobile = !breakpoints.includes('md'); const isTablet = !breakpoints.includes('lg'); - const shouldToggle = isTablet || isMobile; + const shouldToggle = enhancedNavigationEnabled ? isTablet || isMobile : isMobile; useEffect(() => { setIsCollapsed(shouldToggle); diff --git a/apps/meteor/client/providers/MeteorProvider.tsx b/apps/meteor/client/providers/MeteorProvider.tsx index 704a4dce2eece..b3bed574fd046 100644 --- a/apps/meteor/client/providers/MeteorProvider.tsx +++ b/apps/meteor/client/providers/MeteorProvider.tsx @@ -38,9 +38,9 @@ const MeteorProvider = ({ children }: MeteorProviderProps) => ( - - - + + + @@ -66,9 +66,9 @@ const MeteorProvider = ({ children }: MeteorProviderProps) => ( - - - + + + diff --git a/apps/meteor/client/views/room/NotSubscribedRoom.tsx b/apps/meteor/client/views/room/NotSubscribedRoom.tsx index f055ead54d0e3..f2ece26605932 100644 --- a/apps/meteor/client/views/room/NotSubscribedRoom.tsx +++ b/apps/meteor/client/views/room/NotSubscribedRoom.tsx @@ -6,7 +6,7 @@ import type { ReactElement } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import RoomLayout from './layout/RoomLayout'; -import { SidebarTogglerV2 } from '../../components/SidebarTogglerV2'; +import SidebarToggler from '../../components/SidebarToggler'; import { useJoinRoom } from '../../hooks/useJoinRoom'; type NotSubscribedRoomProps = { @@ -30,7 +30,7 @@ const NotSubscribedRoom = ({ rid, reference, type }: NotSubscribedRoomProps): Re
- +
diff --git a/apps/meteor/client/views/room/RoomNotFound.tsx b/apps/meteor/client/views/room/RoomNotFound.tsx index 3d13b1a5e930f..ca4945ba96d58 100644 --- a/apps/meteor/client/views/room/RoomNotFound.tsx +++ b/apps/meteor/client/views/room/RoomNotFound.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import RoomLayout from './layout/RoomLayout'; import NotFoundState from '../../components/NotFoundState'; -import { SidebarTogglerV2 } from '../../components/SidebarTogglerV2'; +import SidebarToggler from '../../components/SidebarToggler'; const RoomNotFound = (): ReactElement => { const { t } = useTranslation(); @@ -20,7 +20,7 @@ const RoomNotFound = (): ReactElement => {
- +
diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index bf2eefeb11a37..872c6b46b7d92 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -61,6 +61,18 @@ export class HomeSidenav { return this.sidebarToolbar.getByRole('button', { name: 'Home' }); } + get btnDisplay(): Locator { + return this.sidebarToolbar.getByRole('button', { name: 'Display' }); + } + + get btnCreateNew(): Locator { + return this.sidebarToolbar.getByRole('button', { name: 'Create new' }); + } + + get btnAdministration(): Locator { + return this.sidebarToolbar.getByRole('button', { name: 'Administration' }); + } + async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise { await this.sidebarToolbar.getByRole('button', { name: 'Display', exact: true }).click(); await this.sidebarToolbar.getByRole('menuitemcheckbox', { name: mode }).click(); diff --git a/apps/meteor/tests/e2e/sidebar.spec.ts b/apps/meteor/tests/e2e/sidebar.spec.ts index 29fd1a1bc0f59..b45f2caa7f89b 100644 --- a/apps/meteor/tests/e2e/sidebar.spec.ts +++ b/apps/meteor/tests/e2e/sidebar.spec.ts @@ -14,6 +14,14 @@ test.describe.serial('sidebar', () => { await page.waitForSelector('main'); }); + test('should sidebar`s toolbar buttons not be disabled in tablet view', async ({ page }) => { + await page.setViewportSize({ width: 1023, height: 767 }); + + await expect(poHomeChannel.sidenav.btnDisplay).not.toBeDisabled(); + await expect(poHomeChannel.sidenav.btnCreateNew).not.toBeDisabled(); + await expect(poHomeChannel.sidenav.btnAdministration).not.toBeDisabled(); + }); + test('should navigate on sidebar toolbar using arrow keys', async ({ page }) => { await poHomeChannel.sidenav.userProfileMenu.focus(); await page.keyboard.press('Tab'); From 292fab4f94a647f290f5ae13bbeb4385a40fad23 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 29 Apr 2025 17:18:13 -0300 Subject: [PATCH 162/187] regression: fix app installation (#35888) --- packages/apps-engine/src/server/AppManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/apps-engine/src/server/AppManager.ts b/packages/apps-engine/src/server/AppManager.ts index cb398d506085a..5e5b1f96e97eb 100644 --- a/packages/apps-engine/src/server/AppManager.ts +++ b/packages/apps-engine/src/server/AppManager.ts @@ -619,6 +619,8 @@ export class AppManager { return aff; } + app.getStorageItem()._id = created._id; + this.apps.set(app.getID(), app); aff.setApp(app); From 21b219ef830e475fdd24cc56ad3cd1ef2c4cbe26 Mon Sep 17 00:00:00 2001 From: Lucas Pelegrino Date: Wed, 30 Apr 2025 08:13:05 -0300 Subject: [PATCH 163/187] fix: Contact's custom fields are now properly sent to api (#35879) Co-authored-by: Aleksander Nicacio da Silva <6494543+aleksandernsilva@users.noreply.github.com> --- .changeset/warm-steaks-fetch.md | 5 + .../ContactInfo/ReviewContactModal.tsx | 18 +++- ...mnichannel-contact-conflict-review.spec.ts | 93 +++++++++++++++++++ .../fragments/home-omnichannel-content.ts | 4 + .../omnichannel-contact-review-modal.ts | 25 +++++ .../e2e/utils/omnichannel/custom-field.ts | 54 +++++++++++ 6 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 .changeset/warm-steaks-fetch.md create mode 100644 apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts create mode 100644 apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts diff --git a/.changeset/warm-steaks-fetch.md b/.changeset/warm-steaks-fetch.md new file mode 100644 index 0000000000000..842d7f04b46e6 --- /dev/null +++ b/.changeset/warm-steaks-fetch.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes contact's conflict resolution not working due to invalid parameters diff --git a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx index bb5eacfe17d9a..7ca34aa3c4dc7 100644 --- a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx +++ b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx @@ -47,7 +47,7 @@ const ReviewContactModal = ({ contact, onCancel }: ReviewContactModalProps) => { const payload = { name, contactManager, - ...(customFields && { ...customFields }), + ...(customFields && { customFields }), wipeConflicts: true, }; @@ -86,7 +86,7 @@ const ReviewContactModal = ({ contact, onCancel }: ReviewContactModalProps) => { return ( - {t(label as TranslationKey)} + {t(label as TranslationKey)} { rules={{ required: isContactManagerField ? undefined : t('Required_field', { field: t(label as TranslationKey) }), }} - render={({ field: { value, onChange } }) => } + render={({ field: { value, onChange } }) => ( + + )} /> - + {t('different_values_found', { number: values.length })} - {errors?.[name] && {errors?.[name]?.message}} + {errors?.[name] && {errors?.[name]?.message}} ); })} diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts new file mode 100644 index 0000000000000..f2845a1713195 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts @@ -0,0 +1,93 @@ +import { faker } from '@faker-js/faker'; + +import { createFakeVisitor } from '../../mocks/data'; +import { IS_EE } from '../config/constants'; +import { Users } from '../fixtures/userStates'; +import { HomeOmnichannel } from '../page-objects'; +import { createCustomField } from '../utils/omnichannel/custom-field'; +import { createConversation } from '../utils/omnichannel/rooms'; +import { test, expect } from '../utils/test'; + +const visitor = createFakeVisitor(); + +test.skip(!IS_EE, 'Omnichannel Contact Review > Enterprise Only'); + +test.use({ storageState: Users.user1.state }); + +test.describe.serial('OC - Contact Review', () => { + let poHomeChannel: HomeOmnichannel; + + const customFieldName = faker.string.uuid(); + const visitorToken = faker.string.uuid(); + let conversation: Awaited>; + let customField: Awaited>; + + test.beforeAll(async ({ api }) => { + ( + await Promise.all([ + api.post('/livechat/users/agent', { username: 'user1' }), + api.post('/livechat/users/manager', { username: 'user1' }), + ]) + ).every((res) => expect(res.status()).toBe(200)); + + customField = await createCustomField(api, { field: customFieldName }); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeOmnichannel(page); + }); + + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.locator('.main-content').waitFor(); + }); + + test.beforeEach(async ({ api }) => { + conversation = await createConversation(api, { visitorName: visitor.name, agentId: `user1`, visitorToken }); + }); + + test.beforeEach(async ({ api }) => { + const resCustomFieldA = await api.post('/livechat/custom.field', { + token: visitorToken, + key: customFieldName, + value: 'custom-field-value', + overwrite: true, + }); + + expect(resCustomFieldA.status()).toBe(200); + + const resCustomFieldB = await api.post('/livechat/custom.field', { + token: visitorToken, + key: customFieldName, + value: 'custom-field-value-2', + overwrite: false, + }); + + expect(resCustomFieldB.status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + (await Promise.all([api.delete('/livechat/users/agent/user1'), api.delete('/livechat/users/manager/user1')])).every((res) => + expect(res.status()).toBe(200), + ); + + await conversation.delete(); + await customField.delete(); + }); + + test('OC - Contact Review - Update custom field conflicting', async ({ page }) => { + await poHomeChannel.sidenav.getSidebarItemByName(visitor.name).click(); + await poHomeChannel.content.btnContactInformation.click(); + + await poHomeChannel.content.contactReviewModal.btnSeeConflicts.click(); + + await poHomeChannel.content.contactReviewModal.getFieldByName(customFieldName).click(); + await poHomeChannel.content.contactReviewModal.findOption('custom-field-value-2').click(); + await poHomeChannel.content.contactReviewModal.btnSave.click(); + + const response = await page.waitForResponse('**/api/v1/omnichannel/contacts.update'); + await expect(response.status()).toBe(200); + + await expect(poHomeChannel.content.contactReviewModal.btnSeeConflicts).not.toBeVisible(); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts index d6c8cf9decf72..335cec483cb19 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts @@ -3,16 +3,20 @@ import type { Locator, Page } from '@playwright/test'; import { OmnichannelTransferChatModal } from '../omnichannel-transfer-chat-modal'; import { HomeContent } from './home-content'; import { OmnichannelCloseChatModal } from './omnichannel-close-chat-modal'; +import { OmnichannelContactReviewModal } from '../omnichannel-contact-review-modal'; export class HomeOmnichannelContent extends HomeContent { readonly closeChatModal: OmnichannelCloseChatModal; readonly forwardChatModal: OmnichannelTransferChatModal; + readonly contactReviewModal: OmnichannelContactReviewModal; + constructor(page: Page) { super(page); this.closeChatModal = new OmnichannelCloseChatModal(page); this.forwardChatModal = new OmnichannelTransferChatModal(page); + this.contactReviewModal = new OmnichannelContactReviewModal(page); } get btnReturnToQueue(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts new file mode 100644 index 0000000000000..520faa2891ada --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts @@ -0,0 +1,25 @@ +import type { Locator, Page } from '@playwright/test'; + +export class OmnichannelContactReviewModal { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get btnSeeConflicts(): Locator { + return this.page.getByRole('button', { name: 'See conflicts', exact: true }); + } + + get btnSave(): Locator { + return this.page.getByRole('button', { name: 'Save', exact: true }); + } + + getFieldByName(name: string): Locator { + return this.page.getByLabel(name, { exact: true }); + } + + findOption(name: string): Locator { + return this.page.getByRole('option', { name, exact: true }); + } +} diff --git a/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts b/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts new file mode 100644 index 0000000000000..bc476aae918c8 --- /dev/null +++ b/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts @@ -0,0 +1,54 @@ +import type { ILivechatCustomField } from '@rocket.chat/core-typings'; + +import { parseMeteorResponse } from '../parseMeteorResponse'; +import type { BaseTest } from '../test'; + +type CustomField = Omit & { field: string }; + +export const removeCustomField = (api: BaseTest['api'], id: string) => { + return api.post('/method.call/livechat:deleteCustomField', { + method: 'livechat:saveCustomField', + params: [id], + id: 'id', + msg: 'method', + }); +}; + +export const createCustomField = async (api: BaseTest['api'], overwrides: Partial) => { + const response = await api.post('/method.call/livechat:saveCustomField', { + message: JSON.stringify({ + method: 'livechat:saveCustomField', + params: [ + null, + { + field: overwrides.field, + label: overwrides.label || overwrides.field, + visibility: 'visible', + scope: 'visitor', + searchable: false, + regexp: '', + type: 'input', + required: false, + defaultValue: '', + options: '', + public: false, + ...overwrides, + }, + ], + id: 'id', + msg: 'method', + }), + }); + + if (!response.ok()) { + throw new Error(`Failed to create custom field [http status: ${response.status()}]`); + } + + const customField = await parseMeteorResponse(response); + + return { + response, + customField, + delete: () => removeCustomField(api, customField._id), + }; +}; From 05c57beb60fa6cf347beb8d809818783ad3c1e4a Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 30 Apr 2025 10:30:07 -0300 Subject: [PATCH 164/187] test: Adjusted contact review e2e test to prevent possible flaky behavior (#35911) --- .../omnichannel/omnichannel-contact-conflict-review.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts index f2845a1713195..b808d28f7aca4 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts @@ -83,9 +83,9 @@ test.describe.serial('OC - Contact Review', () => { await poHomeChannel.content.contactReviewModal.getFieldByName(customFieldName).click(); await poHomeChannel.content.contactReviewModal.findOption('custom-field-value-2').click(); + const responseListener = page.waitForResponse('**/api/v1/omnichannel/contacts.update'); await poHomeChannel.content.contactReviewModal.btnSave.click(); - - const response = await page.waitForResponse('**/api/v1/omnichannel/contacts.update'); + const response = await responseListener; await expect(response.status()).toBe(200); await expect(poHomeChannel.content.contactReviewModal.btnSeeConflicts).not.toBeVisible(); From 166e669d5df17bf92ecd5439aa45f47db27144e2 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 29 Apr 2025 23:03:03 -0300 Subject: [PATCH 165/187] test: flaky feature preview tests (#35891) Co-authored-by: juliajforesti --- apps/meteor/tests/e2e/feature-preview.spec.ts | 4 ++++ .../meteor/tests/e2e/message-mentions.spec.ts | 4 ++-- .../page-objects/fragments/home-content.ts | 20 ++++++++++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index b477cf664ac2b..72259e8b5665b 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -45,6 +45,7 @@ test.describe.serial('feature preview', () => { test('should show "Message" and "Navigation" feature sections', async ({ page }) => { await page.goto('/account/feature-preview'); + await page.waitForSelector('.main-content'); await expect(page.getByRole('button', { name: 'Message' })).toBeVisible(); await expect(page.getByRole('button', { name: 'Navigation' })).toBeVisible(); @@ -202,6 +203,9 @@ test.describe.serial('feature preview', () => { await poHomeChannel.content.sendMessage('hello world'); const item = poHomeChannel.sidebar.getSearchRoomByName(targetChannel); + + await expect(item).toBeVisible(); + await poHomeChannel.sidebar.markItemAsUnread(item); await poHomeChannel.sidebar.escSearch(); diff --git a/apps/meteor/tests/e2e/message-mentions.spec.ts b/apps/meteor/tests/e2e/message-mentions.spec.ts index 15abd9c36b400..45f4bd657a643 100644 --- a/apps/meteor/tests/e2e/message-mentions.spec.ts +++ b/apps/meteor/tests/e2e/message-mentions.spec.ts @@ -56,7 +56,7 @@ test.describe.serial('Should not allow to send @all mention if permission to do await expect(page).toHaveURL(`/group/${targetChannel2}`); }); await test.step('receive notify message', async () => { - await adminPage.content.sendMessage('@all '); + await adminPage.content.sendMessage('@all ', false); await expect(adminPage.content.lastUserMessage).toContainText('Notify all in this room is not allowed'); }); }); @@ -98,7 +98,7 @@ test.describe.serial('Should not allow to send @here mention if permission to do await expect(page).toHaveURL(`/group/${targetChannel2}`); }); await test.step('receive notify message', async () => { - await adminPage.content.sendMessage('@here '); + await adminPage.content.sendMessage('@here ', false); await expect(adminPage.content.lastUserMessage).toContainText('Notify all in this room is not allowed'); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 905be5aa9c530..63e03a71e38aa 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -81,11 +81,25 @@ export class HomeContent { await this.joinRoom(); } - async sendMessage(text: string): Promise { + async sendMessage(text: string, enforce = true): Promise { await this.joinRoomIfNeeded(); await this.page.waitForSelector('[name="msg"]:not([disabled])'); await this.page.locator('[name="msg"]').fill(text); + const responsePromise = this.page.waitForResponse( + (response) => + /api\/v1\/method.call\/sendMessage/.test(response.url()) && response.status() === 200 && response.request().method() === 'POST', + ); await this.page.getByRole('button', { name: 'Send', exact: true }).click(); + + if (enforce) { + const response = await (await responsePromise).json(); + + const mid = JSON.parse(response.message).result._id; + const messageLocator = this.getMessageById(mid); + + await expect(messageLocator).toBeVisible(); + await expect(messageLocator).not.toHaveClass('rcx-message--pending'); + } } async dispatchSlashCommand(text: string): Promise { @@ -432,6 +446,10 @@ export class HomeContent { return this.page.locator('[role="listitem"][aria-roledescription="message"]', { hasText: text }); } + getMessageById(id: string): Locator { + return this.page.locator(`[data-qa-type="message"][id="${id}"]`); + } + async waitForChannel(): Promise { await this.page.locator('role=main').waitFor(); await this.page.locator('role=main >> role=heading[level=1]').waitFor(); From 16885a6f2b36e2daf0c8ff80e9384fe6ef4794f9 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 30 Apr 2025 10:25:32 -0300 Subject: [PATCH 166/187] test(flaky): ensure agent info is saved before reopening tab (#35910) --- apps/meteor/tests/e2e/e2e-encryption.spec.ts | 2 ++ .../e2e/omnichannel/omnichannel-agents.spec.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 39ba5fe64d015..0a50877d6894a 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -884,6 +884,8 @@ test.describe.serial('e2ee room setup', () => { await page.goto('/home'); + await page.waitForSelector('.main-content'); + await expect(poHomeChannel.bannerSaveEncryptionPassword).toBeVisible(); const channelName = faker.string.uuid(); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts index 239978928126e..643e672af6e1b 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-agents.spec.ts @@ -114,8 +114,21 @@ test.describe.serial('OC - Manage Agents', () => { await poOmnichannelAgents.btnEdit.click(); await poOmnichannelAgents.selectDepartment(department.data.name); + const reg = new RegExp(`/api/v1/method.call/${encodeURIComponent('livechat:saveAgentInfo')}`); + const response = page.waitForResponse(reg); await poOmnichannelAgents.btnSave.click(); + /** + * between saving and opening the agent info again it is necessary to + * wait for the agent to be saved, since after successfully saving + * the contextual bar is closed + * otherwise content will be closed even if the current one is not the editing one + */ + + await response; + + await expect(poOmnichannelAgents.editCtxBar).not.toBeVisible(); + await test.step('expect the selected department is visible', async () => { await poOmnichannelAgents.findRowByUsername('user1').click(); From 81aede6098030ee2b141363ceb9830c07bcd5db7 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 30 Apr 2025 14:23:28 -0300 Subject: [PATCH 167/187] regression: `NavbarSearch` closing list when clicking on field (#35909) --- .../client/NavBarV2/NavBarSearch/NavBarSearch.tsx | 3 +++ .../NavBarV2/NavBarSearch/hooks/useSearchClick.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchClick.ts diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearch.tsx b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearch.tsx index eab5c43c16015..23b7493789b76 100644 --- a/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearch.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSearch/NavBarSearch.tsx @@ -9,6 +9,7 @@ import tinykeys from 'tinykeys'; import NavBarSearchListBox from './NavBarSearchListbox'; import { getShortcutLabel } from './getShortcutLabel'; +import { useSearchClick } from './hooks/useSearchClick'; import { useSearchFocus } from './hooks/useSearchFocus'; import { useSearchInputNavigation } from './hooks/useSearchNavigation'; @@ -38,6 +39,7 @@ const NavBarSearch = () => { const handleKeyDown = useSearchInputNavigation(state); const handleFocus = useSearchFocus(state); + const handleClick = useSearchClick(state); const handleEscSearch = useCallback(() => { resetField('filterText'); @@ -78,6 +80,7 @@ const NavBarSearch = () => { {...triggerProps} onFocus={handleFocus} onKeyDown={handleKeyDown} + onClick={handleClick} autoComplete='off' placeholder={placeholder} ref={mergedRefs} diff --git a/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchClick.ts b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchClick.ts new file mode 100644 index 0000000000000..887d41bf27294 --- /dev/null +++ b/apps/meteor/client/NavBarV2/NavBarSearch/hooks/useSearchClick.ts @@ -0,0 +1,14 @@ +import { useCallback } from 'react'; +import type { OverlayTriggerState } from 'react-stately'; + +export const useSearchClick = (state: OverlayTriggerState) => { + const handleClick = useCallback(() => { + if (state.isOpen) { + return; + } + + state.setOpen(true); + }, [state]); + + return handleClick; +}; From 2e21d6ff9ac76afbf7511e3135eea62928dffe69 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 30 Apr 2025 14:05:15 -0300 Subject: [PATCH 168/187] regression: fix jump from message lists (#35880) --- .../ui-utils/client/lib/RoomHistoryManager.ts | 65 +++++++++++++++---- .../message/list/MessageListContext.tsx | 6 +- .../client/lib/utils/legacyJumpToMessage.ts | 6 +- .../MessageList/hooks/useJumpToMessage.ts | 9 ++- .../providers/MessageListProvider.tsx | 4 +- .../client/views/room/body/RoomBody.tsx | 13 +++- .../client/views/room/body/RoomBodyV2.tsx | 13 +++- .../views/room/body/hooks/useGetMore.spec.ts | 1 + .../views/room/body/hooks/useGetMore.ts | 36 +++++++--- .../hooks/useMessageComposerMergedRefs.ts | 22 ++++--- .../Threads/components/ThreadMessageList.tsx | 4 +- 11 files changed, 130 insertions(+), 49 deletions(-) diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index db11d3ece34fc..9b8c29b086ea1 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -14,6 +14,8 @@ import { waitForElement } from '../../../../client/lib/utils/waitForElement'; import { Messages, Subscriptions } from '../../../models/client'; import { getUserPreference } from '../../../utils/client'; +const waitAfterFlush = () => new Promise((resolve) => Tracker.afterFlush(() => resolve(void 0))); + export async function upsertMessage( { msg, @@ -40,18 +42,22 @@ export async function upsertMessage( return collection.upsert({ _id }, msg); } -export function upsertMessageBulk( +export async function upsertMessageBulk( { msgs, subscription }: { msgs: IMessage[]; subscription?: ISubscription }, collection: MinimongoCollection = Messages, ) { const { queries } = collection; collection.queries = []; - msgs.forEach((msg, index) => { - if (index === msgs.length - 1) { - collection.queries = queries; - } - void upsertMessage({ msg, subscription }, collection); - }); + const lastMessage = msgs.pop(); + + for await (const msg of msgs) { + await upsertMessage({ msg, subscription }, collection); + } + + if (lastMessage) { + collection.queries = queries; + await upsertMessage({ msg: lastMessage, subscription }, collection); + } } const defaultLimit = parseInt(getConfig('roomListLimit') ?? '50') || 50; @@ -69,6 +75,10 @@ class RoomHistoryManagerClass extends Emitter { firstUnread: ReactiveVar; loaded: number | undefined; oldestTs?: Date; + scroll?: { + scrollHeight: number; + scrollTop: number; + }; } > = {}; @@ -162,13 +172,20 @@ class RoomHistoryManagerClass extends Emitter { room.oldestTs = messages[messages.length - 1].ts; } - await waitForElement('.messages-box .wrapper [data-overlayscrollbars-viewport]'); + const wrapper = await waitForElement('.messages-box .wrapper [data-overlayscrollbars-viewport]'); + + room.scroll = { + scrollHeight: wrapper.scrollHeight, + scrollTop: wrapper.scrollTop, + }; - upsertMessageBulk({ + await upsertMessageBulk({ msgs: messages.filter((msg) => msg.t !== 'command'), subscription, }); + this.emit('loaded-messages'); + if (!room.loaded) { room.loaded = 0; } @@ -185,7 +202,27 @@ class RoomHistoryManagerClass extends Emitter { return this.getMore(rid); } + this.emit('loaded-messages'); + room.isLoading.set(false); + await waitAfterFlush(); + } + + public restoreScroll(rid: IRoom['_id']) { + const room = this.getRoom(rid); + const wrapper = document.querySelector('.messages-box .wrapper [data-overlayscrollbars-viewport]'); + + if (room.scroll === undefined) { + return; + } + + if (!wrapper) { + return; + } + + const heightDiff = wrapper.scrollHeight - (room.scroll.scrollHeight ?? NaN); + wrapper.scrollTop = room.scroll.scrollTop + heightDiff; + room.scroll = undefined; } public async getMoreNext(rid: IRoom['_id'], atBottomRef: MutableRefObject) { @@ -206,11 +243,13 @@ class RoomHistoryManagerClass extends Emitter { if (lastMessage?.ts) { const { ts } = lastMessage; const result = await callWithErrorHandling('loadNextMessages', rid, ts, defaultLimit); - upsertMessageBulk({ + await upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription, }); + this.emit('loaded-messages'); + room.isLoading.set(false); if (!room.loaded) { room.loaded = 0; @@ -247,7 +286,7 @@ class RoomHistoryManagerClass extends Emitter { return room.isLoading.get(); } - public async clear(rid: IRoom['_id']) { + public clear(rid: IRoom['_id']) { const room = this.getRoom(rid); Messages.remove({ rid }); room.isLoading.set(true); @@ -269,7 +308,7 @@ class RoomHistoryManagerClass extends Emitter { } const room = this.getRoom(message.rid); - void this.clear(message.rid); + this.clear(message.rid); const subscription = Subscriptions.findOne({ rid: message.rid }); @@ -279,7 +318,7 @@ class RoomHistoryManagerClass extends Emitter { return; } - upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription }); + await upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription }); Tracker.afterFlush(async () => { this.emit('loaded-messages'); diff --git a/apps/meteor/client/components/message/list/MessageListContext.tsx b/apps/meteor/client/components/message/list/MessageListContext.tsx index 2f3b1e54c5377..c175dac1baf8b 100644 --- a/apps/meteor/client/components/message/list/MessageListContext.tsx +++ b/apps/meteor/client/components/message/list/MessageListContext.tsx @@ -1,5 +1,5 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import type { KeyboardEvent, MouseEvent, MutableRefObject } from 'react'; +import type { KeyboardEvent, MouseEvent, RefCallback } from 'react'; import { createContext, useContext } from 'react'; export type MessageListContextValue = { @@ -25,7 +25,7 @@ export type MessageListContextValue = { showColors: boolean; jumpToMessageParam?: string; username: string | undefined; - messageListRef?: MutableRefObject; + messageListRef?: RefCallback; }; export const MessageListContext = createContext({ @@ -43,7 +43,7 @@ export const MessageListContext = createContext({ showUsername: false, showColors: false, username: undefined, - messageListRef: { current: undefined }, + messageListRef: undefined, }); export const useShowTranslated: MessageListContextValue['useShowTranslated'] = (...args) => diff --git a/apps/meteor/client/lib/utils/legacyJumpToMessage.ts b/apps/meteor/client/lib/utils/legacyJumpToMessage.ts index 9ae917e8cae9a..b972f93bbbd52 100644 --- a/apps/meteor/client/lib/utils/legacyJumpToMessage.ts +++ b/apps/meteor/client/lib/utils/legacyJumpToMessage.ts @@ -36,13 +36,11 @@ export const legacyJumpToMessage = async (message: IMessage) => { } if (RoomManager.opened === message.rid) { - RoomHistoryManager.getSurroundingMessages(message); + await RoomHistoryManager.getSurroundingMessages(message); return; } await goToRoomById(message.rid); - setTimeout(() => { - RoomHistoryManager.getSurroundingMessages(message); - }, 400); + await RoomHistoryManager.getSurroundingMessages(message); }; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts index 096a6cd4f7777..85a27beeefc03 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts @@ -4,6 +4,7 @@ import { useCallback } from 'react'; import { useMessageListJumpToMessageParam, useMessageListRef } from '../../../../components/message/list/MessageListContext'; import { useSafeRefCallback } from '../../../../hooks/useSafeRefCallback'; +import { setRef } from '../../composer/hooks/useMessageComposerMergedRefs'; import { setHighlightMessage, clearHighlightMessage } from '../providers/messageHighlightSubscription'; // this is an arbitrary value so that there's a gap between the header and the message; @@ -20,10 +21,12 @@ export const useJumpToMessage = (messageId: IMessage['_id']) => { return; } - if (listRef) { - listRef.current = node; + if (!listRef) { + return; } + setRef(listRef, node); + node.scrollIntoView({ behavior: 'smooth', block: 'center', @@ -60,7 +63,7 @@ export const useJumpToMessage = (messageId: IMessage['_id']) => { return () => { observer.disconnect(); if (listRef) { - listRef.current = undefined; + setRef(listRef, undefined); } }; }, diff --git a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx index 2cf9cc5014359..040806751cc81 100644 --- a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx +++ b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx @@ -1,6 +1,6 @@ import { isThreadMainMessage } from '@rocket.chat/core-typings'; import { useLayout, useUser, useUserPreference, useSetting, useEndpoint, useSearchParameter } from '@rocket.chat/ui-contexts'; -import type { MutableRefObject, ReactNode } from 'react'; +import type { ReactNode, RefCallback } from 'react'; import { useMemo, memo } from 'react'; import { getRegexHighlight, getRegexHighlightUrl } from '../../../../../app/highlight-words/client/helper'; @@ -15,7 +15,7 @@ import { useLoadSurroundingMessages } from '../hooks/useLoadSurroundingMessages' type MessageListProviderProps = { children: ReactNode; - messageListRef?: MutableRefObject; + messageListRef?: RefCallback; attachmentDimension?: { width?: number; height?: number; diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index e4b64f35c5864..6be98ae7592f4 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -91,9 +91,18 @@ const RoomBody = (): ReactElement => { const { innerRef: dateScrollInnerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll(); - const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom, jumpToRef } = useListIsAtBottom(); + const { + innerRef: isAtBottomInnerRef, + atBottomRef, + sendToBottom, + sendToBottomIfNecessary, + isAtBottom, + jumpToRef: jumpToRefIsAtBottom, + } = useListIsAtBottom(); + + const { innerRef: getMoreInnerRef, jumpToRef: jumpToRefGetMore } = useGetMore(room._id, atBottomRef); - const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); + const jumpToRef = useMergedRefs(jumpToRefGetMore, jumpToRefIsAtBottom); const { uploads, diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 2038163f82637..1b3c8cfb72300 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -91,9 +91,18 @@ const RoomBody = (): ReactElement => { const { innerRef: dateScrollInnerRef, bubbleRef, listStyle, ...bubbleDate } = useDateScroll(); - const { innerRef: isAtBottomInnerRef, atBottomRef, sendToBottom, sendToBottomIfNecessary, isAtBottom, jumpToRef } = useListIsAtBottom(); + const { + innerRef: isAtBottomInnerRef, + atBottomRef, + sendToBottom, + sendToBottomIfNecessary, + isAtBottom, + jumpToRef: jumpToRefIsAtBottom, + } = useListIsAtBottom(); + + const { innerRef: getMoreInnerRef, jumpToRef: jumpToRefGetMore } = useGetMore(room._id, atBottomRef); - const { innerRef: getMoreInnerRef } = useGetMore(room._id, atBottomRef); + const jumpToRef = useMergedRefs(jumpToRefIsAtBottom, jumpToRefGetMore); const { uploads, diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts index ca289f7f5772b..e9e36dcd2d1da 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts @@ -11,6 +11,7 @@ jest.mock('../../../../../app/ui-utils/client', () => ({ hasMoreNext: jest.fn(), getMore: jest.fn(), getMoreNext: jest.fn(), + restoreScroll: jest.fn(), }, })); diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.ts index 32a6b1fb78e72..5c57bf12358df 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.ts @@ -1,5 +1,6 @@ import type { MutableRefObject } from 'react'; import { useEffect, useRef } from 'react'; +import { flushSync } from 'react-dom'; import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; @@ -7,6 +8,8 @@ import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; export const useGetMore = (rid: string, atBottomRef: MutableRefObject) => { const ref = useRef(null); + const jumpToRef = useRef(undefined); + useEffect(() => { if (!ref.current) { return; @@ -14,31 +17,48 @@ export const useGetMore = (rid: string, atBottomRef: MutableRefObject) const refValue = ref.current; - const handleScroll = withThrottling({ wait: 100 })((event) => { + const handleScroll = withThrottling({ wait: 300 })(async (event) => { const lastScrollTopRef = event.target.scrollTop; const height = event.target.clientHeight; const isLoading = RoomHistoryManager.isLoading(rid); const hasMore = RoomHistoryManager.hasMore(rid); const hasMoreNext = RoomHistoryManager.hasMoreNext(rid); - if ((isLoading === false && hasMore === true) || hasMoreNext === true) { - if (hasMore === true && lastScrollTopRef <= height / 3) { - RoomHistoryManager.getMore(rid); - } else if (hasMoreNext === true && Math.ceil(lastScrollTopRef) >= event.target.scrollHeight - height) { - RoomHistoryManager.getMoreNext(rid, atBottomRef); - atBottomRef.current = false; + if (!((isLoading === false && hasMore === true) || hasMoreNext === true)) { + return; + } + + if (jumpToRef.current) { + return; + } + + if (hasMore === true && lastScrollTopRef <= height / 3) { + await RoomHistoryManager.getMore(rid); + + if (jumpToRef.current) { + return; } + flushSync(() => { + RoomHistoryManager.restoreScroll(rid); + }); + } else if (hasMoreNext === true && Math.ceil(lastScrollTopRef) >= event.target.scrollHeight - height) { + RoomHistoryManager.getMoreNext(rid, atBottomRef); + atBottomRef.current = false; } }); - refValue.addEventListener('scroll', handleScroll); + refValue.addEventListener('scroll', handleScroll, { + passive: true, + }); return () => { + handleScroll.cancel(); refValue.removeEventListener('scroll', handleScroll); }; }, [rid, atBottomRef]); return { innerRef: ref, + jumpToRef, }; }; diff --git a/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts b/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts index 02f129df3b6db..aea0318ce831d 100644 --- a/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts +++ b/apps/meteor/client/views/room/composer/hooks/useMessageComposerMergedRefs.ts @@ -4,6 +4,17 @@ import { useCallback } from 'react'; const isRefCallback = (x: unknown): x is RefCallback => typeof x === 'function'; const isMutableRefObject = (x: unknown): x is MutableRefObject => typeof x === 'object'; +export const setRef = (ref: Ref | undefined, refValue: T) => { + if (isRefCallback(ref)) { + ref(refValue); + return; + } + + if (isMutableRefObject(ref)) { + ref.current = refValue; + } +}; + /** * Merges multiple refs into a single ref callback. * it was not meant to be used with in any different place than MessageBox @@ -13,16 +24,7 @@ const isMutableRefObject = (x: unknown): x is MutableRefObject => typeof x */ export const useMessageComposerMergedRefs = (...refs: (Ref | undefined)[]): RefCallback => { return useCallback((refValue: T) => { - refs.forEach((ref) => { - if (isRefCallback(ref)) { - ref(refValue); - return; - } - - if (isMutableRefObject(ref)) { - ref.current = refValue; - } - }); + refs.forEach((ref) => setRef(ref, refValue)); // eslint-disable-next-line react-hooks/exhaustive-deps }, refs); }; 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 b52e8c7b0cc57..208065e4beab8 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx @@ -2,7 +2,7 @@ import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; import { differenceInSeconds } from 'date-fns'; -import type { ReactElement } from 'react'; +import type { ReactElement, RefCallback } from 'react'; import { Fragment } from 'react'; import { useTranslation } from 'react-i18next'; @@ -79,7 +79,7 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen ) : ( - + }> {[mainMessage, ...messages].map((message, index, { [index - 1]: previous }) => { const sequential = isMessageSequential(message, previous, messageGroupingPeriod); const newDay = isMessageNewDay(message, previous); From 8409f2fff8c5fbea78489454a7c5b87005a78cfc Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 30 Apr 2025 14:29:17 -0300 Subject: [PATCH 169/187] regression: close idle connections (#35912) --- apps/meteor/client/hooks/useIdleConnection.ts | 7 +++---- apps/meteor/client/providers/UserProvider/UserProvider.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/hooks/useIdleConnection.ts b/apps/meteor/client/hooks/useIdleConnection.ts index e6b7336236384..6c7ea02853db5 100644 --- a/apps/meteor/client/hooks/useIdleConnection.ts +++ b/apps/meteor/client/hooks/useIdleConnection.ts @@ -1,17 +1,16 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { ServerContext, useConnectionStatus, useSetting, useUserId } from '@rocket.chat/ui-contexts'; +import { ServerContext, useConnectionStatus, useSetting } from '@rocket.chat/ui-contexts'; import { useContext } from 'react'; import { useIdleActiveEvents } from './useIdleActiveEvents'; -export const useIdleConnection = () => { - const uid = useUserId(); +export const useIdleConnection = (uid: string | null) => { const { status } = useConnectionStatus(); const allowAnonymousRead = useSetting('Accounts_AllowAnonymousRead'); const { disconnect: disconnectServer, reconnect: reconnectServer } = useContext(ServerContext); const disconnect = useEffectEvent(() => { - if (status === 'offline') { + if (status !== 'offline') { if (!uid && allowAnonymousRead !== true) { disconnectServer(); } diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 619f9d3a14b2b..f2fa76732f19f 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -63,7 +63,7 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => { useDeleteUser(); useUpdateAvatar(); - useIdleConnection(); + useIdleConnection(userId); const contextValue = useMemo( (): ContextType => ({ From 25b3fe169d5c6857f9d0200716b2cf9d5c8890f5 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Wed, 30 Apr 2025 14:33:25 -0300 Subject: [PATCH 170/187] regression: `audit.settings` endpoint not behind license module (#35906) --- apps/meteor/app/api/server/index.ts | 1 - .../meteor/app/api/server/v1/server-events.ts | 104 ------------------ apps/meteor/ee/server/api/audit.ts | 102 ++++++++++++++++- apps/meteor/tests/end-to-end/api/settings.ts | 3 +- 4 files changed, 103 insertions(+), 107 deletions(-) delete mode 100644 apps/meteor/app/api/server/v1/server-events.ts diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts index e4f6370bfb069..9efdb889fd0d7 100644 --- a/apps/meteor/app/api/server/index.ts +++ b/apps/meteor/app/api/server/index.ts @@ -48,7 +48,6 @@ import './v1/voip/omnichannel'; import './v1/voip'; import './v1/federation'; import './v1/moderation'; -import './v1/server-events'; // This has to come last so all endpoints are registered before generating the OpenAPI documentation import './default/openApi'; diff --git a/apps/meteor/app/api/server/v1/server-events.ts b/apps/meteor/app/api/server/v1/server-events.ts deleted file mode 100644 index 59fe33d41e530..0000000000000 --- a/apps/meteor/app/api/server/v1/server-events.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { ServerEvents } from '@rocket.chat/models'; -import { isServerEventsAuditSettingsProps } from '@rocket.chat/rest-typings'; -import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv'; - -import { API } from '../api'; -import { getPaginationItems } from '../helpers/getPaginationItems'; - -API.v1.get( - 'audit.settings', - { - response: { - 200: ajv.compile({ - additionalProperties: false, - type: 'object', - properties: { - events: { - type: 'array', - items: { - type: 'object', - }, - }, - count: { - type: 'number', - description: 'The number of events returned in this response.', - }, - offset: { - type: 'number', - description: 'The number of events that were skipped in this response.', - }, - total: { - type: 'number', - description: 'The total number of events that match the query.', - }, - success: { - type: 'boolean', - description: 'Indicates if the request was successful.', - }, - }, - required: ['events', 'count', 'offset', 'total', 'success'], - }), - 400: ajv.compile({ - type: 'object', - properties: { - success: { - type: 'boolean', - enum: [false], - }, - error: { - type: 'string', - }, - errorType: { - type: 'string', - }, - }, - required: ['success', 'error'], - }), - }, - query: isServerEventsAuditSettingsProps, - authRequired: true, - permissionsRequired: ['can-audit'], - }, - async function action() { - const { start, end, settingId, actor } = this.queryParams; - - if (start && isNaN(Date.parse(start as string))) { - return API.v1.failure('The "start" query parameter must be a valid date.'); - } - - if (end && isNaN(Date.parse(end as string))) { - return API.v1.failure('The "end" query parameter must be a valid date.'); - } - - const { offset, count } = await getPaginationItems(this.queryParams as Record); - const { sort } = await this.parseJsonQuery(); - const _sort = { ts: sort?.ts ? sort?.ts : -1 }; - - const { cursor, totalCount } = ServerEvents.findPaginated( - { - ...(settingId && { 'data.key': 'id', 'data.value': settingId }), - ...(actor && { actor }), - ts: { - $gte: start ? new Date(start as string) : new Date(0), - $lte: end ? new Date(end as string) : new Date(), - }, - t: 'settings.changed', - }, - { - sort: _sort, - skip: offset, - limit: count, - allowDiskUse: true, - }, - ); - - const [events, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - events, - count: events.length, - offset, - total, - }); - }, -); diff --git a/apps/meteor/ee/server/api/audit.ts b/apps/meteor/ee/server/api/audit.ts index 61a0c618b410d..f86db47db4df2 100644 --- a/apps/meteor/ee/server/api/audit.ts +++ b/apps/meteor/ee/server/api/audit.ts @@ -1,5 +1,6 @@ import type { IUser, IRoom } from '@rocket.chat/core-typings'; -import { Rooms, AuditLog } from '@rocket.chat/models'; +import { Rooms, AuditLog, ServerEvents } from '@rocket.chat/models'; +import { isServerEventsAuditSettingsProps } from '@rocket.chat/rest-typings'; import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings'; import Ajv from 'ajv'; @@ -98,3 +99,102 @@ API.v1.addRoute( }, }, ); + +API.v1.get( + 'audit.settings', + { + response: { + 200: ajv.compile({ + additionalProperties: false, + type: 'object', + properties: { + events: { + type: 'array', + items: { + type: 'object', + }, + }, + count: { + type: 'number', + description: 'The number of events returned in this response.', + }, + offset: { + type: 'number', + description: 'The number of events that were skipped in this response.', + }, + total: { + type: 'number', + description: 'The total number of events that match the query.', + }, + success: { + type: 'boolean', + description: 'Indicates if the request was successful.', + }, + }, + required: ['events', 'count', 'offset', 'total', 'success'], + }), + 400: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [false], + }, + error: { + type: 'string', + }, + errorType: { + type: 'string', + }, + }, + required: ['success', 'error'], + }), + }, + query: isServerEventsAuditSettingsProps, + authRequired: true, + permissionsRequired: ['can-audit'], + license: ['auditing'], + }, + async function action() { + const { start, end, settingId, actor } = this.queryParams; + + if (start && isNaN(Date.parse(start as string))) { + return API.v1.failure('The "start" query parameter must be a valid date.'); + } + + if (end && isNaN(Date.parse(end as string))) { + return API.v1.failure('The "end" query parameter must be a valid date.'); + } + + const { offset, count } = await getPaginationItems(this.queryParams as Record); + const { sort } = await this.parseJsonQuery(); + const _sort = { ts: sort?.ts ? sort?.ts : -1 }; + + const { cursor, totalCount } = ServerEvents.findPaginated( + { + ...(settingId && { 'data.key': 'id', 'data.value': settingId }), + ...(actor && { actor }), + ts: { + $gte: start ? new Date(start as string) : new Date(0), + $lte: end ? new Date(end as string) : new Date(), + }, + t: 'settings.changed', + }, + { + sort: _sort, + skip: offset, + limit: count, + allowDiskUse: true, + }, + ); + + const [events, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + events, + count: events.length, + offset, + total, + }); + }, +); diff --git a/apps/meteor/tests/end-to-end/api/settings.ts b/apps/meteor/tests/end-to-end/api/settings.ts index 6cd635e41e8f8..be301ed4a088a 100644 --- a/apps/meteor/tests/end-to-end/api/settings.ts +++ b/apps/meteor/tests/end-to-end/api/settings.ts @@ -4,6 +4,7 @@ import { before, describe, it, after } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; +import { IS_EE } from '../../e2e/config/constants'; describe('[Settings]', () => { before((done) => getCredentials(done)); @@ -274,7 +275,7 @@ describe('[Settings]', () => { }); }); - describe('/audit.settings', () => { + (IS_EE ? describe : describe.skip)('/audit.settings', () => { const formatDate = (date: Date) => date.toISOString().slice(0, 10).replace(/-/g, '/'); it('should return list of settings changed (no filters)', async () => { From 7f6230e0262f214bda902f0654dddf7366c3aa9c Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Wed, 30 Apr 2025 17:40:39 +0000 Subject: [PATCH 171/187] Release 7.6.0-rc.3 [no ci] --- .changeset/bump-patch-1746034829149.md | 5 +++ .changeset/pre.json | 2 + apps/meteor/CHANGELOG.md | 37 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 +++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 12 ++++++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 14 +++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 14 +++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 15 ++++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 15 ++++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 14 +++++++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 14 +++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 13 +++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 +++++ ee/packages/license/package.json | 2 +- ee/packages/network-broker/CHANGELOG.md | 9 +++++ ee/packages/network-broker/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 +++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 9 +++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 ++++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 +++++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 10 +++++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 11 ++++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 2 + packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 +++++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 ++++++ packages/ddp-client/package.json | 2 +- packages/freeswitch/CHANGELOG.md | 9 +++++ packages/freeswitch/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 13 +++++++ packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/CHANGELOG.md | 11 ++++++ packages/gazzodown/package.json | 2 +- packages/instance-status/CHANGELOG.md | 9 +++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 9 +++++ packages/livechat/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 +++++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 9 +++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 10 +++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 9 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 +++++ packages/ui-avatar/package.json | 2 +- packages/ui-client/CHANGELOG.md | 10 +++++ packages/ui-client/package.json | 2 +- packages/ui-contexts/CHANGELOG.md | 11 ++++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 10 +++++ packages/ui-video-conf/package.json | 2 +- packages/ui-voip/CHANGELOG.md | 11 ++++++ packages/ui-voip/package.json | 2 +- packages/web-ui-registration/CHANGELOG.md | 9 +++++ packages/web-ui-registration/package.json | 2 +- 76 files changed, 461 insertions(+), 38 deletions(-) create mode 100644 .changeset/bump-patch-1746034829149.md diff --git a/.changeset/bump-patch-1746034829149.md b/.changeset/bump-patch-1746034829149.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746034829149.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/pre.json b/.changeset/pre.json index a6b3179fe07c6..3f9f1e6e7738f 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -76,6 +76,7 @@ "bright-forks-drop", "bump-patch-1745354933374", "bump-patch-1745631711629", + "bump-patch-1746034829149", "dirty-seas-explode", "eighty-wombats-smile", "eleven-laws-crash", @@ -114,6 +115,7 @@ "ten-schools-collect", "thin-cycles-return", "tidy-cups-smoke", + "warm-steaks-fetch", "wet-penguins-end", "witty-buttons-greet", "witty-foxes-thank", diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 28529ed343b14..4f4412250081c 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,42 @@ # @rocket.chat/meteor +## 7.6.0-rc.3 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#35879](https://github.com/RocketChat/Rocket.Chat/pull/35879)) Fixes contact's conflict resolution not working due to invalid parameters + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/license@1.0.12-rc.3 + - @rocket.chat/omnichannel-services@0.3.18-rc.3 + - @rocket.chat/pdf-worker@0.3.0-rc.3 + - @rocket.chat/presence@0.2.21-rc.3 + - @rocket.chat/api-client@0.2.21-rc.3 + - @rocket.chat/apps@0.5.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/cron@0.1.21-rc.3 + - @rocket.chat/freeswitch@1.2.8-rc.3 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.3 + - @rocket.chat/gazzodown@18.0.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.3 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 + - @rocket.chat/ui-client@18.0.0-rc.3 + - @rocket.chat/ui-video-conf@18.0.0-rc.3 + - @rocket.chat/ui-voip@8.0.0-rc.3 + - @rocket.chat/web-ui-registration@18.0.0-rc.3 + - @rocket.chat/instance-status@0.1.21-rc.3 +
+ ## 7.6.0-rc.2 ### Patch Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 033ae320af393..8515d75266419 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.6.0-rc.2" + "version": "7.6.0-rc.3" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 158c05bc2a348..20bc4f6527473 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,19 @@ # rocketchat-services +## 2.0.12-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
+ ## 2.0.12-rc.2 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index cea1bcdf2b220..de2803a284bfb 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "2.0.12-rc.2", + "version": "2.0.12-rc.3", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 236aeffe895b8..96e0d4840cae8 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.6.0-rc.2", + "version": "7.6.0-rc.3", "private": true, "type": "commonjs", "author": { diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index f0f14105a402c..0cb16a3563d93 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/uikit-playground +## 0.6.12-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.3 + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 +
+ ## 0.6.12-rc.2 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 43971ffa5d64d..2f979a885e987 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.6.12-rc.2", + "version": "0.6.12-rc.3", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 655316c0c195a..c00a4ff705065 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/account-service +## 0.4.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
+ ## 0.4.21-rc.2 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index 5d390957804bd..f1be6fb10929e 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.21-rc.2", + "version": "0.4.21-rc.3", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 7f6cd148ab3a6..f9866db469817 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/authorization-service +## 0.4.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
+ ## 0.4.21-rc.2 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 5d89f4b256e8c..9c427b57f1001 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.21-rc.2", + "version": "0.4.21-rc.3", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 692daac916163..0d45ff49cfae6 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/ddp-streamer +## 0.3.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 + - @rocket.chat/instance-status@0.1.21-rc.3 +
+ ## 0.3.21-rc.2 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 3d23da747e85d..6285c52da65e9 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.21-rc.2", + "version": "0.3.21-rc.3", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 40dca780d9322..861be24a7d8fd 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/omnichannel-transcript +## 0.4.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/omnichannel-services@0.3.18-rc.3 + - @rocket.chat/pdf-worker@0.3.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
+ ## 0.4.21-rc.2 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 94ef481760bb8..31f3888ca8563 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.21-rc.2", + "version": "0.4.21-rc.3", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index ad1ac4c38eca8..63782dd340c60 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/presence-service +## 0.4.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/presence@0.2.21-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
+ ## 0.4.21-rc.2 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index da9d6a38b0844..ad0a34d259430 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.21-rc.2", + "version": "0.4.21-rc.3", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index e6669f5548fba..fd8e40f8a96f8 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/queue-worker +## 0.4.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/omnichannel-services@0.3.18-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
+ ## 0.4.21-rc.2 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index d263de9d37fdd..f4c542d490202 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.21-rc.2", + "version": "0.4.21-rc.3", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index 5f4a39ec01bcc..75100b1e945bc 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/stream-hub-service +## 0.4.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 + - @rocket.chat/network-broker@0.2.0-rc.3 +
+ ## 0.4.21-rc.2 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 311561baab79d..3989eff7d297d 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.21-rc.2", + "version": "0.4.21-rc.3", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 86038701ed546..7b772aec7dc6c 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 1.0.12-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 +
+ ## 1.0.12-rc.2 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 6af52de929518..8ce1ec884e769 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "1.0.12-rc.2", + "version": "1.0.12-rc.3", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/network-broker/CHANGELOG.md b/ee/packages/network-broker/CHANGELOG.md index 3d0639ef15d41..04416273f8069 100644 --- a/ee/packages/network-broker/CHANGELOG.md +++ b/ee/packages/network-broker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/network-broker +## 0.2.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.3 +
+ ## 0.2.0-rc.2 ### Patch Changes diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index 50dfd34316842..ece5b22d411a5 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/network-broker", - "version": "0.2.0-rc.2", + "version": "0.2.0-rc.3", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 27cbeda56ac26..14450b303c249 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.18-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/pdf-worker@0.3.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 +
+ ## 0.3.18-rc.2 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 191981120bd34..3a354cf9dfdb8 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.18-rc.2", + "version": "0.3.18-rc.3", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 1043cd53022b1..7a50625e8b7c8 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.3.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 +
+ ## 0.3.0-rc.2 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index 2244b0f0d6e73..c37254eeeab35 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.3.0-rc.2", + "version": "0.3.0-rc.3", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 01eca6709894a..f9acb4b1d15c6 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/core-services@0.9.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 +
+ ## 0.2.21-rc.2 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 1e900122bfb69..0996e7fabe28a 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.21-rc.2", + "version": "0.2.21-rc.3", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/package.json b/package.json index 62322a7d61f25..f7cf5dba4fc82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.6.0-rc.2", + "version": "7.6.0-rc.3", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 068b8634b12c6..39766a5495c46 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 +
+ ## 0.2.21-rc.2 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 5c757d5f10cd2..b64dd0f0e5d9a 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.21-rc.2", + "version": "0.2.21-rc.3", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index b8acd52b9f750..ec22a15ca6b82 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.5.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 +
+ ## 0.5.0-rc.2 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index 102ae2f68083c..988de671c4a57 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.5.0-rc.2", + "version": "0.5.0-rc.3", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index fb319a1fbef93..192541fb8e1a9 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/core-services +## 0.9.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 +
+ ## 0.9.0-rc.2 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index d8ca9112e8d03..8066eec55a6fc 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.9.0-rc.2", + "version": "0.9.0-rc.3", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index 2eb112a709fb2..7dbcea90dd01d 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,7 @@ # @rocket.chat/core-typings +## 7.6.0-rc.3 + ## 7.6.0-rc.2 ## 7.6.0-rc.1 diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 701a7507f9633..267e55ebb7c93 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.6.0-rc.2", + "version": "7.6.0-rc.3", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 14e1ce680526a..978dc1fe237af 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/models@1.5.0-rc.3 +
+ ## 0.1.21-rc.2 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index b99adef14d846..a181efa366bb8 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.21-rc.2", + "version": "0.1.21-rc.3", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index ee0d2d7bb606a..8da3cb3273542 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/api-client@0.2.21-rc.3 +
+ ## 0.3.21-rc.2 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index cc18b23c05c09..47257901c1208 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.21-rc.2", + "version": "0.3.21-rc.3", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/freeswitch/CHANGELOG.md b/packages/freeswitch/CHANGELOG.md index 5a32d81d9abd4..cd673bb3b4ca3 100644 --- a/packages/freeswitch/CHANGELOG.md +++ b/packages/freeswitch/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/freeswitch +## 1.2.8-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 +
+ ## 1.2.8-rc.2 ### Patch Changes diff --git a/packages/freeswitch/package.json b/packages/freeswitch/package.json index f2e5812975a2f..7497afcd1e914 100644 --- a/packages/freeswitch/package.json +++ b/packages/freeswitch/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/freeswitch", - "version": "1.2.8-rc.2", + "version": "1.2.8-rc.3", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index d9b7eb66a1e9a..fabed57938a5c 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 18.0.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/gazzodown@18.0.0-rc.3 + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 + - @rocket.chat/ui-video-conf@18.0.0-rc.3 +
+ ## 18.0.0-rc.2 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 9f0b64625c76b..751787d9300b6 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/fuselage-ui-kit", - "version": "18.0.0-rc.2", + "version": "18.0.0-rc.3", "private": true, "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index 19229def82ba8..f57071f0fb43e 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/gazzodown +## 18.0.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-client@18.0.0-rc.3 +
+ ## 18.0.0-rc.2 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index ba4f5e96d27c6..3e03c61bd0e48 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "18.0.0-rc.2", + "version": "18.0.0-rc.3", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index 15b7e1f9b0642..60cea4a6062ac 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.21-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.3 +
+ ## 0.1.21-rc.2 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 74a2607ac34c5..65eddc51a98bb 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.21-rc.2", + "version": "0.1.21-rc.3", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 327d3411e9494..1515ab11b8f35 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/livechat Change Log +## 1.22.8-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.3 +
+ ## 1.22.8-rc.2 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index e66380aeb1d22..80cda1ce94743 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.22.8-rc.2", + "version": "1.22.8-rc.3", "files": [ "/build" ], diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index 8ce85aabcc5b2..833242286ab58 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.2.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 +
+ ## 0.2.0-rc.2 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 96f0d043604ab..9459b14bc6455 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.2.0-rc.2", + "version": "0.2.0-rc.3", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index 0e2411fa6bea3..aed1d551083f3 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/model-typings +## 1.6.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 +
+ ## 1.6.0-rc.2 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 6742dddeff1d9..476a4f90d4c1f 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "1.6.0-rc.2", + "version": "1.6.0-rc.3", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index b99f73cb61ec1..12225e1fc5001 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/models +## 1.5.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/model-typings@1.6.0-rc.3 +
+ ## 1.5.0-rc.2 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index 4223155b7080b..baa9795961ec8 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "1.5.0-rc.2", + "version": "1.5.0-rc.3", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 6ee2961a5a5da..ba763846899f6 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/rest-typings +## 7.6.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 +
+ ## 7.6.0-rc.2 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index fd9e090619bcf..5e091d13e8be5 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.6.0-rc.2", + "version": "7.6.0-rc.3", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index abc0906577def..1fe93ed247f3b 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 14.0.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 +
+ ## 14.0.0-rc.2 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index dd995c12b1251..108059925a0a5 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "14.0.0-rc.2", + "version": "14.0.0-rc.3", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 3ea7ec75e8063..7eda92e451a02 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-client +## 18.0.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 +
+ ## 18.0.0-rc.2 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index e285029bbe7e0..72fd8d1e48fd5 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "18.0.0-rc.2", + "version": "18.0.0-rc.3", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index 3edc745cdd828..e0430df86ab84 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-contexts +## 18.0.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.3 + - @rocket.chat/rest-typings@7.6.0-rc.3 + - @rocket.chat/ddp-client@0.3.21-rc.3 +
+ ## 18.0.0-rc.2 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index ad0a092fb0df0..057e821d21037 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "18.0.0-rc.2", + "version": "18.0.0-rc.3", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index 4f3376e0097fd..654c52ea1bb89 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 18.0.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 +
+ ## 18.0.0-rc.2 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 9264b5e0a9316..b3246dd37a65f 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "18.0.0-rc.2", + "version": "18.0.0-rc.3", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-voip/CHANGELOG.md b/packages/ui-voip/CHANGELOG.md index d657134fb54d1..409ad404baa5e 100644 --- a/packages/ui-voip/CHANGELOG.md +++ b/packages/ui-voip/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-voip +## 8.0.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 + - @rocket.chat/ui-avatar@14.0.0-rc.3 + - @rocket.chat/ui-client@18.0.0-rc.3 +
+ ## 8.0.0-rc.2 ### Patch Changes diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index b27ab094a792f..41e7320412d27 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-voip", - "version": "8.0.0-rc.2", + "version": "8.0.0-rc.3", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index 2628163e51a76..ca3369218dd37 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 18.0.0-rc.3 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.3 +
+ ## 18.0.0-rc.2 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 1c08596493fce..c4a953df0fd2b 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "18.0.0-rc.2", + "version": "18.0.0-rc.3", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", From 06d14cf4ef30a3734810ffbacf0c6f7f68e90a31 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 30 Apr 2025 16:24:02 -0300 Subject: [PATCH 172/187] chore: prevent multiple listener.once `updateSubscription` (#35908) --- apps/meteor/client/lib/chats/readStateManager.ts | 6 ++++-- .../room/providers/hooks/useChatMessagesInstance.spec.ts | 1 + .../views/room/providers/hooks/useChatMessagesInstance.ts | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/lib/chats/readStateManager.ts b/apps/meteor/client/lib/chats/readStateManager.ts index 89b85c52401f9..fbb0c849e9680 100644 --- a/apps/meteor/client/lib/chats/readStateManager.ts +++ b/apps/meteor/client/lib/chats/readStateManager.ts @@ -38,6 +38,10 @@ export class ReadStateManager extends Emitter { return this.firstUnreadRecordId; }; + public subscribeToMessages() { + return RoomHistoryManager.on('loaded-messages', () => this.updateFirstUnreadRecordId()); + } + public updateSubscription(subscription?: ISubscription) { if (!subscription) { return; @@ -88,8 +92,6 @@ export class ReadStateManager extends Emitter { ); this.setFirstUnreadRecordId(firstUnreadRecord?._id); - - RoomHistoryManager.once('loaded-messages', () => this.updateFirstUnreadRecordId()); } private setFirstUnreadRecordId(firstUnreadRecordId: string | undefined) { diff --git a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.spec.ts b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.spec.ts index b63056ec4e228..40401bfb1b992 100644 --- a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.spec.ts +++ b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.spec.ts @@ -35,6 +35,7 @@ jest.mock('../../../../../app/ui/client/lib/ChatMessages', () => { release: jest.fn(), readStateManager: { updateSubscription: updateSubscriptionMock, + subscribeToMessages: jest.fn(), }, }; }), diff --git a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts index fbadf90e2947d..06bf65eebdf43 100644 --- a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts +++ b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts @@ -30,6 +30,12 @@ export function useChatMessagesInstance({ return [instance, () => instance.release()]; }, [rid, tmid, uid, encrypted, e2eRoomState]); + useEffect(() => { + if (subscription?.rid) { + return chatMessages?.readStateManager.subscribeToMessages(); + } + }, [subscription?.rid, chatMessages?.readStateManager]); + useEffect(() => { if (subscription) { chatMessages?.readStateManager.updateSubscription(subscription); From 06ffde55e707e44545437195e826dfc2a0e45c46 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Wed, 30 Apr 2025 20:49:29 +0000 Subject: [PATCH 173/187] Release 7.6.0-rc.4 [no ci] --- .changeset/bump-patch-1746046159552.md | 5 +++ .changeset/pre.json | 1 + apps/meteor/CHANGELOG.md | 35 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 ++++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 12 +++++++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 14 ++++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 14 ++++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 15 ++++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 15 ++++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 14 ++++++++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 14 ++++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 13 +++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 +++++ ee/packages/license/package.json | 2 +- ee/packages/network-broker/CHANGELOG.md | 9 +++++ ee/packages/network-broker/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 ++++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 9 +++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 ++++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 ++++++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 10 ++++++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 11 ++++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 2 ++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 ++++++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 ++++++ packages/ddp-client/package.json | 2 +- packages/freeswitch/CHANGELOG.md | 9 +++++ packages/freeswitch/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 13 +++++++ packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/CHANGELOG.md | 11 ++++++ packages/gazzodown/package.json | 2 +- packages/instance-status/CHANGELOG.md | 9 +++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 9 +++++ packages/livechat/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 +++++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 9 +++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 10 ++++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 9 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 +++++ packages/ui-avatar/package.json | 2 +- packages/ui-client/CHANGELOG.md | 10 ++++++ packages/ui-client/package.json | 2 +- packages/ui-contexts/CHANGELOG.md | 11 ++++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 10 ++++++ packages/ui-video-conf/package.json | 2 +- packages/ui-voip/CHANGELOG.md | 11 ++++++ packages/ui-voip/package.json | 2 +- packages/web-ui-registration/CHANGELOG.md | 9 +++++ packages/web-ui-registration/package.json | 2 +- 76 files changed, 458 insertions(+), 38 deletions(-) create mode 100644 .changeset/bump-patch-1746046159552.md diff --git a/.changeset/bump-patch-1746046159552.md b/.changeset/bump-patch-1746046159552.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746046159552.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/pre.json b/.changeset/pre.json index 3f9f1e6e7738f..fbd0755b7aa15 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -77,6 +77,7 @@ "bump-patch-1745354933374", "bump-patch-1745631711629", "bump-patch-1746034829149", + "bump-patch-1746046159552", "dirty-seas-explode", "eighty-wombats-smile", "eleven-laws-crash", diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 4f4412250081c..5f362cb922b66 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,40 @@ # @rocket.chat/meteor +## 7.6.0-rc.4 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/license@1.0.12-rc.4 + - @rocket.chat/omnichannel-services@0.3.18-rc.4 + - @rocket.chat/pdf-worker@0.3.0-rc.4 + - @rocket.chat/presence@0.2.21-rc.4 + - @rocket.chat/api-client@0.2.21-rc.4 + - @rocket.chat/apps@0.5.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/cron@0.1.21-rc.4 + - @rocket.chat/freeswitch@1.2.8-rc.4 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.4 + - @rocket.chat/gazzodown@18.0.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.4 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.4 + - @rocket.chat/ui-client@18.0.0-rc.4 + - @rocket.chat/ui-video-conf@18.0.0-rc.4 + - @rocket.chat/ui-voip@8.0.0-rc.4 + - @rocket.chat/web-ui-registration@18.0.0-rc.4 + - @rocket.chat/instance-status@0.1.21-rc.4 +
+ ## 7.6.0-rc.3 ### Patch Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 8515d75266419..657a1f5dd2c4f 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.6.0-rc.3" + "version": "7.6.0-rc.4" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 20bc4f6527473..a6285805c0277 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,19 @@ # rocketchat-services +## 2.0.12-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
+ ## 2.0.12-rc.3 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index de2803a284bfb..82f96d989f1fe 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "2.0.12-rc.3", + "version": "2.0.12-rc.4", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 96e0d4840cae8..08187cd027b4c 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.6.0-rc.3", + "version": "7.6.0-rc.4", "private": true, "type": "commonjs", "author": { diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index 0cb16a3563d93..3c263fe92e582 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/uikit-playground +## 0.6.12-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.4 + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-avatar@14.0.0-rc.4 +
+ ## 0.6.12-rc.3 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 2f979a885e987..6d366eede2923 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.6.12-rc.3", + "version": "0.6.12-rc.4", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index c00a4ff705065..a5ee24b694be2 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/account-service +## 0.4.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
+ ## 0.4.21-rc.3 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index f1be6fb10929e..e27b76fbc2d2f 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.21-rc.3", + "version": "0.4.21-rc.4", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index f9866db469817..3434f0b31d1ea 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/authorization-service +## 0.4.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
+ ## 0.4.21-rc.3 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 9c427b57f1001..904b3e3c9a425 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.21-rc.3", + "version": "0.4.21-rc.4", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 0d45ff49cfae6..5a8a1ea769fc5 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/ddp-streamer +## 0.3.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 + - @rocket.chat/instance-status@0.1.21-rc.4 +
+ ## 0.3.21-rc.3 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 6285c52da65e9..4320f6ebc607f 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.21-rc.3", + "version": "0.3.21-rc.4", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 861be24a7d8fd..011a06fce6e61 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/omnichannel-transcript +## 0.4.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/omnichannel-services@0.3.18-rc.4 + - @rocket.chat/pdf-worker@0.3.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
+ ## 0.4.21-rc.3 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 31f3888ca8563..36df36cedba79 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.21-rc.3", + "version": "0.4.21-rc.4", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 63782dd340c60..4f67dd55cbbb4 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/presence-service +## 0.4.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/presence@0.2.21-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
+ ## 0.4.21-rc.3 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index ad0a34d259430..b052c7bf40c8f 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.21-rc.3", + "version": "0.4.21-rc.4", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index fd8e40f8a96f8..ab32b529e6ef3 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/queue-worker +## 0.4.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/omnichannel-services@0.3.18-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
+ ## 0.4.21-rc.3 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index f4c542d490202..349db44645cde 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.21-rc.3", + "version": "0.4.21-rc.4", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index 75100b1e945bc..80a74780c66d8 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/stream-hub-service +## 0.4.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 + - @rocket.chat/network-broker@0.2.0-rc.4 +
+ ## 0.4.21-rc.3 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 3989eff7d297d..b3db6c95842d0 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.21-rc.3", + "version": "0.4.21-rc.4", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 7b772aec7dc6c..0157f8b746811 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 1.0.12-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 +
+ ## 1.0.12-rc.3 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 8ce1ec884e769..a3ea70c9f66ec 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "1.0.12-rc.3", + "version": "1.0.12-rc.4", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/network-broker/CHANGELOG.md b/ee/packages/network-broker/CHANGELOG.md index 04416273f8069..fba1abe9fac93 100644 --- a/ee/packages/network-broker/CHANGELOG.md +++ b/ee/packages/network-broker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/network-broker +## 0.2.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.4 +
+ ## 0.2.0-rc.3 ### Patch Changes diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index ece5b22d411a5..4cb9278931ddd 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/network-broker", - "version": "0.2.0-rc.3", + "version": "0.2.0-rc.4", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 14450b303c249..76947ad4cba34 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.18-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/pdf-worker@0.3.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 +
+ ## 0.3.18-rc.3 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 3a354cf9dfdb8..de7307bd952fc 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.18-rc.3", + "version": "0.3.18-rc.4", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 7a50625e8b7c8..6ba0989838bd3 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.3.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 +
+ ## 0.3.0-rc.3 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index c37254eeeab35..f87bd228e8ddf 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.3.0-rc.3", + "version": "0.3.0-rc.4", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index f9acb4b1d15c6..8cf1eddc7ec9f 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/core-services@0.9.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 +
+ ## 0.2.21-rc.3 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 0996e7fabe28a..d8107bd0b19e4 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.21-rc.3", + "version": "0.2.21-rc.4", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/package.json b/package.json index f7cf5dba4fc82..37845578ab6ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.6.0-rc.3", + "version": "7.6.0-rc.4", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 39766a5495c46..00f458e9f89d4 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 +
+ ## 0.2.21-rc.3 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index b64dd0f0e5d9a..4a5f8b4a0c952 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.21-rc.3", + "version": "0.2.21-rc.4", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index ec22a15ca6b82..90c18d3fe89fc 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.5.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 +
+ ## 0.5.0-rc.3 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index 988de671c4a57..d638b930ac0a2 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.5.0-rc.3", + "version": "0.5.0-rc.4", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index 192541fb8e1a9..b7cf60a755f15 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/core-services +## 0.9.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 +
+ ## 0.9.0-rc.3 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 8066eec55a6fc..9b749335cd533 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.9.0-rc.3", + "version": "0.9.0-rc.4", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index 7dbcea90dd01d..820da4ad97116 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,7 @@ # @rocket.chat/core-typings +## 7.6.0-rc.4 + ## 7.6.0-rc.3 ## 7.6.0-rc.2 diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 267e55ebb7c93..15b8c0026dae0 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.6.0-rc.3", + "version": "7.6.0-rc.4", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 978dc1fe237af..53e308a54175e 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/models@1.5.0-rc.4 +
+ ## 0.1.21-rc.3 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index a181efa366bb8..044a45123fced 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.21-rc.3", + "version": "0.1.21-rc.4", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index 8da3cb3273542..5e04afad2439b 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/api-client@0.2.21-rc.4 +
+ ## 0.3.21-rc.3 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 47257901c1208..4a4831d2d93f3 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.21-rc.3", + "version": "0.3.21-rc.4", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/freeswitch/CHANGELOG.md b/packages/freeswitch/CHANGELOG.md index cd673bb3b4ca3..20b97feb3447a 100644 --- a/packages/freeswitch/CHANGELOG.md +++ b/packages/freeswitch/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/freeswitch +## 1.2.8-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 +
+ ## 1.2.8-rc.3 ### Patch Changes diff --git a/packages/freeswitch/package.json b/packages/freeswitch/package.json index 7497afcd1e914..c298eb81e2c1f 100644 --- a/packages/freeswitch/package.json +++ b/packages/freeswitch/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/freeswitch", - "version": "1.2.8-rc.3", + "version": "1.2.8-rc.4", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index fabed57938a5c..04dd3a28eb896 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 18.0.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/gazzodown@18.0.0-rc.4 + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-avatar@14.0.0-rc.4 + - @rocket.chat/ui-video-conf@18.0.0-rc.4 +
+ ## 18.0.0-rc.3 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 751787d9300b6..ba61f1e919683 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/fuselage-ui-kit", - "version": "18.0.0-rc.3", + "version": "18.0.0-rc.4", "private": true, "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index f57071f0fb43e..122d61b9d673b 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/gazzodown +## 18.0.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-client@18.0.0-rc.4 +
+ ## 18.0.0-rc.3 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 3e03c61bd0e48..bd86953d066f0 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "18.0.0-rc.3", + "version": "18.0.0-rc.4", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index 60cea4a6062ac..5486def76f686 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.21-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.4 +
+ ## 0.1.21-rc.3 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 65eddc51a98bb..fe882f45f4515 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.21-rc.3", + "version": "0.1.21-rc.4", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 1515ab11b8f35..83bcd653c8e36 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/livechat Change Log +## 1.22.8-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.4 +
+ ## 1.22.8-rc.3 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 80cda1ce94743..d17c197c8b530 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.22.8-rc.3", + "version": "1.22.8-rc.4", "files": [ "/build" ], diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index 833242286ab58..aff0c3957a6dc 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.2.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 +
+ ## 0.2.0-rc.3 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 9459b14bc6455..f48016486b552 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.2.0-rc.3", + "version": "0.2.0-rc.4", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index aed1d551083f3..75ccf33e7df87 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/model-typings +## 1.6.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 +
+ ## 1.6.0-rc.3 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 476a4f90d4c1f..73283a161bb27 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "1.6.0-rc.3", + "version": "1.6.0-rc.4", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 12225e1fc5001..41b6b054cf6d0 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/models +## 1.5.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/model-typings@1.6.0-rc.4 +
+ ## 1.5.0-rc.3 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index baa9795961ec8..a2b236f96a703 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "1.5.0-rc.3", + "version": "1.5.0-rc.4", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index ba763846899f6..67eb7cdbcdfb3 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/rest-typings +## 7.6.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 +
+ ## 7.6.0-rc.3 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 5e091d13e8be5..a45ab9c996d85 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.6.0-rc.3", + "version": "7.6.0-rc.4", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index 1fe93ed247f3b..fb9c060f8e531 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 14.0.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 +
+ ## 14.0.0-rc.3 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 108059925a0a5..d7d4cc73d81ca 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "14.0.0-rc.3", + "version": "14.0.0-rc.4", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 7eda92e451a02..5a8d3e3e2bbea 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-client +## 18.0.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-avatar@14.0.0-rc.4 +
+ ## 18.0.0-rc.3 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 72fd8d1e48fd5..62882821d5a96 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "18.0.0-rc.3", + "version": "18.0.0-rc.4", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index e0430df86ab84..04d8ad6202a03 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-contexts +## 18.0.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.4 + - @rocket.chat/rest-typings@7.6.0-rc.4 + - @rocket.chat/ddp-client@0.3.21-rc.4 +
+ ## 18.0.0-rc.3 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 057e821d21037..3087c096deb89 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "18.0.0-rc.3", + "version": "18.0.0-rc.4", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index 654c52ea1bb89..9939e764d2e88 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 18.0.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-avatar@14.0.0-rc.4 +
+ ## 18.0.0-rc.3 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index b3246dd37a65f..c297d6f79ac08 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "18.0.0-rc.3", + "version": "18.0.0-rc.4", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-voip/CHANGELOG.md b/packages/ui-voip/CHANGELOG.md index 409ad404baa5e..0451d20b24c6e 100644 --- a/packages/ui-voip/CHANGELOG.md +++ b/packages/ui-voip/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-voip +## 8.0.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 + - @rocket.chat/ui-avatar@14.0.0-rc.4 + - @rocket.chat/ui-client@18.0.0-rc.4 +
+ ## 8.0.0-rc.3 ### Patch Changes diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 41e7320412d27..90e2db4b287b5 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-voip", - "version": "8.0.0-rc.3", + "version": "8.0.0-rc.4", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index ca3369218dd37..ace9b7898f5b2 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 18.0.0-rc.4 + +### Patch Changes + +-
Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.4 +
+ ## 18.0.0-rc.3 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index c4a953df0fd2b..d4e20ce16dd56 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "18.0.0-rc.3", + "version": "18.0.0-rc.4", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", From e7472b7601afb5b86dedf8c7150f13959b6e82e5 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 5 May 2025 17:01:30 -0300 Subject: [PATCH 174/187] regression: jump to message (#35918) --- .../ui-utils/client/lib/LegacyRoomManager.ts | 4 +- .../ui-utils/client/lib/RoomHistoryManager.ts | 20 ++- .../app/ui/client/views/app/lib/scrolling.ts | 17 +++ .../client/lib/utils/legacyJumpToMessage.ts | 3 +- .../MessageList/hooks/useJumpToMessage.ts | 39 +++++- .../hooks/useLoadSurroundingMessages.ts | 16 ++- .../providers/MessageListProvider.tsx | 3 - .../client/views/room/body/RoomBody.tsx | 21 ++- .../client/views/room/body/RoomBodyV2.tsx | 21 ++- .../views/room/body/hooks/useGetMore.spec.ts | 81 ------------ .../views/room/body/hooks/useGetMore.spec.tsx | 96 ++++++++++++++ .../views/room/body/hooks/useGetMore.ts | 121 ++++++++++++------ .../hooks/useRestoreScrollPosition.spec.ts | 85 ------------ .../hooks/useRestoreScrollPosition.spec.tsx | 87 +++++++++++++ .../body/hooks/useRestoreScrollPosition.ts | 70 +++++----- .../Threads/components/ThreadMessageList.tsx | 14 +- .../Threads/hooks/useLegacyThreadMessages.ts | 3 +- 17 files changed, 422 insertions(+), 279 deletions(-) delete mode 100644 apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts create mode 100644 apps/meteor/client/views/room/body/hooks/useGetMore.spec.tsx delete mode 100644 apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.ts create mode 100644 apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.tsx diff --git a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts index fa3445b6ad940..dd72504575509 100644 --- a/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/LegacyRoomManager.ts @@ -50,7 +50,7 @@ function close(typeName: string) { if (rid) { RoomManager.close(rid); - return RoomHistoryManager.clear(rid); + return RoomHistoryManager.close(rid); } } } @@ -93,8 +93,6 @@ const computation = Tracker.autorun(() => { const room = roomCoordinator.getRoomDirectives(type).findRoom(name); - void RoomHistoryManager.getMoreIfIsEmpty(record.rid); - if (room) { if (record.streamActive !== true) { void sdk diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index 9b8c29b086ea1..cd19f954ed534 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -122,6 +122,11 @@ class RoomHistoryManagerClass extends Emitter { return setTimeout(fn, 500 - difference); } + public isLoaded(rid: IRoom['_id']) { + const room = this.getRoom(rid); + return room.loaded !== undefined; + } + private unqueue() { const requestId = this.requestsList.pop(); if (!requestId) { @@ -286,10 +291,15 @@ class RoomHistoryManagerClass extends Emitter { return room.isLoading.get(); } + public close(rid: IRoom['_id']) { + Messages.remove({ rid }); + delete this.histories[rid]; + } + public clear(rid: IRoom['_id']) { const room = this.getRoom(rid); Messages.remove({ rid }); - room.isLoading.set(true); + room.isLoading.set(false); room.hasMore.set(true); room.hasMoreNext.set(false); room.oldestTs = undefined; @@ -308,15 +318,21 @@ class RoomHistoryManagerClass extends Emitter { } const room = this.getRoom(message.rid); - this.clear(message.rid); const subscription = Subscriptions.findOne({ rid: message.rid }); const result = await callWithErrorHandling('loadSurroundingMessages', message, defaultLimit); + this.clear(message.rid); + if (!result) { return; } + const { messages = [] } = result; + + if (messages.length > 0) { + room.oldestTs = messages[messages.length - 1].ts; + } await upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription }); diff --git a/apps/meteor/app/ui/client/views/app/lib/scrolling.ts b/apps/meteor/app/ui/client/views/app/lib/scrolling.ts index c3e95535281d4..c4f7afb7f4531 100644 --- a/apps/meteor/app/ui/client/views/app/lib/scrolling.ts +++ b/apps/meteor/app/ui/client/views/app/lib/scrolling.ts @@ -1,3 +1,20 @@ export function isAtBottom(element: HTMLElement, scrollThreshold = 0): boolean { return element.scrollTop + scrollThreshold >= element.scrollHeight - element.clientHeight; } + +// Mainly used for allow mock during testing + +export const getBoundingClientRect = (ref: HTMLElement) => { + const { top, bottom, left, right } = ref.getBoundingClientRect(); + const { scrollTop, scrollHeight, clientHeight } = ref; + + return { + top, + bottom, + left, + right, + scrollTop, + scrollHeight, + clientHeight, + }; +}; diff --git a/apps/meteor/client/lib/utils/legacyJumpToMessage.ts b/apps/meteor/client/lib/utils/legacyJumpToMessage.ts index b972f93bbbd52..8dc2e2fdde319 100644 --- a/apps/meteor/client/lib/utils/legacyJumpToMessage.ts +++ b/apps/meteor/client/lib/utils/legacyJumpToMessage.ts @@ -15,7 +15,6 @@ export const legacyJumpToMessage = async (message: IMessage) => { if (tab === 'thread' && (context === message.tmid || context === message._id)) { return; } - router.navigate( { name: router.getRouteName()!, @@ -32,6 +31,8 @@ export const legacyJumpToMessage = async (message: IMessage) => { }, { replace: false }, ); + await RoomHistoryManager.getSurroundingMessages(message); + return; } diff --git a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts index 85a27beeefc03..e1f55eaaab72b 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts @@ -1,13 +1,44 @@ import type { IMessage } from '@rocket.chat/core-typings'; +import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { useRouter } from '@rocket.chat/ui-contexts'; -import { useCallback } from 'react'; +import { useCallback, useRef } from 'react'; import { useMessageListJumpToMessageParam, useMessageListRef } from '../../../../components/message/list/MessageListContext'; import { useSafeRefCallback } from '../../../../hooks/useSafeRefCallback'; import { setRef } from '../../composer/hooks/useMessageComposerMergedRefs'; import { setHighlightMessage, clearHighlightMessage } from '../providers/messageHighlightSubscription'; -// this is an arbitrary value so that there's a gap between the header and the message; +/** + * That is completely messy, CustomScrollbars force us to initialize the scrollbars inside an effect + * all refCallbacks happen before the effect, more than that, the scrollbars also reset the scroll position + * so we need to check if the scrollbars are initialized and if there is any message to be highlighted + */ + +export const useJumpToMessageImperative = () => { + const jumpToRef = useRef(null); + const containerRef = useRef(null); + + const jumpToRefAction = useCallback(() => { + if (!jumpToRef.current || !containerRef.current) { + return; + } + jumpToRef.current.scrollIntoView({ + block: 'center', + }); + }, []); + + return { + jumpToRef: useMergedRefs(jumpToRef, jumpToRefAction), + innerRef: useMergedRefs(containerRef, jumpToRefAction), + }; +}; + +/** + * `listRef` is a reference to the message node in the message list. + * its shared between other hooks like `useLoadSurroundingMessages`, `useJumpToMessage`, `useGetMore`, `useListIsAtBottom` and `useRestoreScrollPosition` + * since each hook has a different concern, this ref helps each other aware if a message is being highlighted which changes the scroll position + + */ export const useJumpToMessage = (messageId: IMessage['_id']) => { const jumpToMessageParam = useMessageListJumpToMessageParam(); @@ -27,10 +58,6 @@ export const useJumpToMessage = (messageId: IMessage['_id']) => { setRef(listRef, node); - node.scrollIntoView({ - behavior: 'smooth', - block: 'center', - }); const handleScroll = () => { const { msg: _, ...search } = router.getSearchParameters(); router.navigate( diff --git a/apps/meteor/client/views/room/MessageList/hooks/useLoadSurroundingMessages.ts b/apps/meteor/client/views/room/MessageList/hooks/useLoadSurroundingMessages.ts index 7c6d3c6994734..678420171a1e7 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useLoadSurroundingMessages.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useLoadSurroundingMessages.ts @@ -1,11 +1,14 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useSearchParameter } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { legacyJumpToMessage } from '../../../../lib/utils/legacyJumpToMessage'; -export const useLoadSurroundingMessages = (msgId?: IMessage['_id']) => { +export const useLoadSurroundingMessages = () => { + const msgId = useSearchParameter('msg'); + const jumpToRef = useRef(undefined); + const queryClient = useQueryClient(); const getMessage = useEndpoint('GET', '/v1/chat.getMessage'); @@ -13,6 +16,11 @@ export const useLoadSurroundingMessages = (msgId?: IMessage['_id']) => { if (!msgId) { return; } + + if (jumpToRef.current) { + return; + } + const abort = new AbortController(); queryClient @@ -36,4 +44,6 @@ export const useLoadSurroundingMessages = (msgId?: IMessage['_id']) => { abort.abort(); }; }, [msgId, queryClient, getMessage]); + + return { jumpToRef }; }; diff --git a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx index 040806751cc81..b7f1dd8c3917f 100644 --- a/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx +++ b/apps/meteor/client/views/room/MessageList/providers/MessageListProvider.tsx @@ -11,7 +11,6 @@ import { useChat } from '../../contexts/ChatContext'; import { useRoom, useRoomSubscription } from '../../contexts/RoomContext'; import { useAutoTranslate } from '../hooks/useAutoTranslate'; import { useKatex } from '../hooks/useKatex'; -import { useLoadSurroundingMessages } from '../hooks/useLoadSurroundingMessages'; type MessageListProviderProps = { children: ReactNode; @@ -52,8 +51,6 @@ const MessageListProvider = ({ children, messageListRef, attachmentDimension }: const hasSubscription = Boolean(subscription); const msgParameter = useSearchParameter('msg'); - useLoadSurroundingMessages(msgParameter); - const chat = useChat(); const context: MessageListContextValue = useMemo( diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 6be98ae7592f4..2656064b9ed9f 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -36,6 +36,8 @@ import { useListIsAtBottom } from './hooks/useListIsAtBottom'; import { useQuoteMessageByUrl } from './hooks/useQuoteMessageByUrl'; import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; import { useHandleUnread } from './hooks/useUnreadMessages'; +import { useJumpToMessageImperative } from '../MessageList/hooks/useJumpToMessage'; +import { useLoadSurroundingMessages } from '../MessageList/hooks/useLoadSurroundingMessages'; const RoomBody = (): ReactElement => { const chat = useChat(); @@ -81,6 +83,10 @@ const RoomBody = (): ReactElement => { return subscribed; }, [allowAnonymousRead, canPreviewChannelRoom, room, subscribed]); + const { jumpToRef: jumpToRefGetMoreImperative, innerRef: jumpToRefGetMoreImperativeInnerRef } = useJumpToMessageImperative(); + + const { jumpToRef: surroundingMessagesJumpTpRef } = useLoadSurroundingMessages(); + const { wrapperRef: unreadBarWrapperRef, innerRef: unreadBarInnerRef, @@ -102,7 +108,15 @@ const RoomBody = (): ReactElement => { const { innerRef: getMoreInnerRef, jumpToRef: jumpToRefGetMore } = useGetMore(room._id, atBottomRef); - const jumpToRef = useMergedRefs(jumpToRefGetMore, jumpToRefIsAtBottom); + const { innerRef: restoreScrollPositionInnerRef, jumpToRef: jumpToRefRestoreScrollPosition } = useRestoreScrollPosition(room._id); + + const jumpToRef = useMergedRefs( + jumpToRefGetMore, + jumpToRefIsAtBottom, + jumpToRefRestoreScrollPosition, + surroundingMessagesJumpTpRef, + jumpToRefGetMoreImperative, + ); const { uploads, @@ -111,8 +125,6 @@ const RoomBody = (): ReactElement => { targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], } = useFileUpload(); - const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(); - const { messageListRef } = useMessageListNavigation(); const { innerRef: selectAndScrollRef, selectAllAndScrollToTop } = useSelectAllAndScrollToTop(); @@ -132,6 +144,7 @@ const RoomBody = (): ReactElement => { getMoreInnerRef, selectAndScrollRef, messageListRef, + jumpToRefGetMoreImperativeInnerRef, ); const wrapperBoxRefs = useMergedRefs(unreadBarWrapperRef); @@ -237,7 +250,7 @@ const RoomBody = (): ReactElement => { .join(' ')} > - +
    {canPreview ? ( <> diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 1b3c8cfb72300..54f24fd4c4d62 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -36,6 +36,8 @@ import { useListIsAtBottom } from './hooks/useListIsAtBottom'; import { useRestoreScrollPosition } from './hooks/useRestoreScrollPosition'; import { useSelectAllAndScrollToTop } from './hooks/useSelectAllAndScrollToTop'; import { useHandleUnread } from './hooks/useUnreadMessages'; +import { useJumpToMessageImperative } from '../MessageList/hooks/useJumpToMessage'; +import { useLoadSurroundingMessages } from '../MessageList/hooks/useLoadSurroundingMessages'; const RoomBody = (): ReactElement => { const chat = useChat(); @@ -81,6 +83,10 @@ const RoomBody = (): ReactElement => { return subscribed; }, [allowAnonymousRead, canPreviewChannelRoom, room, subscribed]); + const { jumpToRef: jumpToRefGetMoreImperative, innerRef: jumpToRefGetMoreImperativeInnerRef } = useJumpToMessageImperative(); + + const { jumpToRef: surroundingMessagesJumpTpRef } = useLoadSurroundingMessages(); + const { wrapperRef, innerRef: unreadBarInnerRef, @@ -102,7 +108,15 @@ const RoomBody = (): ReactElement => { const { innerRef: getMoreInnerRef, jumpToRef: jumpToRefGetMore } = useGetMore(room._id, atBottomRef); - const jumpToRef = useMergedRefs(jumpToRefIsAtBottom, jumpToRefGetMore); + const { innerRef: restoreScrollPositionInnerRef, jumpToRef: jumpToRefRestoreScrollPosition } = useRestoreScrollPosition(room._id); + + const jumpToRef = useMergedRefs( + jumpToRefIsAtBottom, + jumpToRefGetMore, + jumpToRefRestoreScrollPosition, + jumpToRefGetMoreImperative, + surroundingMessagesJumpTpRef, + ); const { uploads, @@ -111,8 +125,6 @@ const RoomBody = (): ReactElement => { targeDrop: [fileUploadTriggerProps, fileUploadOverlayProps], } = useFileUpload(); - const { innerRef: restoreScrollPositionInnerRef } = useRestoreScrollPosition(); - const { messageListRef } = useMessageListNavigation(); const { innerRef: selectAndScrollRef, selectAllAndScrollToTop } = useSelectAllAndScrollToTop(); @@ -132,6 +144,7 @@ const RoomBody = (): ReactElement => { getMoreInnerRef, selectAndScrollRef, messageListRef, + jumpToRefGetMoreImperativeInnerRef, ); const handleNavigateToPreviousMessage = useCallback((): void => { @@ -240,7 +253,7 @@ const RoomBody = (): ReactElement => { .join(' ')} > - +
      {canPreview ? ( <> diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts deleted file mode 100644 index e9e36dcd2d1da..0000000000000 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import React from 'react'; - -import { useGetMore } from './useGetMore'; -import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; - -jest.mock('../../../../../app/ui-utils/client', () => ({ - RoomHistoryManager: { - isLoading: jest.fn(), - hasMore: jest.fn(), - hasMoreNext: jest.fn(), - getMore: jest.fn(), - getMoreNext: jest.fn(), - restoreScroll: jest.fn(), - }, -})); - -const mockGetMore = jest.fn(); - -describe('useGetMore', () => { - it('should call getMore when scrolling near top and hasMore is true', () => { - (RoomHistoryManager.isLoading as jest.Mock).mockReturnValue(false); - (RoomHistoryManager.hasMore as jest.Mock).mockReturnValue(true); - (RoomHistoryManager.hasMoreNext as jest.Mock).mockReturnValue(false); - (RoomHistoryManager.getMore as jest.Mock).mockImplementation(mockGetMore); - const atBottomRef = { current: false }; - - const mockElement = { - addEventListener: jest.fn((event, handler) => { - if (event === 'scroll') { - handler({ - target: { - scrollTop: 10, - clientHeight: 300, - }, - }); - } - }), - removeEventListener: jest.fn(), - }; - - const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); - - const { unmount } = renderHook(() => useGetMore('room-id', atBottomRef)); - - expect(useRefSpy).toHaveBeenCalledWith(null); - expect(RoomHistoryManager.getMore).toHaveBeenCalledWith('room-id'); - - unmount(); - expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); - }); - - it('should call getMoreNext when scrolling near bottom and hasMoreNext is true', () => { - (RoomHistoryManager.isLoading as jest.Mock).mockReturnValue(false); - (RoomHistoryManager.hasMore as jest.Mock).mockReturnValue(false); - (RoomHistoryManager.hasMoreNext as jest.Mock).mockReturnValue(true); - (RoomHistoryManager.getMoreNext as jest.Mock).mockImplementation(mockGetMore); - - const atBottomRef = { current: false }; - const mockElement = { - addEventListener: jest.fn((event, handler) => { - if (event === 'scroll') { - handler({ - target: { - scrollTop: 600, - clientHeight: 300, - scrollHeight: 800, - }, - }); - } - }), - removeEventListener: jest.fn(), - }; - const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); - - renderHook(() => useGetMore('room-id', atBottomRef)); - - expect(useRefSpy).toHaveBeenCalledWith(null); - expect(RoomHistoryManager.getMoreNext).toHaveBeenCalledWith('room-id', atBottomRef); - }); -}); diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.spec.tsx b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.tsx new file mode 100644 index 0000000000000..5be7ae0494293 --- /dev/null +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.spec.tsx @@ -0,0 +1,96 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { useGetMore } from './useGetMore'; +import { getBoundingClientRect } from '../../../../../app/ui/client/views/app/lib/scrolling'; +import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; + +jest.mock('../../../../../app/ui-utils/client', () => ({ + RoomHistoryManager: { + isLoading: jest.fn(), + isLoadingNext: jest.fn(), + hasMore: jest.fn(), + hasMoreNext: jest.fn(), + getMore: jest.fn(), + getMoreNext: jest.fn(), + restoreScroll: jest.fn(), + }, +})); + +jest.mock('../../../../../app/ui/client/views/app/lib/scrolling', () => ({ + getBoundingClientRect: jest.fn(), +})); + +const mockGetMore = jest.fn(); + +describe('useGetMore', () => { + it('should call getMore when scrolling near top and hasMore is true', async () => { + const root = mockAppRoot(); + + const Test = () => { + const atBottomRef = React.useRef(false); + const { innerRef } = useGetMore('room-id', atBottomRef); + return ( +
      +
      +
      + ); + }; + (RoomHistoryManager.isLoading as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.hasMore as jest.Mock).mockReturnValue(true); + (RoomHistoryManager.hasMoreNext as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.getMore as jest.Mock).mockImplementation(mockGetMore); + + (getBoundingClientRect as jest.Mock).mockReturnValue({ + scrollTop: 10, + clientHeight: 100, + scrollHeight: 800, + }); + + render(, { + wrapper: root.build(), + }); + + const scrollableElement = screen.getByTestId('scrollable-element'); + scrollableElement.scrollTop = 10; + scrollableElement.dispatchEvent(new Event('scroll')); + + expect(screen.getByTestId('scrollable-element')).toBeInTheDocument(); + + await waitFor(() => { + expect(RoomHistoryManager.getMore).toHaveBeenCalledWith('room-id'); + }); + }); + + it('should call getMoreNext when scrolling near bottom and hasMoreNext is true', () => { + const root = mockAppRoot(); + (RoomHistoryManager.isLoading as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.hasMore as jest.Mock).mockReturnValue(false); + (RoomHistoryManager.hasMoreNext as jest.Mock).mockReturnValue(true); + (RoomHistoryManager.getMoreNext as jest.Mock).mockImplementation(mockGetMore); + + const Test = () => { + const atBottomRef = React.useRef(false); + const { innerRef } = useGetMore('room-id', atBottomRef); + return ( +
      +
      +
      + ); + }; + (getBoundingClientRect as jest.Mock).mockReturnValue({ + scrollTop: 700, + clientHeight: 100, + scrollHeight: 800, + }); + render(, { + wrapper: root.build(), + }); + const scrollableElement = screen.getByTestId('scrollable-element'); + scrollableElement.scrollTop = 700; + scrollableElement.dispatchEvent(new Event('scroll')); + expect(screen.getByTestId('scrollable-element')).toBeInTheDocument(); + expect(RoomHistoryManager.getMoreNext).toHaveBeenCalledWith('room-id', expect.anything()); + }); +}); diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.ts index 5c57bf12358df..ccab8fb4d5dc3 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.ts @@ -1,61 +1,100 @@ +import { useSafeRefCallback } from '@rocket.chat/ui-client'; +import { useSearchParameter } from '@rocket.chat/ui-contexts'; import type { MutableRefObject } from 'react'; -import { useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { flushSync } from 'react-dom'; +import { getBoundingClientRect } from '../../../../../app/ui/client/views/app/lib/scrolling'; import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; export const useGetMore = (rid: string, atBottomRef: MutableRefObject) => { - const ref = useRef(null); - + const msgId = useSearchParameter('msg'); + const msgIdRef = useRef(msgId); const jumpToRef = useRef(undefined); useEffect(() => { - if (!ref.current) { - return; - } + msgIdRef.current = msgId; + }, [msgId]); - const refValue = ref.current; + const ref = useSafeRefCallback( + useCallback( + (element: HTMLElement | null) => { + if (!element) { + return; + } + const checkPositionAndGetMore = withThrottling({ wait: 100 })(async () => { + if (!element.isConnected) { + return; + } - const handleScroll = withThrottling({ wait: 300 })(async (event) => { - const lastScrollTopRef = event.target.scrollTop; - const height = event.target.clientHeight; - const isLoading = RoomHistoryManager.isLoading(rid); - const hasMore = RoomHistoryManager.hasMore(rid); - const hasMoreNext = RoomHistoryManager.hasMoreNext(rid); + const { scrollTop, clientHeight, scrollHeight } = getBoundingClientRect(element); - if (!((isLoading === false && hasMore === true) || hasMoreNext === true)) { - return; - } + if (msgIdRef.current && !RoomHistoryManager.isLoaded(rid)) { + return; + } - if (jumpToRef.current) { - return; - } + const lastScrollTopRef = scrollTop; + const height = clientHeight; + const isLoading = RoomHistoryManager.isLoading(rid); + const hasMore = RoomHistoryManager.hasMore(rid); + const hasMoreNext = RoomHistoryManager.hasMoreNext(rid); - if (hasMore === true && lastScrollTopRef <= height / 3) { - await RoomHistoryManager.getMore(rid); + if (jumpToRef.current) { + return; + } - if (jumpToRef.current) { - return; - } - flushSync(() => { - RoomHistoryManager.restoreScroll(rid); + if (isLoading) { + return; + } + + if (hasMore === true && lastScrollTopRef <= height / 3) { + await RoomHistoryManager.getMore(rid); + + if (jumpToRef.current) { + return; + } + flushSync(() => { + RoomHistoryManager.restoreScroll(rid); + }); + } else if (hasMoreNext === true && Math.ceil(lastScrollTopRef) >= scrollHeight - height) { + await RoomHistoryManager.getMoreNext(rid, atBottomRef); + atBottomRef.current = false; + } + }); + + const mutationObserver = new MutationObserver((mutations) => { + mutations.forEach(() => { + checkPositionAndGetMore(); + }); }); - } else if (hasMoreNext === true && Math.ceil(lastScrollTopRef) >= event.target.scrollHeight - height) { - RoomHistoryManager.getMoreNext(rid, atBottomRef); - atBottomRef.current = false; - } - }); - - refValue.addEventListener('scroll', handleScroll, { - passive: true, - }); - - return () => { - handleScroll.cancel(); - refValue.removeEventListener('scroll', handleScroll); - }; - }, [rid, atBottomRef]); + + mutationObserver.observe(element, { childList: true, subtree: true }); + + const observer = new ResizeObserver(() => { + checkPositionAndGetMore(); + }); + + observer.observe(element); + + const handleScroll = function () { + checkPositionAndGetMore(); + }; + + element.addEventListener('scroll', handleScroll, { + passive: true, + }); + + return () => { + observer.disconnect(); + mutationObserver.disconnect(); + checkPositionAndGetMore.cancel(); + element.removeEventListener('scroll', handleScroll); + }; + }, + [rid, atBottomRef], + ), + ); return { innerRef: ref, diff --git a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.ts b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.ts deleted file mode 100644 index 30bfd0322008e..0000000000000 --- a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import React from 'react'; - -import { useRestoreScrollPosition } from './useRestoreScrollPosition'; -import { RoomManager } from '../../../../lib/RoomManager'; - -jest.mock('../../../../lib/RoomManager', () => ({ - RoomManager: { - getStore: jest.fn(), - }, - useOpenedRoom: jest.fn(() => 'room-id'), - useSecondLevelOpenedRoom: jest.fn(() => 'room-id'), -})); - -describe('useRestoreScrollPosition', () => { - it('should restore room scroll position based on store', () => { - (RoomManager.getStore as jest.Mock).mockReturnValue({ scroll: 100, atBottom: false }); - - const mockElement = { - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - }; - - const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); - - const { unmount } = renderHook(() => useRestoreScrollPosition()); - - expect(useRefSpy).toHaveBeenCalledWith(null); - expect(mockElement).toHaveProperty('scrollTop', 100); - expect(mockElement).toHaveProperty('scrollLeft', 30); - - unmount(); - expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); - }); - - it('should do nothing if no previous scroll position is stored', () => { - (RoomManager.getStore as jest.Mock).mockReturnValue({ atBottom: true }); - - const mockElement = { - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - scrollHeight: 800, - scrollTop: 123, - }; - - const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); - - const { unmount } = renderHook(() => useRestoreScrollPosition()); - - expect(useRefSpy).toHaveBeenCalledWith(null); - expect(mockElement).toHaveProperty('scrollTop', 123); - expect(mockElement).not.toHaveProperty('scrollLeft'); - - unmount(); - expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); - }); - - it('should update store based on scroll position', () => { - const update = jest.fn(); - (RoomManager.getStore as jest.Mock).mockReturnValue({ update }); - - const mockElement = { - addEventListener: jest.fn((event, handler) => { - if (event === 'scroll') { - handler({ - target: { - scrollTop: 500, - }, - }); - } - }), - removeEventListener: jest.fn(), - }; - - const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: mockElement }); - - const { unmount } = renderHook(() => useRestoreScrollPosition()); - - expect(useRefSpy).toHaveBeenCalledWith(null); - expect(update).toHaveBeenCalledWith({ scroll: 500, atBottom: false }); - - unmount(); - expect(mockElement.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function)); - }); -}); diff --git a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.tsx b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.tsx new file mode 100644 index 0000000000000..fbf95db89753a --- /dev/null +++ b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.spec.tsx @@ -0,0 +1,87 @@ +import { render, screen, waitFor } from '@testing-library/react'; + +import { useRestoreScrollPosition } from './useRestoreScrollPosition'; +import { RoomManager } from '../../../../lib/RoomManager'; + +jest.mock('../../../../lib/RoomManager', () => ({ + RoomManager: { + getStore: jest.fn(), + }, + useOpenedRoom: jest.fn(() => 'room-id'), + useSecondLevelOpenedRoom: jest.fn(() => 'room-id'), +})); + +describe('useRestoreScrollPosition', () => { + it('should restore room scroll position based on store', () => { + const store = { + scroll: 123, + atBottom: false, + update: jest.fn(), + }; + (RoomManager.getStore as jest.Mock).mockReturnValue(store); + + const Test = () => { + const { innerRef } = useRestoreScrollPosition('GENERAL'); + return ( +
      +
      +
      + ); + }; + + render(); + + expect(screen.getByTestId('scrollable-element')).toBeInTheDocument(); + expect(screen.getByTestId('scrollable-element')).toHaveStyle({ height: '100px', overflowY: 'scroll' }); + expect(screen.getByTestId('scrollable-element')).toHaveProperty('scrollTop', 123); + }); + + it('should do nothing if no previous scroll position is stored', () => { + const store = { + scroll: undefined, + atBottom: false, + update: jest.fn(), + }; + + (RoomManager.getStore as jest.Mock).mockReturnValue(store); + const Test = () => { + const { innerRef } = useRestoreScrollPosition('GENERAL'); + return ( +
      +
      +
      + ); + }; + + (RoomManager.getStore as jest.Mock).mockReturnValue({ scroll: undefined }); + render(); + expect(screen.getByTestId('scrollable-element')).toBeInTheDocument(); + expect(screen.getByTestId('scrollable-element')).toHaveStyle({ height: '100px', overflowY: 'scroll' }); + expect(screen.getByTestId('scrollable-element')).toHaveProperty('scrollTop', 0); + }); + + it('should update store based on scroll position', async () => { + const store = { + scroll: 1, + atBottom: false, + update: jest.fn(), + }; + (RoomManager.getStore as jest.Mock).mockReturnValue(store); + const Test = () => { + const { innerRef } = useRestoreScrollPosition('GENERAL', 0); + return ( +
      +
      +
      + ); + }; + render(); + const scrollableElement = screen.getByTestId('scrollable-element'); + scrollableElement.scrollTop = 50; + scrollableElement.dispatchEvent(new Event('scroll')); + + await waitFor(() => { + expect(store.update).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts index f4a54699d3625..10bf933ab30d4 100644 --- a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts +++ b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts @@ -1,49 +1,37 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useSafeRefCallback } from '@rocket.chat/ui-client'; +import { useCallback, useRef } from 'react'; import { isAtBottom } from '../../../../../app/ui/client/views/app/lib/scrolling'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; -import { RoomManager, useOpenedRoom, useSecondLevelOpenedRoom } from '../../../../lib/RoomManager'; - -export function useRestoreScrollPosition() { - const ref = useRef(null); - const parentRoomId = useOpenedRoom(); - const roomId = useSecondLevelOpenedRoom() ?? parentRoomId; - - const handleRestoreScroll = useCallback(() => { - if (!ref.current || !roomId) { - return; - } - - const store = RoomManager.getStore(roomId); - - if (store?.scroll !== undefined && !store.atBottom) { - ref.current.scrollTop = store.scroll; - ref.current.scrollLeft = 30; - } - }, [roomId]); - - useEffect(() => { - if (!ref.current || !roomId) { - return; - } - - handleRestoreScroll(); - - const refValue = ref.current; - const store = RoomManager.getStore(roomId); - - const handleWrapperScroll = withThrottling({ wait: 100 })((event) => { - store?.update({ scroll: event.target.scrollTop, atBottom: isAtBottom(event.target, 50) }); - }); - - refValue.addEventListener('scroll', handleWrapperScroll, { passive: true }); - - return () => { - refValue.removeEventListener('scroll', handleWrapperScroll); - }; - }, [roomId, handleRestoreScroll]); +import { RoomManager } from '../../../../lib/RoomManager'; + +export function useRestoreScrollPosition(rid: string, wait = 100) { + const jumpToRef = useRef(undefined); + const ref = useSafeRefCallback( + useCallback( + (node: HTMLElement | null) => { + if (!node) { + return; + } + const store = RoomManager.getStore(rid); + if (!jumpToRef.current && store?.scroll !== undefined && !store.atBottom) { + node.scrollTop = store.scroll; + node.scrollLeft = 30; + } + const handleWrapperScroll = withThrottling({ wait })((event) => { + store?.update({ scroll: event.target.scrollTop, atBottom: isAtBottom(event.target, 50) }); + }); + node.addEventListener('scroll', handleWrapperScroll, { passive: true }); + return () => { + node.removeEventListener('scroll', handleWrapperScroll); + }; + }, + [rid, wait], + ), + ); return { + jumpToRef, innerRef: ref, }; } 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 208065e4beab8..04eac38276040 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx +++ b/apps/meteor/client/views/room/contextualBar/Threads/components/ThreadMessageList.tsx @@ -1,8 +1,9 @@ import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; +import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; import { differenceInSeconds } from 'date-fns'; -import type { ReactElement, RefCallback } from 'react'; +import type { ReactElement } from 'react'; import { Fragment } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,6 +12,7 @@ import { MessageTypes } from '../../../../../../app/ui-utils/client'; import { isTruthy } from '../../../../../../lib/isTruthy'; import { CustomScrollbars } from '../../../../../components/CustomScrollbars'; import { BubbleDate } from '../../../BubbleDate'; +import { useJumpToMessageImperative } from '../../../MessageList/hooks/useJumpToMessage'; import { isMessageNewDay } from '../../../MessageList/lib/isMessageNewDay'; import MessageListProvider from '../../../MessageList/providers/MessageListProvider'; import LoadingMessagesIndicator from '../../../body/LoadingMessagesIndicator'; @@ -56,6 +58,10 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen const { innerRef: listScrollRef, jumpToRef } = useLegacyThreadMessageListScrolling(mainMessage); + const { jumpToRef: jumpToRefGetMoreImperative, innerRef: jumpToRefGetMoreImperativeInnerRef } = useJumpToMessageImperative(); + + const customScrollbarsRef = useMergedRefs(listScrollRef, jumpToRefGetMoreImperativeInnerRef); + const hideUsernames = useUserPreference('hideUsernames'); const showUserAvatar = !!useUserPreference('displayAvatars'); const firstUnreadMessageId = useFirstUnreadMessageId(); @@ -63,10 +69,12 @@ const ThreadMessageList = ({ mainMessage }: ThreadMessageListProps): ReactElemen const { messageListRef } = useMessageListNavigation(); + const jumpToRefMessageListProvider = useMergedRefs(jumpToRef, jumpToRefGetMoreImperative); + return (
      - + ) : ( - }> + {[mainMessage, ...messages].map((message, index, { [index - 1]: previous }) => { const sequential = isMessageSequential(message, previous, messageGroupingPeriod); const newDay = isMessageNewDay(message, previous); diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts index 6545d85de16c2..ba0a18e69fe1f 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useLegacyThreadMessages.ts @@ -36,12 +36,11 @@ export const useLegacyThreadMessages = ( }, [tmid]), ); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(messages.length === 0); const getThreadMessages = useMethod('getThreadMessages'); useEffect(() => { - setLoading(true); getThreadMessages({ tmid }).then((messages) => { upsertMessageBulk({ msgs: messages }, Messages); setLoading(false); From dd39c93964525205f91d9722b91a3e48a5d6375d Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Mon, 5 May 2025 20:11:26 +0000 Subject: [PATCH 175/187] Release 7.6.0-rc.5 [no ci] --- .changeset/bump-patch-1746475877217.md | 5 +++ .changeset/pre.json | 1 + apps/meteor/CHANGELOG.md | 35 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 ++++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 12 +++++++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 14 ++++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 14 ++++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 15 ++++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 15 ++++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 14 ++++++++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 14 ++++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 13 +++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 +++++ ee/packages/license/package.json | 2 +- ee/packages/network-broker/CHANGELOG.md | 9 +++++ ee/packages/network-broker/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 ++++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 9 +++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 ++++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 ++++++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 10 ++++++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 11 ++++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 2 ++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 ++++++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 ++++++ packages/ddp-client/package.json | 2 +- packages/freeswitch/CHANGELOG.md | 9 +++++ packages/freeswitch/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 13 +++++++ packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/CHANGELOG.md | 11 ++++++ packages/gazzodown/package.json | 2 +- packages/instance-status/CHANGELOG.md | 9 +++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 9 +++++ packages/livechat/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 +++++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 9 +++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 10 ++++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 9 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 +++++ packages/ui-avatar/package.json | 2 +- packages/ui-client/CHANGELOG.md | 10 ++++++ packages/ui-client/package.json | 2 +- packages/ui-contexts/CHANGELOG.md | 11 ++++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 10 ++++++ packages/ui-video-conf/package.json | 2 +- packages/ui-voip/CHANGELOG.md | 11 ++++++ packages/ui-voip/package.json | 2 +- packages/web-ui-registration/CHANGELOG.md | 9 +++++ packages/web-ui-registration/package.json | 2 +- 76 files changed, 458 insertions(+), 38 deletions(-) create mode 100644 .changeset/bump-patch-1746475877217.md diff --git a/.changeset/bump-patch-1746475877217.md b/.changeset/bump-patch-1746475877217.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746475877217.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/pre.json b/.changeset/pre.json index fbd0755b7aa15..5a38c95669b2d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -78,6 +78,7 @@ "bump-patch-1745631711629", "bump-patch-1746034829149", "bump-patch-1746046159552", + "bump-patch-1746475877217", "dirty-seas-explode", "eighty-wombats-smile", "eleven-laws-crash", diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 5f362cb922b66..a5420c0e2c847 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,40 @@ # @rocket.chat/meteor +## 7.6.0-rc.5 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/license@1.0.12-rc.5 + - @rocket.chat/omnichannel-services@0.3.18-rc.5 + - @rocket.chat/pdf-worker@0.3.0-rc.5 + - @rocket.chat/presence@0.2.21-rc.5 + - @rocket.chat/api-client@0.2.21-rc.5 + - @rocket.chat/apps@0.5.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/cron@0.1.21-rc.5 + - @rocket.chat/freeswitch@1.2.8-rc.5 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.5 + - @rocket.chat/gazzodown@18.0.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.5 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.5 + - @rocket.chat/ui-client@18.0.0-rc.5 + - @rocket.chat/ui-video-conf@18.0.0-rc.5 + - @rocket.chat/ui-voip@8.0.0-rc.5 + - @rocket.chat/web-ui-registration@18.0.0-rc.5 + - @rocket.chat/instance-status@0.1.21-rc.5 +
      + ## 7.6.0-rc.4 ### Patch Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 657a1f5dd2c4f..a8f04613760d6 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.6.0-rc.4" + "version": "7.6.0-rc.5" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index a6285805c0277..78427c4108961 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,19 @@ # rocketchat-services +## 2.0.12-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + ## 2.0.12-rc.4 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 82f96d989f1fe..a4de2ed873e9c 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "2.0.12-rc.4", + "version": "2.0.12-rc.5", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 08187cd027b4c..08b9ccf77a499 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.6.0-rc.4", + "version": "7.6.0-rc.5", "private": true, "type": "commonjs", "author": { diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index 3c263fe92e582..492d2479dc321 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/uikit-playground +## 0.6.12-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.5 + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-avatar@14.0.0-rc.5 +
      + ## 0.6.12-rc.4 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 6d366eede2923..28527b1fbdc1a 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.6.12-rc.4", + "version": "0.6.12-rc.5", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index a5ee24b694be2..15eaa73bfd84a 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/account-service +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + ## 0.4.21-rc.4 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index e27b76fbc2d2f..e261626389608 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.21-rc.4", + "version": "0.4.21-rc.5", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 3434f0b31d1ea..8a7070236f4c6 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/authorization-service +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + ## 0.4.21-rc.4 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 904b3e3c9a425..df88a2f9b6491 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.21-rc.4", + "version": "0.4.21-rc.5", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 5a8a1ea769fc5..b8d60e5ab8d21 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/ddp-streamer +## 0.3.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 + - @rocket.chat/instance-status@0.1.21-rc.5 +
      + ## 0.3.21-rc.4 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 4320f6ebc607f..41bb8a7911157 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.21-rc.4", + "version": "0.3.21-rc.5", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 011a06fce6e61..b3e8d7cfacf8a 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/omnichannel-transcript +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/omnichannel-services@0.3.18-rc.5 + - @rocket.chat/pdf-worker@0.3.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + ## 0.4.21-rc.4 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 36df36cedba79..6d4c7d85bbdc9 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.21-rc.4", + "version": "0.4.21-rc.5", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 4f67dd55cbbb4..13867792c410a 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/presence-service +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/presence@0.2.21-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + ## 0.4.21-rc.4 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index b052c7bf40c8f..760578b613856 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.21-rc.4", + "version": "0.4.21-rc.5", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index ab32b529e6ef3..dee663966be3d 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/queue-worker +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/omnichannel-services@0.3.18-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + ## 0.4.21-rc.4 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 349db44645cde..f8cf9960cb5bb 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.21-rc.4", + "version": "0.4.21-rc.5", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index 80a74780c66d8..e0a3b1ba15aaf 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/stream-hub-service +## 0.4.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 + - @rocket.chat/network-broker@0.2.0-rc.5 +
      + ## 0.4.21-rc.4 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index b3db6c95842d0..a9303849a806a 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.21-rc.4", + "version": "0.4.21-rc.5", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 0157f8b746811..170f2c2e631dc 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 1.0.12-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 +
      + ## 1.0.12-rc.4 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index a3ea70c9f66ec..c53b782ee3620 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "1.0.12-rc.4", + "version": "1.0.12-rc.5", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/network-broker/CHANGELOG.md b/ee/packages/network-broker/CHANGELOG.md index fba1abe9fac93..8fe0e347861bd 100644 --- a/ee/packages/network-broker/CHANGELOG.md +++ b/ee/packages/network-broker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/network-broker +## 0.2.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.5 +
      + ## 0.2.0-rc.4 ### Patch Changes diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index 4cb9278931ddd..1c0325a68dd08 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/network-broker", - "version": "0.2.0-rc.4", + "version": "0.2.0-rc.5", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 76947ad4cba34..fbdd9e7521896 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.18-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/pdf-worker@0.3.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 +
      + ## 0.3.18-rc.4 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index de7307bd952fc..9bd1899a160c2 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.18-rc.4", + "version": "0.3.18-rc.5", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 6ba0989838bd3..3e33a7b8d427b 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.3.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 +
      + ## 0.3.0-rc.4 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index f87bd228e8ddf..fd8abfacfc304 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.3.0-rc.4", + "version": "0.3.0-rc.5", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 8cf1eddc7ec9f..7287c3394aca3 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/core-services@0.9.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 +
      + ## 0.2.21-rc.4 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index d8107bd0b19e4..ae41f6017a786 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.21-rc.4", + "version": "0.2.21-rc.5", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/package.json b/package.json index 37845578ab6ef..9a557bc816b28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.6.0-rc.4", + "version": "7.6.0-rc.5", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 00f458e9f89d4..ebc50ad66b015 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 +
      + ## 0.2.21-rc.4 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 4a5f8b4a0c952..b4176ea1bd7ee 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.21-rc.4", + "version": "0.2.21-rc.5", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index 90c18d3fe89fc..5a51aa4d7f162 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.5.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 +
      + ## 0.5.0-rc.4 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index d638b930ac0a2..16b475e440e58 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.5.0-rc.4", + "version": "0.5.0-rc.5", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index b7cf60a755f15..758c6cbb6e9f8 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/core-services +## 0.9.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 +
      + ## 0.9.0-rc.4 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 9b749335cd533..98e694cf9fecd 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.9.0-rc.4", + "version": "0.9.0-rc.5", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index 820da4ad97116..7b855af9f8175 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,7 @@ # @rocket.chat/core-typings +## 7.6.0-rc.5 + ## 7.6.0-rc.4 ## 7.6.0-rc.3 diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 15b8c0026dae0..05bfb4acf5d40 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.6.0-rc.4", + "version": "7.6.0-rc.5", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 53e308a54175e..58465fe5054e1 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/models@1.5.0-rc.5 +
      + ## 0.1.21-rc.4 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index 044a45123fced..49b97d2d48252 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.21-rc.4", + "version": "0.1.21-rc.5", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index 5e04afad2439b..f0b37101a1317 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/api-client@0.2.21-rc.5 +
      + ## 0.3.21-rc.4 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 4a4831d2d93f3..536769656158b 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.21-rc.4", + "version": "0.3.21-rc.5", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/freeswitch/CHANGELOG.md b/packages/freeswitch/CHANGELOG.md index 20b97feb3447a..58ffd5a13608a 100644 --- a/packages/freeswitch/CHANGELOG.md +++ b/packages/freeswitch/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/freeswitch +## 1.2.8-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 +
      + ## 1.2.8-rc.4 ### Patch Changes diff --git a/packages/freeswitch/package.json b/packages/freeswitch/package.json index c298eb81e2c1f..899a316091486 100644 --- a/packages/freeswitch/package.json +++ b/packages/freeswitch/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/freeswitch", - "version": "1.2.8-rc.4", + "version": "1.2.8-rc.5", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 04dd3a28eb896..a8f91bd12965d 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/gazzodown@18.0.0-rc.5 + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-avatar@14.0.0-rc.5 + - @rocket.chat/ui-video-conf@18.0.0-rc.5 +
      + ## 18.0.0-rc.4 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index ba61f1e919683..17139c4dcfe88 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/fuselage-ui-kit", - "version": "18.0.0-rc.4", + "version": "18.0.0-rc.5", "private": true, "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index 122d61b9d673b..cd4d4301c888f 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/gazzodown +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-client@18.0.0-rc.5 +
      + ## 18.0.0-rc.4 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index bd86953d066f0..ddbf9b09312ec 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "18.0.0-rc.4", + "version": "18.0.0-rc.5", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index 5486def76f686..7f40e2722c7cd 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.21-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.5 +
      + ## 0.1.21-rc.4 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index fe882f45f4515..e5fe463e9d574 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.21-rc.4", + "version": "0.1.21-rc.5", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 83bcd653c8e36..48e7b7d07e0dd 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/livechat Change Log +## 1.22.8-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.5 +
      + ## 1.22.8-rc.4 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index d17c197c8b530..af4da5bb48954 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.22.8-rc.4", + "version": "1.22.8-rc.5", "files": [ "/build" ], diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index aff0c3957a6dc..942ae56ff02aa 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.2.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 +
      + ## 0.2.0-rc.4 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index f48016486b552..71cb3b8be2c67 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.2.0-rc.4", + "version": "0.2.0-rc.5", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index 75ccf33e7df87..ae1208af6c6b0 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/model-typings +## 1.6.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 +
      + ## 1.6.0-rc.4 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 73283a161bb27..dc2b1c9cdae90 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "1.6.0-rc.4", + "version": "1.6.0-rc.5", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 41b6b054cf6d0..d29649d597730 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/models +## 1.5.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/model-typings@1.6.0-rc.5 +
      + ## 1.5.0-rc.4 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index a2b236f96a703..f2ccc669d37ed 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "1.5.0-rc.4", + "version": "1.5.0-rc.5", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 67eb7cdbcdfb3..5355aa4ea6530 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/rest-typings +## 7.6.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 +
      + ## 7.6.0-rc.4 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index a45ab9c996d85..807d1e4b38304 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.6.0-rc.4", + "version": "7.6.0-rc.5", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index fb9c060f8e531..20e7b500a5dbf 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 14.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 +
      + ## 14.0.0-rc.4 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index d7d4cc73d81ca..446a58c6d0dc3 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "14.0.0-rc.4", + "version": "14.0.0-rc.5", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 5a8d3e3e2bbea..346a19d854e34 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-client +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-avatar@14.0.0-rc.5 +
      + ## 18.0.0-rc.4 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 62882821d5a96..887ee6c42153e 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "18.0.0-rc.4", + "version": "18.0.0-rc.5", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index 04d8ad6202a03..a88b003bd5735 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-contexts +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.5 + - @rocket.chat/rest-typings@7.6.0-rc.5 + - @rocket.chat/ddp-client@0.3.21-rc.5 +
      + ## 18.0.0-rc.4 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 3087c096deb89..1318ab6becd26 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "18.0.0-rc.4", + "version": "18.0.0-rc.5", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index 9939e764d2e88..ca475a4f673f5 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-avatar@14.0.0-rc.5 +
      + ## 18.0.0-rc.4 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index c297d6f79ac08..3984dae3769b1 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "18.0.0-rc.4", + "version": "18.0.0-rc.5", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-voip/CHANGELOG.md b/packages/ui-voip/CHANGELOG.md index 0451d20b24c6e..6b8337a41d8e0 100644 --- a/packages/ui-voip/CHANGELOG.md +++ b/packages/ui-voip/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-voip +## 8.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 + - @rocket.chat/ui-avatar@14.0.0-rc.5 + - @rocket.chat/ui-client@18.0.0-rc.5 +
      + ## 8.0.0-rc.4 ### Patch Changes diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 90e2db4b287b5..21f5b3f5fdaff 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-voip", - "version": "8.0.0-rc.4", + "version": "8.0.0-rc.5", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index ace9b7898f5b2..c995a41b16107 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 18.0.0-rc.5 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.5 +
      + ## 18.0.0-rc.4 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index d4e20ce16dd56..adaef6d429d84 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "18.0.0-rc.4", + "version": "18.0.0-rc.5", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", From 622ee7ab84292c6e702ee03f042af530fde21919 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 5 May 2025 17:26:35 -0300 Subject: [PATCH 176/187] chore: fix ts import --- apps/meteor/client/views/room/body/hooks/useGetMore.ts | 2 +- .../client/views/room/body/hooks/useRestoreScrollPosition.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/body/hooks/useGetMore.ts b/apps/meteor/client/views/room/body/hooks/useGetMore.ts index ccab8fb4d5dc3..3d54ac514026a 100644 --- a/apps/meteor/client/views/room/body/hooks/useGetMore.ts +++ b/apps/meteor/client/views/room/body/hooks/useGetMore.ts @@ -1,4 +1,3 @@ -import { useSafeRefCallback } from '@rocket.chat/ui-client'; import { useSearchParameter } from '@rocket.chat/ui-contexts'; import type { MutableRefObject } from 'react'; import { useCallback, useEffect, useRef } from 'react'; @@ -7,6 +6,7 @@ import { flushSync } from 'react-dom'; import { getBoundingClientRect } from '../../../../../app/ui/client/views/app/lib/scrolling'; import { RoomHistoryManager } from '../../../../../app/ui-utils/client'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; +import { useSafeRefCallback } from '../../../../hooks/useSafeRefCallback'; export const useGetMore = (rid: string, atBottomRef: MutableRefObject) => { const msgId = useSearchParameter('msg'); diff --git a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts index 10bf933ab30d4..6718dbe3f6785 100644 --- a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts +++ b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts @@ -1,8 +1,8 @@ -import { useSafeRefCallback } from '@rocket.chat/ui-client'; import { useCallback, useRef } from 'react'; import { isAtBottom } from '../../../../../app/ui/client/views/app/lib/scrolling'; import { withThrottling } from '../../../../../lib/utils/highOrderFunctions'; +import { useSafeRefCallback } from '../../../../hooks/useSafeRefCallback'; import { RoomManager } from '../../../../lib/RoomManager'; export function useRestoreScrollPosition(rid: string, wait = 100) { From 5f1eaca58ce691eec2d233afed4cc5aa77d73859 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Mon, 5 May 2025 20:33:45 +0000 Subject: [PATCH 177/187] Release 7.6.0-rc.6 [no ci] --- .changeset/bump-patch-1746477216002.md | 5 +++ .changeset/pre.json | 1 + apps/meteor/CHANGELOG.md | 35 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 ++++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 12 +++++++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 14 ++++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 14 ++++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 15 ++++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 15 ++++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 14 ++++++++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 14 ++++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 13 +++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 +++++ ee/packages/license/package.json | 2 +- ee/packages/network-broker/CHANGELOG.md | 9 +++++ ee/packages/network-broker/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 ++++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 9 +++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 ++++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 ++++++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 10 ++++++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 11 ++++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 2 ++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 ++++++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 ++++++ packages/ddp-client/package.json | 2 +- packages/freeswitch/CHANGELOG.md | 9 +++++ packages/freeswitch/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 13 +++++++ packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/CHANGELOG.md | 11 ++++++ packages/gazzodown/package.json | 2 +- packages/instance-status/CHANGELOG.md | 9 +++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 9 +++++ packages/livechat/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 +++++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 9 +++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 10 ++++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 9 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 +++++ packages/ui-avatar/package.json | 2 +- packages/ui-client/CHANGELOG.md | 10 ++++++ packages/ui-client/package.json | 2 +- packages/ui-contexts/CHANGELOG.md | 11 ++++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 10 ++++++ packages/ui-video-conf/package.json | 2 +- packages/ui-voip/CHANGELOG.md | 11 ++++++ packages/ui-voip/package.json | 2 +- packages/web-ui-registration/CHANGELOG.md | 9 +++++ packages/web-ui-registration/package.json | 2 +- 76 files changed, 458 insertions(+), 38 deletions(-) create mode 100644 .changeset/bump-patch-1746477216002.md diff --git a/.changeset/bump-patch-1746477216002.md b/.changeset/bump-patch-1746477216002.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746477216002.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/pre.json b/.changeset/pre.json index 5a38c95669b2d..6596612fc7db8 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -79,6 +79,7 @@ "bump-patch-1746034829149", "bump-patch-1746046159552", "bump-patch-1746475877217", + "bump-patch-1746477216002", "dirty-seas-explode", "eighty-wombats-smile", "eleven-laws-crash", diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index a5420c0e2c847..0fc425cfbf683 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,40 @@ # @rocket.chat/meteor +## 7.6.0-rc.6 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/license@1.0.12-rc.6 + - @rocket.chat/omnichannel-services@0.3.18-rc.6 + - @rocket.chat/pdf-worker@0.3.0-rc.6 + - @rocket.chat/presence@0.2.21-rc.6 + - @rocket.chat/api-client@0.2.21-rc.6 + - @rocket.chat/apps@0.5.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/cron@0.1.21-rc.6 + - @rocket.chat/freeswitch@1.2.8-rc.6 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.6 + - @rocket.chat/gazzodown@18.0.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.6 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.6 + - @rocket.chat/ui-client@18.0.0-rc.6 + - @rocket.chat/ui-video-conf@18.0.0-rc.6 + - @rocket.chat/ui-voip@8.0.0-rc.6 + - @rocket.chat/web-ui-registration@18.0.0-rc.6 + - @rocket.chat/instance-status@0.1.21-rc.6 +
      + ## 7.6.0-rc.5 ### Patch Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index a8f04613760d6..8d9386e3e5088 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.6.0-rc.5" + "version": "7.6.0-rc.6" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 78427c4108961..f349acc1dd3f6 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,19 @@ # rocketchat-services +## 2.0.12-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + ## 2.0.12-rc.5 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index a4de2ed873e9c..123c983c6144f 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "2.0.12-rc.5", + "version": "2.0.12-rc.6", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 08b9ccf77a499..add41489c1706 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.6.0-rc.5", + "version": "7.6.0-rc.6", "private": true, "type": "commonjs", "author": { diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index 492d2479dc321..4d1dabbfaa7ef 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/uikit-playground +## 0.6.12-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.6 + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-avatar@14.0.0-rc.6 +
      + ## 0.6.12-rc.5 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 28527b1fbdc1a..19847c6bc0429 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.6.12-rc.5", + "version": "0.6.12-rc.6", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 15eaa73bfd84a..5f780b32b9f46 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/account-service +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + ## 0.4.21-rc.5 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index e261626389608..f1c115aa3c3e6 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.21-rc.5", + "version": "0.4.21-rc.6", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 8a7070236f4c6..5b9f0fabe80af 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/authorization-service +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + ## 0.4.21-rc.5 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index df88a2f9b6491..0bbf54136cd9b 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.21-rc.5", + "version": "0.4.21-rc.6", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index b8d60e5ab8d21..b974df15321f5 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/ddp-streamer +## 0.3.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 + - @rocket.chat/instance-status@0.1.21-rc.6 +
      + ## 0.3.21-rc.5 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 41bb8a7911157..1b34bda1b8bbf 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.21-rc.5", + "version": "0.3.21-rc.6", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index b3e8d7cfacf8a..d59097afc9ba0 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/omnichannel-transcript +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/omnichannel-services@0.3.18-rc.6 + - @rocket.chat/pdf-worker@0.3.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + ## 0.4.21-rc.5 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 6d4c7d85bbdc9..97a61f77f6189 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.21-rc.5", + "version": "0.4.21-rc.6", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 13867792c410a..e1368cdd9f8f6 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/presence-service +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/presence@0.2.21-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + ## 0.4.21-rc.5 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 760578b613856..a114a335d355b 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.21-rc.5", + "version": "0.4.21-rc.6", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index dee663966be3d..e1a933e64c3a8 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/queue-worker +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/omnichannel-services@0.3.18-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + ## 0.4.21-rc.5 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index f8cf9960cb5bb..be9084c2b8042 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.21-rc.5", + "version": "0.4.21-rc.6", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index e0a3b1ba15aaf..aab7257c6cbb5 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/stream-hub-service +## 0.4.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 + - @rocket.chat/network-broker@0.2.0-rc.6 +
      + ## 0.4.21-rc.5 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index a9303849a806a..c19f99724f6e8 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.21-rc.5", + "version": "0.4.21-rc.6", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 170f2c2e631dc..94bcae2b389aa 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 1.0.12-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 +
      + ## 1.0.12-rc.5 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index c53b782ee3620..8dac611c0417e 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "1.0.12-rc.5", + "version": "1.0.12-rc.6", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/network-broker/CHANGELOG.md b/ee/packages/network-broker/CHANGELOG.md index 8fe0e347861bd..e0fd01fc6fa52 100644 --- a/ee/packages/network-broker/CHANGELOG.md +++ b/ee/packages/network-broker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/network-broker +## 0.2.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.6 +
      + ## 0.2.0-rc.5 ### Patch Changes diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index 1c0325a68dd08..e22c0cfe25978 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/network-broker", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index fbdd9e7521896..2cd1f251850b9 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.18-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/pdf-worker@0.3.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 +
      + ## 0.3.18-rc.5 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 9bd1899a160c2..d2b82f149baec 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.18-rc.5", + "version": "0.3.18-rc.6", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 3e33a7b8d427b..12555de0f2644 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.3.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 +
      + ## 0.3.0-rc.5 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index fd8abfacfc304..ed0886490a31d 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.3.0-rc.5", + "version": "0.3.0-rc.6", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 7287c3394aca3..95a59599fc7b0 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/core-services@0.9.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 +
      + ## 0.2.21-rc.5 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index ae41f6017a786..4a5ca734d8bae 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.21-rc.5", + "version": "0.2.21-rc.6", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/package.json b/package.json index 9a557bc816b28..947b20bbaef6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.6.0-rc.5", + "version": "7.6.0-rc.6", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index ebc50ad66b015..2a1f958a324cd 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 +
      + ## 0.2.21-rc.5 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index b4176ea1bd7ee..c4c03fceac67a 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.21-rc.5", + "version": "0.2.21-rc.6", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index 5a51aa4d7f162..946f50d827afe 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.5.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 +
      + ## 0.5.0-rc.5 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index 16b475e440e58..d00b428be193b 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.5.0-rc.5", + "version": "0.5.0-rc.6", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index 758c6cbb6e9f8..f02b2a4e344ca 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/core-services +## 0.9.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 +
      + ## 0.9.0-rc.5 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 98e694cf9fecd..a5fece2f7915a 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.9.0-rc.5", + "version": "0.9.0-rc.6", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index 7b855af9f8175..cbe0567760773 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,7 @@ # @rocket.chat/core-typings +## 7.6.0-rc.6 + ## 7.6.0-rc.5 ## 7.6.0-rc.4 diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 05bfb4acf5d40..d87f8b9b0f7b1 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.6.0-rc.5", + "version": "7.6.0-rc.6", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 58465fe5054e1..f7710471c08d3 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/models@1.5.0-rc.6 +
      + ## 0.1.21-rc.5 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index 49b97d2d48252..5acd43020374a 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.21-rc.5", + "version": "0.1.21-rc.6", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index f0b37101a1317..706524bd4ff34 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/api-client@0.2.21-rc.6 +
      + ## 0.3.21-rc.5 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 536769656158b..d3c1fc255f040 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.21-rc.5", + "version": "0.3.21-rc.6", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/freeswitch/CHANGELOG.md b/packages/freeswitch/CHANGELOG.md index 58ffd5a13608a..2dcb26ce53264 100644 --- a/packages/freeswitch/CHANGELOG.md +++ b/packages/freeswitch/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/freeswitch +## 1.2.8-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 +
      + ## 1.2.8-rc.5 ### Patch Changes diff --git a/packages/freeswitch/package.json b/packages/freeswitch/package.json index 899a316091486..da97537c2710a 100644 --- a/packages/freeswitch/package.json +++ b/packages/freeswitch/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/freeswitch", - "version": "1.2.8-rc.5", + "version": "1.2.8-rc.6", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index a8f91bd12965d..84b2233bfe954 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/gazzodown@18.0.0-rc.6 + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-avatar@14.0.0-rc.6 + - @rocket.chat/ui-video-conf@18.0.0-rc.6 +
      + ## 18.0.0-rc.5 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 17139c4dcfe88..baeb5e5857af9 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/fuselage-ui-kit", - "version": "18.0.0-rc.5", + "version": "18.0.0-rc.6", "private": true, "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index cd4d4301c888f..345710b90194b 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/gazzodown +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-client@18.0.0-rc.6 +
      + ## 18.0.0-rc.5 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index ddbf9b09312ec..e27449a9e49e1 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "18.0.0-rc.5", + "version": "18.0.0-rc.6", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index 7f40e2722c7cd..bb046d4133dae 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.21-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.6 +
      + ## 0.1.21-rc.5 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index e5fe463e9d574..5455ef2e176cd 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.21-rc.5", + "version": "0.1.21-rc.6", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 48e7b7d07e0dd..5a9241de9bf23 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/livechat Change Log +## 1.22.8-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.6 +
      + ## 1.22.8-rc.5 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index af4da5bb48954..f38ab5b29bf11 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.22.8-rc.5", + "version": "1.22.8-rc.6", "files": [ "/build" ], diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index 942ae56ff02aa..56db4e0b7fdaa 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.2.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 +
      + ## 0.2.0-rc.5 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 71cb3b8be2c67..06b9322c17139 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index ae1208af6c6b0..7c11aabff7a45 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/model-typings +## 1.6.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 +
      + ## 1.6.0-rc.5 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index dc2b1c9cdae90..b5131e04c806e 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "1.6.0-rc.5", + "version": "1.6.0-rc.6", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index d29649d597730..abf99e718bbb4 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/models +## 1.5.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/model-typings@1.6.0-rc.6 +
      + ## 1.5.0-rc.5 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index f2ccc669d37ed..936025d10c26a 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "1.5.0-rc.5", + "version": "1.5.0-rc.6", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 5355aa4ea6530..d7f2b35cbabd6 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/rest-typings +## 7.6.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 +
      + ## 7.6.0-rc.5 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 807d1e4b38304..d532ebabd004f 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.6.0-rc.5", + "version": "7.6.0-rc.6", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index 20e7b500a5dbf..5ad0a3a79b0fb 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 14.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 +
      + ## 14.0.0-rc.5 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 446a58c6d0dc3..1ee9482e53a45 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "14.0.0-rc.5", + "version": "14.0.0-rc.6", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 346a19d854e34..63b54863c5544 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-client +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-avatar@14.0.0-rc.6 +
      + ## 18.0.0-rc.5 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 887ee6c42153e..ec0e950c36c82 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "18.0.0-rc.5", + "version": "18.0.0-rc.6", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index a88b003bd5735..8e6bbe8b5f3e9 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-contexts +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.6 + - @rocket.chat/rest-typings@7.6.0-rc.6 + - @rocket.chat/ddp-client@0.3.21-rc.6 +
      + ## 18.0.0-rc.5 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 1318ab6becd26..b87b17e5a6484 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "18.0.0-rc.5", + "version": "18.0.0-rc.6", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index ca475a4f673f5..7a3ac60a7bfc8 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-avatar@14.0.0-rc.6 +
      + ## 18.0.0-rc.5 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 3984dae3769b1..107b88bf9f4d3 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "18.0.0-rc.5", + "version": "18.0.0-rc.6", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-voip/CHANGELOG.md b/packages/ui-voip/CHANGELOG.md index 6b8337a41d8e0..861ee39ba3e11 100644 --- a/packages/ui-voip/CHANGELOG.md +++ b/packages/ui-voip/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-voip +## 8.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 + - @rocket.chat/ui-avatar@14.0.0-rc.6 + - @rocket.chat/ui-client@18.0.0-rc.6 +
      + ## 8.0.0-rc.5 ### Patch Changes diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 21f5b3f5fdaff..df0f00f619920 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-voip", - "version": "8.0.0-rc.5", + "version": "8.0.0-rc.6", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index c995a41b16107..8e26745ff3dd5 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 18.0.0-rc.6 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.6 +
      + ## 18.0.0-rc.5 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index adaef6d429d84..b4c0fe7385bdf 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "18.0.0-rc.5", + "version": "18.0.0-rc.6", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", From a14b259653aeedf21fa0c61bf0160cb5dcfa138d Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Tue, 6 May 2025 12:33:16 -0300 Subject: [PATCH 178/187] regression: API endpoints query parameter validation not working for nested properties (#35920) --- apps/meteor/app/api/server/router.spec.ts | 40 +++++++++++++++++++++++ apps/meteor/app/api/server/router.ts | 7 ++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/api/server/router.spec.ts b/apps/meteor/app/api/server/router.spec.ts index ca5bd8970ae84..5c11ef6e0456f 100644 --- a/apps/meteor/app/api/server/router.spec.ts +++ b/apps/meteor/app/api/server/router.spec.ts @@ -48,4 +48,44 @@ describe('Router use method', () => { expect(response2.statusCode).toBe(200); expect(response2.headers).not.toHaveProperty('x-api-version'); }); + + it('should parse nested query params into object for GET requests', async () => { + const ajv = new Ajv(); + const app = express(); + + const isTestQueryParams = ajv.compile({ + type: 'object', + properties: { + outerProperty: { type: 'object', properties: { innerProperty: { type: 'string' } } }, + }, + additionalProperties: false, + }); + + const api = new Router('/api').get( + '/test', + { + response: { + 200: isTestQueryParams, + }, + query: isTestQueryParams, + }, + async function action() { + const { outerProperty } = this.queryParams as any; + return { + statusCode: 200, + body: { + outerProperty, + }, + }; + }, + ); + + app.use(api.router); + + const response1 = await request(app).get('/api/test?outerProperty[innerProperty]=test'); + + expect(response1.statusCode).toBe(200); + expect(response1.body).toHaveProperty('outerProperty'); + expect(response1.body.outerProperty).toHaveProperty('innerProperty', 'test'); + }); }); diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index a919ffb2ac00b..c18598f03c8b5 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -191,9 +191,12 @@ export class Router< this.innerRouter[method.toLowerCase() as Lowercase](`/${subpath}`.replace('//', '/'), ...middlewares, async (c) => { const { req, res } = c; req.raw.route = `${c.var.route ?? ''}${subpath}`; + + const queryParams = this.parseQueryParams(req); + if (options.query) { const validatorFn = options.query; - if (typeof options.query === 'function' && !validatorFn(req.query())) { + if (typeof options.query === 'function' && !validatorFn(queryParams)) { return c.json( { success: false, @@ -229,7 +232,7 @@ export class Router< { requestIp: c.get('remoteAddress'), urlParams: req.param(), - queryParams: this.parseQueryParams(req), + queryParams, bodyParams, request: req.raw.clone(), path: req.path, From bfdf17e5a2ce2ed21450b090f0f9941d1dc70138 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 6 May 2025 15:11:29 -0300 Subject: [PATCH 179/187] chore: increase idle time connection to 1 minute --- apps/meteor/client/hooks/useIdleConnection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/hooks/useIdleConnection.ts b/apps/meteor/client/hooks/useIdleConnection.ts index 6c7ea02853db5..a058941c80433 100644 --- a/apps/meteor/client/hooks/useIdleConnection.ts +++ b/apps/meteor/client/hooks/useIdleConnection.ts @@ -23,5 +23,5 @@ export const useIdleConnection = (uid: string | null) => { } }); - useIdleActiveEvents({ id: 'useLoginPresence', time: 3000, awayOnWindowBlur: true }, disconnect, reconnect); + useIdleActiveEvents({ id: 'useLoginPresence', time: 60 * 1000, awayOnWindowBlur: true }, disconnect, reconnect); }; From 29264b8620ed6cff942dce29c720f57785f21152 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Tue, 6 May 2025 16:52:07 -0300 Subject: [PATCH 180/187] regression: Admin settings with `enableQuery` and `displayQuery` do not react to changes (#35928) --- .../admin/EditableSettingsContext.spec.tsx | 61 +++++++++++++++++++ .../views/admin/EditableSettingsContext.ts | 34 +++++++++++ .../settings/EditableSettingsProvider.tsx | 31 ++-------- .../views/admin/settings/Setting/Setting.tsx | 9 ++- 4 files changed, 106 insertions(+), 29 deletions(-) create mode 100644 apps/meteor/client/views/admin/EditableSettingsContext.spec.tsx diff --git a/apps/meteor/client/views/admin/EditableSettingsContext.spec.tsx b/apps/meteor/client/views/admin/EditableSettingsContext.spec.tsx new file mode 100644 index 0000000000000..8941812d83c0c --- /dev/null +++ b/apps/meteor/client/views/admin/EditableSettingsContext.spec.tsx @@ -0,0 +1,61 @@ +import { mockAppRoot as _mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook } from '@testing-library/react'; + +import { useEditableSettingVisibilityQuery } from './EditableSettingsContext'; +import EditableSettingsProvider from './settings/EditableSettingsProvider'; + +const mockAppRoot = () => _mockAppRoot().wrap((children) => {children}); + +describe('useEditableSettingVisibilityQuery', () => { + it('should return true when no query is provided', () => { + const { result } = renderHook(() => useEditableSettingVisibilityQuery(), { + wrapper: mockAppRoot().build(), + }); + + expect(result.current).toBe(true); + }); + + it('should handle settings with a query', () => { + const { result } = renderHook(() => useEditableSettingVisibilityQuery({ _id: 'setting1', value: true }), { + wrapper: mockAppRoot().withSetting('setting1', true).build(), + }); + + expect(result.current).toBe(true); + }); + + it('should handle multiple conditions in enableQuery', () => { + const { result } = renderHook( + () => + useEditableSettingVisibilityQuery([ + { _id: 'setting5', value: true }, + { _id: 'setting6', value: true }, + ]), + { + wrapper: mockAppRoot().withSetting('setting5', true).withSetting('setting6', true).build(), + }, + ); + + expect(result.current).toBe(true); + + const { result: result2 } = renderHook( + () => + useEditableSettingVisibilityQuery([ + { _id: 'setting5', value: true }, + { _id: 'setting6', value: true }, + ]), + { + wrapper: mockAppRoot().withSetting('setting5', true).withSetting('setting6', false).build(), + }, + ); + + expect(result2.current).toBe(false); + }); + + it('should handle string queries', () => { + const { result } = renderHook(() => useEditableSettingVisibilityQuery(JSON.stringify({ _id: 'setting7', value: true })), { + wrapper: mockAppRoot().withSetting('setting7', true).build(), + }); + + expect(result.current).toBe(true); + }); +}); diff --git a/apps/meteor/client/views/admin/EditableSettingsContext.ts b/apps/meteor/client/views/admin/EditableSettingsContext.ts index cbfb86259fec5..9d34eaa4341f2 100644 --- a/apps/meteor/client/views/admin/EditableSettingsContext.ts +++ b/apps/meteor/client/views/admin/EditableSettingsContext.ts @@ -1,4 +1,5 @@ import type { ISetting } from '@rocket.chat/core-typings'; +import { createFilterFromQuery } from '@rocket.chat/mongo-adapter'; import { createContext, useContext } from 'react'; import { create, type StoreApi, type UseBoundStore } from 'zustand'; import { useShallow } from 'zustand/shallow'; @@ -21,6 +22,28 @@ export const compareSettings = (a: EditableSetting, b: EditableSetting): number return i18nLabel; }; +export const performSettingQuery = ( + query: + | string + | { + _id: string; + value: unknown; + } + | { + _id: string; + value: unknown; + }[] + | undefined, + settings: ISetting[], +) => { + if (!query) { + return true; + } + + const queries = [].concat(typeof query === 'string' ? JSON.parse(query) : query); + return queries.every((query) => settings.some(createFilterFromQuery(query))); +}; + type EditableSettingsContextQuery = | { group: ISetting['_id']; @@ -125,3 +148,14 @@ export const useEditableSettingsDispatch = (): ((changes: Partial state.mutate); }; + +export const useEditableSettingVisibilityQuery = (query?: ISetting['enableQuery'] | ISetting['displayQuery']): boolean => { + const { useEditableSettingsStore } = useContext(EditableSettingsContext); + + return useEditableSettingsStore((state) => { + if (!query) { + return true; + } + return performSettingQuery(query, state.state); + }); +}; diff --git a/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx b/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx index 8fd724ee1b361..5124da92d4686 100644 --- a/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx +++ b/apps/meteor/client/views/admin/settings/EditableSettingsProvider.tsx @@ -1,37 +1,14 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import { createFilterFromQuery } from '@rocket.chat/mongo-adapter'; import { useSettings } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import { useEffect, useMemo, useState } from 'react'; import { create } from 'zustand'; import type { EditableSetting, IEditableSettingsState } from '../EditableSettingsContext'; -import { EditableSettingsContext } from '../EditableSettingsContext'; +import { EditableSettingsContext, performSettingQuery } from '../EditableSettingsContext'; const defaultOmit: Array = ['Cloud_Workspace_AirGapped_Restrictions_Remaining_Days']; -const performSettingQuery = ( - query: - | string - | { - _id: string; - value: unknown; - } - | { - _id: string; - value: unknown; - }[] - | undefined, - settings: ISetting[], -) => { - if (!query) { - return true; - } - - const queries = [].concat(typeof query === 'string' ? JSON.parse(query) : query); - return queries.every((query) => settings.some(createFilterFromQuery(query))); -}; - type EditableSettingsProviderProps = { children?: ReactNode; }; @@ -48,6 +25,8 @@ const EditableSettingsProvider = ({ children }: EditableSettingsProviderProps) = (persisted): EditableSetting => ({ ...persisted, changed: false, + // TODO: This might not be needed anymore due to implementation of useEditableSettingVisibilityQuery + // This was left here to avoid unexpected breaking changes disabled: persisted.blocked || !performSettingQuery(persisted.enableQuery, persistedSettings), invisible: !performSettingQuery(persisted.displayQuery, persistedSettings), }), @@ -62,6 +41,8 @@ const EditableSettingsProvider = ({ children }: EditableSettingsProviderProps) = ...state.find(({ _id }) => _id === persisted._id), ...persisted, changed: false, + // TODO: This might not be needed anymore due to implementation of useEditableSettingVisibilityQuery + // This was left here to avoid unexpected breaking changes disabled: persisted.blocked || !performSettingQuery(persisted.enableQuery, state), invisible: !performSettingQuery(persisted.displayQuery, state), }), @@ -85,8 +66,6 @@ const EditableSettingsProvider = ({ children }: EditableSettingsProviderProps) = return { ...current, ...change, - disabled: persisted.blocked || !performSettingQuery(persisted.enableQuery, state), - invisible: !performSettingQuery(persisted.displayQuery, state), }; }), })); diff --git a/apps/meteor/client/views/admin/settings/Setting/Setting.tsx b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx index 484eb34cacfa3..04f4e8cf061e9 100644 --- a/apps/meteor/client/views/admin/settings/Setting/Setting.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/Setting.tsx @@ -10,7 +10,7 @@ import { useTranslation } from 'react-i18next'; import MemoizedSetting from './MemoizedSetting'; import MarkdownText from '../../../../components/MarkdownText'; -import { useEditableSetting, useEditableSettingsDispatch } from '../../EditableSettingsContext'; +import { useEditableSetting, useEditableSettingsDispatch, useEditableSettingVisibilityQuery } from '../../EditableSettingsContext'; import { useHasSettingModule } from '../hooks/useHasSettingModule'; type SettingProps = { @@ -96,7 +96,10 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr // eslint-disable-next-line react-hooks/exhaustive-deps }, [setting.value, (setting as ISettingColor).editor, update, persistedSetting]); - const { _id, disabled, readonly, type, packageValue, i18nLabel, i18nDescription, alert, invisible } = setting; + const { _id, readonly, type, packageValue, i18nLabel, i18nDescription, alert } = setting; + + const disabled = !useEditableSettingVisibilityQuery(persistedSetting.enableQuery); + const invisible = !useEditableSettingVisibilityQuery(persistedSetting.displayQuery); const labelText = (i18n.exists(i18nLabel) && t(i18nLabel)) || (i18n.exists(_id) && t(_id)) || i18nLabel || _id; @@ -162,7 +165,7 @@ function Setting({ className = undefined, settingId, sectionChanged }: SettingPr showUpgradeButton={showUpgradeButton} sectionChanged={sectionChanged} {...setting} - disabled={setting.disabled || shouldDisableEnterprise} + disabled={disabled || shouldDisableEnterprise} value={value} editor={editor} hasResetButton={hasResetButton} From f5ca547ba8e2fd52a2ba851262b12552f47c2549 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 6 May 2025 14:09:57 -0600 Subject: [PATCH 181/187] regression: Passing `servedBy` instead of `servedBy.id` to `findOne` call when converting a room from app (#35935) --- apps/meteor/app/apps/server/converters/rooms.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 045c341186810..2404451366af5 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -59,12 +59,12 @@ export class AppRoomsConverter { }; } - async __getUserIdAndUsername(uid) { - if (!uid) { + async __getUserIdAndUsername(userObj) { + if (!userObj?.id) { return; } - const user = await Users.findOneById(uid, { projection: { _id: 1, username: 1 } }); + const user = await Users.findOneById(userObj.id, { projection: { _id: 1, username: 1 } }); if (!user) { return; } From 976f7b3b5d85b9bf5b195747fecd0855a489bcb8 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Tue, 6 May 2025 20:55:09 +0000 Subject: [PATCH 182/187] Release 7.6.0-rc.7 [no ci] --- .changeset/bump-patch-1746564900627.md | 5 +++ .changeset/pre.json | 1 + apps/meteor/CHANGELOG.md | 35 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 ++++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 12 +++++++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 14 ++++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 14 ++++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 15 ++++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 15 ++++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 14 ++++++++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 14 ++++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 13 +++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 +++++ ee/packages/license/package.json | 2 +- ee/packages/network-broker/CHANGELOG.md | 9 +++++ ee/packages/network-broker/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 ++++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 9 +++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 ++++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 ++++++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 10 ++++++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 11 ++++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 2 ++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 ++++++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 ++++++ packages/ddp-client/package.json | 2 +- packages/freeswitch/CHANGELOG.md | 9 +++++ packages/freeswitch/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 13 +++++++ packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/CHANGELOG.md | 11 ++++++ packages/gazzodown/package.json | 2 +- packages/instance-status/CHANGELOG.md | 9 +++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 9 +++++ packages/livechat/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 +++++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 9 +++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 10 ++++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 9 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 +++++ packages/ui-avatar/package.json | 2 +- packages/ui-client/CHANGELOG.md | 10 ++++++ packages/ui-client/package.json | 2 +- packages/ui-contexts/CHANGELOG.md | 11 ++++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 10 ++++++ packages/ui-video-conf/package.json | 2 +- packages/ui-voip/CHANGELOG.md | 11 ++++++ packages/ui-voip/package.json | 2 +- packages/web-ui-registration/CHANGELOG.md | 9 +++++ packages/web-ui-registration/package.json | 2 +- 76 files changed, 458 insertions(+), 38 deletions(-) create mode 100644 .changeset/bump-patch-1746564900627.md diff --git a/.changeset/bump-patch-1746564900627.md b/.changeset/bump-patch-1746564900627.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746564900627.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/pre.json b/.changeset/pre.json index 6596612fc7db8..e900e4f8f8c67 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -80,6 +80,7 @@ "bump-patch-1746046159552", "bump-patch-1746475877217", "bump-patch-1746477216002", + "bump-patch-1746564900627", "dirty-seas-explode", "eighty-wombats-smile", "eleven-laws-crash", diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 0fc425cfbf683..8da425d6db373 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,40 @@ # @rocket.chat/meteor +## 7.6.0-rc.7 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/license@1.0.12-rc.7 + - @rocket.chat/omnichannel-services@0.3.18-rc.7 + - @rocket.chat/pdf-worker@0.3.0-rc.7 + - @rocket.chat/presence@0.2.21-rc.7 + - @rocket.chat/api-client@0.2.21-rc.7 + - @rocket.chat/apps@0.5.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/cron@0.1.21-rc.7 + - @rocket.chat/freeswitch@1.2.8-rc.7 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.7 + - @rocket.chat/gazzodown@18.0.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.7 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.7 + - @rocket.chat/ui-client@18.0.0-rc.7 + - @rocket.chat/ui-video-conf@18.0.0-rc.7 + - @rocket.chat/ui-voip@8.0.0-rc.7 + - @rocket.chat/web-ui-registration@18.0.0-rc.7 + - @rocket.chat/instance-status@0.1.21-rc.7 +
      + ## 7.6.0-rc.6 ### Patch Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 8d9386e3e5088..9ace006d2049f 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.6.0-rc.6" + "version": "7.6.0-rc.7" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index f349acc1dd3f6..3224aef96e212 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,19 @@ # rocketchat-services +## 2.0.12-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + ## 2.0.12-rc.6 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 123c983c6144f..6fcb8ae5c08dd 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "2.0.12-rc.6", + "version": "2.0.12-rc.7", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index add41489c1706..727725c3deb7a 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.6.0-rc.6", + "version": "7.6.0-rc.7", "private": true, "type": "commonjs", "author": { diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index 4d1dabbfaa7ef..ed0e579d0a8cb 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/uikit-playground +## 0.6.12-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.7 + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-avatar@14.0.0-rc.7 +
      + ## 0.6.12-rc.6 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 19847c6bc0429..05affa6e76d9f 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.6.12-rc.6", + "version": "0.6.12-rc.7", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 5f780b32b9f46..61a5dcf2a8e14 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/account-service +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + ## 0.4.21-rc.6 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index f1c115aa3c3e6..0933f4c597218 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.21-rc.6", + "version": "0.4.21-rc.7", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 5b9f0fabe80af..513e6721b77c4 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/authorization-service +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + ## 0.4.21-rc.6 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 0bbf54136cd9b..16a36355a98e7 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.21-rc.6", + "version": "0.4.21-rc.7", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index b974df15321f5..28052bd931b5f 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/ddp-streamer +## 0.3.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 + - @rocket.chat/instance-status@0.1.21-rc.7 +
      + ## 0.3.21-rc.6 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 1b34bda1b8bbf..062e150c2932a 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.21-rc.6", + "version": "0.3.21-rc.7", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index d59097afc9ba0..120d8fdac311b 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/omnichannel-transcript +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/omnichannel-services@0.3.18-rc.7 + - @rocket.chat/pdf-worker@0.3.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + ## 0.4.21-rc.6 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 97a61f77f6189..686bfe007ed00 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.21-rc.6", + "version": "0.4.21-rc.7", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index e1368cdd9f8f6..884cbfa29aef2 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/presence-service +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/presence@0.2.21-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + ## 0.4.21-rc.6 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index a114a335d355b..cbee694a44a81 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.21-rc.6", + "version": "0.4.21-rc.7", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index e1a933e64c3a8..159b8a8d64e1f 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/queue-worker +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/omnichannel-services@0.3.18-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + ## 0.4.21-rc.6 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index be9084c2b8042..fae1edc48912c 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.21-rc.6", + "version": "0.4.21-rc.7", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index aab7257c6cbb5..c97b8757d238a 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/stream-hub-service +## 0.4.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 + - @rocket.chat/network-broker@0.2.0-rc.7 +
      + ## 0.4.21-rc.6 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index c19f99724f6e8..fe93d78f1beb2 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.21-rc.6", + "version": "0.4.21-rc.7", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 94bcae2b389aa..a7409850cb2fc 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 1.0.12-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 +
      + ## 1.0.12-rc.6 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 8dac611c0417e..d210018190caa 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "1.0.12-rc.6", + "version": "1.0.12-rc.7", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/network-broker/CHANGELOG.md b/ee/packages/network-broker/CHANGELOG.md index e0fd01fc6fa52..e877bb5e762e5 100644 --- a/ee/packages/network-broker/CHANGELOG.md +++ b/ee/packages/network-broker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/network-broker +## 0.2.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.7 +
      + ## 0.2.0-rc.6 ### Patch Changes diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index e22c0cfe25978..6c8806d9477a4 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/network-broker", - "version": "0.2.0-rc.6", + "version": "0.2.0-rc.7", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 2cd1f251850b9..8288c0fc56327 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.18-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/pdf-worker@0.3.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 +
      + ## 0.3.18-rc.6 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index d2b82f149baec..0afdf699f3db1 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.18-rc.6", + "version": "0.3.18-rc.7", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 12555de0f2644..5bc2ec9d347e7 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.3.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 +
      + ## 0.3.0-rc.6 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index ed0886490a31d..9c8f0bd818d58 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.3.0-rc.6", + "version": "0.3.0-rc.7", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 95a59599fc7b0..3b18414204e25 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/core-services@0.9.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 +
      + ## 0.2.21-rc.6 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 4a5ca734d8bae..ff7029b55543d 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.21-rc.6", + "version": "0.2.21-rc.7", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/package.json b/package.json index 947b20bbaef6c..e6d80b4e62700 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.6.0-rc.6", + "version": "7.6.0-rc.7", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 2a1f958a324cd..adf83f2e92692 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 +
      + ## 0.2.21-rc.6 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index c4c03fceac67a..98a5c4450bb91 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.21-rc.6", + "version": "0.2.21-rc.7", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index 946f50d827afe..908b0cbf116ae 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.5.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 +
      + ## 0.5.0-rc.6 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index d00b428be193b..682ca7ec4ace3 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.5.0-rc.6", + "version": "0.5.0-rc.7", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index f02b2a4e344ca..121f5a6529c78 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/core-services +## 0.9.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 +
      + ## 0.9.0-rc.6 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index a5fece2f7915a..ff71f55991e46 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.9.0-rc.6", + "version": "0.9.0-rc.7", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index cbe0567760773..cb877be00fc7d 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,7 @@ # @rocket.chat/core-typings +## 7.6.0-rc.7 + ## 7.6.0-rc.6 ## 7.6.0-rc.5 diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index d87f8b9b0f7b1..4833aa8f0c746 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.6.0-rc.6", + "version": "7.6.0-rc.7", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index f7710471c08d3..0bfdd58c15275 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/models@1.5.0-rc.7 +
      + ## 0.1.21-rc.6 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index 5acd43020374a..94bd1e3150160 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.21-rc.6", + "version": "0.1.21-rc.7", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index 706524bd4ff34..c71c2be8bef82 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/api-client@0.2.21-rc.7 +
      + ## 0.3.21-rc.6 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index d3c1fc255f040..d5b4228741daa 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.21-rc.6", + "version": "0.3.21-rc.7", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/freeswitch/CHANGELOG.md b/packages/freeswitch/CHANGELOG.md index 2dcb26ce53264..7c0babf06efdf 100644 --- a/packages/freeswitch/CHANGELOG.md +++ b/packages/freeswitch/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/freeswitch +## 1.2.8-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 +
      + ## 1.2.8-rc.6 ### Patch Changes diff --git a/packages/freeswitch/package.json b/packages/freeswitch/package.json index da97537c2710a..de6cd36e8efe7 100644 --- a/packages/freeswitch/package.json +++ b/packages/freeswitch/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/freeswitch", - "version": "1.2.8-rc.6", + "version": "1.2.8-rc.7", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 84b2233bfe954..7c0bedd63f713 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/gazzodown@18.0.0-rc.7 + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-avatar@14.0.0-rc.7 + - @rocket.chat/ui-video-conf@18.0.0-rc.7 +
      + ## 18.0.0-rc.6 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index baeb5e5857af9..7299945c91ecb 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/fuselage-ui-kit", - "version": "18.0.0-rc.6", + "version": "18.0.0-rc.7", "private": true, "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index 345710b90194b..288bbcc33cfa4 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/gazzodown +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-client@18.0.0-rc.7 +
      + ## 18.0.0-rc.6 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index e27449a9e49e1..05cc736c38152 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "18.0.0-rc.6", + "version": "18.0.0-rc.7", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index bb046d4133dae..c49b1ee103492 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.21-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.7 +
      + ## 0.1.21-rc.6 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 5455ef2e176cd..357810be2f8b5 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.21-rc.6", + "version": "0.1.21-rc.7", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 5a9241de9bf23..55b9b3446e55c 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/livechat Change Log +## 1.22.8-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.7 +
      + ## 1.22.8-rc.6 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index f38ab5b29bf11..fd9b145604c01 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.22.8-rc.6", + "version": "1.22.8-rc.7", "files": [ "/build" ], diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index 56db4e0b7fdaa..213c4cbbc97c9 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.2.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 +
      + ## 0.2.0-rc.6 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 06b9322c17139..76657d329838b 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.2.0-rc.6", + "version": "0.2.0-rc.7", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index 7c11aabff7a45..fa0f2581e9fad 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/model-typings +## 1.6.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 +
      + ## 1.6.0-rc.6 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index b5131e04c806e..1c8b0d09dca20 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "1.6.0-rc.6", + "version": "1.6.0-rc.7", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index abf99e718bbb4..f2450f3a20fb8 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/models +## 1.5.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/model-typings@1.6.0-rc.7 +
      + ## 1.5.0-rc.6 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index 936025d10c26a..9002aa88a6f2b 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "1.5.0-rc.6", + "version": "1.5.0-rc.7", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index d7f2b35cbabd6..9cd8292384049 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/rest-typings +## 7.6.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 +
      + ## 7.6.0-rc.6 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index d532ebabd004f..8b1fdfa04b0d2 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.6.0-rc.6", + "version": "7.6.0-rc.7", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index 5ad0a3a79b0fb..93b483ad4f151 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 14.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 +
      + ## 14.0.0-rc.6 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 1ee9482e53a45..b8ace14d001a0 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "14.0.0-rc.6", + "version": "14.0.0-rc.7", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 63b54863c5544..d221cf7240473 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-client +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-avatar@14.0.0-rc.7 +
      + ## 18.0.0-rc.6 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index ec0e950c36c82..99d603ede0606 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "18.0.0-rc.6", + "version": "18.0.0-rc.7", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index 8e6bbe8b5f3e9..08e24f681f950 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-contexts +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.7 + - @rocket.chat/rest-typings@7.6.0-rc.7 + - @rocket.chat/ddp-client@0.3.21-rc.7 +
      + ## 18.0.0-rc.6 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index b87b17e5a6484..35592ea1604c9 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "18.0.0-rc.6", + "version": "18.0.0-rc.7", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index 7a3ac60a7bfc8..c6002d0e3a765 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-avatar@14.0.0-rc.7 +
      + ## 18.0.0-rc.6 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 107b88bf9f4d3..56b3dfdb27bdc 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "18.0.0-rc.6", + "version": "18.0.0-rc.7", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-voip/CHANGELOG.md b/packages/ui-voip/CHANGELOG.md index 861ee39ba3e11..0dc7f330481a2 100644 --- a/packages/ui-voip/CHANGELOG.md +++ b/packages/ui-voip/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-voip +## 8.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 + - @rocket.chat/ui-avatar@14.0.0-rc.7 + - @rocket.chat/ui-client@18.0.0-rc.7 +
      + ## 8.0.0-rc.6 ### Patch Changes diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index df0f00f619920..628242c702455 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-voip", - "version": "8.0.0-rc.6", + "version": "8.0.0-rc.7", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index 8e26745ff3dd5..94f6e0256f8e8 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 18.0.0-rc.7 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.7 +
      + ## 18.0.0-rc.6 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index b4c0fe7385bdf..e7d72b62fea98 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "18.0.0-rc.6", + "version": "18.0.0-rc.7", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", From 9a626ad19cd431d80aec233e017bb0f6c609806d Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 8 May 2025 11:09:55 -0600 Subject: [PATCH 183/187] regression: `Cannot read properties of undefined` when running `request.route.includes` (#35948) --- .../app/api/server/helpers/parseJsonQuery.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts index 068e808751e52..9879f1cb4f9bc 100644 --- a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts +++ b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts @@ -24,15 +24,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ */ query: Record; }> { - const { - userId, - queryParams: params, - logger, - queryFields, - queryOperations, - response, - request: { route }, - } = api; + const { userId, queryParams: params, logger, queryFields, queryOperations, response, path } = api; let sort; if (params.sort) { @@ -80,7 +72,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ // Verify the user's selected fields only contains ones which their role allows if (typeof fields === 'object') { let nonSelectableFields = Object.keys(API.v1.defaultFieldsToExclude); - if (route.includes('/v1/users.')) { + if (path.includes('/v1/users.')) { nonSelectableFields = nonSelectableFields.concat( Object.keys( (await hasPermissionAsync(userId, 'view-full-other-user-info')) @@ -99,7 +91,7 @@ export async function parseJsonQuery(api: PartialThis): Promise<{ // Limit the fields by default fields = Object.assign({}, fields, API.v1.defaultFieldsToExclude); - if (api.path.includes('/v1/users.')) { + if (path.includes('/v1/users.')) { if (await hasPermissionAsync(userId, 'view-full-other-user-info')) { fields = Object.assign(fields, API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser); } else { From 21af1780aa31041db627f3ada17aea469f6e5b33 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 8 May 2025 20:26:20 -0300 Subject: [PATCH 184/187] regression: solve restore position on same room types (#35950) --- apps/meteor/client/hooks/useMergedRefsV2.ts | 30 +++++++++++++++++++ .../MessageList/hooks/useJumpToMessage.ts | 8 +++-- .../client/views/room/body/RoomBody.tsx | 8 ++--- .../client/views/room/body/RoomBodyV2.tsx | 6 ++-- .../body/hooks/useRestoreScrollPosition.ts | 2 ++ 5 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 apps/meteor/client/hooks/useMergedRefsV2.ts diff --git a/apps/meteor/client/hooks/useMergedRefsV2.ts b/apps/meteor/client/hooks/useMergedRefsV2.ts new file mode 100644 index 0000000000000..beb3e45c28fa7 --- /dev/null +++ b/apps/meteor/client/hooks/useMergedRefsV2.ts @@ -0,0 +1,30 @@ +import type { MutableRefObject, Ref, RefCallback } from 'react'; +import { useCallback } from 'react'; + +const isRefCallback = (x: unknown): x is RefCallback => typeof x === 'function'; +const isMutableRefObject = (x: unknown): x is MutableRefObject => typeof x === 'object'; + +export const setRef = (ref: Ref | undefined, refValue: T) => { + if (isRefCallback(ref)) { + ref(refValue); + return; + } + + if (isMutableRefObject(ref)) { + ref.current = refValue; + } +}; + +// TODO: backport to fuselage-hooks +/** + * Merges multiple refs into a single ref callback + * + * @param refs The refs to merge. + * @returns The merged ref callback. + */ +export const useMergedRefsV2 = (...refs: (Ref | undefined)[]): RefCallback => { + return useCallback((refValue: T) => { + refs.forEach((ref) => setRef(ref, refValue)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, refs); +}; diff --git a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts index e1f55eaaab72b..6554603fb7fee 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts +++ b/apps/meteor/client/views/room/MessageList/hooks/useJumpToMessage.ts @@ -22,9 +22,11 @@ export const useJumpToMessageImperative = () => { if (!jumpToRef.current || !containerRef.current) { return; } - jumpToRef.current.scrollIntoView({ - block: 'center', - }); + + // calculate the scroll position to center the message + // avoiding scrollIntoView because it will can scroll parent elements + containerRef.current.scrollTop = + jumpToRef.current.offsetTop - containerRef.current.clientHeight / 2 + jumpToRef.current.offsetHeight / 2; }, []); return { diff --git a/apps/meteor/client/views/room/body/RoomBody.tsx b/apps/meteor/client/views/room/body/RoomBody.tsx index 2656064b9ed9f..54aee2f2c1578 100644 --- a/apps/meteor/client/views/room/body/RoomBody.tsx +++ b/apps/meteor/client/views/room/body/RoomBody.tsx @@ -1,5 +1,4 @@ import { Box } from '@rocket.chat/fuselage'; -import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import type { MouseEvent, ReactElement } from 'react'; import { memo, useCallback, useMemo } from 'react'; @@ -16,6 +15,7 @@ import { useReadMessageWindowEvents } from './hooks/useReadMessageWindowEvents'; import { isTruthy } from '../../../../lib/isTruthy'; import { CustomScrollbars } from '../../../components/CustomScrollbars'; import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; +import { useMergedRefsV2 } from '../../../hooks/useMergedRefsV2'; import { BubbleDate } from '../BubbleDate'; import MessageListErrorBoundary from '../MessageList/MessageListErrorBoundary'; import RoomAnnouncement from '../RoomAnnouncement'; @@ -110,7 +110,7 @@ const RoomBody = (): ReactElement => { const { innerRef: restoreScrollPositionInnerRef, jumpToRef: jumpToRefRestoreScrollPosition } = useRestoreScrollPosition(room._id); - const jumpToRef = useMergedRefs( + const jumpToRef = useMergedRefsV2( jumpToRefGetMore, jumpToRefIsAtBottom, jumpToRefRestoreScrollPosition, @@ -135,7 +135,7 @@ const RoomBody = (): ReactElement => { isAtBottom, }); - const innerRef = useMergedRefs( + const innerRef = useMergedRefsV2( dateScrollInnerRef, restoreScrollPositionInnerRef, isAtBottomInnerRef, @@ -147,7 +147,7 @@ const RoomBody = (): ReactElement => { jumpToRefGetMoreImperativeInnerRef, ); - const wrapperBoxRefs = useMergedRefs(unreadBarWrapperRef); + const wrapperBoxRefs = useMergedRefsV2(unreadBarWrapperRef); const handleNavigateToPreviousMessage = useCallback((): void => { chat.messageEditing.toPreviousMessage(); diff --git a/apps/meteor/client/views/room/body/RoomBodyV2.tsx b/apps/meteor/client/views/room/body/RoomBodyV2.tsx index 54f24fd4c4d62..b8d5246ecf543 100644 --- a/apps/meteor/client/views/room/body/RoomBodyV2.tsx +++ b/apps/meteor/client/views/room/body/RoomBodyV2.tsx @@ -1,5 +1,4 @@ import { Box } from '@rocket.chat/fuselage'; -import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { usePermission, useRole, useSetting, useTranslation, useUser, useUserPreference } from '@rocket.chat/ui-contexts'; import type { MouseEvent, ReactElement } from 'react'; import { memo, useCallback, useMemo } from 'react'; @@ -7,6 +6,7 @@ import { memo, useCallback, useMemo } from 'react'; import { isTruthy } from '../../../../lib/isTruthy'; import { CustomScrollbars } from '../../../components/CustomScrollbars'; import { useEmbeddedLayout } from '../../../hooks/useEmbeddedLayout'; +import { useMergedRefsV2 } from '../../../hooks/useMergedRefsV2'; import { BubbleDate } from '../BubbleDate'; import { MessageList } from '../MessageList'; import DropTargetOverlay from './DropTargetOverlay'; @@ -110,7 +110,7 @@ const RoomBody = (): ReactElement => { const { innerRef: restoreScrollPositionInnerRef, jumpToRef: jumpToRefRestoreScrollPosition } = useRestoreScrollPosition(room._id); - const jumpToRef = useMergedRefs( + const jumpToRef = useMergedRefsV2( jumpToRefIsAtBottom, jumpToRefGetMore, jumpToRefRestoreScrollPosition, @@ -135,7 +135,7 @@ const RoomBody = (): ReactElement => { isAtBottom, }); - const innerRef = useMergedRefs( + const innerRef = useMergedRefsV2( dateScrollInnerRef, restoreScrollPositionInnerRef, isAtBottomInnerRef, diff --git a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts index 6718dbe3f6785..325cff721059e 100644 --- a/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts +++ b/apps/meteor/client/views/room/body/hooks/useRestoreScrollPosition.ts @@ -19,10 +19,12 @@ export function useRestoreScrollPosition(rid: string, wait = 100) { node.scrollLeft = 30; } const handleWrapperScroll = withThrottling({ wait })((event) => { + const store = RoomManager.getStore(rid); store?.update({ scroll: event.target.scrollTop, atBottom: isAtBottom(event.target, 50) }); }); node.addEventListener('scroll', handleWrapperScroll, { passive: true }); return () => { + handleWrapperScroll.cancel(); node.removeEventListener('scroll', handleWrapperScroll); }; }, From 1668121b76d808096aa52561e3b5d673bab9e18b Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Thu, 8 May 2025 20:28:50 -0300 Subject: [PATCH 185/187] fix: OAuth login buttons not showing up (#35864) --- .changeset/shy-dolls-protect.md | 5 ++++ apps/meteor/app/apple/client/index.ts | 2 +- .../app/custom-oauth/client/CustomOAuth.ts | 30 +++++++++++++++++++ .../app/dolphin/client/hooks/useDolphin.ts | 2 +- .../app/drupal/client/hooks/useDrupal.ts | 2 +- .../client/hooks/useGitHubEnterpriseAuth.ts | 2 +- .../app/gitlab/client/hooks/useGitLabAuth.ts | 2 +- .../app/nextcloud/client/useNextcloud.ts | 2 +- .../client/hooks/useTokenPassAuth.tsx | 2 +- apps/meteor/client/lib/loginServices.ts | 12 ++++++-- .../client/sidebar/hooks/useCustomOAuth.ts | 30 ++++++++++--------- .../views/root/hooks/useWordPressOAuth.ts | 2 +- .../externals/meteor/accounts-base.d.ts | 2 ++ 13 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 .changeset/shy-dolls-protect.md diff --git a/.changeset/shy-dolls-protect.md b/.changeset/shy-dolls-protect.md new file mode 100644 index 0000000000000..9295c3eac9a22 --- /dev/null +++ b/.changeset/shy-dolls-protect.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where OAuth login buttons were not showing up on the login page diff --git a/apps/meteor/app/apple/client/index.ts b/apps/meteor/app/apple/client/index.ts index 2c59dbe5b3d45..f2579fed790d6 100644 --- a/apps/meteor/app/apple/client/index.ts +++ b/apps/meteor/app/apple/client/index.ts @@ -1,4 +1,4 @@ import { CustomOAuth } from '../../custom-oauth/client/CustomOAuth'; import { config } from '../lib/config'; -new CustomOAuth('apple', config); +CustomOAuth.configureOAuthService('apple', config); diff --git a/apps/meteor/app/custom-oauth/client/CustomOAuth.ts b/apps/meteor/app/custom-oauth/client/CustomOAuth.ts index 7849498fa4b9a..36b02abb5ca50 100644 --- a/apps/meteor/app/custom-oauth/client/CustomOAuth.ts +++ b/apps/meteor/app/custom-oauth/client/CustomOAuth.ts @@ -18,6 +18,8 @@ import { isURL } from '../../../lib/utils/isURL'; // completion. Takes one argument, credentialToken on success, or Error on // error. +const configuredOAuthServices = new Map(); + export class CustomOAuth implements IOAuthProvider { public serverURL: string; @@ -122,4 +124,32 @@ export class CustomOAuth implements IOAuthProvider { }, }); } + + static configureOAuthService(serviceName: string, options: OauthConfig): CustomOAuth { + const existingInstance = configuredOAuthServices.get(serviceName); + if (existingInstance) { + existingInstance.configure(options); + return existingInstance; + } + + // If we don't have a reference to the instance for this service and it was already registered on meteor, + // then there's nothing we can do to update it + if (Accounts.oauth.serviceNames().includes(serviceName)) { + throw new Error(`CustomOAuth service [${serviceName}] already registered, skipping new configuration.`); + } + + const instance = new CustomOAuth(serviceName, options); + configuredOAuthServices.set(serviceName, instance); + return instance; + } + + static configureCustomOAuthService(serviceName: string, options: OauthConfig): CustomOAuth | undefined { + // Custom OAuth services are configured based on the login service list, so if this ends up being called multiple times, simply ignore it + // Non-Custom OAuth services are configured based on code, so if configureOAuthService is called multiple times for them, it's a bug and it should throw. + try { + return this.configureOAuthService(serviceName, options); + } catch (e) { + console.error(e); + } + } } diff --git a/apps/meteor/app/dolphin/client/hooks/useDolphin.ts b/apps/meteor/app/dolphin/client/hooks/useDolphin.ts index 7a9d700238c95..7ae3266f5a824 100644 --- a/apps/meteor/app/dolphin/client/hooks/useDolphin.ts +++ b/apps/meteor/app/dolphin/client/hooks/useDolphin.ts @@ -16,7 +16,7 @@ const config = { accessTokenParam: 'access_token', }; -const Dolphin = new CustomOAuth('dolphin', config); +const Dolphin = CustomOAuth.configureOAuthService('dolphin', config); export const useDolphin = () => { const enabled = useSetting('Accounts_OAuth_Dolphin'); diff --git a/apps/meteor/app/drupal/client/hooks/useDrupal.ts b/apps/meteor/app/drupal/client/hooks/useDrupal.ts index 33295b33684a3..f4808caa78b93 100644 --- a/apps/meteor/app/drupal/client/hooks/useDrupal.ts +++ b/apps/meteor/app/drupal/client/hooks/useDrupal.ts @@ -23,7 +23,7 @@ const config: OauthConfig = { accessTokenParam: 'access_token', }; -const Drupal = new CustomOAuth('drupal', config); +const Drupal = CustomOAuth.configureOAuthService('drupal', config); export const useDrupal = () => { const drupalUrl = useSetting('API_Drupal_URL') as string; diff --git a/apps/meteor/app/github-enterprise/client/hooks/useGitHubEnterpriseAuth.ts b/apps/meteor/app/github-enterprise/client/hooks/useGitHubEnterpriseAuth.ts index f61af7bde79e2..79af68ef234aa 100644 --- a/apps/meteor/app/github-enterprise/client/hooks/useGitHubEnterpriseAuth.ts +++ b/apps/meteor/app/github-enterprise/client/hooks/useGitHubEnterpriseAuth.ts @@ -18,7 +18,7 @@ const config: OauthConfig = { }, }; -const GitHubEnterprise = new CustomOAuth('github_enterprise', config); +const GitHubEnterprise = CustomOAuth.configureOAuthService('github_enterprise', config); export const useGitHubEnterpriseAuth = () => { const githubApiUrl = useSetting('API_GitHub_Enterprise_URL') as string; diff --git a/apps/meteor/app/gitlab/client/hooks/useGitLabAuth.ts b/apps/meteor/app/gitlab/client/hooks/useGitLabAuth.ts index e5f7c32c701e6..c5776723e4981 100644 --- a/apps/meteor/app/gitlab/client/hooks/useGitLabAuth.ts +++ b/apps/meteor/app/gitlab/client/hooks/useGitLabAuth.ts @@ -16,7 +16,7 @@ const config: OauthConfig = { accessTokenParam: 'access_token', }; -const Gitlab = new CustomOAuth('gitlab', config); +const Gitlab = CustomOAuth.configureOAuthService('gitlab', config); export const useGitLabAuth = () => { const gitlabApiUrl = useSetting('API_Gitlab_URL') as string; diff --git a/apps/meteor/app/nextcloud/client/useNextcloud.ts b/apps/meteor/app/nextcloud/client/useNextcloud.ts index 22151c7ff8c29..580d72e92a42a 100644 --- a/apps/meteor/app/nextcloud/client/useNextcloud.ts +++ b/apps/meteor/app/nextcloud/client/useNextcloud.ts @@ -17,7 +17,7 @@ const config: OauthConfig = { }, }; -const Nextcloud = new CustomOAuth('nextcloud', config); +const Nextcloud = CustomOAuth.configureOAuthService('nextcloud', config); export const useNextcloud = (): void => { const nextcloudURL = useSetting('Accounts_OAuth_Nextcloud_URL') as string; diff --git a/apps/meteor/app/tokenpass/client/hooks/useTokenPassAuth.tsx b/apps/meteor/app/tokenpass/client/hooks/useTokenPassAuth.tsx index 24e31db422260..26dbe3a900e2d 100644 --- a/apps/meteor/app/tokenpass/client/hooks/useTokenPassAuth.tsx +++ b/apps/meteor/app/tokenpass/client/hooks/useTokenPassAuth.tsx @@ -20,7 +20,7 @@ const config: OauthConfig = { accessTokenParam: 'access_token', }; -const Tokenpass = new CustomOAuth('tokenpass', config); +const Tokenpass = CustomOAuth.configureOAuthService('tokenpass', config); export const useTokenPassAuth = () => { const setting = useSetting('API_Tokenpass_URL') as string | undefined; diff --git a/apps/meteor/client/lib/loginServices.ts b/apps/meteor/client/lib/loginServices.ts index ad5ee926ccc78..740e7a1f966c6 100644 --- a/apps/meteor/client/lib/loginServices.ts +++ b/apps/meteor/client/lib/loginServices.ts @@ -49,7 +49,11 @@ class LoginServices extends Emitter { if (state === 'loaded') { this.retries = 0; - this.emit('loaded', services); + try { + this.emit('loaded', services); + } catch (e) { + console.error('Failed to apply loaded listed of login services.', e); + } } } @@ -113,13 +117,15 @@ class LoginServices extends Emitter { return this.serviceButtons; } - public onLoad(callback: (services: LoginServiceConfiguration[]) => void) { + public onLoad(callback: (services: LoginServiceConfiguration[]) => void): () => void { if (this.ready) { - return callback(this.services); + callback(this.services); + return () => undefined; } void this.loadServices(); this.once('loaded', callback); + return () => this.off('loaded', callback); } public async loadServices(): Promise { diff --git a/apps/meteor/client/sidebar/hooks/useCustomOAuth.ts b/apps/meteor/client/sidebar/hooks/useCustomOAuth.ts index b6c4acf6176e4..174cb3173433f 100644 --- a/apps/meteor/client/sidebar/hooks/useCustomOAuth.ts +++ b/apps/meteor/client/sidebar/hooks/useCustomOAuth.ts @@ -4,19 +4,21 @@ import { CustomOAuth } from '../../../app/custom-oauth/client/CustomOAuth'; import { loginServices } from '../../lib/loginServices'; export const useCustomOAuth = () => { - useEffect(() => { - loginServices.onLoad((services) => { - for (const service of services) { - if (!('custom' in service && service.custom)) { - continue; - } + useEffect( + () => + loginServices.onLoad((services) => { + for (const service of services) { + if (!('custom' in service && service.custom)) { + continue; + } - new CustomOAuth(service.service, { - serverURL: service.serverURL, - authorizePath: service.authorizePath, - scope: service.scope, - }); - } - }); - }, []); + CustomOAuth.configureCustomOAuthService(service.service, { + serverURL: service.serverURL, + authorizePath: service.authorizePath, + scope: service.scope, + }); + } + }), + [], + ); }; diff --git a/apps/meteor/client/views/root/hooks/useWordPressOAuth.ts b/apps/meteor/client/views/root/hooks/useWordPressOAuth.ts index f6a539be2d60e..c6b90d741c178 100644 --- a/apps/meteor/client/views/root/hooks/useWordPressOAuth.ts +++ b/apps/meteor/client/views/root/hooks/useWordPressOAuth.ts @@ -13,7 +13,7 @@ const configDefault: OauthConfig = { accessTokenParam: 'access_token', }; -const WordPress = new CustomOAuth('wordpress', configDefault); +const WordPress = CustomOAuth.configureOAuthService('wordpress', configDefault); const configureServerType = ( serverType: string, diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 29445a5a0218a..cffaa88ad8675 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -73,6 +73,8 @@ declare module 'meteor/accounts-base' { ): (credentialTokenOrError?: string | globalThis.Error | Meteor.Error | Meteor.TypedError) => void; function registerService(name: string): void; + + function serviceNames(): string[]; } } } From 6932b48ce387103258fca574b198265759464ae6 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Thu, 24 Apr 2025 17:53:22 -0300 Subject: [PATCH 186/187] fix: OAuth errors are not handled properly due to an internal exception on the error handling code (#35852) --- .changeset/three-parrots-lie.md | 5 +++++ apps/meteor/app/2fa/server/loginHandler.ts | 2 +- apps/meteor/definition/externals/meteor/oauth.d.ts | 9 ++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .changeset/three-parrots-lie.md diff --git a/.changeset/three-parrots-lie.md b/.changeset/three-parrots-lie.md new file mode 100644 index 0000000000000..609fa5183e7c6 --- /dev/null +++ b/.changeset/three-parrots-lie.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where rocket.chat would not properly log OAuth errors nor remove the credential with the error from the internal list diff --git a/apps/meteor/app/2fa/server/loginHandler.ts b/apps/meteor/app/2fa/server/loginHandler.ts index b554330e140f4..1d039c7a89e4a 100644 --- a/apps/meteor/app/2fa/server/loginHandler.ts +++ b/apps/meteor/app/2fa/server/loginHandler.ts @@ -85,7 +85,7 @@ OAuth._retrievePendingCredential = async function (key, ...args): Promise = Omit< + Mongo.Collection, + 'remove' | 'findOne' | 'insert' | 'update' | 'upsert' + >; + interface IOauthCredentials extends IRocketChatRecord { key: string; credentialSecret: string; @@ -17,7 +23,8 @@ declare module 'meteor/oauth' { function openSecret(secret: string): string; function retrieveCredential(credentialToken: string, credentialSecret: string); function _retrieveCredentialSecret(credentialToken: string): string | null; - const _pendingCredentials: Mongo.Collection; + // luckily we don't have any reference to this collection on the client code, so let's type it according to what can be used on the server + const _pendingCredentials: MeteorServerMongoCollection; const _storageTokenPrefix: string; function launchLogin(options: { From 1d51c53b4955b15f264ab0416da2405332e108ca Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Thu, 8 May 2025 23:49:57 +0000 Subject: [PATCH 187/187] Release 7.6.0-rc.8 [no ci] --- .changeset/bump-patch-1746748187032.md | 5 +++ .changeset/pre.json | 3 ++ apps/meteor/CHANGELOG.md | 39 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 +++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 12 ++++++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 14 +++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 14 +++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 15 +++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 15 +++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 14 +++++++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 14 +++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 13 +++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 +++++ ee/packages/license/package.json | 2 +- ee/packages/network-broker/CHANGELOG.md | 9 +++++ ee/packages/network-broker/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 +++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 9 +++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 ++++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 +++++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 10 +++++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 11 ++++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 2 + packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 +++++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 ++++++ packages/ddp-client/package.json | 2 +- packages/freeswitch/CHANGELOG.md | 9 +++++ packages/freeswitch/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 13 +++++++ packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/CHANGELOG.md | 11 ++++++ packages/gazzodown/package.json | 2 +- packages/instance-status/CHANGELOG.md | 9 +++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 9 +++++ packages/livechat/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 +++++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 9 +++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 10 +++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 9 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 +++++ packages/ui-avatar/package.json | 2 +- packages/ui-client/CHANGELOG.md | 10 +++++ packages/ui-client/package.json | 2 +- packages/ui-contexts/CHANGELOG.md | 11 ++++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 10 +++++ packages/ui-video-conf/package.json | 2 +- packages/ui-voip/CHANGELOG.md | 11 ++++++ packages/ui-voip/package.json | 2 +- packages/web-ui-registration/CHANGELOG.md | 9 +++++ packages/web-ui-registration/package.json | 2 +- 76 files changed, 464 insertions(+), 38 deletions(-) create mode 100644 .changeset/bump-patch-1746748187032.md diff --git a/.changeset/bump-patch-1746748187032.md b/.changeset/bump-patch-1746748187032.md new file mode 100644 index 0000000000000..e1eaa7980afb1 --- /dev/null +++ b/.changeset/bump-patch-1746748187032.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/pre.json b/.changeset/pre.json index e900e4f8f8c67..13daa4eeb9e85 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -81,6 +81,7 @@ "bump-patch-1746475877217", "bump-patch-1746477216002", "bump-patch-1746564900627", + "bump-patch-1746748187032", "dirty-seas-explode", "eighty-wombats-smile", "eleven-laws-crash", @@ -109,6 +110,7 @@ "purple-hairs-hang", "rotten-candles-train", "serious-grapes-smell", + "shy-dolls-protect", "slow-ravens-tie", "small-snails-burn", "strong-shoes-end", @@ -118,6 +120,7 @@ "tasty-pianos-bathe", "ten-schools-collect", "thin-cycles-return", + "three-parrots-lie", "tidy-cups-smoke", "warm-steaks-fetch", "wet-penguins-end", diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 8da425d6db373..057a50115a37b 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,44 @@ # @rocket.chat/meteor +## 7.6.0-rc.8 + +### Patch Changes + +- Bump @rocket.chat/meteor version. + +- ([#35864](https://github.com/RocketChat/Rocket.Chat/pull/35864)) Fixes an issue where OAuth login buttons were not showing up on the login page + +- ([#35852](https://github.com/RocketChat/Rocket.Chat/pull/35852)) Fixes an issue where rocket.chat would not properly log OAuth errors nor remove the credential with the error from the internal list + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/license@1.0.12-rc.8 + - @rocket.chat/omnichannel-services@0.3.18-rc.8 + - @rocket.chat/pdf-worker@0.3.0-rc.8 + - @rocket.chat/presence@0.2.21-rc.8 + - @rocket.chat/api-client@0.2.21-rc.8 + - @rocket.chat/apps@0.5.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/cron@0.1.21-rc.8 + - @rocket.chat/freeswitch@1.2.8-rc.8 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.8 + - @rocket.chat/gazzodown@18.0.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/network-broker@0.2.0-rc.8 + - @rocket.chat/ui-theming@0.4.3 + - @rocket.chat/ui-avatar@14.0.0-rc.8 + - @rocket.chat/ui-client@18.0.0-rc.8 + - @rocket.chat/ui-video-conf@18.0.0-rc.8 + - @rocket.chat/ui-voip@8.0.0-rc.8 + - @rocket.chat/web-ui-registration@18.0.0-rc.8 + - @rocket.chat/instance-status@0.1.21-rc.8 +
      + ## 7.6.0-rc.7 ### Patch Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 9ace006d2049f..19c2c4bb18695 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.6.0-rc.7" + "version": "7.6.0-rc.8" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 3224aef96e212..e5ef569ecb3ed 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,19 @@ # rocketchat-services +## 2.0.12-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + ## 2.0.12-rc.7 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 6fcb8ae5c08dd..311dc3bc98769 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "2.0.12-rc.7", + "version": "2.0.12-rc.8", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 727725c3deb7a..32e28f829000a 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.6.0-rc.7", + "version": "7.6.0-rc.8", "private": true, "type": "commonjs", "author": { diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index ed0e579d0a8cb..50dacc6171da0 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/uikit-playground +## 0.6.12-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/fuselage-ui-kit@18.0.0-rc.8 + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-avatar@14.0.0-rc.8 +
      + ## 0.6.12-rc.7 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 05affa6e76d9f..c2dafd5f56df2 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.6.12-rc.7", + "version": "0.6.12-rc.8", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 61a5dcf2a8e14..9a7caf80b6e8c 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/account-service +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + ## 0.4.21-rc.7 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index 0933f4c597218..bc7f1c718cdcd 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.21-rc.7", + "version": "0.4.21-rc.8", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 513e6721b77c4..6b8425aba07c3 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/authorization-service +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + ## 0.4.21-rc.7 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 16a36355a98e7..3daf1a15b8042 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.21-rc.7", + "version": "0.4.21-rc.8", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 28052bd931b5f..134c40573dba5 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/ddp-streamer +## 0.3.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 + - @rocket.chat/instance-status@0.1.21-rc.8 +
      + ## 0.3.21-rc.7 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 062e150c2932a..0aa8302bcbcf7 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.21-rc.7", + "version": "0.3.21-rc.8", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 120d8fdac311b..10efeecdcb03d 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/omnichannel-transcript +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/omnichannel-services@0.3.18-rc.8 + - @rocket.chat/pdf-worker@0.3.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + ## 0.4.21-rc.7 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 686bfe007ed00..af99458f62ade 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.21-rc.7", + "version": "0.4.21-rc.8", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 884cbfa29aef2..b491534f3b981 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/presence-service +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/presence@0.2.21-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + ## 0.4.21-rc.7 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index cbee694a44a81..9f719ce088fdd 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.21-rc.7", + "version": "0.4.21-rc.8", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index 159b8a8d64e1f..2512dd35a7d47 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/queue-worker +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/omnichannel-services@0.3.18-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + ## 0.4.21-rc.7 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index fae1edc48912c..84bb3314ed7d2 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.21-rc.7", + "version": "0.4.21-rc.8", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index c97b8757d238a..be61167d12246 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/stream-hub-service +## 0.4.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 + - @rocket.chat/network-broker@0.2.0-rc.8 +
      + ## 0.4.21-rc.7 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index fe93d78f1beb2..26feefba44419 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.21-rc.7", + "version": "0.4.21-rc.8", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index a7409850cb2fc..02aae794eb287 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 1.0.12-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 +
      + ## 1.0.12-rc.7 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index d210018190caa..ea7128f6ced9f 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "1.0.12-rc.7", + "version": "1.0.12-rc.8", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/network-broker/CHANGELOG.md b/ee/packages/network-broker/CHANGELOG.md index e877bb5e762e5..8ac59acc29ca8 100644 --- a/ee/packages/network-broker/CHANGELOG.md +++ b/ee/packages/network-broker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/network-broker +## 0.2.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-services@0.9.0-rc.8 +
      + ## 0.2.0-rc.7 ### Patch Changes diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index 6c8806d9477a4..e35e35ab7e5a0 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/network-broker", - "version": "0.2.0-rc.7", + "version": "0.2.0-rc.8", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 8288c0fc56327..a3037ae4f2199 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.18-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/pdf-worker@0.3.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 +
      + ## 0.3.18-rc.7 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 0afdf699f3db1..d67c0d7408df1 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.18-rc.7", + "version": "0.3.18-rc.8", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 5bc2ec9d347e7..e52b030de3bbe 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.3.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 +
      + ## 0.3.0-rc.7 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index 9c8f0bd818d58..1051e6cc44304 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.3.0-rc.7", + "version": "0.3.0-rc.8", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 3b18414204e25..bc466f1b0549b 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/core-services@0.9.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 +
      + ## 0.2.21-rc.7 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index ff7029b55543d..e721892b1515d 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.21-rc.7", + "version": "0.2.21-rc.8", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/package.json b/package.json index e6d80b4e62700..32378f60dda88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.6.0-rc.7", + "version": "7.6.0-rc.8", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index adf83f2e92692..4d82cc2577803 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 +
      + ## 0.2.21-rc.7 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 98a5c4450bb91..35df2c8236fbf 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.21-rc.7", + "version": "0.2.21-rc.8", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index 908b0cbf116ae..6bad5953285be 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.5.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 +
      + ## 0.5.0-rc.7 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index 682ca7ec4ace3..a885412150fcd 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.5.0-rc.7", + "version": "0.5.0-rc.8", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index 121f5a6529c78..989fa02520dba 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/core-services +## 0.9.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 +
      + ## 0.9.0-rc.7 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index ff71f55991e46..b2b1dae9e028d 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.9.0-rc.7", + "version": "0.9.0-rc.8", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index cb877be00fc7d..78ce955ca5fc2 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,7 @@ # @rocket.chat/core-typings +## 7.6.0-rc.8 + ## 7.6.0-rc.7 ## 7.6.0-rc.6 diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 4833aa8f0c746..ce8084c83ab7a 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.6.0-rc.7", + "version": "7.6.0-rc.8", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 0bfdd58c15275..cd9c9ef64e06e 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/models@1.5.0-rc.8 +
      + ## 0.1.21-rc.7 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index 94bd1e3150160..ad5a5ca4678e7 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.21-rc.7", + "version": "0.1.21-rc.8", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index c71c2be8bef82..d45a2a406eec1 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/api-client@0.2.21-rc.8 +
      + ## 0.3.21-rc.7 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index d5b4228741daa..8def562d76a28 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.21-rc.7", + "version": "0.3.21-rc.8", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.14", diff --git a/packages/freeswitch/CHANGELOG.md b/packages/freeswitch/CHANGELOG.md index 7c0babf06efdf..392c827f397d3 100644 --- a/packages/freeswitch/CHANGELOG.md +++ b/packages/freeswitch/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/freeswitch +## 1.2.8-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 +
      + ## 1.2.8-rc.7 ### Patch Changes diff --git a/packages/freeswitch/package.json b/packages/freeswitch/package.json index de6cd36e8efe7..1776a21518074 100644 --- a/packages/freeswitch/package.json +++ b/packages/freeswitch/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/freeswitch", - "version": "1.2.8-rc.7", + "version": "1.2.8-rc.8", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 7c0bedd63f713..520dc2040cfec 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/gazzodown@18.0.0-rc.8 + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-avatar@14.0.0-rc.8 + - @rocket.chat/ui-video-conf@18.0.0-rc.8 +
      + ## 18.0.0-rc.7 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 7299945c91ecb..c37a1b4210635 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/fuselage-ui-kit", - "version": "18.0.0-rc.7", + "version": "18.0.0-rc.8", "private": true, "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index 288bbcc33cfa4..c24ca4c8ecddd 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/gazzodown +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-client@18.0.0-rc.8 +
      + ## 18.0.0-rc.7 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 05cc736c38152..7c26ae320ceb6 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "18.0.0-rc.7", + "version": "18.0.0-rc.8", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index c49b1ee103492..75c42bbe71f00 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.21-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/models@1.5.0-rc.8 +
      + ## 0.1.21-rc.7 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 357810be2f8b5..7bcb10f5a558e 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.21-rc.7", + "version": "0.1.21-rc.8", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 55b9b3446e55c..e88df49a38106 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/livechat Change Log +## 1.22.8-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/gazzodown@18.0.0-rc.8 +
      + ## 1.22.8-rc.7 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index fd9b145604c01..ebc55aedcb787 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.22.8-rc.7", + "version": "1.22.8-rc.8", "files": [ "/build" ], diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index 213c4cbbc97c9..54af2e39efb80 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.2.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 +
      + ## 0.2.0-rc.7 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 76657d329838b..c1284dd7a9abf 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.2.0-rc.7", + "version": "0.2.0-rc.8", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index fa0f2581e9fad..39049f7d1d933 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/model-typings +## 1.6.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 +
      + ## 1.6.0-rc.7 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 1c8b0d09dca20..e5ebdc6867b86 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "1.6.0-rc.7", + "version": "1.6.0-rc.8", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index f2450f3a20fb8..5c77862c98291 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/models +## 1.5.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/model-typings@1.6.0-rc.8 +
      + ## 1.5.0-rc.7 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index 9002aa88a6f2b..33c861e7e8439 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "1.5.0-rc.7", + "version": "1.5.0-rc.8", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 9cd8292384049..edd532bf843aa 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/rest-typings +## 7.6.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 +
      + ## 7.6.0-rc.7 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 8b1fdfa04b0d2..a260dcc60e981 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.6.0-rc.7", + "version": "7.6.0-rc.8", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index 93b483ad4f151..df654755c455d 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 14.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 +
      + ## 14.0.0-rc.7 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index b8ace14d001a0..28c58d121091e 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "14.0.0-rc.7", + "version": "14.0.0-rc.8", "private": true, "devDependencies": { "@babel/core": "~7.26.0", diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index d221cf7240473..332a157940c92 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-client +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-avatar@14.0.0-rc.8 +
      + ## 18.0.0-rc.7 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 99d603ede0606..de31109761248 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "18.0.0-rc.7", + "version": "18.0.0-rc.8", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index 08e24f681f950..30dac1c163578 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-contexts +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/core-typings@7.6.0-rc.8 + - @rocket.chat/rest-typings@7.6.0-rc.8 + - @rocket.chat/ddp-client@0.3.21-rc.8 +
      + ## 18.0.0-rc.7 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 35592ea1604c9..2870a25e0eedc 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "18.0.0-rc.7", + "version": "18.0.0-rc.8", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index c6002d0e3a765..ebecd4f7bb7f5 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-avatar@14.0.0-rc.8 +
      + ## 18.0.0-rc.7 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 56b3dfdb27bdc..7411e43d6bd0e 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "18.0.0-rc.7", + "version": "18.0.0-rc.8", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-voip/CHANGELOG.md b/packages/ui-voip/CHANGELOG.md index 0dc7f330481a2..ea17859e5c493 100644 --- a/packages/ui-voip/CHANGELOG.md +++ b/packages/ui-voip/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ui-voip +## 8.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 + - @rocket.chat/ui-avatar@14.0.0-rc.8 + - @rocket.chat/ui-client@18.0.0-rc.8 +
      + ## 8.0.0-rc.7 ### Patch Changes diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 628242c702455..db15ccef619e8 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-voip", - "version": "8.0.0-rc.7", + "version": "8.0.0-rc.8", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index 94f6e0256f8e8..822f0f45f6c46 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 18.0.0-rc.8 + +### Patch Changes + +-
      Updated dependencies []: + + - @rocket.chat/ui-contexts@18.0.0-rc.8 +
      + ## 18.0.0-rc.7 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index e7d72b62fea98..c81da6286b533 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "18.0.0-rc.7", + "version": "18.0.0-rc.8", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js",