diff --git a/.changeset/cool-zoos-move.md b/.changeset/cool-zoos-move.md new file mode 100644 index 000000000000..dda6fbe2b02e --- /dev/null +++ b/.changeset/cool-zoos-move.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed threads breaking when sending messages too fast diff --git a/.changeset/eleven-gorillas-deliver.md b/.changeset/eleven-gorillas-deliver.md new file mode 100644 index 000000000000..403bd294828b --- /dev/null +++ b/.changeset/eleven-gorillas-deliver.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix trying to upload same file again and again. diff --git a/.changeset/gentle-radios-relate.md b/.changeset/gentle-radios-relate.md new file mode 100644 index 000000000000..8d5f12b3a286 --- /dev/null +++ b/.changeset/gentle-radios-relate.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed DM room with "guest" user kept as "read only" after reactivating user diff --git a/.changeset/heavy-ads-carry.md b/.changeset/heavy-ads-carry.md new file mode 100644 index 000000000000..c04e52fb48a0 --- /dev/null +++ b/.changeset/heavy-ads-carry.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: Change plan name from Enterprise to Premium on marketplace filtering diff --git a/.changeset/lucky-vans-develop.md b/.changeset/lucky-vans-develop.md new file mode 100644 index 000000000000..e57b7a1e68d5 --- /dev/null +++ b/.changeset/lucky-vans-develop.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue with file attachments in rooms' messages export having no content diff --git a/.changeset/old-zoos-hang.md b/.changeset/old-zoos-hang.md new file mode 100644 index 000000000000..eb39a6c9d83c --- /dev/null +++ b/.changeset/old-zoos-hang.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +fix: mobile ringing notification missing call id diff --git a/.changeset/perfect-pianos-yawn.md b/.changeset/perfect-pianos-yawn.md new file mode 100644 index 000000000000..349bca33ecf7 --- /dev/null +++ b/.changeset/perfect-pianos-yawn.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/presence': minor +--- + +Add peak connections monitoring and methods to get and reset the counter diff --git a/.changeset/popular-actors-cheat.md b/.changeset/popular-actors-cheat.md new file mode 100644 index 000000000000..aad5ec6ae638 --- /dev/null +++ b/.changeset/popular-actors-cheat.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +Do not allow auto-translation to be enabled in E2E rooms diff --git a/.changeset/proud-shrimps-cheat.md b/.changeset/proud-shrimps-cheat.md new file mode 100644 index 000000000000..cad8bc8bfa32 --- /dev/null +++ b/.changeset/proud-shrimps-cheat.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: Unable to send attachments via email as an omni-agent diff --git a/.changeset/rich-dogs-smell.md b/.changeset/rich-dogs-smell.md new file mode 100644 index 000000000000..be27db28e227 --- /dev/null +++ b/.changeset/rich-dogs-smell.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Fix typing indicator of Apps user diff --git a/.changeset/shiny-pillows-run.md b/.changeset/shiny-pillows-run.md new file mode 100644 index 000000000000..9a85d37a2f9d --- /dev/null +++ b/.changeset/shiny-pillows-run.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: cloud alerts not working diff --git a/.changeset/slow-coats-shout.md b/.changeset/slow-coats-shout.md new file mode 100644 index 000000000000..4a226e84d161 --- /dev/null +++ b/.changeset/slow-coats-shout.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +--- + +Add the daily and monthly peaks of concurrent connections to statistics + - Added `dailyPeakConnections` statistic for monitoring the daily peak of concurrent connections in a workspace; + - Added `maxMonthlyPeakConnections` statistic for monitoring the last 30 days peak of concurrent connections in a workspace; diff --git a/.changeset/stale-masks-learn.md b/.changeset/stale-masks-learn.md new file mode 100644 index 000000000000..1523b02b0c95 --- /dev/null +++ b/.changeset/stale-masks-learn.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/server-fetch': patch +--- + +Fixed an issue where the payload of an HTTP request made by an app wouldn't be correctly encoded in some cases diff --git a/.changeset/tough-apples-turn.md b/.changeset/tough-apples-turn.md new file mode 100644 index 000000000000..056a0645186e --- /dev/null +++ b/.changeset/tough-apples-turn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Forward headers when using proxy for file uploads diff --git a/.changeset/weak-cameras-pay.md b/.changeset/weak-cameras-pay.md new file mode 100644 index 000000000000..724f3af69a29 --- /dev/null +++ b/.changeset/weak-cameras-pay.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue with message read receipts not being created when accessing a room the first time diff --git a/.changeset/wicked-jars-double.md b/.changeset/wicked-jars-double.md new file mode 100644 index 000000000000..23deffe8606f --- /dev/null +++ b/.changeset/wicked-jars-double.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Handle the username update in the background diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 76a0545c8801..0ace08bb8446 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -288,7 +288,7 @@ export class AppLivechatBridge extends LivechatBridge { throw new Error('Could not get the message converter to process livechat room messages'); } - const livechatMessages = await Livechat.getRoomMessages({ rid: roomId }); + const livechatMessages = await LivechatTyped.getRoomMessages({ rid: roomId }); return Promise.all(livechatMessages.map((message) => messageConverter.convertMessage(message) as Promise)); } diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index d75a0c244674..e4d09018176d 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -103,7 +103,11 @@ export class AppMessageBridge extends MessageBridge { protected async typing({ scope, id, username, isTyping }: ITypingDescriptor): Promise { switch (scope) { case 'room': - notifications.notifyRoom(id, 'typing', username!, isTyping); + if (!username) { + throw new Error('Invalid username'); + } + + notifications.notifyRoom(id, 'user-activity', username, isTyping ? ['user-typing'] : []); return; default: throw new Error('Unrecognized typing scope provided'); diff --git a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts index 1ba5bcdfcd76..e396d78887a9 100644 --- a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts +++ b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts @@ -1,4 +1,4 @@ -import { Subscriptions } from '@rocket.chat/models'; +import { Subscriptions, Rooms } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -46,6 +46,13 @@ Meteor.methods({ switch (field) { case 'autoTranslate': + const room = await Rooms.findE2ERoomById(rid, { projection: { _id: 1 } }); + if (room && value === '1') { + throw new Meteor.Error('error-e2e-enabled', 'Enabling auto-translation in E2E encrypted rooms is not allowed', { + method: 'saveAutoTranslateSettings', + }); + } + await Subscriptions.updateAutoTranslateById(subscription._id, value === '1'); if (!subscription.autoTranslateLanguage && options.defaultLanguage) { await Subscriptions.updateAutoTranslateLanguageById(subscription._id, options.defaultLanguage); diff --git a/apps/meteor/app/cas/server/cas_server.js b/apps/meteor/app/cas/server/cas_server.js index 25d3b9fdd698..60880c77d4f4 100644 --- a/apps/meteor/app/cas/server/cas_server.js +++ b/apps/meteor/app/cas/server/cas_server.js @@ -257,7 +257,7 @@ Accounts.registerLoginHandler('cas', async (options) => { if (roomName) { let room = await Rooms.findOneByNameAndType(roomName, 'c'); if (!room) { - room = await createRoom('c', roomName, user.username); + room = await createRoom('c', roomName, user); } } } diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts index dc57307b1c4c..ed07540ba2b0 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomEncrypted.ts @@ -1,7 +1,7 @@ import { Message } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; import { isRegisterUser } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import { Rooms, Subscriptions } from '@rocket.chat/models'; import { Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import type { UpdateResult } from 'mongodb'; @@ -25,5 +25,9 @@ export const saveRoomEncrypted = async function (rid: string, encrypted: boolean await Message.saveSystemMessage(type, rid, user.username, user); } + + if (encrypted) { + await Subscriptions.disableAutoTranslateByRoomId(rid); + } return update; }; diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts index 2ab8a4b27a62..f9f0cfadc669 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts @@ -73,7 +73,7 @@ export async function getWorkspaceLicense(): Promise<{ updated: boolean; license const payload = await fetchCloudWorkspaceLicensePayload({ token }); - if (Date.parse(payload.updatedAt) <= currentLicense._updatedAt.getTime()) { + if (currentLicense.value && Date.parse(payload.updatedAt) <= currentLicense._updatedAt.getTime()) { return fromCurrentLicense(); } diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts index 26d98b4a7574..f3885c1e95c2 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/announcementSync.ts @@ -107,7 +107,7 @@ export async function announcementSync() { } catch (err) { SystemLogger.error({ msg: 'Failed to sync with Rocket.Chat Cloud', - url: '/sync', + url: '/comms/workspace', err, }); } diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts b/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts index 3173e652afe5..bdd898b510f7 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts @@ -1,3 +1,4 @@ +import { SystemLogger } from '../../../../../server/lib/logger/system'; import { CloudWorkspaceAccessTokenError } from '../getWorkspaceAccessToken'; import { getCachedSupportedVersionsToken } from '../supportedVersionsToken/supportedVersionsToken'; import { announcementSync } from './announcementSync'; @@ -7,10 +8,11 @@ export async function syncWorkspace() { try { await syncCloudData(); await announcementSync(); - } catch (error) { - if (error instanceof CloudWorkspaceAccessTokenError) { + } catch (err) { + if (err instanceof CloudWorkspaceAccessTokenError) { // TODO: Remove License if there is no access token } + SystemLogger.error({ msg: 'Error during workspace sync', err }); } await getCachedSupportedVersionsToken.reset(); diff --git a/apps/meteor/app/file-upload/server/config/AmazonS3.ts b/apps/meteor/app/file-upload/server/config/AmazonS3.ts index b97ff60d86d6..567e5e5d71eb 100644 --- a/apps/meteor/app/file-upload/server/config/AmazonS3.ts +++ b/apps/meteor/app/file-upload/server/config/AmazonS3.ts @@ -32,12 +32,15 @@ const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, const copy: FileUploadClass['copy'] = async function (this: FileUploadClass, file, out) { const fileUrl = await this.store.getRedirectURL(file); - if (fileUrl) { - const request = /^https:/.test(fileUrl) ? https : http; - request.get(fileUrl, (fileRes) => fileRes.pipe(out)); - } else { + if (!fileUrl) { out.end(); + return; } + + const request = /^https:/.test(fileUrl) ? https : http; + return new Promise((resolve) => { + request.get(fileUrl, (fileRes) => fileRes.pipe(out).on('finish', () => resolve())); + }); }; const AmazonS3Uploads = new FileUploadClass({ diff --git a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts index 124bad4365a0..41eb4350b876 100644 --- a/apps/meteor/app/file-upload/server/config/GoogleStorage.ts +++ b/apps/meteor/app/file-upload/server/config/GoogleStorage.ts @@ -32,12 +32,15 @@ const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, const copy: FileUploadClass['copy'] = async function (this: FileUploadClass, file, out) { const fileUrl = await this.store.getRedirectURL(file, false); - if (fileUrl) { - const request = /^https:/.test(fileUrl) ? https : http; - request.get(fileUrl, (fileRes) => fileRes.pipe(out)); - } else { + if (!fileUrl) { out.end(); + return; } + + const request = /^https:/.test(fileUrl) ? https : http; + return new Promise((resolve) => { + request.get(fileUrl, (fileRes) => fileRes.pipe(out).on('finish', () => resolve())); + }); }; const GoogleCloudStorageUploads = new FileUploadClass({ diff --git a/apps/meteor/app/file-upload/server/config/Webdav.ts b/apps/meteor/app/file-upload/server/config/Webdav.ts index fb8c1ca82ca4..901c74e9c149 100644 --- a/apps/meteor/app/file-upload/server/config/Webdav.ts +++ b/apps/meteor/app/file-upload/server/config/Webdav.ts @@ -19,7 +19,9 @@ const get: FileUploadClass['get'] = async function (this: FileUploadClass, file, }; const copy: FileUploadClass['copy'] = async function (this: FileUploadClass, file, out) { - (await this.store.getReadStream(file._id, file)).pipe(out); + return new Promise(async (resolve) => { + (await this.store.getReadStream(file._id, file)).pipe(out).on('finish', () => resolve()); + }); }; const WebdavUploads = new FileUploadClass({ diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index 8f929a17fe34..e512e5d09bfe 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -562,7 +562,32 @@ export const FileUpload = { ) { res.setHeader('Content-Disposition', `${forceDownload ? 'attachment' : 'inline'}; filename="${encodeURI(fileName)}"`); - request.get(fileUrl, (fileRes) => fileRes.pipe(res)); + request.get(fileUrl, (fileRes) => { + if (fileRes.statusCode !== 200) { + res.setHeader('x-rc-proxyfile-status', String(fileRes.statusCode)); + res.setHeader('content-length', 0); + res.writeHead(500); + res.end(); + return; + } + + // eslint-disable-next-line prettier/prettier + const headersToProxy = [ + 'age', + 'cache-control', + 'content-length', + 'content-type', + 'date', + 'expired', + 'last-modified', + ]; + + headersToProxy.forEach((header) => { + fileRes.headers[header] && res.setHeader(header, String(fileRes.headers[header])); + }); + + fileRes.pipe(res); + }); }, generateJWTToFileUrls({ rid, userId, fileId }: { rid: string; userId: string; fileId: string }) { diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js index 0968eacc5340..bb5053ffdd71 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js @@ -16,7 +16,7 @@ export default async function handleJoinedChannel(args) { let room = await Rooms.findOneByName(args.roomName); if (!room) { - const createdRoom = await createRoom('c', args.roomName, user.username, []); + const createdRoom = await createRoom('c', args.roomName, user, []); room = await Rooms.findOne({ _id: createdRoom.rid }); this.log(`${user.username} created room ${args.roomName}`); diff --git a/apps/meteor/app/lib/server/functions/saveUser.js b/apps/meteor/app/lib/server/functions/saveUser.js index 42438be4ab7a..46bef4c7d1aa 100644 --- a/apps/meteor/app/lib/server/functions/saveUser.js +++ b/apps/meteor/app/lib/server/functions/saveUser.js @@ -366,6 +366,7 @@ export const saveUser = async function (userId, userData) { _id: userData._id, username: userData.username, name: userData.name, + updateUsernameInBackground: true, })) ) { throw new Meteor.Error('error-could-not-save-identity', 'Could not save user identity', { diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts index 2eb360e150c6..34ca0ca246db 100644 --- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts +++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts @@ -1,5 +1,7 @@ +import type { IUser } from '@rocket.chat/core-typings'; import { Messages, VideoConference, LivechatDepartmentAgents, Rooms, Subscriptions, Users } from '@rocket.chat/models'; +import { SystemLogger } from '../../../../server/lib/logger/system'; import { FileUpload } from '../../../file-upload/server'; import { _setRealName } from './setRealName'; import { _setUsername } from './setUsername'; @@ -11,7 +13,17 @@ import { validateName } from './validateName'; * @param {object} changes changes to the user */ -export async function saveUserIdentity({ _id, name: rawName, username: rawUsername }: { _id: string; name?: string; username?: string }) { +export async function saveUserIdentity({ + _id, + name: rawName, + username: rawUsername, + updateUsernameInBackground = false, +}: { + _id: string; + name?: string; + username?: string; + updateUsernameInBackground?: boolean; // TODO: remove this +}) { if (!_id) { return false; } @@ -48,46 +60,91 @@ export async function saveUserIdentity({ _id, name: rawName, username: rawUserna // if coming from old username, update all references if (previousUsername) { - if (usernameChanged && typeof rawUsername !== 'undefined') { - const fileStore = FileUpload.getStore('Avatars'); - const previousFile = await fileStore.model.findOneByName(previousUsername); - const file = await fileStore.model.findOneByName(username); - if (file) { - await fileStore.model.deleteFile(file._id); - } - if (previousFile) { - await fileStore.model.updateFileNameById(previousFile._id, username); - } - - await Messages.updateAllUsernamesByUserId(user._id, username); - await Messages.updateUsernameOfEditByUserId(user._id, username); - - const cursor = Messages.findByMention(previousUsername); - for await (const msg of cursor) { - const updatedMsg = msg.msg.replace(new RegExp(`@${previousUsername}`, 'ig'), `@${username}`); - await Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername(msg._id, previousUsername, username, updatedMsg); - } - - await Rooms.replaceUsername(previousUsername, username); - await Rooms.replaceMutedUsername(previousUsername, username); - await Rooms.replaceUsernameOfUserByUserId(user._id, username); - await Subscriptions.setUserUsernameByUserId(user._id, username); - - await LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username); + const handleUpdateParams = { + username, + previousUsername, + rawUsername, + usernameChanged, + user, + name, + previousName, + rawName, + nameChanged, + }; + if (updateUsernameInBackground) { + setImmediate(async () => { + try { + await updateUsernameReferences(handleUpdateParams); + } catch (err) { + SystemLogger.error(err); + } + }); + } else { + await updateUsernameReferences(handleUpdateParams); } + } + + return true; +} - // update other references if either the name or username has changed - if (usernameChanged || nameChanged) { - // update name and fname of 1-on-1 direct messages - await Subscriptions.updateDirectNameAndFnameByName(previousUsername, rawUsername && username, rawName && name); +async function updateUsernameReferences({ + username, + previousUsername, + rawUsername, + usernameChanged, + user, + name, + previousName, + rawName, + nameChanged, +}: { + username: string; + previousUsername: string; + rawUsername?: string; + usernameChanged: boolean; + user: IUser; + name: string; + previousName: string | undefined; + rawName?: string; + nameChanged: boolean; +}): Promise { + if (usernameChanged && typeof rawUsername !== 'undefined') { + const fileStore = FileUpload.getStore('Avatars'); + const previousFile = await fileStore.model.findOneByName(previousUsername); + const file = await fileStore.model.findOneByName(username); + if (file) { + await fileStore.model.deleteFile(file._id); + } + if (previousFile) { + await fileStore.model.updateFileNameById(previousFile._id, username); + } - // update name and fname of group direct messages - await updateGroupDMsName(user); + await Messages.updateAllUsernamesByUserId(user._id, username); + await Messages.updateUsernameOfEditByUserId(user._id, username); - // update name and username of users on video conferences - await VideoConference.updateUserReferences(user._id, username || previousUsername, name || previousName); + const cursor = Messages.findByMention(previousUsername); + for await (const msg of cursor) { + const updatedMsg = msg.msg.replace(new RegExp(`@${previousUsername}`, 'ig'), `@${username}`); + await Messages.updateUsernameAndMessageOfMentionByIdAndOldUsername(msg._id, previousUsername, username, updatedMsg); } + + await Rooms.replaceUsername(previousUsername, username); + await Rooms.replaceMutedUsername(previousUsername, username); + await Rooms.replaceUsernameOfUserByUserId(user._id, username); + await Subscriptions.setUserUsernameByUserId(user._id, username); + + await LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username); } - return true; + // update other references if either the name or username has changed + if (usernameChanged || nameChanged) { + // update name and fname of 1-on-1 direct messages + await Subscriptions.updateDirectNameAndFnameByName(previousUsername, rawUsername && username, rawName && name); + + // update name and fname of group direct messages + await updateGroupDMsName(user); + + // update name and username of users on video conferences + await VideoConference.updateUserReferences(user._id, username || previousUsername, name || previousName); + } } diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 6540b67d79aa..095baefaa294 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -17,6 +17,7 @@ import { } from '../../../server/api/lib/departments'; import { DepartmentHelper } from '../../../server/lib/Departments'; import { Livechat } from '../../../server/lib/Livechat'; +import { Livechat as LivechatTs } from '../../../server/lib/LivechatTyped'; API.v1.addRoute( 'livechat/department', @@ -192,7 +193,7 @@ API.v1.addRoute( }, { async post() { - await Livechat.archiveDepartment(this.urlParams._id); + await LivechatTs.archiveDepartment(this.urlParams._id); return API.v1.success(); }, @@ -207,11 +208,8 @@ API.v1.addRoute( }, { async post() { - if (await Livechat.unarchiveDepartment(this.urlParams._id)) { - return API.v1.success(); - } - - return API.v1.failure(); + await LivechatTs.unarchiveDepartment(this.urlParams._id); + return API.v1.success(); }, }, ); diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 86629e636bf8..8f6151797463 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -18,7 +18,7 @@ import { callbacks } from '../../../../../lib/callbacks'; import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; import { isWidget } from '../../../../api/server/helpers/isWidget'; -import { canAccessRoomAsync } from '../../../../authorization/server'; +import { canAccessRoomAsync, roomAccessAttributes } from '../../../../authorization/server'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; import { settings as rcSettings } from '../../../../settings/server'; @@ -352,7 +352,12 @@ API.v1.addRoute( API.v1.addRoute( 'livechat/room.visitor', - { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isPUTLivechatRoomVisitorParams, deprecationVersion: '7.0.0' }, + { + authRequired: true, + permissionsRequired: ['change-livechat-room-visitor'], + validateParams: isPUTLivechatRoomVisitorParams, + deprecationVersion: '7.0.0', + }, { async put() { // This endpoint is deprecated and will be removed in future versions. @@ -363,7 +368,7 @@ API.v1.addRoute( throw new Error('invalid-visitor'); } - const room = await LivechatRooms.findOneById(rid, { _id: 1, v: 1 }); // TODO: check _id + const room = await LivechatRooms.findOneById(rid, { projection: { ...roomAccessAttributes, _id: 1, t: 1, v: 1 } }); // TODO: check _id if (!room) { throw new Error('invalid-room'); } @@ -373,7 +378,7 @@ API.v1.addRoute( throw new Error('invalid-room-visitor'); } - const roomAfterChange = await Livechat.changeRoomVisitor(this.userId, rid, visitor); + const roomAfterChange = await LivechatTyped.changeRoomVisitor(this.userId, room, visitor); if (!roomAfterChange) { return API.v1.failure(); diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.ts b/apps/meteor/app/livechat/server/api/v1/videoCall.ts index 5ce0ddc4ca37..52cd8738bec9 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.ts +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.ts @@ -6,7 +6,7 @@ import { i18n } from '../../../../../server/lib/i18n'; import { API } from '../../../../api/server'; import { canSendMessageAsync } from '../../../../authorization/server/functions/canSendMessage'; import { settings as rcSettings } from '../../../../settings/server'; -import { Livechat } from '../../lib/Livechat'; +import { Livechat } from '../../lib/LivechatTyped'; import { settings } from '../lib/livechat'; API.v1.addRoute( diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index ae9d1ea4fd83..84f7b96e155d 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -174,7 +174,7 @@ API.v1.addRoute('livechat/visitor.callStatus', { if (!guest) { throw new Meteor.Error('invalid-token'); } - await Livechat.updateCallStatus(callId, rid, callStatus, guest); + await LivechatTyped.updateCallStatus(callId, rid, callStatus, guest); return API.v1.success({ token, callStatus }); }, }); diff --git a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts index 7b4d9b89f14c..6f42a910417d 100644 --- a/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts +++ b/apps/meteor/app/livechat/server/hooks/saveContactLastChat.ts @@ -1,7 +1,7 @@ import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { callbacks } from '../../../../lib/callbacks'; -import { Livechat } from '../lib/Livechat'; +import { Livechat } from '../lib/LivechatTyped'; callbacks.add( 'livechat.newRoom', diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index c560f3dd7aa7..e1d6626c7ddb 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -4,7 +4,7 @@ import dns from 'dns'; import util from 'util'; -import { Message, VideoConf, api } from '@rocket.chat/core-services'; +import { Message } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { LivechatVisitors, @@ -30,7 +30,6 @@ 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, roomAccessAttributes } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { FileUpload } from '../../../file-upload/server'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; @@ -718,40 +717,6 @@ export const Livechat = { return ret; }, - async unarchiveDepartment(_id) { - check(_id, String); - - const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - - // TODO: these kind of actions should be on events instead of here - await LivechatDepartmentAgents.enableAgentsByDepartmentId(_id); - return LivechatDepartmentRaw.unarchiveDepartment(_id); - }, - - async archiveDepartment(_id) { - check(_id, String); - - const department = await LivechatDepartmentRaw.findOneById(_id, { projection: { _id: 1 } }); - - if (!department) { - throw new Meteor.Error('department-not-found', 'Department not found', { - method: 'livechat:removeDepartment', - }); - } - - await LivechatDepartmentAgents.disableAgentsByDepartmentId(_id); - await LivechatDepartmentRaw.archiveDepartment(_id); - - this.logger.debug({ msg: 'Running livechat.afterDepartmentArchived callback for department:', departmentId: _id }); - await callbacks.run('livechat.afterDepartmentArchived', department); - }, - showConnecting() { const { showConnecting } = RoutingManager.getConfig(); return showConnecting; @@ -767,28 +732,6 @@ export const Livechat = { }); }, - async getRoomMessages({ rid }) { - check(rid, String); - - const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); - if (room?.t !== 'l') { - throw new Meteor.Error('invalid-room'); - } - - const ignoredMessageTypes = [ - 'livechat_navigation_history', - 'livechat_transcript_history', - 'command', - 'livechat-close', - 'livechat-started', - 'livechat_video_call', - ]; - - return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { - sort: { ts: 1 }, - }).toArray(); - }, - async requestTranscript({ rid, email, subject, user }) { check(rid, String); check(email, String); @@ -892,20 +835,6 @@ export const Livechat = { }); }, - async notifyAgentStatusChanged(userId, status) { - 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 allowAgentChangeServiceStatus(statusLivechat, agentId) { if (statusLivechat !== 'available') { return true; @@ -913,56 +842,4 @@ export const Livechat = { return businessHourManager.allowAgentChangeServiceStatus(agentId); }, - - notifyRoomVisitorChange(roomId, visitor) { - void api.broadcast('omnichannel.room', roomId, { - type: 'visitorData', - visitor, - }); - }, - - async changeRoomVisitor(userId, roomId, visitor) { - const user = await Users.findOneById(userId); - if (!user) { - throw new Error('error-user-not-found'); - } - - if (!(await hasPermissionAsync(userId, 'change-livechat-room-visitor'))) { - throw new Error('error-not-authorized'); - } - - const room = await LivechatRooms.findOneById(roomId, { ...roomAccessAttributes, _id: 1, t: 1 }); - - if (!room) { - throw new Meteor.Error('invalid-room'); - } - - if (!(await canAccessRoomAsync(room, user))) { - throw new Error('error-not-allowed'); - } - - await LivechatRooms.changeVisitorByRoomId(room._id, visitor); - - Livechat.notifyRoomVisitorChange(room._id, visitor); - - return LivechatRooms.findOneById(roomId); - }, - async updateLastChat(contactId, lastChat) { - const updateUser = { - $set: { - lastChat, - }, - }; - await LivechatVisitors.updateById(contactId, updateUser); - }, - async updateCallStatus(callId, rid, status, user) { - 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() }, user); - } - }, }; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index c443bc7873c7..afb649488300 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1,4 +1,4 @@ -import { Message } from '@rocket.chat/core-services'; +import { Message, VideoConf, api } from '@rocket.chat/core-services'; import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo, @@ -23,6 +23,7 @@ import { Users, LivechatDepartmentAgents, ReadReceipts, + Rooms, } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; @@ -32,8 +33,10 @@ import type { FindCursor, UpdateFilter } from 'mongodb'; import { Apps, AppEvents } from '../../../../ee/server/apps'; import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; +import { canAccessRoomAsync } from '../../../authorization/server'; import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { updateMessage } from '../../../lib/server/functions/updateMessage'; import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; @@ -808,6 +811,112 @@ class LivechatClass { 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 updateLastChat(contactId: string, lastChat: Required) { + const updateUser = { + $set: { + lastChat, + }, + }; + await LivechatVisitors.updateById(contactId, updateUser); + } + + 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 getRoomMessages({ rid }: { rid: string }) { + const room = await Rooms.findOneById(rid, { projection: { t: 1 } }); + if (room?.t !== 'l') { + throw new Meteor.Error('invalid-room'); + } + + const ignoredMessageTypes: MessageTypesValues[] = [ + 'livechat_navigation_history', + 'livechat_transcript_history', + 'command', + 'livechat-close', + 'livechat-started', + 'livechat_video_call', + ]; + + return Messages.findVisibleByRoomIdNotContainingTypes(rid, ignoredMessageTypes, { + sort: { ts: 1 }, + }).toArray(); + } + + async archiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Error('department-not-found'); + } + + await Promise.all([LivechatDepartmentAgents.disableAgentsByDepartmentId(_id), LivechatDepartment.archiveDepartment(_id)]); + + await callbacks.run('livechat.afterDepartmentArchived', department); + } + + async unarchiveDepartment(_id: string) { + const department = await LivechatDepartment.findOneById(_id, { projection: { _id: 1 } }); + + if (!department) { + throw new Meteor.Error('department-not-found'); + } + + // TODO: these kind of actions should be on events instead of here + await Promise.all([LivechatDepartmentAgents.enableAgentsByDepartmentId(_id), LivechatDepartment.unarchiveDepartment(_id)]); + return true; + } } export const Livechat = new LivechatClass(); diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.js index d0ef8157137d..f76c33fa1f81 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.js @@ -295,7 +295,7 @@ export default class RocketAdapter { try { const isPrivate = slackChannel.is_private; - const rocketChannel = await createRoom(isPrivate ? 'p' : 'c', slackChannel.name, rocketUserCreator.username, rocketUsers); + const rocketChannel = await createRoom(isPrivate ? 'p' : 'c', slackChannel.name, rocketUserCreator, rocketUsers); slackChannel.rocketId = rocketChannel.rid; } catch (e) { if (!hasRetried) { diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 64543deb88a1..1d60def846b5 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -1,7 +1,7 @@ import { log } from 'console'; import os from 'os'; -import { Analytics, Team, VideoConf } from '@rocket.chat/core-services'; +import { Analytics, Team, VideoConf, Presence } from '@rocket.chat/core-services'; import type { IRoom, IStats } from '@rocket.chat/core-typings'; import { UserStatus } from '@rocket.chat/core-typings'; import { @@ -579,6 +579,11 @@ export const statistics = { const defaultLoggedInCustomScript = (await Settings.findOneById('Custom_Script_Logged_In'))?.packageValue; statistics.loggedInCustomScriptChanged = settings.get('Custom_Script_Logged_In') !== defaultLoggedInCustomScript; + statistics.dailyPeakConnections = await Presence.getPeakConnections(true); + + const peak = await Statistics.findMonthlyPeakConnections(); + statistics.maxMonthlyPeakConnections = Math.max(statistics.dailyPeakConnections, peak?.dailyPeakConnections || 0); + statistics.matrixFederation = await getMatrixFederationStatistics(); // Omnichannel call stats diff --git a/apps/meteor/app/version-check/server/functions/getNewUpdates.ts b/apps/meteor/app/version-check/server/functions/getNewUpdates.ts index ac0c0e443453..d17191a09be7 100644 --- a/apps/meteor/app/version-check/server/functions/getNewUpdates.ts +++ b/apps/meteor/app/version-check/server/functions/getNewUpdates.ts @@ -50,18 +50,16 @@ export const getNewUpdates = async () => { infoUrl: String, }), ], - alerts: [ - Match.Optional([ - Match.ObjectIncluding({ - id: String, - title: String, - text: String, - textArguments: [Match.Any], - modifiers: [String] as [StringConstructor], - infoUrl: String, - }), - ]), - ], + alerts: Match.Optional([ + Match.ObjectIncluding({ + id: String, + title: String, + text: String, + textArguments: [Match.Any], + modifiers: [String] as [StringConstructor], + infoUrl: String, + }), + ]), }), ); diff --git a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx index 32948eb42243..04b07e9cd627 100644 --- a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx @@ -71,7 +71,7 @@ const RoomAvatarEditor = ({ disabled = false, room, roomAvatar, onChangeAvatar } danger icon='trash' title={t('Accounts_SetDefaultAvatar')} - disabled={roomAvatar === null || isRoomFederated(room) || disabled} + disabled={!roomAvatar || isRoomFederated(room) || disabled} onClick={clickReset} /> diff --git a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts index e1c3126985ae..73b0f34836e1 100644 --- a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts @@ -5,13 +5,15 @@ import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { e2e } from '../../../app/e2e/client/rocketchat.e2e'; -import { useRoom } from '../../views/room/contexts/RoomContext'; +import { dispatchToastMessage } from '../../lib/toast'; +import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext'; import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; import { useReactiveValue } from '../useReactiveValue'; export const useE2EERoomAction = () => { const enabled = useSetting('E2E_Enable', false); const room = useRoom(); + const subscription = useRoomSubscription(); const readyToEncrypt = useReactiveValue(useCallback(() => e2e.isReady(), [])) || room.encrypted; const permittedToToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id); const permittedToEditRoom = usePermission('edit-room', room._id); @@ -21,8 +23,22 @@ export const useE2EERoomAction = () => { const toggleE2E = useEndpoint('POST', '/v1/rooms.saveRoomSettings'); - const action = useMutableCallback(() => { - void toggleE2E({ rid: room._id, encrypted: !room.encrypted }); + const action = useMutableCallback(async () => { + const { success } = await toggleE2E({ rid: room._id, encrypted: !room.encrypted }); + if (!success) { + return; + } + + dispatchToastMessage({ + type: 'success', + message: room.encrypted + ? t('E2E_Encryption_disabled_for_room', { roomName: room.name }) + : t('E2E_Encryption_enabled_for_room', { roomName: room.name }), + }); + + if (subscription?.autoTranslate) { + dispatchToastMessage({ type: 'success', message: t('AutoTranslate_Disabled_for_room', { roomName: room.name }) }); + } }); const enabledOnRoom = !!room.encrypted; diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 1630071be658..8242de07d791 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -144,7 +144,7 @@ export type ChatAPI = { ActionManager: any; readonly flows: { - readonly uploadFiles: (files: readonly File[]) => Promise; + readonly uploadFiles: (files: readonly File[], resetFileInput?: () => void) => Promise; readonly sendMessage: ({ text, tshow }: { text: string; tshow?: boolean; previewUrls?: string[] }) => Promise; readonly processSlashCommand: (message: IMessage, userId: string | null) => Promise; readonly processTooLongMessage: (message: IMessage) => Promise; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 58eb18400a30..1411ad5a004e 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -6,7 +6,7 @@ import { imperativeModal } from '../../imperativeModal'; import { prependReplies } from '../../utils/prependReplies'; import type { ChatAPI } from '../ChatAPI'; -export const uploadFiles = async (chat: ChatAPI, files: readonly File[]): Promise => { +export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { const replies = chat.composer?.quotedMessages.get() ?? []; const msg = await prependReplies('', replies); @@ -52,4 +52,5 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[]): Promis }; uploadNextFile(); + resetFileInput?.(); }; diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index b96c54d4c955..f275ff2800d8 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -33,6 +33,30 @@ const getMessage = (room: IRoom, lastMessage: IMessage | undefined, t: ReturnTyp return `${lastMessage.u.name || lastMessage.u.username}: ${normalizeSidebarMessage(lastMessage, t)}`; }; +const getBadgeTitle = ( + userMentions: number, + threadUnread: number, + groupMentions: number, + unread: number, + t: ReturnType, +) => { + const title = [] as string[]; + if (userMentions) { + title.push(t('mentions_counter', { count: userMentions })); + } + if (threadUnread) { + title.push(t('threads_counter', { count: threadUnread })); + } + if (groupMentions) { + title.push(t('group_mentions_counter', { count: groupMentions })); + } + const count = unread - userMentions - groupMentions; + if (count > 0) { + title.push(t('unread_messages_counter', { count })); + } + return title.join(', '); +}; + type RoomListRowProps = { extended: boolean; t: ReturnType; @@ -137,10 +161,12 @@ function SideBarItemTemplateWithData({ const isUnread = unread > 0 || threadUnread; const showBadge = !hideUnreadStatus || (!hideMentionStatus && (Boolean(userMentions) || tunreadUser.length > 0)); + const badgeTitle = getBadgeTitle(userMentions, tunread.length, groupMentions, unread, t); + const badges = ( {showBadge && isUnread && ( - + {unread + tunread?.length} )} diff --git a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx index 41709d247f8b..d3242e68ae2c 100644 --- a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx @@ -272,6 +272,8 @@ export default { totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, push: 0, + dailyPeakConnections: 0, + maxMonthlyPeakConnections: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx index 29c0c00d5814..d05c0f7372a2 100644 --- a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx +++ b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx @@ -302,6 +302,8 @@ export default { totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, push: 0, + dailyPeakConnections: 0, + maxMonthlyPeakConnections: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx index a65e645b17d0..4c6cc871ede7 100644 --- a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx +++ b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx @@ -250,6 +250,8 @@ export default { totalWebRTCCalls: 0, uncaughtExceptionsCount: 0, push: 0, + dailyPeakConnections: 0, + maxMonthlyPeakConnections: 0, matrixFederation: { enabled: false, }, diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.tsx b/apps/meteor/client/views/admin/rooms/EditRoom.tsx index b12b5e3e1ab9..8130f2e9ec8b 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -13,18 +13,17 @@ import { TextAreaInput, } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useRoute, usePermission, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar'; -import GenericModal from '../../../components/GenericModal'; import RoomAvatarEditor from '../../../components/avatar/RoomAvatarEditor'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; import { useForm } from '../../../hooks/useForm'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; -import DeleteTeamModalWithRooms from '../../teams/contextualBar/info/DeleteTeam'; +import { useDeleteRoom } from '../../hooks/roomActions/useDeleteRoom'; type EditRoomProps = { room: Pick; @@ -65,11 +64,6 @@ const getInitialValues = (room: Pick): EditRoomFormV const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => { const t = useTranslation(); - const [deleting, setDeleting] = useState(false); - - const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); - const { values, handlers, hasUnsavedChanges, reset } = useForm(getInitialValues(room)); const [canViewName, canViewTopic, canViewAnnouncement, canViewArchived, canViewDescription, canViewType, canViewReadOnly] = @@ -119,9 +113,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => const changeArchivation = archived !== !!room.archived; - const roomsRoute = useRoute('admin-rooms'); - - const canDelete = usePermission(`delete-${room.t}`); + const { handleDelete, canDeleteRoom, isDeleting } = useDeleteRoom(room, { reload: onDelete }); const archiveSelector = room.archived ? 'unarchive' : 'archive'; const archiveMessage = room.archived ? 'Room_has_been_unarchived' : 'Room_has_been_archived'; @@ -161,61 +153,6 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => handleRoomType(roomType === 'p' ? 'c' : 'p'); }); - const deleteRoom = useEndpoint('POST', '/v1/rooms.delete'); - const deleteTeam = useEndpoint('POST', '/v1/teams.delete'); - - const handleDelete = useMutableCallback(() => { - const handleDeleteTeam = async (roomsToRemove: IRoom['_id'][]) => { - try { - setDeleting(true); - setModal(null); - await deleteTeam({ teamId: room.teamId as string, ...(roomsToRemove.length && { roomsToRemove }) }); - dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); - roomsRoute.push({}); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - setDeleting(false); - } finally { - onDelete(); - } - }; - - if (room.teamMain) { - setModal( - setModal(null)} teamId={room.teamId as string} />, - ); - - return; - } - - const handleDeleteRoom = async (): Promise => { - try { - setDeleting(true); - setModal(null); - await deleteRoom({ roomId: room._id }); - dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); - roomsRoute.push({}); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - setDeleting(false); - } finally { - onDelete(); - } - }; - - setModal( - setModal(null)} - onCancel={(): void => setModal(null)} - confirmText={t('Yes_delete_it')} - > - {t('Delete_Room_Warning')} - , - ); - }); - return ( <> e.preventDefault())}> @@ -227,7 +164,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Name')} - + {room.t !== 'd' && ( @@ -246,7 +183,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Topic')} - + )} @@ -281,7 +218,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Private')} - + {t('Just_invited_people_can_access_this_channel')} @@ -292,7 +229,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Read_only')} - + {t('Only_authorized_users_can_write_new_messages')} @@ -314,7 +251,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Room_archivation_state_true')} - + @@ -325,7 +262,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Default')} - + @@ -333,7 +270,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Favorite')} - + @@ -341,22 +278,22 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Featured')} - + - - - diff --git a/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx b/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx index cd300c14a481..9307a2596a71 100644 --- a/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx +++ b/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx @@ -1,4 +1,4 @@ -import { Box, Icon, Skeleton } from '@rocket.chat/fuselage'; +import { Box, Icon, Skeleton, Scrollable } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -10,8 +10,8 @@ const AnalyticsReports = () => { const { data, isLoading, isSuccess, isError } = useAnalyticsObject(); return ( - <> - + + @@ -23,18 +23,15 @@ const AnalyticsReports = () => { {t('Analytics_page_briefing_second_paragraph')} - - {isSuccess &&
{JSON.stringify(data, null, '\t')}
} - {isError && t('Something_went_wrong_try_again_later')} - {isLoading && ( - <> - - - - - )} -
- + + + {isSuccess &&
{JSON.stringify(data, null, '\t')}
} + {isError && t('Something_went_wrong_try_again_later')} + {isLoading && Array.from({ length: 10 }).map((_, index) => )} + <> +
+
+
); }; diff --git a/apps/meteor/client/views/admin/viewLogs/ServerLogs.tsx b/apps/meteor/client/views/admin/viewLogs/ServerLogs.tsx index 4787dfb12a6d..aa496de024fa 100644 --- a/apps/meteor/client/views/admin/viewLogs/ServerLogs.tsx +++ b/apps/meteor/client/views/admin/viewLogs/ServerLogs.tsx @@ -166,7 +166,7 @@ const ServerLogs = (): ReactElement => { }, [sendToBottomIfNecessary]); return ( - + { const t = useTranslation(); - const [tab, setTab] = useState('Logs'); return ( - + - - + + setTab('Logs')} selected={tab === 'Logs'}> {t('Logs')} @@ -24,8 +23,8 @@ const ViewLogsPage = (): ReactElement => { {t('Analytic_reports')} - {tab === 'Logs' ? : } - +
+ {tab === 'Logs' ? : } ); }; diff --git a/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx new file mode 100644 index 000000000000..be4728732284 --- /dev/null +++ b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx @@ -0,0 +1,89 @@ +import type { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useSetModal, useToastMessageDispatch, useRouter, usePermission, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import React from 'react'; + +import GenericModal from '../../../components/GenericModal'; +import DeleteTeamModal from '../../teams/contextualBar/info/DeleteTeam'; + +export const useDeleteRoom = (room: IRoom | Pick, { reload }: { reload?: () => void } = {}) => { + const t = useTranslation(); + const router = useRouter(); + const setModal = useSetModal(); + const dispatchToastMessage = useToastMessageDispatch(); + const hasPermissionToDelete = usePermission(`delete-${room.t}`, room._id); + const canDeleteRoom = isRoomFederated(room) ? false : hasPermissionToDelete; + + const isAdminRoute = router.getRouteName() === 'admin-rooms'; + + const deleteRoomEndpoint = useEndpoint('POST', '/v1/rooms.delete'); + const deleteTeamEndpoint = useEndpoint('POST', '/v1/teams.delete'); + + const deleteRoomMutation = useMutation({ + mutationFn: deleteRoomEndpoint, + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); + if (isAdminRoute) { + return router.navigate('/admin/rooms'); + } + + return router.navigate('/home'); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { + setModal(null); + reload?.(); + }, + }); + + const deleteTeamMutation = useMutation({ + mutationFn: deleteTeamEndpoint, + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); + if (isAdminRoute) { + return router.navigate('/admin/rooms'); + } + + return router.navigate('/home'); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { + setModal(null); + reload?.(); + }, + }); + + const isDeleting = deleteTeamMutation.isLoading || deleteRoomMutation.isLoading; + + const handleDelete = useMutableCallback(() => { + const handleDeleteTeam = async (roomsToRemove: IRoom['_id'][]) => { + if (!room.teamId) { + return; + } + + deleteTeamMutation.mutateAsync({ teamId: room.teamId, ...(roomsToRemove.length && { roomsToRemove }) }); + }; + + if (room.teamMain && room.teamId) { + return setModal( setModal(null)} teamId={room.teamId} />); + } + + const handleDeleteRoom = async () => { + deleteRoomMutation.mutateAsync({ roomId: room._id }); + }; + + setModal( + setModal(null)} confirmText={t('Yes_delete_it')}> + {t('Delete_Room_Warning')} + , + ); + }); + + return { handleDelete, canDeleteRoom, isDeleting }; +}; diff --git a/apps/meteor/client/views/marketplace/AppInstallPage.js b/apps/meteor/client/views/marketplace/AppInstallPage.js index 77afd5361c88..f3b4f02b0c16 100644 --- a/apps/meteor/client/views/marketplace/AppInstallPage.js +++ b/apps/meteor/client/views/marketplace/AppInstallPage.js @@ -1,4 +1,5 @@ -import { Button, ButtonGroup, Icon, Field, FieldGroup, TextInput, Throbber } from '@rocket.chat/fuselage'; +import { Button, ButtonGroup, Icon, Field, FieldGroup, FieldLabel, FieldRow, TextInput } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useEndpoint, @@ -8,13 +9,13 @@ import { useRouter, useSearchParameter, } from '@rocket.chat/ui-contexts'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useState } from 'react'; +import { useForm, Controller } from 'react-hook-form'; import { AppClientOrchestratorInstance } from '../../../ee/client/apps/orchestrator'; import Page from '../../components/Page'; import { useAppsReload } from '../../contexts/hooks/useAppsReload'; import { useFileInput } from '../../hooks/useFileInput'; -import { useForm } from '../../hooks/useForm'; import AppPermissionsReviewModal from './AppPermissionsReviewModal'; import AppUpdateModal from './AppUpdateModal'; import AppInstallModal from './components/AppInstallModal/AppInstallModal'; @@ -48,22 +49,12 @@ function AppInstallPage() { const appCountQuery = useAppsCountQuery('private'); - const { values, handlers } = useForm({ - file: {}, - url: queryUrl, - }); + const { control, setValue, watch } = useForm({ defaultValues: { url: queryUrl || '' } }); + const { file, url } = watch(); - const { file, url } = values; + const canSave = !!url || !!file?.name; - const canSave = !!url || !!file.name; - - const { handleFile, handleUrl } = handlers; - - useEffect(() => { - queryUrl && handleUrl(queryUrl); - }, [queryUrl, handleUrl]); - - const [handleUploadButtonClick] = useFileInput(handleFile, 'app'); + const [handleUploadButtonClick] = useFileInput((value) => setValue('file', value), 'app'); const sendFile = async (permissionsGranted, appFile, appId) => { let app; @@ -200,35 +191,52 @@ function AppInstallPage() { }); }; + const urlField = useUniqueId(); + const fileField = useUniqueId(); + return ( - {t('App_Url_to_Install_From')} - - } /> - + {t('App_Url_to_Install_From')} + + ( + } {...field} /> + )} + /> + - {t('App_Url_to_Install_From_File')} - - - {t('Browse_Files')} - - } + {t('App_Url_to_Install_From_File')} + + ( + + {t('Browse_Files')} + + } + /> + )} /> - + diff --git a/apps/meteor/client/views/marketplace/hooks/useFilteredApps.ts b/apps/meteor/client/views/marketplace/hooks/useFilteredApps.ts index 221990f7af2a..437c8d35207d 100644 --- a/apps/meteor/client/views/marketplace/hooks/useFilteredApps.ts +++ b/apps/meteor/client/views/marketplace/hooks/useFilteredApps.ts @@ -80,7 +80,7 @@ export const useFilteredApps = ({ explore: fallback, installed: fallback, private: fallback, - premium: (apps: App[]) => apps.filter(({ categories }) => categories.includes('Enterprise')), + premium: (apps: App[]) => apps.filter(({ categories }) => categories.includes('Premium')), requested: (apps: App[]) => apps.filter(({ appRequestStats, installed }) => Boolean(appRequestStats) && !installed), }; diff --git a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx index 35aecc1e2dfb..dbfda21f5b7a 100644 --- a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx +++ b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx @@ -2,20 +2,31 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import colors from '@rocket.chat/fuselage-tokens/colors'; import { HeaderState } from '@rocket.chat/ui-client'; -import { useSetting, usePermission, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetting, usePermission, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; +import { dispatchToastMessage } from '../../../../lib/toast'; + const Encrypted = ({ room }: { room: IRoom }) => { const t = useTranslation(); const e2eEnabled = useSetting('E2E_Enable'); - const toggleE2E = useMethod('saveRoomSettings'); + const toggleE2E = useEndpoint('POST', '/v1/rooms.saveRoomSettings'); const canToggleE2E = usePermission('toggle-room-e2e-encryption'); const encryptedLabel = canToggleE2E ? t('Encrypted_key_title') : t('Encrypted'); - const handleE2EClick = useMutableCallback(() => { + const handleE2EClick = useMutableCallback(async () => { if (!canToggleE2E) { return; } - toggleE2E(room._id, 'encrypted', !room?.encrypted); + + const { success } = await toggleE2E({ rid: room._id, encrypted: !room.encrypted }); + if (!success) { + return; + } + + dispatchToastMessage({ + type: 'success', + message: t('E2E_Encryption_disabled_for_room', { roomName: room.name }), + }); }); return e2eEnabled && room?.encrypted ? ( diff --git a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx index b84a867d11d7..4677bb5e1ad3 100644 --- a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx +++ b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameList.tsx @@ -1,4 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { Margins } from '@rocket.chat/fuselage'; import { useSetting } from '@rocket.chat/ui-contexts'; import type { VFC } from 'react'; import React from 'react'; @@ -11,7 +12,7 @@ type RoomForewordUsernameListProps = { usernames: Array = ({ usernames }) => { const useRealName = Boolean(useSetting('UI_Use_Real_Name')); return ( - <> + {usernames.map((username) => ( = ({ username useRealName={useRealName} /> ))} - + ); }; diff --git a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx index 5ac168b91846..a0732b35d29d 100644 --- a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx +++ b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx @@ -1,5 +1,5 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { Box, Icon, Tag, Skeleton } from '@rocket.chat/fuselage'; +import { Icon, Tag, Skeleton } from '@rocket.chat/fuselage'; import type { VFC } from 'react'; import React from 'react'; @@ -16,13 +16,11 @@ const RoomForewordUsernameListItem: VFC = ({ const { data, isLoading, isError } = useUserInfoQuery({ username }); return ( - - } className='mention-link' data-username={username} large> - {isLoading && } - {!isLoading && isError && username} - {!isLoading && !isError && getUserDisplayName(data?.user?.name, username, useRealName)} - - + } data-username={username} large href={href}> + {isLoading && } + {!isLoading && isError && username} + {!isLoading && !isError && getUserDisplayName(data?.user?.name, username, useRealName)} + ); }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx index 73b293a60047..f9c826fceb4b 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx @@ -18,6 +18,14 @@ const FileUploadAction = ({ collapsed, chatContext, disabled, ...props }: FileUp const fileInputRef = useRef(null); const chat = useChat() ?? chatContext; + const resetFileInput = () => { + if (!fileInputRef.current) { + return; + } + + fileInputRef.current.value = ''; + }; + const handleUploadChange = async (e: ChangeEvent) => { const { mime } = await import('../../../../../../../app/utils/lib/mimeTypes'); const filesToUpload = Array.from(e.target.files ?? []).map((file) => { @@ -26,8 +34,7 @@ const FileUploadAction = ({ collapsed, chatContext, disabled, ...props }: FileUp }); return file; }); - - chat?.flows.uploadFiles(filesToUpload); + chat?.flows.uploadFiles(filesToUpload, resetFileInput); }; const handleUpload = () => { diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx index 6952b5b1dafe..ad1560d3078d 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx @@ -1,4 +1,4 @@ -import { FieldGroup, Field, FieldLabel, FieldRow, ToggleSwitch, Select } from '@rocket.chat/fuselage'; +import { Callout, FieldGroup, Field, FieldLabel, FieldRow, ToggleSwitch, Select } from '@rocket.chat/fuselage'; import type { SelectOption } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent } from 'react'; @@ -11,6 +11,7 @@ import { ContextualbarIcon, ContextualbarContent, } from '../../../../components/Contextualbar'; +import { useRoom } from '../../contexts/RoomContext'; type AutoTranslateProps = { language: string; @@ -30,6 +31,7 @@ const AutoTranslate = ({ handleClose, }: AutoTranslateProps): ReactElement => { const t = useTranslation(); + const room = useRoom(); return ( <> @@ -40,14 +42,24 @@ const AutoTranslate = ({ + {room.encrypted && ( + + {t('Automatic_translation_not_available_info')} + + )} - + {t('Automatic_Translation')} - {t('Language')} + {t('Translate_to')}