Skip to content

Conversation

@Khizarshah01
Copy link

@Khizarshah01 Khizarshah01 commented Aug 18, 2025

Proposed changes (including videos or screenshots)

This PR adds image cropping functionality to Rocket.Chat when uploading images in chat.

  • Users can crop images before sending them.
  • Implemented using react-easy-crop.
  • The cropping UI appears when an image is selected.
  • After cropping, the preview updates, and the cropped image is sent.
crop-imageRocketChat.mp4

Issue(s)

#27735

Steps to test or reproduce

  1. Go to any channel.
  2. Click the attachment icon and choose image from computer.
  3. Crop button appear, click on it.
  4. The cropping interface should appear.
  5. Adjust the crop area and click "Crop".
  6. The preview should update to the cropped image.
  7. Send the image and verify it appears correctly in chat.

Further comments

  • This PR only adds cropping. Other editing features such as rotation, filters, or drawing can be added in future PRs.

Summary by CodeRabbit

  • New Features

    • Crop images before sending in chat with an adjustable preview and apply/cancel controls in the upload modal.
    • Uploaded previews and sent files reflect the cropped image when applied.
    • Image Crop Preview available as a toggleable feature in Feature Preview settings (with localized UI text).
  • Chores

    • Added runtime dependency to support image cropping.

@Khizarshah01 Khizarshah01 requested a review from a team as a code owner August 18, 2025 18:56
@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented Aug 18, 2025

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is missing the required milestone or project

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link

changeset-bot bot commented Aug 18, 2025

🦋 Changeset detected

Latest commit: 4035891

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 40 packages
Name Type
@rocket.chat/meteor Minor
@rocket.chat/core-typings Minor
@rocket.chat/rest-typings Minor
@rocket.chat/uikit-playground Patch
@rocket.chat/api-client Patch
@rocket.chat/apps Patch
@rocket.chat/core-services Patch
@rocket.chat/cron Patch
@rocket.chat/ddp-client Patch
@rocket.chat/fuselage-ui-kit Major
@rocket.chat/gazzodown Major
@rocket.chat/http-router Patch
@rocket.chat/livechat Patch
@rocket.chat/model-typings Patch
@rocket.chat/ui-avatar Major
@rocket.chat/ui-client Major
@rocket.chat/ui-contexts Major
@rocket.chat/ui-voip Major
@rocket.chat/web-ui-registration Major
@rocket.chat/account-service Patch
@rocket.chat/authorization-service Patch
@rocket.chat/ddp-streamer Patch
@rocket.chat/omnichannel-transcript Patch
@rocket.chat/presence-service Patch
@rocket.chat/queue-worker Patch
@rocket.chat/abac Patch
@rocket.chat/federation-matrix Patch
@rocket.chat/license Patch
@rocket.chat/media-calls Patch
@rocket.chat/omnichannel-services Patch
@rocket.chat/pdf-worker Patch
@rocket.chat/presence Patch
rocketchat-services Patch
@rocket.chat/models Patch
@rocket.chat/network-broker Patch
@rocket.chat/omni-core-ee Patch
@rocket.chat/mock-providers Patch
@rocket.chat/ui-video-conf Major
@rocket.chat/instance-status Patch
@rocket.chat/omni-core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@CLAassistant
Copy link

CLAassistant commented Aug 18, 2025

CLA assistant check
All committers have signed the CLA.

@jeanfbrito
Copy link
Contributor

Hey @Khizarshah01 ! I was looking at this some weeks ago! Perfect timing!
What do you think about adding this to the avatar upload too? It was there that I was thinking in use it.

@Khizarshah01
Copy link
Author

Hey @jeanfbrito , thanks for the suggestion! 😄
I was actually thinking the same for avatars, adding cropping for avatars would be really useful.
Should I extend this PR to cover avatar cropping as well, or keep this one focused on general image uploads and open a separate PR for avatars?
Also, do you think adding a rotation feature here would be useful?

@MartinSchoeler
Copy link
Member

@Khizarshah01 Nice contribution!

Should I extend this PR to cover avatar cropping as well, or keep this one focused on general image uploads and open a separate PR for avatars?

Let's do a separate PR for the avatars, it makes easier for the maintainers to review

