diff --git a/docs.json b/docs.json index 604e2521b..b5de80544 100644 --- a/docs.json +++ b/docs.json @@ -4540,6 +4540,19 @@ "sdk/flutter/reactions" ] }, + { + "group": "Offline Support", + "pages": [ + "sdk/flutter/offline-overview", + "sdk/flutter/offline-enable", + "sdk/flutter/offline-cached-data", + "sdk/flutter/offline-send", + "sdk/flutter/offline-sync", + "sdk/flutter/offline-media-caching", + "sdk/flutter/offline-encryption", + "sdk/flutter/offline-reference" + ] + }, { "group": "Users", "pages": [ diff --git a/sdk/flutter/additional-message-filtering.mdx b/sdk/flutter/additional-message-filtering.mdx index 421032cdb..31ac89211 100644 --- a/sdk/flutter/additional-message-filtering.mdx +++ b/sdk/flutter/additional-message-filtering.mdx @@ -3,6 +3,8 @@ title: "Additional Message Filtering" description: "Filter CometChat messages in Flutter apps by limit, message ID, timestamp, user, group, category, type, and other request options." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + The `MessagesRequest` class as you must be familiar with helps you to fetch messages based on the various parameters provided to it. This document will help you understand better the various options that are available using the `MessagesRequest` class. @@ -222,6 +224,8 @@ This can be useful in finding the messages that have been received or updated af ## Updated messages only +With offline support on, this filter is answered by the server only; the cached page is empty. + *In other words, how do I fetch messages that have been updated after a particular date or time* This can be achieved easily by setting the updatesOnly parameter to true. To do so, you can use the updatesOnly() method. This method takes a boolean input and can be used with the `updatedAfter` parameter to get just the updated messages and not the messages sent/received after the specified time. This method cannot be used independently and always needs to be used with the `updatedAfter` parameter. @@ -410,6 +414,8 @@ MessagesRequest messageRequest = (MessagesRequestBuilder() ## Messages with links +With offline support on, this filter is answered by the server only; the cached page is empty. + In other words, as a logged-in user, how do I fetch messages that contains links? In order to do this, you can use the `hasLinks` parameter. This parameter accepts boolean as input. When set to `true` , the SDK will fetch messages which have links in the text. The default value for this parameter is `false`. @@ -602,6 +608,8 @@ This can be useful when you want to reduce the payload size and don't need the f *Available since v5.0.7.* Set the `pinned` parameter to `true` to fetch only the pinned messages of a conversation. A pinned list is conversation-scoped, so pair it with `uid` or `guid` — exactly one of the two. The list is ordered by pin time, most recently pinned first. +With offline support on, the pinned list is served from the cache first, then from the server. See [Pin A Message](/sdk/flutter/pin-message). + ```dart @@ -621,6 +629,8 @@ See [Pin A Message](/sdk/flutter/pin-message) for pinning, the pin fields on a m *Available since v5.0.7.* Set the `saved` parameter to `true` to fetch the logged-in user's saved messages. Saves span every conversation, so leave `uid` and `guid` unset. The list is ordered by save time, most recently saved first. +With offline support on, the saved list is served from the cache first, then from the server. See [Save A Message](/sdk/flutter/save-message). + ```dart @@ -634,3 +644,35 @@ MessagesRequest messageRequest = (MessagesRequestBuilder() See [Save A Message](/sdk/flutter/save-message) for saving, the `savedAt` field, and real-time save events. + +## Offline Behaviour + + + +With offline support enabled, `fetchPrevious`/`fetchNext` answer from the on-device cache first (`onCachedMessages`, even when empty) and then from the server (`onServerMessages`). The cache answers a page **only when every filter on the request is locally computable** — a single server-only filter makes the cached page empty with no error, while the server phase still answers in full. Offline, the fetch ends with `onError` (code `ERR_CONNECTION`, details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. See [Show Cached Data](/sdk/flutter/offline-cached-data). + +| Builder method | Offline behaviour | +| --- | --- | +| `setUnread(bool)` | Cached | +| `setHideMessagesFromBlockedUsers(bool)` | Cached | +| `setUpdatesOnly(bool)` | Server only — cached page is empty | +| `setCategories(List)` | Cached | +| `setTypes(List)` | Cached | +| `setParentMessageId(int)` | Cached | +| `setHideReplies(bool)` | Cached | +| `hideDeletedMessages(bool)` | Cached | +| `setHideQuotedMessages(bool)` | Cached | +| `setTags(List)` | Cached | +| `setWithTags(bool)` | Cached | +| `setHasLinks(bool)` | Server only — cached page is empty | +| `setSearchKeyword(String)` | Server only — cached page is empty | +| `setInteractionGoalCompletedOnly(bool)` | Server only — cached page is empty | +| `setHasAttachments(bool)` | Cached | +| `setHasReactions(bool)` | Cached | +| `setAttachmentTypes(List)` | Cached | +| `setHasMentions(bool)` | Cached | +| `setMentionedUIDs(List)` | Cached | +| `setPinned(bool)` | Cached | +| `setSaved(bool)` | Cached | + +`setPinned`/`setSaved` are served from the separate pinned and saved stores, not the message cache — the other filters in this table are not applied to a pinned or saved read. `setMessageId`, `setTimestamp`, `setUpdatedAfter`, `setLimit`, `setUID` and `setGUID` are scope and pagination inputs, not filters, and do not change what the cache can answer — except that setting `setUID` and `setGUID` together outside a thread (one sender's messages in a group) makes the read server-only. diff --git a/sdk/flutter/ai-moderation.mdx b/sdk/flutter/ai-moderation.mdx index 05f4d7664..b446f6ea0 100644 --- a/sdk/flutter/ai-moderation.mdx +++ b/sdk/flutter/ai-moderation.mdx @@ -3,6 +3,8 @@ title: "AI Moderation" description: "Automatically review CometChat messages for inappropriate content in Flutter apps using AI moderation rules and message status updates." --- +import FlutterOfflineSend from '/snippets/sdk/flutter-offline-send.mdx'; + | Field | Value | @@ -45,6 +47,7 @@ sequenceDiagram participant Moderation App->>SDK: sendMessage() + SDK-->>App: onMessagePending (with a MessageSentListener, offline support on) SDK->>CometChat: Send Message CometChat->>Moderation: Process Message CometChat-->>SDK: Message (status: PENDING) @@ -57,7 +60,7 @@ sequenceDiagram | Step | Description | |------|-------------| | 1. Send Message | App sends a text, image, or video message | -| 2. Pending Status | Message is sent with `PENDING` moderation status | +| 2. Pending Status | Message is sent with `PENDING` moderation status. This is separate from the send status: with offline support on and a `MessageSentListener`, `onMessagePending` fires first, before the message reaches the server. | | 3. AI Processing | Moderation service analyzes the content | | 4. Result Event | `onMessageModerated` event fires with final status | @@ -89,6 +92,8 @@ The `moderationStatus` property returns one of the following enum values: When you send a text, image, or video message, check the initial moderation status: + + ```dart @@ -108,6 +113,7 @@ When you send a text, image, or video message, check the initial moderation stat } }, onError: (CometChatException e) { + // This is a direct send (no listener:), so it is never queued offline. print("Message sending failed: ${e.message}"); }, ); diff --git a/sdk/flutter/authentication-overview.mdx b/sdk/flutter/authentication-overview.mdx index 21e700cda..46dbad921 100644 --- a/sdk/flutter/authentication-overview.mdx +++ b/sdk/flutter/authentication-overview.mdx @@ -152,6 +152,10 @@ After the user logs in, their information is returned in the `User` object. You can use the `logout()` method to log out the user from CometChat. We suggest you call this method once your user has been successfully logged out from your app. + +With offline support enabled, logging out — or logging in as a different user — wipes the current user's on-device cache (its messages, media and queued messages). See [Stay in Sync](/sdk/flutter/offline-sync). + + ```dart diff --git a/sdk/flutter/block-users.mdx b/sdk/flutter/block-users.mdx index bbb8eb19c..5e1517f12 100644 --- a/sdk/flutter/block-users.mdx +++ b/sdk/flutter/block-users.mdx @@ -3,6 +3,8 @@ title: "Block Users" description: "Block and unblock CometChat users in Flutter apps to stop direct communication and manage blocked user lists." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -100,6 +102,8 @@ BlockedUsersRequest blockedUsersRequest = (BlockedUsersRequestBuilder() ### Set Search Keyword +With offline support on, the cached page is filtered by this keyword on the device too, as a case-insensitive substring match on the user's name. + This method allows you to set the search string based on which the blocked users are to be fetched. @@ -138,20 +142,48 @@ Finally, once all the parameters are set in the builder class, you need to call Once you have the object of the `BlockedUsersRequest` class, you need to call the `fetchNext()` method. Calling this method will return a list of `User` objects containing n number of blocked users where N is the limit set in the builder class. + + ```dart +class Class_Name extends BlockedUsersFetchListener { + @override + void onCachedBlockedUsers(List users) { + // Cached page: fires first (even when empty). Render immediately. + debugPrint("Cached blocked users: ${users.length}"); + } + + @override + void onServerBlockedUsers(List users) { + // Server page: reconcile your displayed list, keyed by uid. + debugPrint("Server blocked users: ${users.length}"); + } + + @override + void onError(CometChatException e) { + // Handle failure (offline: ERR_CONNECTION after the cached page). + debugPrint("Blocked User Request failed with exception: ${e.message}"); + } +} + BlockedUsersRequest blockedUsersRequest = (BlockedUsersRequestBuilder() ..limit = 30 ).build(); -blockedUsersRequest.fetchNext(onSuccess: (List userList){ - debugPrint("Custom Message Sent Successfully : $userList "); - }, onError: (CometChatException e){ - debugPrint("Blocked User Request failed with exception: ${e.message}"); -}); +blockedUsersRequest.fetchNext(onSuccess: null, onError: null, listener: Class_Name()); ``` + +## Offline Behaviour + +With offline support enabled, `BlockedUsersRequest.fetchNext()` with a `BlockedUsersFetchListener` first delivers `onCachedBlockedUsers` from the SDK's own blocked-users list, which it refreshes on login and on reconnect and updates when you call `blockUsers()` or `unblockUsers()`. Then `onServerBlockedUsers` delivers the live page to reconcile by `uid`; the server page is not saved to the device. The cached page is empty when no users are blocked; with `searchKeyword` set, it holds the cached blocked users whose name contains the keyword. Offline, the read ends with `onError` with code `ERR_CONNECTION` (details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. See [Show Cached Data](/sdk/flutter/offline-cached-data). + +| Builder option | Offline | +|---|---| +| `limit` | Cached | +| `direction` | Cached | +| `searchKeyword` | Cached (substring match on the name) | diff --git a/sdk/flutter/campaigns.mdx b/sdk/flutter/campaigns.mdx index 23de1eae7..80b60c9e7 100644 --- a/sdk/flutter/campaigns.mdx +++ b/sdk/flutter/campaigns.mdx @@ -3,6 +3,8 @@ title: "Campaigns" description: "Fetch notification feed items, listen for real-time delivery, mark items as read/delivered, report engagement, and retrieve unread counts in Flutter." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -205,6 +207,8 @@ final request = (NotificationFeedRequestBuilder() ### Fetch Items + + ```dart @@ -245,6 +249,20 @@ Call `fetchNext()` repeatedly for pagination. When the server has no more items, --- +## Offline Behaviour + +With offline support enabled, `NotificationFeedRequest.fetchNext()` on a `NotificationFeedFetchListener` first delivers `onCachedNotificationFeedItems` from the device (empty when nothing was recorded), then `onServerNotificationFeedItems` with the live page to reconcile by item `id`. Offline, the read ends with `onError` (code `ERR_CONNECTION`, details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. A `fetchNext()` call made while another fetch of the same request is still running ends with `onError` (code `ERROR_REQUEST_IN_PROGRESS`). Once the list has ended, a further `fetchNext()` asks the server again and delivers only items this request has not delivered before. See [Show Cached Data](/sdk/flutter/offline-cached-data). + +| Builder method | Offline | +|---|---| +| `setLimit` | Cached | +| `setReadState` | Cached | +| `setCategory` | Cached | +| `setChannelId` | Cached | +| `setTags` | Cached | +| `setDateFrom` | Cached | +| `setDateTo` | Cached | + ## Retrieve Notification Categories Use `NotificationCategoriesRequest` to fetch available categories for filter chips. diff --git a/sdk/flutter/connection-status.mdx b/sdk/flutter/connection-status.mdx index a1e161ad8..ab1b3aa30 100644 --- a/sdk/flutter/connection-status.mdx +++ b/sdk/flutter/connection-status.mdx @@ -24,6 +24,10 @@ description: "Monitor CometChat WebSocket connection status in Flutter apps with CometChat SDK provides you with a mechanism to get real-time status of the connection to CometChat web-socket servers. To achieve this you need to use the `ConnectionListener` class provided by the CometChat SDK + +With offline support enabled, your app keeps working without a connection: cached chats are shown instantly, and messages you send with a `MessageSentListener` are queued and delivered automatically on reconnect. See [Show Cached Data](/sdk/flutter/offline-cached-data) and [Send While Offline](/sdk/flutter/offline-send). + + Connection Status provides you with the below 3 methods to get the status of the connection to CometChat web-socket servers: | Delegate Method | Information | diff --git a/sdk/flutter/delete-conversation.mdx b/sdk/flutter/delete-conversation.mdx index 3d3d66fd1..eb639e9fd 100644 --- a/sdk/flutter/delete-conversation.mdx +++ b/sdk/flutter/delete-conversation.mdx @@ -25,6 +25,10 @@ In case you want to delete a conversation, you can use the `deleteConversation() This method takes two parameters. The unique id (UID/GUID) of the conversation to be deleted & the type (user/group) of conversation to be deleted. + +Deleting a conversation needs a connection — with offline support enabled, `deleteConversation()` is **not** queued for later. On success the conversation and its cached messages are removed from the on-device cache. See [Send While Offline](/sdk/flutter/offline-send). + + ```dart diff --git a/sdk/flutter/delete-group.mdx b/sdk/flutter/delete-group.mdx index aa7ef80bc..bafcf6c1c 100644 --- a/sdk/flutter/delete-group.mdx +++ b/sdk/flutter/delete-group.mdx @@ -25,6 +25,10 @@ description: "Delete CometChat groups from Flutter apps by GUID when the logged- To delete a group you need to use the `deleteGroup()` method. The user must be an **Admin** of the group they are trying to delete. + +Deleting a group needs a connection — with offline support enabled, `deleteGroup()` is **not** queued for later. After it succeeds, the group's conversation and cached messages are removed from the on-device cache when the group-deleted event arrives or at the next reconnect sync; any messages still queued to it are then reported as failed with reason `discarded`. See [Send While Offline](/sdk/flutter/offline-send). + + ```dart diff --git a/sdk/flutter/delete-message.mdx b/sdk/flutter/delete-message.mdx index e5fd0f910..e46fe55ea 100644 --- a/sdk/flutter/delete-message.mdx +++ b/sdk/flutter/delete-message.mdx @@ -33,6 +33,10 @@ While [deleting a message](/sdk/flutter/delete-message#delete-a-message) is stra In case you have to delete a message, you can use the `deleteMessage()` method. This method takes the message ID of the message to be deleted. + +Deleting a message needs a connection — with offline support enabled, `deleteMessage()` is **not** queued for later. After `onSuccess`, the deletion is applied to the message in the on-device cache if it is already cached; it never adds a new message. See [Send While Offline](/sdk/flutter/offline-send). + + ```dart diff --git a/sdk/flutter/edit-message.mdx b/sdk/flutter/edit-message.mdx index c3f80ef9f..8bf1ef50a 100644 --- a/sdk/flutter/edit-message.mdx +++ b/sdk/flutter/edit-message.mdx @@ -33,6 +33,10 @@ While editing a message is straightforward, receiving events for edited messages In order to edit a message, you can use the `editMessage()` method. This method takes an object of the `BaseMessage` class. At the moment, you are only allowed to edit `TextMessage` and `CustomMessage`. Thus, the `BaseMessage` object must either be a Text or a Custom Message. + +Editing a message needs a connection — with offline support enabled, `editMessage()` is **not** queued for later. After `onSuccess`, the edit is applied to the message in the on-device cache if it is already cached; it never adds a new message. See [Send While Offline](/sdk/flutter/offline-send). + + ### Add/Update Tags While editing a message, you can update the tags associated with the Message. You can use the `setTags()` method to do so. The tags added while editing a message will replace the tags set when the message was sent. diff --git a/sdk/flutter/group-kick-member.mdx b/sdk/flutter/group-kick-member.mdx index 8d186ad93..0e4beaf67 100644 --- a/sdk/flutter/group-kick-member.mdx +++ b/sdk/flutter/group-kick-member.mdx @@ -3,6 +3,8 @@ title: "Ban/Kick Member From A Group" description: "Kick, ban, and unban CometChat group members in Flutter apps when the logged-in user is an admin or moderator." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -149,6 +151,8 @@ BannedGroupMembersRequest bannedGroupMembersRequest = (builder ### Set Search Keyword +With offline support on, this filter is answered by the server only; the cached page is empty. + This method allows you to set the search string based on which the banned group members are to be fetched. @@ -165,29 +169,76 @@ BannedGroupMembersRequest bannedGroupMembersRequest = (builder +### Set Scopes + +This method allows you to fetch banned group members based on the specified scopes. + + + +```dart +List scopes = ["admin", "moderator"]; +BannedGroupMembersRequestBuilder builder = BannedGroupMembersRequestBuilder(guid: conversationWith); +BannedGroupMembersRequest bannedGroupMembersRequest = (builder + ..limit = 50 + ..scopes = scopes + ).build(); +``` + + + + + Finally, once all the parameters are set to the builder class, you need to call the `build()` method to get the object of the `BannedGroupMembersRequest` class. Once you have the object of the `BannedGroupMembersRequest` class, you need to call the `fetchNext()` method. Calling this method will return a list of `GroupMember` objects containing n number of banned members where n is the limit set in the builder class. + + ```dart +class Class_Name extends BannedGroupMemberFetchListener { + @override + void onCachedBannedGroupMembers(List members) { + // Cached page: fires first (even when empty). Render immediately. + debugPrint("Cached banned members: ${members.length}"); + } + + @override + void onServerBannedGroupMembers(List members) { + // Server page: reconcile your displayed list, keyed by uid. + debugPrint("Server banned members: ${members.length}"); + } + + @override + void onError(CometChatException e) { + // Handle failure (offline: ERR_CONNECTION after the cached page). + debugPrint("Banned Group Members Fetch failed with exception: ${e.message}"); + } +} + BannedGroupMembersRequestBuilder builder = BannedGroupMembersRequestBuilder(guid: conversationWith); BannedGroupMembersRequest bannedGroupMembersRequest = (builder ..limit = 50 ).build(); -bannedGroupMembersRequest.fetchNext( onSuccess: (List groupMembers) { - debugPrint("Banned Group Members Fetched Successfully : $groupMembers"); - },onError: (CometChatException e) { - debugPrint("Banned Group Members Fetch failed with exception: ${e.message}"); - }); +bannedGroupMembersRequest.fetchNext(onSuccess: null, onError: null, listener: Class_Name()); ``` +## Offline Behaviour + +With offline support enabled, `BannedGroupMembersRequest.fetchNext()` with a `BannedGroupMemberFetchListener` first delivers `onCachedBannedGroupMembers` from the device (empty when nothing was recorded), then `onServerBannedGroupMembers` with the live page to reconcile by `uid`. Offline, the read ends with `onError` with code `ERR_CONNECTION` (details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. A request that uses a server-only filter records nothing, so its cached page is always empty. See [Show Cached Data](/sdk/flutter/offline-cached-data). + +| Builder option | Offline | +|---|---| +| `limit` | Cached | +| `scopes` | Cached | +| `searchKeyword` | Server only — cached page is empty | + ## Real-Time Group Member Kicked/Banned Events *In other words, as a member of a group, how do I know when someone is banned/kicked when my app is running?* diff --git a/sdk/flutter/leave-group.mdx b/sdk/flutter/leave-group.mdx index 259ed10bf..932e524eb 100644 --- a/sdk/flutter/leave-group.mdx +++ b/sdk/flutter/leave-group.mdx @@ -26,6 +26,10 @@ description: "Leave CometChat groups from Flutter apps by GUID and listen for re In order to stop receiving updates and messages for any particular joined group, you will have to leave the group using the `leaveGroup()` method. + +With offline support enabled, leaving a group clears that group's conversation from the on-device cache; any messages still queued to it are dropped and reported as failed with reason `discarded`. See [Send While Offline](/sdk/flutter/offline-send). + + ```dart diff --git a/sdk/flutter/llms-flutter-v5.mdx b/sdk/flutter/llms-flutter-v5.mdx index 82a049f3d..670916021 100644 --- a/sdk/flutter/llms-flutter-v5.mdx +++ b/sdk/flutter/llms-flutter-v5.mdx @@ -84,6 +84,16 @@ long-tail methods, group/user management, calling, or edge-case listeners. - [Pin A Message](/sdk/flutter/pin-message) - [Save A Message](/sdk/flutter/save-message) +## Offline support +- [Offline Support — Overview](/sdk/flutter/offline-overview) +- [Enable Offline Support](/sdk/flutter/offline-enable) +- [Show Cached Data](/sdk/flutter/offline-cached-data) +- [Send While Offline](/sdk/flutter/offline-send) +- [Stay in Sync](/sdk/flutter/offline-sync) +- [Media Caching](/sdk/flutter/offline-media-caching) +- [Encrypt the Cache](/sdk/flutter/offline-encryption) +- [Offline Configuration & API Reference](/sdk/flutter/offline-reference) + ## Conversations - [Retrieve Conversations](/sdk/flutter/retrieve-conversations) - [Delete A Conversation](/sdk/flutter/delete-conversation) diff --git a/sdk/flutter/mentions.mdx b/sdk/flutter/mentions.mdx index b4948ffa1..b93ef392d 100644 --- a/sdk/flutter/mentions.mdx +++ b/sdk/flutter/mentions.mdx @@ -3,6 +3,9 @@ title: "Mentions" description: "Send, receive, and inspect CometChat mentioned messages in Flutter apps using user UIDs in one-on-one and group conversations." --- +import FlutterOfflineSend from '/snippets/sdk/flutter-offline-send.mdx'; +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -29,6 +32,8 @@ Mentions in messages enable users to refer to specific individuals within a conv Every User object has a String unique identifier associated with them which can be found in a property called uid. To mention a user in a message, the message text should contain the uid in following format: `<@uid:UID_OF_THE_USER>`. For example, to mention the user with UID cometchat-uid-1 in a text message, your text should be "`<@uid:cometchat-uid-1>`" + + ```dart @@ -101,6 +106,8 @@ By default, the SDK will fetch all the messages irrespective of the fact that th ## Mentions With Tag Info + + To get a list of messages in a conversation where users are mentioned along with the tags of the mentioned users. @@ -242,3 +249,14 @@ message.hasMentionedMe + +## Offline Behaviour + +With offline support enabled, these mention fetches run in two phases: pass a `MessageFetchListener` to `fetchPrevious()` or `fetchNext()`. `onCachedMessages` delivers the cached page first (even when empty), then `onServerMessages` delivers the reconciled server copy. Offline, the fetch ends with `onError` (code `ERR_CONNECTION`, details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. See [Show Cached Data](/sdk/flutter/offline-cached-data). + +`mentionsWithTagInfo` and `mentionsWithBlockedInfo` enrich a read without narrowing it, so they never empty the cached page: the cached phase serves the matching messages as stored, and the server phase supplies the mentioned users' tag and blocked-relationship detail. + +| Builder method | Offline behaviour | +| --- | --- | +| `mentionsWithTagInfo(bool)` | Cached | +| `mentionsWithBlockedInfo(bool)` | Cached | diff --git a/sdk/flutter/offline-cached-data.mdx b/sdk/flutter/offline-cached-data.mdx new file mode 100644 index 000000000..fcf872f73 --- /dev/null +++ b/sdk/flutter/offline-cached-data.mdx @@ -0,0 +1,271 @@ +--- +title: "Show Cached Data" +sidebarTitle: "Show Cached Data" +description: "Render cached messages, conversations, and lists instantly and reconcile them against the server with the CometChat Flutter SDK." +--- + +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + +{/* TL;DR for Agents and Quick Reference */} + + +```dart +// Every read fires up to two phases: cached first (instant), then server. +// Pass a `listener:` to turn on the cache phase. +final request = + (MessagesRequestBuilder()..uid = "UID"..limit = 30).build(); + +request.fetchPrevious(listener: MyMessageFetchListener()); + +class MyMessageFetchListener extends MessageFetchListener { + @override + void onCachedMessages(List messages) { + // Phase 1: local cache. Fires first, even when empty. Render immediately. + } + @override + void onServerMessages(List messages) { + // Phase 2: server. Replaces the same slice — reconcile, don't append. Never fires offline. + } + @override + void onError(CometChatException e) { + // Offline ends here, after the cached page, with code ERR_CONNECTION. + } +} + +// Single items +CometChat.getConversation("UID", CometChatConversationType.user, + listener: ChatConversationDetailsListener(), onSuccess: null, onError: null); +CometChatHelper.getMessageDetails(messageId, listener: ChatMessageDetailsListener()); +``` + + + +With offline support enabled, every list and lookup the SDK already offers answers from the on-device cache first and then from the server. Your UI paints instantly from the cache, then reconciles when the server replies. This page describes that contract, the reads it applies to, and how to merge the two phases. + +## The two-phase contract + +Each read fires **up to two** callbacks, always in this order: + + + +The SDK delivers a page from the local cache **first**, synchronously-fast. It fires **even when the page is empty** — an empty cached page is indistinguishable from a cache miss, by design. Render it right away. + + +After a successful server response, the SDK delivers the server's copy for the **same slice** and writes it through to the cache before the callback. Reconcile your displayed list against it. (Blocked users are the exception: their server page is delivered but not written to the cache, which keeps that list current through its own background refresh.) + + + +Two rules follow from this: + +- **Offline, the read ends with an error.** After the cached page, a read that gets no answer because the device is offline settles with `onError`, code `ERR_CONNECTION`. A real server error reaches `onError` unchanged. The call always settles, as long as you wait for each fetch to return before starting the next one on the same request object. +- **The server phase never fires offline.** It fires only on a genuine server response. Never treat a cached page as "end of history"; an empty cached page only means the end of the *cached* window. + + + +The `listener:` turns on the cache phase. The `onSuccess` / `onError` form of the same method keeps working and receives only the server result — the cached phase never fires through it. Nothing changes until you call `OfflineConfig(enabled: true)` (see [Enable Offline Support](/sdk/flutter/offline-enable)). Minimum version **5.1.0**. + + + +## Read messages + +`MessagesRequest` supports both directions. `fetchPrevious` pages toward older messages; `fetchNext` pages toward newer ones. Pass a `MessageFetchListener` to either. + + + + + +```dart +final request = + (MessagesRequestBuilder()..uid = "UID"..limit = 30).build(); + +request.fetchPrevious(listener: ChatMessageFetchListener()); + +class ChatMessageFetchListener extends MessageFetchListener { + @override + void onCachedMessages(List messages) { + // Render the cached page immediately. + } + + @override + void onServerMessages(List messages) { + // Reconcile this slice against the cached page, keyed by message id. + } + + @override + void onError(CometChatException e) { + debugPrint("Fetch failed: ${e.code}"); + } +} +``` + + + +## Read conversations + +`ConversationsRequest` pages in one direction with `fetchNext`, taking a `ConversationFetchListener`. + + + + + +```dart +final request = (ConversationsRequestBuilder()..limit = 30).build(); + +request.fetchNext(listener: ChatConversationFetchListener()); + +class ChatConversationFetchListener extends ConversationFetchListener { + @override + void onCachedConversations(List conversations) { + // Render the cached list immediately. + } + + @override + void onServerConversations(List conversations) { + // Reconcile, keyed by conversation id. + } + + @override + void onError(CometChatException e) { + debugPrint("Fetch failed: ${e.code}"); + } +} +``` + + + +## Read the other lists + +Every remaining list read gains the same two-phase behaviour. Each has its own listener with cached/server callbacks of the matching type plus `onError`. + +| Request | Fetch method(s) | Listener | Cached / server callback | +|---|---|---|---| +| `MessagesRequest` | `fetchPrevious`, `fetchNext` | `MessageFetchListener` | `onCachedMessages` / `onServerMessages` | +| `ConversationsRequest` | `fetchNext` | `ConversationFetchListener` | `onCachedConversations` / `onServerConversations` | +| `ThreadsRequest` | `fetchNext` | `ThreadsFetchListener` | `onCachedThreads` / `onServerThreads` | +| `UsersRequest` | `fetchNext` | `UserFetchListener` | `onCachedUsers` / `onServerUsers` | +| `BlockedUsersRequest` | `fetchNext` | `BlockedUsersFetchListener` | `onCachedBlockedUsers` / `onServerBlockedUsers` | +| `GroupsRequest` | `fetchNext` | `GroupFetchListener` | `onCachedGroups` / `onServerGroups` | +| `GroupMembersRequest` | `fetchNext` | `GroupMemberFetchListener` | `onCachedGroupMembers` / `onServerGroupMembers` | +| `BannedGroupMembersRequest` | `fetchNext` | `BannedGroupMemberFetchListener` | `onCachedBannedGroupMembers` / `onServerBannedGroupMembers` | +| `ReactionsRequest` | `fetchNext` | `ReactionFetchListener` | `onCachedReactions` / `onServerReactions` | +| `NotificationFeedRequest` | `fetchNext` | `NotificationFeedFetchListener` | `onCachedNotificationFeedItems` / `onServerNotificationFeedItems` | + +On these requests — `UsersRequest`, `BlockedUsersRequest`, `GroupsRequest`, `GroupMembersRequest`, `BannedGroupMembersRequest`, `ReactionsRequest`, and `NotificationFeedRequest` — `onSuccess` and `onError` are declared `required`. When you drive the read with a `listener:`, pass `onSuccess: null, onError: null` and handle every result on the listener. + + + +```dart +final request = (UsersRequestBuilder()..limit = 30).build(); + +request.fetchNext( + listener: ChatUserFetchListener(), + onSuccess: null, + onError: null, +); + +class ChatUserFetchListener extends UserFetchListener { + @override + void onCachedUsers(List users) { + // render + } + + @override + void onServerUsers(List users) { + // reconcile, keyed by uid + } + + @override + void onError(CometChatException e) { + debugPrint(e.code); + } +} +``` + + + + + +`ReactionsRequest` caches the `fetchNext` direction; `fetchPrevious` returns the server page directly. Users, groups, members, banned members, reactions, and the notification feed are served from a per-request snapshot rather than a shared store; per-builder details live on each request's own page and in the [Configuration & API Reference](/sdk/flutter/offline-reference). + + + +## Read a single item + +`getConversation` and `getMessageDetails` each deliver a two-phase result through a listener. Unlike the list reads, the cached callback fires **only when the item is cached** — never with an empty or null object. A cache miss goes straight to the server phase online, or straight to `onError` offline. + +- `CometChat.getConversation(...)` takes a `ConversationDetailsListener`. Its `onSuccess` / `onError` are `required`, so pass `onSuccess: null, onError: null` when you use the listener. +- The two-phase message lookup lives on `CometChatHelper.getMessageDetails(messageId, listener: ...)`. + + + + + +```dart +CometChat.getConversation( + "UID", + CometChatConversationType.user, + listener: ChatConversationDetailsListener(), + onSuccess: null, + onError: null, +); + +class ChatConversationDetailsListener extends ConversationDetailsListener { + @override + void onCachedConversation(Conversation conversation) { + // Fires only on a cache hit. + } + + @override + void onServerConversation(Conversation conversation) { + // Server copy, written through before delivery. + } + + @override + void onError(CometChatException e) { + debugPrint("Lookup failed: ${e.code}"); + } +} + +CometChatHelper.getMessageDetails(messageId, listener: ChatMessageDetailsListener()); + +class ChatMessageDetailsListener extends MessageDetailsListener { + @override + void onCachedMessage(BaseMessage message) { + // cache hit only + } + + @override + void onServerMessage(BaseMessage message) { + // server copy + } + + @override + void onError(CometChatException e) { + debugPrint(e.code); + } +} +``` + + + +## Merge the two phases + +The server phase **replaces the same slice** — it is a reconcile, not a second batch. Render the cached page straight away, then on the server phase diff it into your displayed list keyed by the item's stable id (message id, conversation id, uid, guid). Do not append the server page to the cached one. + + + +Key your list by id so the two phases merge cleanly: update items that changed, add new ones, and drop cached items the server slice omits. To page further back, issue the next `fetchPrevious()` / `fetchNext()` rather than treating the server phase as more pages. + + + +## When the cached page is empty + +The cache answers a read **only if every filter on it is locally computable**. A single server-only predicate makes the entire cached phase return an empty list — with no error and no warning — while the server phase still answers in full. The guiding rule is *better empty than wrong*. + +The most common server-only predicate is `searchKeyword`, which blanks the cached phase on any request that carries a search keyword — except `BlockedUsersRequest`, which answers it from the cache as a substring match on the user's name (threads, reactions and the notification feed never carry one). Other examples include `hasLinks`, `updatesOnly`, and `interactionGoalCompletedOnly` on `MessagesRequest`, setting a `uid` and a `guid` together outside a thread (one sender's messages in a group), and the presence filters (`UsersRequestBuilder.userStatus`, `GroupMembersRequestBuilder.status`) on the entity lists. + + + +An offline-critical screen must use only cache-supported filters — one innocuous builder flag can silently disable the cached phase for a whole screen. The full per-builder matrix of what the cache answers lives in the [Configuration & API Reference](/sdk/flutter/offline-reference). + + diff --git a/sdk/flutter/offline-enable.mdx b/sdk/flutter/offline-enable.mdx new file mode 100644 index 000000000..e34fc1d9e --- /dev/null +++ b/sdk/flutter/offline-enable.mdx @@ -0,0 +1,116 @@ +--- +title: "Enable Offline Support" +sidebarTitle: "Enable Offline Support" +description: "Turn on local caching and the durable send outbox, and verify offline support is active with the CometChat Flutter SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```dart +// 1. Build an OfflineConfig (off by default — set enabled: true). +final offlineConfig = OfflineConfig(enabled: true); + +// 2. Attach it to AppSettings. +AppSettings appSettings = (AppSettingsBuilder() + ..region = "REGION" + ..setOfflineConfig(offlineConfig) // explicit config; enabled value wins + // ..offline(true) // shorthand: enable with all defaults + ).build(); + +// 3. Pass the settings to init. +CometChat.init(appId, appSettings, + onSuccess: (String message) {}, onError: (CometChatException e) {}); + +// 4. Verify it is active for the logged-in user. +final active = CometChat.isOfflineEnabled(); // bool +``` + + + +Offline support is **off by default**. You turn it on once, at initialization, by attaching an `OfflineConfig` to your `AppSettings`. Until you do, the SDK behaves exactly as before. This page covers the minimal setup; for every tunable option see the [Configuration & API Reference](/sdk/flutter/offline-reference). + + + + +Offline support requires CometChat Flutter SDK **5.1.0** or later. Add or update the dependency in your `pubspec.yaml`: + +```yaml +dependencies: + cometchat_sdk: ^5.1.0 +``` + +Then run `flutter pub get`. This release needs Dart **3.10+**, Flutter **3.38+**, iOS **13+**, and Android **minSdk 21**. + +If you are new to the SDK, complete the [standard setup](/sdk/flutter/setup) first; the steps below only add offline configuration to your existing `init`. + + + +Construct an `OfflineConfig` and set `enabled: true`. Every other field has a sensible default, so set only what you need. + + + +```dart +final offlineConfig = OfflineConfig(enabled: true); +``` + + + + + +Pass the config to `AppSettingsBuilder.setOfflineConfig(...)`. To enable with all defaults instead of building a config, use the `offline(bool)` shorthand. + + + +```dart +AppSettings appSettings = (AppSettingsBuilder() + ..region = "REGION" + ..setOfflineConfig(offlineConfig) // explicit config + // ..offline(true) // shorthand: enable with defaults + ).build(); +``` + + + + +If you call both `setOfflineConfig(...)` and `offline(bool)`, the explicit `OfflineConfig` wins — its `enabled` value is authoritative. + + + + +Pass the settings to `CometChat.init(...)` as you do today. No other call changes. + + + +```dart +CometChat.init(appId, appSettings, + onSuccess: (String message) { + debugPrint("Initialization completed successfully: $message"); + }, + onError: (CometChatException e) { + debugPrint("Initialization failed: ${e.message}"); + }, +); +``` + + + + + +Call `CometChat.isOfflineEnabled()`. It returns `true` only while offline support is actually active for the logged-in user. It is `false` before anyone logs in (even when you enabled it in `AppSettings`), after logout or session expiry, and when the encrypted cache cannot open. Read it at the moment of each decision and keep no copy of the result. + + + +```dart +final active = CometChat.isOfflineEnabled(); +debugPrint("Offline support active: $active"); +``` + + + + + + +## Next steps + +Offline support is now active for the logged-in user. Continue with [Show Cached Data](/sdk/flutter/offline-cached-data) to adopt the new fetch listeners, and [Send While Offline](/sdk/flutter/offline-send) for the durable outbox. Tune caps, TTLs, media, and encryption from the [Configuration & API Reference](/sdk/flutter/offline-reference). diff --git a/sdk/flutter/offline-encryption.mdx b/sdk/flutter/offline-encryption.mdx new file mode 100644 index 000000000..6f902849a --- /dev/null +++ b/sdk/flutter/offline-encryption.mdx @@ -0,0 +1,171 @@ +--- +title: "Encrypt the Cache" +sidebarTitle: "Encrypt the Cache" +description: "Encrypt the offline message cache at rest, choose what happens when encryption cannot run, and inspect the live encryption status with the CometChat Flutter SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```dart +// 1. Select the cipher engine in your APP's own pubspec.yaml (the SDK cannot do it for you): +// hooks: +// user_defines: +// sqlite3: +// source: sqlite3mc + +// 2. Turn encryption on in OfflineConfig (off by default) +final offlineConfig = OfflineConfig( + enabled: true, + encryption: true, // encrypt the cache database at rest + encryptionFailureBehavior: EncryptionFailureBehavior.onlineOnly, // default; never writes plaintext +); + +// 3. Inspect what encryption is actually doing +final status = CometChat.getOfflineEncryptionStatus(); +status.enabled; // what was configured +status.atRest; // OfflineAtRest.sqlcipher or OfflineAtRest.none +status.keyStorage; // OfflineKeyStorage.keystore / .software / .none +status.cipherAvailable; // is the cipher engine present in this build? +status.cipher; // engine cipher string once proven, else null +``` + + + +Offline support keeps the logged-in user's messages in a local database on the device. Turn encryption on to store that database **encrypted at rest** using [SQLite3 Multiple Ciphers](https://utelle.github.io/SQLite3MultipleCiphers/). Encryption is **opt-in** and off by default; nothing changes until you set `encryption: true`. The minimum SDK version is **5.1.0**. It works on all Flutter targets — Android, iOS, macOS, Windows, Linux and web. + + +Encryption covers the **message database only**. Downloaded media files (photos, voice notes, documents) stay as ordinary unencrypted files on disk, because their local paths are handed to native image and video loaders. See [What is and isn't encrypted](#what-gets-encrypted). + + +## Add the cipher engine + +Unlike the other `OfflineConfig` flags, `encryption: true` is not sufficient on its own. The cipher engine is chosen by a build hook whose user-defines are read from the **root package's** `pubspec.yaml` only, so the SDK cannot select it on your behalf. Add this stanza to your **own app's** `pubspec.yaml`: + +```yaml +hooks: + user_defines: + sqlite3: + source: sqlite3mc +``` + +`hooks:` is a top-level key in your app's `pubspec.yaml` — a sibling of `dependencies:`, not nested under it. You do not need to add any `sqlite3` dependency yourself; the SDK already depends on it, and this stanza only selects the cipher-enabled build. + + +Without this stanza the app links stock SQLite, which cannot encrypt. The cache then **cannot run encrypted**, and what happens is decided by [`encryptionFailureBehavior`](#choose-what-happens-when-encryption-cannot-run) — by default the session runs server-only (no local cache). The SDK never creates a database that is labelled encrypted but is not. The status API reports this case as `enabled: true` with `cipherAvailable: false`. + + +### Web + +Flutter web needs one extra step: copy the cipher-enabled WebAssembly build the SDK ships into your app's web root. + +```bash +# macOS / Linux +cp ~/.pub-cache/hosted/pub.dev/cometchat_sdk-/lib/assets/web/sqlite3mc.wasm web/sqlite3mc.wasm + +# Windows +copy %LOCALAPPDATA%\Pub\Cache\hosted\pub.dev\cometchat_sdk-\lib\assets\web\sqlite3mc.wasm web\sqlite3mc.wasm +``` + +Replace `` with your installed `cometchat_sdk` version, and copy the file again after every SDK upgrade — the asset must match the SDK version. Use the copy the SDK ships, not one from the `sqlite3` package — only the bundled build can key a database in the browser. If the asset is missing, the encrypted open fails and `encryptionFailureBehavior` decides what happens. On web the cipher is `ChaCha20-Poly1305` (native targets report `AES-256-CBC/HMAC-SHA512`) and the key is a non-extractable WebCrypto key, so `keyStorage` reads `OfflineKeyStorage.software`; both are real whole-database encryption. + +## Turn on encryption + +Set `encryption: true` on the `OfflineConfig` you pass to the SDK during initialization (see [Enable Offline Support](/sdk/flutter/offline-enable)). + +```dart +final offlineConfig = OfflineConfig( + enabled: true, + encryption: true, + encryptionFailureBehavior: EncryptionFailureBehavior.onlineOnly, +); +``` + +## Choose what happens when encryption cannot run + +`encryptionFailureBehavior` decides what the cache does when encryption **cannot be applied at all** on this build or device — for example the cipher engine is not linked, or secure key storage is unavailable. It governs only that case: a transient failure still retries, and a lost or wrong key self-heals by wiping and rebuilding the cache **encrypted**. + +| Value | Behavior | Writes plaintext? | +|---|---|---| +| `onlineOnly` *(default)* | Do not open a local cache at all — the session runs server-only. Makes a setup mistake visible: offline simply does not work until the cipher engine is added. | No | +| `plaintextFallback` | Open an **unencrypted** cache so offline keeps working, and log the downgrade at error level. The status API then reports `atRest: none` and `cipherAvailable: false` so the downgrade stays visible. | Yes | + + +`plaintextFallback` means cached message content may be stored **unencrypted** on disk whenever encryption cannot run. This availability-over-guarantee trade is wrong for compliance use — keep the default `onlineOnly` if cached content must never be written in the clear. + + +## Key storage + +You do not supply or manage the encryption key. The SDK generates a random 32-byte key, one per app ID and logged-in user (UID), stores it in the platform's secure key store, and never returns it from any API. The key never leaves the device. If the key is lost or the stored database no longer matches it, the cache resets and rebuilds — see [Cache resets](#cache-resets-on-key-loss). + +| Platform | Key store | +|---|---| +| Android | Android Keystore | +| iOS / macOS | Keychain | +| Windows | DPAPI | +| Linux | libsecret | +| Web | WebCrypto non-extractable key | + + +On all five native targets `keyStorage` reports `OfflineKeyStorage.keystore`, which names the *kind* of vault, not that hardware is involved: iOS and Android are hardware-backed, while Windows DPAPI, Linux libsecret and the macOS login keychain are OS software stores tied to the login. Web reports `OfflineKeyStorage.software`. + + +## Check encryption status + +`CometChat.getOfflineEncryptionStatus()` returns an `OfflineEncryptionStatus` describing what encryption is **actually** doing — never what was merely requested. It is safe to call at any time and never returns the key or any part of it. + +```dart +final status = CometChat.getOfflineEncryptionStatus(); +debugPrint("enabled=${status.enabled}" + " atRest=${status.atRest}" + " keyStorage=${status.keyStorage}" + " cipherAvailable=${status.cipherAvailable}" + " cipher=${status.cipher}"); +``` + +| Field | Type | Meaning | +|---|---|---| +| `enabled` | `bool` | What was **configured** (`OfflineConfig.encryption`), not what was achieved. | +| `atRest` | `OfflineAtRest` | Coverage actually achieved: `OfflineAtRest.sqlcipher` once an encrypted open is proven, otherwise `OfflineAtRest.none`. | +| `keyStorage` | `OfflineKeyStorage` | `OfflineKeyStorage.keystore` when a key was resolved from the OS vault, `.software` on web, `.none` when this session holds no key. Read `none` as "no key in use", not "no key exists". | +| `cipherAvailable` | `bool` | Whether the cipher engine is present and linked in this build. `false` when encryption is off or the `pubspec.yaml` hook / web asset is missing. | +| `cipher` | `String?` | The engine's cipher string once an encrypted open is proven (`AES-256-CBC/HMAC-SHA512` native, `ChaCha20-Poly1305` on web), else `null`. | + + +The single most useful support signal is `enabled == true` with `cipherAvailable == false`: encryption was switched on but could not run. On Flutter that almost always means the `hooks:` stanza was never added to the app's own `pubspec.yaml` (or, on web, the `sqlite3mc.wasm` asset is missing). + + + +The normative, cross-SDK wire strings behind these enums (`SQLCIPHER`, `NONE`, `KEYSTORE`, `SOFTWARE`, `ONLINE_ONLY`, `PLAINTEXT_FALLBACK`) are available through each value's `wireValue`, so an auditor gets the same answer from every CometChat SDK. + + +## What gets encrypted + +| Data | Encrypted at rest? | +|---|---| +| Message database (messages, conversations, metadata, the pending-send outbox) | Yes, when `atRest` is `OfflineAtRest.sqlcipher` | +| Downloaded media files on disk (images, video, audio, documents) | **No** — stored as ordinary files outside the database | + +Media bytes live outside the database because `Attachment.localPath` is public API and those paths are passed straight to native media loaders. "Encrypted at rest" here always means the message database, never the media cache. + +## Cache resets on key loss + +If the key can no longer decrypt the stored database, the SDK wipes the local cache and rebuilds it **encrypted** — it never silently falls back to plaintext for a cache that could be encrypted. Any unsent messages still queued in the outbox at that point are lost. + +How they are reported depends on the reason. When the reset is `encryptionChanged` (you turned encryption on or off between launches), each queued message fails with reason `discarded` and `pendingMessagesLost` is the exact count. For a key or corruption reset, the queued rows lived inside the unreadable database, so they cannot be counted one by one: they are lost silently and `pendingMessagesLost` is `null`. **`null` is not zero** — it means an unknown number were lost. + +Register an `OfflineCacheResetListener` to be told when this happens: + +```dart +class _MyCacheResetListener extends OfflineCacheResetListener { + @override + void onOfflineCacheReset(OfflineCacheResetReason reason, int? pendingMessagesLost) { + // pendingMessagesLost is null when the number lost is unknown, not zero. + debugPrint("Cache reset: $reason, pending lost=$pendingMessagesLost"); + } +} + +CometChat.addOfflineCacheResetListener("LISTENER_ID", _MyCacheResetListener()); +``` + +The cache-reset reasons are `keyLost`, `wrongKey`, `corrupted`, `escalated`, and `encryptionChanged` (turning encryption on or off between launches, which also discards any queued messages and the cached media). See [Stay in Sync](/sdk/flutter/offline-sync) for the full cache-reset flow, and the [API Reference](/sdk/flutter/offline-reference) for every field and enum. diff --git a/sdk/flutter/offline-media-caching.mdx b/sdk/flutter/offline-media-caching.mdx new file mode 100644 index 000000000..0695d8691 --- /dev/null +++ b/sdk/flutter/offline-media-caching.mdx @@ -0,0 +1,164 @@ +--- +title: "Media Caching" +sidebarTitle: "Media Caching" +description: "Cache image, video, audio and file attachments on the device so media stays viewable offline with the CometChat Flutter SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```dart +// Set the media caching mode at init (fixed for the session) +final offlineConfig = OfflineConfig( + enabled: true, + mediaCaching: MediaCachingMode.auto, // auto (default) or off + maxMediaCacheSize: 500 * 1024 * 1024, // bytes; 0 = unlimited +); + +// Read the mode in effect +final MediaCachingMode mode = CometChat.getMediaCachingMode(); // off when offline is not active + +// Resolve cached bytes for a message's attachment +final String? localPath = mediaMessage.attachment?.localPath; // hydrated automatically on reads +final String? cached = await CometChat.getCachedMediaPath(mediaMessage); // null if not cached + +// Download on demand +await CometChat.downloadMedia( + mediaMessage, + listener: MediaCallbackListener( + onSuccess: (localPath) {}, + onProgress: (percent) {}, + onError: (MediaException e) {}, + ), +); + +// Observe background (auto) downloads +CometChat.addMediaDownloadListener("MEDIA_LISTENER", _MyMediaDownloadListener()); +``` + + + +With offline support on, the SDK can cache attachment bytes durably on the device so media stays viewable offline — even after the server's presigned URL has expired. This page covers the caching modes, what downloads automatically, how to read cached bytes, and how to download media on demand. + + + +Media caching applies only while offline support is active for the logged-in user (`CometChat.isOfflineEnabled()` is `true`). See [Enable Offline Support](/sdk/flutter/offline-enable). + + + +## Caching modes + +The caching mode is set once, in the `OfflineConfig` you pass at initialization, and is fixed for the SDK session. There is no runtime switch — to change it, re-initialize the SDK with a new config. + +| `MediaCachingMode` | Behavior | +|---|---| +| `auto` (default) | Incoming media bytes are eagerly downloaded and cached on any network, then LRU-managed. The outgoing copy is retained after the server acknowledges the send. | +| `off` | Incoming media bytes are **not** cached. The message metadata (including the file URL) is still cached, so the message appears offline and the UI loads media live from the URL when online. The outgoing copy is dropped after the send is acknowledged. | + +Sending media always works in both modes. + +```dart +final offlineConfig = OfflineConfig( + enabled: true, + mediaCaching: MediaCachingMode.auto, + maxMediaCacheSize: 500 * 1024 * 1024, +); +``` + +Read the mode in effect at runtime with `CometChat.getMediaCachingMode()`. It returns `MediaCachingMode.off` whenever offline support is not active, whatever the app configured, so a UI layer can rely on it without reading the config itself. + +## What downloads automatically + +In `auto` mode, media bytes are downloaded and cached automatically on the two live receive paths: + +- a **realtime** media message received while connected, and +- opening a chat **online** (the server read refreshes and caches its media). + +A media message that arrives through a reconnect catch-up [sync](/sdk/flutter/offline-sync) is cached as **metadata only** and renders as a placeholder; its bytes download lazily the next time you open that chat online. This holds in both `auto` and `off`. + +In `off` mode, nothing is auto-downloaded on any path; media loads live from its URL when online. + +## Reading cached media + +When bytes are already cached, the SDK **hydrates the local path automatically** on every message it delivers — you do not have to ask. Read it straight off the attachment: + +```dart +final attachment = mediaMessage.attachment; +final localPath = attachment?.localPath; // non-null when bytes are cached +if (localPath != null) { + // Render directly from disk — no network fetch. +} +``` + +To look up the cached path explicitly, call `getCachedMediaPath`. It is a cheap filesystem check that never touches the network and returns `null` when the bytes are not cached (call `downloadMedia` to fetch them). The path is a cache hint that can disappear (LRU eviction, cache clear, auth wipe): re-resolve it on each render and never persist it in your own storage. + +```dart +final String? path = await CometChat.getCachedMediaPath(mediaMessage); // null if not cached +``` + + + +Each attachment of a multi-attachment message resolves independently. An attachment with no cached bytes keeps `localPath == null` and the UI loads it from the URL as before. + + + +## Downloading on demand + +Call `downloadMedia` to fetch and cache a message's attachment bytes — for example when the user taps a placeholder. It is honored in **both** caching modes (an explicit download always wins). The SDK resolves off the main isolate (cached bytes, then a valid URL, then, for a CometChat-hosted URL the server rejects as expired or forbidden (HTTP 403 or 410), one re-signed URL) and reports back through the `MediaCallbackListener` you pass. + +```dart +await CometChat.downloadMedia( + mediaMessage, + listener: MediaCallbackListener( + onSuccess: (localPath) { + // Bytes cached; render from localPath. + }, + onProgress: (percent) { + // 0..100, best-effort. + }, + onError: (MediaException e) { + // e.code is a typed MediaErrorCode. + }, + ), +); +``` + +`onProgress(int percent)` reports `0..100`, best-effort and monotonic non-decreasing. It is emitted only when the server reports a content length; for an unknown-length download it is skipped and the call goes straight to `onSuccess`. Progress never blocks or fails the download. + +`onError` receives a `MediaException` whose `code` is one of these typed `MediaErrorCode` values: + +| `MediaErrorCode` | Meaning | +|---|---| +| `offlineNotCached` | The device is offline and the bytes are not cached — expected, not a bug. | +| `noUrl` | The attachment carries no usable URL. | +| `resignFailed` | The presigned URL expired and could not be re-signed. | +| `downloadFailed` | The bytes could not be fetched or written. | +| `unavailable` | Offline support (or its media store) is not active. | + +## Observe background downloads + +In `auto` mode, incoming media downloads in the background with no per-call callback. Register a `MediaDownloadListener` to learn when the bytes land (or fail) so a UI tile can refresh. All callbacks fire on the main isolate. + +```dart +class _MyMediaDownloadListener extends MediaDownloadListener { + @override + void onMediaDownloaded(String? messageLocalId, String attachmentId, String localPath) { + // messageLocalId: the message's server id (or local id if not yet sent) + // attachmentId: the attachment URL without its query string + // localPath: where the bytes landed + } + + @override + void onMediaDownloadFailed(String? messageLocalId, String attachmentId, MediaException error) {} +} + +CometChat.addMediaDownloadListener("MEDIA_LISTENER", _MyMediaDownloadListener()); +``` + +Remove the listener with `CometChat.removeMediaDownloadListener("MEDIA_LISTENER")`. The background `MediaDownloadListener` carries no progress callback — progress is reported only on the explicit `downloadMedia` path above, and its `onMediaDownloadFailed` delivers a typed `MediaException` rather than a raw HTTP failure. + +## Media cache size limit + +Cached media bytes are bounded by `maxMediaCacheSize` (default **500 MB**; `0` means unlimited). When the cache exceeds the limit, the least recently accessed media is deleted first until it is back under budget. Media for un-sent (queued) messages is never evicted, though it still counts toward the budget; copies held for an in-progress upload are excluded from the budget. Because protected media is never evicted, the cache can briefly exceed `maxMediaCacheSize`. Bulk list loads and syncs do not refresh an item's recency. + +Media is stored as files in durable, app-private per-user storage; the location is not configurable. Eviction runs silently in the background — there is no cache-limit callback. To reason about size, use `CometChat.getCacheSize()` (a `Future` of the total local bytes held) and the configured limits. See the [configuration reference](/sdk/flutter/offline-reference) for the full set of options. diff --git a/sdk/flutter/offline-overview.mdx b/sdk/flutter/offline-overview.mdx new file mode 100644 index 000000000..307175c31 --- /dev/null +++ b/sdk/flutter/offline-overview.mdx @@ -0,0 +1,89 @@ +--- +title: "Offline Support" +sidebarTitle: "Offline Support" +description: "Understand how local caching, the send outbox, and reconnect catch-up keep chat working without a connection with the CometChat Flutter SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```dart +// Offline support is OFF by default. Turn it on at init (minimum SDK 5.1.0). +final offlineConfig = OfflineConfig(enabled: true); + +AppSettings appSettings = (AppSettingsBuilder() + ..region = "REGION" + ..setOfflineConfig(offlineConfig)) // or ..offline(true) for defaults + .build(); + +CometChat.init(appId, appSettings, + onSuccess: (String message) {}, onError: (CometChatException e) {}); + +// Is offline support active right now for the logged-in user? +final active = CometChat.isOfflineEnabled(); // bool +``` + + + +Offline support keeps the logged-in user's conversations and messages on the device so your app can show them instantly, lets the user send messages while disconnected, and catches up automatically when the connection returns. It is **off by default** — nothing changes in your app until you enable it ([Enable Offline Support](/sdk/flutter/offline-enable)). This page explains the mental model; the other pages show the exact APIs. + + +Offline support requires CometChat Flutter SDK **5.1.0** or later. + + +## How it works + +Three ideas carry the whole feature. + + + +Every read goes through the local cache first. For list reads the SDK delivers the cached page immediately, even when it is empty. Single-item lookups (`getMessageDetails`, `getConversation`) deliver a cached result only when that item is already cached. The SDK then fetches from the server and delivers the fresh result. Your UI renders what is on the device without waiting for the network, then updates when the server responds. See [Show Cached Data](/sdk/flutter/offline-cached-data). + + +A message sent while offline is saved to a durable outbox and reported as pending instead of failing. It survives app restarts and is sent when the connection returns. See [Send While Offline](/sdk/flutter/offline-send). + + +When the device reconnects, the SDK syncs the conversations and messages that changed while it was away and flushes the outbox, so the local cache matches the server again. See [Stay in Sync](/sdk/flutter/offline-sync). + + + +```mermaid +flowchart LR + A[App read] --> B[(Local cache)] + B -- cached page first --> A + B --> C{Online?} + C -- yes --> D[Server] + D -- fresh result --> A + C -- no --> E[onError:\nERR_CONNECTION] + F[Send while offline] --> G[(Durable outbox)] + G -- on reconnect --> D +``` + +## What changes in your app + +With offline support enabled, your fetch and send callbacks fire **twice** on the read path and gain a pending stage on the send path. To receive these cached and pending results, pass a `listener:` to the fetch and send methods — `MessageFetchListener`, `ConversationFetchListener`, `MessageSentListener`, and the others listed on each page. The `onSuccess`/`onError` form of the same method keeps working: it receives only the server result, and a send through it is never queued. With offline support off, every API behaves exactly as it did before. + +## One cache per user + +The SDK keeps a single local cache for the user who is logged in. + +- The cache survives app restarts while the same user stays logged in, so the data is there instantly on next launch. +- Logging out, a session expiring, or logging in as a **different** user wipes the cache, including any unsent messages still in the outbox, which are lost. You never clear it yourself — the SDK does it. + +See [Stay in Sync](/sdk/flutter/offline-sync) for cache resets and logout behaviour. + +## How much is kept + +By default the cache is bounded by size, not age: it holds up to **100 conversations**, up to **500 messages per conversation**, around **250 MB** of data, and up to **500 MB** of cached media. Older entries are evicted first as these limits are reached. Every limit is tunable — see the [Configuration & API Reference](/sdk/flutter/offline-reference). + +## Next steps + + + Turn it on and verify it is active. + The cached-first, server-next read contract. + The durable outbox and pending messages. + Reconnect catch-up, cache resets, logout. + Download and cache attachments for offline viewing. + Encrypt the local database at rest. + Every option, method, listener, enum, and error. + diff --git a/sdk/flutter/offline-reference.mdx b/sdk/flutter/offline-reference.mdx new file mode 100644 index 000000000..420357c50 --- /dev/null +++ b/sdk/flutter/offline-reference.mdx @@ -0,0 +1,205 @@ +--- +title: "Offline Configuration & API Reference" +sidebarTitle: "Configuration & API Reference" +description: "A complete reference for the offline configuration options, public methods, listeners, enums, model fields, and error codes with the CometChat Flutter SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```dart +// Build the config (every field has a default; set only what you change) +final offlineConfig = OfflineConfig( + enabled: true, + maxConversations: 100, + maxMessagesPerConversation: 500, + maxPendingMessages: 50, + mediaCaching: MediaCachingMode.auto, + encryption: false, +); + +// Introspection / control +CometChat.isOfflineEnabled(); // bool +CometChat.getMediaCachingMode(); // MediaCachingMode +CometChat.getOfflineEncryptionStatus(); // OfflineEncryptionStatus +CometChat.getSyncState(); // SyncState +CometChat.getLastSyncTimestamp(); // Future (device epoch ms) +CometChat.getCacheSize(); // Future (bytes) +CometChat.getPendingMessages(); // Future> +CometChat.forceSyncNow(); // or forceSyncNow(callback: …) +CometChat.clearOfflineCache(); +``` + + + +This page is the full surface of CometChat's offline support on Flutter: configuration options, public methods, listeners, enums, model fields, and error codes. The minimum SDK version is **5.1.0**, and offline support is **off by default**. For task-focused guides see [Enable Offline Support](/sdk/flutter/offline-enable), [Show Cached Data](/sdk/flutter/offline-cached-data), [Send While Offline](/sdk/flutter/offline-send), [Stay in Sync](/sdk/flutter/offline-sync), [Media Caching](/sdk/flutter/offline-media-caching), and [Encrypt the Cache](/sdk/flutter/offline-encryption). + +## OfflineConfig options + +Construct with the `const OfflineConfig({...})` named-parameter constructor. Every field is pre-set to its default, so pass only the ones you want to change; the resulting `OfflineConfig` is immutable. Byte caps and the TTL treat `<= 0` as **unlimited** (that dimension's eviction is disabled); the count caps are clamped and have no "unlimited". The shorthand `OfflineConfig.fromFlag(bool)` is the same as `offline(bool)`, and `OfflineConfig.disabled` is the off-by-default configuration. + +| Parameter | Type | Default | Range / clamp | Effect | +|---|---|---|---|---| +| `enabled` | `bool` | `false` | — | Master gate for the whole offline engine. With it off the SDK behaves exactly as before. | +| `cacheDurationDays` | `int` | `0` (unlimited) | `>= 0`; negative treated as `0` | Message retention TTL in days. `0` = no age-based prune; eviction is by the byte/count caps instead. | +| `maxStorageSize` | `int` bytes | `262144000` (250 MB) | `<= 0` = unlimited | Byte ceiling for the message/metadata database. | +| `maxMessagesPerConversation` | `int` | `500` | clamped `[1, 1000]`; `<= 0` or over → `1000` | Per-conversation cached-message window. | +| `maxConversations` | `int` | `100` | clamped `[1, 1000]`; `<= 0` or over → `1000` | Cached-conversation cap. | +| `maxPendingMessages` | `int` | `50` | clamped `[1, 100]`; `<= 0` or over → `100` | Pending-send outbox capacity. | +| `autoSendPendingOnReconnect` | `bool` | `true` | — | Auto-flush queued `PENDING` sends on reconnect. Gates only the outbox flush; the reconnect sync sweep runs regardless. | +| `pendingQueueOverflowPolicy` | `PendingMessageOverflowPolicy` | `rejectNewest` | — | What the outbox does when it is full. | +| `maxMediaCacheSize` | `int` bytes | `524288000` (500 MB) | `<= 0` = unlimited | Media-only byte budget (LRU). | +| `mediaCaching` | `MediaCachingMode` | `auto` | — | Receive-side media mode. Fixed at init. | +| `encryption` | `bool` | `false` | — | Encrypt the cache database at rest. Requires a build-hook stanza in your app's `pubspec.yaml`; without it, `encryptionFailureBehavior` decides what happens. See [Encrypt the Cache](/sdk/flutter/offline-encryption). | +| `encryptionFailureBehavior` | `EncryptionFailureBehavior` | `onlineOnly` | — | What happens when encryption cannot run at all. | +| `maxSnapshotQueries` | `int` | `100` | `>= 1`; `<= 0` → `100`; no upper clamp | Entity-list snapshots kept for offline replay, across users, groups, group members, banned members, reactions, the notification feed and threads with `setParticipatedByMe(false)`; LRU. | +| `snapshotTtlDays` | `int` | `7` | `>= 1`; `<= 0` → `7` | Days an entity-list snapshot is kept after it was first recorded. | + +Attach the config at init via `AppSettingsBuilder.setOfflineConfig(OfflineConfig)`, or enable with defaults via the `offline(bool)` shorthand — see [Enable Offline Support](/sdk/flutter/offline-enable). + +## Public methods on CometChat + +| Method | Returns | Purpose | +|---|---|---| +| `isOfflineEnabled()` | `bool` | Whether offline support is active for the current session. | +| `getMediaCachingMode()` | `MediaCachingMode` | The effective media caching mode (`off` whenever offline is not active). | +| `getOfflineEncryptionStatus()` | `OfflineEncryptionStatus` | What encryption at rest is actually doing, as typed fields (`enabled`, `atRest`, `keyStorage`, `cipherAvailable`, `cipher`). | +| `getPendingMessages()` | `Future>` | All queued/failed messages in the outbox. | +| `retryMessage(String localMessageId, {required MessageSentListener listener})` | `Future` | Retry one failed message by its local id; the outcome arrives on `listener`. | +| `retryAllFailedMessages()` | `Future` | Retry every failed message in the outbox; outcomes arrive on the registered `OfflineMessageListener`. | +| `discardLocalMessage(String localMessageId, {required onSuccess, required onError})` | `Future` | Remove one unsent message from the outbox. | +| `forceSyncNow({ForceSyncListener? callback})` | `Future` | Request a catch-up sync. Silently dropped if a sync is already running or one ran within the cooldown window, and a no-op when offline is not active. Pass `callback` to learn why a sync did not start. | +| `getSyncState()` | `SyncState` | Whether a catch-up sweep is currently running. | +| `getLastSyncTimestamp()` | `Future` | Device-clock time of the last successful sync, in epoch milliseconds; `null` if the cache has never synced. | +| `clearOfflineCache()` | `Future` | Wipe the local cache (and fail any unsent messages). | +| `getCacheSize()` | `Future` | Current on-disk cache size in bytes (database + media). | +| `downloadMedia(BaseMessage, {MediaCallbackListener? listener})` | `Future` | Download and cache a message's attachment bytes. | +| `getCachedMediaPath(BaseMessage)` | `Future` | Local path to already-cached bytes, or `null`. | +| `getExistingUploadFileRequest(String receiverId, String receiverType, {int? parentMessageId})` | `Future` | Re-attach to an in-progress offline upload (optionally for a thread parent). | + +On the returned `UploadFileRequest`, `getFiles()` lists each file as an `UploadFileView`, `retryAttachment(String fileId)` retries one failed file, and `addUploadListener(UploadFileListener)` observes progress. New batches are created with `CometChat.createUploadFileRequest`; see [Upload Files](/sdk/flutter/upload-files). + +### Listener registration + +| Method | Listener | +|---|---| +| `addMessageListener(String, MessageListener)` / `removeMessageListener(String)` | `MessageListener` (adds `onMessagesSynced`) | +| `addOfflineMessageListener(String, OfflineMessageListener)` / `removeOfflineMessageListener(String)` | `OfflineMessageListener` | +| `addSyncListener(String, SyncListener)` / `removeSyncListener(String)` | `SyncListener` | +| `addOfflineCacheResetListener(String, OfflineCacheResetListener)` / `removeOfflineCacheResetListener(String)` | `OfflineCacheResetListener` | +| `addMediaDownloadListener(String, MediaDownloadListener)` / `removeMediaDownloadListener(String)` | `MediaDownloadListener` | + +## Listeners and callbacks + +### Two-phase fetch listeners + +Each delivers the cached page first (when offline is active and a user is logged in), then the server result, then errors. Without a `listener:` the cached callback never fires. + +| Listener | Callbacks | +|---|---| +| `MessageFetchListener` | `onCachedMessages(List)`, `onServerMessages(List)`, `onError(CometChatException)` | +| `ThreadsFetchListener` | `onCachedThreads(List)`, `onServerThreads(List)`, `onError(CometChatException)` | +| `MessageDetailsListener` | `onCachedMessage(BaseMessage)`, `onServerMessage(BaseMessage)`, `onError(CometChatException)` | +| `ConversationFetchListener` | `onCachedConversations(List)`, `onServerConversations(List)`, `onError(CometChatException)` | +| `ConversationDetailsListener` | `onCachedConversation(Conversation)`, `onServerConversation(Conversation)`, `onError(CometChatException)` | +| `UserFetchListener` | `onCachedUsers(List)`, `onServerUsers(List)`, `onError(CometChatException)` | +| `BlockedUsersFetchListener` | `onCachedBlockedUsers(List)`, `onServerBlockedUsers(List)`, `onError(CometChatException)` | +| `GroupFetchListener` | `onCachedGroups(List)`, `onServerGroups(List)`, `onError(CometChatException)` | +| `GroupMemberFetchListener` | `onCachedGroupMembers(List)`, `onServerGroupMembers(List)`, `onError(CometChatException)` | +| `BannedGroupMemberFetchListener` | `onCachedBannedGroupMembers(List)`, `onServerBannedGroupMembers(List)`, `onError(CometChatException)` | +| `ReactionFetchListener` | `onCachedReactions(List)`, `onServerReactions(List)`, `onError(CometChatException)` | +| `NotificationFeedFetchListener` | `onCachedNotificationFeedItems(List)`, `onServerNotificationFeedItems(List)`, `onError(CometChatException)` | + +### Send, sync, cache, and media listeners + +| Listener | Callbacks | +|---|---| +| `MessageSentListener` | `onMessagePending(BaseMessage)`, `onMessageSent(BaseMessage)`, `onError(CometChatException)` | +| `OfflineMessageListener` | `onOutboxMessagePending(BaseMessage)`, `onOutboxMessageSent(BaseMessage)`, `onOutboxMessageFailed(BaseMessage, MessageFailureReason)` | +| `SyncListener` | `onConversationSyncStarted()`, `onConversationSyncCompleted(List)`, `onConversationSyncError(CometChatException)`, `onMessageSyncStarted()`, `onMessageSyncCompleted(List, SyncOutcome)`, `onMessageSyncError(CometChatException)` | +| `ForceSyncListener` | `onForceSyncComplete()`, `onForceSyncError(CometChatException)` | +| `OfflineCacheResetListener` | `onOfflineCacheReset(OfflineCacheResetReason, int? pendingMessagesLost)`; `pendingMessagesLost` is `null` when the number cannot be known (not zero) | +| `MediaCallbackListener` | `onSuccess(String localPath)`, `onProgress(int percent)`, `onError(MediaException)` | +| `MediaDownloadListener` | `onMediaDownloaded(String? messageLocalId, String attachmentId, String localPath)`, `onMediaDownloadFailed(String? messageLocalId, String attachmentId, MediaException)` | +| `MessageListener` | `onMessagesSynced(List)` (offline addition to the existing listener) | + +## Enums + +| Enum | Values | +|---|---| +| `MediaCachingMode` | `auto` (auto-download + retain received media), `off` (skip auto-download; metadata still cached) | +| `MediaErrorCode` | `offlineNotCached`, `noUrl`, `resignFailed`, `downloadFailed`, `unavailable` | +| `PendingMessageOverflowPolicy` | `dropOldest` (evict the oldest pending to admit a newer send), `rejectNewest` (reject the new send) | +| `EncryptionFailureBehavior` | `onlineOnly` (server-only; never plaintext), `plaintextFallback` (open an unencrypted cache) | +| `CometChatMessageStatus` | `pending`, `sending`, `sent`, `delivered`, `read`, `failed` | +| `MessageFailureReason` | `transient` (retry likely helps), `permanent` (retry won't help), `queueOverflow` (evicted to admit a newer send), `queueFull` (new send rejected, queue full), `byteOverflow` (media exceeded the byte cap), `discarded` (removed before it could send) | +| `SyncOutcome` | `caughtUp` (gap replayed exactly), `partial` (a page was ingested but more remains) | +| `SyncState` | `idle`, `syncing` | +| `OfflineCacheResetReason` | `keyLost`, `wrongKey`, `corrupted`, `escalated` (unclassified failure recurred and was escalated), `encryptionChanged` (the `encryption` setting changed between launches) | +| `OfflineAtRest` | `none`, `sqlcipher`, `fieldAesGcm`, `opfsIsolation` | +| `OfflineKeyStorage` | `keystore`, `software`, `asyncStorage`, `none` | +| `UploadFileState` | `queued`, `requestingUrl`, `uploading`, `uploaded`, `attached`, `rejected`, `failed`, `cancelled` | + +On Flutter, `OfflineAtRest` is only ever `sqlcipher` or `none`, and `OfflineKeyStorage` only `keystore`, `software`, or `none`. The `fieldAesGcm`, `opfsIsolation`, and `asyncStorage` values exist for cross-SDK parity and are never reported by the Flutter SDK. + +## Model fields + +| Field | Type | Meaning | +|---|---|---| +| `BaseMessage.localMessageId` | `String?` | SDK-generated local handle for an optimistic/offline send; the stable key to reconcile the row when the server ack arrives. `null` for messages that never went through the outbox. | +| `BaseMessage.status` | `CometChatMessageStatus` | Lifecycle status of the message. | +| `BaseMessage.failureReason` | `MessageFailureReason?` | Why the message failed; non-`null` only while `status` is `failed`, otherwise `null`. | +| `Attachment.localPath` | `String?` | Absolute device path to cached media bytes, or `null` when not cached. Never sent to the server. | + +## Error codes + +Compare `CometChatException.code` against these values. The offline send and sync codes are exposed as constants on `OfflineSendError` and `OfflineSyncError`; the rest are raw strings. + +| Code | Raised when | +|---|---| +| `ERR_CONNECTION` | A read reached the server phase with no connection (fires after the cached page on a two-phase fetch; the details carry `ERROR_INTERNET_UNAVAILABLE`). On a send made with a `listener:`, it means the message was queued — `onMessageSent` will still fire once the connection returns. On an upload, it means the transfer hit a network error. | +| `ERROR_OFFLINE_SYNC_IN_PROGRESS` | `forceSyncNow(callback: …)` was called while a sync is already running (`OfflineSyncError.inProgress`). | +| `ERROR_OFFLINE_SYNC_COOLDOWN` | `forceSyncNow(callback: …)` was called within the throttle cooldown (`OfflineSyncError.cooldown`). | +| `ERROR_OFFLINE_DISABLED` | An offline-only operation was called while offline support is not active (`OfflineSyncError.disabled`). | +| `ERROR_OFFLINE_MESSAGE_DISCARDED` | A queued message was removed before it could send (`OfflineSendError.discarded`). | +| `ERROR_OFFLINE_MESSAGE_IN_FLIGHT` | `retryMessage` / `discardLocalMessage` targeted a message that is still being sent (`OfflineSendError.inFlight`). | +| `ERROR_OFFLINE_MESSAGE_NOT_FOUND` | `retryMessage` / `discardLocalMessage` found no unsent message with the given local id (`OfflineSendError.notFound`). | +| `ERROR_OFFLINE_MESSAGE_PERSIST_FAILED` | The local store was briefly unavailable, so `retryMessage` / `discardLocalMessage` changed nothing. Try again (`OfflineSendError.persistFailed`). | +| `ERROR_OUTBOX_OVERFLOW` | A send was refused because the outbox is full; the reason is `queueFull`. | +| `ERROR_OUTBOX_BYTE_OVERFLOW` | A media send was refused because it exceeds the media byte budget; the reason is `byteOverflow`. | +| `ERROR_OUTBOX_MEDIA_COPY_FAILED` | A media send could not be queued because its file could not be copied. | +| `ERROR_USER_NOT_LOGGED_IN` | An offline-only call was made with no logged-in user. | +| `ERR_REQUEST_IN_PROGRESS` | A `MessagesRequest` or `ThreadsRequest` fetch was started while an earlier fetch on the same request object is still running. | +| `ERROR_REQUEST_IN_PROGRESS` | The same, on `ReactionsRequest` and `NotificationFeedRequest`. | + +Media downloads fail with a typed `MediaException` carrying a `MediaErrorCode` (`offlineNotCached`, `noUrl`, `resignFailed`, `downloadFailed`, `unavailable`) rather than a string code — see [Media Caching](/sdk/flutter/offline-media-caching). + +## Cached results and the `listener:` parameter + +The two-phase fetch, single-item, and send methods each take an optional `listener:`. Pass one to receive the cached result first (when offline support is on), then the server result. Keep using `onSuccess` / `onError` instead when you only want the server result — those parameters work unchanged, and a send made through them is never queued. + +| Method | `onSuccess` / `onError` form | `listener:` form | +|---|---|---| +| `MessagesRequest.fetchNext` / `fetchPrevious` | server result only | `listener: MessageFetchListener` | +| `ThreadsRequest.fetchNext` | server result only | `listener: ThreadsFetchListener` | +| `ConversationsRequest.fetchNext` | server result only | `listener: ConversationFetchListener` | +| `UsersRequest.fetchNext` | server result only | `listener: UserFetchListener` | +| `BlockedUsersRequest.fetchNext` | server result only | `listener: BlockedUsersFetchListener` | +| `GroupsRequest.fetchNext` | server result only | `listener: GroupFetchListener` | +| `GroupMembersRequest.fetchNext` | server result only | `listener: GroupMemberFetchListener` | +| `BannedGroupMembersRequest.fetchNext` | server result only | `listener: BannedGroupMemberFetchListener` | +| `ReactionsRequest.fetchNext` | server result only | `listener: ReactionFetchListener` | +| `NotificationFeedRequest.fetchNext` | server result only | `listener: NotificationFeedFetchListener` | +| `CometChat.getConversation` | server result only | `listener: ConversationDetailsListener` | +| `CometChatHelper.getMessageDetails` | server result only | `listener: MessageDetailsListener` | +| `CometChat.sendMessage` / `sendMediaMessage` / `sendCustomMessage` / `sendInteractiveMessage` | direct send (never queued) | `listener: MessageSentListener` (durable outbox) | + +### Deprecated members + +Three members carry a `@Deprecated` annotation; each delegates to the method above, so migrate by moving the `listener:` to the standard method. + +| Member | Deprecation guidance | +|---|---| +| `MessagesRequest.fetch({required MessageFetchListener listener})` | Pass the listener to `fetchPrevious` instead: `fetchPrevious(listener: myMessageFetchListener)`. The two-phase read now lives on the standard fetch methods. | +| `ConversationsRequest.fetch({required ConversationFetchListener listener})` | Pass the listener to `fetchNext` instead: `fetchNext(listener: myConversationFetchListener)`. The two-phase read now lives on the standard fetch method. | +| `CometChat.sendMessageWithListener(BaseMessage, {required MessageSentListener listener})` | Pass the listener to the typed send method instead: `sendMessage(message, listener: myMessageSentListener)` — likewise `sendMediaMessage` / `sendCustomMessage` / `sendInteractiveMessage`. The durable outbox now lives on the standard send methods. | diff --git a/sdk/flutter/offline-send.mdx b/sdk/flutter/offline-send.mdx new file mode 100644 index 000000000..9a025bf1d --- /dev/null +++ b/sdk/flutter/offline-send.mdx @@ -0,0 +1,336 @@ +--- +title: "Send While Offline" +sidebarTitle: "Send While Offline" +description: "Queue messages sent without a connection, track their status, and retry failed sends with the CometChat Flutter SDK." +--- + +import FlutterOfflineSend from '/snippets/sdk/flutter-offline-send.mdx'; + +{/* TL;DR for Agents and Quick Reference */} + + +```dart +// Send with the offline-aware listener (queued when offline, auto-sent on reconnect) +class ChatSendListener extends MessageSentListener { + @override + void onMessagePending(BaseMessage message) { /* status == pending, fires fast */ } + @override + void onMessageSent(BaseMessage message) { /* server ack: real id + sentAt */ } + @override + void onError(CometChatException e) { /* ERR_CONNECTION = queued, sends on reconnect */ } +} + +CometChat.sendMessage(textMessage, listener: ChatSendListener()); + +// Read offline state off a message +final localId = message.localMessageId; // stable client handle while offline is running +final status = message.status; // CometChatMessageStatus: pending, sending, sent, delivered, read, failed +final reason = message.failureReason; // MessageFailureReason, set only while failed + +// Inspect and recover the outbox +final pending = await CometChat.getPendingMessages(); // pending + sending + failed +CometChat.retryMessage(localId, listener: ChatSendListener()); // one message +CometChat.retryAllFailedMessages(); // transient failures only +CometChat.discardLocalMessage(localId, onSuccess: (id) {}, onError: (e) {}); // delete an unsent message + +// Observe every outbox transition, app-wide +class OutboxWatcher extends OfflineMessageListener { + @override + void onOutboxMessagePending(BaseMessage message) {} + @override + void onOutboxMessageSent(BaseMessage message) {} + @override + void onOutboxMessageFailed(BaseMessage message, MessageFailureReason reason) {} +} + +CometChat.addOfflineMessageListener("LISTENER_ID", OutboxWatcher()); +``` + + + +With offline support enabled, a message you send while the device has no connection is **durably queued on the device** instead of failing. It keeps its place across an app restart and is sent automatically once the connection is back and stable (with `autoSendPendingOnReconnect` on, the default). This page shows the offline-aware send listener, how to read a message's status, how to inspect and recover the send queue, and the queue limits. + + + +Offline support is **off by default** and requires `cometchat_sdk` **5.1.0** or newer. Queuing only happens once you [enable it](/sdk/flutter/offline-enable); with offline off, a send over no connection fails exactly as before. + + + + + +## Send a Message + +Pass a `MessageSentListener` to `sendMessage` through the `listener:` parameter. Its lifecycle is normally `onMessagePending` first (the message is durably queued), then its outcome: `onMessageSent` (the server accepted it) or `onError`. Offline, `onError` is a queued-not-failed notice and `onMessageSent` still fires later on reconnect, so both can fire for one send (see below). If the message cannot be queued at all, for example because its media could not be copied (code `ERROR_OUTBOX_MEDIA_COPY_FAILED`, reason `permanent`), only `onError` fires. + + + +```dart +class ChatSendListener extends MessageSentListener { + @override + void onMessagePending(BaseMessage message) { + // Queued and persisted; message.status == CometChatMessageStatus.pending + debugPrint("Pending, local id: ${message.localMessageId}"); + } + + @override + void onMessageSent(BaseMessage message) { + // Server ack: message now carries its real id and sentAt + debugPrint("Sent: ${message.id}"); + } + + @override + void onError(CometChatException e) { + // ERR_CONNECTION means queued, not failed; real failures arrive in onOutboxMessageFailed + debugPrint("Send error: ${e.code}"); + } +} + +final textMessage = TextMessage( + text: "Hello!", + receiverUid: "UID", + receiverType: CometChatConversationType.user, + type: CometChatMessageType.text, +); + +try { + await CometChat.sendMessage(textMessage, listener: ChatSendListener()); +} on CometChatException catch (e) { + // A send with a listener returns a Future that throws when the attempt gets + // no answer. Code ERR_CONNECTION here means the message is still queued and + // onMessageSent will fire on reconnect — it is not a failure. + debugPrint("Send threw: ${e.code}"); +} +``` + + + + + +A send with a `listener:` returns a `Future`: it resolves with the server-acked message once the send is answered, and **rejects** (throws) when the send is attempted but gets no answer — the row stays durably `pending` and `onMessageSent` fires on a later reconnect. A send without a `listener:` is a direct send and is never queued. + +The same `MessageSentListener` is accepted by all four persistent send methods: + + + +```dart +CometChat.sendMessage(textMessage, listener: listener); // TextMessage +CometChat.sendMediaMessage(mediaMessage, listener: listener); // MediaMessage +CometChat.sendCustomMessage(customMessage, listener: listener); // CustomMessage +CometChat.sendInteractiveMessage(interactiveMessage, listener: listener); // InteractiveMessage +``` + + + + + +## Queued, Not Failed + +When the device is offline, the send still fires `onMessagePending`, then fires `onError` with the code `ERR_CONNECTION` (details `ERROR_INTERNET_UNAVAILABLE`) for that attempt. This is a **notification, not a failure**: the message stays durably `pending`, and `onMessageSent` fires **later**, on reconnect, when it actually goes. This is the default behaviour, with `autoSendPendingOnReconnect` on. With it off, an offline send becomes `failed` (`transient`) right away, and you recover it with `retryMessage`. + +So for a single send, `onError` and `onMessageSent` can **both** fire (offline, then reconnect), and `onError` does not always mean the message failed. Tell the two apart like this: + +| Signal | Meaning | What to do | +|---|---|---| +| `onError` with code `ERR_CONNECTION` | Queued; not sent yet. Auto-sends on reconnect (with `autoSendPendingOnReconnect` on, the default). | Show as sending/queued; nothing to do. | +| `OfflineMessageListener.onOutboxMessageFailed(message, reason)` | A real failure; will not retry on its own. `message.status` is `failed` and `message.failureReason` says why. | Offer the user a manual retry (see below). | + +Every failure reaches `onOutboxMessageFailed`, whatever its cause. Match it to the message in your UI by `localMessageId`: the SDK sets it on the message you passed to `sendMessage` before `onMessagePending` fires. The `status` of that passed-in object is not updated; read status from the messages the SDK delivers (`onMessagePending`, `onOutboxMessageFailed`, `getPendingMessages()`). + + + +```dart +// In your MessageSentListener +@override +void onError(CometChatException e) { + if (e.code == "ERR_CONNECTION") { + // Queued offline — it will send automatically on reconnect + } +} + +// App-wide: every real failure lands here +class OutboxFailures extends OfflineMessageListener { + @override + void onOutboxMessagePending(BaseMessage message) {} + + @override + void onOutboxMessageSent(BaseMessage message) {} + + @override + void onOutboxMessageFailed(BaseMessage message, MessageFailureReason reason) { + // message.status == CometChatMessageStatus.failed; surface a retry action + // for the UI row whose localMessageId matches message.localMessageId + } +} +``` + + + + + +### Message status and failure reason + +Every message exposes its offline state. `localMessageId` is the SDK-generated handle that is stamped at compose (while offline is running) and stays stable across the whole lifecycle — use it to retry or discard the message. + +| Accessor | Type | Notes | +|---|---|---| +| `localMessageId` | `String?` | Client handle; present while offline is running. | +| `status` | `CometChatMessageStatus` | `pending`, `sending`, `sent`, `delivered`, `read`, `failed`. | +| `failureReason` | `MessageFailureReason?` | Set only while `failed`; otherwise `null`. | + +`MessageFailureReason` values: + +| Reason | When | +|---|---| +| `transient` | A temporary server error (5xx, or 429 rate-limited), no answer after 5 automatic reconnect attempts, or, with `autoSendPendingOnReconnect` off, no answer at all. Retryable. | +| `permanent` | The server rejected the message, the error was not recognized, or the message's media could not be copied for the offline send. A manual retry will not help. | +| `queueFull` | A new send was refused because the queue is full (count cap). | +| `queueOverflow` | An older queued message was evicted to admit a newer one (`dropOldest` only). | +| `byteOverflow` | A media send was refused because caching its bytes would exceed `maxMediaCacheSize`, the budget for the whole media cache (received and queued media). | +| `discarded` | A queued message was removed before it could send: by `clearOfflineCache`, by leaving or deleting its conversation, or by logout. Reported with code `ERROR_OFFLINE_MESSAGE_DISCARDED`. (`discardLocalMessage` reports `onSuccess` instead.) | + +## Inspect the Queue + +`getPendingMessages` returns the **entire un-acked outbox** — every `pending`, `sending`, and `failed` message, ordered by local compose time. Sent messages are not included. Read each message's `status` to group them. + + + +```dart +final messages = await CometChat.getPendingMessages(); +for (final m in messages) { + debugPrint("${m.localMessageId} -> ${m.status}"); +} +``` + + + + + +### Observe the queue app-wide + +The per-call `MessageSentListener` lives only as long as the call. To watch every queue transition — a live send, a reconnect auto-flush, a manual retry, or a resumed send after an app restart — register a global `OfflineMessageListener`. Its failed callback carries the `MessageFailureReason`. + + + +```dart +class OutboxWatcher extends OfflineMessageListener { + @override + void onOutboxMessagePending(BaseMessage message) {} + + @override + void onOutboxMessageSent(BaseMessage message) {} + + @override + void onOutboxMessageFailed(BaseMessage message, MessageFailureReason reason) { + debugPrint("Failed: ${message.localMessageId} ($reason)"); + } +} + +CometChat.addOfflineMessageListener("LISTENER_ID", OutboxWatcher()); + +// Remove it when no longer needed +CometChat.removeOfflineMessageListener("LISTENER_ID"); +``` + + + + + +## Retry and Discard + +There is no automatic retry of a `failed` message — recovery is explicit and user-invoked. A `failed` message stays in the outbox until you retry or discard it. Use the message's `localMessageId`. + +- **`retryMessage(localMessageId, listener: ...)`** re-attempts one message. A `failed` message is reset to `pending` and sent; a `pending` message is attempted now rather than waiting for a reconnect. The listener receives that attempt's outcome. +- **`retryAllFailedMessages()`** retries **transient** failures only; permanent ones are skipped. It takes no listener — outcomes arrive on the global `OfflineMessageListener`. +- **`discardLocalMessage(localMessageId, onSuccess: ..., onError: ...)`** deletes an unsent (`pending` or `failed`) message from the device, including its copied media. `onSuccess` receives the `localMessageId`. + + + +```dart +CometChat.retryMessage(localMessageId, listener: ChatSendListener()); + +CometChat.retryAllFailedMessages(); + +CometChat.discardLocalMessage( + localMessageId, + onSuccess: (discardedLocalId) {}, + onError: (e) { + debugPrint("Discard rejected: ${e.code}"); + }, +); +``` + + + + + +`retryMessage` and `discardLocalMessage` reject, changing nothing, when: + +| Error code | Reason | +|---|---| +| `ERROR_OFFLINE_MESSAGE_IN_FLIGHT` | The message is already sending (`sending`). | +| `ERROR_OFFLINE_MESSAGE_NOT_FOUND` | No unsent message has that id (unknown, or already sent). | +| `ERROR_OFFLINE_DISABLED` | Offline support is not running. | +| `ERROR_OFFLINE_MESSAGE_PERSIST_FAILED` | The local cache was briefly unavailable. Try again. | + +## Auto-send on Reconnect + +By default, queued `pending` messages are sent automatically when the realtime connection is re-established. This is controlled by `OfflineConfig.autoSendPendingOnReconnect` (default `true`); see [Enable Offline Support](/sdk/flutter/offline-enable) and the [reference](/sdk/flutter/offline-reference). A queued message that keeps getting no answer is retried on each reconnect, up to 5 times, then becomes `failed` (`transient`). With it off, nothing retries a queued message on reconnect — a send that gets no answer becomes `failed` (`transient`), which you recover with `retryMessage`. + +Reconnect also runs a catch-up sync for received messages — see [Stay in Sync](/sdk/flutter/offline-sync). + +## Queue Limits + +The outbox holds at most `maxPendingMessages` messages still trying to send (`pending` + `sending`); `failed` messages do not count. When the queue is full, `OfflineConfig.pendingQueueOverflowPolicy` decides what happens: + +| `PendingMessageOverflowPolicy` | Behaviour on overflow | +|---|---| +| `rejectNewest` (**default**) | The new send is refused and reported with reason `queueFull`. Nothing already queued is lost. | +| `dropOldest` | The oldest `pending` message is evicted to admit the new one, and reported with `onError` (reason `queueOverflow`). If there is no `pending` message to evict, the new send is refused with `queueFull`. | + +A count-based refusal or eviction is reported with code `ERROR_OUTBOX_OVERFLOW` (the exact reason is in the exception details). A media send whose bytes would push the media cache over `maxMediaCacheSize` is refused with reason `byteOverflow` and code `ERROR_OUTBOX_BYTE_OVERFLOW`; nothing is evicted to make room. + +A **refused** send (`queueFull` / `byteOverflow`) is never queued, so it fires **no** `onMessagePending`; it reports `onError`, and the global `OfflineMessageListener` receives `onOutboxMessageFailed(message, reason)` carrying the exact reason. + + + +A refused or evicted message (`queueFull`, `queueOverflow`, `byteOverflow`) has **no durable row** — it does not appear in `getPendingMessages`, and calling `retryMessage` on its id fails with `ERROR_OFFLINE_MESSAGE_NOT_FOUND`. Capture these from the failure callback and handle them at that moment. Prefer the default `rejectNewest`, which never silently drops an already-queued message from another conversation. + + + +## Edits, Deletes, and Reactions Need a Connection + +Offline queuing applies to **sending new messages** only. Changes to existing messages — edit, delete, add or remove a reaction — and other own actions are **not queued offline**. They are sent to the server whenever you trigger them, and they report the not-connected error if the request gets no answer. Their effect reaches the cache only once the server accepts them. + +## Durable Media Uploads + +A multi-attachment upload survives an app restart, but it never resumes on its own. Fetch the existing request for a conversation with `getExistingUploadFileRequest`, and read each file's progress through `getFiles()`, which returns a list of `UploadFileView` (each with a `state`: `queued`, `requestingUrl`, `uploading`, `uploaded`, `attached`, `cancelled`, `failed`, or `rejected`, which cannot be retried). Retry an individual file with `retryAttachment(fileId)`, and observe progress by registering an `UploadFileListener` with `addUploadListener`. + + + +```dart +final request = await CometChat.getExistingUploadFileRequest( + "UID", + CometChatConversationType.user, +); + +if (request != null) { + for (final item in request.getFiles()) { + debugPrint("${item.fileId} -> ${item.state}"); + if (item.state == UploadFileState.failed) { + request.retryAttachment(item.fileId); + } + } +} +``` + + + + + +See [Media Caching](/sdk/flutter/offline-media-caching) for downloading and caching received attachments, and [Upload Files & Send Attachments](/sdk/flutter/upload-files) for building a multi-attachment message. + + + +For the full list of offline methods, listeners, enums, model fields, and error codes, see the [Offline Configuration & API Reference](/sdk/flutter/offline-reference). + + diff --git a/sdk/flutter/offline-sync.mdx b/sdk/flutter/offline-sync.mdx new file mode 100644 index 000000000..c0ae70827 --- /dev/null +++ b/sdk/flutter/offline-sync.mdx @@ -0,0 +1,272 @@ +--- +title: "Stay in Sync" +sidebarTitle: "Stay in Sync" +description: "Catch up on everything that changed while offline, observe sync progress, trigger a manual sync, and manage the local cache with the CometChat Flutter SDK." +--- + +{/* TL;DR for Agents and Quick Reference */} + + +```dart +// Observe the reconnect catch-up sync (conversation phase, then message phase) +class ChatSyncListener extends SyncListener { + @override + void onConversationSyncCompleted(List conversations) { /* blind-replace list */ } + @override + void onMessageSyncCompleted(List newPlusUpdates, SyncOutcome outcome) {} + @override + void onMessageSyncError(CometChatException error) {} +} +CometChat.addSyncListener("SYNC_LISTENER", ChatSyncListener()); +CometChat.removeSyncListener("SYNC_LISTENER"); + +// Poll sync state +final state = CometChat.getSyncState(); // SyncState.idle | SyncState.syncing +final lastSync = await CometChat.getLastSyncTimestamp(); // device epoch ms, null if never + +// Trigger a manual catch-up (with an acceptance callback) +class ForceSync extends ForceSyncListener { + @override + void onForceSyncComplete() {} + @override + void onForceSyncError(CometChatException error) { /* inProgress / cooldown / disabled / not-logged-in */ } +} +CometChat.forceSyncNow(callback: ForceSync()); + +// Observe cache rebuilds +class ResetWatcher extends OfflineCacheResetListener { + @override + void onOfflineCacheReset(OfflineCacheResetReason reason, int? pendingMessagesLost) {} +} +CometChat.addOfflineCacheResetListener("RESET_LISTENER", ResetWatcher()); + +// Inspect / clear the cache +final bytes = await CometChat.getCacheSize(); +CometChat.clearOfflineCache(); // destructive; drops the un-sent outbox +``` + + + +When the device comes back online, the SDK runs a **catch-up sync**: it refreshes the conversation list, then replays everything that changed in your messages since the last sync, and merges the results into the local cache. This page covers how to observe that sync, trigger one on demand, and manage the cache it maintains. + + + +Everything on this page is a no-op unless offline support is active for the logged-in user (`CometChat.isOfflineEnabled()` is `true`). See [Enable Offline Support](/sdk/flutter/offline-enable). + + + +## How catch-up works + +A sync runs automatically whenever the connection settles, including the first connect after login or launch. It never runs on a brief connect-then-drop flap, and overlapping triggers coalesce: at most one follow-up sweep is queued behind the running one. It proceeds in two phases, in order: + +1. **Conversation phase** — the conversation list is fetched as one fresh page. A conversation-list UI can blind-replace its list with the result. +2. **Message phase** — the SDK replays the message changelog from the last sync point. New messages and updates to already-cached messages are merged into the cache. + +The sync is incremental and safe to repeat: it only moves forward from the last successful point, and a failed or interrupted phase simply resumes on the next reconnect. The conversation refresh does remove conversations you are no longer part of, along with their cached messages, media, and any still-queued messages for them (which fail with reason `discarded`). A first-ever sync (cold start) seeds its starting point from the newest conversation and fetches no message history — older history is reached by scrolling online. + +## Listen for sync progress + +Register a `SyncListener` to observe the lifecycle of every automatic and manual sync. Each phase fires `started`, then exactly one of `completed` / `error`, all on the main isolate. Override only the callbacks you need. + + + +```dart +class ChatSyncListener extends SyncListener { + @override + void onConversationSyncStarted() {} + + @override + void onConversationSyncCompleted(List conversations) { + // The full, fresh conversation page. Blind-replace your list. + } + + @override + void onConversationSyncError(CometChatException error) {} + + @override + void onMessageSyncStarted() {} + + @override + void onMessageSyncCompleted(List newPlusUpdates, SyncOutcome outcome) { + // New + changed messages reconciled this sync. Merge by message id. + } + + @override + void onMessageSyncError(CometChatException error) {} +} + +CometChat.addSyncListener("SYNC_LISTENER", ChatSyncListener()); +``` + + + + +| Callback | When it fires | +|---|---| +| `onConversationSyncStarted()` | The conversation refresh began. | +| `onConversationSyncCompleted(List conversations)` | The refresh succeeded; `conversations` is the full conversation page. | +| `onConversationSyncError(CometChatException)` | The refresh failed. A transport failure leaves the cache untouched; a malformed response still merges the rows it could parse but removes nothing. Either way the refresh retries on the next reconnect. | +| `onMessageSyncStarted()` | The message changelog sweep began. Never fires on a cold start. | +| `onMessageSyncCompleted(List newPlusUpdates, SyncOutcome)` | The sweep finished; `newPlusUpdates` carries the new and changed messages. | +| `onMessageSyncError(CometChatException)` | The sweep failed; it resumes on the next reconnect. | + +The two phases are independent — a conversation-phase error does not stop the message phase. A phase error is informational: the cache is never left in a wrong state and the sweep resumes automatically. + +The `SyncListener` is self-sufficient: `onMessageSyncCompleted` carries the swept messages, including `action` and `call` messages that the `MessageListener` does not deliver, so you can use it without a `MessageListener`. + +`onMessageSyncCompleted` reports the outcome: + +| `SyncOutcome` | Meaning | +|---|---| +| `caughtUp` | The changelog was fully replayed; the cache reflects every server change up to the sync point. | +| `partial` | A page was ingested and the sync point advanced, but a backlog remains. Each sync, automatic or from `forceSyncNow`, drains one page of up to 1000 changes. | + +Remove the listener with `CometChat.removeSyncListener("SYNC_LISTENER")`. + +## Receiving synced messages + +If you already use a `MessageListener`, synced messages still arrive there, so you usually do not need a `SyncListener` to keep your chat up to date: + +- **Genuinely new** messages arrive one-by-one on the typed callbacks (`onTextMessageReceived`, `onMediaMessageReceived`, `onCustomMessageReceived`, …), each tagged with source `'SYNC'`. +- **Updates** to already-cached messages arrive together on `onMessagesSynced(List)`. + + + +```dart +class ChatMessageListener with MessageListener { + @override + void onTextMessageReceived(TextMessage message) { + if (message.source == 'SYNC') { + // Delivered by a catch-up sync. Suppress notification spam if you wish. + } + } + + // Do the same in onMediaMessageReceived, onCustomMessageReceived and the other typed callbacks. + + @override + void onMessagesSynced(List messages) { + // Updates to already-cached messages. Merge by id. + } +} + +CometChat.addMessageListener("MSG_LISTENER", ChatMessageListener()); +``` + + + + + +On reconnect, queued messages are sent first, then the sync runs. `forceSyncNow` runs only the sync; it does not send queued messages (use `retryMessage` or `retryAllFailedMessages` for those). A message deleted on the server reaches you as an update on `onMessagesSynced`, not as a removal. + + +## Check sync state + +Poll the current sync state and the time of the last completed sync at any time. + + + +```dart +final SyncState state = CometChat.getSyncState(); // SyncState.idle or SyncState.syncing +final int? lastSync = await CometChat.getLastSyncTimestamp(); // device epoch ms; null if never synced +``` + + + + +`getSyncState()` returns `SyncState.syncing` while a sweep is in flight, otherwise `SyncState.idle` (also when offline support is disabled). `getLastSyncTimestamp()` returns the time of the last completed message-sync as a device-clock epoch value in **milliseconds**, and `null` if the cache has never synced. It is informational — a value to show in a UI, not a cursor the sync depends on. + +## Trigger a manual sync + +Call `forceSyncNow` to run a catch-up immediately — for example behind a pull-to-refresh. It routes through the same throttle as the automatic sync, so it will not start a second overlapping sweep or hammer the backend. + +Pass a `ForceSyncListener` to learn whether the request was accepted; the sync's actual progress and outcome still arrive on your registered `SyncListener`. + + + +```dart +class ForceSync extends ForceSyncListener { + @override + void onForceSyncComplete() { + // Accepted and started. + } + + @override + void onForceSyncError(CometChatException error) { + // Rejected — see the code. + } +} + +CometChat.forceSyncNow(callback: ForceSync()); +``` + + + + +If you do not need the acceptance result, call `CometChat.forceSyncNow()` with no `callback`; a rejected request is silently dropped. + +| Rejection code | Meaning | +|---|---| +| `ERROR_OFFLINE_SYNC_IN_PROGRESS` | A sync (automatic or manual) is already running. | +| `ERROR_OFFLINE_SYNC_COOLDOWN` | A sync ran too recently; the exception details carry the remaining cooldown in milliseconds (retry-after). | +| `ERROR_OFFLINE_DISABLED` | A user is logged in but offline support is not active. | +| `ERROR_USER_NOT_LOGGED_IN` | Nobody is logged in. | + +## When the cache resets + +In rare cases the SDK cannot keep the existing cache — for example the encryption key became unrecoverable, or the database file is corrupt. When this happens it rebuilds the cache from scratch and notifies an `OfflineCacheResetListener`. Any un-sent messages that were queued are lost in the rebuild; `pendingMessagesLost` reports how many — or `null` when it cannot be known. + + + +```dart +class ResetWatcher extends OfflineCacheResetListener { + @override + void onOfflineCacheReset(OfflineCacheResetReason reason, int? pendingMessagesLost) { + // The cache was rebuilt. Re-fetch anything you need; it will re-sync from the server. + // pendingMessagesLost == null means an unknown number were lost, not zero. + } +} + +CometChat.addOfflineCacheResetListener("RESET_LISTENER", ResetWatcher()); +``` + + + + +| `OfflineCacheResetReason` | Cause | +|---|---| +| `keyLost` | The device key could not be recovered (OS update, cleared app data, restored backup); a fresh key was minted. | +| `wrongKey` | The database exists but does not decrypt under the current key. | +| `corrupted` | The database file is corrupt on disk. | +| `escalated` | An unclassified open failure recurred across launches and was escalated to a permanent rebuild. | +| `encryptionChanged` | The `encryption` setting changed, so the on-disk cache was rebuilt in the configured format. This also discards any queued messages and the cached media. | + +Remove the listener with `CometChat.removeOfflineCacheResetListener("RESET_LISTENER")`. See [Encrypt the Cache](/sdk/flutter/offline-encryption) for the encryption-related reasons. + +## Manage the cache + +Read the current cache size, or clear the cache entirely. + + + +```dart +final int bytes = await CometChat.getCacheSize(); // DB + media byte store, in bytes + +CometChat.clearOfflineCache(); // wipe the entire per-user cache +``` + + + + +`getCacheSize()` returns the total cache size in bytes (local database plus the media byte store) as a single figure, or `0` when offline support is disabled or nobody is logged in. + + + +`clearOfflineCache()` is destructive and irreversible. It empties the entire per-user cache — cached messages, conversations, media bytes **and the un-sent outbox**. Each still-queued `pending` / `sending` message fails with reason `discarded`; messages that had already failed are removed without a further callback. It is a no-op unless offline support is active. + + + + + +You never need to clear the cache on logout — the SDK does it for you. The cache is stored **per user**: it survives app restarts and a token refresh for the same logged-in user, and is wiped automatically on logout, when the server rejects the auth token, or when a different user or app ID is used. See [Offline Support](/sdk/flutter/offline-overview). + + diff --git a/sdk/flutter/overview.mdx b/sdk/flutter/overview.mdx index b0eff39c1..138654728 100644 --- a/sdk/flutter/overview.mdx +++ b/sdk/flutter/overview.mdx @@ -25,7 +25,7 @@ Use this Chat SDK directly only if you need custom UI or advanced control. If you're migrating an existing app from CometChat SDK v4, check out the [Upgrading from v4](/sdk/flutter/upgrading-from-v4-guide) guide for breaking changes, deprecated methods, and migration instructions. -Before integrating the Chat SDK, ensure you have a [CometChat Account](https://app.cometchat.com/signup) with your App ID, Region, and Auth Key. Flutter SDK `>=1.2` is required, with Android API Level 21+ and iOS 11+. Users must exist in CometChat to send or receive messages — see [Authentication](/sdk/flutter/authentication-overview) for details. +Before integrating the Chat SDK, ensure you have a [CometChat Account](https://app.cometchat.com/signup) with your App ID, Region, and Auth Key. `cometchat_sdk: ^5.1.0` requires Dart 3.10+, Flutter 3.38+, Android API Level 21+, and iOS 13+. Users must exist in CometChat to send or receive messages — see [Authentication](/sdk/flutter/authentication-overview) for details. ```mermaid sequenceDiagram diff --git a/sdk/flutter/pin-conversation.mdx b/sdk/flutter/pin-conversation.mdx index 42c9070b3..4b7daa0d4 100644 --- a/sdk/flutter/pin-conversation.mdx +++ b/sdk/flutter/pin-conversation.mdx @@ -3,6 +3,8 @@ title: "Pin A Conversation" description: "Pin and unpin conversations, read the pin-ordered conversation list, and listen for conversation pin events with the CometChat Flutter SDK." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -83,19 +85,35 @@ The returned `Conversation` carries its pin fields cleared to `null`. Only a pin The default conversation list is already **pin-ordered** by the server: system pins first, then the user's own pins, then everything else by latest activity. Fetch it with the regular `ConversationsRequest` described in [Retrieve Conversations](/sdk/flutter/retrieve-conversations) — no extra filter is needed. + + ```dart +class Class_Name extends ConversationFetchListener { + @override + void onCachedConversations(List conversations) { + // Render the cached pinned list immediately (even when empty). + final pinned = conversations.where((c) => c.pinnedAt != null); + debugPrint("Cached pinned conversations: ${pinned.length}"); + } + + @override + void onServerConversations(List conversations) { + // Reconcile, keyed by conversation id. + } + + @override + void onError(CometChatException e) { + debugPrint("Conversation fetching failed with exception: ${e.message}"); + } +} + ConversationsRequest conversationsRequest = (ConversationsRequestBuilder() ..limit = 30) .build(); -conversationsRequest.fetchNext(onSuccess: (List conversations) { - final pinned = conversations.where((c) => c.pinnedAt != null); - debugPrint("Pinned conversations on this page: ${pinned.length}"); -}, onError: (CometChatException e) { - debugPrint("Conversation fetching failed with exception: ${e.message}"); -}); +conversationsRequest.fetchNext(listener: Class_Name()); ``` @@ -224,6 +242,16 @@ if (CometChat.isPinConversationEnabled()) { --- +## Offline Behaviour + +With [offline support](/sdk/flutter/offline-cached-data) enabled, `ConversationsRequest.fetchNext()` with a `ConversationFetchListener` delivers a cached page first (`onCachedConversations`, even when empty), then the server list (`onServerConversations`); offline, it ends with `onError` with code `ERR_CONNECTION` (details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. The cached page keeps the same pin ordering — system pins first, then the user's own pins — so a pinned list renders correctly from cache. + +| Builder option | Offline | +|---|---| +| `pinnedBy` | Cached | + +--- + ## Next Steps diff --git a/sdk/flutter/pin-message.mdx b/sdk/flutter/pin-message.mdx index 42d74534f..d76d89bc6 100644 --- a/sdk/flutter/pin-message.mdx +++ b/sdk/flutter/pin-message.mdx @@ -3,6 +3,8 @@ title: "Pin A Message" description: "Pin and unpin messages in a conversation, fetch the pinned list, and listen for pin events with the CometChat Flutter SDK." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -79,6 +81,8 @@ CometChat.unpinMessage(messageId, onSuccess: (BaseMessage message) { ## Fetch Pinned Messages + + Build a `MessagesRequest` with the `pinned` field of the `MessagesRequestBuilder` set to `true`. A pinned list belongs to one conversation, so pair it with `uid` for a one-on-one conversation or `guid` for a group — exactly one of the two. @@ -237,6 +241,14 @@ The flag is served on the logged-in user's login payload, with the app settings --- +## Offline Behaviour + +With [offline support](/sdk/flutter/offline-cached-data) enabled, `fetchPrevious()` on a `setPinned(true)` request delivers a cached page first (`onCachedMessages`, even when empty) and then the server list (`onServerMessages`); offline, it ends with `onError` (code `ERR_CONNECTION`, details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. `onCachedMessages` holds the conversation's cached pinned messages, newest pin first. The pinned list is answered from its own store, so other `MessagesRequestBuilder` filters are ignored, as on the server. + +| Builder method | Offline | +|---|---| +| `setPinned` (with `setUID` / `setGUID`) | Cached | + ## Next Steps diff --git a/sdk/flutter/reactions.mdx b/sdk/flutter/reactions.mdx index 3834f6c46..a4cbcaa33 100644 --- a/sdk/flutter/reactions.mdx +++ b/sdk/flutter/reactions.mdx @@ -3,6 +3,8 @@ title: "Reactions" description: "Add, remove, fetch, and listen for CometChat message reactions in Flutter apps for text, media, and custom messages." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -67,16 +69,19 @@ CometChat.removeReaction(messageId, "😴", onSuccess: (message) { ## Fetch Reactions for a Message -To get all reactions for a specific message, first create a `ReactionRequest` using `ReactionRequestBuilder`. You can specify the number of reactions to fetch with `limit` with max limit 100. For this, you will require the ID of the message. This ID needs to be passed to the `messageId` property of the builder class. The `reaction` property will allow you to fetch details for specific reaction or emoji. +To get all reactions for a specific message, first create a `ReactionsRequest` using `ReactionsRequestBuilder`. You can specify the number of reactions to fetch with `limit` with maximum limit 1000. For this, you will require the ID of the message. This ID needs to be passed to the `messageId` property of the builder class. The `reaction` property will allow you to fetch details for specific reaction or emoji. | Setting | Description | |-----------------------------------|------------------------------------------------------------------------------------------------------------------| | `setMessageId(int value)` | Specifies the unique identifier of the message for which you want to fetch reactions. This parameter is mandatory as it tells the SDK which message's reactions are being requested. | -| `setReaction(String value)` | Filters the reactions fetched by the specified reaction type (e.g., "😊", "😂", "👍"). When set, this method will cause the ReactionRequest to only retrieve details of the provided reaction for the given message. | +| `setReaction(String value)` | Filters the reactions fetched by the specified reaction type (e.g., "😊", "😂", "👍"). When set, this method will cause the ReactionsRequest to only retrieve details of the provided reaction for the given message. | ## Fetch Next + + + The fetchNext() method fetches the next set of reactions for the message. @@ -86,7 +91,7 @@ The fetchNext() method fetches the next set of reactions for the message. ```dart int messageId = 1; -ReactionRequest reactionRequest = (ReactionRequestBuilder()..limit = 30..messageId = messageId).build(); +ReactionsRequest reactionRequest = (ReactionsRequestBuilder()..limit = 30..messageId = messageId).build(); reactionRequest.fetchNext( onSuccess: (messageReactions) { @@ -111,7 +116,7 @@ The `fetchPrevious()` method fetches the previous set of reactions for the messa ```dart int messageId = 1; -ReactionRequest reactionRequest = (ReactionRequestBuilder()..limit = 30..messageId = messageId).build(); +ReactionsRequest reactionRequest = (ReactionsRequestBuilder()..limit = 30..messageId = messageId).build(); reactionRequest.fetchPrevious( onSuccess: (messageReactions) { @@ -229,3 +234,15 @@ BaseMessage modifiedBaseMessage = await CometChatHelper.updateMessageWithReactio + +## Offline Behaviour + +With offline support enabled, `ReactionsRequest.fetchNext()` on a `ReactionFetchListener` first delivers `onCachedReactions` from the device (empty when nothing was recorded), then `onServerReactions` with the live page to reconcile by reaction id. `fetchPrevious()` does not take a listener, so it is not a two-phase read. A request object caches only the direction it fetches, so use a separate `ReactionsRequest` for each direction. Offline, a read ends with `onError` (code `ERROR_REQUEST_IN_PROGRESS` while a fetch on the same object is still running, or code `ERR_CONNECTION` with details `ERROR_INTERNET_UNAVAILABLE` for no connection) after the cached page. See [Show Cached Data](/sdk/flutter/offline-cached-data). + +| Builder method | Offline | +|---|---| +| `setLimit` | Cached | +| `setMessageId` | Cached | +| `setReaction` | Cached | + +The cached phase replays the page this request last received from the server online, in the same direction; it does not re-run these settings as a query against the cache. diff --git a/sdk/flutter/real-time-listeners.mdx b/sdk/flutter/real-time-listeners.mdx index f1004d3fe..e990ac8b7 100644 --- a/sdk/flutter/real-time-listeners.mdx +++ b/sdk/flutter/real-time-listeners.mdx @@ -30,6 +30,8 @@ CometChat provides 5 listeners viz. 4. [ConversationListener](#conversation-listener) 5. [AIAssistantListener](#ai-assistant-listener) +With offline support enabled, four more listeners report what the SDK does on the device; see [Offline listeners](#offline-listeners). + ## User Listener The `UserListener` class provides you with live events related to users. Below are the callback methods provided by the `UserListener` class. @@ -159,6 +161,10 @@ CometChat.removeGroupListener(UNIQUE_LISTENER_ID) The `MessageListener` class provides you with live events related to messages. Below are the callback methods provided by the `MessageListener` class. + +With offline support enabled, `MessageListener` also receives `onMessagesSynced` after the SDK catches up on reconnect. It carries updates to already-cached messages that changed while offline (reactions, edits, deletes and receipts); messages that arrived while offline still come through the typed callbacks such as `onTextMessageReceived`. See [Stay in Sync](/sdk/flutter/offline-sync). + + | Method | Information | | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `onTextMessageReceived(TextMessage message)` | This event is triggered when a Text Message is received. | @@ -409,3 +415,14 @@ CometChat.removeAIAssistantListener("UNIQUE_LISTENER_ID"); The `AIAssistantListener` delivers real-time streaming events. After a run completes, persisted agentic messages (`AIAssistantMessage`, `AIToolResultMessage`, `AIToolArgumentMessage`) are delivered via the `MessageListener` callbacks listed above. + +## Offline listeners + +With offline support enabled (`cometchat_sdk` 5.1.0+), these listeners report what the SDK does on the device rather than events from the server. Each is documented with examples on its own page. + +| Listener | What it reports | Details | +|---|---|---| +| `OfflineMessageListener` | Every outbox transition, app-wide: a message queued, sent or failed | [Send While Offline](/sdk/flutter/offline-send) | +| `SyncListener` | Progress and outcome of each catch-up sync | [Stay in Sync](/sdk/flutter/offline-sync) | +| `OfflineCacheResetListener` | The on-device cache was rebuilt, for example after the encryption key was lost | [When the cache resets](/sdk/flutter/offline-sync#when-the-cache-resets) | +| `MediaDownloadListener` | A background media download finished or failed | [Media Caching](/sdk/flutter/offline-media-caching) | diff --git a/sdk/flutter/receive-messages.mdx b/sdk/flutter/receive-messages.mdx index 42cf334f1..85e25a7fe 100644 --- a/sdk/flutter/receive-messages.mdx +++ b/sdk/flutter/receive-messages.mdx @@ -3,6 +3,8 @@ title: "Receive A Message" description: "Receive CometChat messages in Flutter apps with real-time MessageListener callbacks and missed message retrieval." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -98,6 +100,12 @@ As a sender, you will not receive your own message in a real-time message event. + + +With offline support enabled, a registered `MessageListener` also receives `onMessagesSynced(List messages)` after the SDK catches up on reconnect, carrying updates to already-cached messages that changed while offline: reactions, edits, deletes and receipts. Messages that arrived while offline still come one at a time through the usual `onTextMessageReceived`, `onMediaMessageReceived` and other typed callbacks. See [Stay in Sync](/sdk/flutter/offline-sync). + + + ### Receiving Card Messages `onCardMessageReceived(CardMessage cardMessage)` is triggered when a card message (category `"card"`) is received. Card Messages are **receive-only** — they are created via the Platform REST API or the Dashboard Bubble Builder, not sent from the SDK (see [Send a Message → Card Message](/sdk/flutter/send-message#card-message)). @@ -125,6 +133,8 @@ Now using the `fetchNext()` method, you can fetch all the messages that were sen Calling the `fetchNext()` method on the same object repeatedly allows you to fetch all the offline messages for the logged in user in a paginated manner + + ### For a Particular One-on-one Conversation @@ -347,6 +357,8 @@ In other words, as a logged-in user, how do I search a message? In order to do this, you can use the `searchKeyword` method. This method accepts string as input. When set, the SDK will fetch messages which match the `searchKeyword`. +With offline support on, this filter is answered by the server only; the cached page is empty. + By default, the searchKey is searched only in message text. However, if you enable `Conversation & Advanced Search`, the searchKey will be searched in message text, file name, mentions & mime type of the file. @@ -693,3 +705,39 @@ CometChat.getUnreadMessageCountForAllGroups(hideMessagesFromBlockedUsers: hideMe In the `onSuccess()` callback, you will receive a `Map` which will contain the `GUIDs` of the groups as the key and the unread message counts as the values. + +## Offline Behaviour + +With offline support enabled, message fetches run in two phases. Pass a `MessageFetchListener` to `fetchNext()` or `fetchPrevious()`: `onCachedMessages` delivers a page from the on-device cache first (even when empty), then `onServerMessages` delivers the reconciled server copy. Offline, the fetch ends with `onError` (code `ERR_CONNECTION`, details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. See [Show Cached Data](/sdk/flutter/offline-cached-data). + + + +```dart +class ConversationFetchHandler extends MessageFetchListener { + @override + void onCachedMessages(List messages) { + // Render the cached page immediately. + } + + @override + void onServerMessages(List messages) { + // Reconcile with the authoritative server page. + } + + @override + void onError(CometChatException e) { + debugPrint("Message fetch failed: ${e.code}"); + } +} + +messageRequest.fetchNext(listener: ConversationFetchHandler()); +``` + + + +| Builder method | Offline behaviour | +| --- | --- | +| `setUnread(true)` | Cached | +| `setSearchKeyword(String)` | Server only — cached page is empty | + +The unread-count methods (`getUnreadMessageCountForUser`, `getUnreadMessageCountForGroup`, and the `getUnreadMessageCount` variants) are not two-phase reads and are unaffected. diff --git a/sdk/flutter/retrieve-conversations.mdx b/sdk/flutter/retrieve-conversations.mdx index e9cdb65b1..4699df110 100644 --- a/sdk/flutter/retrieve-conversations.mdx +++ b/sdk/flutter/retrieve-conversations.mdx @@ -3,6 +3,8 @@ title: "Retrieve Conversations" description: "Fetch, filter, tag, and search CometChat conversations in Flutter apps using conversation request builders." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -164,6 +166,8 @@ This feature is only available with `Conversation & Advanced Search`. The `Conve +With offline support on, this filter is answered by the server only; the cached page is empty. + ```dart @@ -276,20 +280,33 @@ Once you have the object of the `ConversationsRequest` class, you need to call t A Maximum of only 50 Conversations can be fetched at once. + + ```dart +class Class_Name extends ConversationFetchListener { + @override + void onCachedConversations(List conversations) { + // Render the cached list immediately (even when empty). + } + + @override + void onServerConversations(List conversations) { + // Reconcile, keyed by conversation id. + } + + @override + void onError(CometChatException e) { + // Handle failure (offline: ERR_CONNECTION after the cached page). + } +} + ConversationsRequest conversationRequest = (ConversationsRequestBuilder() ..limit = 50 ).build(); -conversationRequest.fetchNext( - onSuccess: (List conversations){ - - - }, onError: (CometChatException e){ - - } +conversationRequest.fetchNext(listener: Class_Name()); ``` @@ -366,18 +383,34 @@ In order to fetch a specific conversation, you can use the `getConversation` met * user - Only fetches user conversation. * group - Only fetches group conversations. + +Pass a `ConversationDetailsListener` to `getConversation()` to receive the cached copy first. With offline support on, `onCachedConversation` delivers the cached conversation when there is one, then `onServerConversation` delivers the server copy, written through before delivery. Offline, the read ends with `onError` with code `ERR_CONNECTION` (details `ERROR_INTERNET_UNAVAILABLE`); when nothing is cached, `onError` is the only callback. Without a listener, `onSuccess` receives the server copy and `onError` is unchanged. `onSuccess` and `onError` are required either way — pass `null` for both when you use a listener. + + ```dart +class Class_Name extends ConversationDetailsListener { + @override + void onCachedConversation(Conversation conversation) { + // Fires only on a cache hit. + } + + @override + void onServerConversation(Conversation conversation) { + // Server copy, written through before delivery. + } + + @override + void onError(CometChatException e) { + debugPrint("Fetch Conversation failed : ${e.message}"); + } +} + String conversationWith = "cometchat-uid-1"; //id of the user/group String conversationType = "user"; CometChat.getConversation(conversationWith, conversationType, - onSuccess: (Conversation conversation) { - debugPrint("Fetch Conversation Successfully : $conversation"); - }, onError: (CometChatException e) { - debugPrint("Fetch Conversation failed : ${e.message}"); - } -); + onSuccess: null, onError: null, listener: Class_Name()); ``` @@ -403,3 +436,19 @@ Conversation conversation = CometChat.getConversationFromMessage(message); While converting `Message` object to `Conversation` object, the `unreadMessageCount` & `tags` will not be available in the `Conversation` object. The unread message count needs to be managed in your client-side code. + +## Offline Behaviour + +With [offline support](/sdk/flutter/offline-cached-data) enabled, `fetchNext()` delivers a cached page first (`onCachedConversations`, even when empty) and then the server list (`onServerConversations`); offline, the read ends with `onError` with code `ERR_CONNECTION` (details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. The cached page is answered only when every builder filter on the request is locally computable — a single server-only filter blanks the cached page while the server phase still answers in full. + +| Builder option | Offline | +|---|---| +| `conversationType` | Cached | +| `tags` | Cached | +| `userTags` / `groupTags` | Cached | +| `unread` | Cached (last-known count; the server phase corrects it) | +| `includeBlockedUsers` / `withBlockedInfo` | Cached | +| `withUserAndGroupTags` / `withTags` | Cached (enrichment) | +| `hideAgentic` / `onlyAgentic` | Cached | +| `pinnedBy` | Cached | +| `searchKeyword` | Server only — cached page is empty | diff --git a/sdk/flutter/retrieve-group-members.mdx b/sdk/flutter/retrieve-group-members.mdx index ec820f39b..89a887b34 100644 --- a/sdk/flutter/retrieve-group-members.mdx +++ b/sdk/flutter/retrieve-group-members.mdx @@ -3,6 +3,8 @@ title: "Retrieve Group Members" description: "Fetch CometChat group members in Flutter apps with group GUID, pagination limits, search keywords, and member scopes." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -46,6 +48,8 @@ String GUID = "GUID"; ### Set Search Keyword +With offline support on, this filter is answered by the server only; the cached page is empty. + This method allows you to set the search string based on which the group members are to be fetched. @@ -85,6 +89,8 @@ GroupMembersRequest groupMembersRequest = (GroupMembersRequestBuilder(GUID) ### Set Status +With offline support on, this filter is answered by the server only; the cached page is empty. + This method allows you to filter group members based on their online/offline status. @@ -111,21 +117,50 @@ Finally, once all the parameters are set to the builder class, you need to call Once you have the object of the `GroupMembersRequest` class, you need to call the `fetchNext()` method. Calling this method will return a list of `GroupMember` objects containing N number of members depending on the limit set. + + ```dart +class Class_Name extends GroupMemberFetchListener { + @override + void onCachedGroupMembers(List members) { + // Cached page: fires first (even when empty). Render immediately. + debugPrint("Cached group members: ${members.length}"); + } + + @override + void onServerGroupMembers(List members) { + // Server page: reconcile your displayed list, keyed by uid. + debugPrint("Server group members: ${members.length}"); + } + + @override + void onError(CometChatException e) { + // Handle failure (offline: ERR_CONNECTION after the cached page). + debugPrint("Group Members fetch failed with exception: ${e.message}"); + } +} + String GUID = "GUID"; GroupMembersRequest groupMembersRequest = (GroupMembersRequestBuilder(GUID) ..limit = 20 ).build(); -groupMembersRequest.fetchNext(onSuccess: (List groupMemberList){ - debugPrint("Group Members fetched Successfully : $groupMemberList "); - }, onError: (CometChatException e) { - debugPrint("Delete Group failed with exception: ${e.message}"); - }); +groupMembersRequest.fetchNext(onSuccess: null, onError: null, listener: Class_Name()); ``` + +## Offline Behaviour + +With offline support enabled, `GroupMembersRequest.fetchNext()` with a `GroupMemberFetchListener` first delivers `onCachedGroupMembers` from the device (empty when nothing was recorded), then `onServerGroupMembers` with the live page to reconcile by `uid`. Offline, the read ends with `onError` with code `ERR_CONNECTION` (details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. A request that uses a server-only filter records nothing, so its cached page is always empty. See [Show Cached Data](/sdk/flutter/offline-cached-data). + +| Builder option | Offline | +|---|---| +| `limit` | Cached | +| `scopes` | Cached | +| `searchKeyword` | Server only — cached page is empty | +| `status` | Server only — cached page is empty | diff --git a/sdk/flutter/retrieve-groups.mdx b/sdk/flutter/retrieve-groups.mdx index dc7883a28..edf4eca09 100644 --- a/sdk/flutter/retrieve-groups.mdx +++ b/sdk/flutter/retrieve-groups.mdx @@ -3,6 +3,8 @@ title: "Retrieve Groups" description: "Fetch CometChat groups in Flutter apps with pagination, search keywords, joined-only filters, tags, and group request builders." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -45,6 +47,8 @@ GroupsRequest groupsRequest = (GroupsRequestBuilder() ### Set Search Keyword +With offline support on, this filter is answered by the server only; the cached page is empty. + This method allows you to set the search string based on which the groups are to be fetched. @@ -119,18 +123,36 @@ Once you have the object of the `GroupsRequest` class, you need to call the `fet The list of groups fetched will only have the public and password type groups. The private groups will only be available if the user is a member of that private group. + + ```dart +class Class_Name extends GroupFetchListener { + @override + void onCachedGroups(List groups) { + // Cached page: fires first (even when empty). Render immediately. + debugPrint("Cached groups: ${groups.length}"); + } + + @override + void onServerGroups(List groups) { + // Server page: reconcile your displayed list, keyed by guid. + debugPrint("Server groups: ${groups.length}"); + } + + @override + void onError(CometChatException e) { + // Handle failure (offline: ERR_CONNECTION after the cached page). + debugPrint("Group Request failed with exception: ${e.message}"); + } +} + GroupsRequest groupsRequest = (GroupsRequestBuilder() ..limit= 20 ).build(); -groupsRequest.fetchNext(onSuccess: (List groupList) { - debugPrint("Fetched Group Successfully : $groupList "); - }, onError: (CometChatException e) { - debugPrint("Group Request failed with exception: ${e.message}"); - }); +groupsRequest.fetchNext(onSuccess: null, onError: null, listener: Class_Name()); ``` @@ -189,3 +211,15 @@ CometChat.getOnlineGroupMemberCount(guids, This method returns a `Map` with the GUID of the group as the key and the online member count for that group as the value. + +## Offline Behaviour + +With offline support enabled, `GroupsRequest.fetchNext()` with a `GroupFetchListener` first delivers `onCachedGroups` from the device (empty when nothing was recorded), then `onServerGroups` with the live page to reconcile by `guid`. Offline, the read ends with `onError` with code `ERR_CONNECTION` (details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. A request that uses a server-only filter records nothing, so its cached page is always empty. See [Show Cached Data](/sdk/flutter/offline-cached-data). + +| Builder option | Offline | +|---|---| +| `limit` | Cached | +| `joinedOnly` | Cached | +| `tags` | Cached | +| `withTags` | Cached | +| `searchKeyword` | Server only — cached page is empty | diff --git a/sdk/flutter/retrieve-users.mdx b/sdk/flutter/retrieve-users.mdx index d323abcaa..59cb0550e 100644 --- a/sdk/flutter/retrieve-users.mdx +++ b/sdk/flutter/retrieve-users.mdx @@ -3,6 +3,8 @@ title: "Retrieve Users" description: "Fetch CometChat users in Flutter apps, get logged-in user details, and filter user lists with request builder options." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -61,6 +63,8 @@ UsersRequest usersRequest = (UsersRequestBuilder() ### Set Search Keyword +With offline support on, this filter is answered by the server only; the cached page is empty. + This method allows you to set the search string based on which the users are to be fetched. @@ -78,6 +82,8 @@ UsersRequest usersRequest = (UsersRequestBuilder() ### Set Status +With offline support on, this filter is answered by the server only; the cached page is empty. + The status based on which the users are to be fetched. The status parameter can contain one of the below two values: * CometChatUserStatus.online - will return the list of only online users. @@ -289,18 +295,36 @@ Finally, once all the parameters are set to the builder class, you need to call Once you have the object of the `UsersRequest` class, you need to call the `fetchNext()` method. Calling this method will return a list of `User` objects containing 'n' number of users depending on the limit set. + + ```dart +class Class_Name extends UserFetchListener { + @override + void onCachedUsers(List users) { + // Cached page: fires first (even when empty). Render immediately. + debugPrint("Cached users: ${users.length}"); + } + + @override + void onServerUsers(List users) { + // Server page: reconcile your displayed list, keyed by uid. + debugPrint("Server users: ${users.length}"); + } + + @override + void onError(CometChatException e) { + // Handle failure (offline: ERR_CONNECTION after the cached page). + debugPrint("User List Fetch Failed: ${e.message}"); + } +} + UsersRequest usersRequest = (UsersRequestBuilder() ..limit = 25 ).build(); -usersRequest.fetchNext(onSuccess: (List userList){ - debugPrint("User List Fetched Successfully : $userList"); - },onError: (CometChatException e){ - debugPrint("User List Fetch Failed: ${e.message}"); - }); +usersRequest.fetchNext(onSuccess: null, onError: null, listener: Class_Name()); ``` @@ -352,3 +376,22 @@ CometChat.getOnlineUserCount(onSuccess: (int count){ + +## Offline Behaviour + +With offline support enabled, `UsersRequest.fetchNext()` with a `UserFetchListener` first delivers `onCachedUsers` from the device — the pages the server last returned for this exact request, replayed on-device (empty when nothing was recorded) — then `onServerUsers` with the live page to reconcile by `uid`. Offline, the read ends with `onError` with code `ERR_CONNECTION` (details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. A request that uses a server-only filter records nothing, so its cached page is always empty. See [Show Cached Data](/sdk/flutter/offline-cached-data). + +| Builder option | Offline | +|---|---| +| `limit` | Cached | +| `searchKeyword` | Server only — cached page is empty | +| `searchIn` | Used only with `searchKeyword`, so server only | +| `userStatus` | Server only — cached page is empty | +| `hideBlockedUsers` | Cached | +| `roles` | Cached | +| `friendsOnly` | Cached | +| `tags` | Cached | +| `withTags` | Cached | +| `uids` | Cached | +| `sortBy` | Cached | +| `sortByOrder` | Cached | diff --git a/sdk/flutter/save-message.mdx b/sdk/flutter/save-message.mdx index 58dbaa2ee..6e1c77175 100644 --- a/sdk/flutter/save-message.mdx +++ b/sdk/flutter/save-message.mdx @@ -3,6 +3,8 @@ title: "Save A Message" description: "Save and unsave messages privately, fetch the saved list across conversations, and listen for save events with the CometChat Flutter SDK." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -73,6 +75,8 @@ CometChat.unsaveMessage(messageId, onSuccess: (BaseMessage message) { ## Fetch Saved Messages + + Build a `MessagesRequest` with the `saved` field of the `MessagesRequestBuilder` set to `true`. The saved list is **user-level**, so unlike the pinned list you do **not** set `uid` or `guid` — leaving both unset is what makes it cross-conversation. @@ -225,6 +229,14 @@ When neither the login payload nor the app settings carry the flag, `isSaveMessa --- +## Offline Behaviour + +With [offline support](/sdk/flutter/offline-cached-data) enabled, `fetchPrevious()` on a `setSaved(true)` request delivers a cached page first (`onCachedMessages`, even when empty) and then the server list (`onServerMessages`); offline, it ends with `onError` (code `ERR_CONNECTION`, details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. `onCachedMessages` holds the user's cached saved messages across all conversations. The saved list is answered from its own store, so other `MessagesRequestBuilder` filters are ignored, as on the server. + +| Builder method | Offline | +|---|---| +| `setSaved` | Cached | + ## Next Steps diff --git a/sdk/flutter/send-message.mdx b/sdk/flutter/send-message.mdx index e7b41c0ae..f849969cf 100644 --- a/sdk/flutter/send-message.mdx +++ b/sdk/flutter/send-message.mdx @@ -3,6 +3,8 @@ title: "Send A Message" description: "Send CometChat text, media, and custom messages to users and groups from Flutter apps, and handle receive-only card messages." --- +import FlutterOfflineSend from '/snippets/sdk/flutter-offline-send.mdx'; + | Field | Value | @@ -39,6 +41,8 @@ You can also send metadata along with a text or media message. Think, for exampl To send a text message to a single user or group, you need to use the `sendMessage()` method and pass a `TextMessage` object to it. + + ### Add Metadata To send custom data along with a text message, you can use the `setMetadata` method and pass a `Map` to it. @@ -209,6 +213,8 @@ When a text message is sent successfully, the response will include a `TextMessa To send a media message to any user or group, you need to use the `sendMediaMessage()` method and pass a `MediaMessage` object to it. + + ### Add Metadata To send custom data along with a media message, you can use the `setMetadata` method and pass a `Map` to it. @@ -424,6 +430,8 @@ CometChat allows you to send custom messages which are neither text nor media me In order to send a custom message, you need to use the `sendCustomMessage()` method. + + The `sendCustomMessage()` methods takes an object of the `CustomMessage` which can be obtained using the below constructor. diff --git a/sdk/flutter/setup.mdx b/sdk/flutter/setup.mdx index 6febe0114..a666f1012 100644 --- a/sdk/flutter/setup.mdx +++ b/sdk/flutter/setup.mdx @@ -31,8 +31,8 @@ Minimum Requirement * Android API Level 21 * AndroidX Compatibility -* iOS 12 or higher -* Flutter 3.19.0 or higher (Dart 3.3.0 or higher) +* iOS 13 or higher +* Flutter 3.38.0 or higher (Dart 3.10.0 or higher) @@ -40,7 +40,7 @@ Minimum Requirement ```yaml dependencies: - cometchat_sdk: ^5.0.8 + cometchat_sdk: ^5.1.0 ``` Then run: @@ -88,7 +88,7 @@ end **Apple Silicon (M1/M2/M3) users:** Excluding `arm64` from the simulator build prevents the app from running natively on Apple Silicon Macs. If you are developing on an Apple Silicon Mac, consider excluding only `i386` instead of `arm64 i386` to enable native simulator builds. -3. For iOS, change the deployment target to `11` or higher. +3. For iOS, change the deployment target to `13` or higher. 4. For iOS, navigate to your iOS folder in terminal or CMD and do `pod install` . For apple chip system use rosetta terminal. 5. For iOS you can set the Enabled Bitcode settings to **NO** present in build settings of XCODE project @@ -112,6 +112,10 @@ import 'package:cometchat_sdk/cometchat_sdk.dart'; The `init()` method initialises the settings required for CometChat. The `init()` method takes the below parameters: + +Offline support (SDK 5.1.0+) is **off by default** and opt-in — initialization is unchanged until you turn it on. To cache chats on the device and keep them available without a connection, enable it when you build your settings. See [Enable Offline Support](/sdk/flutter/offline-enable). + + 1. appID - You CometChat App ID 2. appSettings - An object of the AppSettings class can be created using the AppSettingsBuilder class. The region field is mandatory and can be set using the `setRegion()` method. diff --git a/sdk/flutter/thread-subscription.mdx b/sdk/flutter/thread-subscription.mdx index 92ba2dd34..865dbeeb0 100644 --- a/sdk/flutter/thread-subscription.mdx +++ b/sdk/flutter/thread-subscription.mdx @@ -3,6 +3,8 @@ title: "Thread Subscription" description: "Subscribe to and unsubscribe from message threads, read subscription state off a message, and fetch participated threads with the CometChat Flutter SDK." --- +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -14,7 +16,7 @@ description: "Subscribe to and unsubscribe from message threads, read subscripti | Key classes | `ThreadsRequestBuilder` · `ThreadsRequest` · `MessageThread` · `BaseMessage` · `RepliesOptions` | | Key fields | `BaseMessage.threadSubscribed` · `MessageThread.subscribed` | | Prerequisites | SDK initialised via [`CometChat.init()`](/sdk/flutter/setup) and a logged-in user via [`CometChat.login()`](/sdk/flutter/authentication-overview). | -| Constraints | A thread is identified by its **parent message ID**. The SDK keeps no subscription state and fires no subscription event — the resolved call is the acknowledgement. | +| Constraints | A thread is identified by its **parent message ID**. The SDK keeps no subscription state beyond the flag on each message (cached with it when offline support is on) and fires no subscription event — the resolved call is the acknowledgement. | | Related | [Threaded Messages](/sdk/flutter/threaded-messages) · [Notification Preferences](/notifications/preferences) | | Full reference | [`BaseMessage`](/sdk/reference/messages#basemessage) · [`CometChatException`](/sdk/reference/auxiliary#cometchatexception) | @@ -34,9 +36,9 @@ Thread subscription builds on [Threaded Messages](/sdk/flutter/threaded-messages ## How State Works -The SDK keeps **no subscription state of its own**. There is no cache and no listener to reconcile: +The SDK keeps **no subscription state of its own** beyond the flag on each message, and there is no listener to reconcile: -- Message-list fetches and `getMessageDetails()` ask the server for the flag, and it arrives on the message as the `threadSubscribed` field. +- Message-list fetches and `getMessageDetails()` ask the server for the flag, and it arrives on the message as the `threadSubscribed` field. With [offline support](/sdk/flutter/offline-cached-data) on, the flag is also cached with the message, so a cached read returns the last known value and the server read then updates it. - `subscribeToThread()` and `unsubscribeFromThread()` complete when the server has accepted the change. The `onSuccess` callback **is** the acknowledgement — there is no follow-up event. Your app owns the resulting UI state. That means you decide when to flip a toggle optimistically, and you decide what a thread's state is before you have fetched it. @@ -188,6 +190,8 @@ Replies sent from **this** device do not arrive on a listener — bump your thre ## Fetch the Threads a User Participates In + + Use `ThreadsRequest` to build a thread inbox. Every returned thread is one the logged-in user is subscribed to — presence in the list *is* a subscription. `ThreadsRequest`, `MessageThread` and `ConversationListener` are exported from `package:cometchat_sdk/cometchat_sdk.dart`, not from the modular `core.dart` or `messaging.dart` entry points. @@ -208,7 +212,7 @@ threadsRequest.fetchNext(onSuccess: (List threads) { -`fetchNext()` takes **required** `onSuccess` and `onError` callbacks, like every other request in the SDK. Call it repeatedly on the same object to page through the list, using `hasMore()` as the loop condition — page on it rather than on the size of the last result. Once the list is exhausted, `fetchNext()` resolves an empty list. +`fetchNext()` takes optional `listener`, `onSuccess`, and `onError` parameters — pass whichever you need. Call it repeatedly on the same object to page through the list, using `hasMore()` as the loop condition — page on it rather than on the size of the last result. Once the list is exhausted, `fetchNext()` resolves an empty list. @@ -275,6 +279,15 @@ Each row is a `MessageThread`: Sort a thread inbox on `lastReply?.sentAt` falling back to `parentMessage?.sentAt` — a thread with no replies has no last reply, and `updatedAt` is a cursor, not a sort key. A row carries only the raw `receiverUid`, with no name or avatar: render the ID immediately and resolve the display name lazily with `CometChat.getUser()` or `CometChat.getGroup()` rather than dropping the row. +## Offline Behaviour + +With [offline support](/sdk/flutter/offline-cached-data) enabled, `fetchNext()` on a `ThreadsFetchListener` delivers a cached page first (`onCachedThreads`, even when empty) and then the server list (`onServerThreads`); offline, it ends with `onError` (code `ERR_CONNECTION`, details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. With `setParticipatedByMe(true)`, `onCachedThreads` holds the user's cached participated threads, in the same sort order; with `false`, it replays the last page fetched online for this request. `setUid()` / `setGuid()` scope that list to one conversation. + +| Builder method | Offline | +|---|---| +| `setParticipatedByMe` | Cached (`true`: your participated threads; `false`: the last page fetched online) | +| `setUid` / `setGuid` | Cached (scope) | + ## Notification Preferences The notification preference for replies carries a value that pairs with this feature, so a user can be notified only about the threads they follow: `SUBSCRIBE_TO_SUBSCRIBED_THREADS` in the `RepliesOptions` enum. diff --git a/sdk/flutter/threaded-messages.mdx b/sdk/flutter/threaded-messages.mdx index fbbd85dfe..fa8bba160 100644 --- a/sdk/flutter/threaded-messages.mdx +++ b/sdk/flutter/threaded-messages.mdx @@ -3,6 +3,9 @@ title: "Threaded Messages" description: "Send, receive, and fetch CometChat threaded messages in Flutter apps using parent message IDs." --- +import FlutterOfflineSend from '/snippets/sdk/flutter-offline-send.mdx'; +import FlutterOfflineListener from '/snippets/sdk/flutter-offline-listener.mdx'; + | Field | Value | @@ -29,6 +32,8 @@ Each Thread is attached to a message which is the Parent message for that thread As mentioned in the [Send a Message](/sdk/flutter/send-message) section, you can send a message to a User or a Group by mentioning the receiver (uid/guid) and `receiverType`(user/group). + + A message can be categorized as: 1. Text Message @@ -112,6 +117,8 @@ void onCustomMessageReceived(CustomMessage customMessage) { ### Fetch all the messages for any particular thread. + + You can fetch all the messages belonging to a particular thread by using the `MessagesRequest` class. The `MessageRequestBuilder` builds the `MessageRequest` object using the following functions: @@ -170,3 +177,12 @@ The above snippet will return messages between the logged in user and `cometchat ## Subscribe to a Thread Users can subscribe to or unsubscribe from a thread to control whether they are notified about its replies, and you can fetch the list of threads a user participates in to build a thread inbox. See [Thread Subscription](/sdk/flutter/thread-subscription). + +## Offline Behaviour + +With offline support enabled, thread fetches run in two phases. Pass a `MessageFetchListener` to `fetchPrevious()` or `fetchNext()`: `onCachedMessages` delivers the cached thread page first (even when empty), then `onServerMessages` delivers the reconciled server copy. Offline, the fetch ends with `onError` (code `ERR_CONNECTION`, details `ERROR_INTERNET_UNAVAILABLE`) after the cached page. A thread read is answered from the cache even when both `setUID` and `setGUID` are set. See [Show Cached Data](/sdk/flutter/offline-cached-data). + +| Builder method | Offline behaviour | +| --- | --- | +| `setParentMessageId(int)` | Cached | +| `setHideReplies(bool)` | Cached | diff --git a/sdk/flutter/upgrading-from-v4-guide.mdx b/sdk/flutter/upgrading-from-v4-guide.mdx index 14c0e6a94..88a778a80 100644 --- a/sdk/flutter/upgrading-from-v4-guide.mdx +++ b/sdk/flutter/upgrading-from-v4-guide.mdx @@ -13,7 +13,7 @@ Update your `pubspec.yaml` to the latest v5 SDK: ```yaml dependencies: - cometchat_sdk: ^5.0.8 + cometchat_sdk: ^5.1.0 ``` Then run: diff --git a/sdk/flutter/upload-files.mdx b/sdk/flutter/upload-files.mdx index a0d1c0c35..641be494a 100644 --- a/sdk/flutter/upload-files.mdx +++ b/sdk/flutter/upload-files.mdx @@ -4,6 +4,8 @@ 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." --- +import FlutterOfflineSend from '/snippets/sdk/flutter-offline-send.mdx'; + | Field | Value | @@ -31,6 +33,10 @@ A request object is scoped to **one destination** (`receiverId` / `receiverType` The classic path (passing a `files` list 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. + +With offline support enabled (SDK 5.1.0+), an upload batch and its files are persisted on the device: an in-progress batch survives an app restart, but it never resumes on its own (fetch it with `CometChat.getExistingUploadFileRequest` and retry each failed file — see [Restore a batch](#restore-a-batch-after-a-restart)), and a media message you send without a connection is durably queued and sent on reconnect. See [Send While Offline](/sdk/flutter/offline-send). + + ## The upload-then-send flow @@ -211,6 +217,8 @@ Attach the `Attachment` objects the batch produced — the ones you kept from `o Keep one message to a single kind. `type` is what a receiving UI picks its bubble from, so images in a message typed `file` render as file cards rather than a grid. If the user picked a mix, group the attachments by kind and send one message per kind — this is exactly what the UI Kit's composer does, tagging each message with a shared `batchId` so they display as one group. + + ```dart @@ -222,14 +230,36 @@ MediaMessage mediaMessage = MediaMessage( caption: "From the trip", // optional ); -await CometChat.sendMediaMessage(mediaMessage, - onSuccess: (MediaMessage message) { - debugPrint("Sent with ${message.attachments?.length} attachments"); - }, - onError: (CometChatException e) { +// Pass a listener for the offline-aware flow. onMessageSent is the drop-in for +// onSuccess; with offline support on, onMessagePending fires first and the +// message is sent on reconnect. +await CometChat.sendMediaMessage(mediaMessage, listener: _SendListener(request)); +``` + +```dart +class _SendListener extends MessageSentListener { + _SendListener(this.request); + final UploadFileRequest request; + + @override + void onMessagePending(BaseMessage message) { + // Fires on every send while offline support is enabled; + // message.status == CometChatMessageStatus.pending. + } + + @override + void onMessageSent(BaseMessage message) { + debugPrint("Media message sent"); + request.clearAll(); // release the batch + } + + @override + void onError(CometChatException e) { + // With offline support on, this also fires with code ERR_CONNECTION when a + // message is queued offline; real failures arrive in onOutboxMessageFailed. debugPrint("Sending failed: ${e.message}"); } -); +} ``` @@ -249,9 +279,33 @@ await CometChat.sendMediaMessage(mediaMessage, The non-retryable codes arrive via `onFileError`, the retryable ones via `onFileFailure`. -Upload state is in-memory and does not survive the app being killed — a batch cannot be resumed after a restart. A file with no progress for 30 seconds is treated as stalled and failed (retryable). +Without offline support, upload state is in-memory and does not survive the app being killed — a batch cannot be resumed after a restart. With offline support enabled the batch persists; see [Restore a batch](#restore-a-batch-after-a-restart). A file with no progress for 30 seconds is treated as stalled and failed (retryable). +## Restore a batch after a restart + +With offline support enabled, an in-progress upload batch and its files are persisted on the device, so a composer can rebuild its attachment tray after a connection drop or an app restart. Fetch the persisted batch with `CometChat.getExistingUploadFileRequest(receiverId, receiverType, {parentMessageId})`. It returns a `Future` — `null` when there is no batch for that destination (or when offline support is off). Pass `parentMessageId` to restore a thread composer's batch. + +```dart +final request = await CometChat.getExistingUploadFileRequest( + "cometchat-uid-1", + CometChatConversationType.user, +); + +if (request != null) { + request.addUploadListener(myUploadListener); // re-attach your batch-wide listener + for (final file in request.getFiles()) { + if (file.state == UploadFileState.failed) { + request.retryAttachment(file.fileId); // re-upload each failed file + } + } +} +``` + +A batch is **never** auto-resumed — the user drives retry. `getFiles()` returns the batch's files in add order as a `List`; each view carries the app-supplied `fileId`, `name`, `size`, `mimeType`, the current `state`, the uploaded `attachment` (when done), an `errorMessage`, and `localCopyPath` (the SDK's durable copy of the bytes, usable as a thumbnail source on restore). A restored file comes back as either `UploadFileState.uploaded` (done) or `UploadFileState.failed` (retryable) — a file that was mid-flight at app-kill comes back `failed`. + +`UploadFileState` has eight values across the file's lifecycle: `queued`, `requestingUrl`, `uploading`, `uploaded`, `attached`, `rejected` (not retryable), `failed` (retryable), and `cancelled`. + ## Next Steps diff --git a/snippets/sdk/flutter-offline-listener.mdx b/snippets/sdk/flutter-offline-listener.mdx new file mode 100644 index 000000000..be7e0e55a --- /dev/null +++ b/snippets/sdk/flutter-offline-listener.mdx @@ -0,0 +1,3 @@ + +**Starting with v5.1.0, {method} accepts a `listener:` parameter.** Pass a {listener} to receive the on-device cached result. The `listener:` parameter requires `cometchat_sdk` 5.1.0 or later, so on 5.0.x keep using `onSuccess`/`onError`. With offline support off, the listener behaves exactly like the callbacks: `onError` is unchanged, and the server result arrives in the `onServer…` callback instead of `onSuccess`. With offline support on, the listener also delivers the cached result first, then the server result. `onSuccess`/`onError` keep working and receive only the server result. See [Show Cached Data](/sdk/flutter/offline-cached-data). + diff --git a/snippets/sdk/flutter-offline-send.mdx b/snippets/sdk/flutter-offline-send.mdx new file mode 100644 index 000000000..1183c0539 --- /dev/null +++ b/snippets/sdk/flutter-offline-send.mdx @@ -0,0 +1,3 @@ + +**Starting with v5.1.0, {method} accepts a `listener:` parameter** that takes a `MessageSentListener`. The `listener:` parameter requires `cometchat_sdk` 5.1.0 or later. With offline support off, the listener behaves exactly like the old callback: the sent message arrives in `onMessageSent` (the drop-in for `onSuccess`) and `onError` is unchanged. With offline support on, `onMessagePending` fires first — the message is persisted locally. A message sent without a connection stays `pending` and is sent when the connection returns: `onError` fires once with code `ERR_CONNECTION` (details `ERROR_INTERNET_UNAVAILABLE`) as a not-connected notice, and `onMessageSent` follows once the message is sent. In `onError`, code `ERR_CONNECTION` means the message is queued; a real failure also reaches `OfflineMessageListener.onOutboxMessageFailed`, with the message's `status` set to `failed`. A send without a `listener:` is never queued. See [Send While Offline](/sdk/flutter/offline-send). +