Implementing Image Sharing in Mobile Chat
User taps paperclip, selects photo from gallery — and waits. If nothing happens in that time, they tap again. Classic scenario when image upload is done without queue and progress indicator. Next — duplicates in chat, crash on slow network, 1-star review.
What Goes Wrong With Naive Implementation
Most common mistake — upload original directly. Modern smartphone photos weigh 4–12 MB. Sending it to chat via multipart/form-data without prior compression means: long wait, UI freeze on main thread (if compression happens there too), duplication on retry after timeout.
On iOS typical implementation through PHPickerViewController (iOS 14+) gives NSItemProvider, from which you need to asynchronously get UIImage. Doing this synchronously in completion handler and immediately calling ImageIO for resize — interface hangs ~300 ms on iPhone 12, even longer on SE 2nd gen. Right way: get data in background through loadObject(ofClass:), then dispatch to DispatchQueue.global(qos: .userInitiated) for compression via vImage or UIGraphicsImageRenderer with target size for recipient's screen.
On Android similar problem with ActivityResultContracts.GetContent(): decoding Bitmap on main thread through BitmapFactory.decodeStream() without inSampleSize — OutOfMemoryError on 2GB RAM devices when selecting multiple photos in a row.
Building Reliable Pipeline
Full chain: selection → validation → compression → upload with progress → save link → display thumbnail.
Selection and validation. On iOS — PHPickerViewController with filter: .images, selection limit through selectionLimit. Check MIME type through UTType before loading data. On Android — PhotoPicker API (Android 13+) or Intent(Intent.ACTION_PICK) for older; check through ContentResolver.getType().
Compression. Goal — no more than 1 MB for chat preview. On iOS: UIGraphicsImageRenderer with target size 1280×1280, jpegData(compressionQuality: 0.75). On Android: Bitmap.createScaledBitmap() + compress(Bitmap.CompressFormat.JPEG, 80, outputStream). In Flutter use flutter_image_compress — it calls native codec, doesn't block Dart isolate.
Upload. Multipart upload via URLSession.uploadTask(with:from:) on iOS with delegate urlSession(_:task:didSendBodyData:) for progress. On Android — OkHttp with RequestBody.create() and custom CountingRequestBody. In React Native convenient axios with onUploadProgress, but remember: progress event fires on JS thread, state update needs debounce.
Optimistic UI. Show thumbnail immediately after selection, "loading" status — while upload proceeds. If request failed — don't delete message, show retry button. Each message needs local localId and status (pending / sent / failed).
Fullscreen view. On iOS — UIScrollView + UIImageView with pinch-to-zoom through UIPinchGestureRecognizer. Lazy load original on open through SDWebImage or Kingfisher. On Android — PhotoView library or ZoomableImageView from coil + accompanist.
Storage and CDN
Images don't go in database. Upload to S3-compatible storage (AWS S3, Cloudflare R2, MinIO), save only URL to chat. Generate thumbnail on server via Lambda/Cloud Function on upload — saves client from recompressing when displaying message list.
Presigned URL (presigned URL) with TTL 1–24 hours — mandatory if chat is private. Cache on client through NSCache (iOS) or DiskLruCache (Android).
Process and Timeframe
Basic implementation (selection, compression, upload, thumbnail, fullscreen view) — 2–3 days with ready backend. Multi-select (up to 10 photos), upload queue with pause/resume, GIF support — another 1–2 days. Cost calculated individually after analyzing requirements.