Also would you mind using our feature preview flag for this new feature? This will allow us to merge this as an "Experimental" feature.

From reading your code, I think you can do that by creating a new apps/meteor/client/views/room/modals/FileUploadModal/FilePreview.tsx file as CropFilePreview.tsx and encapsulating your code there, then create a switch in apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx using our useFeaturePreview hook

I'll help you out along the way

@jeanfbrito
Copy link
Contributor

@Khizarshah01 sorry I forgot to send the message... 🤦
Let's do it in separate PRs. However, I would like to see it as a component, so we could simply add it anywhere we upload images, such as in the avatar, in the future.
What do you think about adding all the features the library has available? It would be great as it would be just adding on to the UI, right?
I can see it being accepted by the core team if we work well in the UI using Fuselage.

@jeanfbrito
Copy link
Contributor

In Avatar we have a fixed size, so it would be easier to make it work directly, for the image uploads would be better if we could draw a box to select what we want to crop.

@Khizarshah01
Copy link
Author

Thanks @MartinSchoeler , I have applied your feedback, created CropFilePreview.tsx, added the feature preview flag, and Kept original FilePreview.tsx unchanged. For now I have kept it only for general image uploads, and will open a separate PR for avatars as you suggested.

@Khizarshah01
Copy link
Author

Khizarshah01 commented Aug 28, 2025

Thanks @jeanfbrito , for the suggestions 🙏 I completely agree

I would like to see it as a component, so we could simply add it anywhere we upload images, such as in the avatar, in the future.

For this PR, should I continue implementing these changes here?

@Khizarshah01
Copy link
Author

rocketChatCropImage.mp4

This PR has been updated with all the requested feedback, feature preview flag, separate component, and (UI) Fuselage usage. Ready for review whenever you have time.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 23, 2025

Walkthrough

Adds an image-cropping flow to file uploads: new CropFilePreview component, FileUploadModal updates to support cropping and pass a File to submit, upload flow updated to handle an optional cropped File through encryption and upload; feature flag, i18n keys, and react-easy-crop dependency added.

Changes

Cohort / File(s) Change Summary
Upload flow handling
apps/meteor/client/lib/chats/flows/uploadFiles.ts
onSubmit now accepts an optional croppedFile; logic unified to use fileToUpload (cropped or original) for E2EE, early exits, metadata, and upload; filename/metadata references updated.
File upload modal & cropping UI
apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx, apps/meteor/client/views/room/modals/FileUploadModal/CropFilePreview.tsx
New CropFilePreview component with cropping UI (react-easy-crop); FileUploadModal gains currentFile/isCropping state, Crop button, cropping modal, and updated onSubmit signature to pass the File; exports new FilePreviewType enum and default CropFilePreview.
Feature flag definition
packages/ui-client/src/hooks/useFeaturePreviewList.ts
Adds imageCropPreview to FeaturesAvailable and defaultFeaturesPreview with i18n keys (default enabled value present).
Localization
packages/i18n/src/locales/en.i18n.json
Adds keys: Crop, Crop_Image, Cropped_preview, Image_crop_preview, Image_crop_preview_description.
Dependencies
apps/meteor/package.json, package.json
Adds runtime dependency react-easy-crop ^5.5.0 to app and root package.json.
Changeset
.changeset/young-buses-grow.md
New changeset documenting minor version bump and image cropping feature.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant M as FileUploadModal
  participant C as CropFilePreview
  participant F as uploadFiles
  participant E as E2EE
  participant S as Server

  U->>M: Select file
  M-->>U: Show preview (+ Crop button if image/feature)
  alt User chooses Crop
    M->>C: Open cropping modal with File
    C-->>M: Return cropped File
  else No cropping
    M-->>M: Use original File
  end
  M->>F: onSubmit(name, desc, file)
  opt Encryption enabled
    F->>E: Encrypt fileToUpload
    E-->>F: Encrypted data
  end
  F->>S: Upload attachment (uses fileToUpload metadata)
  S-->>U: Message with attachment
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Pay attention to: apps/meteor/client/views/room/modals/FileUploadModal/CropFilePreview.tsx (image-to-canvas cropping, object URL lifecycle), apps/meteor/client/lib/chats/flows/uploadFiles.ts (E2EE/integration using new File), and FileUploadModal prop signature change impacting callers.

