diff --git a/docs.json b/docs.json index 96952f214..3c478e277 100644 --- a/docs.json +++ b/docs.json @@ -457,6 +457,7 @@ "group": "Chat", "pages": [ "ui-kit/react/core-features", + "ui-kit/react/offline-support", "ui-kit/react/extensions", "ui-kit/react/ai-features" ] @@ -2583,6 +2584,7 @@ "group": "Chat", "pages": [ "ui-kit/angular/core-features", + "ui-kit/angular/offline-support", "ui-kit/angular/extensions", "ui-kit/angular/ai-features" ] @@ -3160,7 +3162,8 @@ "sdk/javascript/transient-messages", "sdk/javascript/delivery-read-receipts", "sdk/javascript/mentions", - "sdk/javascript/reactions" + "sdk/javascript/reactions", + "sdk/javascript/offline-support" ] }, "sdk/javascript/calling-overview", @@ -10291,6 +10294,10 @@ "source": "/ui-kit/react/v7/core-features", "destination": "/ui-kit/react/core-features" }, + { + "source": "/ui-kit/react/v7/offline-support", + "destination": "/ui-kit/react/offline-support" + }, { "source": "/ui-kit/react/v7/event-system", "destination": "/ui-kit/react/event-system" diff --git a/sdk/javascript/llms-javascript-v4.mdx b/sdk/javascript/llms-javascript-v4.mdx index 98f212703..57a5ca476 100644 --- a/sdk/javascript/llms-javascript-v4.mdx +++ b/sdk/javascript/llms-javascript-v4.mdx @@ -59,6 +59,7 @@ parameters, long-tail methods, group/user management, calling, or edge-case list - [Delivery & Read Receipts](/sdk/javascript/delivery-read-receipts) - [Mentions](/sdk/javascript/mentions) - [Reactions](/sdk/javascript/reactions) +- [Offline Support](/sdk/javascript/offline-support) ## Calling - [Calling — Overview](/sdk/javascript/calling-overview) diff --git a/sdk/javascript/offline-support.mdx b/sdk/javascript/offline-support.mdx new file mode 100644 index 000000000..168c6fdcc --- /dev/null +++ b/sdk/javascript/offline-support.mdx @@ -0,0 +1,727 @@ +--- +title: "Offline Support" +sidebarTitle: "Offline Support" +description: "Cache conversations and messages on the device, show them instantly, and queue messages sent without a connection with the CometChat JavaScript SDK." +--- + + + +| Field | Value | +| --- | --- | +| Package | `@cometchat/chat-sdk-javascript` | +| Import | `import { CometChat } from "@cometchat/chat-sdk-javascript";` | +| Enable | `new CometChat.AppSettingsBuilder().setOfflineConfig({ enabled: true, ... })` before `CometChat.init()` — offline support is **off by default** | +| Key methods | `CometChat.isOfflineEnabled()` · `CometChat.getPendingMessages()` · `CometChat.retryMessage(message)` / `CometChat.retryAllFailedMessages()` · `CometChat.addSyncListener(name, listener)` / `CometChat.removeSyncListener(name)` · `CometChat.getOfflineEncryptionStatus()` · see [Other Offline Methods](#other-offline-methods) | +| Two-phase reads | Pass a listener to `fetchPrevious()` / `fetchNext()` — e.g. `new CometChat.MessageFetchListener({ onCachedMessages, onServerMessages, onError })`; `CometChat.getConversation(with, type, new CometChat.SingleConversationFetchListener({...}))` | +| Queued sends | Pass `new CometChat.MessageSentListener({ onMessagePending, onMessageSent, onError })` as the second argument of `sendMessage()` / `sendMediaMessage()` / `sendCustomMessage()` | +| Offline send result | `onMessagePending` → `onError` (`FAILED_TO_FETCH`) and the Promise rejects, while the message stays queued → `onMessageSent` after reconnect. A send **without** a listener is not queued | +| Failed sends | Status `FAILED`; never retried automatically. Retry with `CometChat.retryMessage(message)` or `CometChat.retryAllFailedMessages()` | +| Storage | Browser IndexedDB (SQLite in a configured Electron app), one store per app ID and user, deleted on logout | +| Constraints | Cached reads skip any request that uses a server-only filter such as `setSearchKeyword()`. Encryption is opt-in and covers the database only, not cached media | +| Related | [Send a Message](/sdk/javascript/send-message) · [Receive a Message](/sdk/javascript/receive-message) · [Retrieve Conversations](/sdk/javascript/retrieve-conversations) · [Connection Status](/sdk/javascript/connection-status) | + + + +Offline support keeps a local copy of the logged-in user's conversations and messages on the device. Your app can render that copy the moment a screen opens — with or without a network — and the SDK replaces it with fresh server data as soon as it arrives. Messages sent while the device is offline are saved in a queue (the outbox) and sent automatically when the connection returns. + +Offline support is **opt-in**. Until you enable it, the SDK behaves exactly as it does today: nothing is stored on the device and every read goes to the server. + +{/* TODO(release): replace vX.Y.Z with the first JavaScript SDK release that ships offline support. */} +Requires JavaScript SDK vX.Y.Z or later. + +## How It Works + +- **One local store per user.** The SDK keeps a separate store for each app ID and user, in the browser's IndexedDB. Logging out, or logging in as a different user, deletes the previous user's store. +- **Cached, then server.** When you pass a listener to a read such as `fetchPrevious()`, the SDK first hands you what it has cached, then the server's result. Every server result is written back to the cache. +- **Queued sends.** A message sent with a `MessageSentListener` is stored in the outbox first. If the device is offline, it stays queued and is sent automatically once the connection is back. +- **Catch-up sync.** After a reconnect, the SDK sends queued messages, then refreshes the cached conversations and messages so changes made while you were offline (new, edited and deleted messages) reach the cache. +- **The server wins.** If the cached copy and the server copy of a message differ, the server copy is kept. + +## Enable Offline Support + +Pass an offline configuration to `setOfflineConfig()` on the `AppSettingsBuilder`, then initialize the SDK as usual. Always include `enabled: true`. + + + + ```typescript + let appID: string = "APP_ID"; + let region: string = "APP_REGION"; + + let appSetting: CometChat.AppSettings = new CometChat.AppSettingsBuilder() + .subscribePresenceForAllUsers() + .setRegion(region) + .setOfflineConfig({ + enabled: true, + maxPendingMessages: 50, + mediaCaching: CometChat.MediaCachingMode.AUTO, + }) + .build(); + + CometChat.init(appID, appSetting).then( + (initialized: boolean) => { + console.log("Initialization completed successfully", initialized); + }, + (error: CometChat.CometChatException) => { + console.log("Initialization failed with error:", error); + } + ); + ``` + + + ```javascript + let appID = "APP_ID"; + let region = "APP_REGION"; + + let appSetting = new CometChat.AppSettingsBuilder() + .subscribePresenceForAllUsers() + .setRegion(region) + .setOfflineConfig({ + enabled: true, + maxPendingMessages: 50, + mediaCaching: CometChat.MediaCachingMode.AUTO, + }) + .build(); + + CometChat.init(appID, appSetting).then( + () => { + console.log("Initialization completed successfully"); + }, + (error) => { + console.log("Initialization failed with error:", error); + } + ); + ``` + + + +The offline store opens when a user logs in (or an existing session is restored) and closes on logout. + + + Access the offline enums through the `CometChat` namespace — for example + `CometChat.MediaCachingMode.AUTO` and + `CometChat.PendingMessageOverflowPolicy.REJECT_NEWEST`. + + +### Configuration Options + +Set `enabled: true` to turn offline support on. Every other option is optional, and anything you leave out uses its default. The count limits accept the range shown; a value outside it, including `0`, is treated as the maximum. + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `enabled` | `boolean` | `false` | Set `enabled: true` to turn offline support on. Nothing is stored on the device unless offline support is on. | +| `maxPendingMessages` | `number` | `50` | How many messages the outbox can hold. Range 1–100. | +| `pendingQueueOverflowPolicy` | `CometChat.PendingMessageOverflowPolicy` | `REJECT_NEWEST` | What happens when the outbox is full: `REJECT_NEWEST` rejects the new send, `DROP_OLDEST` marks the oldest queued message as failed to make room. | +| `autoSendPendingOnReconnect` | `boolean` | `true` | Sends queued messages automatically after a reconnect. When `false`, a queued message is sent only when you call `retryMessage()` for it or a new message is sent in the same conversation. It does not affect the catch-up sync. | +| `mediaCaching` | `CometChat.MediaCachingMode` | `AUTO` | `AUTO` downloads attachments of fetched and received messages so they can be viewed offline. `OFF` caches the message and its attachment details but not the file itself. | +| `maxMediaCacheSize` | `number` (bytes) | `524288000` (500 MB) | Space cached media may use. The least recently used files are removed first. `0` means no limit. | +| `maxStorageSize` | `number` (bytes) | `262144000` (250 MB) | Space the cached conversations and messages may use. The oldest messages are removed first. `0` means no limit. | +| `maxConversations` | `number` | `100` | How many conversations the cache keeps. The least recently used conversation is removed first, with its messages. Range 1–1000. | +| `maxMessagesPerConversation` | `number` | `500` | How many of the latest messages the cache keeps for each conversation. Range 1–1000. | +| `cacheDurationDays` | `number` (days) | `0` | Removes a cached conversation that has had no new message and hasn't been opened for this many days. `0` means no time limit. | +| `maxSnapshotQueries` | `number` | `100` | How many different list requests — users, groups, members and the other [lists](#other-lists), each with its own filters — are kept for offline use. The least recently used is removed first. Range 1–1000. | +| `snapshotTtlDays` | `number` (days) | `7` | How long a cached list request stays available offline. After that it returns nothing offline until it is loaded online again. `0` means no time limit. | +| `encryption` | `boolean` | `false` | Encrypts the cached data at rest. See [Encrypt the Cache](#encrypt-the-cache). | +| `encryptionFailureBehavior` | `string` | `"ONLINE_ONLY"` | Applies only when `encryption` is `true` and encryption can't run on the device. `"ONLINE_ONLY"` opens no cache and the app works online only. `"PLAINTEXT_FALLBACK"` opens an unencrypted cache so offline support keeps working. | + +Messages waiting in the outbox, including failed ones, are never removed by these limits. + +## Check Whether Offline Is Running + +`CometChat.isOfflineEnabled()` returns `true` only while offline support is actually running for the logged-in user: you enabled it, a user is logged in, and the local store is open. It returns `false` before login, after logout, and when CometChat has switched offline support off for your app. Read it each time you need it instead of storing the value. + + + + ```typescript + let offlineRunning: boolean = CometChat.isOfflineEnabled(); + ``` + + + ```javascript + let offlineRunning = CometChat.isOfflineEnabled(); + ``` + + + +CometChat can turn offline support off for an app from the server. When that happens, the SDK stops using the local store, deletes the cached data and `isOfflineEnabled()` returns `false`. Your app keeps working online. + +## Read From the Cache, Then the Server + +Pass a fetch listener to a request's `fetchPrevious()` or `fetchNext()` to receive results in two steps: + +1. `onCached*` — what is stored on the device, delivered immediately. It can be an empty list. +2. `onServer*` — the server's result, which also updates the cache. If the device is offline, or the request fails, `onError` is called instead. + +The call still returns a Promise that settles with the server result, so handle the outcome in one place. Without a listener, the request reads from the server only, exactly as before. + +### Messages + + + + ```typescript + let UID: string = "UID"; + let limit: number = 30; + + let messagesRequest: CometChat.MessagesRequest = + new CometChat.MessagesRequestBuilder() + .setUID(UID) + .setLimit(limit) + .build(); + + messagesRequest + .fetchPrevious( + new CometChat.MessageFetchListener({ + onCachedMessages: (messages: CometChat.BaseMessage[]) => { + // Render the cached page right away + }, + onServerMessages: (messages: CometChat.BaseMessage[]) => { + // Replace it with the server's page + }, + onError: (error: CometChat.CometChatException) => { + // Offline or failed — keep showing the cached page + }, + }) + ) + .catch(() => { + // Already handled in onError + }); + ``` + + + ```javascript + let UID = "UID"; + let limit = 30; + + let messagesRequest = new CometChat.MessagesRequestBuilder() + .setUID(UID) + .setLimit(limit) + .build(); + + messagesRequest + .fetchPrevious( + new CometChat.MessageFetchListener({ + onCachedMessages: (messages) => { + // Render the cached page right away + }, + onServerMessages: (messages) => { + // Replace it with the server's page + }, + onError: (error) => { + // Offline or failed — keep showing the cached page + }, + }) + ) + .catch(() => { + // Already handled in onError + }); + ``` + + + +### Conversations + + + + ```typescript + let conversationsRequest: CometChat.ConversationsRequest = + new CometChat.ConversationsRequestBuilder().setLimit(30).build(); + + conversationsRequest + .fetchNext( + new CometChat.ConversationFetchListener({ + onCachedConversations: (conversations: CometChat.Conversation[]) => { + // Render the cached list + }, + onServerConversations: (conversations: CometChat.Conversation[]) => { + // Replace it with the server's list + }, + onError: (error: CometChat.CometChatException) => { + // Offline or failed + }, + }) + ) + .catch(() => {}); + ``` + + + ```javascript + let conversationsRequest = new CometChat.ConversationsRequestBuilder() + .setLimit(30) + .build(); + + conversationsRequest + .fetchNext( + new CometChat.ConversationFetchListener({ + onCachedConversations: (conversations) => { + // Render the cached list + }, + onServerConversations: (conversations) => { + // Replace it with the server's list + }, + onError: (error) => { + // Offline or failed + }, + }) + ) + .catch(() => {}); + ``` + + + +### A Single Conversation + +`getConversation()` accepts a third argument. When the conversation is not cached, `onCachedConversation` is not called. + + + + ```typescript + let conversationWith: string = "UID"; + let conversationType: string = "user"; + + CometChat.getConversation( + conversationWith, + conversationType, + new CometChat.SingleConversationFetchListener({ + onCachedConversation: (conversation: CometChat.Conversation) => {}, + onServerConversation: (conversation: CometChat.Conversation) => {}, + onError: (error: CometChat.CometChatException) => {}, + }) + ).catch(() => {}); + ``` + + + ```javascript + CometChat.getConversation( + "UID", + "user", + new CometChat.SingleConversationFetchListener({ + onCachedConversation: (conversation) => {}, + onServerConversation: (conversation) => {}, + onError: (error) => {}, + }) + ).catch(() => {}); + ``` + + + +### Other Lists + +The same pattern works for these requests: + +| Request | Listener | Callbacks | +| --- | --- | --- | +| `UsersRequest`, `BlockedUsersRequest` | `CometChat.UserFetchListener` | `onCachedUsers` · `onServerUsers` · `onError` | +| `GroupsRequest` | `CometChat.GroupFetchListener` | `onCachedGroups` · `onServerGroups` · `onError` | +| `GroupMembersRequest` | `CometChat.GroupMemberFetchListener` | `onCachedGroupMembers` · `onServerGroupMembers` · `onError` | +| `BannedMembersRequest` | `CometChat.BannedMemberFetchListener` | `onCachedBannedMembers` · `onServerBannedMembers` · `onError` | +| `ReactionsRequest` | `CometChat.ReactionFetchListener` | `onCachedReactions` · `onServerReactions` · `onError` | +| `NotificationFeedRequest` | `CometChat.NotificationFeedFetchListener` | `onCachedNotificationFeedItems` · `onServerNotificationFeedItems` · `onError` | + +These lists are served from the cache only for a request that was loaded online before with the same filters. A list that has never been loaded online has nothing to show offline. + +### When the Cache Is Skipped + +The cache only answers a request when it can apply every filter on the request correctly. If a request uses a filter that only the server can evaluate — for example `setSearchKeyword()` — `onCached*` is not called and only the server answers. The cache phase is also skipped while offline support is not running. + +## Send Messages While Offline + +Pass a `MessageSentListener` as the second argument to `sendMessage()`, `sendMediaMessage()` or `sendCustomMessage()`. The listener has three callbacks: + +| Callback | When it fires | +| --- | --- | +| `onMessagePending(message)` | The message is saved in the outbox. Show it in the UI as pending. | +| `onMessageSent(message)` | The server accepted the message. Replace the pending copy with this one. | +| `onError(error)` | The send failed, or the device is offline (see below). | + + + + ```typescript + let receiverID: string = "UID"; + let receiverType: string = CometChat.RECEIVER_TYPE.USER; + let textMessage: CometChat.TextMessage = new CometChat.TextMessage( + receiverID, + "Hello", + receiverType + ); + + CometChat.sendMessage( + textMessage, + new CometChat.MessageSentListener({ + onMessagePending: (message: CometChat.BaseMessage) => { + // Show the message with a pending indicator + }, + onMessageSent: (message: CometChat.BaseMessage) => { + // Swap the pending copy for the sent message + }, + onError: (error: CometChat.CometChatException) => { + // Offline: the message is still queued and will be sent on reconnect + }, + }) + ).catch(() => { + // Already handled in the listener + }); + ``` + + + ```javascript + let receiverID = "UID"; + let receiverType = CometChat.RECEIVER_TYPE.USER; + let textMessage = new CometChat.TextMessage(receiverID, "Hello", receiverType); + + CometChat.sendMessage( + textMessage, + new CometChat.MessageSentListener({ + onMessagePending: (message) => { + // Show the message with a pending indicator + }, + onMessageSent: (message) => { + // Swap the pending copy for the sent message + }, + onError: (error) => { + // Offline: the message is still queued and will be sent on reconnect + }, + }) + ).catch(() => { + // Already handled in the listener + }); + ``` + + + +What happens depends on the connection: + +- **Online:** `onMessagePending`, then `onMessageSent`. The Promise resolves with the sent message. +- **Offline:** `onMessagePending`, then `onError` once with the code `FAILED_TO_FETCH`, and the Promise rejects. The message is **not** lost — it stays in the outbox and is sent after the connection returns, when `onMessageSent` fires. Do not resend it yourself, or the recipient receives it twice. +- **Outbox full:** with the default `REJECT_NEWEST` policy, the new send fails with `onError` (code `QUEUE_FULL`), the Promise rejects and the message is not saved. +- **Rejected later:** if a queued message can't be sent once the connection is back, it is marked as failed. While the page that sent it is still open, `onError` is called again with the reason. See [Failed Messages and Retry](#failed-messages-and-retry). + + + Only sends that pass a `MessageSentListener` are queued. A send without a + listener behaves exactly as before: while offline, its Promise rejects and + the message is not saved. + + +Use `message.getLocalMessageId()` to match the pending message to the sent one. The SDK assigns this ID when the message is queued; it is separate from any `muid` you set. + +### Get Pending Messages + +`CometChat.getPendingMessages()` resolves with the messages that are still in the outbox, including failed ones. Pass a conversation ID (`message.getConversationId()`) to get only that conversation's messages. Use it to show queued messages again after a page reload. + + + + ```typescript + CometChat.getPendingMessages().then((messages: CometChat.BaseMessage[]) => { + console.log("Messages waiting to be sent:", messages); + }); + ``` + + + ```javascript + CometChat.getPendingMessages().then((messages) => { + console.log("Messages waiting to be sent:", messages); + }); + ``` + + + +It resolves with an empty list while offline support is not running. + +### Failed Messages and Retry + +`message.getStatus()` on a message from the outbox returns one of these `CometChat.MessageStatus` values: + +| Status | Meaning | +| --- | --- | +| `PENDING` | Queued. It is sent automatically when the device is connected. | +| `SENDING` | Being sent now. | +| `FAILED` | It could not be sent. It stays in the outbox until you retry it. | + +A queued message becomes `FAILED` when: + +- the server rejects it — for example, the receiver can't be messaged, the request isn't allowed, or the rate limit is exceeded; +- the request keeps failing without a response from the server — the SDK retries it in the background a few times with an increasing delay first; +- it is dropped from a full outbox by the `DROP_OLDEST` policy. + +The SDK never retries a `FAILED` message on its own. Retry it from your UI, for example from a "tap to retry" action: + +| Method | Description | +| --- | --- | +| `CometChat.retryMessage(message: BaseMessage \| string, listener?: MessageSentListener): Promise` | Queues one message again. Pass the message, or its `getLocalMessageId()`. | +| `CometChat.retryAllFailedMessages(listener?: MessageSentListener): Promise` | Queues again every message that failed without a server response. Messages the server rejected, and messages dropped by `DROP_OLDEST`, are skipped; retry those one at a time with `retryMessage()`. | + + + + ```typescript + CometChat.getPendingMessages().then((messages: CometChat.BaseMessage[]) => { + messages + .filter((message) => message.getStatus() === CometChat.MessageStatus.FAILED) + .forEach((message) => { + CometChat.retryMessage(message); + }); + }); + ``` + + + ```javascript + CometChat.getPendingMessages().then((messages) => { + messages + .filter((message) => message.getStatus() === CometChat.MessageStatus.FAILED) + .forEach((message) => { + CometChat.retryMessage(message); + }); + }); + ``` + + + +A retried message is sent right away when the device is connected, and otherwise on the next reconnect. Read its status with `getPendingMessages()` to update your UI. + +## Listen for Sync Events + +After a reconnect, the SDK runs a catch-up sync in two phases: conversations, then messages. Register a `SyncListener` to know when each phase starts, finishes or fails — for example, to refresh a screen once the sync is complete. + + + + ```typescript + let listenerID: string = "UNIQUE_LISTENER_ID"; + + CometChat.addSyncListener( + listenerID, + new CometChat.SyncListener({ + onConversationSyncStarted: () => {}, + onConversationSyncCompleted: () => { + // Re-fetch the conversation list + }, + onConversationSyncError: (error: CometChat.CometChatException) => {}, + onMessageSyncStarted: () => {}, + onMessageSyncCompleted: () => { + // Re-fetch the open message list + }, + onMessageSyncError: (error: CometChat.CometChatException) => {}, + }) + ); + + // Remove it when you no longer need it + CometChat.removeSyncListener(listenerID); + ``` + + + ```javascript + let listenerID = "UNIQUE_LISTENER_ID"; + + CometChat.addSyncListener( + listenerID, + new CometChat.SyncListener({ + onConversationSyncStarted: () => {}, + onConversationSyncCompleted: () => { + // Re-fetch the conversation list + }, + onConversationSyncError: (error) => {}, + onMessageSyncStarted: () => {}, + onMessageSyncCompleted: () => { + // Re-fetch the open message list + }, + onMessageSyncError: (error) => {}, + }) + ); + + // Remove it when you no longer need it + CometChat.removeSyncListener(listenerID); + ``` + + + +You can register a sync listener at any time, including before login. Registering the same ID again replaces the earlier listener. + +### Updates Picked Up by the Sync + +The catch-up sync also tells your existing `MessageListener` what changed while the device was offline. Messages that are new to the device arrive through `onMessageReceived` as usual (for example `onTextMessageReceived`). Edits, deletions and other changes to messages that were already cached arrive together in `onMessagesSynced`. Update the matching messages in your UI by ID instead of appending them. + + + + ```typescript + CometChat.addMessageListener( + "UNIQUE_LISTENER_ID", + new CometChat.MessageListener({ + onMessagesSynced: (messages: CometChat.BaseMessage[]) => { + // Replace each message in your list by messages[i].getId() + }, + }) + ); + ``` + + + ```javascript + CometChat.addMessageListener( + "UNIQUE_LISTENER_ID", + new CometChat.MessageListener({ + onMessagesSynced: (messages) => { + // Replace each message in your list by messages[i].getId() + }, + }) + ); + ``` + + + +## Other Offline Methods + +| Method | Description | +| --- | --- | +| `CometChat.forceSyncNow(callback?: (error?: CometChatException) => void): void` | Starts a catch-up sync now. The callback is called with `null` when it completes, or with an error code: `ERROR_OFFLINE_SYNC_IN_PROGRESS`, `ERROR_OFFLINE_SYNC_COOLDOWN` (within 10 seconds of the previous forced sync; the error has `retryAfterMs`), `ERROR_OFFLINE_DISABLED` or `USER_NOT_LOGGED_IN`. It doesn't send queued messages. | +| `CometChat.getSyncState(): string` | `"SYNCING"` while a catch-up sync runs, otherwise `"IDLE"`. | +| `CometChat.getLastSyncTimestamp(): Promise` | When the last catch-up sync completed, in milliseconds, or `0` if none has. | +| `CometChat.getCacheSize(): Promise` | Size of the local cache in bytes. `0` while offline support is not running. | +| `CometChat.clearOfflineCache(): Promise` | Deletes the current user's cached data and cached media, **including queued and failed messages**, without a callback for them. You don't need it on logout; the SDK clears the store itself. | +| `CometChat.downloadMedia(attachment, callback?: MediaCallbackListener): void` | Downloads and caches an attachment, for example when `mediaCaching` is `OFF`. The callback has `onSuccess`, `onProgress(percent)` and `onError`. | +| `CometChat.getCachedMediaObjectURL(attachment): Promise` | A `blob:` URL for a cached attachment that you can use as an `` or `