diff --git a/docs.json b/docs.json index ad3c1f53d..cf15961f6 100644 --- a/docs.json +++ b/docs.json @@ -4150,6 +4150,7 @@ "pages": [ "sdk/android/v5/messaging-overview", "sdk/android/v5/send-message", + "sdk/android/v5/upload-files", "sdk/android/v5/receive-messages", "sdk/android/v5/additional-message-filtering", "sdk/android/v5/retrieve-conversations", diff --git a/images/multi_attachments_mobile.png b/images/multi_attachments_mobile.png new file mode 100644 index 000000000..7cde2441f Binary files /dev/null and b/images/multi_attachments_mobile.png differ diff --git a/sdk/android/v5/send-message.mdx b/sdk/android/v5/send-message.mdx index b9d66c5c3..aa799028a 100644 --- a/sdk/android/v5/send-message.mdx +++ b/sdk/android/v5/send-message.mdx @@ -503,9 +503,15 @@ If you wish to send a caption or some text along with the Media Message, you can ## Multiple Attachments in a Media Message -Starting version 3.0.9 & above the SDK supports sending of multiple attachments in a single media message. As in case for single attachment in a media message, there are two ways you can send Media Messages using the CometChat SDK: +The SDK supports sending multiple attachments in a single media message. A media message can carry up to a configurable maximum number of attachments (default **10**, controlled by the `file.count.max` app setting). `sendMediaMessage()` calls back `onError` with `ERR_FILE_COUNT_EXCEEDED` if you exceed it — read the current limit at runtime with `CometChat.getMaxAttachmentCount()`. -1. **By providing an array of files:** You can now share a List of files while creating an object of the MediaMessage class. When the media message is sent using the `sendMediaMessage()` method, the files are uploaded to the CometChat servers & the URL of the files are sent in the success response of the `sendMediaMessage()` method. + +**Recommended: upload first, then send.** For a real composer — where you want a progress bar per file, and the ability to remove or retry individual files — create an upload request with [`CometChat.createUploadFileRequest()`](/sdk/android/v5/upload-files), upload your files through it, then collect the resulting `Attachment`s (`request.getAttachments()`) and attach them here with `setAttachments()`. This decouples the (slow, cancellable) upload from the (instant) send. See [Upload Files & Send Attachments](/sdk/android/v5/upload-files) for the full flow. + + +There are two ways to send a multi-attachment media message using the CometChat SDK: + +1. **By providing a list of files:** You can share a `List` of files while creating an object of the MediaMessage class. When the media message is sent using the `sendMediaMessage()` method, the files are uploaded to the CometChat servers & the URL of the files is sent in the success response of the `sendMediaMessage()` method. This is the simplest path, but it gives no upload progress and no per-file cancel/retry — for that, [upload first](/sdk/android/v5/upload-files). @@ -618,7 +624,7 @@ The `MediaMessage` class constructor takes the following parameters: | **messageType** | The type of the message that needs to be sent which in this case can be:
1. `CometChatConstants.MESSAGE_TYPE_IMAGE`
2. `CometChatConstants.MESSAGE_TYPE_VIDEO`
3. `CometChatConstants.MESSAGE_TYPE_AUDIO`
4. `CometChatConstants.MESSAGE_TYPE_FILE` | | **receiverType** | The type of the receiver to whom the message is to be sent.
1. `CometChatConstants.RECEIVER_TYPE_USER`
2. `CometChatConstants.RECEIVER_TYPE_GROUP` | -2. **By providing the URL of the multiple files:** The second way to send multiple attachments using the CometChat SDK is to provide the SDK with the URL of multiple files that is hosted on your servers or any cloud storage. To achieve this you will have to make use of the `Attachment` class. For more information, you can refer to the below code snippet: +2. **By providing the URL of the multiple files:** The second way to send multiple attachments using the CometChat SDK is to provide the SDK with `Attachment` objects that already point at files hosted on your servers or any cloud storage. This is also the path the [upload-then-send flow](/sdk/android/v5/upload-files) uses — the `Attachment`s from an upload request's `getAttachments()` are ready to pass straight to `setAttachments()`. For more information, you can refer to the below code snippet: diff --git a/sdk/android/v5/upload-files.mdx b/sdk/android/v5/upload-files.mdx new file mode 100644 index 000000000..bd3b93287 --- /dev/null +++ b/sdk/android/v5/upload-files.mdx @@ -0,0 +1,578 @@ +--- +title: "Upload Files & Send Attachments" +sidebarTitle: "Upload Files" +description: "Upload files directly to storage with per-file progress, remove, and retry through an UploadFileRequest — then send them as one or more media messages with multiple attachments." +--- + +`CometChat.createUploadFileRequest(receiverId, receiverType)` returns an **`UploadFileRequest`** — the entry point for uploading files **directly to storage** with **per-file progress, success, and failure**. Upload is **decoupled from sending**: each uploaded file yields an `Attachment` (carrying a hosted URL), which you then attach to a `MediaMessage` and send with [`sendMediaMessage()`](/sdk/android/v5/send-message#media-message). + +A request object is scoped to **one destination** (`receiverId` / `receiverType`) and **one upload batch**. This is the recommended way to build a **multi-attachment composer**: create a request, upload a batch of files, show a progress bar per file, let the user remove or retry individual files, then send them as a single media message with multiple attachments (or split across several). + + +**Why upload separately instead of passing files to `sendMediaMessage()`?** + +The classic path (passing a `File`, or a list of files, straight to the `MediaMessage` constructor) uploads and sends in one blocking call — you get no progress, no per-file remove, and no per-file retry. An `UploadFileRequest` moves the upload out of the send call so you can drive a rich composer UI, then send instantly because the files are already hosted. + + +## The upload-then-send flow + + + + Call `CometChat.createUploadFileRequest(receiverId, receiverType)`. The **recipient** is required — the server uses it to apply role- and scope-based access control before issuing upload URLs. + + + Call `request.uploadAttachments(items, listener)`, where each `UploadFileItem` pairs a file with an **app-supplied `fileId`** (required) that is echoed back on every event, so you can map callbacks to your UI rows. Validation, presigning, and the byte transfer run asynchronously and report through the listener. + + + Your `UploadFileListener` receives `onFileProgress` per file, then `onFileUploaded` (success), `onFileError` (rejected — not retryable), or `onFileFailure` (failed — retryable). Use `request.removeAttachment()` or re-upload the same `fileId` as the user acts. + + + Each `onFileUploaded` hands you an `Attachment` with a hosted URL. You can also read them from the request at any time with `request.getAttachments()` / `request.getAttachmentsByType()`. + + + Put the attachments on a `MediaMessage` with `setAttachments(...)` and call `sendMediaMessage()`. Because the attachments already have URLs, the message is sent as JSON — no re-upload. + + + Call `request.clearAll()` after a successful send (or to abandon the composer) to release the batch from memory. + + + +## Create an upload request + + + +```java +String receiverId = "cometchat-uid-1"; + +UploadFileRequest request = CometChat.createUploadFileRequest( + receiverId, + CometChatConstants.RECEIVER_TYPE_USER +); +``` + + + +```kotlin +val receiverId = "cometchat-uid-1" + +val request = CometChat.createUploadFileRequest( + receiverId, + CometChatConstants.RECEIVER_TYPE_USER +) +``` + + + +`createUploadFileRequest()` accepts: + +| Parameter | Type | Description | Required | +| -------------- | -------- | ---------------------------------------------------------------------------------------- | -------- | +| `receiverId` | `String` | UID of the user or GUID of the group the files will be sent to. | Yes | +| `receiverType` | `String` | `CometChatConstants.RECEIVER_TYPE_USER` or `CometChatConstants.RECEIVER_TYPE_GROUP`. | Yes | + + +**The recipient must match the message you'll eventually send.** `receiverId` / `receiverType` are sent to the upload-authorization (presign) endpoint, which enforces the sender's **role- and scope-based access control** for that conversation *before* any bytes transfer. If the server declines a file (billing, plan, or content-type policy), it is **rejected** via `onFileError` — not retryable. Pass the same recipient you'll set on the `MediaMessage` at send time. + + +## Upload files + +Build an `UploadFileListener` and call `uploadAttachments()` (or `uploadAttachment()` for a single file). Each `UploadFileItem` pairs a file with a **required, app-supplied `fileId`**. An item is backed by either a `File` or a content `Uri` — Uri-backed items (from the Android document/media pickers) need the `Context` overload so the SDK can resolve each Uri's name, size, and mime type. + + + +```java +// Pair each file with an id you own, so you can map events back to your UI. +List items = new ArrayList<>(); +items.add(new UploadFileItem(UUID.randomUUID().toString(), new File("/storage/emulated/0/Download/photo-1.jpg"))); +items.add(new UploadFileItem(UUID.randomUUID().toString(), new File("/storage/emulated/0/Download/photo-2.jpg"))); + +UploadFileListener listener = new UploadFileListener() { + @Override + public void onFileProgress(String fileId, long loaded, long total, int percent) { + Log.d(TAG, fileId + ": " + percent + "%"); + } + + @Override + public void onFileUploaded(String fileId, Attachment attachment) { + Log.d(TAG, fileId + " uploaded -> " + attachment.getFileUrl()); + } + + @Override + public void onFileError(String fileId, CometChatException error) { + Log.d(TAG, fileId + " rejected (not retryable): " + error.getCode()); + } + + @Override + public void onFileFailure(String fileId, CometChatException error) { + Log.d(TAG, fileId + " failed (retryable): " + error.getCode()); + } + + @Override + public void onComplete(UploadResult result) { + Log.d(TAG, "Batch settled: " + result); + } +}; + +request.uploadAttachments(items, listener); +``` + + + +```kotlin +// Pair each file with an id you own, so you can map events back to your UI. +val items = listOf( + UploadFileItem(UUID.randomUUID().toString(), File("/storage/emulated/0/Download/photo-1.jpg")), + UploadFileItem(UUID.randomUUID().toString(), File("/storage/emulated/0/Download/photo-2.jpg")) +) + +val listener = object : UploadFileListener() { + override fun onFileProgress(fileId: String, loaded: Long, total: Long, percent: Int) { + Log.d(TAG, "$fileId: $percent%") + } + + override fun onFileUploaded(fileId: String, attachment: Attachment) { + Log.d(TAG, "$fileId uploaded -> ${attachment.fileUrl}") + } + + override fun onFileError(fileId: String, error: CometChatException) { + Log.d(TAG, "$fileId rejected (not retryable): ${error.code}") + } + + override fun onFileFailure(fileId: String, error: CometChatException) { + Log.d(TAG, "$fileId failed (retryable): ${error.code}") + } + + override fun onComplete(result: UploadResult) { + Log.d(TAG, "Batch settled: $result") + } +} + +request.uploadAttachments(items, listener) +``` + + + +For files picked through the system pickers (content `Uri`s), use the `Context` overloads: + + + +```java +List items = new ArrayList<>(); +for (Uri uri : pickedUris) { + items.add(new UploadFileItem(UUID.randomUUID().toString(), uri)); +} +request.uploadAttachments(context, items, listener); +``` + + + +```kotlin +val items = pickedUris.map { uri -> UploadFileItem(UUID.randomUUID().toString(), uri) } +request.uploadAttachments(context, items, listener) +``` + + + +| Method | Description | +| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| `uploadAttachments(items, listener)` | Upload multiple `File`-backed `UploadFileItem`s. `listener` receives events for these files. | +| `uploadAttachments(context, items, listener)` | Same, but with a `Context` so content-`Uri`-backed items can be read. | +| `uploadAttachment(fileId, file, listener)` | Upload a single `File` under `fileId`. | +| `uploadAttachment(context, fileId, uri, listener)` | Upload a single content `Uri` under `fileId`. | + + +**`fileId` is app-supplied and required.** The SDK does not generate one — you provide a stable id per file (echoed back unchanged on every event) so you can line each file up with its UI row. The app owns id uniqueness within the batch: **re-using a `fileId` replaces that entry** with a fresh queued upload (aborting the old one if it was in flight) — that is also the [retry path](#remove-retry--clear). `Uri`-backed items uploaded through an overload without a `Context` are rejected as invalid. + +Validation, presigning, and the byte transfer all run **asynchronously** after `uploadAttachments()` returns — track outcomes through the listener. + + +## The UploadFileListener + +`UploadFileListener` is an abstract class — override only the callbacks you need. Unlike [`MessageListener`](/sdk/android/v5/receive-messages) or `CallListener`, it is **not** registered globally with a string id; it lives for the duration of the upload batch. + +| Callback | Signature | Fires when | +| ---------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `onFileProgress` | `(fileId, loaded, total, percent)` | Transfer progress for a file. Throttled internally, so it is safe to update the UI on every event. | +| `onFileUploaded` | `(fileId, attachment)` | A file finished uploading. `attachment` is ready to include via `setAttachments(...)`. | +| `onFileError` | `(fileId, error)` | A file was **rejected** — **not retryable** (fix the input or permissions). | +| `onFileFailure` | `(fileId, error)` | A file's transfer **failed** — **retryable** by re-uploading the same `fileId`. | +| `onComplete` | `(result)` | The batch drained (no files in flight). Delivers an aggregate [`UploadResult`](#uploadresult). | + + +**Rejected vs. failed — the key distinction.** Exactly one of the two fires per non-successful file. `onFileError` (rejected) means the request itself is unacceptable — an invalid or unreadable file, the size or count limit breached, or a server-side authorization/policy denial at presign time. Retrying won't help; the user must remove or replace the file. `onFileFailure` (failed) means a transient transport problem — network drop, a storage error, an expired upload URL, or a stalled upload. These **can** be retried by re-uploading the same `fileId` through the request. + + +### UploadResult + +`onComplete` receives the batch's settled state. It fires **each time** the batch drains — including after more files are added and it drains again — and always reflects the **whole batch's** current state, so treat it idempotently (recompute from `result`; *set* your Send-enabled flag, don't toggle it). + +| Method | Returns | Description | +| ----------------- | ------------------------------ | ---------------------------------------------------------------------------------- | +| `getBatchId()` | `String` | The upload batch this result belongs to. | +| `getSuccessful()` | `List` | Uploaded files — each pairs `getFileId()` with its ready `getAttachment()`. | +| `getRejected()` | `List` | Files reported via `onFileError` — each pairs `getFileId()` with `getError()`. **Not retryable.** | +| `getFailed()` | `List` | Files reported via `onFileFailure` — **retryable** by re-uploading the same `fileId`. | + +## Configuring the request + +Chainable setters let you configure the batch before (or between) uploads: + + + +```java +UploadFileRequest request = CometChat.createUploadFileRequest(receiverId, CometChatConstants.RECEIVER_TYPE_USER) + .setConcurrency(3) // upload up to 3 files at once (default 1, sequential) + .setParentMessageId(parentId); // mark this batch as belonging to a thread + +String batchId = request.getBatchId(); // auto-generated UUID unless you set one +``` + + + +```kotlin +val request = CometChat.createUploadFileRequest(receiverId, CometChatConstants.RECEIVER_TYPE_USER) + .setConcurrency(3) // upload up to 3 files at once (default 1, sequential) + .setParentMessageId(parentId) // mark this batch as belonging to a thread + +val batchId = request.batchId // auto-generated UUID unless you set one +``` + + + +| Method | Description | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `setConcurrency(count)` | How many files upload simultaneously. Default `1` (sequential). Applies to queued files from the next drain; uploads already in flight are never interrupted. | +| `setBatchId(batchId)` | Set the batch id all files share. Optional — the request auto-generates a UUID. **Locked once set / once uploading starts.** | +| `getBatchId()` | The effective batch id (app-set or auto-generated). | +| `setParentMessageId(id)` | Mark the batch as belonging to a thread — sent to the presign endpoint with every file. Omit for a top-level message. | + + +Parallel uploads of large files over cellular data can saturate the connection and stall every file — raise `setConcurrency` only on known-fast networks (ideally network-aware: higher on Wi-Fi, `1` on cellular). + + + +Each request owns exactly one batch. For separate destinations (e.g. a main conversation and a thread), create a **separate `UploadFileRequest`** for each — their file ids never cross. + + +### Adding more files to the batch + +To add files incrementally (e.g. the user picks more while earlier uploads are still running), just call `uploadAttachments()` again **on the same request** — they join the same batch, and `onComplete` refires when the batch next drains. + +## Per-call and global listeners + +There are two listener scopes, and events fire on **both** (per-call first): + +- **Per-call listener** — the `listener` you pass to `uploadAttachment()` / `uploadAttachments()`. It receives events only for the files in *that* call. +- **Global batch listener** — registered with `request.addUploadListener(listener)`. It receives events for **every** file across all upload calls on the request. There is a **single global slot**: a later `addUploadListener()` replaces the previous one; `removeUploadListener()` clears it. + + + +```java +// One listener for the whole batch, regardless of how many upload calls you make. +request.addUploadListener(new UploadFileListener() { + @Override + public void onFileProgress(String fileId, long loaded, long total, int percent) { + updateRow(fileId, percent); + } + + @Override + public void onFileUploaded(String fileId, Attachment attachment) { + markUploaded(fileId, attachment); + } + + @Override + public void onComplete(UploadResult result) { + Log.d(TAG, "Whole batch settled: " + result); + } +}); + +// Later (e.g. on composer unmount): +request.removeUploadListener(); +``` + + + +```kotlin +// One listener for the whole batch, regardless of how many upload calls you make. +request.addUploadListener(object : UploadFileListener() { + override fun onFileProgress(fileId: String, loaded: Long, total: Long, percent: Int) { + updateRow(fileId, percent) + } + + override fun onFileUploaded(fileId: String, attachment: Attachment) { + markUploaded(fileId, attachment) + } + + override fun onComplete(result: UploadResult) { + Log.d(TAG, "Whole batch settled: $result") + } +}) + +// Later (e.g. on composer unmount): +request.removeUploadListener() +``` + + + +## Reading the batch + +Query the request's current state at any time — useful when the user hits "send": + +| Method | Returns | Notes | +| ---------------------------- | -------------------- | -------------------------------------------------------------------------------------------------- | +| `getAttachment(fileId)` | `Attachment` or `null` | The uploaded attachment for `fileId`, or `null` if unknown or **not yet uploaded**. | +| `getAttachments()` | `List` | All **uploaded** attachments in the batch, in the order the files were added. | +| `getAttachmentsByType(type)` | `List` | Uploaded attachments of one kind (`CometChatConstants.MESSAGE_TYPE_IMAGE` / `_VIDEO` / `_AUDIO` / `_FILE`, derived from each attachment's mime type) — always a list. | +| `getAttachmentCount()` | `int` | Total files in the batch in **any** state (in-progress, failed, rejected, or uploaded). | +| `getStatus()` | `UploadStatus` | `IN_PROGRESS` while any file is still uploading, else `IDLE` (an empty batch is `IDLE`). `onComplete` fires exactly at the transition to `IDLE`. | + + +The attachment getters (`getAttachment`, `getAttachments`, `getAttachmentsByType`) return **only uploaded** files, so they're safe to hand straight to `setAttachments()`. `getAttachmentCount()` counts every file regardless of state, so you can compare it against `getAttachments().size()` to see how many are still pending. + + +## Remove, retry & clear + + + +```java +// Remove one file: aborts its in-flight upload (silently — no onFileFailure), +// or drops it from the batch if it has already uploaded. +request.removeAttachment(fileId); + +// Retry one FAILED file: re-upload it under the SAME fileId. The fresh entry +// replaces the failed one and re-presigns automatically if the URL expired. +request.uploadAttachment(fileId, file, listener); + +// Release the whole batch from memory, aborting anything still in flight. +// Call this after a successful send, or to abandon the composer. +request.clearAll(); +``` + + + +```kotlin +// Remove one file: aborts its in-flight upload (silently — no onFileFailure), +// or drops it from the batch if it has already uploaded. +request.removeAttachment(fileId) + +// Retry one FAILED file: re-upload it under the SAME fileId. The fresh entry +// replaces the failed one and re-presigns automatically if the URL expired. +request.uploadAttachment(fileId, file, listener) + +// Release the whole batch from memory, aborting anything still in flight. +// Call this after a successful send, or to abandon the composer. +request.clearAll() +``` + + + +| Action | Call | Behavior | +| ---------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| **Remove** | `removeAttachment(fileId)` | Removes one file from the batch. If it's still uploading, the transfer is aborted **silently** (no `onFileFailure`); if it already uploaded, it's dropped from the set (the unreferenced storage object is cleaned up server-side). | +| **Retry** | `uploadAttachment(fileId, file, listener)` | Re-uploading the **same `fileId`** replaces the failed entry with a fresh one and re-presigns automatically if the earlier upload URL expired. Don't retry rejected files — the same check would just fail again. | +| **Clear** | `clearAll()` | Aborts any in-flight uploads and drops the whole batch from SDK memory. | + + +There is no auto-clear on send or logout — call `clearAll()` yourself after a successful send (or when abandoning the composer) so the batch doesn't linger in memory. + + +## Send the uploaded files as a media message + +Once your files are uploaded, collect their `Attachment`s (from `request.getAttachments()` or from `result.getSuccessful()` in `onComplete`), set them on a `MediaMessage` built with the **no-file constructor**, and send. One batch can go out as a single multi-attachment message, or you can split the attachments across several messages — e.g. one message per media type using `getAttachmentsByType()`, reusing the same batch id across the sends. + + + +```java +String receiverId = "cometchat-uid-1"; + +UploadFileRequest request = CometChat.createUploadFileRequest(receiverId, CometChatConstants.RECEIVER_TYPE_USER) + .setConcurrency(3); + +UploadFileListener listener = new UploadFileListener() { + @Override + public void onFileProgress(String fileId, long loaded, long total, int percent) { + updateRow(fileId, percent); + } + + @Override + public void onFileFailure(String fileId, CometChatException error) { + markRetryable(fileId, error); // offer Retry → re-upload the same fileId + } + + @Override + public void onFileError(String fileId, CometChatException error) { + markRejected(fileId, error); // offer Remove/Replace + } + + @Override + public void onComplete(UploadResult result) { + List attachments = request.getAttachments(); + if (attachments.isEmpty()) return; + + MediaMessage mediaMessage = new MediaMessage( + receiverId, + CometChatConstants.MESSAGE_TYPE_IMAGE, // no raw file — attachments already carry hosted URLs + CometChatConstants.RECEIVER_TYPE_USER + ); + mediaMessage.setAttachments(new ArrayList<>(attachments)); + mediaMessage.setCaption("Trip photos"); + mediaMessage.setMuid(request.getBatchId()); // reconcile the message with its upload batch + + CometChat.sendMediaMessage(mediaMessage, new CometChat.CallbackListener() { + @Override + public void onSuccess(MediaMessage message) { + Log.d(TAG, "Media message sent successfully"); + request.clearAll(); // release the batch + } + + @Override + public void onError(CometChatException e) { + Log.d(TAG, "Media message sending failed: " + e.getMessage()); + } + }); + } +}; + +List items = new ArrayList<>(); +for (File f : pickedFiles) { + items.add(new UploadFileItem(UUID.randomUUID().toString(), f)); +} +request.uploadAttachments(items, listener); +``` + + + +```kotlin +val receiverId = "cometchat-uid-1" + +val request = CometChat.createUploadFileRequest(receiverId, CometChatConstants.RECEIVER_TYPE_USER) + .setConcurrency(3) + +val listener = object : UploadFileListener() { + override fun onFileProgress(fileId: String, loaded: Long, total: Long, percent: Int) { + updateRow(fileId, percent) + } + + override fun onFileFailure(fileId: String, error: CometChatException) { + markRetryable(fileId, error) // offer Retry → re-upload the same fileId + } + + override fun onFileError(fileId: String, error: CometChatException) { + markRejected(fileId, error) // offer Remove/Replace + } + + override fun onComplete(result: UploadResult) { + val attachments = request.getAttachments() + if (attachments.isEmpty()) return + + val mediaMessage = MediaMessage( + receiverId, + CometChatConstants.MESSAGE_TYPE_IMAGE, // no raw file — attachments already carry hosted URLs + CometChatConstants.RECEIVER_TYPE_USER + ) + mediaMessage.attachments = ArrayList(attachments) + mediaMessage.caption = "Trip photos" + mediaMessage.muid = request.batchId // reconcile the message with its upload batch + + CometChat.sendMediaMessage(mediaMessage, object : CometChat.CallbackListener() { + override fun onSuccess(message: MediaMessage) { + Log.d(TAG, "Media message sent successfully") + request.clearAll() // release the batch + } + + override fun onError(e: CometChatException) { + Log.d(TAG, "Media message sending failed: ${e.message}") + } + }) + } +} + +val items = pickedFiles.map { f -> UploadFileItem(UUID.randomUUID().toString(), f) } +request.uploadAttachments(items, listener) +``` + + + + +Build the message with the constructor that takes **no files** — `MediaMessage(receiverId, messageType, receiverType)` — when sending pre-uploaded attachments. If you also pass `File` objects, the SDK re-uploads them on send, which defeats the upload-first flow. + + + +A message's attachments should all match the message's own type — send mixed picks as one message per kind (images → videos → audios → files) using `getAttachmentsByType()`. `sendMediaMessage()` also enforces the **maximum attachments per message** (see [Limits](#limits)); if a batch has more successful uploads than the limit allows, split them across multiple `MediaMessage`s. See [Multiple Attachments in a Media Message](/sdk/android/v5/send-message#multiple-attachments-in-a-media-message). + + +## Limits + +Two limits guard the flow, each read from your app settings (configured in the CometChat dashboard, with a built-in fallback): + +| Limit | Enforced in | App setting | Default | On breach | +| --------------------------- | ---------------------------------------- | ---------------- | -------- | ------------------------------------------------------------------------- | +| **Per-file size** | `uploadAttachments()` | `file.size.max` | 100 MB | That file is rejected via `onFileError` with `ERR_FILE_SIZE_EXCEEDED`. | +| **Attachments per message** | `uploadAttachments()` and `sendMediaMessage()` | `file.count.max` | 10 | At upload time, files beyond the batch's remaining capacity are rejected via `onFileError` with `ERR_FILE_COUNT_EXCEEDED` (accepted in input order); at send time, `sendMediaMessage()` calls back `onError` with the same code if the message carries more attachments than allowed. | + +Read the current limits at runtime — useful for capping your picker's selection and pre-validating file sizes before uploading: + + + +```java +int maxFiles = CometChat.getMaxAttachmentCount(); // file.count.max — defaults to 10 +long maxBytes = CometChat.getMaxFileSize(); // file.size.max — defaults to 104857600 (100 MB) +``` + + + +```kotlin +val maxFiles = CometChat.getMaxAttachmentCount() // file.count.max — defaults to 10 +val maxBytes = CometChat.getMaxFileSize() // file.size.max — defaults to 104857600 (100 MB) +``` + + + + +An oversized or over-count file only rejects **that file** — the rest of the batch continues uploading. Removing a rejected file frees no capacity (rejected entries don't consume it); removing a queued, failed, or uploaded file does. + + +## Reliability behavior + +The SDK handles a few transport edge cases for you: + +- **Stalled uploads** — if a file makes no progress for 30 seconds, its upload is aborted and reported through `onFileFailure` with `ERR_UPLOAD_STALLED`. Retry it by re-uploading the same `fileId`. +- **Expired upload URLs** — each file's pre-signed upload URL has a limited validity. If it expires before the transfer finishes, the file fails with `ERR_PRESIGNED_URL_EXPIRED`; re-uploading the same `fileId` requests a fresh URL automatically, so retries keep working even after a long delay. +- **Storage errors** — if storage rejects the upload or the network drops, the failure surfaces through `onFileFailure` with `ERR_S3_UPLOAD_FAILED` and, where available, the transport's own message. + +## Error handling + +Every error delivered to `onFileError` / `onFileFailure` (and `onError` callbacks from `sendMediaMessage()`) is a `CometChatException` — read `getCode()` to branch: + +| Code | Surfaced via | Retryable | Meaning | +| --------------------------- | -------------------- | --------- | ----------------------------------------------------------------------- | +| `ERR_INVALID_FILE_OBJECT` | `onFileError` | No | The file is invalid, unreadable, missing a `fileId`, or its size can't be determined. | +| `ERR_FILE_SIZE_EXCEEDED` | `onFileError` | No | The file is larger than `file.size.max`. | +| `ERR_FILE_COUNT_EXCEEDED` | `onFileError`, `sendMediaMessage()` | No | More files than `file.count.max` allows — at upload time (batch capacity) or send time (attachments on the message). | +| `ERR_PRESIGN_REJECTED` | `onFileError` | No | The server refused an upload URL (billing, plan, or content-type policy). | +| `ERR_PRESIGN_FAILED` | `onFileFailure` | Yes | Failed to obtain an upload URL for the file. | +| `ERR_S3_UPLOAD_FAILED` | `onFileFailure` | Yes | The file failed to upload to storage, or the transport errored. | +| `ERR_PRESIGNED_URL_EXPIRED` | `onFileFailure` | Yes | The upload URL expired before the file finished uploading (a retry re-presigns automatically). | +| `ERR_UPLOAD_STALLED` | `onFileFailure` | Yes | No progress for 30s; the upload was aborted. | +| `ERR_UPLOAD_CANCELLED` | — | — | The upload was cancelled via `removeAttachment()` / `clearAll()` (recorded in `UploadResult`, not delivered as a callback). | + + +Server-side rejections at presign time (e.g. policy or plan denials) carry the server's own code and message where one is provided — `ERR_PRESIGN_REJECTED` is the fallback classification. + + +## Next Steps + + + + Send text, media, and custom messages + + + Send several attachments in one media message + + + Listen for incoming messages in real-time + + + Upload into a thread with setParentMessageId + + diff --git a/ui-kit/android/v6/core-features.mdx b/ui-kit/android/v6/core-features.mdx index ad33a43b5..89386b4f3 100644 --- a/ui-kit/android/v6/core-features.mdx +++ b/ui-kit/android/v6/core-features.mdx @@ -32,16 +32,27 @@ Real-time text messaging — users can send and receive instant messages. ## Media Sharing -Share images, videos, audio files, and documents within conversations. +Share multiple images, videos, audio files, and documents in a single send. Tapping the attachment button opens a multi-select picker; the picks stage in a preview tray above the input box with per-file upload progress, and send together with an optional caption — all out of the box. - + Multi-attachment message list showing an image grid, a file card, and an audio bubble with caption, plus the composer staging tray with mixed image, video, and file tiles | Component | Role | | --- | --- | -| [CometChatMessageComposer](/ui-kit/android/v6/message-composer) | Provides an action sheet with options for sharing media files. | -| [CometChatMessageList](/ui-kit/android/v6/message-list) | Renders media message bubbles — [Image](/ui-kit/android/v6/message-bubble-styling#image-bubble), [File](/ui-kit/android/v6/message-bubble-styling#file-bubble), [Audio](/ui-kit/android/v6/message-bubble-styling#audio-bubble), [Video](/ui-kit/android/v6/message-bubble-styling#video-bubble). | +| [CometChatMessageComposer](/ui-kit/android/v6/message-composer#multiple-attachments) | Multi-select picker, an attachment tray that uploads before send, and the composer text as the batch caption. | +| [CometChatMessageList](/ui-kit/android/v6/message-list#multiple-attachments) | Renders received attachments with per-type bubbles and groups a multi-type send as one visual batch. | + +A mixed pick is split into one message per attachment type (images → videos → audios → files) that render as grouped bubbles: an image/video **grid**, a stacked **audio player** list, and a connected **file card** stack. Per-message file count and per-file size limits come from your app's dashboard settings. + +**Supported media types:** + +| Type | Single | Multiple | +| --- | --- | --- | +| Images | [Image Bubble](/ui-kit/android/v6/message-bubble-styling#image-bubble) | [Images Bubble](/ui-kit/android/v6/message-bubble-styling#images-bubble) | +| Videos | [Video Bubble](/ui-kit/android/v6/message-bubble-styling#video-bubble) | [Videos Bubble](/ui-kit/android/v6/message-bubble-styling#videos-bubble) | +| Audio | [Audio Bubble](/ui-kit/android/v6/message-bubble-styling#audio-bubble) (voice notes) | [Audios Bubble](/ui-kit/android/v6/message-bubble-styling#audios-bubble) | +| Files | [File Bubble](/ui-kit/android/v6/message-bubble-styling#file-bubble) | [Files Bubble](/ui-kit/android/v6/message-bubble-styling#files-bubble) | ## Read Receipts diff --git a/ui-kit/android/v6/message-bubble-styling.mdx b/ui-kit/android/v6/message-bubble-styling.mdx index ed3e0fdaf..24df7d281 100644 --- a/ui-kit/android/v6/message-bubble-styling.mdx +++ b/ui-kit/android/v6/message-bubble-styling.mdx @@ -163,24 +163,38 @@ incomingMessageBubbleStyle = CometChatIncomingMessageBubbleStyle.default().copy( -### Audio Bubble +### Images Bubble + +Renders a message that carries **multiple image attachments** (`CometChatImagesBubble`) as a count-based grid (1, 2, 3, 4, or 5+ tiles). When there are more images than visible tiles, the last tile shows a "+N" overflow overlay. Tapping a tile opens the fullscreen gallery viewer. A caption renders underneath the grid. This is the multi-attachment bubble the list renders for image messages; the single-attachment [Image Bubble](#image-bubble) remains for standalone use. + +The image grid derives its look from your [CometChat theme](/ui-kit/android/v6/theme-introduction). In **Jetpack Compose** it is governed by the dedicated `CometChatImagesBubbleStyle` class. Its grid-specific properties: + +| Property | Type | Description | +| --- | --- | --- | +| `tileCornerRadius` | `Dp` | Corner radius of each grid tile | +| `gridSpacing` | `Dp` | Gap between tiles (default `2.dp`) | +| `tilePlaceholderColor` | `Color` | Shown while a tile's image loads | +| `moreOverlayBackgroundColor` | `Color` | Scrim over the last tile when items overflow | +| `moreOverlayTextColor` / `moreOverlayTextStyle` | `Color` / `TextStyle` | The "+N" overflow label | +| `captionTextColor` / `captionTextStyle` | `Color` / `TextStyle` | The caption row under the grid | + +### Video Bubble - + ```xml themes.xml lines - ``` @@ -189,9 +203,9 @@ incomingMessageBubbleStyle = CometChatIncomingMessageBubbleStyle.default().copy( ```kotlin lines incomingMessageBubbleStyle = CometChatIncomingMessageBubbleStyle.default().copy( - audioBubbleStyle = CometChatAudioBubbleStyle.default().copy( - playIconTint = Color(0xFFF76808), - backgroundColor = Color(0xFFFEEDE1) + videoBubbleStyle = CometChatVideoBubbleStyle.default().copy( + backgroundColor = Color(0xFFFEEDE1), + playIconTint = Color(0xFFF76808) ) ) ``` @@ -199,23 +213,40 @@ incomingMessageBubbleStyle = CometChatIncomingMessageBubbleStyle.default().copy( -### Video Bubble +### Videos Bubble + +Renders a message that carries **multiple video attachments** (`CometChatVideosBubble`) as a count-based grid (1, 2, 3, 4, or 5+ tiles). Each tile shows a poster thumbnail with a centered play badge and a duration chip; the last tile shows a "+N" overflow overlay when there are more videos than visible tiles. Tapping a tile opens the fullscreen player. A caption renders underneath the grid. This is the multi-attachment bubble the list renders for video messages; the single-attachment [Video Bubble](#video-bubble) remains for standalone use. + +The video grid derives its look from your [CometChat theme](/ui-kit/android/v6/theme-introduction). In **Jetpack Compose** it is governed by the dedicated `CometChatVideosBubbleStyle` class. Its grid-specific properties: + +| Property | Type | Description | +| --- | --- | --- | +| `tileCornerRadius` / `gridSpacing` | `Dp` | Tile corner radius and gap | +| `tilePlaceholderColor` | `Color` | Shown while a thumbnail loads | +| `moreOverlayBackgroundColor` / `moreOverlayTextColor` / `moreOverlayTextStyle` | `Color` / `TextStyle` | The "+N" overflow overlay | +| `playBadgeBackgroundColor` / `playIconTint` | `Color` | The centered play badge on video tiles | +| `durationChipBackgroundColor` / `durationTextColor` / `durationTextStyle` | `Color` / `TextStyle` | The duration chip | +| `showVideoDuration` | `Boolean` | Whether the duration chip is shown | +| `captionTextColor` / `captionTextStyle` | `Color` / `TextStyle` | The caption row under the grid | + +### Audio Bubble - + ```xml themes.xml lines - ``` @@ -224,9 +255,9 @@ incomingMessageBubbleStyle = CometChatIncomingMessageBubbleStyle.default().copy( ```kotlin lines incomingMessageBubbleStyle = CometChatIncomingMessageBubbleStyle.default().copy( - videoBubbleStyle = CometChatVideoBubbleStyle.default().copy( - backgroundColor = Color(0xFFFEEDE1), - playIconTint = Color(0xFFF76808) + audioBubbleStyle = CometChatAudioBubbleStyle.default().copy( + playIconTint = Color(0xFFF76808), + backgroundColor = Color(0xFFFEEDE1) ) ) ``` @@ -234,6 +265,30 @@ incomingMessageBubbleStyle = CometChatIncomingMessageBubbleStyle.default().copy( +### Audios Bubble + +Renders a message carrying **audio file attachments** (`CometChatAudiosBubble`) — audio chosen through the file/audio picker — one player row per file, each with a play/pause button, a seekable progress bar, the time, the file name, and a download control. Four or more files collapse behind a "Show N more" toggle. Only one row plays at a time. + + + +This bubble is distinct from the [Audio Bubble](#audio-bubble), which renders **voice notes** recorded with the composer's microphone. Voice notes keep their waveform look and are never grouped with other attachments. + + + +In **Jetpack Compose** it is governed by `CometChatAudiosBubbleStyle`; the **Kotlin (XML Views)** list reuses your existing [Audio Bubble](#audio-bubble) style. Key Compose properties: + +| Property | Type | Description | +| --- | --- | --- | +| `cardBackgroundColor` / `cardCornerRadius` | `Color` / `Dp` | Each player card (applies only when the message carries multiple files) | +| `itemSpacing` | `Dp` | Vertical gap between cards | +| `playButtonBackgroundColor` / `playIconTint` | `Color` | The play/pause circle | +| `seekFillColor` / `seekTrackColor` / `seekKnobColor` / `seekKnobBorderColor` | `Color` | The seek bar | +| `titleTextColor` / `titleTextStyle` | `Color` / `TextStyle` | File name text | +| `durationTextColor` / `durationTextStyle` | `Color` / `TextStyle` | The "mm:ss / mm:ss" time text | +| `downloadIconTint` | `Color` | Per-card download icon | +| `toggleTextColor` / `toggleTextStyle` | `Color` / `TextStyle` | The "Show N more" / "Show less" toggle | +| `captionTextColor` / `captionTextStyle` | `Color` / `TextStyle` | The caption row under the cards | + ### File Bubble @@ -269,6 +324,37 @@ incomingMessageBubbleStyle = CometChatIncomingMessageBubbleStyle.default().copy(
+### Files Bubble + +Renders a message carrying **document attachments** as a connected stack of file cards — each with a file-type icon (PDF, Word, Excel, PowerPoint, ZIP, …), the file name, and its size/extension metadata. Four or more files collapse behind a "+N more" toggle. Tapping a card opens the file. A caption renders under the stack. + + + +`cardBackgroundColor` only applies when the message carries **multiple** files — a single file card sits directly on the bubble background. + + + +In **Jetpack Compose** it is governed by `CometChatFilesBubbleStyle`; the **Kotlin (XML Views)** list reuses your existing [File Bubble](#file-bubble) style. Key Compose properties: + +| Property | Type | Description | +| --- | --- | --- | +| `cardBackgroundColor` / `cardCornerRadius` | `Color` / `Dp` | Each file card (multiples only) | +| `itemSpacing` | `Dp` | Vertical gap between cards | +| `fileIconBackgroundColor` / `fileIconCornerRadius` / `fileIconSize` | `Color` / `Dp` | The file-type icon plate | +| `titleTextColor` / `titleTextStyle` | `Color` / `TextStyle` | File name text | +| `subtitleTextColor` / `subtitleTextStyle` | `Color` / `TextStyle` | File size/extension text | +| `downloadIconTint` | `Color` | Per-card download icon | +| `toggleTextColor` / `toggleTextStyle` | `Color` / `TextStyle` | The "+N more" / "Show less" toggle | +| `captionTextColor` / `captionTextStyle` | `Color` / `TextStyle` | The caption row under the cards | + + + +Captions containing rich text (code blocks, quote blocks, inline code) render through the same pipeline as [Text Bubbles](#text-bubble), so their formatting looks identical across message types. + + + +--- + ### Sticker Bubble diff --git a/ui-kit/android/v6/message-composer.mdx b/ui-kit/android/v6/message-composer.mdx index ab5b71005..8fb4e0fd2 100644 --- a/ui-kit/android/v6/message-composer.mdx +++ b/ui-kit/android/v6/message-composer.mdx @@ -171,6 +171,77 @@ The MessageComposer does not attach SDK listeners directly. Typing indicators ar | `setParentMessageId(id)` | `parentMessageId = id` | Set parent message ID for threaded replies | | `setTextFormatters(list)` | `textFormatters = list` | Custom text formatters (mentions, etc.) | | `setEnableRichTextFormatting(true)` | `enableRichTextFormatting = true` | Enable rich text formatting | +| `setEnableMultipleAttachments(true)` | `enableMultipleAttachments = true` | Multi-select attachment picking with a preview tray (default `true`); `false` reverts to legacy single-attachment, send-immediately behavior. See [Multiple Attachments](#multiple-attachments) | + +--- + +## Multiple Attachments + +The composer supports sending several attachments in one go. Tapping a media or file option opens a multi-select picker; the picks are staged in an **attachment tray** above the input box, upload immediately, and are sent together when the user taps send. + +**How it works:** + +- **Multi-select picking** — the photo/video picker and the document picker both allow selecting multiple items, capped to the app's per-message limit. Camera captures and picker audio stage into the same tray. +- **Attachment tray** — each staged file shows as a tile (thumbnail for media, chip for files/audio) with a live upload progress indicator, a ✕ to cancel or remove, and tap-to-retry on a transfer failure. The tray is only visible while at least one file is staged. +- **Upload before send** — files upload as soon as they are staged (via the SDK's upload-first flow); the send button stays disabled until every tile finishes uploading. Text typed in the composer becomes the **caption** of the batch, with rich text formatting preserved. +- **One message per attachment type** — a mixed pick is split into separate messages in a fixed order (images → videos → audios → files) that share a `batchId` and render as grouped bubbles in the message list. +- **Clipboard paste** — images, videos, documents, and audio copied to the clipboard can be pasted straight into the composer and stage into the tray. +- **Voice notes stay standalone** — a mic recording always sends immediately as its own message and is never staged. Audio chosen through the file picker, by contrast, stages as a file attachment. +- Editing a media message edits its **caption** only; the message's attachments are preserved. + +### Limits and Validation + +Limits are governed by the app's server settings (CometChat dashboard) — the composer reads them at runtime through the SDK. + +| Limit | Setting | Behavior on violation | +| --- | --- | --- | +| Files per message | `file.count.max` (default 10) | Picker selection is capped; overflow picks are dropped and a `MAX_ATTACHMENTS_EXCEEDED` error is emitted ("You can attach up to N files at a time.") | +| Per-file size | `file.size.max` (default 100 MB) | The file stages as a **rejected** (non-retryable) error tile; sending is blocked until it is removed | +| File type | Server-side validation | The upload is refused by the server and the file becomes a **rejected** error tile | + +Each tile carries an `AttachmentUploadStatus` (`UPLOADING`, `DONE`, `FAILED`, `REJECTED`, `CANCELLED`) mapped directly from the SDK's per-file upload callbacks, so the tray shows the right affordance — cancel while uploading, **retry** on a `FAILED` (transient) tile, **remove** on a `REJECTED` (non-retryable) tile. Errors surface through the composer ViewModel's `errorEvent`, which you can observe to show your own snack bar. + +### Disabling Multiple Attachments + +Set the flag to `false` to restore the legacy behavior — single-item pickers that send immediately with no tray: + + + + +```kotlin lines +messageComposer.setEnableMultipleAttachments(false) +``` + + + + +```kotlin lines +CometChatMessageComposer( + user = user, + enableMultipleAttachments = false +) +``` + + + + +This is composer-side only: received multi-attachment messages still render with the grouped bubbles (see [Message List](/ui-kit/android/v6/message-list)). + +### Styling the Attachment Tray (Compose) + +In the Compose UI Kit the tray strip is themed through `CometChatAttachmentTrayStyle`: + +```kotlin lines +CometChatMessageComposer( + user = user, + attachmentTrayStyle = CometChatAttachmentTrayStyle.default().copy( + backgroundColor = Color(0xFFFEEDE1), + tileSpacing = 8.dp + ) +) +``` + +The individual staged-tile visuals and the per-type message bubbles are covered in [Message Bubble Styling](/ui-kit/android/v6/message-bubble-styling). --- diff --git a/ui-kit/android/v6/message-list.mdx b/ui-kit/android/v6/message-list.mdx index e8e7b263c..c50cf5af9 100644 --- a/ui-kit/android/v6/message-list.mdx +++ b/ui-kit/android/v6/message-list.mdx @@ -278,6 +278,40 @@ The component listens to SDK message events internally. No manual setup needed. | `setMessagesRequestBuilder(builder)` | `messagesRequestBuilder = builder` | Custom message request builder | | `setBubbleFactories(list)` | `bubbleFactories = list` | Set custom bubble factories for message types | | `setOnThreadRepliesClick { }` | `onThreadRepliesClick = { }` | Thread reply tap callback | +| `setEnableMultipleAttachments(true)` | `enableMultipleAttachments = true` | Render multi-attachment messages with per-type bubbles and batch grouping (default `true`). See [Multiple Attachments](#multiple-attachments) | + +--- + +## Multiple Attachments + +Media messages that carry multiple attachments render with dedicated per-type bubbles — an image grid for images, a video grid for videos, a stacked player list for audio files, and a connected card stack for documents (see [Message Bubble Styling](/ui-kit/android/v6/message-bubble-styling) for each one). + +When a single composer send is split into several messages (one per attachment type), the list **groups them visually as one batch**: the avatar and sender name appear only on the first message of the batch, the timestamp and read receipt only on the last, while reactions stay per message. Grouping is derived from list-neighbor adjacency on the `batchId` the composer stamps on each message — there are no separate index/count keys. A mic-recorded voice note carries a `voice_note` marker and always renders standalone, never grouped. + +The behavior is on by default and can be toggled: + + + + +```kotlin lines +val messageList = findViewById(R.id.message_list) +messageList.setEnableMultipleAttachments(true) // default +``` + + + + +```kotlin lines +CometChatMessageList( + user = user, + enableMultipleAttachments = true // default +) +``` + + + + +Setting it to `false` renders every media message with the classic single-attachment bubbles instead. ---