Possibly related PRs

Suggested labels

stat: ready to merge, stat: QA assured

Suggested reviewers

  • MartinSchoeler

Poem

A nibble, a trim, a hop and a crop,
I frame the pixels—snip, snip, stop!
With canvas bright and whiskers twitch,
I shape your pics with one quick stitch.
Off they go—trimmed, tidy, and hip. 🐇✂️🖼️

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: enable cropping of images before sending in chat" is concise, clear, and directly aligned with the primary objective of this changeset. The title accurately summarizes the main feature addition—image cropping functionality for file uploads—which is consistently reflected across all modified files, including the new CropFilePreview component, updated FileUploadModal flow, dependency additions, and localization keys. The title uses the semantic versioning "feat:" prefix and avoids vague language, making it immediately understandable to team members reviewing the project history.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 131d8af and 944f734.

📒 Files selected for processing (2)
  • .changeset/young-buses-grow.md (1 hunks)
  • apps/meteor/client/lib/chats/flows/uploadFiles.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/young-buses-grow.md
🧰 Additional context used
🧬 Code graph analysis (1)
apps/meteor/client/lib/chats/flows/uploadFiles.ts (3)
apps/meteor/client/lib/settings/settings.ts (1)
  • settings (68-68)
apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts (1)
  • shouldConvertSentMessages (167-183)
packages/core-typings/src/IMessage/IMessage.ts (1)
  • IE2EEMessage (432-436)
🔇 Additional comments (4)
apps/meteor/client/lib/chats/flows/uploadFiles.ts (4)

73-74: LGTM: Clean fallback logic for cropped files.

The signature extension and fallback pattern correctly prioritize the cropped file when provided while maintaining backward compatibility.


75-78: LGTM: Consistent property override pattern.

The name assignment correctly targets fileToUpload, maintaining the existing pattern while supporting the cropped file path.


84-100: LGTM: Consistent use of fileToUpload across all paths.

All upload and encryption operations correctly use fileToUpload, ensuring the cropped file (when provided) is processed in both E2EE and non-E2EE flows.


107-107: LGTM: Attachment title correctly uses fileToUpload.

The title properly references fileToUpload.name, reflecting the name of the actually uploaded (and potentially cropped) file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
apps/meteor/client/lib/chats/flows/uploadFiles.ts (4)

73-79: Don’t mutate File.name; create a new File when renaming.

Directly redefining File.name is unreliable and can throw. Create a new File with the desired name.

Apply this diff:

