image update for profile - #24
Conversation
📝 WalkthroughWalkthroughAdds avatar selection and upload capability to the edit-profile screen, including image picking, MIME-type normalization, a signed-upload flow (sign, PUT upload, publicUrl assignment), and preview UI. Also adjusts avatar image rendering in PostCard (contentFit and alignment class). ChangesAvatar upload feature
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant EditProfileScreen
participant UploadsAPI
participant SignedUrlStorage
participant ProfileAPI
User->>EditProfileScreen: pickAvatar()
EditProfileScreen->>EditProfileScreen: store avatarLocalUri, avatarMimeType
User->>EditProfileScreen: handleSave()
EditProfileScreen->>UploadsAPI: POST /uploads/sign (contentType)
UploadsAPI-->>EditProfileScreen: signedUrl, publicUrl
EditProfileScreen->>SignedUrlStorage: PUT file blob to signedUrl
EditProfileScreen->>ProfileAPI: updateProfile(avatar_url=publicUrl)
ProfileAPI-->>EditProfileScreen: success
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/mobile/app/edit-profile.tsx`:
- Around line 150-160: The avatar upload flow in the edit-profile screen
currently uses fetch(avatarLocalUri).blob() followed by a PUT, which can buffer
the full file in JS and is brittle in React Native. Replace that path in the
avatar upload logic with a native file-stream upload API such as
expo-file-system uploadAsync targeting the signedUrl, and preserve the existing
content type and error handling around the upload step.
- Around line 104-113: The avatar selection flow in edit-profile.tsx is falling
back to image/jpeg for unsupported picker formats without changing the
underlying file bytes. In the image-picker handling logic around
setAvatarLocalUri and setAvatarMimeType, either transcode unsupported assets to
a real JPEG before uploading or reject them instead of relabeling. Make sure the
upload path uses a contentType that matches the actual bytes so the storage
extension derived by /uploads/sign stays consistent.
In `@apps/mobile/components/PostCard.tsx`:
- Around line 59-60: The avatar image in PostCard should use a fill style
instead of contain so the fixed circular frame does not letterbox non-square
images; update the Image/Avatar usage in PostCard to use contentFit="cover" and
remove the redundant className="object-center", matching the other avatar image
patterns in this component.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ce689937-0899-4fcc-b0e5-66716b476b7e
📒 Files selected for processing (2)
apps/mobile/app/edit-profile.tsxapps/mobile/components/PostCard.tsx
| if (!result.canceled && result.assets[0]) { | ||
| const mimeType = result.assets[0].mimeType; | ||
| setAvatarLocalUri(result.assets[0].uri); | ||
| setAvatarMimeType( | ||
| mimeType && SUPPORTED_IMAGE_TYPES.has(mimeType) | ||
| ? mimeType | ||
| : "image/jpeg", | ||
| ); | ||
| } | ||
| }, [isSubmitting]); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
expo-image-picker allowsEditing mimeType heic jpeg output format iOS Android
💡 Result:
In expo-image-picker, the allowsEditing property enables a system UI for cropping (iOS and Android) and rotating (Android only) [1][2][3]. Its interaction with output formats like HEIC and JPEG depends on how the picker processes the asset [2][4]. Since Expo SDK 54, the default configuration (allowsEditing: false, videoExportPreset: 'Passthrough') is designed to return the original asset instantly, which preserves HEIC or AVIF formats if selected [2][4][5]. However, when allowsEditing is enabled (set to true), the library invokes system editing tools that typically transcode or compress the image, which often results in a JPEG output [2][4]. Key points regarding mimeType and format: 1. Format Conversion: When allowsEditing is true, the native OS cropping/editing flow frequently forces a conversion to a widely supported format like JPEG [2][4]. 2. HEIC Handling: Even when the original asset is HEIC, if the picker performs processing (such as through allowsEditing), the resulting file is commonly output as a JPEG [6][7]. 3. MimeType Accuracy: There have been documented cases where the returned asset's mimeType property incorrectly reports image/jpeg even when the underlying file remains HEIC, or when the system has not successfully transcoded it, leading to inconsistencies in how developers handle the returned URI [7][8]. 4. iOS Specifics: On iOS, cropping a.bmp image will convert it to.png [3]. For other types, editing typically results in a JPEG [6]. If your application requires a specific format (e.g., ensuring an image is JPEG), it is recommended to perform manual image processing using a library like expo-image-manipulator after the picker returns the URI, rather than relying on the native picker's editing capabilities to guarantee a specific output mimeType [1][2].
Citations:
- 1: https://docs.expo.dev/versions/latest/sdk/imagepicker/
- 2: https://docs.expo.dev/versions/unversioned/sdk/imagepicker
- 3: https://github.com/expo/expo/blob/68661df5/packages/expo-image-picker/build/ImagePicker.types.d.ts
- 4: https://docs.expo.dev/versions/v56.0.0/sdk/imagepicker
- 5: https://docs.expo.dev/versions/v57.0.0/sdk/imagepicker/
- 6: [image-picker][iOS] Use preferred representation mode to support heic expo/expo#35840
- 7: Expo-Image-Picker: Gallery Photo is HEIC but Mime is returned as JPEG When UIImagePickerPreferredAssetRepresentationMode.Current expo/expo#35714
- 8: [expo-image-picker] When Base64 is true does not return JPEG data as docs suggest expo/expo#43790
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the edit-profile screen and the upload helper it calls.
git ls-files 'apps/mobile/app/edit-profile.tsx' 'apps/mobile/**/*' | sed -n '1,120p'
echo '--- edit-profile.tsx (relevant range) ---'
cat -n apps/mobile/app/edit-profile.tsx | sed -n '1,220p'
echo '--- search for upload/content-type handling ---'
rg -n "Content-Type|contentType|mimeType|avatarMimeType|SUPPORTED_IMAGE_TYPES|upload" apps/mobile -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**'Repository: OumB2021/Journal
Length of output: 15907
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the exact upload call path and any server-side handling that might rely on MIME type.
rg -n "avatarMimeType|SUPPORT|contentType|Content-Type|putObject|signed upload|presign|presigned|multipart" . -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**'Repository: OumB2021/Journal
Length of output: 2271
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- apps/api/src/services/storageService.ts ---'
cat -n apps/api/src/services/storageService.ts
echo '--- packages/shared/src/index.ts (relevant upload schema) ---'
cat -n packages/shared/src/index.ts | sed -n '1,120p'
echo '--- apps/mobile/components/AddPostPanel.tsx (relevant picker/upload range) ---'
cat -n apps/mobile/components/AddPostPanel.tsx | sed -n '290,340p'Repository: OumB2021/Journal
Length of output: 7789
Don't relabel unsupported picker formats as JPEG without re-encoding. When mimeType falls back to image/jpeg, the app still uploads the original bytes. Since /uploads/sign derives the storage extension from contentType, a HEIC/other source can end up stored as .jpg with a JPEG content-type, which breaks the upload contract. Transcode first or reject unsupported formats.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/app/edit-profile.tsx` around lines 104 - 113, The avatar
selection flow in edit-profile.tsx is falling back to image/jpeg for unsupported
picker formats without changing the underlying file bytes. In the image-picker
handling logic around setAvatarLocalUri and setAvatarMimeType, either transcode
unsupported assets to a real JPEG before uploading or reject them instead of
relabeling. Make sure the upload path uses a contentType that matches the actual
bytes so the storage extension derived by /uploads/sign stays consistent.
| const fileRes = await fetch(avatarLocalUri); | ||
| const blob = await fileRes.blob(); | ||
|
|
||
| const uploadRes = await fetch(signedUrl, { | ||
| method: "PUT", | ||
| headers: { "Content-Type": contentType }, | ||
| body: blob, | ||
| }); | ||
| if (!uploadRes.ok) { | ||
| throw new Error("Failed to upload avatar. Please try again."); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
expo react native upload local file signed url PUT blob vs FileSystem.uploadAsync
💡 Result:
When uploading a file to a signed URL using a PUT request in an Expo/React Native environment, you should generally prefer expo-file-system's UploadTask over standard fetch/blob approaches to ensure memory efficiency and reliability, particularly for large files [1][2][3]. Key Considerations for PUT vs. Multipart Uploads: 1. expo-file-system (Recommended for Performance): - Use FileSystem.createUploadTask(url, fileUri, options) for the most robust implementation [1]. - Configure options with httpMethod: 'PUT' and uploadType: FileSystem.FileSystemUploadType.BINARY_CONTENT [4][1][5]. This method reads directly from the device filesystem, avoiding the "bridge" performance bottlenecks and potential out-of-memory (OOM) errors associated with loading large files into memory as Blobs or Base64 strings [2][3]. - UploadTask supports an onProgress callback, which is essential for user feedback during file uploads [4][1]. 2. Handling Signed URLs: - When using a signed PUT URL (e.g., for AWS S3 or Google Cloud Storage), ensure that your headers (specifically Content-Type) match exactly what was used to generate the signed URL [5][6]. - If the signed URL generation requires specific headers, pass them in the headers object within your UploadOptions [1][6]. 3. Common Pitfalls: - Versioning and Platform Consistency: Issues with FileSystem.uploadAsync sometimes emerge on specific iOS versions or SDK releases (e.g., empty body issues or background session failures) [7][8][9]. Always check the current Expo SDK documentation, as these are frequently addressed in patches [1][8]. - Binary vs. Multipart: Do not use FileSystemUploadType.MULTIPART for a simple PUT request. A PUT request typically expects a binary stream; using multipart headers will often cause the signature validation to fail because the request body format will not match what the signed URL provider expects [5][2]. Summary: For standard signed PUT uploads, use FileSystem.createUploadTask with BINARY_CONTENT. This avoids the memory overhead of converting files to Blobs and allows for native-level file streaming, which is significantly more stable for production applications [1][2][3].
Citations:
- 1: https://docs.expo.dev/versions/latest/sdk/filesystem/
- 2: https://gist.github.com/tomduncalf/17f57adf5a1343d20b3b3eee11cc7893
- 3: https://github.com/RonRadtke/react-native-blob-util
- 4: https://stackoverflow.com/questions/77787306/how-to-show-upload-progress-using-expo-filesystem-uploadasync
- 5: [SDK 50] expo-file-system -
uploadAsyncError: Unable to upload the file (cancelled) expo/expo#26750 - 6: https://gist.github.com/developius/1fa35f2192b886dfce4e7f4eaed8b923
- 7: FileSystem.uploadAsync body is empty iOS expo/expo#12636
- 8: [SDK50] [ios] [expo-file-system] uploadAsync not working anymore expo/expo#26789
- 9: File System Upload Async fails when sessionType is background and file is larger expo/expo#16453
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the edit-profile screen around the upload logic
sed -n '120,190p' apps/mobile/app/edit-profile.tsx
# Check Expo-related dependencies to see whether expo-file-system is available
cat package.json 2>/dev/null || true
cat apps/mobile/package.json 2>/dev/null || true
# Search for other upload implementations or file-system usage in the mobile app
rg -n "uploadAsync|createUploadTask|FileSystem\.|fetch\(.*\.blob\(\)|blob\(\)" apps/mobile -t ts -t tsxRepository: OumB2021/Journal
Length of output: 4317
🌐 Web query:
React Native Hermes fetch local file uri blob upload signed url reliability expo-file-system uploadAsync
💡 Result:
For reliable file uploads in React Native using Hermes, direct standard fetch-based Blob uploads are often problematic due to inconsistencies in how local file system URIs are handled across Android and iOS [1][2]. Reliability Recommendations: 1. Use specialized libraries for production-grade uploads. Libraries like react-native-blob-util provide a robust, cross-platform mechanism to wrap local file paths and stream them directly to a destination, avoiding the memory overhead of converting files to Blobs or Base64 [3]. This is the industry-standard approach for handling S3 pre-signed URLs reliably [4]. 2. Expo File System's uploadAsync is designed for native efficiency but has historically faced platform-specific bugs, particularly with multipart uploads on iOS and background session types [5][6][7][8]. If you choose this route, ensure you are on the latest Expo SDK version, as significant fixes for iOS multipart uploads were implemented in recent SDK releases [6][7]. 3. Avoid the common "fetch blob" anti-pattern. Many older tutorials suggest creating a Blob via XMLHttpRequest to then use in a standard fetch PUT request [4][2]. This approach often fails on Android (e.g., status code 0 errors) or causes high memory pressure [2][9]. Native fetch in React Native does not natively stream files from the local file system in the same way it handles standard network requests [1]. 4. Prefer direct file system access. When using pre-signed URLs, the most reliable pattern is to stream the file directly from its local path [4][3]. Libraries like react-native-blob-util allow you to use a.wrap function (e.g., RNBlobUtil.wrap(filePath)) to identify the file to the native layer, allowing the platform's networking stack to handle the file upload efficiently without loading the entire content into JavaScript memory [3]. 5. Workflow best practices: - Always validate the existence of the file URI before initiating an upload [9]. - If using pre-signed URLs, ensure your backend correctly handles the Content-Type and CORS settings to match what the mobile app sends [4][10]. - For large files, avoid background upload types if possible, as they are susceptible to connection loss and have platform-specific constraints on file size and session persistence [8]. In summary, for maximum reliability, leverage react-native-blob-util for direct file streaming [3]. If you must use Expo's ecosystem, prefer FileSystem.uploadAsync but be prepared to manage potential edge cases in multipart handling, and avoid the standard fetch-to-blob conversion pattern entirely [2][8][9].
Citations:
- 1: React Native 0.82 Fetch Issues fetching local file react/react-native#54626
- 2: [image-picker][Android] fetch response provided status code 0 expo/expo#23917
- 3: https://github.com/joltup/react-native-fetch-blob
- 4: https://stackoverflow.com/questions/37760355/react-native-upload-to-s3-with-presigned-url
- 5: [SDK 50] expo-file-system -
uploadAsyncError: Unable to upload the file (cancelled) expo/expo#26750 - 6: [ios][file-system] Fix uploads expo/expo#26880
- 7: [SDK50] [ios] [expo-file-system] uploadAsync not working anymore expo/expo#26789
- 8: File System Upload Async fails when sessionType is background and file is larger expo/expo#16453
- 9: https://capgo.app/blog/expo-image-picker/
- 10: https://github.com/jacantwell/s3-mobile
Use a native file-stream upload here The fetch(avatarLocalUri).blob() → PUT path is fragile in React Native and can buffer the whole file in JS; use a native upload API such as expo-file-system’s uploadAsync for this signed URL instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/mobile/app/edit-profile.tsx` around lines 150 - 160, The avatar upload
flow in the edit-profile screen currently uses fetch(avatarLocalUri).blob()
followed by a PUT, which can buffer the full file in JS and is brittle in React
Native. Replace that path in the avatar upload logic with a native file-stream
upload API such as expo-file-system uploadAsync targeting the signedUrl, and
preserve the existing content type and error handling around the upload step.
Summary by CodeRabbit