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/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/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/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/autotranslate/server/methods/saveSettings.ts b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts index 1ba5bcdfcd76..e396d78887a9 100644 --- a/apps/meteor/app/autotranslate/server/methods/saveSettings.ts +++ b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts @@ -1,4 +1,4 @@ -import { Subscriptions } from '@rocket.chat/models'; +import { Subscriptions, Rooms } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -46,6 +46,13 @@ Meteor.methods({ switch (field) { case 'autoTranslate': + const room = await Rooms.findE2ERoomById(rid, { projection: { _id: 1 } }); + if (room && value === '1') { + throw new Meteor.Error('error-e2e-enabled', 'Enabling auto-translation in E2E encrypted rooms is not allowed', { + method: 'saveAutoTranslateSettings', + }); + } + await Subscriptions.updateAutoTranslateById(subscription._id, value === '1'); if (!subscription.autoTranslateLanguage && options.defaultLanguage) { await Subscriptions.updateAutoTranslateLanguageById(subscription._id, options.defaultLanguage); diff --git a/apps/meteor/app/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/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 { + 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/client/components/avatar/RoomAvatarEditor.tsx b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx index 32948eb42243..04b07e9cd627 100644 --- a/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/RoomAvatarEditor.tsx @@ -71,7 +71,7 @@ const RoomAvatarEditor = ({ disabled = false, room, roomAvatar, onChangeAvatar } danger icon='trash' title={t('Accounts_SetDefaultAvatar')} - disabled={roomAvatar === null || isRoomFederated(room) || disabled} + disabled={!roomAvatar || isRoomFederated(room) || disabled} onClick={clickReset} /> diff --git a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts index e1c3126985ae..73b0f34836e1 100644 --- a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts @@ -5,13 +5,15 @@ import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { e2e } from '../../../app/e2e/client/rocketchat.e2e'; -import { useRoom } from '../../views/room/contexts/RoomContext'; +import { dispatchToastMessage } from '../../lib/toast'; +import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext'; import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; import { useReactiveValue } from '../useReactiveValue'; export const useE2EERoomAction = () => { const enabled = useSetting('E2E_Enable', false); const room = useRoom(); + const subscription = useRoomSubscription(); const readyToEncrypt = useReactiveValue(useCallback(() => e2e.isReady(), [])) || room.encrypted; const permittedToToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id); const permittedToEditRoom = usePermission('edit-room', room._id); @@ -21,8 +23,22 @@ export const useE2EERoomAction = () => { const toggleE2E = useEndpoint('POST', '/v1/rooms.saveRoomSettings'); - const action = useMutableCallback(() => { - void toggleE2E({ rid: room._id, encrypted: !room.encrypted }); + const action = useMutableCallback(async () => { + const { success } = await toggleE2E({ rid: room._id, encrypted: !room.encrypted }); + if (!success) { + return; + } + + dispatchToastMessage({ + type: 'success', + message: room.encrypted + ? t('E2E_Encryption_disabled_for_room', { roomName: room.name }) + : t('E2E_Encryption_enabled_for_room', { roomName: room.name }), + }); + + if (subscription?.autoTranslate) { + dispatchToastMessage({ type: 'success', message: t('AutoTranslate_Disabled_for_room', { roomName: room.name }) }); + } }); const enabledOnRoom = !!room.encrypted; diff --git a/apps/meteor/client/lib/chats/ChatAPI.ts b/apps/meteor/client/lib/chats/ChatAPI.ts index 1630071be658..8242de07d791 100644 --- a/apps/meteor/client/lib/chats/ChatAPI.ts +++ b/apps/meteor/client/lib/chats/ChatAPI.ts @@ -144,7 +144,7 @@ export type ChatAPI = { ActionManager: any; readonly flows: { - readonly uploadFiles: (files: readonly File[]) => Promise; + readonly uploadFiles: (files: readonly File[], resetFileInput?: () => void) => Promise; readonly sendMessage: ({ text, tshow }: { text: string; tshow?: boolean; previewUrls?: string[] }) => Promise; readonly processSlashCommand: (message: IMessage, userId: string | null) => Promise; readonly processTooLongMessage: (message: IMessage) => Promise; diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 58eb18400a30..1411ad5a004e 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -6,7 +6,7 @@ import { imperativeModal } from '../../imperativeModal'; import { prependReplies } from '../../utils/prependReplies'; import type { ChatAPI } from '../ChatAPI'; -export const uploadFiles = async (chat: ChatAPI, files: readonly File[]): Promise => { +export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFileInput?: () => void): Promise => { const replies = chat.composer?.quotedMessages.get() ?? []; const msg = await prependReplies('', replies); @@ -52,4 +52,5 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[]): Promis }; uploadNextFile(); + resetFileInput?.(); }; diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.tsx b/apps/meteor/client/views/admin/rooms/EditRoom.tsx index b12b5e3e1ab9..8130f2e9ec8b 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -13,18 +13,17 @@ import { TextAreaInput, } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetModal, useToastMessageDispatch, useRoute, usePermission, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; -import React, { useState, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar'; -import GenericModal from '../../../components/GenericModal'; import RoomAvatarEditor from '../../../components/avatar/RoomAvatarEditor'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; import { useForm } from '../../../hooks/useForm'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; -import DeleteTeamModalWithRooms from '../../teams/contextualBar/info/DeleteTeam'; +import { useDeleteRoom } from '../../hooks/roomActions/useDeleteRoom'; type EditRoomProps = { room: Pick; @@ -65,11 +64,6 @@ const getInitialValues = (room: Pick): EditRoomFormV const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => { const t = useTranslation(); - const [deleting, setDeleting] = useState(false); - - const setModal = useSetModal(); - const dispatchToastMessage = useToastMessageDispatch(); - const { values, handlers, hasUnsavedChanges, reset } = useForm(getInitialValues(room)); const [canViewName, canViewTopic, canViewAnnouncement, canViewArchived, canViewDescription, canViewType, canViewReadOnly] = @@ -119,9 +113,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => const changeArchivation = archived !== !!room.archived; - const roomsRoute = useRoute('admin-rooms'); - - const canDelete = usePermission(`delete-${room.t}`); + const { handleDelete, canDeleteRoom, isDeleting } = useDeleteRoom(room, { reload: onDelete }); const archiveSelector = room.archived ? 'unarchive' : 'archive'; const archiveMessage = room.archived ? 'Room_has_been_unarchived' : 'Room_has_been_archived'; @@ -161,61 +153,6 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => handleRoomType(roomType === 'p' ? 'c' : 'p'); }); - const deleteRoom = useEndpoint('POST', '/v1/rooms.delete'); - const deleteTeam = useEndpoint('POST', '/v1/teams.delete'); - - const handleDelete = useMutableCallback(() => { - const handleDeleteTeam = async (roomsToRemove: IRoom['_id'][]) => { - try { - setDeleting(true); - setModal(null); - await deleteTeam({ teamId: room.teamId as string, ...(roomsToRemove.length && { roomsToRemove }) }); - dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); - roomsRoute.push({}); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - setDeleting(false); - } finally { - onDelete(); - } - }; - - if (room.teamMain) { - setModal( - setModal(null)} teamId={room.teamId as string} />, - ); - - return; - } - - const handleDeleteRoom = async (): Promise => { - try { - setDeleting(true); - setModal(null); - await deleteRoom({ roomId: room._id }); - dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); - roomsRoute.push({}); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - setDeleting(false); - } finally { - onDelete(); - } - }; - - setModal( - setModal(null)} - onCancel={(): void => setModal(null)} - confirmText={t('Yes_delete_it')} - > - {t('Delete_Room_Warning')} - , - ); - }); - return ( <> e.preventDefault())}> @@ -227,7 +164,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Name')} - + {room.t !== 'd' && ( @@ -246,7 +183,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Topic')} - + )} @@ -281,7 +218,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Private')} - + {t('Just_invited_people_can_access_this_channel')} @@ -292,7 +229,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Read_only')} - + {t('Only_authorized_users_can_write_new_messages')} @@ -314,7 +251,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Room_archivation_state_true')} - + @@ -325,7 +262,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Default')} - + @@ -333,7 +270,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Favorite')} - + @@ -341,22 +278,22 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => {t('Featured')} - + - - - diff --git a/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx b/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx index cd300c14a481..9307a2596a71 100644 --- a/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx +++ b/apps/meteor/client/views/admin/viewLogs/AnalyticsReports.tsx @@ -1,4 +1,4 @@ -import { Box, Icon, Skeleton } from '@rocket.chat/fuselage'; +import { Box, Icon, Skeleton, Scrollable } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -10,8 +10,8 @@ const AnalyticsReports = () => { const { data, isLoading, isSuccess, isError } = useAnalyticsObject(); return ( - <> - + + @@ -23,18 +23,15 @@ const AnalyticsReports = () => { {t('Analytics_page_briefing_second_paragraph')} - - {isSuccess &&
{JSON.stringify(data, null, '\t')}
} - {isError && t('Something_went_wrong_try_again_later')} - {isLoading && ( - <> - - - - - )} -
- + + + {isSuccess &&
{JSON.stringify(data, null, '\t')}
} + {isError && t('Something_went_wrong_try_again_later')} + {isLoading && Array.from({ length: 10 }).map((_, index) => )} + <> +
+
+
); }; diff --git a/apps/meteor/client/views/admin/viewLogs/ServerLogs.tsx b/apps/meteor/client/views/admin/viewLogs/ServerLogs.tsx index 4787dfb12a6d..aa496de024fa 100644 --- a/apps/meteor/client/views/admin/viewLogs/ServerLogs.tsx +++ b/apps/meteor/client/views/admin/viewLogs/ServerLogs.tsx @@ -166,7 +166,7 @@ const ServerLogs = (): ReactElement => { }, [sendToBottomIfNecessary]); return ( - + { const t = useTranslation(); - const [tab, setTab] = useState('Logs'); return ( - + - - + + setTab('Logs')} selected={tab === 'Logs'}> {t('Logs')} @@ -24,8 +23,8 @@ const ViewLogsPage = (): ReactElement => { {t('Analytic_reports')} - {tab === 'Logs' ? : } - + + {tab === 'Logs' ? : } ); }; diff --git a/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx new file mode 100644 index 000000000000..be4728732284 --- /dev/null +++ b/apps/meteor/client/views/hooks/roomActions/useDeleteRoom.tsx @@ -0,0 +1,89 @@ +import type { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; +import { isRoomFederated } from '@rocket.chat/core-typings'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useSetModal, useToastMessageDispatch, useRouter, usePermission, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import React from 'react'; + +import GenericModal from '../../../components/GenericModal'; +import DeleteTeamModal from '../../teams/contextualBar/info/DeleteTeam'; + +export const useDeleteRoom = (room: IRoom | Pick, { reload }: { reload?: () => void } = {}) => { + const t = useTranslation(); + const router = useRouter(); + const setModal = useSetModal(); + const dispatchToastMessage = useToastMessageDispatch(); + const hasPermissionToDelete = usePermission(`delete-${room.t}`, room._id); + const canDeleteRoom = isRoomFederated(room) ? false : hasPermissionToDelete; + + const isAdminRoute = router.getRouteName() === 'admin-rooms'; + + const deleteRoomEndpoint = useEndpoint('POST', '/v1/rooms.delete'); + const deleteTeamEndpoint = useEndpoint('POST', '/v1/teams.delete'); + + const deleteRoomMutation = useMutation({ + mutationFn: deleteRoomEndpoint, + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); + if (isAdminRoute) { + return router.navigate('/admin/rooms'); + } + + return router.navigate('/home'); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { + setModal(null); + reload?.(); + }, + }); + + const deleteTeamMutation = useMutation({ + mutationFn: deleteTeamEndpoint, + onSuccess: () => { + dispatchToastMessage({ type: 'success', message: t('Team_has_been_deleted') }); + if (isAdminRoute) { + return router.navigate('/admin/rooms'); + } + + return router.navigate('/home'); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + }, + onSettled: () => { + setModal(null); + reload?.(); + }, + }); + + const isDeleting = deleteTeamMutation.isLoading || deleteRoomMutation.isLoading; + + const handleDelete = useMutableCallback(() => { + const handleDeleteTeam = async (roomsToRemove: IRoom['_id'][]) => { + if (!room.teamId) { + return; + } + + deleteTeamMutation.mutateAsync({ teamId: room.teamId, ...(roomsToRemove.length && { roomsToRemove }) }); + }; + + if (room.teamMain && room.teamId) { + return setModal( setModal(null)} teamId={room.teamId} />); + } + + const handleDeleteRoom = async () => { + deleteRoomMutation.mutateAsync({ roomId: room._id }); + }; + + setModal( + setModal(null)} confirmText={t('Yes_delete_it')}> + {t('Delete_Room_Warning')} + , + ); + }); + + return { handleDelete, canDeleteRoom, isDeleting }; +}; diff --git a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx index 35aecc1e2dfb..dbfda21f5b7a 100644 --- a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx +++ b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx @@ -2,20 +2,31 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import colors from '@rocket.chat/fuselage-tokens/colors'; import { HeaderState } from '@rocket.chat/ui-client'; -import { useSetting, usePermission, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetting, usePermission, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; +import { dispatchToastMessage } from '../../../../lib/toast'; + const Encrypted = ({ room }: { room: IRoom }) => { const t = useTranslation(); const e2eEnabled = useSetting('E2E_Enable'); - const toggleE2E = useMethod('saveRoomSettings'); + const toggleE2E = useEndpoint('POST', '/v1/rooms.saveRoomSettings'); const canToggleE2E = usePermission('toggle-room-e2e-encryption'); const encryptedLabel = canToggleE2E ? t('Encrypted_key_title') : t('Encrypted'); - const handleE2EClick = useMutableCallback(() => { + const handleE2EClick = useMutableCallback(async () => { if (!canToggleE2E) { return; } - toggleE2E(room._id, 'encrypted', !room?.encrypted); + + const { success } = await toggleE2E({ rid: room._id, encrypted: !room.encrypted }); + if (!success) { + return; + } + + dispatchToastMessage({ + type: 'success', + message: t('E2E_Encryption_disabled_for_room', { roomName: room.name }), + }); }); return e2eEnabled && room?.encrypted ? ( diff --git a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx index 5ac168b91846..fe148aa79d78 100644 --- a/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx +++ b/apps/meteor/client/views/room/body/RoomForeword/RoomForewordUsernameListItem.tsx @@ -12,11 +12,12 @@ type RoomForewordUsernameListItemProps = { useRealName: boolean; }; +// TODO: Improve `Tag` a11y to be used as a link const RoomForewordUsernameListItem: VFC = ({ username, href, useRealName }) => { const { data, isLoading, isError } = useUserInfoQuery({ username }); return ( - + } className='mention-link' data-username={username} large> {isLoading && } {!isLoading && isError && username} diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx index 73b293a60047..f9c826fceb4b 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/actions/FileUploadAction.tsx @@ -18,6 +18,14 @@ const FileUploadAction = ({ collapsed, chatContext, disabled, ...props }: FileUp const fileInputRef = useRef(null); const chat = useChat() ?? chatContext; + const resetFileInput = () => { + if (!fileInputRef.current) { + return; + } + + fileInputRef.current.value = ''; + }; + const handleUploadChange = async (e: ChangeEvent) => { const { mime } = await import('../../../../../../../app/utils/lib/mimeTypes'); const filesToUpload = Array.from(e.target.files ?? []).map((file) => { @@ -26,8 +34,7 @@ const FileUploadAction = ({ collapsed, chatContext, disabled, ...props }: FileUp }); return file; }); - - chat?.flows.uploadFiles(filesToUpload); + chat?.flows.uploadFiles(filesToUpload, resetFileInput); }; const handleUpload = () => { diff --git a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx index 6952b5b1dafe..ad1560d3078d 100644 --- a/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx +++ b/apps/meteor/client/views/room/contextualBar/AutoTranslate/AutoTranslate.tsx @@ -1,4 +1,4 @@ -import { FieldGroup, Field, FieldLabel, FieldRow, ToggleSwitch, Select } from '@rocket.chat/fuselage'; +import { Callout, FieldGroup, Field, FieldLabel, FieldRow, ToggleSwitch, Select } from '@rocket.chat/fuselage'; import type { SelectOption } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent } from 'react'; @@ -11,6 +11,7 @@ import { ContextualbarIcon, ContextualbarContent, } from '../../../../components/Contextualbar'; +import { useRoom } from '../../contexts/RoomContext'; type AutoTranslateProps = { language: string; @@ -30,6 +31,7 @@ const AutoTranslate = ({ handleClose, }: AutoTranslateProps): ReactElement => { const t = useTranslation(); + const room = useRoom(); return ( <> @@ -40,14 +42,24 @@ const AutoTranslate = ({ + {room.encrypted && ( + + {t('Automatic_translation_not_available_info')} + + )} - + {t('Automatic_Translation')} - {t('Language')} + {t('Translate_to')}