-				onSubmit: async (fileName: string, description?: string, croppedFile?: File): Promise<void> => {
-					const fileToUpload = croppedFile ?? file;
-					Object.defineProperty(fileToUpload, 'name', {
-						writable: true,
-						value: fileName,
-					});
+				onSubmit: async (fileName: string, description?: string, croppedFile?: File): Promise<void> => {
+					const baseFile = croppedFile ?? file;
+					const fileToUpload =
+						baseFile.name === fileName
+							? baseFile
+							: new File([baseFile], fileName, {
+									type: baseFile.type,
+									lastModified: baseFile.lastModified,
+							  });

106-170: Use the cropped/renamed file for all metadata (type, size, dims, name) in E2EE content.

This block still references the original file, producing wrong metadata and dimensions after cropping.

Apply this diff:

 						const getContent = async (_id: string, fileUrl: string): Promise<IE2EEMessage['content']> => {
 							const attachments = [];
 
 							const attachment: FileAttachmentProps = {
-								title: fileToUpload.name,
+								title: fileToUpload.name,
 								type: 'file',
 								description,
 								title_link: fileUrl,
 								title_link_download: true,
 								encryption: {
 									key: encryptedFile.key,
 									iv: encryptedFile.iv,
 								},
 								hashes: {
 									sha256: encryptedFile.hash,
 								},
 							};
 
-							if (/^image\/.+/.test(file.type)) {
-								const dimensions = await getHeightAndWidthFromDataUrl(window.URL.createObjectURL(file));
+							if (/^image\/.+/.test(fileToUpload.type)) {
+								const objectUrl = window.URL.createObjectURL(fileToUpload);
+								const dimensions = await getHeightAndWidthFromDataUrl(objectUrl);
+								window.URL.revokeObjectURL(objectUrl);
 
 								attachments.push({
 									...attachment,
 									image_url: fileUrl,
-									image_type: file.type,
-									image_size: file.size,
+									image_type: fileToUpload.type,
+									image_size: fileToUpload.size,
 									...(dimensions && {
 										image_dimensions: dimensions,
 									}),
 								});
-							} else if (/^audio\/.+/.test(file.type)) {
+							} else if (/^audio\/.+/.test(fileToUpload.type)) {
 								attachments.push({
 									...attachment,
 									audio_url: fileUrl,
-									audio_type: file.type,
-									audio_size: file.size,
+									audio_type: fileToUpload.type,
+									audio_size: fileToUpload.size,
 								});
-							} else if (/^video\/.+/.test(file.type)) {
+							} else if (/^video\/.+/.test(fileToUpload.type)) {
 								attachments.push({
 									...attachment,
 									video_url: fileUrl,
-									video_type: file.type,
-									video_size: file.size,
+									video_type: fileToUpload.type,
+									video_size: fileToUpload.size,
 								});
 							} else {
 								attachments.push({
 									...attachment,
-									size: file.size,
-									format: getFileExtension(file.name),
+									size: fileToUpload.size,
+									format: getFileExtension(fileToUpload.name),
 								});
 							}
 
 							const files = [
 								{
 									_id,
-									name: file.name,
-									type: file.type,
-									size: file.size,
+									name: fileToUpload.name,
+									type: fileToUpload.type,
+									size: fileToUpload.size,
 									// "format": "png"
 								},
 							] as IMessage['files'];
 
 							return e2eRoom.encryptMessageContent({
 								attachments,
 								files,
 								file: files?.[0],
 							});
 						};

172-183: Build fileContentData from the actual file being sent.

Keep metadata consistent with the cropped/renamed file.

Apply this diff:

-						const fileContentData = {
-							type: file.type,
-							typeGroup: file.type.split('/')[0],
-							name: fileName,
+						const fileContentData = {
+							type: fileToUpload.type,
+							typeGroup: fileToUpload.type.split('/')[0],
+							name: fileToUpload.name,
 							encryption: {
 								key: encryptedFile.key,
 								iv: encryptedFile.iv,
 							},
 							hashes: {
 								sha256: encryptedFile.hash,
 							},
 						};

121-123: Revoke temporary object URLs to avoid leaks.

createObjectURL must be revoked after use.

Included in the previous diff: store the URL in a variable and call URL.revokeObjectURL(objectUrl) after awaiting dimensions.

apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx (1)

65-76: Validate against the cropped file’s size.

Size check uses the original file; use currentFile so users don’t get false results after cropping.

Apply this diff:

-	const submit = ({ name, description }: { name: string; description?: string }): void => {
+	const submit = ({ name, description }: { name: string; description?: string }): void => {
 		// -1 maxFileSize means there is no limit
-		if (maxFileSize > -1 && (file.size || 0) > maxFileSize) {
+		if (maxFileSize > -1 && (currentFile.size || 0) > maxFileSize) {
 			onClose();
 			return dispatchToastMessage({
 				type: 'error',
 				message: t('File_exceeds_allowed_size_of_bytes', { size: fileSize(maxFileSize) }),
 			});
 		}
 
 		onSubmit(name, description, currentFile);
 	};
🧹 Nitpick comments (4)
package.json (1)

86-88: Avoid duplicating app-only deps at the repo root.

react-easy-crop is only used by @rocket.chat/meteor; keep it there to reduce surface area and lockfile churn.

Apply this diff to remove the root-level dependency:

   "dependencies": {
     "@types/stream-buffers": "^3.0.7",
-    "node-gyp": "^10.2.0",
-    "react-easy-crop": "^5.5.0"
+    "node-gyp": "^10.2.0"
   }

If you intend to pin the version across workspaces, prefer a root "resolutions" entry instead:

   "resolutions": {
+    "react-easy-crop": "^5.5.0",
     "minimist": "1.2.6",
     ...
   }
apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx (1)

50-55: Sync the file name input after cropping.

After applying a crop, update the form field so users see the actual filename being uploaded.

Apply this diff:

-	const {
-		register,
-		handleSubmit,
-		formState: { errors, isSubmitting },
-	} = useForm({ mode: 'onBlur', defaultValues: { name: fileName, description: fileDescription } });
+	const {
+		register,
+		handleSubmit,
+		formState: { errors, isSubmitting },
+		setValue,
+	} = useForm({ mode: 'onBlur', defaultValues: { name: fileName, description: fileDescription } });
-					<CropFilePreview
+					<CropFilePreview
 						file={currentFile}
 						onFileChange={(newFile) => {
 							setCurrentFile(newFile);
+							setValue('name', newFile.name, { shouldValidate: true });
 							setIsCropping(false);
 						}}
 						onCancel={() => setIsCropping(false)}
 					/>

Also applies to: 116-123

apps/meteor/client/views/room/modals/FileUploadModal/CropFilePreview.tsx (2)

11-15: Avoid re‑declaring FilePreviewType; import it to prevent type drift.

Duplicating the enum risks type mismatches with MediaPreview/FilePreview. Prefer importing the existing type.

If FilePreview.tsx exports FilePreviewType, switch to:

-import { isIE11 } from '../../../../lib/utils/isIE11';
-
-export enum FilePreviewType {
-	IMAGE = 'image',
-	AUDIO = 'audio',
-	VIDEO = 'video',
-}
+import { isIE11 } from '../../../../lib/utils/isIE11';
+import { FilePreviewType } from './FilePreview';

If it doesn’t, consider exporting it there and reusing here. Verify MediaPreview’s prop typing to ensure compatibility.


115-124: Dead UI branch after onFileChange.

Parent immediately closes the crop modal; the local croppedImageURL preview won’t render. Safe to remove for simplicity.

You can drop croppedImageURL state and the corresponding early return to reduce complexity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 652ff73 and 131d8af.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (8)
  • .changeset/young-buses-grow.md (1 hunks)
  • apps/meteor/client/lib/chats/flows/uploadFiles.ts (2 hunks)
  • apps/meteor/client/views/room/modals/FileUploadModal/CropFilePreview.tsx (1 hunks)
  • apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx (6 hunks)
  • apps/meteor/package.json (1 hunks)
  • package.json (1 hunks)
  • packages/i18n/src/locales/en.i18n.json (2 hunks)
  • packages/ui-client/src/hooks/useFeaturePreviewList.ts (2 hunks)
🔇 Additional comments (7)
packages/ui-client/src/hooks/useFeaturePreviewList.ts (2)

8-9: Add imageCropPreview and secondarySidebar to FeaturesAvailable: LGTM.

Prevents union/type drift with defaultFeaturesPreview.


46-53: Feature preview entry verified — i18n keys and gating present

  • i18n keys exist: packages/i18n/src/locales/en.i18n.json — Image_crop_preview, Image_crop_preview_description.
  • Flag is consumed in UI: apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx uses useFeaturePreview('imageCropPreview'); entry is in packages/ui-client/src/hooks/useFeaturePreviewList.ts.
apps/meteor/package.json (1)

431-431: Approve: react-easy-crop added at app scope; peer deps compatible with React 18.
Usage confirmed in apps/meteor/client/views/room/modals/FileUploadModal/CropFilePreview.tsx; npm registry (v5.5.1) lists peerDependencies { react: '>=16.4.0', 'react-dom': '>=16.4.0' } — compatible with React 18.

packages/i18n/src/locales/en.i18n.json (1)

1491-1493: LGTM on new crop UI strings

The keys and copy look fine for the crop action and modal title.

apps/meteor/client/views/room/modals/FileUploadModal/FileUploadModal.tsx (1)

145-149: Nice: feature‑flagged Crop UI, image‑only.

Good gating via useFeaturePreview and MIME check.

Please confirm the feature key is imageCropPreview and is included in defaultFeaturesPreview/useFeaturePreviewList.

apps/meteor/client/views/room/modals/FileUploadModal/CropFilePreview.tsx (2)

99-110: Object URL lifecycle handled—LGTM.

Good creation and cleanup of object URLs for previews.


136-153: Cropper config: sensible defaults.

Free aspect, wheel zoom, and bounded container are fine for v1.

@Khizarshah01
Copy link
Author

this PR is now ready for review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants