Skip to content

feat(mobile): Allow users to set profile picture from asset viewer#25517

Merged
alextran1502 merged 28 commits intoimmich-app:mainfrom
timonrieger:feat/mobile-profile-picture
Feb 22, 2026
Merged

feat(mobile): Allow users to set profile picture from asset viewer#25517
alextran1502 merged 28 commits intoimmich-app:mainfrom
timonrieger:feat/mobile-profile-picture

Conversation

@timonrieger
Copy link
Collaborator

@timonrieger timonrieger commented Jan 25, 2026

Description

Adds the ability to set a profile picture from any remote asset in the mobile app. Users can select a remote asset and crop it before setting it as their profile picture.

  • Added setProfilePicture action button type with visibility logic (owner, not locked, RemoteAsset)
  • Created SetProfilePictureActionButton widget
  • Created ProfilePictureCropPage with 1:1 aspect ratio cropping
  • Extracted imageToUint8List utility function to remove duplicate code
  • Updated uploadProfileImageProvider to accept optional fileName parameter
  • Integrated into the action button system and viewer kebab menu

How Has This Been Tested?

  • Added unit tests for setProfilePicture button visibility logic covering:
    • Should show when owner, not locked, and asset is RemoteAsset
    • Should not show when not owner
    • Should not show when in locked view
    • Should not show when asset is not RemoteAsset
  • Manual testing of profile picture selection and cropping flow
  • Verified image conversion utility works correctly (and does not break the the other references)

Screen Recording

ScreenRecording_01-26-2026.00-29-44_1.mov

Checklist:

  • I have performed a self-review of my own code
  • I have made corresponding changes to the documentation if applicable
  • I have no unrelated changes in the PR.
  • I have confirmed that any new dependencies are strictly necessary.
  • I have written tests for new code (if applicable)
  • I have followed naming conventions/patterns in the surrounding code
  • All code in src/services/ uses repositories implementations for database calls, filesystem operations, etc.
  • All code in src/repositories/ is pretty basic/simple and does not have any immich specific logic (that belongs in src/services/)

Please describe to which degree, if any, an LLM was used in creating this pull request.

An LLM was used to generate unit tests for the setProfilePicture action button visibility logic, following the existing test patterns in the codebase.

**Root cause:**
The autogenerated Dart OpenAPI client (`UsersApi.createProfileImage()`) had two issues:
1. It set `Content-Type: multipart/form-data` without a boundary, which overrode the correct header that Dart's `MultipartRequest` would set (`multipart/form-data; boundary=...`).
2. It added the file to both `mp.fields` and `mp.files`, creating a duplicate text field.

**Result:**
Multer on the server failed to parse the multipart body, so `@UploadedFile()` was `undefined` → accessing `file.path` in `UserService.createProfileImage()` threw → **500 Internal Server Error**.

**Workaround:**
Bypass the autogenerated method in `UserApiRepository.createProfileImage()` and send the multipart request directly using the same `ApiClient` (basePath + auth), ensuring:
- No manual `Content-Type` header (let `MultipartRequest` set it with boundary)
- File only in `mp.files`, not `mp.fields`
- Proper filename fallback
This reverts commit fcf37d2.
…. Replace inline image-to-Uint8List conversion with the new utility in EditImagePage, DriftEditImagePage, and ProfilePictureCropPage.
This reverts commit 68a6165.
This reverts commit 8e85057.
Comment on lines +64 to +67
Future<bool> upload(XFile file, {String? fileName}) async {
state = state.copyWith(status: UploadProfileStatus.loading);

var profileImagePath = await _userService.createProfileImage(file.name, await file.readAsBytes());
var profileImagePath = await _userService.createProfileImage(fileName ?? file.name, await file.readAsBytes());
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

XFile.fromData() ignores the name parameter for IO-backed files (see flutter/flutter#87812 and flutter/packages#4416), so we must pass fileName explicitly, applied in

final xFile = XFile.fromData(pngBytes, mimeType: 'image/png');

compatible with existing implementation

var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

pulled this into a new util function, as we had this in use in three locations now

Copy link
Member

@shenlong-tanwen shenlong-tanwen left a comment

Choose a reason for hiding this comment

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

It might also be best if we can remove the image_picker dependency and just let user pick a profile picture from their remote assets. But that can be a separate PR. Can you refactor the widget before I start testing the implementation?

Comment on lines 31 to 33
final cropController = useCropController();
final isLoading = useState<bool>(false);
final didInitCropController = useRef(false);
Copy link
Member

Choose a reason for hiding this comment

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

We are moving away from using hooks. Can you refactor this to not use hooks but a StatefulWidget instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done and tested

@alextran1502 alextran1502 enabled auto-merge (squash) February 22, 2026 05:52
@alextran1502 alextran1502 merged commit f0cf331 into immich-app:main Feb 22, 2026
44 checks passed
@timonrieger timonrieger deleted the feat/mobile-profile-picture branch February 22, 2026 09:46
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.

3 participants