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/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/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<IAppsEngineMesage>)); } 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<ServerMethods>({ 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/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/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<void> { + 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<ILivechatVisitor['lastChat']>) { + 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/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/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} /> </ButtonGroup> 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<void>; + readonly uploadFiles: (files: readonly File[], resetFileInput?: () => void) => Promise<void>; readonly sendMessage: ({ text, tshow }: { text: string; tshow?: boolean; previewUrls?: string[] }) => Promise<boolean>; readonly processSlashCommand: (message: IMessage, userId: string | null) => Promise<boolean>; readonly processTooLongMessage: (message: IMessage) => Promise<boolean>; 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<void> => { +export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise<void> => { 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<typeof useTranslation>, +) => { + 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<typeof useTranslation>; @@ -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 = ( <Margins inlineStart={8}> {showBadge && isUnread && ( - <Badge {...({ style: { display: 'inline-flex', flexShrink: 0 } } as any)} variant={variant}> + <Badge {...({ style: { display: 'inline-flex', flexShrink: 0 } } as any)} variant={variant} title={badgeTitle}> {unread + tunread?.length} </Badge> )} 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<IRoom, RoomAdminFieldsType>; @@ -65,11 +64,6 @@ const getInitialValues = (room: Pick<IRoom, RoomAdminFieldsType>): 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( - <DeleteTeamModalWithRooms onConfirm={handleDeleteTeam} onCancel={(): void => setModal(null)} teamId={room.teamId as string} />, - ); - - return; - } - - const handleDeleteRoom = async (): Promise<void> => { - 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( - <GenericModal - variant='danger' - onConfirm={handleDeleteRoom} - onClose={(): void => setModal(null)} - onCancel={(): void => setModal(null)} - confirmText={t('Yes_delete_it')} - > - {t('Delete_Room_Warning')} - </GenericModal>, - ); - }); - return ( <> <ContextualbarScrollableContent is='form' onSubmit={useMutableCallback((e) => e.preventDefault())}> @@ -227,7 +164,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => <Field> <FieldLabel required>{t('Name')}</FieldLabel> <FieldRow> - <TextInput disabled={deleting || !canViewName} value={roomName} onChange={handleRoomName} flexGrow={1} /> + <TextInput disabled={isDeleting || !canViewName} value={roomName} onChange={handleRoomName} flexGrow={1} /> </FieldRow> </Field> {room.t !== 'd' && ( @@ -246,7 +183,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => <FieldRow> <TextAreaInput rows={4} - disabled={deleting || isRoomFederated(room)} + disabled={isDeleting || isRoomFederated(room)} value={roomDescription} onChange={handleRoomDescription} flexGrow={1} @@ -260,7 +197,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => <FieldRow> <TextAreaInput rows={4} - disabled={deleting || isRoomFederated(room)} + disabled={isDeleting || isRoomFederated(room)} value={roomAnnouncement} onChange={handleRoomAnnouncement} flexGrow={1} @@ -272,7 +209,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => <Field> <FieldLabel>{t('Topic')}</FieldLabel> <FieldRow> - <TextAreaInput rows={4} disabled={deleting} value={roomTopic} onChange={handleRoomTopic} flexGrow={1} /> + <TextAreaInput rows={4} disabled={isDeleting} value={roomTopic} onChange={handleRoomTopic} flexGrow={1} /> </FieldRow> </Field> )} @@ -281,7 +218,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> <FieldLabel>{t('Private')}</FieldLabel> <FieldRow> - <ToggleSwitch disabled={deleting || isRoomFederated(room)} checked={roomType === 'p'} onChange={changeRoomType} /> + <ToggleSwitch disabled={isDeleting || isRoomFederated(room)} checked={roomType === 'p'} onChange={changeRoomType} /> </FieldRow> </Box> <FieldHint>{t('Just_invited_people_can_access_this_channel')}</FieldHint> @@ -292,7 +229,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> <FieldLabel>{t('Read_only')}</FieldLabel> <FieldRow> - <ToggleSwitch disabled={deleting || isRoomFederated(room)} checked={readOnly} onChange={handleReadOnly} /> + <ToggleSwitch disabled={isDeleting || isRoomFederated(room)} checked={readOnly} onChange={handleReadOnly} /> </FieldRow> </Box> <FieldHint>{t('Only_authorized_users_can_write_new_messages')}</FieldHint> @@ -314,7 +251,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> <FieldLabel>{t('Room_archivation_state_true')}</FieldLabel> <FieldRow> - <ToggleSwitch disabled={deleting || isRoomFederated(room)} checked={archived} onChange={handleArchived} /> + <ToggleSwitch disabled={isDeleting || isRoomFederated(room)} checked={archived} onChange={handleArchived} /> </FieldRow> </Box> </Field> @@ -325,7 +262,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> <FieldLabel>{t('Default')}</FieldLabel> <FieldRow> - <ToggleSwitch disabled={deleting || isRoomFederated(room)} checked={isDefault} onChange={handleIsDefault} /> + <ToggleSwitch disabled={isDeleting || isRoomFederated(room)} checked={isDefault} onChange={handleIsDefault} /> </FieldRow> </Box> </Field> @@ -333,7 +270,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> <FieldLabel>{t('Favorite')}</FieldLabel> <FieldRow> - <ToggleSwitch disabled={deleting} checked={favorite} onChange={handleFavorite} /> + <ToggleSwitch disabled={isDeleting} checked={favorite} onChange={handleFavorite} /> </FieldRow> </Box> </Field> @@ -341,22 +278,22 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> <FieldLabel>{t('Featured')}</FieldLabel> <FieldRow> - <ToggleSwitch disabled={deleting || isRoomFederated(room)} checked={featured} onChange={handleFeatured} /> + <ToggleSwitch disabled={isDeleting || isRoomFederated(room)} checked={featured} onChange={handleFeatured} /> </FieldRow> </Box> </Field> </ContextualbarScrollableContent> <ContextualbarFooter> <ButtonGroup stretch> - <Button type='reset' disabled={!hasUnsavedChanges || deleting} onClick={reset}> + <Button type='reset' disabled={!hasUnsavedChanges || isDeleting} onClick={reset}> {t('Reset')} </Button> - <Button disabled={!hasUnsavedChanges || deleting} onClick={handleSave}> + <Button disabled={!hasUnsavedChanges || isDeleting} onClick={handleSave}> {t('Save')} </Button> </ButtonGroup> <ButtonGroup mbs={8} stretch> - <Button icon='trash' danger disabled={deleting || !canDelete || isRoomFederated(room)} onClick={handleDelete}> + <Button icon='trash' danger disabled={isDeleting || !canDeleteRoom || isRoomFederated(room)} onClick={handleDelete}> {t('Delete')} </Button> </ButtonGroup> 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 ( - <> - <Box p={20} pbe={28} mbe={24}> + <Box display='flex' flexDirection='column' overflow='hidden' height='100%'> + <Box backgroundColor='light' p={20} pbe={28} mbe={16} borderRadius={4}> <Box display='flex' flexDirection='row' alignItems='center' mbe={20}> <Box display='flex' justifyContent='center' alignItems='center' borderRadius={2} p={4} mie={8} bg='status-background-info'> <Icon name='info' size={20} color='info' /> @@ -23,18 +23,15 @@ const AnalyticsReports = () => { </Box> <Box fontScale='p1'>{t('Analytics_page_briefing_second_paragraph')}</Box> </Box> - <Box display='flex' flexDirection='column' padding={8} flexGrow={1} color='default' bg='neutral' borderRadius={4} overflow='scroll'> - {isSuccess && <pre>{JSON.stringify(data, null, '\t')}</pre>} - {isError && t('Something_went_wrong_try_again_later')} - {isLoading && ( - <> - <Skeleton /> - <Skeleton /> - <Skeleton /> - </> - )} - </Box> - </> + <Scrollable vertical> + <Box mbe={8} padding={8} bg='neutral' borderRadius={4} height='100%'> + {isSuccess && <pre>{JSON.stringify(data, null, '\t')}</pre>} + {isError && t('Something_went_wrong_try_again_later')} + {isLoading && Array.from({ length: 10 }).map((_, index) => <Skeleton key={index} />)} + <></> + </Box> + </Scrollable> + </Box> ); }; 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 ( - <Box width='full' height='full' overflow='hidden' position='relative' display='flex' marginBlock={8}> + <Box width='full' height='full' overflow='hidden' position='relative' display='flex' mbe={8}> <Scrollable vertical> <Box ref={wrapperRef} diff --git a/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx b/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx index 2c1613f3ef74..8defbb997c36 100644 --- a/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx +++ b/apps/meteor/client/views/admin/viewLogs/ViewLogsPage.tsx @@ -1,4 +1,4 @@ -import { Tabs } from '@rocket.chat/fuselage'; +import { Margins, Tabs } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; @@ -9,14 +9,13 @@ import ServerLogs from './ServerLogs'; const ViewLogsPage = (): ReactElement => { const t = useTranslation(); - const [tab, setTab] = useState('Logs'); return ( - <Page> + <Page background='tint'> <Page.Header title={t('Reports')} /> - <Page.Content> - <Tabs mbe={24}> + <Margins blockEnd={24}> + <Tabs> <Tabs.Item onClick={() => setTab('Logs')} selected={tab === 'Logs'}> {t('Logs')} </Tabs.Item> @@ -24,8 +23,8 @@ const ViewLogsPage = (): ReactElement => { {t('Analytic_reports')} </Tabs.Item> </Tabs> - {tab === 'Logs' ? <ServerLogs /> : <AnalyticsReports />} - </Page.Content> + </Margins> + <Page.Content>{tab === 'Logs' ? <ServerLogs /> : <AnalyticsReports />}</Page.Content> </Page> ); }; 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<IRoom, RoomAdminFieldsType>, { 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(<DeleteTeamModal onConfirm={handleDeleteTeam} onCancel={(): void => setModal(null)} teamId={room.teamId} />); + } + + const handleDeleteRoom = async () => { + deleteRoomMutation.mutateAsync({ roomId: room._id }); + }; + + setModal( + <GenericModal variant='danger' onConfirm={handleDeleteRoom} onCancel={(): void => setModal(null)} confirmText={t('Yes_delete_it')}> + {t('Delete_Room_Warning')} + </GenericModal>, + ); + }); + + 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 ( <Page flexDirection='column'> <Page.Header title={t('App_Installation')} /> <Page.ScrollableContent> <FieldGroup display='flex' flexDirection='column' alignSelf='center' maxWidth='x600' w='full'> <Field> - <Field.Label>{t('App_Url_to_Install_From')}</Field.Label> - <Field.Row> - <TextInput placeholder={placeholderUrl} value={url} onChange={handleUrl} addon={<Icon name='permalink' size='x20' />} /> - </Field.Row> + <FieldLabel htmlFor={urlField}>{t('App_Url_to_Install_From')}</FieldLabel> + <FieldRow> + <Controller + name='url' + control={control} + render={({ field }) => ( + <TextInput id={urlField} placeholder={placeholderUrl} addon={<Icon name='permalink' size='x20' />} {...field} /> + )} + /> + </FieldRow> </Field> <Field> - <Field.Label>{t('App_Url_to_Install_From_File')}</Field.Label> - <Field.Row> - <TextInput - value={file.name || ''} - addon={ - <Button icon='upload' small primary onClick={handleUploadButtonClick} mb='neg-x4' mie='neg-x8'> - {t('Browse_Files')} - </Button> - } + <FieldLabel htmlFor={fileField}>{t('App_Url_to_Install_From_File')}</FieldLabel> + <FieldRow> + <Controller + name='file' + control={control} + render={({ field }) => ( + <TextInput + id={fileField} + readOnly + {...field} + value={field.value?.name || ''} + addon={ + <Button icon='upload' small primary onClick={handleUploadButtonClick} mb='neg-x4' mie='neg-x8'> + {t('Browse_Files')} + </Button> + } + /> + )} /> - </Field.Row> + </FieldRow> </Field> <Field> <ButtonGroup> <Button disabled={!canSave || installing} onClick={install}> - {!installing && t('Install')} - {installing && <Throbber inheritColor />} + {installing ? t('Installing') : t('Install')} </Button> <Button onClick={handleCancel}>{t('Cancel')}</Button> </ButtonGroup> 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 ? ( <HeaderState title={encryptedLabel} icon='key' onClick={handleE2EClick} color={colors.s500} tiny /> 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<NonNullable<IUser['usern const RoomForewordUsernameList: VFC<RoomForewordUsernameListProps> = ({ usernames }) => { const useRealName = Boolean(useSetting('UI_Use_Real_Name')); return ( - <> + <Margins inline={4}> {usernames.map((username) => ( <RoomForewordUsernameListItem username={username} @@ -20,7 +21,7 @@ const RoomForewordUsernameList: VFC<RoomForewordUsernameListProps> = ({ username useRealName={useRealName} /> ))} - </> + </Margins> ); }; 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<RoomForewordUsernameListItemProps> = ({ const { data, isLoading, isError } = useUserInfoQuery({ username }); return ( - <Box mi={4} is='a' href={href}> - <Tag icon={<Icon name='user' size='x20' />} className='mention-link' data-username={username} large> - {isLoading && <Skeleton variant='rect' />} - {!isLoading && isError && username} - {!isLoading && !isError && getUserDisplayName(data?.user?.name, username, useRealName)} - </Tag> - </Box> + <Tag icon={<Icon name='user' size='x20' />} data-username={username} large href={href}> + {isLoading && <Skeleton variant='rect' />} + {!isLoading && isError && username} + {!isLoading && !isError && getUserDisplayName(data?.user?.name, username, useRealName)} + </Tag> ); }; 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<HTMLInputElement>(null); const chat = useChat() ?? chatContext; + const resetFileInput = () => { + if (!fileInputRef.current) { + return; + } + + fileInputRef.current.value = ''; + }; + const handleUploadChange = async (e: ChangeEvent<HTMLInputElement>) => { 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 = ({ </ContextualbarHeader> <ContextualbarContent pbs={24}> <FieldGroup> + {room.encrypted && ( + <Callout title={t('Automatic_translation_not_available')} type='warning'> + {t('Automatic_translation_not_available_info')} + </Callout> + )} <Field> <FieldRow> - <ToggleSwitch id='automatic-translation' onChange={handleSwitch} defaultChecked={translateEnable} /> + <ToggleSwitch + id='automatic-translation' + onChange={handleSwitch} + defaultChecked={translateEnable} + disabled={room.encrypted && !translateEnable} + /> <FieldLabel htmlFor='automatic-translation'>{t('Automatic_Translation')}</FieldLabel> </FieldRow> </Field> <Field> - <FieldLabel htmlFor='language'>{t('Language')}</FieldLabel> + <FieldLabel htmlFor='translate-to'>{t('Translate_to')}</FieldLabel> <FieldRow verticalAlign='middle'> <Select id='language' diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx index 6bfc4ff03a80..d31e8bde76b9 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslateWithData.tsx @@ -2,9 +2,11 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useLanguage } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useMemo, useEffect, useState, memo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useEndpointAction } from '../../../../hooks/useEndpointAction'; import { useEndpointData } from '../../../../hooks/useEndpointData'; +import { dispatchToastMessage } from '../../../../lib/toast'; import { useRoom, useRoomSubscription } from '../../contexts/RoomContext'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import AutoTranslate from './AutoTranslate'; @@ -16,10 +18,12 @@ const AutoTranslateWithData = (): ReactElement => { const userLanguage = useLanguage(); const [currentLanguage, setCurrentLanguage] = useState(subscription?.autoTranslateLanguage ?? ''); const saveSettings = useEndpointAction('POST', '/v1/autotranslate.saveSettings'); + const { t } = useTranslation(); const { value: translateData } = useEndpointData('/v1/autotranslate.getSupportedLanguages', { params: useMemo(() => ({ targetLanguage: userLanguage }), [userLanguage]), }); + const languagesDict = translateData ? Object.fromEntries(translateData.languages.map((lang) => [lang.language, lang.name])) : {}; const handleChangeLanguage = useMutableCallback((value) => { setCurrentLanguage(value); @@ -29,6 +33,10 @@ const AutoTranslateWithData = (): ReactElement => { field: 'autoTranslateLanguage', value, }); + dispatchToastMessage({ + type: 'success', + message: t('AutoTranslate_language_set_to', { language: languagesDict[value] }), + }); }); const handleSwitch = useMutableCallback((event) => { @@ -37,6 +45,18 @@ const AutoTranslateWithData = (): ReactElement => { field: 'autoTranslate', value: event.target.checked, }); + dispatchToastMessage({ + type: 'success', + message: event.target.checked + ? t('AutoTranslate_Enabled_for_room', { roomName: room.name }) + : t('AutoTranslate_Disabled_for_room', { roomName: room.name }), + }); + if (event.target.checked && currentLanguage) { + dispatchToastMessage({ + type: 'success', + message: t('AutoTranslate_language_set_to', { language: languagesDict[currentLanguage] }), + }); + } }); useEffect(() => { diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js deleted file mode 100644 index 22c84fbdebf1..000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannel.js +++ /dev/null @@ -1,514 +0,0 @@ -import { isRoomFederated } from '@rocket.chat/core-typings'; -import { - Field, - TextInput, - PasswordInput, - ToggleSwitch, - MultiSelect, - Accordion, - Callout, - NumberInput, - FieldGroup, - FieldLabel, - FieldRow, - FieldHint, - Button, - ButtonGroup, - Box, - TextAreaInput, -} from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { - useSetModal, - useSetting, - usePermission, - useAtLeastOnePermission, - useRole, - useMethod, - useTranslation, - useRouter, -} from '@rocket.chat/ui-contexts'; -import React, { useCallback, useMemo, useRef } from 'react'; - -import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; -import { MessageTypesValues } from '../../../../../../app/lib/lib/MessageTypes'; -import { RoomSettingsEnum } from '../../../../../../definition/IRoomTypeConfig'; -import { - ContextualbarHeader, - ContextualbarBack, - ContextualbarTitle, - ContextualbarClose, - ContextualbarScrollableContent, - ContextualbarFooter, -} from '../../../../../components/Contextualbar'; -import GenericModal from '../../../../../components/GenericModal'; -import RawText from '../../../../../components/RawText'; -import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor'; -import { useEndpointAction } from '../../../../../hooks/useEndpointAction'; -import { useForm } from '../../../../../hooks/useForm'; -import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; - -const typeMap = { - c: 'Channels', - p: 'Groups', - d: 'DMs', -}; - -const useInitialValues = (room, settings) => { - const { - t, - ro, - archived, - topic, - description, - announcement, - joinCodeRequired, - sysMes, - encrypted, - retention = {}, - reactWhenReadOnly, - } = room; - - const { retentionPolicyEnabled, maxAgeDefault } = settings; - - const retentionEnabledDefault = useSetting(`RetentionPolicy_AppliesTo${typeMap[room.t]}`); - const excludePinnedDefault = useSetting('RetentionPolicy_DoNotPrunePinned'); - const filesOnlyDefault = useSetting('RetentionPolicy_FilesOnly'); - - return useMemo( - () => ({ - roomName: t === 'd' ? room.usernames.join(' x ') : roomCoordinator.getRoomName(t, { type: t, ...room }), - roomType: t, - readOnly: !!ro, - reactWhenReadOnly, - archived: !!archived, - roomTopic: topic ?? '', - roomDescription: description ?? '', - roomAnnouncement: announcement ?? '', - roomAvatar: undefined, - joinCode: '', - joinCodeRequired: !!joinCodeRequired, - systemMessages: Array.isArray(sysMes) ? sysMes : [], - hideSysMes: !!sysMes?.length, - encrypted, - ...(retentionPolicyEnabled && { - retentionEnabled: retention.enabled ?? retentionEnabledDefault, - retentionOverrideGlobal: !!retention.overrideGlobal, - retentionMaxAge: Math.min(retention.maxAge, maxAgeDefault) || maxAgeDefault, - retentionExcludePinned: retention.excludePinned ?? excludePinnedDefault, - retentionFilesOnly: retention.filesOnly ?? filesOnlyDefault, - }), - }), - [ - announcement, - archived, - description, - excludePinnedDefault, - filesOnlyDefault, - joinCodeRequired, - maxAgeDefault, - retention.enabled, - retention.excludePinned, - retention.filesOnly, - retention.maxAge, - retention.overrideGlobal, - retentionEnabledDefault, - retentionPolicyEnabled, - ro, - room, - sysMes, - t, - topic, - encrypted, - reactWhenReadOnly, - ], - ); -}; - -const getCanChangeType = (room, canCreateChannel, canCreateGroup, isAdmin) => - (!room.default || isAdmin) && ((room.t === 'p' && canCreateChannel) || (room.t === 'c' && canCreateGroup)); - -function EditChannel({ room, onClickClose, onClickBack }) { - const t = useTranslation(); - - const setModal = useSetModal(); - - const retentionPolicyEnabled = useSetting('RetentionPolicy_Enabled'); - const maxAgeDefault = useSetting(`RetentionPolicy_MaxAge_${typeMap[room.t]}`) || 30; - - const saveData = useRef({}); - const router = useRouter(); - - const onChange = useCallback(({ initialValue, value, key }) => { - const { current } = saveData; - if (JSON.stringify(initialValue) !== JSON.stringify(value)) { - if (key === 'systemMessages' && value?.length > 0) { - current.hideSysMes = true; - } - current[key] = value; - } else { - delete current[key]; - } - }, []); - - const { values, handlers, hasUnsavedChanges, reset, commit } = useForm( - useInitialValues(room, { retentionPolicyEnabled, maxAgeDefault }), - onChange, - ); - - const sysMesOptions = useMemo(() => MessageTypesValues.map(({ key, i18nLabel }) => [key, t(i18nLabel)]), [t]); - - const { - roomName, - roomType, - readOnly, - encrypted, - roomAvatar, - archived, - roomTopic, - roomDescription, - roomAnnouncement, - reactWhenReadOnly, - joinCode, - joinCodeRequired, - systemMessages, - hideSysMes, - retentionEnabled, - retentionOverrideGlobal, - retentionMaxAge, - retentionExcludePinned, - retentionFilesOnly, - } = values; - - const { - handleJoinCode, - handleJoinCodeRequired, - handleSystemMessages, - handleEncrypted, - handleHideSysMes, - handleRoomName, - handleReadOnly, - handleArchived, - handleRoomAvatar, - handleReactWhenReadOnly, - handleRoomType, - handleRoomTopic, - handleRoomDescription, - handleRoomAnnouncement, - handleRetentionEnabled, - handleRetentionOverrideGlobal, - handleRetentionMaxAge, - handleRetentionExcludePinned, - handleRetentionFilesOnly, - } = handlers; - - const [ - canViewName, - canViewTopic, - canViewAnnouncement, - canViewArchived, - canViewDescription, - canViewType, - canViewReadOnly, - canViewHideSysMes, - canViewJoinCode, - canViewEncrypted, - ] = useMemo(() => { - const isAllowed = roomCoordinator.getRoomDirectives(room.t).allowRoomSettingChange || (() => {}); - return [ - isAllowed(room, RoomSettingsEnum.NAME), - isAllowed(room, RoomSettingsEnum.TOPIC), - isAllowed(room, RoomSettingsEnum.ANNOUNCEMENT), - isAllowed(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE), - isAllowed(room, RoomSettingsEnum.DESCRIPTION), - isAllowed(room, RoomSettingsEnum.TYPE), - isAllowed(room, RoomSettingsEnum.READ_ONLY), - isAllowed(room, RoomSettingsEnum.SYSTEM_MESSAGES), - isAllowed(room, RoomSettingsEnum.JOIN_CODE), - isAllowed(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY), - isAllowed(room, RoomSettingsEnum.E2E), - ]; - }, [room]); - - const isAdmin = useRole('admin'); - - const canCreateChannel = usePermission('create-c'); - const canCreateGroup = usePermission('create-p'); - const canChangeType = getCanChangeType(room, canCreateChannel, canCreateGroup, isAdmin); - const canSetRo = usePermission('set-readonly', room._id); - const canSetReactWhenRo = usePermission('set-react-when-readonly', room._id); - const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id); - const canArchiveOrUnarchive = useAtLeastOnePermission( - useMemo(() => ['archive-room', 'unarchive-room'], []), - room._id, - ); - const canDelete = usePermission(`delete-${room.t}`); - const canToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id) && (room.encrypted || e2e.isReady()); - - const changeArchivation = archived !== !!room.archived; - const archiveSelector = room.archived ? 'unarchive' : 'archive'; - const archiveMessage = room.archived ? 'Room_has_been_unarchived' : 'Room_has_been_archived'; - const saveAction = useEndpointAction('POST', '/v1/rooms.saveRoomSettings', { - successMessage: t('Room_updated_successfully'), - }); - const archiveAction = useEndpointAction('POST', '/v1/rooms.changeArchivationState', { successMessage: t(archiveMessage) }); - - const handleSave = useMutableCallback(async () => { - const { joinCodeRequired, hideSysMes, ...data } = saveData.current; - delete data.archived; - const save = () => - saveAction({ - rid: room._id, - ...data, - ...(joinCode && { joinCode: joinCodeRequired ? joinCode : '' }), - ...((data.systemMessages || !hideSysMes) && { - systemMessages: hideSysMes && systemMessages, - }), - }); - - const archive = () => archiveAction({ rid: room._id, action: archiveSelector }); - - await Promise.all([hasUnsavedChanges && save(), changeArchivation && archive()].filter(Boolean)); - saveData.current = {}; - commit(); - }); - - const deleteRoom = useMethod('eraseRoom'); - - const handleDelete = useMutableCallback(() => { - const onCancel = () => setModal(undefined); - const onConfirm = async () => { - await deleteRoom(room._id); - onCancel(); - router.navigate('/home'); - }; - - setModal( - <GenericModal variant='danger' onConfirm={onConfirm} onCancel={onCancel} confirmText={t('Yes_delete_it')}> - {t('Delete_Room_Warning')} - </GenericModal>, - ); - }); - - const changeRoomType = useMutableCallback(() => { - handleRoomType(roomType === 'p' ? 'c' : 'p'); - }); - - const onChangeMaxAge = useMutableCallback((e) => { - handleRetentionMaxAge(Math.max(1, Number(e.currentTarget.value))); - }); - - const isFederated = useMemo(() => isRoomFederated(room), [room]); - - return ( - <> - <ContextualbarHeader> - {onClickBack && <ContextualbarBack onClick={onClickBack} />} - <ContextualbarTitle>{room.teamId ? t('edit-team') : t('edit-room')}</ContextualbarTitle> - {onClickClose && <ContextualbarClose onClick={onClickClose} />} - </ContextualbarHeader> - <ContextualbarScrollableContent p={24} is='form' onSubmit={useMutableCallback((e) => e.preventDefault())}> - <Box display='flex' justifyContent='center'> - <RoomAvatarEditor room={room} roomAvatar={roomAvatar} onChangeAvatar={handleRoomAvatar} /> - </Box> - <Field> - <FieldLabel required>{t('Name')}</FieldLabel> - <FieldRow> - <TextInput disabled={!canViewName} value={roomName} onChange={handleRoomName} flexGrow={1} /> - </FieldRow> - </Field> - {canViewDescription && ( - <Field> - <FieldLabel>{t('Description')}</FieldLabel> - <FieldRow> - <TextAreaInput disabled={isFederated} rows={4} value={roomDescription} onChange={handleRoomDescription} flexGrow={1} /> - </FieldRow> - </Field> - )} - {canViewAnnouncement && ( - <Field> - <FieldLabel>{t('Announcement')}</FieldLabel> - <FieldRow> - <TextAreaInput disabled={isFederated} rows={4} value={roomAnnouncement} onChange={handleRoomAnnouncement} flexGrow={1} /> - </FieldRow> - </Field> - )} - {canViewTopic && ( - <Field> - <FieldLabel>{t('Topic')}</FieldLabel> - <FieldRow> - <TextAreaInput rows={4} value={roomTopic} onChange={handleRoomTopic} flexGrow={1} /> - </FieldRow> - </Field> - )} - {canViewType && ( - <Field> - <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> - <FieldLabel>{t('Private')}</FieldLabel> - <FieldRow> - <ToggleSwitch disabled={!canChangeType || isFederated} checked={roomType === 'p'} onChange={changeRoomType} /> - </FieldRow> - </Box> - <FieldHint>{t('Teams_New_Private_Description_Enabled')}</FieldHint> - </Field> - )} - {canViewReadOnly && ( - <Field> - <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> - <FieldLabel>{t('Read_only')}</FieldLabel> - <FieldRow> - <ToggleSwitch disabled={!canSetRo || isFederated} checked={readOnly} onChange={handleReadOnly} /> - </FieldRow> - </Box> - <FieldHint>{t('Only_authorized_users_can_write_new_messages')}</FieldHint> - </Field> - )} - {readOnly && ( - <Field> - <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> - <FieldLabel>{t('React_when_read_only')}</FieldLabel> - <FieldRow> - <ToggleSwitch disabled={!canSetReactWhenRo} checked={reactWhenReadOnly} onChange={handleReactWhenReadOnly} /> - </FieldRow> - </Box> - <FieldHint>{t('Only_authorized_users_can_react_to_messages')}</FieldHint> - </Field> - )} - {canViewArchived && ( - <Field> - <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> - <FieldLabel>{t('Room_archivation_state_true')}</FieldLabel> - <FieldRow> - <ToggleSwitch disabled={!canArchiveOrUnarchive} checked={archived} onChange={handleArchived} /> - </FieldRow> - </Box> - </Field> - )} - {canViewJoinCode && ( - <Field> - <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> - <FieldLabel>{t('Password_to_access')}</FieldLabel> - <FieldRow> - <ToggleSwitch disabled={isFederated} checked={joinCodeRequired} onChange={handleJoinCodeRequired} /> - </FieldRow> - </Box> - <FieldRow> - <PasswordInput - disabled={!joinCodeRequired} - value={joinCode} - onChange={handleJoinCode} - placeholder={t('Reset_password')} - flexGrow={1} - /> - </FieldRow> - </Field> - )} - {canViewHideSysMes && ( - <Field> - <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> - <FieldLabel>{t('Hide_System_Messages')}</FieldLabel> - <FieldRow> - <ToggleSwitch checked={hideSysMes} disabled={isFederated} onChange={handleHideSysMes} /> - </FieldRow> - </Box> - <FieldRow> - <MultiSelect - maxWidth='100%' - options={sysMesOptions} - disabled={!hideSysMes || isFederated} - value={systemMessages} - onChange={handleSystemMessages} - placeholder={t('Select_an_option')} - flexGrow={1} - /> - </FieldRow> - </Field> - )} - {canViewEncrypted && ( - <Field> - <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> - <FieldLabel>{t('Encrypted')}</FieldLabel> - <FieldRow> - <ToggleSwitch disabled={!canToggleEncryption || isFederated} checked={encrypted} onChange={handleEncrypted} /> - </FieldRow> - </Box> - </Field> - )} - {retentionPolicyEnabled && ( - <Accordion> - <Accordion.Item title={t('Prune')}> - <FieldGroup> - <Field> - <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> - <FieldLabel>{t('RetentionPolicyRoom_Enabled')}</FieldLabel> - <FieldRow> - <ToggleSwitch checked={retentionEnabled} onChange={handleRetentionEnabled} /> - </FieldRow> - </Box> - </Field> - <Field> - <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> - <FieldLabel>{t('RetentionPolicyRoom_OverrideGlobal')}</FieldLabel> - <FieldRow> - <ToggleSwitch - disabled={!retentionEnabled || !canEditRoomRetentionPolicy} - checked={retentionOverrideGlobal} - onChange={handleRetentionOverrideGlobal} - /> - </FieldRow> - </Box> - </Field> - {retentionOverrideGlobal && ( - <> - <Callout type='danger'> - <RawText>{t('RetentionPolicyRoom_ReadTheDocs')}</RawText> - </Callout> - <Field> - <FieldLabel>{t('RetentionPolicyRoom_MaxAge', { max: maxAgeDefault })}</FieldLabel> - <FieldRow> - <NumberInput value={retentionMaxAge} onChange={onChangeMaxAge} flexGrow={1} /> - </FieldRow> - </Field> - <Field> - <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> - <FieldLabel>{t('RetentionPolicyRoom_ExcludePinned')}</FieldLabel> - <FieldRow> - <ToggleSwitch checked={retentionExcludePinned} onChange={handleRetentionExcludePinned} /> - </FieldRow> - </Box> - </Field> - <Field> - <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> - <FieldLabel>{t('RetentionPolicyRoom_FilesOnly')}</FieldLabel> - <FieldRow> - <ToggleSwitch checked={retentionFilesOnly} onChange={handleRetentionFilesOnly} /> - </FieldRow> - </Box> - </Field> - </> - )} - </FieldGroup> - </Accordion.Item> - </Accordion> - )} - </ContextualbarScrollableContent> - <ContextualbarFooter> - <ButtonGroup stretch> - <Button type='reset' disabled={!hasUnsavedChanges} onClick={reset}> - {t('Reset')} - </Button> - <Button disabled={!hasUnsavedChanges} onClick={handleSave}> - {t('Save')} - </Button> - </ButtonGroup> - <ButtonGroup stretch mbs={8}> - <Button icon='trash' danger disabled={!canDelete || isFederated} onClick={handleDelete}> - {t('Delete')} - </Button> - </ButtonGroup> - </ContextualbarFooter> - </> - ); -} - -export default EditChannel; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js deleted file mode 100644 index e64dcc17562a..000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditChannelWithData.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { useRoom } from '../../../contexts/RoomContext'; -import { useRoomToolbox } from '../../../contexts/RoomToolboxContext'; -import EditChannel from './EditChannel'; - -function EditChannelWithData({ onClickBack }) { - const room = useRoom(); - const { closeTab } = useRoomToolbox(); - - return <EditChannel onClickClose={closeTab} onClickBack={onClickBack} room={{ type: room?.t, ...room }} />; -} - -export default EditChannelWithData; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx new file mode 100644 index 000000000000..b2c552fd3d87 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx @@ -0,0 +1,487 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; +import type { SelectOption } from '@rocket.chat/fuselage'; +import { + Field, + FieldRow, + FieldLabel, + FieldHint, + TextInput, + PasswordInput, + ToggleSwitch, + MultiSelect, + Accordion, + Callout, + NumberInput, + FieldGroup, + Button, + ButtonGroup, + Box, + TextAreaInput, +} from '@rocket.chat/fuselage'; +import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useSetting, useTranslation, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; +import React, { useMemo } from 'react'; +import { useForm, Controller } from 'react-hook-form'; + +import { MessageTypesValues } from '../../../../../../app/lib/lib/MessageTypes'; +import { + ContextualbarHeader, + ContextualbarBack, + ContextualbarTitle, + ContextualbarClose, + ContextualbarScrollableContent, + ContextualbarFooter, +} from '../../../../../components/Contextualbar'; +import RawText from '../../../../../components/RawText'; +import RoomAvatarEditor from '../../../../../components/avatar/RoomAvatarEditor'; +import { getDirtyFields } from '../../../../../lib/getDirtyFields'; +import { useDeleteRoom } from '../../../../hooks/roomActions/useDeleteRoom'; +import { useEditRoomInitialValues } from './useEditRoomInitialValues'; +import { useEditRoomPermissions } from './useEditRoomPermissions'; + +type EditRoomInfoProps = { + room: IRoomWithRetentionPolicy; + onClickClose: () => void; + onClickBack: () => void; +}; + +const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const isFederated = useMemo(() => isRoomFederated(room), [room]); + + const retentionPolicy = useSetting<boolean>('RetentionPolicy_Enabled'); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room); + const defaultValues = useEditRoomInitialValues(room); + + const { + watch, + reset, + control, + handleSubmit, + formState: { isDirty, dirtyFields, errors }, + } = useForm({ mode: 'onBlur', defaultValues }); + + const sysMesOptions: SelectOption[] = useMemo( + () => MessageTypesValues.map(({ key, i18nLabel }) => [key, t(i18nLabel as TranslationKey)]), + [t], + ); + + const { readOnly, archived, joinCodeRequired, hideSysMes, retentionEnabled, retentionMaxAge, retentionOverrideGlobal } = watch(); + + const { + canChangeType, + canSetReadOnly, + canSetReactWhenReadOnly, + canEditRoomRetentionPolicy, + canArchiveOrUnarchive, + canToggleEncryption, + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewHideSysMes, + canViewJoinCode, + canViewEncrypted, + } = useEditRoomPermissions(room); + + const changeArchiving = archived !== !!room.archived; + + const saveAction = useEndpoint('POST', '/v1/rooms.saveRoomSettings'); + const archiveAction = useEndpoint('POST', '/v1/rooms.changeArchivationState'); + + const handleUpdateRoomData = useMutableCallback(async ({ hideSysMes, ...formData }) => { + const data = getDirtyFields(formData, dirtyFields); + + try { + await saveAction({ + rid: room._id, + ...data, + ...(data.joinCode && { joinCode: joinCodeRequired ? data.joinCode : '' }), + ...((data.systemMessages || !hideSysMes) && { + systemMessages: hideSysMes && data.systemMessages, + }), + }); + + dispatchToastMessage({ type: 'success', message: t('Room_updated_successfully') }); + onClickClose(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + const handleArchive = useMutableCallback(async () => { + try { + await archiveAction({ rid: room._id, action: room.archived ? 'unarchive' : 'archive' }); + dispatchToastMessage({ type: 'success', message: room.archived ? t('Room_has_been_unarchived') : t('Room_has_been_archived') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + const handleSave = useMutableCallback(async (data) => { + await Promise.all([isDirty && handleUpdateRoomData(data), changeArchiving && handleArchive()].filter(Boolean)); + }); + + const formId = useUniqueId(); + const roomNameField = useUniqueId(); + const roomDescriptionField = useUniqueId(); + const roomAnnouncementField = useUniqueId(); + const roomTopicField = useUniqueId(); + const roomTypeField = useUniqueId(); + const readOnlyField = useUniqueId(); + const reactWhenReadOnlyField = useUniqueId(); + const archivedField = useUniqueId(); + const joinCodeRequiredField = useUniqueId(); + const hideSysMesField = useUniqueId(); + const encryptedField = useUniqueId(); + const retentionEnabledField = useUniqueId(); + const retentionOverrideGlobalField = useUniqueId(); + const retentionMaxAgeField = useUniqueId(); + const retentionExcludePinnedField = useUniqueId(); + const retentionFilesOnlyField = useUniqueId(); + + return ( + <> + <ContextualbarHeader> + {onClickBack && <ContextualbarBack onClick={onClickBack} />} + <ContextualbarTitle>{room.teamId ? t('edit-team') : t('edit-room')}</ContextualbarTitle> + {onClickClose && <ContextualbarClose onClick={onClickClose} />} + </ContextualbarHeader> + <ContextualbarScrollableContent p={24}> + <form id={formId} onSubmit={handleSubmit(handleSave)}> + <Box display='flex' justifyContent='center'> + <Controller + control={control} + name='roomAvatar' + render={({ field: { onChange, value } }) => <RoomAvatarEditor room={room} roomAvatar={value} onChangeAvatar={onChange} />} + /> + </Box> + <FieldGroup> + <Field> + <FieldLabel htmlFor={roomNameField} required> + {t('Name')} + </FieldLabel> + <FieldRow> + <Controller + name='roomName' + control={control} + rules={{ required: t('error-the-field-is-required', { field: t('Name') }) }} + render={({ field }) => <TextInput id={roomNameField} {...field} disabled={!canViewName} />} + /> + </FieldRow> + {errors.roomName && <Field.Error>{errors.roomName.message}</Field.Error>} + </Field> + {canViewDescription && ( + <Field> + <FieldLabel htmlFor={roomDescriptionField}>{t('Description')}</FieldLabel> + <FieldRow> + <Controller + name='roomDescription' + control={control} + render={({ field }) => <TextAreaInput id={roomDescriptionField} {...field} disabled={isFederated} rows={4} />} + /> + </FieldRow> + </Field> + )} + {canViewAnnouncement && ( + <Field> + <FieldLabel htmlFor={roomAnnouncementField}>{t('Announcement')}</FieldLabel> + <FieldRow> + <Controller + name='roomAnnouncement' + control={control} + render={({ field }) => <TextAreaInput id={roomAnnouncementField} {...field} disabled={isFederated} rows={4} />} + /> + </FieldRow> + </Field> + )} + {canViewTopic && ( + <Field> + <FieldLabel htmlFor={roomTopicField}>{t('Topic')}</FieldLabel> + <FieldRow> + <Controller + name='roomTopic' + control={control} + render={({ field }) => <TextAreaInput id={roomTopicField} {...field} rows={4} />} + /> + </FieldRow> + </Field> + )} + {canViewType && ( + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <FieldLabel htmlFor={roomTypeField}>{t('Private')}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='roomType' + render={({ field: { name, onBlur, onChange, value, ref } }) => ( + <ToggleSwitch + id={roomTypeField} + ref={ref} + name={name} + onBlur={onBlur} + disabled={!canChangeType || isFederated} + checked={value === 'p'} + onChange={() => onChange(value === 'p' ? 'c' : 'p')} + aria-describedby={`${roomTypeField}-hint`} + /> + )} + /> + </FieldRow> + </Box> + <FieldHint id={`${roomTypeField}-hint`}>{t('Teams_New_Private_Description_Enabled')}</FieldHint> + </Field> + )} + {canViewReadOnly && ( + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <FieldLabel htmlFor={readOnlyField}>{t('Read_only')}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='readOnly' + render={({ field: { value, ...field } }) => ( + <ToggleSwitch + id={readOnlyField} + {...field} + checked={value} + disabled={!canSetReadOnly || isFederated} + aria-describedby={`${readOnlyField}-hint`} + /> + )} + /> + </FieldRow> + </Box> + <FieldHint id={`${readOnlyField}-hint`}>{t('Only_authorized_users_can_write_new_messages')}</FieldHint> + </Field> + )} + {readOnly && ( + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <FieldLabel htmlFor={reactWhenReadOnlyField}>{t('React_when_read_only')}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='reactWhenReadOnly' + render={({ field: { value, ...field } }) => ( + <ToggleSwitch + id={reactWhenReadOnlyField} + {...field} + disabled={!canSetReactWhenReadOnly} + checked={value} + aria-describedby={`${reactWhenReadOnlyField}-hint`} + /> + )} + /> + </FieldRow> + </Box> + <FieldHint id={`${reactWhenReadOnlyField}-hint`}>{t('Only_authorized_users_can_react_to_messages')}</FieldHint> + </Field> + )} + {canViewArchived && ( + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <FieldLabel htmlFor={archivedField}>{t('Room_archivation_state_true')}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='archived' + render={({ field: { value, ...field } }) => ( + <ToggleSwitch id={archivedField} {...field} disabled={!canArchiveOrUnarchive} checked={value} /> + )} + /> + </FieldRow> + </Box> + </Field> + )} + {canViewJoinCode && ( + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <FieldLabel htmlFor={joinCodeRequiredField}>{t('Password_to_access')}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='joinCodeRequired' + render={({ field: { value, ...field } }) => ( + <ToggleSwitch id={joinCodeRequiredField} {...field} disabled={isFederated} checked={value} /> + )} + /> + </FieldRow> + </Box> + <FieldRow> + <Controller + name='joinCode' + control={control} + render={({ field }) => <PasswordInput {...field} placeholder={t('Reset_password')} disabled={!joinCodeRequired} />} + /> + </FieldRow> + </Field> + )} + {canViewHideSysMes && ( + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <FieldLabel htmlFor={hideSysMesField}>{t('Hide_System_Messages')}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='hideSysMes' + render={({ field: { value, ...field } }) => ( + <ToggleSwitch id={hideSysMesField} {...field} checked={value} disabled={isFederated} /> + )} + /> + </FieldRow> + </Box> + <FieldRow> + <Controller + control={control} + name='systemMessages' + render={({ field }) => ( + <MultiSelect + {...field} + options={sysMesOptions} + disabled={!hideSysMes || isFederated} + placeholder={t('Select_an_option')} + /> + )} + /> + </FieldRow> + </Field> + )} + {canViewEncrypted && ( + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <FieldLabel htmlFor={encryptedField}>{t('Encrypted')}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='encrypted' + render={({ field: { value, ...field } }) => ( + <ToggleSwitch id={encryptedField} {...field} disabled={!canToggleEncryption || isFederated} checked={value} /> + )} + /> + </FieldRow> + </Box> + </Field> + )} + </FieldGroup> + {retentionPolicy && ( + <Accordion> + <Accordion.Item title={t('Prune')}> + <FieldGroup> + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <FieldLabel htmlFor={retentionEnabledField}>{t('RetentionPolicyRoom_Enabled')}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='retentionEnabled' + render={({ field: { value, ...field } }) => ( + <ToggleSwitch id={retentionEnabledField} {...field} checked={value} /> + )} + /> + </FieldRow> + </Box> + </Field> + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <FieldLabel htmlFor={retentionOverrideGlobalField}>{t('RetentionPolicyRoom_OverrideGlobal')}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='retentionOverrideGlobal' + render={({ field: { value, ...field } }) => ( + <ToggleSwitch + id={retentionOverrideGlobalField} + {...field} + disabled={!retentionEnabled || !canEditRoomRetentionPolicy} + checked={value} + /> + )} + /> + </FieldRow> + </Box> + </Field> + {retentionOverrideGlobal && ( + <> + <Callout type='danger'> + <RawText>{t('RetentionPolicyRoom_ReadTheDocs')}</RawText> + </Callout> + <Field> + <FieldLabel htmlFor={retentionMaxAgeField}>{t('RetentionPolicyRoom_MaxAge', { max: retentionMaxAge })}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='retentionMaxAge' + render={({ field: { onChange, ...field } }) => ( + <NumberInput + id={retentionMaxAgeField} + {...field} + onChange={(currentValue) => onChange(Math.max(1, Number(currentValue)))} + /> + )} + /> + </FieldRow> + </Field> + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <FieldLabel htmlFor={retentionExcludePinnedField}>{t('RetentionPolicyRoom_ExcludePinned')}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='retentionExcludePinned' + render={({ field: { value, ...field } }) => ( + <ToggleSwitch id={retentionExcludePinnedField} {...field} checked={value} /> + )} + /> + </FieldRow> + </Box> + </Field> + <Field> + <Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}> + <FieldLabel htmlFor={retentionFilesOnlyField}>{t('RetentionPolicyRoom_FilesOnly')}</FieldLabel> + <FieldRow> + <Controller + control={control} + name='retentionFilesOnly' + render={({ field: { value, ...field } }) => ( + <ToggleSwitch id={retentionFilesOnlyField} {...field} checked={value} /> + )} + /> + </FieldRow> + </Box> + </Field> + </> + )} + </FieldGroup> + </Accordion.Item> + </Accordion> + )} + </form> + </ContextualbarScrollableContent> + <ContextualbarFooter> + <ButtonGroup stretch> + <Button type='reset' disabled={!isDirty} onClick={() => reset(defaultValues)}> + {t('Reset')} + </Button> + <Button form={formId} type='submit' disabled={!isDirty}> + {t('Save')} + </Button> + </ButtonGroup> + <ButtonGroup stretch mbs={8}> + <Button icon='trash' danger disabled={!canDeleteRoom || isFederated} onClick={handleDelete}> + {t('Delete')} + </Button> + </ButtonGroup> + </ContextualbarFooter> + </> + ); +}; + +export default EditRoomInfo; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx new file mode 100644 index 000000000000..ad758c1bc8a6 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfoWithData.tsx @@ -0,0 +1,15 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import React from 'react'; + +import { useRoom } from '../../../contexts/RoomContext'; +import { useRoomToolbox } from '../../../contexts/RoomToolboxContext'; +import EditRoomInfo from './EditRoomInfo'; + +const EditRoomInfoWithData = ({ onClickBack }: { onClickBack: () => void }) => { + const room = useRoom() as IRoomWithRetentionPolicy; + const { closeTab } = useRoomToolbox(); + + return <EditRoomInfo onClickClose={closeTab} onClickBack={onClickBack} room={room} />; +}; + +export default EditRoomInfoWithData; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts index 4083ad9a958f..d8b31e17800c 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/index.ts @@ -1 +1 @@ -export { default } from './EditChannelWithData'; +export { default } from './EditRoomInfoWithData'; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts new file mode 100644 index 000000000000..f36802bb9f56 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts @@ -0,0 +1,71 @@ +import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; + +const getPolicyRoomType = (roomType: IRoomWithRetentionPolicy['t']) => { + switch (roomType) { + case 'c': + return 'Channels'; + case 'p': + return 'Groups'; + case 'd': + return 'DMs'; + } +}; + +export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { + const { t, ro, archived, topic, description, announcement, joinCodeRequired, sysMes, encrypted, retention, reactWhenReadOnly } = room; + + const retentionPolicyEnabled = useSetting('RetentionPolicy_Enabled'); + const maxAgeDefault = useSetting<number>(`RetentionPolicy_MaxAge_${getPolicyRoomType(room.t)}`) || 30; + const retentionEnabledDefault = useSetting<boolean>(`RetentionPolicy_AppliesTo${getPolicyRoomType(room.t)}`); + const excludePinnedDefault = useSetting('RetentionPolicy_DoNotPrunePinned'); + const filesOnlyDefault = useSetting('RetentionPolicy_FilesOnly'); + + return useMemo( + () => ({ + roomName: t === 'd' && room.usernames ? room.usernames.join(' x ') : roomCoordinator.getRoomName(t, room), + roomType: t, + readOnly: !!ro, + reactWhenReadOnly, + archived: !!archived, + roomTopic: topic ?? '', + roomDescription: description ?? '', + roomAnnouncement: announcement ?? '', + roomAvatar: undefined, + joinCode: '', + joinCodeRequired: !!joinCodeRequired, + systemMessages: Array.isArray(sysMes) ? sysMes : [], + hideSysMes: Array.isArray(sysMes) ? !!sysMes?.length : !!sysMes, + encrypted, + ...(retentionPolicyEnabled && { + retentionEnabled: retention?.enabled ?? retentionEnabledDefault, + retentionOverrideGlobal: !!retention?.overrideGlobal, + retentionMaxAge: Math.min(retention?.maxAge, maxAgeDefault) || maxAgeDefault, + retentionExcludePinned: retention?.excludePinned ?? excludePinnedDefault, + retentionFilesOnly: retention?.filesOnly ?? filesOnlyDefault, + }), + }), + [ + announcement, + archived, + description, + excludePinnedDefault, + filesOnlyDefault, + joinCodeRequired, + maxAgeDefault, + retention, + retentionEnabledDefault, + retentionPolicyEnabled, + ro, + room, + sysMes, + t, + topic, + encrypted, + reactWhenReadOnly, + ], + ); +}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts new file mode 100644 index 000000000000..7b9e8c353941 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts @@ -0,0 +1,77 @@ +import type { IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import { usePermission, useAtLeastOnePermission, useRole } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; +import { RoomSettingsEnum } from '../../../../../../definition/IRoomTypeConfig'; +import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; + +const getCanChangeType = (room: IRoom | IRoomWithRetentionPolicy, canCreateChannel: boolean, canCreateGroup: boolean, isAdmin: boolean) => + (!room.default || isAdmin) && ((room.t === 'p' && canCreateChannel) || (room.t === 'c' && canCreateGroup)); + +export const useEditRoomPermissions = (room: IRoom | IRoomWithRetentionPolicy) => { + const isAdmin = useRole('admin'); + const canCreateChannel = usePermission('create-c'); + const canCreateGroup = usePermission('create-p'); + + const canChangeType = getCanChangeType(room, canCreateChannel, canCreateGroup, isAdmin); + const canSetReadOnly = usePermission('set-readonly', room._id); + const canSetReactWhenReadOnly = usePermission('set-react-when-readonly', room._id); + const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id); + const canArchiveOrUnarchive = useAtLeastOnePermission( + useMemo(() => ['archive-room', 'unarchive-room'], []), + room._id, + ); + const canToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id) && (room.encrypted || e2e.isReady()); + + const [ + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewHideSysMes, + canViewJoinCode, + canViewEncrypted, + ] = useMemo(() => { + const isAllowed = + roomCoordinator.getRoomDirectives(room.t)?.allowRoomSettingChange || + (() => { + undefined; + }); + return [ + isAllowed(room, RoomSettingsEnum.NAME), + isAllowed(room, RoomSettingsEnum.TOPIC), + isAllowed(room, RoomSettingsEnum.ANNOUNCEMENT), + isAllowed(room, RoomSettingsEnum.ARCHIVE_OR_UNARCHIVE), + isAllowed(room, RoomSettingsEnum.DESCRIPTION), + isAllowed(room, RoomSettingsEnum.TYPE), + isAllowed(room, RoomSettingsEnum.READ_ONLY), + isAllowed(room, RoomSettingsEnum.SYSTEM_MESSAGES), + isAllowed(room, RoomSettingsEnum.JOIN_CODE), + isAllowed(room, RoomSettingsEnum.REACT_WHEN_READ_ONLY), + isAllowed(room, RoomSettingsEnum.E2E), + ]; + }, [room]); + + return { + canChangeType, + canSetReadOnly, + canSetReactWhenReadOnly, + canEditRoomRetentionPolicy, + canArchiveOrUnarchive, + canToggleEncryption, + canViewName, + canViewTopic, + canViewAnnouncement, + canViewArchived, + canViewDescription, + canViewType, + canViewReadOnly, + canViewHideSysMes, + canViewJoinCode, + canViewEncrypted, + }; +}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx b/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx deleted file mode 100644 index b812e896bab9..000000000000 --- a/apps/meteor/client/views/room/contextualBar/Info/hooks/actions/useRoomDelete.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { IRoom } from '@rocket.chat/core-typings'; -import { isRoomFederated } from '@rocket.chat/core-typings'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useTranslation, useEndpoint, usePermission, useRouter } from '@rocket.chat/ui-contexts'; -import React from 'react'; - -import GenericModal from '../../../../../../components/GenericModal'; - -// TODO: resetState for TeamsChannels -export const useRoomDelete = (room: IRoom, resetState?: () => void) => { - const t = useTranslation(); - const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); - const router = useRouter(); - - const hasPermissionToDelete = usePermission(room.t === 'c' ? 'delete-c' : 'delete-p', room._id); - const canDelete = isRoomFederated(room) ? false : hasPermissionToDelete; - - const deleteRoom = useEndpoint('POST', room.t === 'c' ? '/v1/channels.delete' : '/v1/groups.delete'); - - const handleDelete = useMutableCallback(() => { - const onConfirm = async () => { - try { - await deleteRoom({ roomId: room._id }); - dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); - if (resetState) { - return resetState(); - } - - router.navigate('/home'); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - setModal(null); - }; - - setModal( - <GenericModal variant='danger' onConfirm={onConfirm} onCancel={() => setModal(null)} confirmText={t('Yes_delete_it')}> - {t('Delete_Room_Warning')} - </GenericModal>, - ); - }); - - return canDelete ? handleDelete : null; -}; diff --git a/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts b/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts index d0ab03bd9ee7..638cd23b66ed 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/hooks/useRoomActions.ts @@ -1,10 +1,9 @@ -import { isRoomFederated } from '@rocket.chat/core-typings'; import type { IRoom } from '@rocket.chat/core-typings'; import { useTranslation } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; +import { useDeleteRoom } from '../../../../hooks/roomActions/useDeleteRoom'; import { useRoomConvertToTeam } from './actions/useRoomConvertToTeam'; -import { useRoomDelete } from './actions/useRoomDelete'; import { useRoomHide } from './actions/useRoomHide'; import { useRoomLeave } from './actions/useRoomLeave'; import { useRoomMoveToTeam } from './actions/useRoomMoveToTeam'; @@ -16,11 +15,10 @@ type RoomActions = { export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: RoomActions, resetState?: () => void) => { const t = useTranslation(); - const isFederated = isRoomFederated(room); const handleHide = useRoomHide(room); const handleLeave = useRoomLeave(room); - const handleDelete = useRoomDelete(room, resetState); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room, { reload: resetState }); const handleMoveToTeam = useRoomMoveToTeam(room); const handleConvertToTeam = useRoomConvertToTeam(room); @@ -40,7 +38,7 @@ export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: R action: onClickEdit, }, }), - ...(!isFederated && + ...(canDeleteRoom && handleDelete && { delete: { label: t('Delete'), @@ -77,7 +75,7 @@ export const useRoomActions = (room: IRoom, { onClickEnterRoom, onClickEdit }: R }, }), }), - [onClickEdit, t, handleDelete, handleMoveToTeam, handleConvertToTeam, handleHide, handleLeave, onClickEnterRoom, isFederated], + [onClickEdit, t, handleDelete, handleMoveToTeam, handleConvertToTeam, handleHide, handleLeave, onClickEnterRoom, canDeleteRoom], ); return memoizedActions; 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 8b3bef03f793..aca714549cf1 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useThreadMainMessageQuery.ts @@ -1,13 +1,12 @@ -import { isThreadMainMessage } from '@rocket.chat/core-typings'; import type { IMessage, IThreadMainMessage } from '@rocket.chat/core-typings'; import { useStream } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQueryClient, useQuery } from '@tanstack/react-query'; import { useCallback, useEffect, useRef } from 'react'; +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'; import { useGetMessageByID } from './useGetMessageByID'; @@ -87,19 +86,22 @@ export const useThreadMainMessageQuery = ( }, [tmid]); return useQuery(['rooms', room._id, 'threads', tmid, 'main-message'] as const, async ({ queryKey }) => { - const message = await getMessage(tmid); + const mainMessage = await getMessage(tmid); - const mainMessage = (await onClientMessageReceived(message)) || message; - - if (!mainMessage && !isThreadMainMessage(mainMessage)) { + if (!mainMessage) { throw new Error('Invalid main message'); } + const debouncedInvalidate = withDebouncing({ wait: 10000 })(() => { + queryClient.invalidateQueries(queryKey, { exact: true }); + }); + unsubscribeRef.current = unsubscribeRef.current || subscribeToMessage(mainMessage, { - onMutate: () => { - queryClient.invalidateQueries(queryKey, { exact: true }); + onMutate: (message) => { + queryClient.setQueryData(queryKey, () => message); + debouncedInvalidate(); }, onDelete: () => { onDelete?.(); diff --git a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js index 0b72b3f164a6..f5cb4a44c5d2 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js +++ b/apps/meteor/client/views/teams/contextualBar/info/TeamsInfoWithData.js @@ -16,10 +16,10 @@ import { GenericModalDoNotAskAgain } from '../../../../components/GenericModal'; import { useDontAskAgain } from '../../../../hooks/useDontAskAgain'; import { useEndpointAction } from '../../../../hooks/useEndpointAction'; import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; +import { useDeleteRoom } from '../../../hooks/roomActions/useDeleteRoom'; import { useRoom } from '../../../room/contexts/RoomContext'; import { useRoomToolbox } from '../../../room/contexts/RoomToolboxContext'; import ConvertToChannelModal from '../../ConvertToChannelModal'; -import DeleteTeamModal from './DeleteTeam'; import LeaveTeam from './LeaveTeam'; import TeamsInfo from './TeamsInfo'; @@ -56,7 +56,6 @@ const TeamsInfoWithLogic = ({ openEditing }) => { const setModal = useSetModal(); const closeModal = useMutableCallback(() => setModal()); - const deleteTeam = useEndpointAction('POST', '/v1/teams.delete'); const leaveTeam = useEndpointAction('POST', '/v1/teams.leave'); const convertTeamToChannel = useEndpointAction('POST', '/v1/teams.convertToChannel'); @@ -64,28 +63,11 @@ const TeamsInfoWithLogic = ({ openEditing }) => { const router = useRouter(); - const canDelete = usePermission('delete-team', room._id); const canEdit = usePermission('edit-team-channel', room._id); // const canLeave = usePermission('leave-team'); /* && room.cl !== false && joined */ - const onClickDelete = useMutableCallback(() => { - const onConfirm = async (deletedRooms) => { - const roomsToRemove = Array.isArray(deletedRooms) && deletedRooms.length > 0 ? deletedRooms : []; - - try { - await deleteTeam({ teamId: room.teamId, ...(roomsToRemove.length && { roomsToRemove }) }); - dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); - router.navigate('/home'); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } finally { - closeModal(); - } - }; - - setModal(<DeleteTeamModal onConfirm={onConfirm} onCancel={closeModal} teamId={room.teamId} />); - }); + const { handleDelete, canDeleteRoom } = useDeleteRoom(room); const onClickLeave = useMutableCallback(() => { const onConfirm = async (roomsLeft) => { @@ -174,7 +156,7 @@ const TeamsInfoWithLogic = ({ openEditing }) => { retentionPolicy={retentionPolicyEnabled && retentionPolicy} onClickEdit={canEdit && openEditing} onClickClose={closeTab} - onClickDelete={canDelete && onClickDelete} + onClickDelete={canDeleteRoom && handleDelete} onClickLeave={/* canLeave && */ onClickLeave} onClickHide={/* joined && */ handleHide} onClickViewChannels={onClickViewChannels} diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/units.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/units.ts index 58a75abc943c..309834f2d1e8 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/units.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/units.ts @@ -8,7 +8,7 @@ async function hasUnits(): Promise<boolean> { } // Units should't change really often, so we can cache the result -const memoizedHasUnits = mem(hasUnits, { maxAge: 10000 }); +const memoizedHasUnits = mem(hasUnits, { maxAge: process.env.TEST_MODE ? 1 : 10000 }); export async function getUnitsFromUser(): Promise<{ [k: string]: any }[] | undefined> { if (!(await memoizedHasUnits())) { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts b/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts index 47929f384d8f..18ed1b1ea646 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts @@ -14,8 +14,8 @@ async function getDepartmentsFromUserRoles(user: string): Promise<string[]> { return (await LivechatDepartmentAgents.findByAgentId(user).toArray()).map((department) => department.departmentId); } -const memoizedGetUnitFromUserRoles = mem(getUnitsFromUserRoles, { maxAge: 10000 }); -const memoizedGetDepartmentsFromUserRoles = mem(getDepartmentsFromUserRoles, { maxAge: 5000 }); +const memoizedGetUnitFromUserRoles = mem(getUnitsFromUserRoles, { maxAge: process.env.TEST_MODE ? 1 : 10000 }); +const memoizedGetDepartmentsFromUserRoles = mem(getDepartmentsFromUserRoles, { maxAge: process.env.TEST_MODE ? 1 : 10000 }); export const getUnitsFromUser = async (user: string): Promise<string[] | undefined> => { if (!user || (await hasAnyRoleAsync(user, ['admin', 'livechat-manager']))) { diff --git a/apps/meteor/ee/client/hooks/useTagsList.ts b/apps/meteor/ee/client/hooks/useTagsList.ts index 907fa96f4aab..41062d1b9dce 100644 --- a/apps/meteor/ee/client/hooks/useTagsList.ts +++ b/apps/meteor/ee/client/hooks/useTagsList.ts @@ -1,4 +1,3 @@ -import type { ILivechatTagRecord } from '@rocket.chat/core-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useState } from 'react'; @@ -12,8 +11,10 @@ type TagsListOptions = { viewAll?: boolean; }; +type TagListItem = { _id: string; label: string; value: string; _updatedAt: Date }; + type UseTagsListResult = { - itemsList: RecordList<ILivechatTagRecord>; + itemsList: RecordList<TagListItem>; initialItemCount: number; reload: () => void; loadMoreItems: (start: number, end: number) => void; @@ -21,8 +22,8 @@ type UseTagsListResult = { export const useTagsList = (options: TagsListOptions): UseTagsListResult => { const { viewAll, department, filter } = options; - const [itemsList, setItemsList] = useState(() => new RecordList<ILivechatTagRecord>()); - const reload = useCallback(() => setItemsList(new RecordList<ILivechatTagRecord>()), []); + const [itemsList, setItemsList] = useState(() => new RecordList<TagListItem>()); + const reload = useCallback(() => setItemsList(new RecordList<TagListItem>()), []); const getTags = useEndpoint('GET', '/v1/livechat/tags'); diff --git a/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.js b/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx similarity index 68% rename from apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.js rename to apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx index db48060ff546..f0baca991e2e 100644 --- a/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.js +++ b/apps/meteor/ee/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx @@ -1,3 +1,4 @@ +import type { PaginatedMultiSelectOption } from '@rocket.chat/fuselage'; import { PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useTranslation } from '@rocket.chat/ui-contexts'; @@ -7,9 +8,21 @@ import { useRecordList } from '../../../../client/hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../client/hooks/useAsyncState'; import { useTagsList } from '../../hooks/useTagsList'; -const AutoCompleteTagMultiple = (props) => { - const { value, onlyMyTags = false, onChange = () => {}, department, viewAll = false } = props; +type AutoCompleteTagsMultipleProps = { + value?: PaginatedMultiSelectOption[]; + onlyMyTags?: boolean; + onChange?: (value: PaginatedMultiSelectOption[]) => void; + department?: string; + viewAll?: boolean; +}; +const AutoCompleteTagsMultiple = ({ + value = [], + onlyMyTags = false, + onChange = () => undefined, + department, + viewAll = false, +}: AutoCompleteTagsMultipleProps) => { const t = useTranslation(); const [tagsFilter, setTagsFilter] = useState(''); @@ -41,9 +54,11 @@ const AutoCompleteTagMultiple = (props) => { flexShrink={0} flexGrow={0} placeholder={t('Select_an_option')} - endReached={tagsPhase === AsyncStatePhase.LOADING ? () => {} : (start) => loadMoreTags(start, Math.min(50, tagsTotal))} + endReached={ + tagsPhase === AsyncStatePhase.LOADING ? () => undefined : (start) => start && loadMoreTags(start, Math.min(50, tagsTotal)) + } /> ); }; -export default memo(AutoCompleteTagMultiple); +export default memo(AutoCompleteTagsMultiple); diff --git a/apps/meteor/ee/client/omnichannel/tags/CurrentChatTags.tsx b/apps/meteor/ee/client/omnichannel/tags/CurrentChatTags.tsx index 61c1d11af947..553d31b7479d 100644 --- a/apps/meteor/ee/client/omnichannel/tags/CurrentChatTags.tsx +++ b/apps/meteor/ee/client/omnichannel/tags/CurrentChatTags.tsx @@ -3,7 +3,7 @@ import React from 'react'; import AutoCompleteTagsMultiple from './AutoCompleteTagsMultiple'; -type CurrentChatTagsProps = { value: Array<string>; handler: () => void; department?: string; viewAll?: boolean }; +type CurrentChatTagsProps = { value: Array<{ value: string; label: string }>; handler: () => void; department?: string; viewAll?: boolean }; const CurrentChatTags: FC<CurrentChatTagsProps> = ({ value, handler, department, viewAll }) => ( <AutoCompleteTagsMultiple onChange={handler} value={value} department={department} viewAll={viewAll} /> diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 169144cc2788..4d59f52e9cd6 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -18,6 +18,7 @@ import type { ILivechatTagRecord, TransferData, AtLeast, + UserStatus, } from '@rocket.chat/core-typings'; import type { FilterOperators } from 'mongodb'; @@ -52,7 +53,7 @@ interface EventLikeCallbackSignatures { 'livechat.saveRoom': (room: IRoom) => void; 'livechat:afterReturnRoomAsInquiry': (params: { room: IRoom }) => void; 'livechat.setUserStatusLivechat': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void; - 'livechat.agentStatusChanged': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void; + 'livechat.agentStatusChanged': (params: { userId: IUser['_id']; status: UserStatus }) => void; 'livechat.onNewAgentCreated': (agentId: string) => void; 'livechat.afterTakeInquiry': (inq: InquiryWithAgentInfo, agent: { agentId: string; username: string }) => void; 'livechat.afterAgentRemoved': (params: { agent: Pick<IUser, '_id' | 'username'> }) => void; diff --git a/apps/meteor/package.json b/apps/meteor/package.json index c1014a749158..3ee3366f47dd 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -236,7 +236,7 @@ "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.1", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.2", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/fuselage-polyfills": "next", "@rocket.chat/fuselage-toastbar": "next", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json index d6b7e5a0739b..392b4a99b3ad 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ar.i18n.json @@ -4299,10 +4299,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "يمنع هذا الإعداد كل المثيلات من إرسال تغييرات الحالة للمستخدمين إلى عملائهم مع الاحتفاظ بحالة تواجد كل المستخدمين من التحميل الأول!", "Troubleshoot_Disable_Sessions_Monitor": "تعطيل شاشة مراقبة الجلسات", "Troubleshoot_Disable_Sessions_Monitor_Alert": "يوقف هذا الإعداد معالجة جلسات المستخدم، ما يتسبب في توقف الإحصاءات عن العمل بشكل صحيح!", - "Troubleshoot_Disable_Statistics_Generator": "تعطيل منشئ الإحصاءات", - "Troubleshoot_Disable_Statistics_Generator_Alert": "يوقف هذا الإعداد معالجة كل الإحصاءات، ما يجعل صفحة المعلومات قديمة حتى ينقر شخص ما على زر التحديث وقد يتسبب في فقد معلومات أخرى حول النظام!", - "Troubleshoot_Disable_Workspace_Sync": "تعطيل مزامنة مساحة العمل", - "Troubleshoot_Disable_Workspace_Sync_Alert": "يوقف هذا الإعداد مزامنة هذا الخادم مع سحابة Rocket.Chat وقد يتسبب في حدوث مشاكل مع تراخيص السوق والمؤسسة!", "True": "صحيح", "Try_now": "المحاولة الآن", "Try_searching_in_the_marketplace_instead": "محاولة البحث في السوق بدلاً من ذلك", @@ -4855,8 +4851,6 @@ "onboarding.page.requestTrial.subtitle": "جرب أفضل خطة إصدار Enterprise لمدة 30 يومًا مجانًا", "onboarding.page.magicLinkEmail.title": "أرسلنا لك رابط تسجيل الدخول عبر البريد الإلكتروني", "onboarding.page.magicLinkEmail.subtitle": "انقر فوق الرابط الموجود في البريد الإلكتروني الذي أرسلناه لك للتو لتسجيل الدخول إلى مساحة العمل الخاصة بك. <1>ستنتهي صلاحية الرابط خلال 30 دقيقة.</1>", - "onboarding.page.organizationInfoPage.title": "بعض التفاصيل الإضافية...", - "onboarding.page.organizationInfoPage.subtitle": "ستساعدنا هذه على تخصيص مساحة العمل الخاصة بك.", "onboarding.form.adminInfoForm.title": "معلومات المسؤول", "onboarding.form.adminInfoForm.subtitle": "نحتاج إلى هذا لإنشاء ملف شخصي مسؤول داخل مساحة العمل الخاصة بك", "onboarding.form.adminInfoForm.fields.fullName.label": "الاسم الكامل", @@ -4885,10 +4879,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "التكامل مع مقدمي الخدمات الخارجيين (WhatsApp وFacebook وTelegram وTwitter)", "onboarding.form.registeredServerForm.included.apps": "الوصول إلى تطبيقات السوق", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "البريد الإلكتروني لحساب السحابة", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "لتسجيل الخادم الخاص بك، نحتاج إلى توصيله بحسابك السحابي. إذا كان لديك حساب سابقًا، فسنقوم بربطه تلقائيًا. وإن لم يكن لديك، فسيتم إنشاء حساب جديد", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "يرجى إدخال بريدك الإلكتروني", "onboarding.form.registeredServerForm.keepInformed": "أبقني على اطلاع بالأخبار والأحداث", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "يعني التسجيل موافقتي على تلقي تحديثات المنتج والأمان ذات الصلة", "onboarding.form.standaloneServerForm.title": "تأكيد الخادم المستقل", "onboarding.form.standaloneServerForm.servicesUnavailable": "لن تكون بعض الخدمات متاحة أو ستتطلب إعدادًا يدويًا", "onboarding.form.standaloneServerForm.publishOwnApp": "لإرسال الإشعارات، تحتاج إلى تجميع تطبيقك الخاص ونشره على Google Play وApp Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json index b6e15bbf6f66..9f0ef2e27a74 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ca.i18n.json @@ -4227,10 +4227,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Aquesta configuració evita que totes les instàncies enviïn els canvis d'estat dels usuaris als clients, mantenint tots els usuaris amb el seu estat de presència des de la primera càrrega!", "Troubleshoot_Disable_Sessions_Monitor": "Desactiva el monitor de sessions", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Aquesta configuració deté el processament de les sessions de visita de l'LiveChat causant que les estadístiques deixin de funcionar!", - "Troubleshoot_Disable_Statistics_Generator": "Desactivar el generador d'estadístiques", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Aquest ajust deté el processament de totes les estadístiques fent que la pàgina d'informació quedi desactualitzada fins que algú faci clic al botó d'actualització i pot causar que falti altra informació en el sistema!", - "Troubleshoot_Disable_Workspace_Sync": "Desactiva la sincronització de l'espai de treball", - "Troubleshoot_Disable_Workspace_Sync_Alert": "¡Este ajuste detiene la sincronización de este servidor con la nube de Rocket.Chat y puede causar problemas con el mercado y las licencias de las empresas!", "True": "Sí", "Try_now": "Prova-ho ara", "Tuesday": "dimarts", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json index ff081e3a5eeb..c2fbddd26d29 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/cs.i18n.json @@ -3561,10 +3561,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Toto nastavení zakáže všem instancím odesílat změny stavu uživatelů a ponechat si nastavení při prvním načtení", "Troubleshoot_Disable_Sessions_Monitor": "Zakázat monitor sessions", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Toto nastavení přestane zpracovávat uživatelské sessions a statistiky tak přestanou správně fungovat!", - "Troubleshoot_Disable_Statistics_Generator": "Zakázat generování statistik", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Toto nastavení přestane zpracovávat statistiky, takže stránka s informacemi zůstane neaktuální dokud někdo nevynutí aktualizaci. Způsobuje neaktuálnost dat napříč systémem!", - "Troubleshoot_Disable_Workspace_Sync": "Zakázat synchronizaci pracovního prostoru", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Toto nastavení zakáže synchronizaci s Rocket.chat cloud a může způsobit problémy s marketplace a enterprise licencemi!", "True": "Ano", "Try_now": "Zkusit nyní", "Tuesday": "Úterý", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json index 317faac59164..66e3bb1f035e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/da.i18n.json @@ -3582,10 +3582,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Denne indstilling forhindrer alle instancer fra at sende statusændringerne for brugerne til deres klienter, hvilket gør, at alle brugere vil have deres status vedr. tilstedeværelse fra de blev loadet i starten!", "Troubleshoot_Disable_Sessions_Monitor": "Deaktivér sessions-monitor", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Denne indstilling stopper behandlingen af brugersessioner og får statistikkerne til at stoppe med at virke korrekt!", - "Troubleshoot_Disable_Statistics_Generator": "Deaktivér statistik-generator", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Denne indstilling stopper behandlingen af alle statistikker, hvilket gør at informationssiden forældes, indtil nogen klikker på opdateringsknappen og kan også forårsage andre manglende oplysninger rundt omkring i systemet!", - "Troubleshoot_Disable_Workspace_Sync": "Deaktivér synkronisering af Workspace", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Denne indstilling stopper synkroniseringen af denne server med Rocket.Chat's cloud og kan forårsage problemer med marketplace og enteprise-licenser!", "True": "Sandt", "Try_now": "Forsøg nu", "Tuesday": "tirsdag", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json index 47649dd17b2d..52054a3ebdad 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/de.i18n.json @@ -4821,10 +4821,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Diese Einstellung sorgt dafür, dass keine Instanz mehr die Statusänderungen der Benutzer an ihre Clients sendet, sodass die Benutzer den Präsenzstatus behalten, den sie beim ersten Laden hatten!", "Troubleshoot_Disable_Sessions_Monitor": "Sitzungsmonitor deaktivieren", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Diese Einstellung stoppt die Verarbeitung von Benutzersitzungen, was dazu führt, dass die Statistiken nicht mehr ordnungsgemäß funktionieren!", - "Troubleshoot_Disable_Statistics_Generator": "Statistikgenerator deaktivieren", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Diese Einstellung stoppt die Verarbeitung der gesamten Statistik, sodass die Infoseite so lange veraltet ist, bis jemand die Aktualisierungschaltfläche anklickt. Außerdem kann es sein, dass andere Systeminformationen fehlen!", - "Troubleshoot_Disable_Workspace_Sync": "Arbeitsbereichsynchronisierung deaktivieren", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Diese Einstellung stoppt die Synchronisierung des Servers mit der Rocket.Chat-Cloud und kann Probleme mit Marktplatz- und Unternehmenslizenzen verursachen!", "True": "Ja", "Try_now": "Jetzt versuchen", "Try_searching_in_the_marketplace_instead": "Versuchen Sie stattdessen den Marktplatz zu durchsuchen", @@ -4953,7 +4949,7 @@ "User__username__unmuted_in_room__roomName__": "Stummschaltung von Benutzer {{username}} in Raum {{roomName}} aufgehoben", "User_added": "Benutzer hinzugefügt", "User_added_by": "Der Benutzer {{user_added}} wurde von {{user_by}} hinzugefügt", - "User_added_to": "__user_added_ hinzugefügt", + "User_added_to": "hinzugefügt {{user_added}}", "User_added_successfully": "Benutzer erfolgreich hinzugefügt", "User_and_group_mentions_only": "Nur Benutzer- und Gruppenerwähnungen", "User_cant_be_empty": "Benutzer darf nicht leer sein", @@ -5466,8 +5462,6 @@ "onboarding.page.requestTrial.subtitle": "Testen Sie unseren besten Enterprise Edition-Plan 30 Tage lang gratis", "onboarding.page.magicLinkEmail.title": "Wir haben Ihnen einen Anmeldelink gesendet", "onboarding.page.magicLinkEmail.subtitle": "Klicken Sie auf den Link, in der gerade an Sie versandten E-Mail, um sich bei Ihrem Arbeitsbereich anzumelden. <1>Der Link verfällt in 30 Minuten.</1>", - "onboarding.page.organizationInfoPage.title": "Ein paar zusätzliche Details...", - "onboarding.page.organizationInfoPage.subtitle": "Diese helfen uns, Ihren Arbeitsbereich zu personalisieren.", "onboarding.form.adminInfoForm.title": "Admin-Info", "onboarding.form.adminInfoForm.subtitle": "Das ist erforderlich, um ein Admin-Profil in Ihrem Arbeitsbereich zu erstellen", "onboarding.form.adminInfoForm.fields.fullName.label": "Vollständiger Name", @@ -5496,10 +5490,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integration mit externen Anbietern (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Zugriff auf Marktplatz-Apps", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Cloud-Konto-E-Mail", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Um Ihren Server zu registrieren, müssen wir ihn mit Ihrem Cloud-Konto verbinden. Wenn Sie bereits eines haben, werden wir es automatisch verknüpfen. Andernfalls wird ein neues Konto erstellt", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Bitte geben Sie Ihre E-Mail-Adresse ein", "onboarding.form.registeredServerForm.keepInformed": "Informieren Sie mich über Neuigkeiten und Ereignisse", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Mit der Registrierung stimme ich zu, dass ich relevante Produkt- und Sicherheits-Updates erhalte", "onboarding.form.standaloneServerForm.title": "Stand-alone-Server-Bestätigung", "onboarding.form.standaloneServerForm.servicesUnavailable": "Einige der Services werden nicht verfügbar sein oder erfordern eine manuelle Einrichtung", "onboarding.form.standaloneServerForm.publishOwnApp": "Um Push-Benachrichtigungen zu senden, müssen Sie Ihre eigene App kompilieren und in Google Play und im App Store veröffentlichen", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index d4b28724b6e2..001cdf080f7b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -705,6 +705,8 @@ "Authorization_URL": "Authorization URL", "Authorize": "Authorize", "Authorize_access_to_your_account": "Authorize access to your account", + "Automatic_translation_not_available": "Automatic translation not available", + "Automatic_translation_not_available_info": "This room has E2E encryption enabled, translation cannot work with encrypted messages", "Auto_Load_Images": "Auto Load Images", "Auto_Selection": "Auto Selection", "Auto_Translate": "Auto-Translate", @@ -715,11 +717,14 @@ "AutoTranslate_APIKey": "API Key", "AutoTranslate_Change_Language_Description": "Changing the auto-translate language does not translate previous messages.", "AutoTranslate_DeepL": "DeepL", + "AutoTranslate_Disabled_for_room": "Auto-translate disabled for #{{roomName}}", "AutoTranslate_Enabled": "Enable Auto-Translate", "AutoTranslate_Enabled_Description": "Enabling auto-translation will allow people with the `auto-translate` permission to have all messages automatically translated into their selected language. Fees may apply.", + "AutoTranslate_Enabled_for_room": "Auto-translate enabled for #{{roomName}}", "AutoTranslate_AutoEnableOnJoinRoom": "Auto-Translate for non-default language members", "AutoTranslate_AutoEnableOnJoinRoom_Description": "If enabled, whenever a user with a language preference different than the workspace default joins a room, it will be automatically translated for them.", "AutoTranslate_Google": "Google", + "AutoTranslate_language_set_to": "Auto-translate language set to {{language}}", "AutoTranslate_Microsoft": "Microsoft", "AutoTranslate_Microsoft_API_Key": "Ocp-Apim-Subscription-Key", "AutoTranslate_ServiceProvider": "Service Provider", @@ -1676,7 +1681,7 @@ "Discussion": "Discussion", "Discussion_Description": "Discussions are an additional way to organize conversations that allows inviting users from outside channels to participate in specific conversations.", "Discussion_description": "Help keep an overview of what's going on! By creating a discussion, a sub-channel of the one you selected is created and both are linked.", - "Discussion_first_message_disabled_due_to_e2e": "You can start sending End-to-End encrypted messages in this discussion after its creation.", + "Discussion_first_message_disabled_due_to_e2e": "You can start sending End-to-end encrypted messages in this discussion after its creation.", "Discussion_first_message_title": "Your message", "Discussion_name": "Discussion name", "Discussion_start": "Start a Discussion", @@ -1739,6 +1744,8 @@ "Markdown_Marked_Tables": "Enable Marked Tables", "duplicated-account": "Duplicated account", "E2E Encryption": "E2E Encryption", + "E2E_Encryption_enabled_for_room": "End-to-end encryption enabled for #{{roomName}}", + "E2E_Encryption_disabled_for_room": "End-to-end encryption disabled for #{{roomName}}", "Markdown_Parser": "Markdown Parser", "Markdown_SupportSchemesForLink": "Markdown Support Schemes for Link", "E2E Encryption_Description": "Keep conversations private, ensuring only the sender and intended recipients are able to read them.", @@ -1751,7 +1758,7 @@ "E2E_Enabled_Default_DirectRooms": "Enable encryption for Direct Rooms by default", "E2E_Enabled_Default_PrivateRooms": "Enable encryption for Private Rooms by default", "E2E_Encryption_Password_Change": "Change Encryption Password", - "E2E_Encryption_Password_Explanation": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.<br/><br/>This is end to end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store your password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on.", + "E2E_Encryption_Password_Explanation": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.<br/><br/>This is end-to-end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store your password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on.", "E2E_key_reset_email": "E2E Key Reset Notification", "E2E_message_encrypted_placeholder": "This message is end-to-end encrypted. To view it, you must enter your encryption key in your account settings.", "E2E_password_request_text": "To access your encrypted private groups and direct messages, enter your encryption password. <br/>You need to enter this password to encode/decode your messages on every client you use, since the key is not stored on the server.", @@ -1876,7 +1883,7 @@ "Enable_unlimited_apps": "Enable unlimited apps", "Enabled": "Enabled", "Encrypted": "Encrypted", - "Encrypted_channel_Description": "End to end encrypted channel. Search will not work with encrypted channels and notifications may not show the messages content.", + "Encrypted_channel_Description": "End-to-end encrypted channel. Search will not work with encrypted channels and notifications may not show the messages content.", "Encrypted_key_title": "Click here to disable end-to-end encryption for this channel (requires e2ee-permission)", "Encrypted_message": "Encrypted message", "Encrypted_setting_changed_successfully": "Encrypted setting changed successfully", @@ -2604,6 +2611,7 @@ "Install_FxOs_done": "Great! You can now use Rocket.Chat via the icon on your homescreen. Have fun with Rocket.Chat!", "Install_FxOs_error": "Sorry, that did not work as intended! The following error appeared:", "Install_FxOs_follow_instructions": "Please confirm the app installation on your device (press \"Install\" when prompted).", + "Installing": "Installing", "Install_package": "Install package", "Installation": "Installation", "Installed": "Installed", @@ -4986,7 +4994,7 @@ "Teams_New_Description_Label": "Topic", "Teams_New_Description_Placeholder": "What is this team about", "Teams_New_Encrypted_Description_Disabled": "Only available for private team", - "Teams_New_Encrypted_Description_Enabled": "End to end encrypted team. Search will not work with encrypted Teams and notifications may not show the messages content.", + "Teams_New_Encrypted_Description_Enabled": "End-to-end encrypted team. Search will not work with encrypted Teams and notifications may not show the messages content.", "Teams_New_Encrypted_Label": "Encrypted", "Teams_New_Private_Description_Disabled": "When disabled, anyone can join the team", "Teams_New_Private_Description_Enabled": "Only invited people can join", @@ -5185,6 +5193,7 @@ "Transferred": "Transferred", "Translate": "Translate", "Translated": "Translated", + "Translate_to": "Translate to", "Translations": "Translations", "Travel_and_Places": "Travel & Places", "Trigger_removed": "Trigger removed", @@ -6077,6 +6086,14 @@ "Filter_by_room": "Filter by room type", "Filter_by_visibility": "Filter by visibility", "Theme_Appearence": "Theme Appearence", + "mentions_counter": "{{count}} mention", + "mentions_counter_plural": "{{count}} mentions", + "threads_counter": "{{count}} unread threaded message", + "threads_counter_plural": "{{count}} unread threaded messages", + "group_mentions_counter": "{{count}} group mention", + "group_mentions_counter_plural": "{{count}} group mentions", + "unread_messages_counter": "{{count}} unread message", + "unread_messages_counter_plural": "{{count}} unread messages", "Premium": "Premium", "Premium_capability": "Premium capability" -} +} \ No newline at end of file diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json index 50a7b8d63873..aac9f8bfa4db 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/es.i18n.json @@ -4268,10 +4268,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Este ajuste evita que todas las instancias envíen los cambios de estado de los usuarios a sus clientes, lo que mantiene todos los usuarios con su estado de presencia desde la primera carga.", "Troubleshoot_Disable_Sessions_Monitor": "Deshabilitar supervisor de sesiones", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Este ajuste detiene el procesamiento de las sesiones de visita de Omnichannel, lo que provoca que las estadísticas dejen de funcionar correctamente.", - "Troubleshoot_Disable_Statistics_Generator": "Deshabilitar generador de estadísticas", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Este ajuste detiene el procesamiento de todas las estadísticas, lo que provoca que la página de información quede desactualizada hasta que alguien haga clic en el botón para actualizar. Además, puede causar que falte otra información en el sistema.", - "Troubleshoot_Disable_Workspace_Sync": "Deshabilitar sincronización de espacio de trabajo", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Este ajuste detiene la sincronización de este servidor con la nube de Rocket.Chat y puede problemas con las licencias de empresas y Marketplace.", "True": "Verdadero", "Try_now": "Intentar ahora", "Try_searching_in_the_marketplace_instead": "Prueba a buscar en Marketplace en su lugar", @@ -4805,8 +4801,6 @@ "onboarding.page.requestTrial.subtitle": "Prueba nuestro mejor plan Enterprise Edition gratis durante 30 días", "onboarding.page.magicLinkEmail.title": "Te hemos enviado un enlace de inicio de sesión por correo electrónico", "onboarding.page.magicLinkEmail.subtitle": "Haz clic en el enlace del mensaje que acabamos de enviarte para iniciar sesión en tu espacio de trabajo. <1>El enlace caducará en 30 minutos.</1>", - "onboarding.page.organizationInfoPage.title": "Unos detalles más...", - "onboarding.page.organizationInfoPage.subtitle": "Esto nos ayudará a personalizar tu espacio de trabajo.", "onboarding.form.adminInfoForm.title": "Información de administrador", "onboarding.form.adminInfoForm.subtitle": "Necesitamos esto para crear un perfil de administrador en tu espacio de trabajo", "onboarding.form.adminInfoForm.fields.fullName.label": "Nombre completo", @@ -4835,10 +4829,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integración con proveedores externos (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Acceso al Marketplace de aplicaciones", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Cuenta de correo electrónico en la nube", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Para registrar el servidor, necesitamos conectarlo a tu cuenta en la nube. Si ya tienes una, la vincularemos automáticamente. De lo contrario, se creará una cuenta nueva", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Introduce tu correo electrónico", "onboarding.form.registeredServerForm.keepInformed": "Recibir información sobre noticias y eventos", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Al registrarme, acepto recibir actualizaciones sobre seguridad y productos relevantes", "onboarding.form.standaloneServerForm.title": "Confirmación de servidor independiente", "onboarding.form.standaloneServerForm.servicesUnavailable": "Algunos servicios no estarán disponibles o requerirán configuración manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviarte notificaciones push, debes compilar y publicar tu propia aplicación en Google Play y App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json index 34c7ca9fdda4..79a27d83cbe6 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json @@ -4917,10 +4917,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Tämä asetus estää kaikkia instansseja lähettämästä käyttäjien tilamuutoksia asiakkailleen, jolloin kaikki käyttäjät pysyvät läsnäolotilassaan ensimmäisestä latauksesta lähtien!", "Troubleshoot_Disable_Sessions_Monitor": "Istuntojen valvonnan poistaminen käytöstä", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Tämä asetus pysäyttää käyttäjäistuntojen käsittelyn, jolloin tilastot eivät enää toimi oikein!", - "Troubleshoot_Disable_Statistics_Generator": "Poista tilastogeneraattori käytöstä", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Tämä asetus pysäyttää kaikkien tilastojen käsittelyn, jolloin infosivu on vanhentunut, kunnes joku klikkaa päivityspainiketta, ja se voi aiheuttaa muita puuttuvia tietoja järjestelmästä!", - "Troubleshoot_Disable_Workspace_Sync": "Työtilan synkronoinnin poistaminen käytöstä", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Tämä asetus estää tämän palvelimen synkronoinnin Rocket.Chatin pilven kanssa ja saattaa aiheuttaa ongelmia markkinapaikan ja enteprise-lisenssien kanssa!", "True": "Tosi", "Try_now": "Kokeile nyt", "Try_searching_in_the_marketplace_instead": "Kokeile sen sijaan etsiä Kauppapaikalta", @@ -5603,8 +5599,6 @@ "onboarding.page.requestTrial.subtitle": "Kokeile parasta yritysversion sopimustamme 30 päivää maksutta", "onboarding.page.magicLinkEmail.title": "Lähetimme sinulle kirjautumislinkin sähköpostitse", "onboarding.page.magicLinkEmail.subtitle": "Klikkaa juuri lähettämässämme sähköpostiviestissä olevaa linkkiä kirjautuaksesi työtilaasi. <1>Linkki päättyy 30 minuutin kuluttua.</1>", - "onboarding.page.organizationInfoPage.title": "Muutama yksityiskohta vielä...", - "onboarding.page.organizationInfoPage.subtitle": "Nämä auttavat meitä muokkaamaan työtilasi yksilölliseksi.", "onboarding.form.adminInfoForm.title": "Admin Info", "onboarding.form.adminInfoForm.subtitle": "Tarvitsemme tätä luodaksemme ylläpitäjäprofiilin työtilaasi", "onboarding.form.adminInfoForm.fields.fullName.label": "Koko nimi", @@ -5633,12 +5627,10 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integrointi ulkoisten palveluntarjoajien kanssa (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Pääsy kauppapaikan sovelluksiin", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Cloud-tilin sähköposti", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Rekisteröidäksemme palvelimesi meidän on yhdistettävä se pilvitiliisi. Jos sinulla on jo sellainen - yhdistämme sen automaattisesti. Muussa tapauksessa luodaan uusi tili", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Kirjoita sähköpostiosoitteesi", "onboarding.form.registeredServerForm.keepInformed": "Pidä minut ajan tasalla uutisista ja tapahtumista", "onboarding.form.registeredServerForm.registerLater": "Rekisteröidy myöhemmin", "onboarding.form.registeredServerForm.notConnectedToInternet": "Palvelin ei ole yhteydessä internetiin, joten työtila on rekisteröitävä offline-tilassa.", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Rekisteröitymällä suostun vastaanottamaan asiaankuuluvia tuote- ja tietoturvapäivityksiä", "onboarding.form.standaloneServerForm.title": "Itsenäisen palvelimen vahvistus", "onboarding.form.standaloneServerForm.servicesUnavailable": "Jotkin palvelut eivät ole käytettävissä tai vaativat manuaalista asennusta", "onboarding.form.standaloneServerForm.publishOwnApp": "Jotta voit lähettää push-ilmoituksia, sinun on koottava ja julkaistava oma sovelluksesi Google Play- ja App Store -sovelluksissa", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json index 08641b831f94..f460fc0b61de 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/fr.i18n.json @@ -4300,10 +4300,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Ce paramètre empêche toutes les instances d'envoyer les changements de statut des utilisateurs à leurs clients ; le statut de présence du premier chargement est donc conservé !", "Troubleshoot_Disable_Sessions_Monitor": "Désactiver le moniteur de sessions", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Ce paramètre arrête le traitement des sessions utilisateur, ce qui empêche les statistiques de fonctionner correctement !", - "Troubleshoot_Disable_Statistics_Generator": "Désactiver le générateur de statistiques", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Ce paramètre arrête le traitement de toutes les statistiques, ce qui rend la page d'informations obsolète jusqu'à ce que quelqu'un clique sur le bouton d'actualisation ; d'autres informations peuvent être manquantes dans le système !", - "Troubleshoot_Disable_Workspace_Sync": "Désactiver la synchronisation de l'espace de travail", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Ce paramètre arrête la synchronisation de ce serveur avec le cloud Rocket.Chat et peut entraîner des problèmes avec les licences marketplace et entreprise !", "True": "Vrai", "Try_now": "Essayer maintenant", "Try_searching_in_the_marketplace_instead": "Essayez plutôt de chercher sur le marketplace", @@ -4855,8 +4851,6 @@ "onboarding.page.requestTrial.subtitle": "Essayez notre meilleur forfait Enterprise Edition gratuitement pendant 30 jours", "onboarding.page.magicLinkEmail.title": "Nous vous avons envoyé un lien de connexion par e-mail", "onboarding.page.magicLinkEmail.subtitle": "Cliquez sur le lien dans l'e-mail que nous venons de vous envoyer pour vous connecter à votre espace de travail. <1>Le lien expirera dans 30 minutes.</1>", - "onboarding.page.organizationInfoPage.title": "Quelques détails supplémentaires...", - "onboarding.page.organizationInfoPage.subtitle": "Ceux-ci nous aideront à personnaliser votre espace de travail.", "onboarding.form.adminInfoForm.title": "Infos sur l'administrateur", "onboarding.form.adminInfoForm.subtitle": "Nous en avons besoin pour créer un profil d'administrateur dans votre espace de travail", "onboarding.form.adminInfoForm.fields.fullName.label": "Nom complet", @@ -4885,10 +4879,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Intégration avec des fournisseurs externes (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Accès aux applications du marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mail du compte cloud", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Pour enregistrer votre serveur, nous devons le connecter à votre compte cloud. Si vous en avez déjà un, nous l'associerons automatiquement. Sinon, un nouveau compte sera créé", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Entrez votre adresse e-mail", "onboarding.form.registeredServerForm.keepInformed": "Me tenir informé des actualités et des événements", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "En m'inscrivant, j'accepte de recevoir des mises à jour pertinentes concernant les produits et la sécurité", "onboarding.form.standaloneServerForm.title": "Confirmation du serveur autonome", "onboarding.form.standaloneServerForm.servicesUnavailable": "Certains services ne seront pas disponibles ou nécessiteront une configuration manuelle", "onboarding.form.standaloneServerForm.publishOwnApp": "Pour envoyer des notifications push, vous devez compiler et publier votre propre application sur Google Play et App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json index 5272cd9e1d3f..9ee622195a6a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/gl.i18n.json @@ -352,8 +352,6 @@ "onboarding.page.requestTrial.subtitle": "Proba o noso mellor plana de empresas durante 30 días de balde", "onboarding.page.magicLinkEmail.title": "Enviámosche por correo electrónico un link de inicio de sesión", "onboarding.page.magicLinkEmail.subtitle": "Fai clic na ligazón do correo electrónico que che acabamos de enviar para iniciar sesión no teu espazo de traballo. <1>A ligazón caducará en 30 minutos.</1>", - "onboarding.page.organizationInfoPage.title": "Algúns detalles máis...", - "onboarding.page.organizationInfoPage.subtitle": "Estes axudaranos a personalizar o teu espazo de traballo.", "onboarding.form.adminInfoForm.title": "Información administrativa", "onboarding.form.adminInfoForm.subtitle": "Necesitamos isto para crear un perfil de administrador dentro do teu espazo de traballo", "onboarding.form.adminInfoForm.fields.fullName.label": "Nome completo", @@ -382,10 +380,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integración con provedores externos (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Acceso a aplicacións do mercado", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Correo electrónico da conta na nube", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Para rexistrar o teu servidor, necesitamos conectalo á túa conta na nube. Se xa tes un, vincularémolo automaticamente. En caso contrario, crearase unha nova conta", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Introduce o teu correo electrónico", "onboarding.form.registeredServerForm.keepInformed": "Mantéñame informado sobre novidades e eventos", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Ao rexistrarme, acepto recibir actualizacións de produtos e seguridade relevantes", "onboarding.form.standaloneServerForm.title": "Confirmación do servidor autónomo", "onboarding.form.standaloneServerForm.servicesUnavailable": "Algúns dos servizos non estarán dispoñibles ou requirirán unha configuración manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviar notificacións push, debes compilar e publicar a túa propia aplicación en Google Play e App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json index e54f69c81052..1177a356dc71 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/hu.i18n.json @@ -4736,10 +4736,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Ez a beállítás megakadályozza az összes példányt abban, hogy elküldjék a felhasználók állapotváltozásait az ügyfeleiknek, megtartva az összes felhasználót az első betöltésből származó jelenléti állapotával!", "Troubleshoot_Disable_Sessions_Monitor": "Munkamenetek megfigyelőjének letiltása", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Ez a beállítás leállítja a felhasználók munkameneteinek feldolgozását, ami a statisztikák megfelelő működésének megszűnését okozza!", - "Troubleshoot_Disable_Statistics_Generator": "Statisztika-előállító letiltása", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Ez a beállítás leállítja az összes statisztika feldolgozását, ami az információs oldalt elavulttá teszi, amíg valaki nem kattint a frissítés gombra, valamint más információk hiányát is okozhatja a rendszerben!", - "Troubleshoot_Disable_Workspace_Sync": "Munkaterület szinkronizálásának letiltása", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Ez a beállítás leállítja ennek a kiszolgálónak a Rocket.Chat felhőjével való szinkronizálását, és problémákat okozhat a piactérrel és a vállalati licencekkel!", "True": "Igaz", "Try_now": "Próbálja most", "Try_searching_in_the_marketplace_instead": "Próbáljon inkább a piactéren keresni", @@ -5393,8 +5389,6 @@ "onboarding.page.requestTrial.subtitle": "Próbálja ki a legjobb vállalati kiadású előfizetéses csomagunkat 30 napig ingyen", "onboarding.page.magicLinkEmail.title": "Elküldünk Önnek egy bejelentkezési hivatkozást e-mailben", "onboarding.page.magicLinkEmail.subtitle": "Kattintson a most elküldött levélben lévő hivatkozásra, hogy bejelentkezhessen a munkaterületére. <1>A hivatkozás 30 percen belül lejár.</1>", - "onboarding.page.organizationInfoPage.title": "Néhány további részlet…", - "onboarding.page.organizationInfoPage.subtitle": "Ezek segítenek nekünk személyre szabni a munkaterületét.", "onboarding.form.adminInfoForm.title": "Adminisztrátor-információk", "onboarding.form.adminInfoForm.subtitle": "Erre azért van szükségünk, hogy létrehozzunk egy adminisztrátori profilt a munkaterületén belül", "onboarding.form.adminInfoForm.fields.fullName.label": "Teljes név", @@ -5423,10 +5417,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integráció külső szolgáltatókkal (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Hozzáférés a piactér alkalmazásaihoz", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Felhős fiók e-mail-címe", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "A kiszolgálója regisztrálásához csatlakoztatnunk kell azt a felhős fiókjához. Ha már rendelkezik ilyennel, akkor automatikusan összekapcsoljuk. Ellenkező esetben új fiók kerül létrehozásra.", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Adja meg az e-mail-címét", "onboarding.form.registeredServerForm.keepInformed": "Tájékoztassanak a hírekről és az eseményekről", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "A regisztrációval beleegyezem, hogy megkapom a vonatkozó termék- és biztonsági frissítéseket", "onboarding.form.standaloneServerForm.title": "Egyedülálló kiszolgáló megerősítése", "onboarding.form.standaloneServerForm.servicesUnavailable": "Néhány szolgáltatás nem lesz elérhető, vagy kézi beállítást igényel", "onboarding.form.standaloneServerForm.publishOwnApp": "A leküldéses értesítések küldéséhez saját alkalmazást kell összeállítania és közzétennie a Google Play és az App Store áruházakban", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json index 0bf0eb5063ae..31973b5da92a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ja.i18n.json @@ -4259,10 +4259,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "この設定は、すべてのインスタンスがユーザーのステータス変更をクライアントに送信することを防ぎ、すべてのユーザーを最初のロードからのプレゼンスステータスで維持します!", "Troubleshoot_Disable_Sessions_Monitor": "セッションモニターを無効にする", "Troubleshoot_Disable_Sessions_Monitor_Alert": "この設定により、ユーザーセッションの処理が停止し、統計が正しく機能しなくなります!", - "Troubleshoot_Disable_Statistics_Generator": "統計ジェネレーターを無効にする", - "Troubleshoot_Disable_Statistics_Generator_Alert": "この設定では、更新ボタンがクリックされるまですべての統計処理が停止され、情報ページの情報が最新ではなくなり、システムに関するその他の情報が失われる可能性があります。", - "Troubleshoot_Disable_Workspace_Sync": "ワークスペース同期を無効にする", - "Troubleshoot_Disable_Workspace_Sync_Alert": "この設定により、このサーバーとRocket.Chatのクラウドとの同期が停止し、マーケットプレイスとエンタープライズライセンスで問題が発生する可能性があります!", "True": "はい", "Try_now": "今すぐ再試行", "Try_searching_in_the_marketplace_instead": "代わりにマーケットプレイスを検索してみてください", @@ -4805,8 +4801,6 @@ "onboarding.page.requestTrial.subtitle": "30日間の最上位のEnterprise Editionプランを無料でお試しください", "onboarding.page.magicLinkEmail.title": "ログインリンクをメールで送信しました", "onboarding.page.magicLinkEmail.subtitle": "送信されたメールのリンクをクリックし、ワークスペースにサインインしてください。 <1>リンクの有効期間は30分です。</1>", - "onboarding.page.organizationInfoPage.title": "その他の詳細...", - "onboarding.page.organizationInfoPage.subtitle": "これにより、ワークスペースをパーソナライズできます。", "onboarding.form.adminInfoForm.title": "管理者情報", "onboarding.form.adminInfoForm.subtitle": "これはワークスペース内に管理プロファイルを作成するために必要です", "onboarding.form.adminInfoForm.fields.fullName.label": "氏名", @@ -4835,10 +4829,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "外部プロバイダー(WhatsApp、Facebook、Telegram、Twitter)との統合", "onboarding.form.registeredServerForm.included.apps": "マーケットプレイスアプリにアクセス", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "クラウドアカウントメール", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "サーバーを登録するには、サーバーをクラウドアカウントに接続する必要があります。アカウントをすでにお持ちの場合は、自動的にリンクします。それ以外の場合は、新しいアカウントが作成されます", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "メールアドレスを入力してください", "onboarding.form.registeredServerForm.keepInformed": "ニュースとイベントの情報を受け取る", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "登録すると、関連する製品とセキュリティの更新を受け取ることに同意したものとみなされます", "onboarding.form.standaloneServerForm.title": "スタンドアロンサーバーの確認", "onboarding.form.standaloneServerForm.servicesUnavailable": "一部のサービスは利用できないか、手動で設定する必要があります", "onboarding.form.standaloneServerForm.publishOwnApp": "プッシュ通知を送信するには、独自のアプリをコンパイルしてGoogle PlayとApp Storeに公開する必要があります", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json index 15514a726e5b..6dd72917fe9d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ka-GE.i18n.json @@ -3303,10 +3303,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "ეს პარამეტრი ყვენა ინსტანციისთვის თიშავს მომხმარებლის სტატუსის გადაგზავნას კლიენტებისთვის და ტოვებს ყველა მომხმარებელს იმ სტატუსით რომლითაც იყო პირველი ჩატვირთვისას.", "Troubleshoot_Disable_Sessions_Monitor": "გამორთეთ სესიების მონიტორი", "Troubleshoot_Disable_Sessions_Monitor_Alert": "ეს პარამეტრი თიშავს მომხმარებლის სესიების დამუშავებას და იწვევს სტატისტიკის არასწორ მუშაობას", - "Troubleshoot_Disable_Statistics_Generator": "გამორთეთ სტატისტიკის გენერატორი", - "Troubleshoot_Disable_Statistics_Generator_Alert": "ეს პარამეტრი სტატისტიკის დამუშავებას თიშავს სრულად და გვერდი ხდება ვადაგასული ვიდრე ვინმე განახლების ღილაკს არ დააჭერს, ამან შეიძლება გამოიწვიოს ზოგი ინფორმაციის დაკარგვა", - "Troubleshoot_Disable_Workspace_Sync": "გამორთეთ სამუშაო ადგილის სინქრონიზაცია", - "Troubleshoot_Disable_Workspace_Sync_Alert": "ეს პარამეტრი თიშავს ამ სერვერის Rocket.Chat's clou-თან სინქრონიზაციას და შეიძლება გამოიწვიოს პრობლემები მარკეტში და საწარმო ლიცენზიებში!", "True": "მართალია", "Try_now": "სცადე ახლა", "Tuesday": "სამშაბათი", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json index 34f7608c9bed..2574c0b288b8 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ko.i18n.json @@ -3624,10 +3624,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "이 설정은 모든 인스턴스가 사용자의 상태 변경 사항을 클라이언트로 보내지 않으며, 설정 시, 모든 사용자의 상태가 처음 로딩상태로 유지됩니다.!", "Troubleshoot_Disable_Sessions_Monitor": "세션 모니터링 사용중지", "Troubleshoot_Disable_Sessions_Monitor_Alert": "이 설정은 사용자 세션 처리를 중단하는 것입니다. 설정 시 통계가 올바르게 작동하지 않을 수 있습니다. ", - "Troubleshoot_Disable_Statistics_Generator": "통계 생성 사용중지", - "Troubleshoot_Disable_Statistics_Generator_Alert": "이 설정은 누군가가 새로 고침 버튼을 클릭 할 때까지 이전 정보 페이지를 생성하는 모든 통계 처리를 중지하는 것입니다. 설정 시 시스템 주변에 다른 정보가 누락 될 수 있습니다!", - "Troubleshoot_Disable_Workspace_Sync": " Workspace 동기화 사용중지", - "Troubleshoot_Disable_Workspace_Sync_Alert": "이 설정은 서버와 Rocket.Chat의 클라우드 동기화를 중지하는 것입니다. 설정 시 Marketplace 및 기업 라이선스에 문제가 발생할 수 있습니다. ", "True": "설정됨", "Try_now": "지금 시도", "Tuesday": "화요일", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json index f611b39aebd4..eff6811050c1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/nl.i18n.json @@ -4290,10 +4290,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Deze instelling voorkomt dat alle instanties de statuswijzingen van de gebruikers naar hun clients sturen, waarbij alle gebruikers hun aanwezigheidsstatus behouden van de eerste lading!", "Troubleshoot_Disable_Sessions_Monitor": "Schakel sessies monitor uit", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Deze instelling stopt de verwerking van gebruikerssessies waardoor de statistieken niet meer correct werken!", - "Troubleshoot_Disable_Statistics_Generator": "Schakel statistieken generator uit", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Deze instelling stopt de verwerking van alle statistieken waardoor de info-pagina verouderd raakt totdat iemand op de Vernieuwen knop klikt, en kan ontbrekende informatie in het systeem verzoorzaken!", - "Troubleshoot_Disable_Workspace_Sync": "Schakel Workspace Sync uit", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Deze instelling stopt de synchronisatie van deze server met Rocket.Chat's cloud en kan problemen veroorzaken met marktplaats- en bedrijfslicenties!", "True": "Waar", "Try_now": "Probeer nu", "Try_searching_in_the_marketplace_instead": "Probeer in plaats daarvan in de Marketplace te zoeken", @@ -4843,8 +4839,6 @@ "onboarding.page.requestTrial.subtitle": "Probeer gratis onze beste Enterprise Edition-abonnement gedurende 30 dagen", "onboarding.page.magicLinkEmail.title": "We hebben je een inloglink gemaild.", "onboarding.page.magicLinkEmail.subtitle": "Klik op de link in de e-mail die we u zojuist hebben gestuurd om u aan te melden bij uw werkruimte. <1>De link verloopt over 30 minuten.</1>", - "onboarding.page.organizationInfoPage.title": "Nog een paar details...", - "onboarding.page.organizationInfoPage.subtitle": "Deze zullen ons helpen om uw werkruimte te personaliseren.", "onboarding.form.adminInfoForm.title": "Admin info", "onboarding.form.adminInfoForm.subtitle": "We hebben dit nodig om een beheerdersprofiel in uw werkruimte te maken", "onboarding.form.adminInfoForm.fields.fullName.label": "Volledige naam", @@ -4873,10 +4867,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integratie met externe providers (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Toegang tot Marketplace-apps", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mailadres van cloudaccount", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Om uw server te registreren, moeten we deze verbinden met uw cloudaccount. Als u er al een heeft, zullen we deze automatisch koppelen. Anders wordt er een nieuwe account aangemaakt", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Voer uw e-mailadres in", "onboarding.form.registeredServerForm.keepInformed": "Hou me op de hoogte van nieuws en evenementen", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Door te registreren ga ik akkoord met het ontvangen van relevante product- en beveiligingsupdates", "onboarding.form.standaloneServerForm.title": "Standalone serverbevestiging", "onboarding.form.standaloneServerForm.servicesUnavailable": "Sommige diensten zullen niet beschikbaar zijn of vereisen handmatige configuratie", "onboarding.form.standaloneServerForm.publishOwnApp": "Om pushmeldingen te verzenden, moet u uw eigen app compileren en publiceren in Google Play en App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json index 0c4b85e588e0..80ab48d383c0 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pl.i18n.json @@ -4667,10 +4667,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "To ustawienie zapobiega wysyłaniu przez wszystkie instancje zmian statusu użytkowników do ich klientów, utrzymując status wszystkich użytkowników taki jak przy pierwszym załadowaniu!", "Troubleshoot_Disable_Sessions_Monitor": "Wyłącz monitor sesji", "Troubleshoot_Disable_Sessions_Monitor_Alert": "To ustawienie zatrzymuje przetwarzanie sesji użytkowników, co spowoduje niepoprawne działanie statystyk!", - "Troubleshoot_Disable_Statistics_Generator": "Wyłącz generator statystyk", - "Troubleshoot_Disable_Statistics_Generator_Alert": "To ustawienie zatrzymuje przetwarzanie wszystkich statystyk powodując, że strona informacyjna stanie się nieaktualna dopóki nie zostanie naciśnięty przycisk odświeżania i może wywołać utratę innych informacji w całym systemie!", - "Troubleshoot_Disable_Workspace_Sync": "Wyłączenie Workspace Sync", - "Troubleshoot_Disable_Workspace_Sync_Alert": "To ustawienie zatrzymuje synchronizację tego serwera z chmurą Rocket.Chat co może wywołać problemy z marketplace i licencjami korporacyjnymi!", "True": "Tak", "Try_now": "Spróbuj teraz", "Try_searching_in_the_marketplace_instead": "Zamiast tego spróbuj poszukać w Marketplace", @@ -5304,8 +5300,6 @@ "onboarding.page.requestTrial.subtitle": "Wypróbuj nasz najlepszy plan Enterprise Edition przez 30 dni za darmo", "onboarding.page.magicLinkEmail.title": "Wysłaliśmy Ci link do logowania", "onboarding.page.magicLinkEmail.subtitle": "Kliknij link w wiadomości e-mail, którą właśnie do Ciebie wysłaliśmy, aby zalogować się do swojego obszaru roboczego. <1>Link wygaśnie za 30 minut.</1>", - "onboarding.page.organizationInfoPage.title": "Jeszcze kilka szczegółów...", - "onboarding.page.organizationInfoPage.subtitle": "Pomogą nam one spersonalizować Twoje miejsce pracy.", "onboarding.form.adminInfoForm.title": "Admin Info", "onboarding.form.adminInfoForm.subtitle": "Potrzebujemy tego, aby utworzyć profil administratora w twoim obszarze roboczym", "onboarding.form.adminInfoForm.fields.fullName.label": "Pełna nazwa", @@ -5334,10 +5328,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integracja z zewnętrznymi dostawcami (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Dostęp do aplikacji w Marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mail konta w chmurze", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Aby zarejestrować Twój serwer, musimy połączyć go z Twoim kontem w chmurze. Jeśli już je posiadasz - połączymy je automatycznie. W przeciwnym razie, zostanie utworzone nowe konto", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Proszę wpisać swój adres e-mail", "onboarding.form.registeredServerForm.keepInformed": "Informuj mnie o nowościach i wydarzeniach", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Rejestrując się, wyrażam zgodę na otrzymywanie odpowiednich aktualizacji produktów i zabezpieczeń", "onboarding.form.standaloneServerForm.title": "Potwierdzenie serwera standalone", "onboarding.form.standaloneServerForm.servicesUnavailable": "Niektóre z usług będą niedostępne lub będą wymagały ręcznej konfiguracji", "onboarding.form.standaloneServerForm.publishOwnApp": "W celu wysyłania powiadomień push należy skompilować i opublikować własną aplikację w Google Play i App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 80d17019855c..7dd1b47f335e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -621,6 +621,8 @@ "Author_Site": "Página do autor", "Authorization_URL": "URL de autorização", "Authorize": "Autorizar", + "Automatic_translation_not_available": "Tradução automática indisponível", + "Automatic_translation_not_available_info": "Esta sala tem criptografia E2E ativada, a tradução não pode funcionar com mensagens criptografadas", "Auto_Load_Images": "Carregar imagens automaticamente", "Auto_Selection": "Seleção automática", "Auto_Translate": "Traduzir automaticamente", @@ -631,9 +633,12 @@ "AutoTranslate_APIKey": "Key da API", "AutoTranslate_Change_Language_Description": "Alterar o idioma de tradução automática não traduz mensagens anteriores.", "AutoTranslate_DeepL": "DeepL", + "AutoTranslate_Disabled_for_room": "Tradução automática desabilitada para #{{roomName}}", "AutoTranslate_Enabled": "Habilitar tradução automática", "AutoTranslate_Enabled_Description": "Habilitar a tradução automática implicará em permitir que as pessoas com a permissão `auto-translate` tenham todas as suas mensagens automaticamente traduzidas para seu idioma. Taxas podem ser cobradas.", + "AutoTranslate_Enabled_for_room": "Tradução automática habilitada para #{{roomName}}", "AutoTranslate_Google": "Google", + "AutoTranslate_language_set_to": "Linguagem para tradução automática definida como {{language}}", "AutoTranslate_Microsoft": "Microsoft", "AutoTranslate_Microsoft_API_Key": "Ocp-Apim-Subscription-Key", "AutoTranslate_ServiceProvider": "Provedor de serviço", @@ -1515,6 +1520,7 @@ "Markdown_Marked_Tables": "Ativar tabelas marcadas", "duplicated-account": "Conta duplicada", "E2E Encryption": "Criptografia E2E", + "E2E_Encryption_enabled_for_room": "Criptografia E2E habilitada para #{{roomName}}", "Markdown_Parser": "Parser de marcação", "Markdown_SupportSchemesForLink": "Esquemas de links compatíveis com marcação", "Markdown_SupportSchemesForLink_Description": "Lista de esquemas permitidos separados por vírgulas", @@ -4336,6 +4342,7 @@ "Transferred": "Transferido", "Translate": "Traduzir", "Translated": "Traduzido", + "Translate_to": "Traduzir para", "Translations": "Traduções", "Travel_and_Places": "Viagem e Locais", "Trigger_removed": "Gatilho removido", @@ -4355,10 +4362,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Esta configuração impede todas as instâncias de enviar as alterações de status dos usuários aos seus clientes, mantendo o status de presença do primeiro carregamento!", "Troubleshoot_Disable_Sessions_Monitor": "Desativar monitor de sessões", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Esta configuração interrompe o processamento das sessões do usuário, fazendo com que as estatísticas parem de funcionar corretamente!", - "Troubleshoot_Disable_Statistics_Generator": "Desativar gerador de estatísticas", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Esta configuração interrompe o processamento de todas as estatísticas, tornando a informação da página desatualizada até que alguém clique no botão Atualizar, e poderá causar perda de outras informações em todo o sistema!", - "Troubleshoot_Disable_Workspace_Sync": "Desativa a sincronização do espaço de trabalho", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Esta configuração interrompe a sincronização deste servidor com a nuvem do Rocket.Chat e pode causar problemas com licenças de marketplace e enteprise!", "True": "Verdadeiro", "Try_now": "Tentar agora", "Try_searching_in_the_marketplace_instead": "Tente pesquisar no marketplace", @@ -4922,8 +4925,6 @@ "onboarding.page.requestTrial.subtitle": "Experimento nosso melhor plano Enterprise Edition grátis por 30 dias", "onboarding.page.magicLinkEmail.title": "Nós enviamos um link de login por e-mail", "onboarding.page.magicLinkEmail.subtitle": "Clique no link no e-mail que enviamos para iniciar sessão em seu espaço de trabalho. <1>O link vai expirar em 30 minutos.</1>", - "onboarding.page.organizationInfoPage.title": "Mais alguns detalhes...", - "onboarding.page.organizationInfoPage.subtitle": "Isso nos ajudará a personalizar seu espaço de trabalho.", "onboarding.form.adminInfoForm.title": "Informação administrativa", "onboarding.form.adminInfoForm.subtitle": "Precisamos disso para criar um perfil de administração dentro do seu espaço de trabalho.", "onboarding.form.adminInfoForm.fields.fullName.label": "Nome completo", @@ -4952,10 +4953,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integração com provedores externos (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Acesso a aplicativos de Marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-mail da conta da nuvem", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Para registrar seu servidor, precisamos conectar à sua conta da nuvem. Se você já tem uma - nós conectaremos a ela automaticamente. Caso contrário, uma nova conta será criada", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Insira seu e-mail", "onboarding.form.registeredServerForm.keepInformed": "Mantenha-me informado sobre notícias e eventos", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Ao registrar, eu concordo em receber atualizações relevantes do produto e de segurança", "onboarding.form.standaloneServerForm.title": "Confirmação de servidor standalone", "onboarding.form.standaloneServerForm.servicesUnavailable": "Alguns dos serviços estarão indisponíveis ou precisarão de configuração manual", "onboarding.form.standaloneServerForm.publishOwnApp": "Para enviar notificações de push, você precisará compilar e publicar seu próprio aplicativo no Google Play e App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json index 8f71b6f49ab2..48557a3a146e 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/ru.i18n.json @@ -4463,10 +4463,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Эта настройка не позволяет всем инстансам отправлять изменения статуса пользователей своим клиентам, сохраняя всех пользователей со статусом присутствия с первой загрузки!", "Troubleshoot_Disable_Sessions_Monitor": "Отключить монитор сессий", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Эта настройка останавливает обработку пользовательских сессий, в результате чего статистика перестает работать корректно!", - "Troubleshoot_Disable_Statistics_Generator": "Отключить генератор статистики", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Эта настройка останавливает обработку всей статистики, делая информационную страницу устаревшей до тех пор, пока кто-нибудь не нажмет кнопку обновления, это может привести к появлению другой недостающей информации в системе!", - "Troubleshoot_Disable_Workspace_Sync": "Отключить синхронизацию рабочего пространства", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Эта настройка останавливает синхронизацию данного сервера с Rocket.Chat Cloud и может привести к проблемам с корпоративами лицензиями и в магазине приложений!", "True": "Да", "Try_now": "Попробуйте сейчас", "Try_searching_in_the_marketplace_instead": "Попробуйте выполнить поиск в магазине", @@ -5041,8 +5037,6 @@ "onboarding.page.requestTrial.subtitle": "Воспользуйтесь нашим лучшим тарифным планом Enterprise Edition в течение 30 дней бесплатно", "onboarding.page.magicLinkEmail.title": "Мы отправили вам ссылку для входа в систему в электронном письме", "onboarding.page.magicLinkEmail.subtitle": "Нажмите на ссылку в электронном письме, чтобы войти в свое рабочее пространство. <1>Срок действия ссылки истечет через 30 минут.</1>", - "onboarding.page.organizationInfoPage.title": "Дополнительные сведения...", - "onboarding.page.organizationInfoPage.subtitle": "Это поможет нам персонализировать ваше рабочее пространство.", "onboarding.form.adminInfoForm.title": "Информация об администраторе", "onboarding.form.adminInfoForm.subtitle": "Это необходимо для создания профиля администратора в вашем рабочем пространстве", "onboarding.form.adminInfoForm.fields.fullName.label": "Полное имя", @@ -5071,10 +5065,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Интеграция с внешними поставщиками (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Доступ к приложениям магазина", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "Адрес электронной почты учетной записи в облаке", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "Чтобы зарегистрировать сервер, необходимо подключить его к учетной записи облака. Если у вас уже есть такая учетная запись, мы свяжем ее автоматически. В противном случае будет создана новая учетная запись", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Введите адрес электронной почты", "onboarding.form.registeredServerForm.keepInformed": "Сообщайте мне новости и информацию о событиях", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Регистрируясь, я соглашаюсь получать соответствующие обновления продуктов и системы безопасности", "onboarding.form.standaloneServerForm.title": "Подтверждение автономного сервера", "onboarding.form.standaloneServerForm.servicesUnavailable": "Некоторые сервисы будут недоступны или потребуется ручная настройка", "onboarding.form.standaloneServerForm.publishOwnApp": "Чтобы отправлять push-уведомления, необходимо создать и опубликовать собственное приложение в Google Play и App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json index bea75b85c976..5cf724dd49c1 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/sv.i18n.json @@ -4922,10 +4922,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "Med den här inställningen förhindras alla instanser att skicka ändringar av användarnas status till deras klienter, vilket gör att alla användare behåller sin närvarostatus från den första inläsningen.", "Troubleshoot_Disable_Sessions_Monitor": "Inaktivera sessionsövervakning", "Troubleshoot_Disable_Sessions_Monitor_Alert": "Med den här inställningen stoppas bearbetningen av användarsessioner, vilket gör att statistikfunktionen slutar fungera som den ska.", - "Troubleshoot_Disable_Statistics_Generator": "Inaktivera generering av statistik", - "Troubleshoot_Disable_Statistics_Generator_Alert": "Med den här inställningen stoppas bearbetningen av all statistik. Det gör att informationssidan blir inaktuell tills någon klickar på uppdateringsknappen och kan leda till att annan information saknas i systemet.", - "Troubleshoot_Disable_Workspace_Sync": "Inaktivera synkronisering av arbetsyta", - "Troubleshoot_Disable_Workspace_Sync_Alert": "Den här inställningen stoppar serverns synkronisering med Rocket.Chat-molnet och kan orsaka problem med Marketplace och Enterprise-licenser.", "True": "Sant", "Try_now": "Pröva nu", "Try_searching_in_the_marketplace_instead": "Pröva att söka i Marketplace istället", @@ -5608,8 +5604,6 @@ "onboarding.page.requestTrial.subtitle": "Prova på vårt bästa Enterprise Edition-abonnemang i 30 dagar utan kostnad", "onboarding.page.magicLinkEmail.title": "Vi har skickat en inloggningslänk via e-post ", "onboarding.page.magicLinkEmail.subtitle": "Logga in på arbetsytan genom att klicka på länken i e-postmeddelandet vi precis skickade till dig. <1>Länken upphör att gälla om 30 minuter.</1>", - "onboarding.page.organizationInfoPage.title": "Några uppgifter till...", - "onboarding.page.organizationInfoPage.subtitle": "Vi behöver dem för att anpassa arbetsytan.", "onboarding.form.adminInfoForm.title": "Information om administratör", "onboarding.form.adminInfoForm.subtitle": "Vi behöver skapa en administratörsprofil i din arbetsyta", "onboarding.form.adminInfoForm.fields.fullName.label": "Fullständigt namn", @@ -5638,12 +5632,10 @@ "onboarding.form.registeredServerForm.included.externalProviders": "Integrering med externa leverantörer (WhatsApp, Facebook, Telegram, Twitter)", "onboarding.form.registeredServerForm.included.apps": "Åtkomst till appar i Marketplace", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "E-postadress för molnkonto", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "För att registrera servern behöver vi ansluta den till ditt molnkonto. Om du har ett kopplar vi det automatiskt. Annars skapas ett nytt konto", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "Ange din e-postadress", "onboarding.form.registeredServerForm.keepInformed": "Håll mig informerad om nyheter och händelser", "onboarding.form.registeredServerForm.registerLater": "Registrera dig senare", "onboarding.form.registeredServerForm.notConnectedToInternet": "Servern är inte ansluten till internet, så du måste göra en offline-registrering för den här arbetsytan.", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "Genom att registrera mig godkänner jag att ta emot relevanta produkt- och säkerhetsuppdateringar", "onboarding.form.standaloneServerForm.title": "Bekräftelse av fristående server", "onboarding.form.standaloneServerForm.servicesUnavailable": "Vissa av tjänsterna kommer att vara otillgängliga eller måste ställas in manuellt", "onboarding.form.standaloneServerForm.publishOwnApp": "Om du ska kunna skicka pushmeddelanden måste du kompilera och publicera din egen app på Google Play och App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json index 5ad5c0eb1d2b..24dedfcf33e7 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh-TW.i18n.json @@ -4068,10 +4068,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "這個設定可防止所有實務將使用者的狀態更改發送到其客戶端,從而使所有使用者保持首次載入的狀態!", "Troubleshoot_Disable_Sessions_Monitor": "停用 Sessions 監視器", "Troubleshoot_Disable_Sessions_Monitor_Alert": "這個設定將停止處理使用者 sessions,將會導致統計資料無法正常工作!", - "Troubleshoot_Disable_Statistics_Generator": "停用統計資料產生器", - "Troubleshoot_Disable_Statistics_Generator_Alert": "這個設定將停止處理所有統計資料,將會使資料頁面過時,直到有人點擊“重整”按鈕,並可能導致系統周圍缺少其他資料!", - "Troubleshoot_Disable_Workspace_Sync": "停用工作區同步", - "Troubleshoot_Disable_Workspace_Sync_Alert": "這個設定將停止該伺服器與 Rocket.Chat 的雲端同步,並可能導致商店和企業授權出現問題!", "True": "是", "Try_now": "現在再試", "Tuesday": "星期二", @@ -4553,8 +4549,6 @@ "onboarding.page.requestTrial.subtitle": "試用我們最棒的企業版方案,30 天免費", "onboarding.page.magicLinkEmail.title": "我們已透過電子郵件傳送登入連結給您", "onboarding.page.magicLinkEmail.subtitle": "按一下我們剛傳送給您的電子郵件中的連結,即可登入您的工作空間。<1>該連結將在 30 分鐘後到期。</1>", - "onboarding.page.organizationInfoPage.title": "更多詳細資料...", - "onboarding.page.organizationInfoPage.subtitle": "這些資料將可協助我們個人化您的工作空間。", "onboarding.form.adminInfoForm.title": "管理員資訊", "onboarding.form.adminInfoForm.subtitle": "我們需要此資訊以在您的工作空間內建立管理員個人資料", "onboarding.form.adminInfoForm.fields.fullName.label": "全名", @@ -4583,10 +4577,8 @@ "onboarding.form.registeredServerForm.included.externalProviders": "與外部提供者 (WhatsApp、Facebook、Telegram、Twitter) 整合", "onboarding.form.registeredServerForm.included.apps": "存取市集應用程式", "onboarding.form.registeredServerForm.fields.accountEmail.inputLabel": "雲端帳戶電子郵件", - "onboarding.form.registeredServerForm.fields.accountEmail.tooltipLabel": "若要註冊您的伺服器,我們需要將伺服器連線至您的雲端帳戶。如果您已有雲端帳戶,我們將會自動為您連線。否則,將需要建立新的帳戶", "onboarding.form.registeredServerForm.fields.accountEmail.inputPlaceholder": "請輸入您的電子郵件", "onboarding.form.registeredServerForm.keepInformed": "在有新聞與活動消息時通知我", - "onboarding.form.registeredServerForm.agreeToReceiveUpdates": "註冊即代表我同意收到相關產品與安全性更新資訊", "onboarding.form.standaloneServerForm.title": "獨立伺服器確認", "onboarding.form.standaloneServerForm.servicesUnavailable": "部分服務將無法使用或是需要手動設定", "onboarding.form.standaloneServerForm.publishOwnApp": "若要傳送推播通知,您必須對您所擁有的應用程式進行編碼,並將應用程式發佈至 Google Play 和 App Store", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json index df4642f4b0df..283944175541 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/zh.i18n.json @@ -3717,10 +3717,6 @@ "Troubleshoot_Disable_Presence_Broadcast_Alert": "这个设置可以防止所有的实例将用户的状态变化发送给他们的客户端,使所有的用户保持他们第一次加载时的存在状态。", "Troubleshoot_Disable_Sessions_Monitor": "禁用会话监控", "Troubleshoot_Disable_Sessions_Monitor_Alert": "这个设置停止了对用户会话的处理,将导致统计工作无法正常进行!", - "Troubleshoot_Disable_Statistics_Generator": "禁用统计生成器", - "Troubleshoot_Disable_Statistics_Generator_Alert": "这个设置会停止处理所有的统计数据,使信息页面过时,直到有人点击刷新按钮,并可能导致系统中的其他信息缺失!", - "Troubleshoot_Disable_Workspace_Sync": "禁用工作区同步", - "Troubleshoot_Disable_Workspace_Sync_Alert": "该设置会停止同步该服务器到 Rocket.Chat 云端,并可能导致市场和企业许可证出现问题!", "True": "是", "Try_now": "立即尝试", "Tuesday": "星期二", diff --git a/apps/meteor/server/cron/statistics.ts b/apps/meteor/server/cron/statistics.ts index 27c1fc064e25..7e7dea6adbc7 100644 --- a/apps/meteor/server/cron/statistics.ts +++ b/apps/meteor/server/cron/statistics.ts @@ -37,5 +37,7 @@ export async function statsCron(logger: Logger): Promise<void> { const now = new Date(); - await cronJobs.add(name, `12 ${now.getHours()} * * *`, async () => generateStatistics(logger)); + await cronJobs.add(name, `12 ${now.getHours()} * * *`, async () => { + await generateStatistics(logger); + }); } diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts index 61ca75aa65d4..685c7f9e96dd 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts @@ -43,15 +43,15 @@ const sendErrorReplyMessage = async (error: string, options: any) => { return sendMessage(user, message, { _id: options.rid }); }; -const sendSuccessReplyMessage = async (options: any) => { - if (!options?.rid || !options?.msgId) { +const sendSuccessReplyMessage = async (options: { room: IOmnichannelRoom; msgId: string; sender: string }) => { + if (!options?.room?._id || !options?.msgId) { return; } const message = { groupable: false, msg: `@${options.sender} Attachment was sent successfully`, _id: String(Date.now()), - rid: options.rid, + rid: options.room._id, ts: new Date(), }; @@ -60,7 +60,7 @@ const sendSuccessReplyMessage = async (options: any) => { return; } - return sendMessage(user, message, { _id: options.rid }); + return sendMessage(user, message, options.room); }; async function sendEmail(inbox: Inbox, mail: Mail.Options, options?: any): Promise<{ messageId: string }> { @@ -174,7 +174,7 @@ slashCommands.add({ return sendSuccessReplyMessage({ msgId: message._id, sender: message.u.username, - rid: room._id, + room, }); }, options: { diff --git a/apps/meteor/server/lib/dataExport/sendFile.ts b/apps/meteor/server/lib/dataExport/sendFile.ts index c4f75bd48bff..54c691127d9a 100644 --- a/apps/meteor/server/lib/dataExport/sendFile.ts +++ b/apps/meteor/server/lib/dataExport/sendFile.ts @@ -64,9 +64,11 @@ export const sendFile = async (data: ExportFile, user: IUser): Promise<void> => await exportMessages(); + const promises: Promise<void>[] = []; for await (const attachmentData of fullFileList) { - await copyFileUpload(attachmentData, assetsPath); + promises.push(copyFileUpload(attachmentData, assetsPath)); } + await Promise.all(promises); const exportFile = `${baseDir}-export.zip`; await makeZipFile(exportPath, exportFile); diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 673fd83f7800..8b8a6637284c 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -678,6 +678,16 @@ export class RoomsRaw extends BaseRaw<IRoom> implements IRoomsModel { ); } + findE2ERoomById(roomId: IRoom['_id'], options: FindOptions<IRoom> = {}): Promise<IRoom | null> { + return this.findOne( + { + _id: roomId, + encrypted: true, + }, + options, + ); + } + findRoomsInsideTeams(autoJoin = false): FindCursor<IRoom> { return this.find({ teamId: { $exists: true }, diff --git a/apps/meteor/server/models/raw/Statistics.ts b/apps/meteor/server/models/raw/Statistics.ts index 1ad0ab993910..bad44ee07c23 100644 --- a/apps/meteor/server/models/raw/Statistics.ts +++ b/apps/meteor/server/models/raw/Statistics.ts @@ -25,4 +25,25 @@ export class StatisticsRaw extends BaseRaw<IStats> implements IStatisticsModel { ).toArray(); return records?.[0]; } + + async findMonthlyPeakConnections() { + const oneMonthAgo = new Date(); + oneMonthAgo.setDate(oneMonthAgo.getDate() - 30); + oneMonthAgo.setHours(0, 0, 0, 0); + + return this.findOne<Pick<IStats, 'dailyPeakConnections' | 'createdAt'>>( + { + createdAt: { $gte: oneMonthAgo }, + }, + { + sort: { + dailyPeakConnections: -1, + }, + projection: { + dailyPeakConnections: 1, + createdAt: 1, + }, + }, + ); + } } diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index c4ba44bdd7f9..a7b9bb347511 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -604,6 +604,14 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri return this.updateOne(query, update); } + disableAutoTranslateByRoomId(roomId: IRoom['_id']): Promise<UpdateResult | Document> { + const query = { + rid: roomId, + }; + + return this.updateMany(query, { $unset: { autoTranslate: 1 } }); + } + updateAutoTranslateLanguageById(_id: string, autoTranslateLanguage: string): Promise<UpdateResult> { const query = { _id, diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 113f18ea83da..c8cee8f2f6bf 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -2187,7 +2187,6 @@ export class UsersRaw extends BaseRaw { { active: true, type: { $nin: ['app'] }, - roles: { $ne: ['guest'] }, _id: { $in: ids }, }, options, diff --git a/apps/meteor/server/services/omnichannel/service.ts b/apps/meteor/server/services/omnichannel/service.ts index 7f35de104e1c..61c22505ca98 100644 --- a/apps/meteor/server/services/omnichannel/service.ts +++ b/apps/meteor/server/services/omnichannel/service.ts @@ -2,7 +2,7 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; import type { IOmnichannelService } from '@rocket.chat/core-services'; import type { IOmnichannelQueue } from '@rocket.chat/core-typings'; -import { Livechat } from '../../../app/livechat/server'; +import { Livechat } from '../../../app/livechat/server/lib/LivechatTyped'; import { RoutingManager } from '../../../app/livechat/server/lib/RoutingManager'; import { settings } from '../../../app/settings/server'; import { OmnichannelQueue } from './queue'; diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index 77cdc1cbd8e0..c9079b0a2bfb 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -618,6 +618,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf caller: call.createdBy, avatar: getUserAvatarURL(call.createdBy.username), status: call.status, + callId: call._id, }, userId: calleeId, notId: PushNotification.getNotificationId(`${call.rid}|${call._id}`), diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 0c49ea7c7bbf..7cc04efba3a0 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -154,8 +154,7 @@ test.describe.serial('e2e-encryption', () => { await expect(page).toHaveURL(`/group/${channelName}`); - await poHomeChannel.toastSuccess.locator('button >> i.rcx-icon--name-cross.rcx-icon').click(); - await page.mouse.move(0, 0); + await poHomeChannel.dismissToast(); await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); @@ -168,6 +167,7 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.tabs.btnDisableE2E).toBeVisible(); await poHomeChannel.tabs.btnDisableE2E.click({ force: true }); + await poHomeChannel.dismissToast(); await page.waitForTimeout(1000); await poHomeChannel.content.sendMessage('hello world not encrypted'); @@ -178,6 +178,7 @@ test.describe.serial('e2e-encryption', () => { await poHomeChannel.tabs.kebab.click({ force: true }); await expect(poHomeChannel.tabs.btnEnableE2E).toBeVisible(); await poHomeChannel.tabs.btnEnableE2E.click({ force: true }); + await poHomeChannel.dismissToast(); await page.waitForTimeout(1000); await poHomeChannel.content.sendMessage('hello world encrypted again'); @@ -197,7 +198,7 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.toastSuccess).toBeVisible(); - await poHomeChannel.toastSuccess.locator('button >> i.rcx-icon--name-cross.rcx-icon').click(); + await poHomeChannel.dismissToast(); await poHomeChannel.tabs.kebab.click({ force: true }); await expect(poHomeChannel.tabs.btnEnableE2E).toBeVisible(); diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index d2f43c5cfec5..24403b22b845 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -34,4 +34,10 @@ export class HomeChannel { await expect(this.page.locator('role=main >> .rcx-skeleton')).toHaveCount(0); await expect(this.page.locator('role=main >> role=list')).not.toHaveAttribute('aria-busy', 'true'); } + + async dismissToast() { + // this is a workaround for when the toast is blocking the click of the button + await this.toastSuccess.locator('button >> i.rcx-icon--name-cross.rcx-icon').click(); + await this.page.mouse.move(0, 0); + } } diff --git a/apps/meteor/tests/end-to-end/api/00-autotranslate.js b/apps/meteor/tests/end-to-end/api/00-autotranslate.js index 48bb021ce388..7695718bd01f 100644 --- a/apps/meteor/tests/end-to-end/api/00-autotranslate.js +++ b/apps/meteor/tests/end-to-end/api/00-autotranslate.js @@ -4,7 +4,7 @@ import { before, describe, after, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; import { sendSimpleMessage } from '../../data/chat.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; -import { createRoom } from '../../data/rooms.helper'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { password } from '../../data/user'; import { createUser, login } from '../../data/users.helper.js'; @@ -71,6 +71,18 @@ describe('AutoTranslate', function () { }); }); describe('[/autotranslate.saveSettings', () => { + let testGroupId; + before(async () => { + await updateSetting('E2E_Enable', true); + await updateSetting('E2E_Enabled_Default_PrivateRooms', true); + const res = await createRoom({ type: 'p', name: `e2etest-autotranslate-${Date.now()}` }); + testGroupId = res.body.group._id; + }); + after(async () => { + await updateSetting('E2E_Enabled_Default_PrivateRooms', false); + await updateSetting('E2E_Enable', false); + await deleteRoom({ type: 'p', roomId: testGroupId }); + }); it('should throw an error when the "AutoTranslate_Enabled" setting is disabled', (done) => { updateSetting('AutoTranslate_Enabled', false).then(() => { request @@ -223,6 +235,23 @@ describe('AutoTranslate', function () { }) .end(done); }); + it('should throw an error when E2E encryption is enabled', async () => { + await request + .post(api('autotranslate.saveSettings')) + .set(credentials) + .send({ + roomId: testGroupId, + field: 'autoTranslate', + defaultLanguage: 'en', + value: true, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-e2e-enabled'); + }); + }); it('should return success when the setting is saved correctly', (done) => { request .post(api('autotranslate.saveSettings')) diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index e875be80fd3b..3941df1366eb 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -481,21 +481,181 @@ describe('[Groups]', function () { }); describe('/groups.kick', () => { - it('should remove user from group', (done) => { - request + let testUserModerator; + let credsModerator; + let testUserOwner; + let credsOwner; + let testUserMember; + let groupTest; + + const inviteUser = async (userId) => { + await request + .post(api('groups.invite')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }; + + before(async () => { + // had to do them in serie because calling them with Promise.all was failing some times + testUserModerator = await createUser(); + testUserOwner = await createUser(); + testUserMember = await createUser(); + + credsModerator = await login(testUserModerator.username, password); + credsOwner = await login(testUserOwner.username, password); + + await request + .post(api('groups.create')) + .set(credsOwner) + .send({ + name: `kick-test-group-${Date.now()}`, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + groupTest = res.body.group; + }); + + await inviteUser(testUserModerator._id); + + await request + .post(api('groups.addModerator')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId: testUserModerator._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + after(async () => { + await Promise.all([ + request + .post(api('groups.delete')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + }) + .expect('Content-Type', 'application/json') + .expect(200), + // updatePermission('kick-user-from-any-p-room', []), + updatePermission('remove-user', ['admin', 'owner', 'moderator']), + deleteUser(testUserModerator), + deleteUser(testUserOwner), + deleteUser(testUserMember), + ]); + }); + + it("should return an error when user is not a member of the group and doesn't have permission", async () => { + await request + .post(api('groups.kick')) + .set(credentials) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-room-not-found'); + }); + }); + + it('should allow a moderator to remove user from group', async () => { + await inviteUser(testUserMember._id); + + await request + .post(api('groups.kick')) + .set(credsModerator) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + it('should allow an owner to remove user from group', async () => { + await inviteUser(testUserMember._id); + + await request + .post(api('groups.kick')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + it.skip('should kick user from group if not a member of the room but has the required permission', async () => { + await updatePermission('kick-user-from-any-p-room', ['admin']); + await inviteUser(testUserMember._id); + + await request .post(api('groups.kick')) .set(credentials) .send({ roomId: group._id, - userId: 'rocket.cat', + userId: testUserMember._id, }) .expect('Content-Type', 'application/json') - .expect(200) + .expect(200); + }); + + it("should return an error when the owner doesn't have the required permission", async () => { + await updatePermission('remove-user', ['admin', 'moderator']); + await inviteUser(testUserMember._id); + + await request + .post(api('groups.kick')) + .set(credsOwner) + .send({ + roomId: groupTest._id, + userId: testUserMember._id, + }) + .expect('Content-Type', 'application/json') + + .expect(400) .expect((res) => { - expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-not-allowed'); + }); + }); + + it('should return an error when trying to kick the last owner from a group', async () => { + await updatePermission('kick-user-from-any-p-room', ['admin']); + + await request + .post(api('groups.kick')) + .set(credentials) + .send({ + roomId: groupTest._id, + userId: testUserOwner._id, }) - .end(done); + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-you-are-last-owner'); + }); }); + + it('should return an error when trying to kick user that does not exist'); + it('should return an error when trying to kick user from a group that does not exist'); + it('should return an error when trying to kick user from a group that the user is not in the room'); }); describe('/groups.setDescription', () => { diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index 5a534fe2674d..ed3c7eefb15b 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -4,6 +4,7 @@ import path from 'path'; import { expect } from 'chai'; import { after, afterEach, before, beforeEach, describe, it } from 'mocha'; +import { sleep } from '../../../lib/utils/sleep'; import { getCredentials, api, request, credentials } from '../../data/api-data.js'; import { sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; import { imgURL } from '../../data/interactions.js'; @@ -1543,29 +1544,30 @@ describe('[Rooms]', function () { roomId = result.body.room.rid; }); - it('should update group name if user changes username', (done) => { - updateSetting('UI_Use_Real_Name', false).then(() => { - request - .post(api('users.update')) - .set(credentials) - .send({ - userId: testUser._id, - data: { - username: `changed.username.${testUser.username}`, - }, - }) - .end(() => { - request - .get(api('subscriptions.getOne')) - .set(credentials) - .query({ roomId }) - .end((err, res) => { - const { subscription } = res.body; - expect(subscription.name).to.equal(`rocket.cat,changed.username.${testUser.username}`); - done(); - }); - }); - }); + it('should update group name if user changes username', async () => { + await updateSetting('UI_Use_Real_Name', false); + await request + .post(api('users.update')) + .set(credentials) + .send({ + userId: testUser._id, + data: { + username: `changed.username.${testUser.username}`, + }, + }); + + // need to wait for the username update finish + await sleep(300); + + await request + .get(api('subscriptions.getOne')) + .set(credentials) + .query({ roomId }) + .send() + .expect((res) => { + const { subscription } = res.body; + expect(subscription.name).to.equal(`rocket.cat,changed.username.${testUser.username}`); + }); }); it('should update group name if user changes name', (done) => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/22-monitors.ts b/apps/meteor/tests/end-to-end/api/livechat/22-monitors.ts new file mode 100644 index 000000000000..d4c2fd59211c --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/livechat/22-monitors.ts @@ -0,0 +1,201 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { ILivechatDepartment, IUser } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { before, it, describe } from 'mocha'; + +import { getCredentials, api, request } from '../../../data/api-data'; +import { addOrRemoveAgentFromDepartment, createDepartment } from '../../../data/livechat/department'; +import { + createAgent, + createLivechatRoom, + createManager, + createVisitor, + getLivechatRoomInfo, + makeAgentAvailable, +} from '../../../data/livechat/rooms'; +import { createMonitor, createUnit } from '../../../data/livechat/units'; +import { updateSetting, updatePermission } from '../../../data/permissions.helper'; +import { password } from '../../../data/user'; +import { createUser, login, setUserActiveStatus } from '../../../data/users.helper'; +import { IS_EE } from '../../../e2e/config/constants'; + +type TestUser = { user: IUser; credentials: { 'X-Auth-Token': string; 'X-User-Id': string } }; + +(IS_EE ? describe : describe.skip)('Omnichannel - Monitors', () => { + let manager: TestUser; + let monitor: TestUser; + let noUnitDepartment: ILivechatDepartment; + let unitDepartment: ILivechatDepartment; + + before((done) => getCredentials(done)); + before(async () => { + await updateSetting('Livechat_accept_chats_with_no_agents', true); + await setUserActiveStatus('rocketchat.internal.admin.test', true); + await createAgent(); + await makeAgentAvailable(); + }); + before(async () => { + const user: IUser = await createUser(); + const userCredentials = await login(user.username, password); + if (!user.username) { + throw new Error('user not created'); + } + await createManager(user.username); + + manager = { + user, + credentials: userCredentials, + }; + }); + before(async () => { + const user: IUser = await createUser(); + const userCredentials = await login(user.username, password); + if (!user.username) { + throw new Error('user not created'); + } + await createMonitor(user.username); + + monitor = { + user, + credentials: userCredentials, + }; + }); + before(async () => { + noUnitDepartment = await createDepartment(); + unitDepartment = await createDepartment(); + + await createUnit(monitor.user._id, monitor.user.username!, [unitDepartment._id]); + }); + before(async () => { + await updatePermission('transfer-livechat-guest', ['admin', 'livechat-manager', 'livechat-agent', 'livechat-monitor']); + }); + + describe('Monitors & Rooms', () => { + it('should not return a room of a department that the monitor is not assigned to', async () => { + const visitor = await createVisitor(noUnitDepartment._id); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request + .get(api('livechat/rooms')) + .set(monitor.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('rooms').that.is.an('array'); + expect(body.rooms.find((r: any) => r._id === room._id)).to.not.exist; + }); + it('should return a room of a department the monitor is assigned to', async () => { + const visitor = await createVisitor(unitDepartment._id); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request + .get(api('livechat/rooms')) + .set(monitor.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('rooms').that.is.an('array'); + expect(body.rooms.find((r: any) => r._id === room._id)).to.exist; + }); + }); + + describe('Monitors & Departments', () => { + it('should not return a department that the monitor is not assigned to', async () => { + const { body } = await request + .get(api('livechat/department')) + .query({ onlyMyDepartments: true }) + .set(monitor.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('departments').that.is.an('array'); + expect(body.departments.find((d: any) => d._id === noUnitDepartment._id)).to.not.exist; + }); + it('should return a department that the monitor is assigned to', async () => { + const { body } = await request + .get(api('livechat/department')) + .query({ onlyMyDepartments: true }) + .set(monitor.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('departments').that.is.an('array'); + expect(body.departments.length).to.be.equal(1); + expect(body.departments.find((d: any) => d._id === unitDepartment._id)).to.exist; + }); + it('should return both created departments to a manager', async () => { + const { body } = await request + .get(api('livechat/department')) + .query({ onlyMyDepartments: true, sort: '{ "_updatedAt": 1 }' }) + .set(manager.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('departments').that.is.an('array'); + expect(body.departments.find((d: any) => d._id === noUnitDepartment._id)).to.exist; + expect(body.departments.find((d: any) => d._id === unitDepartment._id)).to.exist; + }); + it('should not return a department when monitor is only assigned as agent there', async () => { + await createAgent(monitor.user.username!); + await addOrRemoveAgentFromDepartment( + noUnitDepartment._id, + { agentId: monitor.user._id, username: monitor.user.username!, count: 0, order: 0 }, + true, + ); + + const { body } = await request + .get(api('livechat/department')) + .query({ onlyMyDepartments: true }) + .set(monitor.credentials) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('departments').that.is.an('array'); + expect(body.departments.length).to.be.equal(1); + expect(body.departments.find((d: any) => d._id === noUnitDepartment._id)).to.not.exist; + }); + }); + + describe('Monitors & Forward', () => { + it('should successfully forward a room to another agent', async () => { + const visitor = await createVisitor(unitDepartment._id); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request + .post(api('livechat/room.forward')) + .set(monitor.credentials) + .send({ + roomId: room._id, + userId: 'rocketchat.internal.admin.test', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('success', true); + + const room2 = await getLivechatRoomInfo(room._id); + + expect(room2).to.have.property('servedBy').that.is.an('object'); + expect(room2.servedBy).to.have.property('_id', 'rocketchat.internal.admin.test'); + }); + it('should successfully forward a room to a department', async () => { + const visitor = await createVisitor(noUnitDepartment._id); + const room = await createLivechatRoom(visitor.token); + + const { body } = await request + .post(api('livechat/room.forward')) + .set(monitor.credentials) + .send({ + roomId: room._id, + departmentId: unitDepartment._id, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(body).to.have.property('success', true); + + const room2 = await getLivechatRoomInfo(room._id); + expect(room2.departmentId).to.be.equal(unitDepartment._id); + }); + }); +}); diff --git a/apps/meteor/tests/unit/server/users/saveUserIdentity.spec.ts b/apps/meteor/tests/unit/server/users/saveUserIdentity.spec.ts new file mode 100644 index 000000000000..b91165fb3ca9 --- /dev/null +++ b/apps/meteor/tests/unit/server/users/saveUserIdentity.spec.ts @@ -0,0 +1,134 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +// Create stubs for dependencies +const stubs = { + findOneUserById: sinon.stub(), + updateUsernameAndMessageOfMentionByIdAndOldUsername: sinon.stub(), + updateUsernameOfEditByUserId: sinon.stub(), + updateAllUsernamesByUserId: sinon.stub(), + updateDirectNameAndFnameByName: sinon.stub(), + updateUserReferences: sinon.stub(), + setUsername: sinon.stub(), + setRealName: sinon.stub(), + validateName: sinon.stub(), + FileUpload: sinon.stub(), +}; + +const { saveUserIdentity } = proxyquire.noCallThru().load('../../../../app/lib/server/functions/saveUserIdentity', { + '@rocket.chat/models': { + Users: { + findOneById: stubs.findOneUserById, + }, + Messages: { + updateUsernameAndMessageOfMentionByIdAndOldUsername: stubs.updateUsernameAndMessageOfMentionByIdAndOldUsername, + updateUsernameOfEditByUserId: stubs.updateUsernameOfEditByUserId, + updateAllUsernamesByUserId: stubs.updateAllUsernamesByUserId, + }, + Subscriptions: { + updateDirectNameAndFnameByName: stubs.updateDirectNameAndFnameByName, + }, + VideoConference: { + updateUserReferences: stubs.updateUserReferences, + }, + }, + 'meteor/meteor': { + 'Meteor': sinon.stub(), + '@global': true, + }, + '../../../../app/file-upload/server': { + FileUpload: stubs.FileUpload, + }, + '../../../../app/lib/server/functions/setRealName': { + _setRealName: stubs.setRealName, + }, + '../../../../app/lib/server/functions/setUsername': { + _setUsername: stubs.setUsername, + }, + '../../../../app/lib/server/functions/updateGroupDMsName': { + updateGroupDMsName: sinon.stub(), + }, + '../../../../app/lib/server/functions/validateName': { + validateName: stubs.validateName, + }, +}); + +describe('Users - saveUserIdentity', () => { + beforeEach(() => { + // Reset stubs before each test + Object.values(stubs).forEach((stub) => stub.reset()); + }); + + it('should return false if _id is not provided', async () => { + const result = await saveUserIdentity({ _id: undefined }); + + expect(stubs.findOneUserById.called).to.be.false; + expect(result).to.be.false; + }); + + it('should return false if user does not exist', async () => { + stubs.findOneUserById.returns(undefined); + const result = await saveUserIdentity({ _id: 'valid_id' }); + + expect(stubs.findOneUserById.calledWith('valid_id')).to.be.true; + expect(result).to.be.false; + }); + + it('should return false if username is not allowed', async () => { + stubs.findOneUserById.returns({ username: 'oldUsername' }); + stubs.validateName.returns(false); + const result = await saveUserIdentity({ _id: 'valid_id', username: 'admin' }); + + expect(stubs.validateName.calledWith('admin')).to.be.true; + expect(result).to.be.false; + }); + + it('should return false if username is invalid or unavailable', async () => { + stubs.findOneUserById.returns({ username: 'oldUsername' }); + stubs.validateName.returns(true); + stubs.setUsername.returns(false); + const result = await saveUserIdentity({ _id: 'valid_id', username: 'invalidUsername' }); + + expect(stubs.validateName.calledWith('invalidUsername')).to.be.true; + expect(stubs.setUsername.calledWith('valid_id', 'invalidUsername', { username: 'oldUsername' })).to.be.true; + expect(result).to.be.false; + }); + + it("should not update the username if it's not changed", async () => { + stubs.findOneUserById.returns({ username: 'oldUsername', name: 'oldName' }); + stubs.validateName.returns(true); + stubs.setUsername.returns(true); + await saveUserIdentity({ _id: 'valid_id', username: 'oldUsername', name: 'oldName' }); + + expect(stubs.validateName.called).to.be.false; + expect(stubs.setUsername.called).to.be.false; + expect(stubs.updateUsernameOfEditByUserId.called).to.be.false; + expect(stubs.updateAllUsernamesByUserId.called).to.be.false; + expect(stubs.updateUsernameAndMessageOfMentionByIdAndOldUsername.called).to.be.false; + expect(stubs.updateDirectNameAndFnameByName.called).to.be.false; + expect(stubs.updateUserReferences.called).to.be.false; + }); + + it('should return false if _setName fails', async () => { + stubs.findOneUserById.returns({ name: 'oldName' }); + stubs.setRealName.returns(false); + const result = await saveUserIdentity({ _id: 'valid_id', name: 'invalidName' }); + + expect(stubs.setRealName.calledWith('valid_id', 'invalidName', { name: 'oldName' })).to.be.true; + expect(result).to.be.false; + }); + + it('should update Subscriptions and VideoConference if name changes', async () => { + stubs.findOneUserById.returns({ name: 'oldName', username: 'oldUsername' }); + stubs.setRealName.returns(true); + const result = await saveUserIdentity({ _id: 'valid_id', name: 'name', username: 'oldUsername' }); + + expect(stubs.setUsername.called).to.be.false; + expect(stubs.setRealName.called).to.be.true; + expect(stubs.updateUsernameOfEditByUserId.called).to.be.false; + expect(stubs.updateDirectNameAndFnameByName.called).to.be.true; + expect(stubs.updateUserReferences.called).to.be.true; + expect(result).to.be.true; + }); +}); diff --git a/ee/packages/presence/src/Presence.ts b/ee/packages/presence/src/Presence.ts index fb656fc3e158..5bd69e1f4fc8 100755 --- a/ee/packages/presence/src/Presence.ts +++ b/ee/packages/presence/src/Presence.ts @@ -19,6 +19,8 @@ export class Presence extends ServiceClass implements IPresence { private connsPerInstance = new Map<string, number>(); + private peakConnections = 0; + constructor() { super(); @@ -35,6 +37,7 @@ export class Presence extends ServiceClass implements IPresence { if (diff?.hasOwnProperty('extraInformation.conns')) { this.connsPerInstance.set(id, diff['extraInformation.conns']); + this.peakConnections = Math.max(this.peakConnections, this.getTotalConnections()); this.validateAvailability(); } }); @@ -251,4 +254,16 @@ export class Presence extends ServiceClass implements IPresence { private getTotalConnections(): number { return Array.from(this.connsPerInstance.values()).reduce((acc, conns) => acc + conns, 0); } + + getPeakConnections(reset = false): number { + const peak = this.peakConnections; + if (reset) { + this.resetPeakConnections(); + } + return peak; + } + + resetPeakConnections(): void { + this.peakConnections = 0; + } } diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index 9508e2b8e41e..11aa5fd57ff8 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": "next", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/icons": "^0.32.0", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/ee/packages/ui-theming/src/palette.ts b/ee/packages/ui-theming/src/palette.ts index 39f8d3f2bfed..5d207d63bc21 100644 --- a/ee/packages/ui-theming/src/palette.ts +++ b/ee/packages/ui-theming/src/palette.ts @@ -46,8 +46,8 @@ export const palette = [ { name: 'font-white', token: 'white', color: '#FFFFFF' }, { name: 'font-disabled', token: 'N500', color: '#CBCED1' }, { name: 'font-annotation', token: 'N600', color: '#9EA2A8' }, - { name: 'font-hint', token: 'N700', color: '#6C727A' }, - { name: 'font-secondary-info', token: 'N700', color: '#6C727A' }, + { name: 'font-hint', token: 'N700', color: '#6C737A' }, + { name: 'font-secondary-info', token: 'N700', color: '#6C737A' }, { name: 'font-default', token: 'N800', color: '#2F343D' }, { name: 'font-titles-labels', token: 'N900', color: '#1F2329' }, { name: 'font-info', token: 'P600', color: '#095AD2' }, @@ -76,7 +76,7 @@ export const palette = [ { name: 'status-font-on-info', token: 'P600', color: '#095AD2' }, { name: 'status-font-on-success', token: 'S800', color: '#148660' }, { name: 'status-font-on-danger', token: 'D800', color: '#9B1325' }, - { name: 'status-font-on-warning', token: 'W900', color: '#B88D00' }, + { name: 'status-font-on-warning', token: 'W900', color: '#8E6300' }, { name: 'status-font-on-warning-2', token: 'N800', color: '#2F343D' }, { name: 'status-font-on-service-1', token: 'S1-800', color: '#974809' }, { name: 'status-font-on-service-2 ', token: 'S2-600', color: '#7F1B9F' }, @@ -88,22 +88,22 @@ export const palette = [ description: 'Badge Background', list: [ { name: 'badge-background-level-0', token: '', color: '#E4E7EA' }, - { name: 'badge-background-level-1', token: 'N700', color: '#6C727A' }, - { name: 'badge-background-level-2', token: '', color: '#1D74F5' }, + { name: 'badge-background-level-1', token: 'N700', color: '#6C737A' }, + { name: 'badge-background-level-2', token: '', color: '#156FF5' }, { name: 'badge-background-level-3', token: '', color: '#F38C39' }, - { name: 'badge-background-level-4', token: '', color: '#F5455C' }, + { name: 'badge-background-level-4', token: '', color: '#EC0D2A' }, ], }, { category: 'Status Bullet', description: 'Used to show user status', list: [ - { name: 'status-bullet-online', token: '', color: '#158D65' }, + { name: 'status-bullet-online', token: '', color: '#148660' }, { name: 'status-bullet-away', token: '', color: '#AC892F' }, - { name: 'status-bullet-busy', token: '', color: '#DA1F37' }, + { name: 'status-bullet-busy', token: '', color: '#D40C26' }, { name: 'status-bullet-disabled', token: '', color: '#F38C39' }, - { name: 'status-bullet-offline', token: '', color: '#AC892F' }, - { name: 'status-bullet-loading', token: '', color: '#9ea2a8' }, + { name: 'status-bullet-offline', token: '', color: '#6C737A' }, + { name: 'status-bullet-loading', token: '', color: '#6C737A' }, ], }, { @@ -122,7 +122,7 @@ export const palette = [ list: [ { name: 'button-background-primary-default', token: 'P500', color: '#156FF5' }, { name: 'button-background-primary-hover', token: 'P600', color: '#095AD2' }, - { name: 'button-background-primary-press', token: 'P700', color: '#095AD2' }, + { name: 'button-background-primary-press', token: 'P700', color: '#10529E' }, { name: 'button-background-primary-focus', token: 'P500', color: '#156FF5' }, { name: 'button-background-primary-keyfocus', token: 'P500', color: '#156FF5' }, { name: 'button-background-primary-disabled', token: 'P200', color: '#D1EBFE' }, @@ -133,7 +133,7 @@ export const palette = [ list: [ { name: 'button-background-secondary-default', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-hover', token: 'N500', color: '#CBCED1' }, - { name: 'button-background-secondary-press', token: 'N600', color: '#CBCED1' }, + { name: 'button-background-secondary-press', token: 'N600', color: '#9EA2A8' }, { name: 'button-background-secondary-focus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-keyfocus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-disabled', token: 'N300', color: '#EEEFF1' }, @@ -144,7 +144,7 @@ export const palette = [ list: [ { name: 'button-background-secondary-danger-default', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-danger-hover', token: 'N500', color: '#CBCED1' }, - { name: 'button-background-secondary-danger-press', token: 'N600', color: '#CBCED1' }, + { name: 'button-background-secondary-danger-press', token: 'N600', color: '#9EA2A8' }, { name: 'button-background-secondary-danger-focus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-danger-keyfocus', token: 'N400', color: '#E4E7EA' }, { name: 'button-background-secondary-danger-disabled', token: 'N300', color: '#EEEFF1' }, @@ -164,11 +164,11 @@ export const palette = [ { description: 'Success Background', list: [ - { name: 'button-background-success-default', token: '', color: '#158D65' }, + { name: 'button-background-success-default', token: '', color: '#148660' }, { name: 'button-background-success-hover', token: 'S900', color: '#106D4F' }, { name: 'button-background-success-press', token: 'S1000', color: '#0D5940' }, - { name: 'button-background-success-focus', token: '', color: '#158D65' }, - { name: 'button-background-success-keyfocus', token: '', color: '#158D65' }, + { name: 'button-background-success-focus', token: '', color: '#148660' }, + { name: 'button-background-success-keyfocus', token: '', color: '#148660' }, { name: 'button-background-success-disabled', token: 'S200', color: '#C0F6E4' }, ], }, @@ -179,7 +179,7 @@ export const palette = [ { name: 'button-font-on-primary-disabled', token: 'white', color: '#FFFFFF' }, { name: 'button-font-on-secondary', token: 'N900', color: '#1F2329' }, { name: 'button-font-on-secondary-disabled', token: 'N600', color: '#CBCED1' }, - { name: 'button-font-on-secondary-danger', token: 'D900', color: '#BB0B21' }, + { name: 'button-font-on-secondary-danger', token: '', color: '#BB0B21' }, { name: 'button-font-on-secondary-danger-disabled', token: 'D300', @@ -187,7 +187,7 @@ export const palette = [ }, { name: 'button-font-on-danger', token: 'white', color: '#FFFFFF' }, { name: 'button-font-on-danger-disabled', token: 'white', color: '#FFFFFF' }, - { name: 'button-font-on-success', token: '', color: '#EBECEF' }, + { name: 'button-font-on-success', token: '', color: '#FFFFFF' }, { name: 'button-font-on-success-disabled', token: 'white', color: '#FFFFFF' }, ], }, diff --git a/ee/packages/ui-theming/src/paletteDark.ts b/ee/packages/ui-theming/src/paletteDark.ts index cdb60efffeac..89ac7817be42 100644 --- a/ee/packages/ui-theming/src/paletteDark.ts +++ b/ee/packages/ui-theming/src/paletteDark.ts @@ -9,7 +9,7 @@ export const palette = [ { name: 'stroke-dark', token: 'N600', color: '#9EA2A8' }, { name: 'stroke-extra-dark', token: 'N400', color: '#CBCED1' }, { name: 'stroke-extra-light-highlight', token: '', color: '#87CBFC' }, - { name: 'stroke-highlight', token: '', color: '#3976D1' }, + { name: 'stroke-highlight', token: '', color: '#6292DA' }, { name: 'stroke-extra-light-error', token: '', color: '#F49AA6' }, { name: 'stroke-error', token: '', color: '#BB3E4E' }, ], diff --git a/packages/core-services/src/Events.ts b/packages/core-services/src/Events.ts index 88ca1034b9c9..e2a7f624d8df 100644 --- a/packages/core-services/src/Events.ts +++ b/packages/core-services/src/Events.ts @@ -32,6 +32,7 @@ import type { ILivechatInquiryRecord, ILivechatAgent, IBanner, + ILivechatVisitor, } from '@rocket.chat/core-typings'; import type { AutoUpdateRecord } from './types/IMeteor'; @@ -242,7 +243,8 @@ export type EventSignatures = { data: | { type: 'agentStatus'; status: string } | { type: 'queueData'; data: { [k: string]: unknown } | undefined } - | { type: 'agentData'; data: ILivechatAgent | undefined | { hiddenInfo: boolean } }, + | { type: 'agentData'; data: ILivechatAgent | undefined | { hiddenInfo: boolean } } + | { type: 'visitorData'; visitor: ILivechatVisitor }, ): void; // Send all events from here diff --git a/packages/core-services/src/types/IPresence.ts b/packages/core-services/src/types/IPresence.ts index 197f9b685cf8..5f7c57d67995 100644 --- a/packages/core-services/src/types/IPresence.ts +++ b/packages/core-services/src/types/IPresence.ts @@ -19,4 +19,6 @@ export interface IPresence extends IServiceClass { updateUserPresence(uid: string): Promise<void>; toggleBroadcast(enabled: boolean): void; getConnectionCount(): { current: number; max: number }; + getPeakConnections(reset?: boolean): number; + resetPeakConnections(): void; } diff --git a/packages/core-typings/src/ILivechatVisitor.ts b/packages/core-typings/src/ILivechatVisitor.ts index e80d63ab15d0..21819cc23f24 100644 --- a/packages/core-typings/src/ILivechatVisitor.ts +++ b/packages/core-typings/src/ILivechatVisitor.ts @@ -7,7 +7,7 @@ export interface IVisitorPhone { export interface IVisitorLastChat { _id: string; - ts: string; + ts: Date; } export interface ILivechatVisitorConnectionData { diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts index 443cbfb23957..7fd5cd8218bc 100644 --- a/packages/core-typings/src/IStats.ts +++ b/packages/core-typings/src/IStats.ts @@ -219,6 +219,8 @@ export interface IStats { totalWebRTCCalls: number; uncaughtExceptionsCount: number; push: number; + dailyPeakConnections: number; + maxMonthlyPeakConnections: number; matrixFederation: { enabled: boolean; }; diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 83ab677e2a9f..4555216c2d44 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -56,7 +56,7 @@ "devDependencies": { "@rocket.chat/apps-engine": "1.41.0-alpha.290", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/fuselage-polyfills": "next", "@rocket.chat/icons": "^0.32.0", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 89136a296f9d..e891e5677c75 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.9", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-tokens": "next", "@rocket.chat/message-parser": "next", "@rocket.chat/styled": "next", diff --git a/packages/model-typings/src/models/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts index a074d5c31126..75fe0f54b2eb 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts @@ -71,4 +71,6 @@ export interface ILivechatDepartmentModel extends IBaseModel<ILivechatDepartment checkIfMonitorIsMonitoringDepartmentById(monitorId: string, departmentId: string): Promise<boolean>; countArchived(): Promise<number>; findEnabledInIds(departmentsIds: string[], options?: FindOptions<ILivechatDepartment>): FindCursor<ILivechatDepartment>; + archiveDepartment(_id: string): Promise<Document | UpdateResult>; + unarchiveDepartment(_id: string): Promise<Document | UpdateResult>; } diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index de0cf9d10f96..66ffe9232749 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -130,6 +130,8 @@ export interface IRoomsModel extends IBaseModel<IRoom> { findByE2E(options?: FindOptions<IRoom>): FindCursor<IRoom>; + findE2ERoomById(roomId: IRoom['_id'], options?: FindOptions<IRoom>): Promise<IRoom | null>; + findRoomsInsideTeams(autoJoin?: boolean): FindCursor<IRoom>; findOneDirectRoomContainingAllUserIDs(uid: IDirectMessageRoom['uids'], options?: FindOptions<IRoom>): Promise<IRoom | null>; diff --git a/packages/model-typings/src/models/IStatisticsModel.ts b/packages/model-typings/src/models/IStatisticsModel.ts index ac84a49525b3..fe4534eaee0f 100644 --- a/packages/model-typings/src/models/IStatisticsModel.ts +++ b/packages/model-typings/src/models/IStatisticsModel.ts @@ -4,4 +4,5 @@ import type { IBaseModel } from './IBaseModel'; export interface IStatisticsModel extends IBaseModel<IStats> { findLast(): Promise<IStats>; + findMonthlyPeakConnections(): Promise<Pick<IStats, 'dailyPeakConnections' | 'createdAt'> | null>; } diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 53b0a69ec232..56119982afb3 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -131,6 +131,7 @@ export interface ISubscriptionsModel extends IBaseModel<ISubscription> { findByUserId(userId: string, options?: FindOptions<ISubscription>): FindCursor<ISubscription>; cachedFindByUserId(userId: string, options?: FindOptions<ISubscription>): FindCursor<ISubscription>; updateAutoTranslateById(_id: string, autoTranslate: boolean): Promise<UpdateResult>; + disableAutoTranslateByRoomId(roomId: IRoom['_id']): Promise<UpdateResult | Document>; findAlwaysNotifyDesktopUsersByRoomId(roomId: string): FindCursor<ISubscription>; findOneByRoomNameAndUserId(roomName: string, userId: string): Promise<ISubscription | null>; diff --git a/packages/server-fetch/src/parsers.ts b/packages/server-fetch/src/parsers.ts index 598ecbbd0e8e..ad0a44e96cfb 100644 --- a/packages/server-fetch/src/parsers.ts +++ b/packages/server-fetch/src/parsers.ts @@ -1,32 +1,20 @@ import type { ExtendedFetchOptions, FetchOptions, OriginalFetchOptions } from './types'; -function isPostOrPutOrDeleteWithBody(options?: ExtendedFetchOptions): boolean { - // No method === 'get' - if (!options?.method) { - return false; - } - const { method, body } = options; - const lowerMethod = method?.toLowerCase(); - return ['post', 'put', 'delete'].includes(lowerMethod) && body != null; -} - const jsonParser = (options: ExtendedFetchOptions) => { if (!options) { return {}; } - if (isPostOrPutOrDeleteWithBody(options)) { - try { - if (options && typeof options.body === 'object' && !Buffer.isBuffer(options.body)) { - options.body = JSON.stringify(options.body); - options.headers = { - 'Content-Type': 'application/json', - ...options.headers, - }; - } - } catch (e) { - // Body is not JSON, do nothing + try { + if (typeof options.body === 'object' && !Buffer.isBuffer(options.body)) { + options.body = JSON.stringify(options.body); + options.headers = { + ...options.headers, + 'Content-Type': 'application/json', // force content type to be json + }; } + } catch (e) { + // Body is not JSON, do nothing } return options as FetchOptions; diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index fa227575ccc2..f8ef2d3a1e93 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@babel/core": "~7.22.9", "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/icons": "^0.32.0", "@rocket.chat/mock-providers": "workspace:^", diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 1d5d478e17cd..b5c804b4a2ad 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@babel/core": "~7.22.9", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/icons": "^0.32.0", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index d253b61cc1b0..7ca7b1d86140 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.9", "@rocket.chat/css-in-js": "next", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/icons": "^0.32.0", "@rocket.chat/styled": "next", diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json index 5e8e3bf2cc5c..5a8b1276defb 100644 --- a/packages/uikit-playground/package.json +++ b/packages/uikit-playground/package.json @@ -15,7 +15,7 @@ "@codemirror/tooltip": "^0.19.16", "@lezer/highlight": "^1.1.6", "@rocket.chat/css-in-js": "next", - "@rocket.chat/fuselage": "^0.32.2", + "@rocket.chat/fuselage": "^0.34.0", "@rocket.chat/fuselage-hooks": "^0.32.1", "@rocket.chat/fuselage-polyfills": "next", "@rocket.chat/fuselage-tokens": "next", diff --git a/yarn.lock b/yarn.lock index 7d75c388dc29..b4e4af200f30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8273,7 +8273,7 @@ __metadata: dependencies: "@rocket.chat/apps-engine": 1.41.0-alpha.290 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/fuselage-polyfills": next "@rocket.chat/gazzodown": "workspace:^" @@ -8322,9 +8322,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:^0.32.2": - version: 0.32.2 - resolution: "@rocket.chat/fuselage@npm:0.32.2" +"@rocket.chat/fuselage@npm:^0.34.0": + version: 0.34.0 + resolution: "@rocket.chat/fuselage@npm:0.34.0" dependencies: "@rocket.chat/css-in-js": ^0.31.25 "@rocket.chat/css-supports": ^0.31.25 @@ -8342,7 +8342,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: 28e80385961b090c71d0897c22c3c799ca05d30285456d96d3ca5ff2a1a4ba02362644084e611bd3f2a376acdf4c2e75180b8aee196a63969a7d6559abd73d79 + checksum: 72cd1dd7ef13cc3b69fadac5c064a45cd2b65b8a221cde2e8149fa873ac6de89648c677caedb10979e5cf08d39b79f1d7a30caa6378bdeeb873414c7fbac5e6e languageName: node linkType: hard @@ -8353,7 +8353,7 @@ __metadata: "@babel/core": ~7.22.9 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-tokens": next "@rocket.chat/message-parser": next "@rocket.chat/styled": next @@ -8706,7 +8706,7 @@ __metadata: "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.1 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.2 - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/fuselage-polyfills": next "@rocket.chat/fuselage-toastbar": next @@ -9569,7 +9569,7 @@ __metadata: dependencies: "@babel/core": ~7.22.9 "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/icons": ^0.32.0 "@rocket.chat/mock-providers": "workspace:^" @@ -9620,7 +9620,7 @@ __metadata: dependencies: "@babel/core": ~7.22.9 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/icons": ^0.32.0 "@storybook/addon-actions": ~6.5.16 "@storybook/addon-docs": ~6.5.16 @@ -9691,7 +9691,7 @@ __metadata: resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming" dependencies: "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/icons": ^0.32.0 "@rocket.chat/ui-contexts": "workspace:~" @@ -9734,7 +9734,7 @@ __metadata: "@rocket.chat/css-in-js": next "@rocket.chat/emitter": next "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/icons": ^0.32.0 "@rocket.chat/styled": next @@ -9777,7 +9777,7 @@ __metadata: "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 "@rocket.chat/css-in-js": next - "@rocket.chat/fuselage": ^0.32.2 + "@rocket.chat/fuselage": ^0.34.0 "@rocket.chat/fuselage-hooks": ^0.32.1 "@rocket.chat/fuselage-polyfills": next "@rocket.chat/fuselage-tokens": next