diff --git a/docs.json b/docs.json index ad3c1f53d..d46a4d555 100644 --- a/docs.json +++ b/docs.json @@ -4831,28 +4831,45 @@ "sdk/unreal/overview", "sdk/unreal/setup", "sdk/unreal/key-concepts", + "sdk/unreal/advanced-configuration", { "group": "Authentication", "pages": [ "sdk/unreal/authentication" ] }, + { + "group": "Users & Groups", + "pages": [ + "sdk/unreal/users", + "sdk/unreal/groups" + ] + }, { "group": "Messaging", "pages": [ "sdk/unreal/send-message", "sdk/unreal/receive-messages", - "sdk/unreal/real-time-events", + "sdk/unreal/conversations", + "sdk/unreal/reactions", "sdk/unreal/typing-indicators", + "sdk/unreal/transient-messages", "sdk/unreal/delivery-read-receipts", + "sdk/unreal/real-time-events", "sdk/unreal/connection-status" ] }, { - "group": "Users & Groups", + "group": "AI & Notifications", "pages": [ - "sdk/unreal/users", - "sdk/unreal/groups" + "sdk/unreal/ai", + "sdk/unreal/push-notifications" + ] + }, + { + "group": "Moderation", + "pages": [ + "sdk/unreal/moderation" ] }, { @@ -6875,6 +6892,10 @@ } }, "redirects": [ + { + "source": "/sdk/unreal/ui-components", + "destination": "/sdk/unreal/setup#sample-ui-components-reference" + }, { "source": "/sdk/flutter/group-kick-member", "destination": "/sdk/flutter/group-kick-ban-members" diff --git a/sdk/unreal/ai.mdx b/sdk/unreal/ai.mdx new file mode 100644 index 000000000..503e5543b --- /dev/null +++ b/sdk/unreal/ai.mdx @@ -0,0 +1,352 @@ +--- +title: "AI Features" +description: "Add AI bots, smart replies, conversation starters, and summaries to your Unreal Engine game with CometChat's AI async nodes." +--- + +CometChat's AI features let your game do more than relay text: players can ask an in-game bot for help, get one-tap smart replies, receive ice-breakers when a new match's DM opens, and catch up on a busy guild chat with a one-line summary. The Unreal SDK exposes each of these as an async node (Blueprint) or an async action (C++). + +Every AI call in this guide is a **latent async operation**. On success it returns a raw **JSON `FString`** that your game parses — the SDK does not deserialize it for you, so you stay in control of the shape you expect. See [Parsing the JSON Result](#parsing-the-json-result) below. + +### AI Request Flow + +```mermaid +flowchart LR + A["Your Game"] --> B["AI Async Node"] + B --> C["C++ SDK"] --> D["CometChat AI Service"] + D -->|"On Success"| E["JSON FString Result"] + E --> F["Parse & render in UI"] + + style A fill:#333,stroke:#666,color:#fff + style B fill:#444,stroke:#888,color:#fff + style C fill:#333,stroke:#666,color:#fff + style D fill:#555,stroke:#999,color:#ccc + style E fill:#333,stroke:#666,color:#fff + style F fill:#333,stroke:#666,color:#fff +``` + + +Each feature must be enabled for your app in the [CometChat Dashboard](https://app.cometchat.com) before these calls return data. **Ask Bot** additionally requires a configured **AI Bot**; **Smart Replies**, **Conversation Starter**, and **Conversation Summary** each require their matching AI feature to be turned on. A call to a disabled feature comes back on the **On Failure** pin. + + + +Throughout this page, `ReceiverType` is either `"user"` (a 1:1 conversation, where the receiver ID is a user's UID) or `"group"` (a group conversation, where the receiver ID is the group's GUID). All async **On Failure** handlers receive a simple `(const FCometChatError& Error)`. + + +--- + +## Ask a Bot + +Send a free-form question to an AI bot configured in your dashboard and get its reply. This is perfect for an in-game help bot ("Guide Bot") that answers questions about crafting, quests, or controls without a human on the other end. + + + +Call the **Ask Bot Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Receiver Id | `FString` | UID of the user or GUID of the group the bot conversation belongs to | +| Receiver Type | `FString` | `"user"` or `"group"` | +| Bot Id | `FString` | The UID of the AI bot to ask (as configured in the dashboard) | +| Question | `FString` | The player's free-form question | + +**On Success** returns a JSON `FString` — the bot's reply. Parse it and display the answer in your help panel. + + +```cpp +#include "AsyncActions/CometChatAskBotAction.h" + +void AMyActor::AskGuideBot(const FString& PlayerQuestion) +{ + auto* Action = UCometChatAskBotAction::AskBotAsync( + this, + TEXT("guide-bot"), // Receiver Id — the bot's own conversation (1:1) + TEXT("user"), // Receiver Type + TEXT("guide-bot"), // Bot Id + PlayerQuestion // e.g. "How do I craft a health potion?" + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnBotReplied); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnBotReplied(const FString& Result) +{ + // Result is a JSON string containing the bot's reply — parse and show it. + UE_LOG(LogTemp, Log, TEXT("Guide Bot replied: %s"), *Result); +} + +// Shared async failure handler used across this page. +void AMyActor::OnError(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("AI request failed: %s"), *Error.Message); +} +``` + + + + +Show a "Guide Bot is thinking…" spinner between calling the node and the **On Success** pin firing. AI responses take longer than a normal message send, so give the player feedback that something is happening. + + +--- + +## Get Smart Replies + +Fetch a short list of AI-suggested replies for the latest messages in a conversation. Render them as tap-to-send chips above the keyboard so players can respond with one tap during fast-paced matches. + + + +Call the **Get Smart Replies Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Receiver Id | `FString` | UID of the user (1:1) or GUID of the group | +| Receiver Type | `FString` | `"user"` or `"group"` | + +**On Success** returns a JSON `FString` — a JSON **array** of suggested reply strings (for example, `["Sounds good!","On my way","Give me 5 min"]`). Parse it into chips. + + +```cpp +#include "AsyncActions/CometChatGetSmartRepliesAction.h" + +void AMyActor::LoadSmartReplies(const FString& FriendUid) +{ + auto* Action = UCometChatGetSmartRepliesAction::GetSmartRepliesAsync( + this, + FriendUid, // Receiver Id + TEXT("user") // Receiver Type — a 1:1 conversation + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnSmartRepliesReady); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnSmartRepliesReady(const FString& Result) +{ + // Result is a JSON array of strings — parse it and build reply chips. + TArray> Suggestions; + TSharedRef> Reader = TJsonReaderFactory<>::Create(Result); + if (FJsonSerializer::Deserialize(Reader, Suggestions)) + { + for (const TSharedPtr& Value : Suggestions) + { + const FString Reply = Value->AsString(); + UE_LOG(LogTemp, Log, TEXT("Smart reply: %s"), *Reply); + // AddReplyChipToUI(Reply); + } + } +} +``` + + + + +Smart replies reflect the **most recent messages** in the conversation. Call the node when the chat panel gains focus (or after a new incoming message) so the suggestions stay relevant to what was just said. + + +--- + +## Get Conversation Starter + +Ask the AI for opener suggestions when a conversation has little or no history — ideal for the first DM after two players are matched, or when a new member joins a guild and doesn't know what to say. + + + +Call the **Get Conversation Starter Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Receiver Id | `FString` | UID of the user (1:1) or GUID of the group | +| Receiver Type | `FString` | `"user"` or `"group"` | + +**On Success** returns a JSON `FString` containing one or more suggested opening lines. Parse it and offer them as tappable ice-breakers. + + +```cpp +#include "AsyncActions/CometChatGetConversationStarterAction.h" + +void AMyActor::SuggestOpeners(const FString& MatchedPlayerUid) +{ + auto* Action = UCometChatGetConversationStarterAction::GetConversationStarterAsync( + this, + MatchedPlayerUid, // Receiver Id + TEXT("user") // Receiver Type + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnStartersReady); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnStartersReady(const FString& Result) +{ + // Result is JSON — parse out the opener suggestions and show them as chips. + UE_LOG(LogTemp, Log, TEXT("Conversation starters: %s"), *Result); +} +``` + + + + +Conversation starters are most useful on an **empty** conversation. Once real messages exist, switch to [Smart Replies](#get-smart-replies) so suggestions follow the live conversation instead of starting a fresh one. + + +--- + +## Get Conversation Summary + +Generate a concise, AI-written summary of a conversation. Show it as a "Catch me up" banner when a player returns to a busy guild chat they've missed, so they don't have to scroll through hundreds of messages. + + + +Call the **Get Conversation Summary Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Receiver Id | `FString` | UID of the user (1:1) or GUID of the group | +| Receiver Type | `FString` | `"user"` or `"group"` | + +**On Success** returns a JSON `FString` — the AI-generated summary. Parse out the summary text and display it. + + +```cpp +#include "AsyncActions/CometChatGetConversationSummaryAction.h" + +void AMyActor::CatchUpOnGuildChat(const FString& GuildGuid) +{ + auto* Action = UCometChatGetConversationSummaryAction::GetConversationSummaryAsync( + this, + GuildGuid, // Receiver Id — the group's GUID + TEXT("group") // Receiver Type + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnSummaryReady); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnSummaryReady(const FString& Result) +{ + // Result is a JSON summary — parse it and show a "Catch me up" banner. + UE_LOG(LogTemp, Log, TEXT("Guild chat summary: %s"), *Result); +} +``` + + + + +Summaries are generated from recent conversation history and work best when there are enough messages to summarize. For a quiet 1:1 with only a handful of messages, the summary may simply restate them. + + +--- + +## Parsing the JSON Result + +Ask Bot, Smart Replies, Conversation Starter, and Conversation Summary all deliver their payload as a raw JSON `FString`. Nothing is deserialized for you — parse it yourself in the **On Success** handler. + +- **C++** — use Unreal's built-in JSON module (`FJsonSerializer` with `TJsonReaderFactory`). Deserialize into a `TArray>` for a JSON array (Smart Replies) or a `TSharedPtr` for a JSON object (a summary). Add `"Json"` to your module's `PublicDependencyModuleNames` in the `.Build.cs` file, and include the headers below. +- **Blueprint** — feed the `Result` string into a JSON parsing node (from a JSON Blueprint plugin such as the built-in JSON Blueprint Utilities, or your own C++ helper exposed as a `UFUNCTION`) and read the fields you need. + +```cpp +#include "Serialization/JsonSerializer.h" +#include "Dom/JsonObject.h" + +// Parsing a JSON object result (e.g. a summary payload) +void AMyActor::ParseSummary(const FString& Result) +{ + TSharedPtr Root; + TSharedRef> Reader = TJsonReaderFactory<>::Create(Result); + if (FJsonSerializer::Deserialize(Reader, Root) && Root.IsValid()) + { + const FString Summary = Root->GetStringField(TEXT("summary")); + UE_LOG(LogTemp, Log, TEXT("Summary text: %s"), *Summary); + } +} +``` + + +Never assume the JSON is well-formed. AI responses and network conditions vary — always check the return value of `FJsonSerializer::Deserialize` and guard against missing fields before reading them, or a malformed payload will crash a shipping build. + + +--- + +## Real-Time AI Events + +Beyond the request/response nodes above, the `UCometChatSubsystem` streams **AI assistant activity in real time** over the same WebSocket that delivers messages. Bind these delegates to react as an AI assistant works — for example, to show a streaming "typing" state while a bot composes a reply, or to render tool-call progress. All delegates fire on the **Game Thread**, so you can update UI directly. + +| Delegate | Payload | Fires When | +| -------- | ------- | ---------- | +| `OnAIAssistantEvent` | `FCometChatAIAssistantEvent` | A generic AI assistant lifecycle event occurs (e.g. the assistant starts or finishes) | +| `OnAIAssistantMessageReceived` | `FCometChatMessage` | An AI assistant message arrives | +| `OnAIToolResultReceived` | `FCometChatMessage` | An AI tool finishes and returns a result | +| `OnAIToolArgumentsReceived` | `FCometChatMessage` | An AI tool's arguments are received | + + + +1. Get a reference to the **CometChat Subsystem** +2. Drag off the Subsystem pin and search for the delegate name (e.g., **On AI Assistant Event**) +3. Select **Bind Event** to wire it to a custom event node + +Bind **On AI Assistant Event** to receive an `FCometChatAIAssistantEvent`; bind **On AI Assistant Message Received**, **On AI Tool Result Received**, or **On AI Tool Arguments Received** to receive an `FCometChatMessage`. + + +```cpp +void AMyActor::BeginPlay() +{ + Super::BeginPlay(); + + UCometChatSubsystem* Chat = GetGameInstance()->GetSubsystem(); + + Chat->OnAIAssistantEvent.AddDynamic(this, &AMyActor::HandleAIAssistantEvent); + Chat->OnAIAssistantMessageReceived.AddDynamic(this, &AMyActor::HandleAIMessage); + Chat->OnAIToolResultReceived.AddDynamic(this, &AMyActor::HandleAIToolResult); + Chat->OnAIToolArgumentsReceived.AddDynamic(this, &AMyActor::HandleAIToolArguments); +} + +void AMyActor::HandleAIAssistantEvent(const FCometChatAIAssistantEvent& Event) +{ + UE_LOG(LogTemp, Log, TEXT("AI event [%s] from %s in conversation %s: %s"), + *Event.EventType, *Event.SenderUid, *Event.ConversationId, *Event.Data); +} + +void AMyActor::HandleAIMessage(const FCometChatMessage& Message) +{ + UE_LOG(LogTemp, Log, TEXT("AI assistant message: %s"), *Message.Text); +} + +void AMyActor::HandleAIToolResult(const FCometChatMessage& Message) +{ + UE_LOG(LogTemp, Log, TEXT("AI tool result received (id %s)"), *Message.Id); +} + +void AMyActor::HandleAIToolArguments(const FCometChatMessage& Message) +{ + UE_LOG(LogTemp, Log, TEXT("AI tool arguments received (id %s)"), *Message.Id); +} +``` + + + +### FCometChatAIAssistantEvent + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `EventType` | `FString` | The kind of AI assistant event (e.g. a start/end or status marker) | +| `Data` | `FString` | The event payload, typically a JSON string you parse for details | +| `ConversationId` | `FString` | The conversation the event belongs to | +| `SenderUid` | `FString` | The UID associated with the event (the bot/assistant) | + + +**Bind AI delegates before calling Login.** Events that arrive between login completing and your bindings being set up will be missed. See [Real-Time Events](/sdk/unreal/real-time-events) for the full delegate catalog and threading guarantees. + + +--- + +## Next Steps + + + + The full catalog of subsystem delegates, including AI assistant events. + + + Send the replies and starters your players pick straight into the chat. + + diff --git a/sdk/unreal/authentication.mdx b/sdk/unreal/authentication.mdx index 01560fe32..3fcc8e8bd 100644 --- a/sdk/unreal/authentication.mdx +++ b/sdk/unreal/authentication.mdx @@ -86,23 +86,72 @@ void AMyActor::BeginPlay() } } -void AMyActor::OnLoginSuccess() +void AMyActor::OnLoginSuccess(const FCometChatUser& User) { - UE_LOG(LogTemp, Log, TEXT("Login successful!")); + // On Success returns the logged-in FCometChatUser + UE_LOG(LogTemp, Log, TEXT("Logged in as %s (%s)"), *User.Name, *User.Uid); } -void AMyActor::OnLoginFailed(const FString& Error) +void AMyActor::OnLoginFailed(const FCometChatError& Error) { - UE_LOG(LogTemp, Error, TEXT("Login failed: %s"), *Error); + UE_LOG(LogTemp, Error, TEXT("Login failed: %s"), *Error.Message); } ``` + +**Use case — "Sign in and enter the lobby."** A player launches your game and you sign them into chat before showing the social hub: + +1. `Event Init` → `Get CometChat Subsystem` → `Configure Async` (App Id, Region) +2. **On Success** → check the returned `FCometChatUser`: if `Uid` is **not** empty, a session was restored — skip to step 4 +3. `Login Async` (Uid, Auth Key) +4. **On Success** (returns `FCometChatUser`) → bind real-time delegates, open the lobby / friends list +5. **On Failure** → `Break FCometChatError` → show the `Message` on your login screen and let the player retry + + **UID format**: UIDs can be alphanumeric with underscores and hyphens. Spaces, punctuation, and other special characters are not allowed. +### Handling login failures + +Read `FCometChatError.Code` to decide how to recover — a bad credential should re-prompt, a network blip should offer retry. + +```cpp +void AMyActor::OnLoginFailed(const FCometChatError& Error) +{ + if (Error.Code == TEXT("ERR_UID_NOT_FOUND") || Error.Code.Contains(TEXT("AUTH"))) + { + ShowLoginScreen(TEXT("Invalid credentials — please sign in again.")); + } + else + { + // Likely a network/connectivity issue — safe to retry + ShowRetryPrompt(Error.Message); + } +} +``` + + +**Bad practice — shipping your Auth Key.** The Auth Key can authenticate *any* user in your app, so never embed it in a packaged client. Use it only for proof-of-concept, and switch to server-minted **Auth Tokens** (below) for production. Also always call **Configure** before **Login** — logging in first fails. + + +### Reference: sample UI + +The bundled `UCometChatGroupChatBox` widget logs in via the **native C++ SDK** (`GetSdk()->login(...)`) rather than the Blueprint node — a lower-level alternative. Prefer the **Login Async** node in your own code. + +```cpp +// UCometChatGroupChatBox::DoLogin() — Plugins/CometChat/Source/CometChat/UI/CometChatGroupChatBox.cpp +Sub->GetSdk()->login(TCHAR_TO_UTF8(*UserUid), TCHAR_TO_UTF8(*AuthKey), + [this](const std::string& err) { + AsyncTask(ENamedThreads::GameThread, [this, err]() { + if (err.empty()) { /* logged in — proceed to join group */ DoJoinGroup(); } + else { SetState(EChatState::Error, /* show err */); } + }); + }); +``` + --- ## Login using Auth Token @@ -170,21 +219,31 @@ void AMyActor::LoginWithToken(const FString& AuthToken) } } -void AMyActor::OnLoginSuccess() +void AMyActor::OnLoginSuccess(const FCometChatUser& User) { - UE_LOG(LogTemp, Log, TEXT("Auth token login successful!")); + UE_LOG(LogTemp, Log, TEXT("Auth token login OK — %s"), *User.Name); } -void AMyActor::OnLoginFailed(const FString& Error) +void AMyActor::OnLoginFailed(const FCometChatError& Error) { - UE_LOG(LogTemp, Error, TEXT("Auth token login failed: %s"), *Error); + UE_LOG(LogTemp, Error, TEXT("Auth token login failed: %s"), *Error.Message); } ``` + +**Use case — "Production sign-in via your backend."** When the player authenticates with your own account system: + +1. Your game server requests an Auth Token from the CometChat REST API for that player's UID +2. Your server returns the token to the client +3. `Login With Auth Token Async` (Auth Token) +4. **On Success** → enter chat +5. **On Failure** → `Break FCometChatError`; an expired/invalid token should trigger your backend to mint a fresh one and retry once + + -**Security**: Never hardcode Auth Tokens in your client. Always fetch them from your backend at runtime. +**Security**: Never hardcode Auth Tokens in your client. Always fetch them from your backend at runtime. An Auth Token is short-lived and scoped to a single user — the safe choice for shipping builds. --- diff --git a/sdk/unreal/connection-status.mdx b/sdk/unreal/connection-status.mdx index 1c0290cac..9a5461650 100644 --- a/sdk/unreal/connection-status.mdx +++ b/sdk/unreal/connection-status.mdx @@ -35,14 +35,19 @@ void AMyActor::HandleConnection(ECometChatConnectionState State) HideConnectionBanner(); break; + case ECometChatConnectionState::Connecting: + UE_LOG(LogTemp, Warning, TEXT("Connecting/Reconnecting...")); + ShowConnectionBanner(TEXT("Reconnecting...")); + break; + case ECometChatConnectionState::Disconnected: UE_LOG(LogTemp, Warning, TEXT("Disconnected — no real-time events")); ShowConnectionBanner(TEXT("Connection lost")); break; - case ECometChatConnectionState::Reconnecting: - UE_LOG(LogTemp, Warning, TEXT("Reconnecting...")); - ShowConnectionBanner(TEXT("Reconnecting...")); + case ECometChatConnectionState::FeatureThrottled: + UE_LOG(LogTemp, Warning, TEXT("Rate limited")); + ShowConnectionBanner(TEXT("Rate limited — slow down")); break; } } @@ -57,8 +62,9 @@ void AMyActor::HandleConnection(ECometChatConnectionState State) | Value | Description | | ----- | ----------- | | `Connected` | WebSocket is active. Real-time events (messages, presence, typing, receipts) are flowing. | +| `Connecting` | The SDK detected a disconnect and is automatically attempting to (re)connect. | | `Disconnected` | WebSocket is closed. No real-time events will be received until reconnection. | -| `Reconnecting` | The SDK detected a disconnect and is automatically attempting to reconnect. | +| `FeatureThrottled` | A feature is being rate-limited by the server (e.g., too many requests in a short window). | --- @@ -66,19 +72,23 @@ void AMyActor::HandleConnection(ECometChatConnectionState State) The SDK handles reconnection automatically: -1. When the WebSocket drops (network change, server timeout, etc.), the state moves to `Reconnecting` +1. When the WebSocket drops (network change, server timeout, etc.), the state moves to `Connecting` 2. The SDK retries with exponential backoff 3. On successful reconnection, the state moves back to `Connected` 4. If reconnection fails permanently, the state moves to `Disconnected` -**UI pattern**: Show a subtle banner at the top of your chat screen when the state is `Disconnected` or `Reconnecting`. Hide it when `Connected` fires again. This gives players confidence that the chat system is working. +**UI pattern**: Show a subtle banner at the top of your chat screen when the state is `Disconnected` or `Connecting`. Hide it when `Connected` fires again. This gives players confidence that the chat system is working. While disconnected, you can still call async nodes like **Get Messages Async** — those use HTTP requests, not the WebSocket. Only real-time push events are affected by the connection state. + +**Resilience pattern**: `OnConnectionStateChanged` is event-driven, so a banner that only updates from the delegate can miss a transition on an unreliable network (e.g., a background app resume, or a carrier switch that drops one packet). For a chat surface that must never get visually stuck, poll `GetConnectionStatus()` on a short timer (e.g., every 2–5 seconds) and reconcile your banner to the returned state, in addition to binding the delegate. The delegate gives you instant updates; the poll is your safety net. + + --- ## Next Steps diff --git a/sdk/unreal/conversations.mdx b/sdk/unreal/conversations.mdx index 3c01cbeb7..c26734354 100644 --- a/sdk/unreal/conversations.mdx +++ b/sdk/unreal/conversations.mdx @@ -11,14 +11,19 @@ Conversations represent the chat threads a user is part of — both 1:1 and grou +**Use case:** build the recent-chats list — one row per thread showing the other party's name, the last-message preview, and an unread badge. + +**Blueprint flow** + 1. Create an `FCometChatConversationsRequest` struct 2. Set filters (conversation type, tags, unread only, etc.) 3. Call the **Fetch Conversations Async** node -4. On Success, iterate the returned `TArray` +4. **On Success** → **For Each Loop** over the returned `TArray` → build a row from the name, `LastMessage.Text`, and an `UnreadMessageCount` badge +5. **On Failure** → a **Handle Fetch Error** custom event that takes an `FCometChatError` → **Break** it, show `Message`, and log `Code` for triage ```cpp @@ -50,6 +55,37 @@ void AMyActor::HandleConversations(const TArray& Convers +### Reference: sample UI + +The bundled sample panel fills its recent-chats list using the **native C++ SDK** directly rather than the async node. In your own game code, prefer the **Fetch Conversations Async** node above — it hops results back to the Game Thread for you. + +```cpp +// From UCometChatPanel::FetchPersonalChats() — CometChatPanel.cpp +// Native C++ SDK call (chatsdk::), shown for reference. Prefer the async node in game code. +void UCometChatPanel::FetchPersonalChats() +{ + auto* Sub = GetSubsystem(); + if (!Sub || !Sub->GetSdk()) return; + + chatsdk::ConversationsRequestBuilder req; + req.limit = 50; + req.conversation_type = "user"; // "user" or "group" + + Sub->GetSdk()->fetch_conversations(req, + [this](const std::string& err, const std::vector& convs) + { + // Native callback runs off the game thread — hop back before touching UMG + AsyncTask(ENamedThreads::GameThread, [this, convs]() + { + PersonalConversations.Empty(); + for (auto& c : convs) + PersonalConversations.Add(CometChatConversions::ToUnreal(c)); + RebuildPersonalList(); // redraw the recent-chats list + }); + }); +} +``` + --- ## FCometChatConversationsRequest @@ -118,6 +154,571 @@ flowchart TD --- +## Conversation Actions + +Beyond listing conversations, you can operate on a single thread by its participant — the other user's **UID** for a 1:1 chat, or the group's **GUID** for a group chat. Always pass the matching `ConversationType` (`user` or `group`) alongside it. + +### Get a Conversation + + + +Call the **Get Conversation Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Conversation With | `FString` | The other user's UID (1:1) or the group's GUID | +| Conversation Type | `FString` | `user` or `group` | + +**On Success** returns the matching `FCometChatConversation`, including its `UnreadMessageCount`, `LastMessage`, and tags. + + +```cpp +#include "AsyncActions/CometChatGetConversationAction.h" + +void AMyActor::OpenConversation() +{ + auto* Action = UCometChatGetConversationAction::GetConversationAsync( + this, + TEXT("cometchat-uid-2"), // other user's UID (or a group GUID) + TEXT("user") // "user" or "group" + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnConversationLoaded); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnConversationLoaded(const FCometChatConversation& Conversation) +{ + UE_LOG(LogTemp, Log, TEXT("Unread: %d — Last: %s"), + Conversation.UnreadMessageCount, *Conversation.LastMessage.Text); +} + +void AMyActor::OnError(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("Conversation op failed: %s"), *Error.Message); +} +``` + + + +### Delete a Conversation + + + +**Use case:** let the user swipe or long-press a row to remove that thread from their recent-chats list. + +Call the **Delete Conversation Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Conversation With | `FString` | The other user's UID (1:1) or the group's GUID | +| Conversation Type | `FString` | `user` or `group` | + +**On Success** returns a `FString` result message confirming the deletion. + +**Blueprint flow** + +1. On the row's **Delete** button **On Clicked**, read that row's UID/GUID and `ConversationType` +2. Call **Delete Conversation Async** +3. **On Success** (`FString` result) → **Remove Index** from your conversation array → refresh the list widget +4. **On Failure** → a **Handle Error** event taking an `FCometChatError` → **Break** it, show `Message`, and leave the row in place + + +```cpp +#include "AsyncActions/CometChatDeleteConversationAction.h" + +void AMyActor::DeleteConversation() +{ + auto* Action = UCometChatDeleteConversationAction::DeleteConversationAsync( + this, + TEXT("cometchat-uid-2"), // other user's UID (or a group GUID) + TEXT("user") // "user" or "group" + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnConversationDeleted); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnConversationDeleted(const FString& Result) +{ + UE_LOG(LogTemp, Log, TEXT("Conversation deleted: %s"), *Result); +} +``` + + + + +Deleting a conversation removes it from **this** user's list only — it does not delete the messages for the other participant. + + +### Tag a Conversation + + + +**Use case:** pin or favorite a thread — write a `pinned` (or `favorite`) tag so you can float it to the top of the list. + +Call the **Tag Conversation Async** node to attach custom tags (for pinning, foldering, or labeling a thread). + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Conversation With | `FString` | The other user's UID (1:1) or the group's GUID | +| Conversation Type | `FString` | `user` or `group` | +| Tags | `TArray` | The tags to set on the conversation | + +**On Success** returns the updated `FCometChatConversation` with its new `Tags`. + +**Blueprint flow** + +1. On the row's **Pin** toggle **On Clicked**, build a `Tags` array (add or drop `pinned`) +2. Call **Tag Conversation Async** with the row's UID/GUID and type +3. **On Success** (`FCometChatConversation`) → read `Tags`, update the pin icon, and re-sort the list +4. **On Failure** → a **Handle Error** event taking an `FCometChatError` → **Break** it, show `Message`, and revert the toggle + + +```cpp +#include "AsyncActions/CometChatTagConversationAction.h" + +void AMyActor::PinConversation() +{ + TArray Tags; + Tags.Add(TEXT("pinned")); + Tags.Add(TEXT("guild-mates")); + + auto* Action = UCometChatTagConversationAction::TagConversationAsync( + this, + TEXT("cometchat-uid-2"), // other user's UID (or a group GUID) + TEXT("user"), // "user" or "group" + Tags + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnConversationTagged); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnConversationTagged(const FCometChatConversation& Result) +{ + UE_LOG(LogTemp, Log, TEXT("Now tagged with %d tag(s)"), Result.Tags.Num()); +} +``` + + + +### Mark a Conversation Read + + + +**Use case:** when the user opens a thread, clear its unread badge in one call instead of marking each message. + +Call the **Mark Conversation Read Async** node to clear the unread badge for an entire thread in one call. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Conversation With | `FString` | The other user's UID (1:1) or the group's GUID | +| Conversation Type | `FString` | `user` or `group` | + +**On Success** returns a `FString` result message. + +**Blueprint flow** + +1. On **Open Conversation**, call **Mark Conversation Read Async** with the thread's UID/GUID and type +2. **On Success** (`FString`) → set that row's `UnreadMessageCount` to `0` and hide the badge +3. **On Failure** → a **Handle Error** event taking an `FCometChatError` → **Break** it, log `Code` / `Message`, and leave the badge until the next fetch + + +```cpp +#include "AsyncActions/CometChatMarkConversationReadAction.h" + +void AMyActor::MarkThreadRead() +{ + auto* Action = UCometChatMarkConversationReadAction::MarkConversationReadAsync( + this, + TEXT("cometchat-uid-2"), // other user's UID (or a group GUID) + TEXT("user") // "user" or "group" + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnConversationRead); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnConversationRead(const FString& Result) +{ + UE_LOG(LogTemp, Log, TEXT("Conversation marked read: %s"), *Result); +} +``` + + + +### Mark a Conversation Delivered + + + +Call the **Mark Conversation Delivered Async** node to send delivery receipts for a whole thread at once. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Conversation With | `FString` | The other user's UID (1:1) or the group's GUID | +| Conversation Type | `FString` | `user` or `group` | + +**On Success** returns a `FString` result message. + + +```cpp +#include "AsyncActions/CometChatMarkConversationDeliveredAction.h" + +void AMyActor::MarkThreadDelivered() +{ + auto* Action = UCometChatMarkConversationDeliveredAction::MarkConversationDeliveredAsync( + this, + TEXT("cometchat-uid-2"), // other user's UID (or a group GUID) + TEXT("user") // "user" or "group" + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnConversationDelivered); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnConversationDelivered(const FString& Result) +{ + UE_LOG(LogTemp, Log, TEXT("Conversation marked delivered: %s"), *Result); +} +``` + + + +--- + +## Muting Conversations + +Muting silences notifications for a thread without leaving it or removing it from the list. You mute and unmute in batches, and you can query which conversations are currently muted. + +### Mute Conversations + + + +Call the **Mute Conversations Async** node with an array of `FCometChatMutedConversation` entries. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Conversations | `TArray` | The conversations to mute, each with an `Id`, `Type`, and `Until` | + +**On Success** fires with **no payload** — the mute has been applied. + + +```cpp +#include "AsyncActions/CometChatMuteConversationsAction.h" + +void AMyActor::MuteGuildChat() +{ + FCometChatMutedConversation ToMute; + ToMute.Id = TEXT("group-abc-123"); // UID for a user, GUID for a group + ToMute.Type = TEXT("group"); // "user" or "group" + ToMute.Until = 0; // 0 = mute indefinitely + + TArray Conversations; + Conversations.Add(ToMute); + + auto* Action = UCometChatMuteConversationsAction::MuteConversationsAsync(this, Conversations); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnConversationsMuted); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnConversationsMuted() +{ + // No payload — notifications are now silenced for the requested threads +} +``` + + + +### Unmute Conversations + + + +Call the **Unmute Conversations Async** node with an array of `FCometChatUnmutedConversation` entries. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Conversations | `TArray` | The conversations to unmute, each with an `Id` and `Type` | + +**On Success** fires with **no payload**. + + +```cpp +#include "AsyncActions/CometChatUnmuteConversationsAction.h" + +void AMyActor::UnmuteGuildChat() +{ + FCometChatUnmutedConversation ToUnmute; + ToUnmute.Id = TEXT("group-abc-123"); // UID for a user, GUID for a group + ToUnmute.Type = TEXT("group"); // "user" or "group" + + TArray Conversations; + Conversations.Add(ToUnmute); + + auto* Action = UCometChatUnmuteConversationsAction::UnmuteConversationsAsync(this, Conversations); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnConversationsUnmuted); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnConversationsUnmuted() +{ + // No payload — notifications are restored +} +``` + + + +### Get Muted Conversations + + + +Call the **Get Muted Conversations Async** node (no parameters) to list every conversation the current user has muted. + +**On Success** returns a `TArray`. + + +```cpp +#include "AsyncActions/CometChatGetMutedConversationsAction.h" + +void AMyActor::LoadMutedConversations() +{ + auto* Action = UCometChatGetMutedConversationsAction::GetMutedConversationsAsync(this); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnMutedConversations); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnMutedConversations(const TArray& Result) +{ + for (const FCometChatMutedConversation& Muted : Result) + { + UE_LOG(LogTemp, Log, TEXT("Muted %s (%s) until %lld"), + *Muted.Id, *Muted.Type, Muted.Until); + } +} +``` + + + +### FCometChatMutedConversation + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Id` | `FString` | The other user's UID (1:1) or the group's GUID | +| `Type` | `FString` | `user` or `group` | +| `Until` | `int64` | Unix-ms timestamp to mute until; use `0` to mute with no expiry | + +### FCometChatUnmutedConversation + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Id` | `FString` | The other user's UID (1:1) or the group's GUID | +| `Type` | `FString` | `user` or `group` | + +--- + +## Unread Counts + +These nodes return the raw unread tallies the server tracks. Each resolves to a `FString` — the app-wide count comes back as a JSON object you parse, while the scoped nodes return a single count string. + +### Get Unread Count + + + +Call the **Get Unread Count Async** node (no parameters). + +**On Success** returns a `FString` (`JsonResult`) — a JSON object of unread counts across all conversations. + + +```cpp +#include "AsyncActions/CometChatGetUnreadCountAction.h" + +void AMyActor::LoadUnreadCounts() +{ + auto* Action = UCometChatGetUnreadCountAction::GetUnreadCountAsync(this); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnUnreadCount); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnUnreadCount(const FString& JsonResult) +{ + // JsonResult is a JSON object — parse it with FJsonSerializer + UE_LOG(LogTemp, Log, TEXT("Unread counts: %s"), *JsonResult); +} +``` + + + +### Get Unread Count For a User + + + +Call the **Get Unread Count For User Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| UID | `FString` | The other user's UID | + +**On Success** returns a `FString` unread count for that 1:1 conversation. + + +```cpp +#include "AsyncActions/CometChatGetUnreadCountForUserAction.h" + +void AMyActor::LoadUserUnread() +{ + auto* Action = UCometChatGetUnreadCountForUserAction::GetUnreadCountForUserAsync( + this, TEXT("cometchat-uid-2")); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnScopedUnread); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnScopedUnread(const FString& Result) +{ + UE_LOG(LogTemp, Log, TEXT("Unread: %s"), *Result); +} +``` + + + +### Get Unread Count For a Group + + + +Call the **Get Unread Count For Group Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| GUID | `FString` | The group's unique identifier | + +**On Success** returns a `FString` unread count for that group. + + +```cpp +#include "AsyncActions/CometChatGetUnreadCountForGroupAction.h" + +void AMyActor::LoadGroupUnread() +{ + auto* Action = UCometChatGetUnreadCountForGroupAction::GetUnreadCountForGroupAsync( + this, TEXT("group-abc-123")); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnScopedUnread); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} +``` + + + +### Get Unread Count For All Users + + + +Call the **Get Unread Count All Users Async** node (no parameters). + +**On Success** returns a `FString` of the combined unread count across all 1:1 conversations. + + +```cpp +#include "AsyncActions/CometChatGetUnreadCountAllUsersAction.h" + +void AMyActor::LoadAllUsersUnread() +{ + auto* Action = UCometChatGetUnreadCountAllUsersAction::GetUnreadCountAllUsersAsync(this); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnScopedUnread); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} +``` + + + +### Get Unread Count For All Groups + + + +Call the **Get Unread Count All Groups Async** node (no parameters). + +**On Success** returns a `FString` of the combined unread count across all groups. + + +```cpp +#include "AsyncActions/CometChatGetUnreadCountAllGroupsAction.h" + +void AMyActor::LoadAllGroupsUnread() +{ + auto* Action = UCometChatGetUnreadCountAllGroupsAction::GetUnreadCountAllGroupsAsync(this); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnScopedUnread); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} +``` + + + + +These nodes return **raw JSON/count strings** that you parse yourself. For rendering a conversation list UI, the per-conversation `UnreadMessageCount` field already included in each `FCometChatConversation` from **Fetch Conversations** is usually more convenient — you get the counts and the last message in a single fetch, with no extra parsing. + + +--- + +## Best Practices + + +**Badge from the conversation, not the count endpoints.** Each `FCometChatConversation` from **Fetch Conversations** already carries `UnreadMessageCount` and `LastMessage` — draw your unread badges straight from that single fetch. Keep the list live by refreshing rows from the **On Message Received** delegate as new messages arrive, so badges tick up without another round trip. Reserve the **Get Unread Count** JSON endpoints for app-icon or tab totals where you truly need a server-wide tally. + + + +**Don't re-fetch the whole list on every new message.** Calling **Fetch Conversations** again for each incoming message hammers the network and makes the list flicker as it rebuilds. Instead, when **On Message Received** fires, find the affected conversation row, bump its `UnreadMessageCount`, swap in the new `LastMessage`, and move it to the top — update the one row, not the whole list. + + +--- + +## Handling Failures + +Every async node exposes an **On Failure** pin (and each C++ action a bindable `OnFailure`) that delivers an `FCometChatError`. Always wire it: a deleted thread, a stale GUID, or a network blip should surface a message, not fail silently. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `Code` | `FString` | Machine-readable error code (e.g. `ERR_GUID_NOT_FOUND`) — branch on this | +| `Message` | `FString` | Human-readable summary — safe to show in a toast | +| `Details` | `FString` | Raw server payload for logging and triage | + + + +1. Drag off the node's **On Failure** execution pin +2. Route it to a **Handle Conversation Error** custom event that takes an `FCometChatError` +3. **Break** the struct to read `Code`, `Message`, and `Details` +4. **Switch on String** (`Code`) → on `ERR_GUID_NOT_FOUND`, remove the row; otherwise show `Message` in a toast and log `Details` + + +```cpp +void AMyActor::HandleConversationError(const FCometChatError& Error) +{ + // Code is machine-readable, Message is user-facing, Details is the raw payload + UE_LOG(LogTemp, Error, TEXT("Conversation op failed [%s]: %s"), + *Error.Code, *Error.Message); + + if (Error.Code == TEXT("ERR_GUID_NOT_FOUND")) + { + // The thread no longer exists — drop it from the local list + RemoveConversationRow(); + return; + } + + // Log the raw payload for anything unexpected, then let the user retry + UE_LOG(LogTemp, Verbose, TEXT("Details: %s"), *Error.Details); + ShowRetryToast(Error.Message); +} +``` + + + +--- + ## Next Steps diff --git a/sdk/unreal/delivery-read-receipts.mdx b/sdk/unreal/delivery-read-receipts.mdx index c435fcdb4..5ce94f996 100644 --- a/sdk/unreal/delivery-read-receipts.mdx +++ b/sdk/unreal/delivery-read-receipts.mdx @@ -3,20 +3,30 @@ title: "Delivery & Read Receipts" description: "Track when messages are delivered to and read by recipients." --- -Delivery and read receipts let you show checkmark indicators (✓ delivered, ✓✓ read) next to messages in your chat UI. The CometChat Unreal SDK delivers these as real-time events through the `OnReceiptReceived` delegate. +Delivery and read receipts let you show checkmark indicators (✓ delivered, ✓✓ read) next to messages in your chat UI. The CometChat Unreal SDK delivers these as real-time events through two delegates on the subsystem — `OnMessagesDelivered` and `OnMessagesRead` — each carrying an `FCometChatMessageReceipt`. A `read` receipt implies `delivered`. --- -## Listen for Receipt Events +## Use Case: Checkmark States (✓ / ✓✓ / blue ✓✓) -Bind to the `OnReceiptReceived` delegate on the Subsystem. +**Goal:** drive per-message tick states like a familiar messaging app — a single grey tick when sent, a double grey tick when delivered, and a blue double tick when read. + +Two real-time delegates carry these transitions, both with an `FCometChatMessageReceipt` payload: + +| State | Source | Meaning | +| ----- | ------ | ------- | +| ✓ (single grey) | your local send success | Message accepted by the server | +| ✓✓ (double grey) | `OnMessagesDelivered` | Reached the recipient's device | +| ✓✓ (blue) | `OnMessagesRead` | Recipient viewed the message | + +Bind both delegates once, before Login (see [Real-Time Events](/sdk/unreal/real-time-events)). 1. Get a reference to the **CometChat Subsystem** -2. Drag off and search for **On Receipt Received** -3. Use **Bind Event** to connect it to a custom event -4. The custom event receives an `FCometChatReceiptEvent` parameter +2. **Bind Event** to **On Messages Delivered** → set that message row to the double grey tick using `Receipt.MessageId` +3. **Bind Event** to **On Messages Read** → set that message row to the blue double tick +4. Use `Receipt.ReceiverType` / `Receipt.ReceiverId` to confirm the receipt is for the conversation you're showing ```cpp @@ -25,63 +35,90 @@ void AMyActor::BeginPlay() Super::BeginPlay(); UCometChatSubsystem* Chat = GetGameInstance()->GetSubsystem(); - Chat->OnReceiptReceived.AddDynamic(this, &AMyActor::HandleReceipt); + Chat->OnMessagesDelivered.AddDynamic(this, &AMyActor::HandleDelivered); + Chat->OnMessagesRead.AddDynamic(this, &AMyActor::HandleRead); +} + +void AMyActor::HandleDelivered(const FCometChatMessageReceipt& Receipt) +{ + SetTickState(Receipt.MessageId, ETick::DeliveredGrey); // ✓✓ } -void AMyActor::HandleReceipt(const FCometChatReceiptEvent& Event) +void AMyActor::HandleRead(const FCometChatMessageReceipt& Receipt) { - if (Event.Status == TEXT("delivered")) - { - // Show single checkmark ✓ - MarkMessageDelivered(Event.MessageId); - } - else if (Event.Status == TEXT("read")) - { - // Show double checkmark ✓✓ - MarkMessageRead(Event.MessageId); - } + SetTickState(Receipt.MessageId, ETick::ReadBlue); // ✓✓ (blue) } ``` ---- - -## FCometChatReceiptEvent +### FCometChatMessageReceipt | Property | Type | Description | | -------- | ---- | ----------- | | `MessageId` | `FString` | The message this receipt applies to | -| `Uid` | `FString` | The user who triggered the receipt (the recipient) | -| `Status` | `FString` | `delivered` — message reached the device; `read` — message was viewed | -| `Timestamp` | `int64` | Unix timestamp when the receipt was generated | +| `Sender` | `FCometChatUser` | The user who generated the receipt (the recipient) | +| `ReceiverType` | `FString` | `"user"` or `"group"` | +| `ReceiverId` | `FString` | The conversation UID or GUID | +| `ReceiptType` | `FString` | `"delivered"` or `"read"` | +| `DeliveredAt` | `int64` | Unix timestamp of delivery (when set) | +| `ReadAt` | `int64` | Unix timestamp of read (when set) | +| `Timestamp` | `int64` | Unix timestamp the receipt was generated | + + +For groups, use `OnMessagesDeliveredToAll` / `OnMessagesReadByAll` to switch the ticks only once **every** member has delivered/read, rather than on the first receipt. + --- -## Receipt Flow - -```mermaid -flowchart TD - A["Sender sends message"] --> B["Message arrives on recipient's device"] - B --> C["'delivered' receipt"] - C -.-> D["OnReceiptReceived — Status: delivered"] - B --> E["Recipient views message"] - E --> F["'read' receipt"] - F -.-> G["OnReceiptReceived — Status: read"] - - style A fill:#333,stroke:#666,color:#fff - style B fill:#444,stroke:#888,color:#fff - style C fill:#333,stroke:#666,color:#fff - style D fill:#333,stroke:#666,color:#fff - style E fill:#444,stroke:#888,color:#fff - style F fill:#333,stroke:#666,color:#fff - style G fill:#333,stroke:#666,color:#fff +## Send a Read Receipt + +Delivery receipts are sent automatically for incoming messages, but you send **read** receipts yourself when a message is actually seen. Use the **Mark As Read Async** node (and **Mark As Delivered Async** if you manage delivery manually). Both are async nodes with **On Success** and **On Failure**. + + + +1. When a message row scrolls fully into view, call **Mark As Read Async** with that `FCometChatMessage` +2. **On Success** → update your local "read" bookkeeping (this is a parameterless success pin) +3. **On Failure** → receives an `FCometChatError`; log it and retry the mark later + + +```cpp +void AMyActor::MarkVisibleMessageRead(const FCometChatMessage& Message) +{ + UCometChatSubsystem* Chat = GetGameInstance()->GetSubsystem(); + if (!Chat->IsLoggedIn()) return; // no connection → nothing to send + + auto* Action = UCometChatMarkAsReadAction::MarkAsReadAsync(this, Message); + Action->OnSuccess.AddDynamic(this, &AMyActor::HandleMarkedRead); + Action->OnFailure.AddDynamic(this, &AMyActor::HandleError); + Action->Activate(); +} + +void AMyActor::HandleMarkedRead() +{ + // Local read state confirmed on the server +} + +void AMyActor::HandleError(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("Mark-as-read failed: %s"), *Error.Message); +} ``` + + -A `read` receipt implies `delivered`. If you receive a `read` event for a message you haven't seen a `delivered` event for, treat it as both delivered and read. +`OnMessagesDelivered` / `OnMessagesRead` are real-time delegates: a dropped socket pauses them, so reconcile tick state after reconnect and pair the view with a [connection banner](/sdk/unreal/connection-status). `MarkAsReadAsync` / `MarkAsDeliveredAsync` are async nodes — always wire **On Failure** (`FCometChatError`) so a failed receipt doesn't silently desync your read state. + +**Best practice:** only call **Mark As Read** when a message is actually visible on screen — fully scrolled into the viewport — not when the conversation merely opens. This keeps unread counts and blue ticks honest. + + + +**Avoid** marking the entire history read the moment a chat opens, or marking off-screen messages read. It corrupts unread counts for you and misreports "read" to the sender. Mark per message as each becomes visible. + + --- ## Next Steps diff --git a/sdk/unreal/groups.mdx b/sdk/unreal/groups.mdx index 3e276c101..7409657e9 100644 --- a/sdk/unreal/groups.mdx +++ b/sdk/unreal/groups.mdx @@ -32,18 +32,26 @@ flowchart TD ## Create a Group -Create a new group with a name and an initial set of member UIDs. +Create a new group by building an `FCometChatGroup` and passing it to the node. Set at least `Name` and `Type`; the server assigns the `Guid` and makes the caller the group **owner**. -Call the **Create Group Async** node. +Call the **Create Group Async** node with an `FCometChatGroup` struct. | Parameter | Type | Description | | --------- | ---- | ----------- | -| Name | `FString` | Display name for the group | -| Member Ids | `TArray` | UIDs of users to add as initial members | +| Group | `FCometChatGroup` | The group to create. Set `Name` and `Type` (`public`, `private`, or `password`). For a `password` group, also set `Password`. Leave `Guid` empty to let the server assign one, or set your own. | -**On Success** returns an `FCometChatGroup` with the server-assigned `Guid`. +**On Success** returns the created `FCometChatGroup` with its server-assigned `Guid`. + +**Use case — host a lobby.** When the host taps *Create Lobby*, create a `public` group so anyone can join, then open its chat view. + +**Blueprint flow** + +1. **Make FCometChatGroup** — set `Name` (for example, "Arena Lobby") and `Type` = `public`. +2. **Create Group Async** — pass the struct. +3. **On Success** (returns `Group`) → open the group chat with `Group.Guid`, then refresh your group list. +4. **On Failure** (returns `Error`) → show a toast with `Error.Message` and keep the create form open so the host can retry. ```cpp @@ -51,16 +59,12 @@ Call the **Create Group Async** node. void AMyActor::CreateLobbyGroup() { - TArray Members; - Members.Add(TEXT("cometchat-uid-1")); - Members.Add(TEXT("cometchat-uid-2")); - Members.Add(TEXT("cometchat-uid-3")); + FCometChatGroup Group; + Group.Name = TEXT("Game Lobby"); + Group.Type = TEXT("public"); // public | private | password + // Group.Password = TEXT("secret"); // required only when Type == "password" - auto* Action = UCometChatCreateGroupAction::CreateGroupAsync( - this, - TEXT("Game Lobby"), // Group name - Members // Initial members - ); + auto* Action = UCometChatCreateGroupAction::CreateGroupAsync(this, Group); Action->OnSuccess.AddDynamic(this, &AMyActor::OnGroupCreated); Action->OnFailure.AddDynamic(this, &AMyActor::OnGroupCreateFailed); Action->Activate(); @@ -72,14 +76,18 @@ void AMyActor::OnGroupCreated(const FCometChatGroup& Group) *Group.Name, *Group.Guid); } -void AMyActor::OnGroupCreateFailed(const FString& Error) +void AMyActor::OnGroupCreateFailed(const FCometChatError& Error) { - UE_LOG(LogTemp, Error, TEXT("Create group failed: %s"), *Error); + UE_LOG(LogTemp, Error, TEXT("Create group failed: %s"), *Error.Message); } ``` + +To seed a group with initial members, create it first, then call **Add Members Async** (see [Group Member Management](#group-member-management)). The group `Type` controls join behavior: anyone can join a `public` group, a `private` group requires being added/invited, and a `password` group requires the password on join. + + --- ## Join a Group @@ -96,9 +104,19 @@ Call the **Join Group Async** node. | Parameter | Type | Description | | --------- | ---- | ----------- | -| Guid | `FString` | The unique identifier of the group to join | +| GUID | `FString` | The unique identifier of the group to join | +| Group Type | `FString` | The group's type: `public`, `private`, or `password` | +| Password | `FString` | The group password — required only when Group Type is `password`; otherwise pass an empty string | + +**On Success** returns the joined `FCometChatGroup`. -**On Success** fires with no output — the user is now a member. +**Use case — join a guild.** From the guild browser, a player taps a group. For a `password` group, prompt for the password first, then join. + +**Blueprint flow** + +1. **Join Group Async** — pass `GUID`, `Group Type`, and `Password` (empty for non-password groups). +2. **On Success** (returns `Group`) → open the group chat and refresh the joined-groups list. +3. **On Failure** (returns `Error`) → branch on `Error.Code` (see [Handle join failures](#handle-join-failures)) to show the right message. ```cpp @@ -106,25 +124,73 @@ Call the **Join Group Async** node. void AMyActor::JoinGroup(const FString& Guid) { - auto* Action = UCometChatJoinGroupAction::JoinGroupAsync(this, Guid); + auto* Action = UCometChatJoinGroupAction::JoinGroupAsync( + this, + Guid, + TEXT("public"), // group type: public | private | password + TEXT("") // password (only needed for password groups) + ); Action->OnSuccess.AddDynamic(this, &AMyActor::OnGroupJoined); Action->OnFailure.AddDynamic(this, &AMyActor::OnJoinFailed); Action->Activate(); } -void AMyActor::OnGroupJoined() +void AMyActor::OnGroupJoined(const FCometChatGroup& Group) +{ + UE_LOG(LogTemp, Log, TEXT("Joined group: %s"), *Group.Name); +} + +void AMyActor::OnJoinFailed(const FCometChatError& Error) { - UE_LOG(LogTemp, Log, TEXT("Successfully joined the group")); + UE_LOG(LogTemp, Error, TEXT("Join failed: %s"), *Error.Message); } +``` + + + +### Handle join failures + +Wrong passwords, private-group restrictions, and full groups all surface on **On Failure** as an `FCometChatError`. Branch on `Error.Code` so players see a useful message instead of a generic failure. + + + +Wire **Join Group Async → On Failure** into a **Switch on String** driven by `Error.Code`: -void AMyActor::OnJoinFailed(const FString& Error) +1. `ERR_WRONG_GROUP_PASS` → reopen the password prompt and let the player retry. +2. `ERR_ALREADY_JOINED` → treat as success and open the group chat. +3. `ERR_GUID_NOT_FOUND` → tell the player the group no longer exists and refresh the list. +4. **Default** → show `Error.Message`; transient network failures land here and are safe to retry. + + +```cpp +void AMyActor::OnJoinFailed(const FCometChatError& Error) { - UE_LOG(LogTemp, Error, TEXT("Join failed: %s"), *Error); + if (Error.Code == TEXT("ERR_WRONG_GROUP_PASS")) + { + ShowPasswordPrompt(); // let the player re-enter the password + } + else if (Error.Code == TEXT("ERR_ALREADY_JOINED")) + { + OpenGroupChat(); // already a member — just enter + } + else if (Error.Code == TEXT("ERR_GUID_NOT_FOUND")) + { + RefreshGroupList(); // group was deleted — refresh the browser + } + else + { + // Network or unexpected error — safe to retry. + UE_LOG(LogTemp, Error, TEXT("Join failed [%s]: %s"), *Error.Code, *Error.Message); + } } ``` + +See the [Error Guide](/articles/error-guide) for the full list of `Error.Code` values — including `ERR_EMPTY_GROUP_PASS`, `ERR_GROUP_JOIN_NOT_ALLOWED`, and `ERR_NO_VACANCY`. + + --- ## Leave a Group @@ -162,9 +228,9 @@ void AMyActor::OnGroupLeft() UE_LOG(LogTemp, Log, TEXT("Left the group")); } -void AMyActor::OnLeaveFailed(const FString& Error) +void AMyActor::OnLeaveFailed(const FCometChatError& Error) { - UE_LOG(LogTemp, Error, TEXT("Leave failed: %s"), *Error); + UE_LOG(LogTemp, Error, TEXT("Leave failed: %s"), *Error.Message); } ``` @@ -205,6 +271,402 @@ void AMyActor::FetchGroups() --- +## Group Administration + +### Get a Group + +Fetch a single group's current details by GUID. + + + +Call the **Get Group Async** node with the group `GUID`. **On Success** returns an `FCometChatGroup`. + + +```cpp +#include "AsyncActions/CometChatGetGroupAction.h" + +void AMyActor::GetGroup(const FString& Guid) +{ + auto* Action = UCometChatGetGroupAction::GetGroupAsync(this, Guid); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnGroupFetched); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnGroupFetched(const FCometChatGroup& Group) +{ + UE_LOG(LogTemp, Log, TEXT("%s — %d members, owner %s"), *Group.Name, Group.MembersCount, *Group.Owner); +} +``` + + + +### Get Joined Groups + +Fetch the GUIDs of every group the logged-in user currently belongs to. + + + +Call the **Get Joined Groups Async** node (no parameters). **On Success** returns a `TArray` of group GUIDs. + + +```cpp +#include "AsyncActions/CometChatGetJoinedGroupsAction.h" + +void AMyActor::GetJoinedGroups() +{ + auto* Action = UCometChatGetJoinedGroupsAction::GetJoinedGroupsAsync(this); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnJoinedGroups); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnJoinedGroups(const TArray& Guids) +{ + UE_LOG(LogTemp, Log, TEXT("Joined %d groups"), Guids.Num()); +} +``` + + + +### Update a Group + +Update a group's name, description, icon, type, or metadata (requires `admin`/owner scope). + + + +Call the **Update Group Async** node with an `FCometChatGroup` whose `Guid` identifies the group and whose other fields carry the new values. **On Success** returns the updated `FCometChatGroup`. + + +```cpp +#include "AsyncActions/CometChatUpdateGroupAction.h" + +void AMyActor::RenameGroup(const FString& Guid) +{ + FCometChatGroup Group; + Group.Guid = Guid; + Group.Name = TEXT("Champions Lobby"); + Group.Description = TEXT("Ranked players only"); + + auto* Action = UCometChatUpdateGroupAction::UpdateGroupAsync(this, Group); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnGroupUpdated); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} +``` + + + +### Transfer Ownership + +Hand group ownership to another member. + + + +Call the **Transfer Ownership Async** node with the group `GUID` and the new owner's `UID`. Only the current owner can transfer ownership. + + +```cpp +#include "AsyncActions/CometChatTransferOwnershipAction.h" + +void AMyActor::TransferOwnership(const FString& Guid, const FString& NewOwnerUid) +{ + auto* Action = UCometChatTransferOwnershipAction::TransferOwnershipAsync(this, Guid, NewOwnerUid); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnOwnershipTransferred); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} +``` + + + +### Delete a Group + +Permanently delete a group (owner only). + + + +Call the **Delete Group Async** node with the group `GUID`. + + +```cpp +#include "AsyncActions/CometChatDeleteGroupAction.h" + +void AMyActor::DeleteGroup(const FString& Guid) +{ + auto* Action = UCometChatDeleteGroupAction::DeleteGroupAsync(this, Guid); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnGroupDeleted); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} +``` + + + + +**Delete Group** is irreversible — the group and its message history are removed for all members. Gate it behind an owner-only confirmation in your UI. + + +### Online Member Count + +Get the number of currently-online members across one or more groups. + + + +Call the **Get Online Group Member Count Async** node with a `TArray` of group GUIDs. + + +```cpp +#include "AsyncActions/CometChatGetOnlineGroupMemberCountAction.h" + +void AMyActor::GetOnlineCounts(const TArray& Guids) +{ + auto* Action = UCometChatGetOnlineGroupMemberCountAction::GetOnlineGroupMemberCountAsync(this, Guids); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnOnlineCounts); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} +``` + + + +--- + +## Group Member Management + +Once a group exists, use these nodes to fetch its members, add new ones, and moderate (kick/ban/unban), and change a member's role. + +### Fetch Members + + + +Call the **Fetch Group Members Async** node with an `FCometChatGroupMembersRequest` struct (set `GUID` to the target group). + + +```cpp +#include "AsyncActions/CometChatFetchGroupMembersAction.h" + +void AMyActor::FetchMembers(const FString& Guid) +{ + FCometChatGroupMembersRequest Request; + Request.GUID = Guid; + Request.Limit = 30; + + auto* Action = UCometChatFetchGroupMembersAction::FetchGroupMembersAsync(this, Request); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnMembersFetched); + Action->OnFailure.AddDynamic(this, &AMyActor::OnFetchFailed); + Action->Activate(); +} + +void AMyActor::OnMembersFetched(const TArray& Members) +{ + for (const FCometChatGroupMember& Member : Members) + { + UE_LOG(LogTemp, Log, TEXT("%s — %s"), *Member.User.Name, *Member.Scope); + } +} +``` + + + +### Reference: sample UI — Fetch Group Members + +The bundled sample panel loads members with the **native C++ SDK API** (`Sub->GetSdk()->fetch_group_members(...)`) and rebuilds its list on the game thread. In your own game, prefer the **Fetch Group Members Async** node shown above, which already marshals the result to the game thread for you. This snippet is trimmed to the SDK call and refresh. + +```cpp +// Trimmed from CometChatPanel.cpp — UCometChatPanel::FetchGroupMembers() +void UCometChatPanel::FetchGroupMembers() +{ + auto* Sub = GetSubsystem(); + if (!Sub || !Sub->GetSdk() || ActiveChatId.IsEmpty()) return; + + chatsdk::GroupMembersRequestBuilder req; + req.guid = TCHAR_TO_UTF8(*ActiveChatId); + req.limit = 50; + + Sub->GetSdk()->fetch_group_members(req, + [this](const std::string& err, const std::vector& members) + { + // The SDK callback runs off the game thread — hop back before touching UI. + AsyncTask(ENamedThreads::GameThread, [this, members]() + { + CurrentGroupMembers.Empty(); + for (const auto& m : members) + { + FCometChatGroupMember um; + um.User = CometChatConversions::ToUnreal(m.user); + um.Scope = UTF8_TO_TCHAR(m.scope.c_str()); + // Skip anyone we just banned — the server can still return them for a + // moment after the ban (replication lag), which would re-add them. + if (BannedUidFilter.Contains(um.User.Uid)) continue; + CurrentGroupMembers.Add(um); + } + RebuildGroupMembersList(); // refresh the list once data arrives + }); + } + ); +} +``` + + +This is the native C++ SDK surface used by the sample UI. Blueprint and C++ game code should call **Fetch Group Members Async** (see [Fetch Members](#fetch-members)) instead, so you never have to marshal threads yourself. + + +### Add Members + + + +Call the **Add Members Async** node with the group `Guid` and a `TArray` (set each entry's `User.Uid` and `Scope`, typically `"participant"`). + + +```cpp +#include "AsyncActions/CometChatAddMembersAction.h" + +void AMyActor::AddMemberToGroup(const FString& Guid, const FString& Uid) +{ + FCometChatGroupMember NewMember; + NewMember.User.Uid = Uid; + NewMember.Scope = TEXT("participant"); + + auto* Action = UCometChatAddMembersAction::AddMembersAsync(this, Guid, { NewMember }); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnMemberAdded); + Action->OnFailure.AddDynamic(this, &AMyActor::OnAddFailed); + Action->Activate(); +} +``` + + + +### Kick, Ban, and Unban + +**Kick** removes a member — they can rejoin. **Ban** removes them and prevents rejoining until **Unban** is called. + + + +Call **Kick Member Async**, **Ban Member Async**, or **Unban Member Async** with the target `Uid` and the group `Guid`. Fetch **Fetch Banned Members Async** (same request shape as fetching members) to list who's currently banned. + +**Use case — moderate a lobby.** A moderator taps a disruptive member and chooses *Ban* to remove them and stop them rejoining. + +**Blueprint flow** + +1. **Ban Member Async** — pass the target `Uid` and the group `Guid`. +2. **On Success** → remove the member from your active list, add them to the banned list, then refetch both to reconcile. +3. **On Failure** (returns `Error`) → show `Error.Message`; `ERR_GROUP_NO_CLEARANCE` means the caller lacks the scope to ban this member (for example, a moderator targeting an admin). + + +```cpp +#include "AsyncActions/CometChatKickMemberAction.h" +#include "AsyncActions/CometChatBanMemberAction.h" +#include "AsyncActions/CometChatUnbanMemberAction.h" +#include "AsyncActions/CometChatFetchBannedMembersAction.h" + +void AMyActor::BanMember(const FString& Uid, const FString& Guid) +{ + auto* Action = UCometChatBanMemberAction::BanMemberAsync(this, Uid, Guid); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnMemberBanned); + Action->OnFailure.AddDynamic(this, &AMyActor::OnActionFailed); + Action->Activate(); +} +``` + + + + +Ban and unban only take effect for other members — the SDK does not let a member ban themselves or the group owner. The server enforces this, but you should also gate the ban/kick/promote/demote UI controls in your app by the logged-in user's own scope (see the permission model below) so unauthorized options aren't even shown. + + +### Reference: sample UI — Ban Member + +The sample panel bans a member with the **native C++ SDK API** (`Sub->GetSdk()->ban_group_member(...)`) and then reconciles its lists. In your own game, prefer the **Ban Member Async** node shown above — this snippet is trimmed to show the success/refresh pattern. + +```cpp +// Trimmed from CometChatPanel.cpp — UCometChatPanel::OnMemberBanClicked() +void UCometChatPanel::OnMemberBanClicked() +{ + auto* Sub = GetSubsystem(); + if (!Sub || !Sub->GetSdk() || SelectedMemberUid.IsEmpty()) return; + + const FString BannedUid = SelectedMemberUid; + Sub->GetSdk()->ban_group_member(TCHAR_TO_UTF8(*BannedUid), TCHAR_TO_UTF8(*ActiveChatId), + [this, BannedUid](const std::string& err) + { + // The SDK callback runs off the game thread — hop back before touching UI. + AsyncTask(ENamedThreads::GameThread, [this, err, BannedUid]() + { + if (!err.empty()) { ShowSnackbar(TEXT("Failed to ban member")); return; } + + ShowSnackbar(TEXT("Member banned")); + // Optimistic update: move the member to the banned list now. A refetch races + // replication and often still returns the banned member, so we also add them + // to BannedUidFilter until the server catches up. + BannedUidFilter.Add(BannedUid); + for (int32 i = CurrentGroupMembers.Num() - 1; i >= 0; --i) + { + if (CurrentGroupMembers[i].User.Uid == BannedUid) + { + BannedMembers.Add(CurrentGroupMembers[i]); + CurrentGroupMembers.RemoveAt(i); + } + } + RebuildGroupMembersList(); + RebuildBannedMembersList(); + FetchGroupMembers(); // reconcile active list with the server + FetchBannedMembers(); // reconcile banned list with the server + }); + } + ); +} +``` + + +**Best practice — update locally, then reconcile.** Gate kick/ban/promote/demote controls by the caller's own `Scope` (or an owner check) so only authorized moderators see them. After a successful action, update your local member and banned lists immediately for a responsive UI, then refetch to reconcile with the server. + + + +**Bad practice — trusting an immediate refetch.** Don't show moderation controls to a `participant`, and don't assume a fetch fired right after a ban/kick/scope-change already reflects it. The server needs a moment to replicate, so a naive refetch can re-add the member you just removed — which is why the sample keeps a short-lived `BannedUidFilter` until the reconcile catches up. + + +### Promote and Demote (Change Scope) + +A member's role (`participant` → `moderator` → `admin`) is changed with the same node in either direction. + + + +Call the **Change Scope Async** node with the target `Uid`, the group `Guid`, and the new `Scope` (`"participant"`, `"moderator"`, or `"admin"`). + +**Use case — promote to moderator.** The owner promotes a trusted player so they can help moderate the group. + +**Blueprint flow** + +1. **Change Scope Async** — pass the target `Uid`, the group `Guid`, and `Scope` = `moderator`. +2. **On Success** → refetch the member list so the new role badge shows. +3. **On Failure** (returns `Error`) → show `Error.Message`; `ERR_SAME_SCOPE` means the member already has that scope, and `ERR_GROUP_NO_SCOPE_CLEARANCE` means the caller can't change this member's scope. + + +```cpp +#include "AsyncActions/CometChatChangeScopeAction.h" + +void AMyActor::PromoteToModerator(const FString& Uid, const FString& Guid) +{ + auto* Action = UCometChatChangeScopeAction::ChangeScopeAsync(this, Uid, Guid, TEXT("moderator")); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnScopeChanged); + Action->OnFailure.AddDynamic(this, &AMyActor::OnActionFailed); + Action->Activate(); +} +``` + + + + +**Permission model**: only `admin`/owner can promote someone to `admin`. A `moderator` can promote a `participant` to `moderator` and kick/ban members with a strictly lower scope than their own, but cannot touch another `admin` or the owner. Read the logged-in user's own scope from `FCometChatGroup.Scope` (or check `UserUid == Group.Owner`) before deciding which member actions to show. + + + +Listen for `OnGroupMemberKicked`, `OnGroupMemberBanned`, `OnGroupMemberUnbanned`, and `OnGroupMemberScopeChanged` on the subsystem to keep member lists in sync in real time — including changes made by other admins/moderators. See [Real-Time Events](/sdk/unreal/real-time-events). + + +--- + ## Send a Group Message Send a text message to all members of a group. See [Send a Message → Group Messages](/sdk/unreal/send-message#send-a-group-message) for details. @@ -224,7 +686,23 @@ Retrieve previous messages from a group with pagination. See [Receive Messages | `Guid` | `FString` | Server-assigned unique group identifier | | `Name` | `FString` | Group display name | | `Description` | `FString` | Group description text | -| `MemberIds` | `TArray` | UIDs of current group members | +| `Type` | `FString` | `public`, `private`, or `password` | +| `Password` | `FString` | Password (for `password`-type groups) | +| `Icon` | `FString` | URL to the group's icon image | +| `Owner` | `FString` | UID of the group owner | +| `Scope` | `FString` | The logged-in user's scope in this group (`admin`, `moderator`, `participant`) | +| `MembersCount` | `int32` | Number of members | +| `bHasJoined` | `bool` | Whether the logged-in user is a member | +| `JoinedAt` | `int64` | When the logged-in user joined | +| `bIsBannedFromGroup` | `bool` | Whether the logged-in user is banned from this group | +| `Metadata` | `FString` | Custom metadata JSON | +| `Tags` | `TArray` | Group tags | +| `CreatedAt` | `int64` | Creation timestamp | +| `UpdatedAt` | `int64` | Last update timestamp | + + +The member list is not a field on the group — fetch it separately with **Fetch Group Members Async** (see [Group Member Management](#group-member-management)). + --- diff --git a/sdk/unreal/key-concepts.mdx b/sdk/unreal/key-concepts.mdx index a0c03be3d..fb12c2cc9 100644 --- a/sdk/unreal/key-concepts.mdx +++ b/sdk/unreal/key-concepts.mdx @@ -88,28 +88,30 @@ All async callbacks fire on the **Game Thread**, so it's safe to update UI, spaw ## Real-Time Delegates -The Subsystem exposes five multicast delegates for real-time push events. Bind to them once after calling **Configure**, and they'll fire whenever the server pushes an update. +The Subsystem exposes many multicast delegates for real-time push events, grouped by listener (message, user, group, connection, login, call, AI). Bind to them once after calling **Configure** (before Login), and they fire whenever the server pushes an update. A few of the most common: | Delegate | Payload Type | Fires When | | -------- | ------------ | ---------- | -| `OnMessageReceived` | `FCometChatMessage` | A new message arrives in any conversation | +| `OnTextMessageReceived` | `FCometChatMessage` | A new text message arrives | | `OnPresenceChanged` | `FCometChatPresence` | A user goes online, offline, or away | -| `OnTypingChanged` | `FCometChatTypingEvent` | A user starts or stops typing | -| `OnReceiptReceived` | `FCometChatReceiptEvent` | A message is delivered or read | +| `OnTypingStarted` / `OnTypingEnded` | `FCometChatTypingIndicator` | A user starts / stops typing | +| `OnMessagesDelivered` / `OnMessagesRead` | `FCometChatMessageReceipt` | A message is delivered / read | | `OnConnectionStateChanged` | `ECometChatConnectionState` | WebSocket connects, disconnects, or reconnects | +See [Real-Time Events](/sdk/unreal/real-time-events) for the full list of delegates and the [API Reference](/sdk/unreal/reference#real-time-delegates) for every listener. + -Drag off the Subsystem reference and search for the delegate name (e.g., **On Message Received**). Use **Bind Event** to wire it to a custom event. +Drag off the Subsystem reference and search for the delegate name (e.g., **On Text Message Received**). Use **Bind Event** to wire it to a custom event. ```cpp UCometChatSubsystem* Chat = GetGameInstance()->GetSubsystem(); -Chat->OnMessageReceived.AddDynamic(this, &AMyActor::HandleNewMessage); +Chat->OnTextMessageReceived.AddDynamic(this, &AMyActor::HandleNewMessage); Chat->OnPresenceChanged.AddDynamic(this, &AMyActor::HandlePresence); -Chat->OnTypingChanged.AddDynamic(this, &AMyActor::HandleTyping); -Chat->OnReceiptReceived.AddDynamic(this, &AMyActor::HandleReceipt); +Chat->OnTypingStarted.AddDynamic(this, &AMyActor::HandleTypingStarted); +Chat->OnMessagesRead.AddDynamic(this, &AMyActor::HandleRead); Chat->OnConnectionStateChanged.AddDynamic(this, &AMyActor::HandleConnection); ``` @@ -180,22 +182,26 @@ The plugin uses Unreal-native `USTRUCT` types — no `std::string` or STL contai | `Status` | `ECometChatPresenceStatus` | `Online`, `Offline`, or `Away` | | `LastActiveAt` | `int64` | Unix timestamp of last activity | -### FCometChatTypingEvent +### FCometChatTypingIndicator | Property | Type | Description | | -------- | ---- | ----------- | -| `Uid` | `FString` | User who is typing | -| `ConversationId` | `FString` | Conversation where typing is happening | -| `bIsTyping` | `bool` | `true` if started typing, `false` if stopped | +| `ReceiverId` | `FString` | The conversation UID or GUID being typed in | +| `ReceiverType` | `FString` | `user` or `group` | +| `Metadata` | `FString` | Optional metadata JSON | +| `Sender` | `FCometChatUser` | The user who is typing | -### FCometChatReceiptEvent +### FCometChatMessageReceipt | Property | Type | Description | | -------- | ---- | ----------- | | `MessageId` | `FString` | The message this receipt is for | -| `Uid` | `FString` | User who triggered the receipt | -| `Status` | `FString` | `delivered` or `read` | -| `Timestamp` | `int64` | Unix timestamp of the receipt | +| `Sender` | `FCometChatUser` | The recipient who generated the receipt | +| `ReceiverType` | `FString` | `user` or `group` | +| `ReceiverId` | `FString` | The conversation UID or GUID | +| `ReceiptType` | `FString` | `delivered` or `read` | +| `DeliveredAt` | `int64` | Delivery timestamp | +| `ReadAt` | `int64` | Read timestamp | ### Enums @@ -204,8 +210,9 @@ The plugin uses Unreal-native `USTRUCT` types — no `std::string` or STL contai | Value | Description | | ----- | ----------- | | `Connected` | WebSocket is connected and active | +| `Connecting` | SDK is attempting to (re)connect | | `Disconnected` | WebSocket is disconnected | -| `Reconnecting` | SDK is attempting to reconnect | +| `FeatureThrottled` | A feature is being rate-limited | **ECometChatPresenceStatus** diff --git a/sdk/unreal/moderation.mdx b/sdk/unreal/moderation.mdx index ff66d1c0b..7f500e439 100644 --- a/sdk/unreal/moderation.mdx +++ b/sdk/unreal/moderation.mdx @@ -1,5 +1,5 @@ --- -title: "Moderation" +title: "Overview" description: "Flag messages for review and track moderation status." --- @@ -18,26 +18,31 @@ Use the **Flag Message Async** node to report a message for moderation review. 1. Create an `FCometChatFlagDetail` struct with the `ReasonId` and optional `Remark` -2. Call the **Flag Message Async** node with the message ID and flag detail -3. Handle **On Success** (message flagged) or **On Failure** (error) +2. Call the **Flag Message Async** node with the message ID (`int64`) and flag detail +3. Handle **On Success** (message flagged) or **On Failure** (receives an `FCometChatError`) ```cpp -void AMyActor::FlagMessage(const FString& MessageId) +void AMyActor::FlagMessage(int64 MessageId) { FCometChatFlagDetail FlagDetail; - FlagDetail.ReasonId = TEXT("spam"); + FlagDetail.ReasonId = TEXT("spam"); // an Id from Get Flag Reasons FlagDetail.Remark = TEXT("This message is spam"); - auto* Action = UCometChatFlagMessageAction::FlagMessage(this, MessageId, FlagDetail); + auto* Action = UCometChatFlagMessageAction::FlagMessageAsync(this, MessageId, FlagDetail); Action->OnSuccess.AddDynamic(this, &AMyActor::HandleFlagSuccess); - Action->OnFailure.AddDynamic(this, &AMyActor::HandleError); + Action->OnFailure.AddDynamic(this, &AMyActor::HandleFlagFailed); Action->Activate(); } -void AMyActor::HandleFlagSuccess() +void AMyActor::HandleFlagSuccess(const FString& Result) { - UE_LOG(LogTemp, Log, TEXT("Message flagged successfully")); + UE_LOG(LogTemp, Log, TEXT("Message flagged: %s"), *Result); +} + +void AMyActor::HandleFlagFailed(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("Flag failed: %s"), *Error.Message); } ``` @@ -59,9 +64,9 @@ Fetch the list of available reasons a user can select when flagging a message. ```cpp void AMyActor::FetchFlagReasons() { - auto* Action = UCometChatGetFlagReasonsAction::GetFlagReasons(this); + auto* Action = UCometChatGetFlagReasonsAction::GetFlagReasonsAsync(this); Action->OnSuccess.AddDynamic(this, &AMyActor::HandleFlagReasons); - Action->OnFailure.AddDynamic(this, &AMyActor::HandleError); + Action->OnFailure.AddDynamic(this, &AMyActor::HandleReasonsFailed); Action->Activate(); } @@ -170,6 +175,61 @@ The struct returned when fetching available flag reasons. --- +## Use Case: Report a Message + +**Goal:** let a player flag another player's message for review. **Flag Message Async** reports it with a reason; wire **both** branches so the reporter gets clear feedback. + + + +1. (Once) call **Get Flag Reasons Async** and cache the returned reasons for your report menu +2. On **Report**, build an `FCometChatFlagDetail` — `ReasonId` from the cached list, optional `Remark` +3. Call **Flag Message Async** with the message's numeric **Message Id** and the flag detail +4. **On Success** → show "Report submitted" and dismiss the menu (the success pin carries a result `FString`) +5. **On Failure** → receives an `FCometChatError`; show "Couldn't submit report, try again" and log `Message` + + +```cpp +void AMyActor::ReportMessage(int64 MessageId, const FString& ReasonId) +{ + FCometChatFlagDetail Detail; + Detail.ReasonId = ReasonId; + Detail.Remark = TEXT("Reported from game client"); + + auto* Action = UCometChatFlagMessageAction::FlagMessageAsync(this, MessageId, Detail); + Action->OnSuccess.AddDynamic(this, &AMyActor::HandleFlagSubmitted); + Action->OnFailure.AddDynamic(this, &AMyActor::HandleFlagError); + Action->Activate(); +} + +void AMyActor::HandleFlagSubmitted(const FString& Result) +{ + UE_LOG(LogTemp, Log, TEXT("Report submitted: %s"), *Result); +} + +// Shared failure handler for Flag Message + Get Flag Reasons +void AMyActor::HandleFlagError(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("Moderation request failed: %s"), *Error.Message); + // Surface a retry affordance in the report UI +} +``` + + + + +**Always wire On Failure** on both **Flag Message Async** and **Get Flag Reasons Async**. Both hit the network and can fail (offline, rate-limited, invalid reason). The pin delivers an `FCometChatError` — log `Error.Message` and let the player retry instead of leaving the report silently dropped. + + + +**Best practice:** call **Get Flag Reasons Async** once (on chat load or the first report) and cache the `TArray`. The list rarely changes, so re-fetching on every report wastes a round-trip. + + + +**Don't expose raw moderation state to everyone.** `ModerationStatus` (`Pending`, `Disapproved`) and who-flagged-what are moderator concerns. Hide or replace disapproved messages for regular players, and surface `Pending`/reviewer detail only in moderator-facing UI. + + +--- + ## Next Steps diff --git a/sdk/unreal/overview.mdx b/sdk/unreal/overview.mdx index ce61ee92b..00076c1b5 100644 --- a/sdk/unreal/overview.mdx +++ b/sdk/unreal/overview.mdx @@ -7,10 +7,6 @@ description: "Add in-app chat to your Unreal Engine game using the CometChat SDK - -**Beta Release** — The CometChat Unreal SDK is currently in beta. APIs and features may change, and you may encounter stability issues. - - This guide walks you through integrating CometChat into an Unreal Engine 5 project. The SDK ships as a native UE5 plugin with full **Blueprint** and **C++** support — so whether you're wiring up a quick prototype in the visual graph or building a production multiplayer title in code, you're covered. @@ -21,12 +17,16 @@ Before you begin, we recommend reading the [Key Concepts](/sdk/unreal/key-concep ## Supported Platforms -| Platform | Status | -| -------- | ------ | -| Windows (Win64) | ✅ Supported | -| macOS | ✅ Supported | -| iOS | ✅ Supported | -| Android | ✅ Supported | +| Platform | Status | How to install | +| -------- | ------ | -------------- | +| macOS | ✅ Supported | Precompiled (UE 5.5 and 5.7) or build from source | +| Windows (Win64) | ✅ Supported | Precompiled (UE 5.5) or build from source | +| iOS | ✅ Supported | Build from source | +| Android | ✅ Supported | Build from source | + + +iOS and Android compile from source and link the bundled static libraries automatically — see [Setup](/sdk/unreal/setup) for both paths. + --- diff --git a/sdk/unreal/push-notifications.mdx b/sdk/unreal/push-notifications.mdx new file mode 100644 index 000000000..67fcea57d --- /dev/null +++ b/sdk/unreal/push-notifications.mdx @@ -0,0 +1,468 @@ +--- +title: "Push Notifications & Preferences" +description: "Register device push tokens and manage per-user notification preferences from your Unreal Engine game." +--- + +Push notifications bring players back to your game — a guild @mention, a friend's challenge, an incoming voice call. The plugin ships the **full push path**: it obtains the device token (FCM on Android, APNs on iOS), registers it with CometChat after login, and displays the incoming notification. You turn it on with a few config values; no Firebase or Objective-C wiring of your own. It also stores **per-user notification preferences** so each player controls exactly which events buzz their phone. + + +**Push is opt-in and inert until you enable it.** Without `bEnablePushNotifications=True` in your `DefaultEngine.ini`, the plugin adds **nothing** to your build — no Firebase dependency, no manifest service, no notification permission, no startup code. Projects that don't use push are completely unaffected. + + +### Push Registration Flow + +```mermaid +flowchart LR + A["Plugin
(FCM / APNs)"] -->|"device token"| B["Plugin registers
on login"] + B --> C["CometChat Cloud"] + C -->|"friend messages you
while app is closed"| D["Push delivered"] + D --> E["Notification shown
+ OnPushNotificationReceived"] + + style A fill:#555,stroke:#999,color:#ccc + style B fill:#444,stroke:#888,color:#fff + style C fill:#555,stroke:#999,color:#ccc + style D fill:#333,stroke:#666,color:#fff + style E fill:#333,stroke:#666,color:#fff +``` + +Once configured, the plugin fetches the device token, waits until the user is authenticated, and registers automatically from **every** login path. It also re-registers when the platform rotates the token, and unregisters on logout so the next user on that device doesn't inherit the previous user's notifications. You don't call anything — but [Register Token Async](#register-a-push-token) remains available if you manage tokens yourself. + +--- + +## Setup + + + +In your app's dashboard, enable the **Notifications** add-on and add a push provider: +- **Android** — an **FCM** provider (upload your Firebase service-account JSON). +- **iOS** — an **APNs** provider (auth key `.p8` or certificate). + +Each provider has a **Provider ID**. Note it for the next step. + + +If the Notifications add-on isn't enabled on your app, token registration fails with `401 ERR_UNAUTHORIZED` / *"Forbidden resource"* no matter how the client is configured. Do this step first. + + + + +```ini +[CometChat] +; Master switch — without this the plugin adds no push wiring to your build at all. +bEnablePushNotifications=True + +; Provider ID from the dashboard (step 1). +PushProviderId=my-fcm-provider + +; Android only — from YOUR google-services.json. +; project_info → project_number / project_id / storage_bucket +; client → mobilesdk_app_id, api_key → current_key +FcmAppId=1:000000000000:android:0000000000000000000000 +FcmApiKey=AIza... +FcmProjectId=my-firebase-project +FcmSenderId=000000000000 +FcmBucket=my-firebase-project.appspot.com +``` + +These are injected into the Android build at package time — you do **not** add `google-services.json` to the plugin, and no credentials live in the plugin itself. + + +Your app's **Package Name** (Project Settings → Android) must match a client registered in that Firebase project, or Firebase issues a token your project can't deliver to. + + + + +Project Settings → **iOS** → **Enable Remote Notifications Support**. + +Unreal adds the `aps-environment` entitlement; the plugin then registers for APNs and forwards the device token to CometChat. Your provisioning profile must have the **Push Notifications** capability. + + +The plugin deliberately does not add this entitlement itself — injecting it into an app whose provisioning profile lacks Push would break code signing. With the setting off, the iOS push code is inert. + + + + +Log a user in, then check the log: +``` +CometChatFCM: FirebaseApp initialized (project=my-firebase-project) +CometChatFCM: FCM token ready (142 chars) +LogCometChat: Registering FCM push token (142 chars) +LogCometChat: FCM push token registered +``` +Background the app and have another user message you. + + + +--- + +## Reacting to a push in-game + +Bind these on the CometChat subsystem to react to notifications. Both are optional — push works without them. + +| Event | Fires when | +| ----- | ---------- | +| `OnPushNotificationReceived` | A push arrives while your game is running | +| `OnPushNotificationTapped` | The player taps a CometChat notification — including a tap that cold-starts the app | + +Both pass the push payload as a JSON `FString`. Read `conversationId`, `sender`, `receiver`, `receiverType`, `title`, `body` from it. + + + +From **Get CometChat Subsystem**, bind **On Push Notification Tapped** and parse the payload with **Deserialize JSON** to route the player to the right conversation. + + +```cpp +void UMyHUD::NativeConstruct() +{ + Super::NativeConstruct(); + if (auto* Sub = GetGameInstance()->GetSubsystem()) + { + Sub->OnPushNotificationTapped.AddDynamic(this, &UMyHUD::HandlePushTapped); + Sub->OnPushNotificationReceived.AddDynamic(this, &UMyHUD::HandlePushReceived); + } +} + +void UMyHUD::HandlePushTapped(const FString& PayloadJson) +{ + // Route to the conversation the notification referred to. + UE_LOG(LogTemp, Log, TEXT("Notification tapped: %s"), *PayloadJson); +} + +void UMyHUD::HandlePushReceived(const FString& PayloadJson) +{ + // App is running — e.g. refresh an unread badge. +} +``` + + + + +A tap can launch the app from cold. The plugin queues the tap until the subsystem exists and your Blueprint has bound, so you won't miss it if you bind during startup. + + +--- + +## Register a Push Token + + +**You normally don't need this.** With [Setup](#setup) done, the plugin registers automatically after login and re-registers on token rotation. Use this node only if you obtain tokens yourself — e.g. a custom Firebase layer, a third-party push plugin, or registering an extra **APNs VoIP** token for CallKit. + + +Registers the given device push token so this user receives notifications on this device. The token is stored against the **logged-in user**, so call it after login. + + + +Call the **Register Token Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Token | `FString` | The device push token from FCM (Android) or APNs (iOS) | +| Platform | `ECometChatPushPlatform` | `FCM`, `APNS`, or `APNS_VOIP` | +| Provider Id | `FString` | The push provider profile ID configured in the CometChat Dashboard | + +**On Success** returns a JSON `FString` result confirming registration. + + +```cpp +#include "AsyncActions/CometChatRegisterTokenAction.h" + +void AMyActor::RegisterAndroidPushToken(const FString& FcmToken) +{ + auto* Action = UCometChatRegisterTokenAction::RegisterTokenAsync( + this, + FcmToken, // Device token from Firebase Cloud Messaging + ECometChatPushPlatform::FCM, // Platform + TEXT("my-fcm-provider") // Provider Id from the dashboard + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnTokenRegistered); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnTokenRegistered(const FString& Result) +{ + UE_LOG(LogTemp, Log, TEXT("Push token registered: %s"), *Result); +} + +// Shared async failure handler used across this page. +void AMyActor::OnError(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("Push request failed: %s"), *Error.Message); +} +``` + + + +### ECometChatPushPlatform + +| Value | Description | +| ----- | ----------- | +| `FCM` | Firebase Cloud Messaging — Android device tokens | +| `APNS` | Apple Push Notification service — standard iOS device tokens | +| `APNS_VOIP` | Apple PushKit VoIP tokens — for high-priority incoming-call pushes on iOS (CallKit) | + + +`Provider Id` is the **push provider profile** you set up under **Notifications** in the [CometChat Dashboard](https://app.cometchat.com) (where you upload your FCM server key or APNs auth key). It selects which credentials CometChat uses to deliver the push — it is not the device token. + + + +On iOS, register the standard APNs token with `APNS` for chat messages, and register the separate PushKit token with `APNS_VOIP` if your game uses [Calls](/sdk/unreal/reference) so incoming voice/video invites can wake the app instantly. They are two different tokens for two different notification paths. + + +--- + +## Unregister a Push Token + +Stop notifications from being delivered to this device — call it at logout, or when the player signs out on a shared device so the next player doesn't receive their messages. + + + +Call the **Unregister Token Async** node. It takes no parameters and unregisters the current device's token for the logged-in user. + +**On Success** fires with **no output** — notifications to this device are now stopped. + + +```cpp +#include "AsyncActions/CometChatUnregisterTokenAction.h" + +void AMyActor::UnregisterPushToken() +{ + auto* Action = UCometChatUnregisterTokenAction::UnregisterTokenAsync(this); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnTokenUnregistered); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnTokenUnregistered() +{ + // Parameterless — the device is no longer registered for push. + UE_LOG(LogTemp, Log, TEXT("Push token unregistered")); +} +``` + + + + +Always unregister before logging a user out on a **shared or public device**. If you skip this, push notifications intended for the previous player keep arriving after the next player signs in. + + +--- + +## Notification Preferences + +Every user has a set of notification preferences that decide which events generate a push. Expose these in an in-game **Settings → Notifications** screen so players can, for example, silence noisy guild join/leave spam while still being pinged for direct messages. + +Preferences are represented by the `FCometChatNotificationPreferences` struct (detailed [below](#fcometchatnotificationpreferences)). Each event flag is an `int32` where **`1` = enabled** and **`0` = disabled**. + +### Fetch Preferences + +Load the user's current preferences — typically when the notifications settings screen opens — to populate the toggles. + + + +Call the **Fetch Preferences Async** node. It takes no parameters. + +**On Success** returns an `FCometChatNotificationPreferences` struct with the user's current settings. + + +```cpp +#include "AsyncActions/CometChatFetchPreferencesAction.h" + +void AMyActor::LoadNotificationSettings() +{ + auto* Action = UCometChatFetchPreferencesAction::FetchPreferencesAsync(this); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnPreferencesFetched); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnPreferencesFetched(const FCometChatNotificationPreferences& Prefs) +{ + UE_LOG(LogTemp, Log, TEXT("1:1 messages: %d, Group messages: %d, DND: %d"), + Prefs.OneOnOne.Messages, Prefs.Group.Messages, Prefs.Mute.DND); + // Populate the settings toggles from Prefs... +} +``` + + + +### Update Preferences + +Save the player's chosen settings. Build an `FCometChatNotificationPreferences` from your toggle states and submit it. The example below keeps direct messages on but silences the group member-churn events (joined/left) that spam large guilds, and enables Do Not Disturb. + + + +Call the **Update Preferences Async** node with a filled-in `FCometChatNotificationPreferences` struct. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Preferences | `FCometChatNotificationPreferences` | The full preferences object to persist | + +**On Success** returns the updated `FCometChatNotificationPreferences` as stored by the server. + + +```cpp +#include "AsyncActions/CometChatUpdatePreferencesAction.h" + +void AMyActor::SaveNotificationSettings() +{ + FCometChatNotificationPreferences Prefs; + + // Keep 1:1 messages and replies buzzing. + Prefs.OneOnOne.Messages = 1; + Prefs.OneOnOne.Replies = 1; + Prefs.OneOnOne.Reactions = 0; // Player doesn't care about reaction pings. + + // Keep guild messages, but silence the noisy join/leave churn. + Prefs.Group.Messages = 1; + Prefs.Group.MemberJoined = 0; + Prefs.Group.MemberLeft = 0; + + // Enable Do Not Disturb with a quiet-hours schedule (JSON string). + Prefs.Mute.DND = 1; + Prefs.Mute.ScheduleJson = + TEXT("{\"monday\":{\"from\":\"22:00\",\"to\":\"08:00\"}}"); + + auto* Action = UCometChatUpdatePreferencesAction::UpdatePreferencesAsync(this, Prefs); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnPreferencesUpdated); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnPreferencesUpdated(const FCometChatNotificationPreferences& Prefs) +{ + UE_LOG(LogTemp, Log, TEXT("Preferences saved. DND is now %d"), Prefs.Mute.DND); +} +``` + + + + +Fetch preferences first, mutate only the fields your UI exposes, then send the whole struct back on **Update**. That way you never accidentally reset a flag the player didn't touch to its default. + + +### Reset Preferences + +Restore the user's preferences to CometChat's defaults — wire this to a "Restore defaults" button. + + + +Call the **Reset Preferences Async** node. It takes no parameters. + +**On Success** returns the default `FCometChatNotificationPreferences`. + + +```cpp +#include "AsyncActions/CometChatResetPreferencesAction.h" + +void AMyActor::RestoreDefaultNotificationSettings() +{ + auto* Action = UCometChatResetPreferencesAction::ResetPreferencesAsync(this); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnPreferencesReset); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnPreferencesReset(const FCometChatNotificationPreferences& Prefs) +{ + UE_LOG(LogTemp, Log, TEXT("Preferences reset to defaults")); + // Re-populate the settings toggles from the returned defaults. +} +``` + + + +--- + +## Preference Structs + +### FCometChatNotificationPreferences + +The top-level preferences object returned by fetch/update/reset and passed into update. + +| Property | Type | Default | Description | +| -------- | ---- | ------- | ----------- | +| `bUsePrivacyTemplate` | `bool` | `false` | When `true`, push payloads hide message content (show a generic "New message" instead of the text) | +| `OneOnOne` | `FCometChatOneOnOnePreferences` | — | Notification flags for 1:1 conversations | +| `Group` | `FCometChatGroupPreferences` | — | Notification flags for group conversations | +| `Mute` | `FCometChatMutePreferences` | — | Do Not Disturb and scheduled quiet hours | + +### FCometChatOneOnOnePreferences + +Flags for direct (1:1) conversations. Each is an `int32`: **`1` = enabled**, **`0` = disabled**. + +| Property | Type | Default | Description | +| -------- | ---- | ------- | ----------- | +| `Messages` | `int32` | `1` | Push for new direct messages | +| `Replies` | `int32` | `1` | Push for replies in a thread | +| `Reactions` | `int32` | `1` | Push when someone reacts to your message | + +### FCometChatGroupPreferences + +Flags for group conversations. Each is an `int32`: **`1` = enabled**, **`0` = disabled**. + +| Property | Type | Default | Description | +| -------- | ---- | ------- | ----------- | +| `Messages` | `int32` | `1` | Push for new group messages | +| `Replies` | `int32` | `1` | Push for replies in a group thread | +| `Reactions` | `int32` | `1` | Push when someone reacts in the group | +| `MemberLeft` | `int32` | `1` | Push when a member leaves the group | +| `MemberAdded` | `int32` | `1` | Push when a member is added to the group | +| `MemberJoined` | `int32` | `1` | Push when a member joins the group | +| `MemberKicked` | `int32` | `1` | Push when a member is kicked | +| `MemberBanned` | `int32` | `1` | Push when a member is banned | +| `MemberUnbanned` | `int32` | `1` | Push when a member is unbanned | +| `MemberScopeChanged` | `int32` | `1` | Push when a member's role/scope changes | + +### FCometChatMutePreferences + +Do Not Disturb and scheduled quiet hours. + +| Property | Type | Default | Description | +| -------- | ---- | ------- | ----------- | +| `DND` | `int32` | `0` | Master Do Not Disturb switch: `1` silences all push, `0` respects the per-event flags above | +| `ScheduleJson` | `FString` | `""` | A JSON string describing scheduled quiet hours (e.g. per-day `from`/`to` windows) | + + +`DND` is a global override. When `DND = 1`, no push is delivered regardless of the individual `OneOnOne` and `Group` flags. Use `ScheduleJson` to restrict the quiet period to specific hours or days rather than muting around the clock. + + + +`ScheduleJson` is stored and applied as an opaque JSON string — validate its shape against your CometChat Dashboard notification configuration before sending. Malformed schedule JSON may be ignored server-side, leaving the player still receiving push during their intended quiet hours. + + +--- + +## Troubleshooting + +Read the device log (`adb logcat` on Android, Xcode console on iOS) — the plugin logs each stage under `CometChatFCM` and `LogCometChat`. + +| Symptom | Cause | Fix | +| ------- | ----- | --- | +| `Push not registered: no provider id` | `PushProviderId` unset | Set it in `[CometChat]`, or pass it to `Register Token Async` | +| `Firebase not configured — push inactive` | `Fcm*` keys missing | Add them from your `google-services.json` (see [Setup](#setup)) | +| `register failed: 401 ... "Forbidden resource"` | Notifications add-on not enabled on the app | Enable it in the dashboard — no client change helps | +| `register failed: ... ERR_INVALID_PROVIDERID` | `PushProviderId` doesn't exist on this app | Use the ID shown in the dashboard for **this** App ID | +| Registers fine, no notification appears | Notification permission denied | Android 13+ requires `POST_NOTIFICATIONS`; the plugin prompts, but check it wasn't declined | +| No `FCM token ready` line at all | Push not enabled, or package mismatch | Check `bEnablePushNotifications=True`; confirm your Package Name matches a Firebase client | +| Works on Android, silent on iOS | Remote notifications not enabled | Project Settings → iOS → **Enable Remote Notifications Support**, and confirm the Push capability on your provisioning profile | + + +A successful register call is **not** proof that delivery works — it only means CometChat stored the token. Delivery also needs a correctly configured provider and a real device token. Always verify by backgrounding the app and having another user message you. + + + +**Why the token must be real:** FCM/APNs only deliver to a token issued to an actual device. A fake or hard-coded token registers successfully and then silently never receives anything. + + +--- + +## Next Steps + + + + Log a user in before registering their device push token. + + + Handle messages in-app while the player is online, push while they're away. + + diff --git a/sdk/unreal/reactions.mdx b/sdk/unreal/reactions.mdx new file mode 100644 index 000000000..fa18f1027 --- /dev/null +++ b/sdk/unreal/reactions.mdx @@ -0,0 +1,384 @@ +--- +title: "Reactions" +description: "Add, remove, fetch, and render emoji reactions on messages in real time." +--- + +Reactions let users respond to a message with an emoji. The CometChat Unreal SDK provides async nodes to add and remove your own reactions, to fetch the full list of who reacted, and multicast delegates so every participant's UI updates the instant a reaction changes. + +There are two distinct pieces of data, and using the right one keeps your chat UI fast: + +- **Per-emoji totals** live directly on `FCometChatMessage.Reactions` — use these to draw the reaction *pills* (e.g. `👍 3`) with **no extra network call**. +- **Who reacted** is fetched on demand with **Fetch Reactions Async** — only needed when the user opens a "who reacted" list. + +### Reaction Flow + +```mermaid +flowchart TD + A["Add / Remove Reaction Async"] --> B["Server updates the message"] + B --> C["OnMessageReactionAdded / Removed
fires on every client"] + C --> D["Re-render pills from
Message.Reactions (counts)"] + D --> E{"User taps a pill?"} + E -->|"show who reacted"| F["Fetch Reactions Async
→ list of FCometChatReaction"] + + style A fill:#444,stroke:#888,color:#fff + style B fill:#333,stroke:#666,color:#fff + style C fill:#444,stroke:#888,color:#fff + style D fill:#333,stroke:#666,color:#fff + style E fill:#333,stroke:#666,color:#fff + style F fill:#444,stroke:#888,color:#fff +``` + +--- + +## Add a Reaction + +Add an emoji reaction to a message by its ID. + + + +**Use case:** the user taps an emoji in the reaction picker on a message, and that message's pill row updates immediately. + +Call the **Add Reaction Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Message Id | `int64` | The ID of the message to react to | +| Reaction | `FString` | The emoji to add (e.g. `😀`, `👍`, `❤️`) | + +**On Success** returns the updated `FCometChatMessage` — its `Reactions` array already reflects the new reaction, so you can re-render the pills straight from the result. + +**Blueprint flow** + +1. On the emoji picker's **On Clicked**, read the selected `Message Id` and the emoji string +2. Call **Add Reaction Async** +3. **On Success** (`FCometChatMessage`) → **For Each Loop** over `Reactions` → rebuild that message's pill row from the result +4. **On Failure** → a **Handle Reaction Error** custom event taking an `FCometChatError` → **Break** it, show `Message`, and roll back the optimistic pill if you drew one + + +```cpp +#include "AsyncActions/CometChatAddReactionAction.h" + +void AMyActor::ReactToMessage(int64 MessageId) +{ + auto* Action = UCometChatAddReactionAction::AddReactionAsync( + this, + MessageId, // ID of the message to react to + TEXT("👍") // Emoji to add + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnReactionAdded); + Action->OnFailure.AddDynamic(this, &AMyActor::OnReactionError); + Action->Activate(); +} + +void AMyActor::OnReactionAdded(const FCometChatMessage& Message) +{ + // Message.Reactions now includes the emoji you just added + UE_LOG(LogTemp, Log, TEXT("Reaction added — message %s now has %d distinct reactions"), + *Message.Id, Message.Reactions.Num()); +} + +void AMyActor::OnReactionError(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("Reaction failed: %s"), *Error.Message); +} +``` + + + +### Reference: sample UI + +The bundled sample panel reacts to a message with the **native C++ SDK** directly rather than the async node, updating its local pill row optimistically in the callback. In your own game code, prefer the **Add Reaction Async** node above — it marshals the result back to the Game Thread for you. + +```cpp +// From UCometChatPanel::AddReactionToMessage() — CometChatPanel.cpp +// Native C++ SDK call (chatsdk::), shown for reference. Prefer the Add Reaction Async node in game code. +void UCometChatPanel::AddReactionToMessage(const FString& Emoji) +{ + auto* Sub = GetSubsystem(); + if (!Sub || !Sub->GetSdk() || SelectedMessageId.IsEmpty()) return; + + const int64 MsgId = FCString::Atoi64(*SelectedMessageId); + Sub->GetSdk()->add_reaction( + static_cast(MsgId), + TCHAR_TO_UTF8(*Emoji), + [this, Emoji](const std::string& err, const chatsdk::Message& result) + { + // Native callback runs off the game thread — hop back before touching UMG + AsyncTask(ENamedThreads::GameThread, [this, err, Emoji]() + { + if (err.empty()) + RebuildChatFromData(); // pill row updated locally, then redrawn + else + ShowSnackbar(TEXT("Failed to react")); + }); + }); +} +``` + +--- + +## Remove a Reaction + +Remove one of your own reactions from a message. + + + +**Use case:** the user taps a pill they already reacted with to toggle their own reaction back off. + +Call the **Remove Reaction Async** node with the same `Message Id` and `Reaction` you added. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Message Id | `int64` | The ID of the message to remove the reaction from | +| Reaction | `FString` | The emoji to remove | + +**On Success** returns the updated `FCometChatMessage` with the reaction taken out of its `Reactions` array. + +**Blueprint flow** + +1. On a pill's **On Clicked**, check `bReactedByMe` — if `true`, treat the tap as a toggle-off +2. Call **Remove Reaction Async** with the same `Message Id` and emoji +3. **On Success** (`FCometChatMessage`) → rebuild the pill row from the result (the pill disappears when its `Count` reaches `0`) +4. **On Failure** → a **Handle Reaction Error** custom event taking an `FCometChatError` → **Break** it and show `Message` + + +```cpp +#include "AsyncActions/CometChatRemoveReactionAction.h" + +void AMyActor::UnreactToMessage(int64 MessageId) +{ + auto* Action = UCometChatRemoveReactionAction::RemoveReactionAsync( + this, + MessageId, + TEXT("👍") + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnReactionRemoved); + Action->OnFailure.AddDynamic(this, &AMyActor::OnReactionError); + Action->Activate(); +} + +void AMyActor::OnReactionRemoved(const FCometChatMessage& Message) +{ + UE_LOG(LogTemp, Log, TEXT("Reaction removed from message %s"), *Message.Id); +} +``` + + + +--- + +## Rendering Reaction Pills + +Every `FCometChatMessage` carries a `Reactions` array of **per-emoji totals**. This is populated when you fetch message history and refreshed on every real-time reaction event, so you can draw the reaction pills without ever calling Fetch Reactions. + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Reaction` | `FString` | The emoji | +| `Count` | `int32` | How many users reacted with this emoji | +| `bReactedByMe` | `bool` | `true` if the logged-in user is one of them (highlight the pill) | + +```cpp +// Draw one pill per distinct emoji, straight from the message — no fetch needed +for (const FCometChatReactionCount& Pill : Message.Reactions) +{ + const bool bMine = Pill.bReactedByMe; // highlight pills the local user tapped + AddReactionPillWidget(Pill.Reaction, Pill.Count, bMine); +} +``` + + +`FCometChatMessage.Reactions` gives you the emoji and its total count — everything needed to render the pill row. Only call **Fetch Reactions Async** (below) when the user wants to see the *list of people* behind a pill. + + + +**Rendering emoji in UMG.** Unreal's default font contains no emoji glyphs, so reaction text renders as blank boxes (`▯`) unless you assign an emoji-capable font — such as **Noto Color Emoji** — to the `UTextBlock`. Two gotchas that will otherwise cost you an afternoon: + +- **Keep the emoji glyph's color pure white.** Slate multiplies a color-emoji glyph by the text tint, so any non-white tint darkens the whole glyph — a dark tint makes it effectively invisible. +- **Render the emoji and its count as separate text blocks.** Most emoji fonts have no digit glyphs, so a combined `"😀 3"` in a single block drops the digits entirely. Put the emoji in an emoji-font block and the count in your normal-font block, side by side. + + +--- + +## Fetch Reactions + +Retrieve the full list of **who reacted** to a message, optionally filtered to a single emoji. Use this to populate a "reacted by" sheet when the user taps a pill. + + + +Call the **Fetch Reactions Async** node with an `FCometChatReactionsRequest` struct. Set `MessageId` to the target message, and optionally set `Reaction` to list only the users who reacted with that specific emoji. + +**On Success** returns a `TArray` — one entry per user, in reverse-chronological order. + + +```cpp +#include "AsyncActions/CometChatFetchReactionsAction.h" + +void AMyActor::FetchWhoReacted(int64 MessageId) +{ + FCometChatReactionsRequest Request; + Request.MessageId = MessageId; + Request.Limit = 25; + // Request.Reaction = TEXT("👍"); // optional: only users who used this emoji + + auto* Action = UCometChatFetchReactionsAction::FetchReactionsAsync(this, Request); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnReactionsFetched); + Action->OnFailure.AddDynamic(this, &AMyActor::OnReactionError); + Action->Activate(); +} + +void AMyActor::OnReactionsFetched(const TArray& Reactions) +{ + for (const FCometChatReaction& R : Reactions) + { + UE_LOG(LogTemp, Log, TEXT("%s reacted with %s"), *R.ReactedBy.Name, *R.Reaction); + } +} +``` + + + +### FCometChatReactionsRequest + +| Property | Type | Default | Description | +| -------- | ---- | ------- | ----------- | +| `Limit` | `int32` | `10` | Max reactions to return per page | +| `MessageId` | `int64` | `-1` | ID of the message to fetch reactions for | +| `Reaction` | `FString` | — | Optional: only fetch users who used this specific emoji | + +### FCometChatReaction + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Reaction` | `FString` | The emoji that was used | +| `ReactedBy` | `FCometChatUser` | The full profile of the user who reacted | +| `ReactedAt` | `int64` | Unix timestamp when the reaction was added | + +--- + +## Real-Time Reaction Events + +Bind to the subsystem's reaction delegates to keep every open message in sync when anyone — including other users — adds or removes a reaction. Both fire on the **Game Thread**, so you can update UMG widgets directly. + +| Delegate | Payload | Fires When | +| -------- | ------- | ---------- | +| `OnMessageReactionAdded` | `FCometChatReactionEvent` | A reaction is added to a message | +| `OnMessageReactionRemoved` | `FCometChatReactionEvent` | A reaction is removed from a message | + + + +**Use case:** when anyone — including other players — reacts, update that message's pills live without re-fetching the thread. + +1. Get a reference to the **CometChat Subsystem** +2. Drag off and search for **On Message Reaction Added** (and **On Message Reaction Removed**) +3. Use **Bind Event** to connect each to a custom event +4. The custom event receives an `FCometChatReactionEvent`. Its `Message` field is the full updated message — re-render that row's pills from `Message.Reactions`. + + +```cpp +void AMyActor::BeginPlay() +{ + Super::BeginPlay(); + + UCometChatSubsystem* Chat = GetGameInstance()->GetSubsystem(); + Chat->OnMessageReactionAdded.AddDynamic(this, &AMyActor::HandleReactionAdded); + Chat->OnMessageReactionRemoved.AddDynamic(this, &AMyActor::HandleReactionRemoved); +} + +void AMyActor::HandleReactionAdded(const FCometChatReactionEvent& Event) +{ + // Event.Message carries the full updated message, including its Reactions array + UE_LOG(LogTemp, Log, TEXT("%s reacted %s to message %s"), + *Event.ReactedBy.Name, *Event.Reaction, *Event.MessageId); + + RefreshReactionPills(Event.Message); // redraw the pill row for this message +} + +void AMyActor::HandleReactionRemoved(const FCometChatReactionEvent& Event) +{ + UE_LOG(LogTemp, Log, TEXT("%s removed %s from message %s"), + *Event.ReactedBy.Name, *Event.Reaction, *Event.MessageId); + + RefreshReactionPills(Event.Message); +} +``` + + + +### FCometChatReactionEvent + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Reaction` | `FString` | The emoji that was added or removed | +| `MessageId` | `FString` | ID of the affected message | +| `ReceiverId` | `FString` | UID (1:1) or GUID (group) the message belongs to | +| `ReceiverType` | `FString` | `user` or `group` | +| `ConversationId` | `FString` | Conversation the message belongs to | +| `ParentMessageId` | `int64` | Parent message ID if this is a threaded reply, `0` otherwise | +| `ReactedBy` | `FCometChatUser` | The user who added or removed the reaction | +| `ReactedAt` | `int64` | Unix timestamp of the change | +| `Message` | `FCometChatMessage` | The full updated message, including its refreshed `Reactions` array | + + +**Bind before you log in.** Reaction events that arrive between login completing and your delegates being bound will be missed. Bind `OnMessageReactionAdded` / `OnMessageReactionRemoved` in `BeginPlay` (or before calling Login) so no updates are dropped. + + +--- + +## Best Practices + + +**Render pills from the message you already have.** `FCometChatMessage.Reactions` holds every emoji with its `Count` and `bReactedByMe`, and it's refreshed on every real-time reaction event — so draw the whole pill row straight from it with **no extra fetch**. Two rendering rules keep the glyphs readable: keep the **emoji glyph pure white** (Slate multiplies a color-emoji glyph by the text tint, so any non-white tint darkens it), and put the **count in a separate normal-font text block** beside the emoji (most emoji fonts ship no digit glyphs). + + + +**Don't call Fetch Reactions just to show counts.** **Fetch Reactions Async** exists to list the *people* behind a pill — calling it for every message to derive counts adds a network round trip per message and can rate-limit you. The counts are already in `Message.Reactions`; only fetch when the user opens a "reacted by" sheet. + + +--- + +## Handling Failures + +Both **Add Reaction Async** and **Remove Reaction Async** expose an **On Failure** pin (and a bindable `OnFailure` in C++) that delivers an `FCometChatError`. Wire it so a failed reaction rolls back the optimistic pill instead of leaving the UI out of sync with the server. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `Code` | `FString` | Machine-readable error code — branch on this | +| `Message` | `FString` | Human-readable summary — safe to show in a toast | +| `Details` | `FString` | Raw server payload for logging and triage | + + + +1. Drag off **Add Reaction Async** / **Remove Reaction Async**'s **On Failure** pin +2. Route it to a **Handle Reaction Error** custom event that takes an `FCometChatError` +3. **Break** the struct to read `Code`, `Message`, and `Details` +4. Roll back the optimistic pill you drew, show `Message` in a toast, and log `Details` + + +```cpp +void AMyActor::OnReactionError(const FCometChatError& Error) +{ + // Code is machine-readable, Message is user-facing, Details is the raw payload + UE_LOG(LogTemp, Error, TEXT("Reaction failed [%s]: %s"), + *Error.Code, *Error.Message); + UE_LOG(LogTemp, Verbose, TEXT("Details: %s"), *Error.Details); + + // Undo the optimistic pill so the row matches the server, then tell the user + RollbackOptimisticReaction(); + ShowReactionToast(Error.Message); +} +``` + + + +--- + +## Next Steps + + + + Fetch message history — each message arrives with its `Reactions` populated. + + + All the subsystem delegates, including reactions, in one place. + + diff --git a/sdk/unreal/real-time-events.mdx b/sdk/unreal/real-time-events.mdx index f0af0c37e..8fa35abcb 100644 --- a/sdk/unreal/real-time-events.mdx +++ b/sdk/unreal/real-time-events.mdx @@ -62,6 +62,73 @@ void AMyActor::BeginPlay() --- +## Use Case: Centralize Binding & Teardown + +**Goal:** bind every listener your game needs in **one** setup function that runs before Login, and mirror it with a teardown that unbinds on shutdown. One binding site keeps event wiring easy to reason about and prevents duplicate handlers and leaks. + + + +1. On your chat actor/widget **Begin Play** (before you call **Login**), get the **CometChat Subsystem** +2. **Bind Event** to each delegate you need — messages, typing, receipts, presence, group, connection — wiring each to a custom event +3. Call **Login** — events now flow into your handlers +4. On **End Play** / widget **Destruct**, **Unbind** from the same delegates + + +```cpp +void AMyActor::BeginPlay() +{ + Super::BeginPlay(); + SubscribeToEvents(); // bind everything BEFORE Login + // ... then call Login +} + +void AMyActor::EndPlay(const EEndPlayReason::Type Reason) +{ + UnsubscribeFromEvents(); + Super::EndPlay(Reason); +} + +void AMyActor::SubscribeToEvents() +{ + UCometChatSubsystem* Chat = GetGameInstance()->GetSubsystem(); + if (!Chat) return; + Chat->OnTextMessageReceived.AddDynamic(this, &AMyActor::HandleTextMessage); + Chat->OnMessagesRead.AddDynamic(this, &AMyActor::HandleRead); + Chat->OnTypingStarted.AddDynamic(this, &AMyActor::HandleTypingStarted); + Chat->OnUserOnline.AddDynamic(this, &AMyActor::HandleUserOnline); + Chat->OnConnected.AddDynamic(this, &AMyActor::HandleConnected); + Chat->OnDisconnected.AddDynamic(this, &AMyActor::HandleDisconnected); +} + +void AMyActor::UnsubscribeFromEvents() +{ + UCometChatSubsystem* Chat = GetGameInstance()->GetSubsystem(); + if (!Chat) return; + Chat->OnTextMessageReceived.RemoveDynamic(this, &AMyActor::HandleTextMessage); + Chat->OnMessagesRead.RemoveDynamic(this, &AMyActor::HandleRead); + Chat->OnTypingStarted.RemoveDynamic(this, &AMyActor::HandleTypingStarted); + Chat->OnUserOnline.RemoveDynamic(this, &AMyActor::HandleUserOnline); + Chat->OnConnected.RemoveDynamic(this, &AMyActor::HandleConnected); + Chat->OnDisconnected.RemoveDynamic(this, &AMyActor::HandleDisconnected); +} +``` + + + + +These delegates only fire while the socket is connected — a drop pauses **every** listener until it reconnects. Bind `OnDisconnected` / `OnConnected` here too, show a reconnection banner, and refresh any state (unread counts, receipts, presence) after reconnect, since events during the gap are missed. + + + +**Best practice:** bind **all** listeners once, before Login, from a single Subscribe function — and unbind from the matching teardown. One binding site makes it easy to see what's wired and guarantees you're listening before the first event arrives. + + + +**Don't re-bind delegates every frame** (from Tick or on every widget rebuild) and **don't forget to unbind on teardown**. `AddDynamic` without a matching `RemoveDynamic` stacks duplicate handlers — the same event fires N times — and leaks bindings to destroyed objects. Bind once, unbind once. + + +--- + ## MessageListener Events These fire when messages, typing indicators, receipts, or reactions arrive. @@ -290,14 +357,14 @@ Many delegates and failure callbacks provide an `FCometChatError` struct with de -All async nodes have an **On Failure** pin. Wire it to a custom event that receives an `FString` error message. Connection and login listener events provide the full `FCometChatError` struct. +All async nodes have an **On Failure** pin. Wire it to a custom event that receives an `FCometChatError` struct (with `Code`, `Message`, and `Details`). ```cpp // Async node failure handler (simple string) -void AMyActor::HandleError(const FString& Error) +void AMyActor::HandleError(const FCometChatError& Error) { - UE_LOG(LogTemp, Error, TEXT("Operation failed: %s"), *Error); + UE_LOG(LogTemp, Error, TEXT("Operation failed: %s"), *Error.Message); // Show error in UI, retry, or fallback } @@ -386,7 +453,7 @@ For full details on manual connection management, see [Advanced Configuration](/ Send and receive typing state. - + Drop-in chat panel and button widgets.
diff --git a/sdk/unreal/receive-messages.mdx b/sdk/unreal/receive-messages.mdx index b339bbc0b..89e78028e 100644 --- a/sdk/unreal/receive-messages.mdx +++ b/sdk/unreal/receive-messages.mdx @@ -53,6 +53,15 @@ Call the **Get Messages Async** node. | Limit | `int32` | Maximum number of messages to return (default: 50) | **On Success** returns a `TArray` — an array of messages sorted by timestamp. + +**Blueprint flow — opening a 1:1 chat** + +1. `On Chat Opened` → `Get CometChat Subsystem` +2. `Get Messages Async` (Uid, Limit = 30) +3. **On Success** → populate the message scroll box, then bind `On Message Received` for live updates +4. **On Failure** → `Break FCometChatError` → show a "couldn't load history" state with a **Retry** button that re-runs step 2 + +Wire the node's green **On Success** pin and red **On Failure** pin to separate handlers. ```cpp @@ -78,9 +87,9 @@ void AMyActor::OnMessagesFetched(const TArray& Messages) } } -void AMyActor::OnFetchFailed(const FString& Error) +void AMyActor::OnFetchFailed(const FCometChatError& Error) { - UE_LOG(LogTemp, Error, TEXT("Fetch failed: %s"), *Error); + UE_LOG(LogTemp, Error, TEXT("Fetch failed: %s"), *Error.Message); } ``` @@ -109,6 +118,15 @@ Call the **Get Group Messages Async** node. **On Success** returns two outputs: - `TArray` — the messages for this page - `FCometChatPagination` — pagination metadata including `HasMore` and `NextCursor` + +**Blueprint flow — opening a group chat** + +1. `On Chat Opened` → `Get CometChat Subsystem` +2. `Get Group Messages Async` (Guid, Limit = 30, Before Message Id = 0) +3. **On Success** → populate the scroll box in order and scroll to the bottom, then bind `On Message Received` for live updates (or keep one app-wide binding from before Login — see [Best Practices](#best-practices)) +4. **On Failure** → `Break FCometChatError` → show a "couldn't load history" state with a **Retry** button that calls step 2 again + +Connect the node's two execution pins to separate handlers — the green **On Success** pin to your populate-list logic, the red **On Failure** pin to a `Break FCometChatError` node feeding your error UI. ```cpp @@ -162,6 +180,356 @@ The `FCometChatPagination` struct tells you where you are in the message history **Infinite scroll pattern**: Start with `BeforeMessageId = 0`, then keep passing `NextCursor` from each response until `HasMore` is `false`. +### Infinite Scroll + +Load older messages as the user scrolls up. Two equivalent approaches: + +**Cursor-based (Get Group Messages Async + `NextCursor`)** + +1. Keep the latest `FCometChatPagination`; start with `Before Message Id = 0` +2. `On Scrolled To Top` → check `HasMore`. If `false`, stop — you've reached the start +3. `Get Group Messages Async` (Guid, Limit = 30, Before Message Id = `NextCursor`) +4. **On Success** → prepend the older page above the current messages and store the new `NextCursor` +5. **On Failure** → `Break FCometChatError` → show an inline "tap to retry" row at the top + +**Request-builder (`FCometChatMessagesRequest` + Fetch Previous)** + +1. Build an `FCometChatMessagesRequest` (set `GUID` or `UID`, plus `Limit`) — see [Advanced Fetch](#advanced-fetch-request-builder) +2. `On Scrolled To Top` → set `MessageId` to the oldest message you've loaded so far, then `Fetch Previous Messages Async` (Request) +3. **On Success** → prepend the returned older messages; an empty array means there's no more history +4. **On Failure** → `Break FCometChatError` → retry the same request + + +Cursor paging is the simplest for a single conversation. Reach for the request builder when you also need filters — threads, search, attachments-only — while scrolling. + + +--- + +## Advanced Fetch (Request Builder) + +When you need full control over what you pull — threaded replies, keyword search, unread-only, attachments-only, category/type filters — use the request-builder nodes. Both share a single `FCometChatMessagesRequest`: set `UID` for a 1:1 conversation or `GUID` for a group, tune the filters, then page in either direction. + +### Fetch Previous Messages + +Pulls **older** messages (history) relative to the request's anchor. + + + +Call the **Fetch Previous Messages Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Request | `FCometChatMessagesRequest` | The configured request builder (see fields below) | + +**On Success** returns a `TArray` of older messages. + + +```cpp +#include "AsyncActions/CometChatFetchMessagesAction.h" + +void AMyActor::LoadHistory() +{ + FCometChatMessagesRequest Request; + Request.UID = TEXT("cometchat-uid-2"); // 1:1 thread (use GUID for a group) + Request.Limit = 30; + + auto* Action = UCometChatFetchPreviousMessagesAction::FetchPreviousMessagesAsync(this, Request); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnHistoryLoaded); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnHistoryLoaded(const TArray& Messages) +{ + for (const FCometChatMessage& Msg : Messages) + { + UE_LOG(LogTemp, Log, TEXT("[%s]: %s"), *Msg.SenderName, *Msg.Text); + } +} + +void AMyActor::OnError(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("Fetch failed: %s"), *Error.Message); +} +``` + + + +### Fetch Next Messages + +Pulls **newer** messages relative to the request's anchor — handy for catching up after a gap, or loading forward through a thread. + + + +Call the **Fetch Next Messages Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Request | `FCometChatMessagesRequest` | The configured request builder (see fields below) | + +**On Success** returns a `TArray` of newer messages. + + +```cpp +#include "AsyncActions/CometChatFetchMessagesAction.h" + +void AMyActor::LoadThreadReplies(int64 ParentId) +{ + FCometChatMessagesRequest Request; + Request.GUID = TEXT("group-abc-123"); // group thread (use UID for 1:1) + Request.ParentMessageId = ParentId; // fetch replies in a thread + Request.Limit = 50; + + auto* Action = UCometChatFetchNextMessagesAction::FetchNextMessagesAsync(this, Request); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnNewerMessages); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnNewerMessages(const TArray& Messages) +{ + // Messages newer than the request's MessageId / Timestamp anchor + UE_LOG(LogTemp, Log, TEXT("Fetched %d newer message(s)"), Messages.Num()); +} +``` + + + +### FCometChatMessagesRequest + +The most useful fields for building a request: + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Limit` | `int32` | Max messages per page (default `30`) | +| `UID` | `FString` | The other user's UID — set for a 1:1 conversation or thread | +| `GUID` | `FString` | The group's GUID — set for a group conversation or thread | +| `MessageId` | `int64` | Anchor message ID to page around (default `-1`) | +| `Timestamp` | `int64` | Anchor Unix timestamp to page around (default `-1`) | +| `bUnread` | `bool` | Only return unread messages | +| `bHideMessagesFromBlockedUsers` | `bool` | Exclude messages from blocked users | +| `SearchKeyword` | `FString` | Full-text search within message bodies | +| `Categories` | `TArray` | Filter by category, e.g. `message`, `custom` | +| `Types` | `TArray` | Filter by type, e.g. `text`, `image` | +| `ParentMessageId` | `int64` | Parent message ID for fetching thread replies (default `-1`) | +| `bHideReplies` | `bool` | Exclude threaded replies | +| `bHideDeleted` | `bool` | Exclude deleted (tombstoned) messages | +| `bHasAttachments` | `bool` | Only messages that carry an attachment | +| `bHasMentions` | `bool` | Only messages containing a mention | +| `bHasReactions` | `bool` | Only messages that have reactions | +| `MentionedUIDs` | `TArray` | Only messages mentioning these UIDs | +| `Tags` | `TArray` | Filter by message tags | + + +Set **`UID`** for a 1:1 thread **or** **`GUID`** for a group thread — provide the one that matches the conversation you're paging through. Combine with `ParentMessageId` to page through a specific thread's replies. + + +--- + +## Get a Single Message + +Fetch one message by its ID — useful for resolving a reply's parent, a quoted message, or a deep link. + + + +Call the **Get Message Details Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Message Id | `int64` | The ID of the message to fetch | + +**On Success** returns the full `FCometChatMessage`. + +**Blueprint flow — resolving a reply's parent** + +1. `On Reply Tapped` → `Get Message Details Async` (Message Id of the parent) +2. **On Success** → show the quoted parent above the composer +3. **On Failure** → `Break FCometChatError` → fall back to an "original message unavailable" placeholder + + +```cpp +#include "AsyncActions/CometChatGetMessageDetailsAction.h" + +void AMyActor::LoadMessage(int64 MessageId) +{ + auto* Action = UCometChatGetMessageDetailsAction::GetMessageDetailsAsync(this, MessageId); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnMessageLoaded); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnMessageLoaded(const FCometChatMessage& Message) +{ + UE_LOG(LogTemp, Log, TEXT("[%s]: %s"), *Message.SenderName, *Message.Text); +} +``` + + + +--- + +## Message Receipts + +Fetch the delivery/read receipts for a specific message, and explicitly mark messages as read, delivered, or unread. + +### Get Receipts + + + +Call the **Get Receipts Async** node to list who has received or read a message. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Message Id | `int64` | The ID of the message to fetch receipts for | + +**On Success** returns a `TArray` — one entry per recipient/status. + +**Blueprint flow — a "Seen by" sheet** + +1. `On Bubble Long-Pressed` → `Get Receipts Async` (Message Id) +2. **On Success** → filter the array to `ReceiptType == read` and list each `Sender` +3. **On Failure** → `Break FCometChatError` → toast the `Message` + + +```cpp +#include "AsyncActions/CometChatGetReceiptsAction.h" + +void AMyActor::LoadReceipts(int64 MessageId) +{ + auto* Action = UCometChatGetReceiptsAction::GetReceiptsAsync(this, MessageId); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnReceipts); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnReceipts(const TArray& Receipts) +{ + for (const FCometChatMessageReceipt& R : Receipts) + { + UE_LOG(LogTemp, Log, TEXT("%s — %s (read at %lld)"), + *R.Sender.Name, *R.ReceiptType, R.ReadAt); + } +} +``` + + + +#### FCometChatMessageReceipt + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `MessageId` | `FString` | The ID of the message the receipt is for | +| `Sender` | `FCometChatUser` | The user the receipt is about | +| `ReceiverType` | `FString` | `user` or `group` | +| `ReceiverId` | `FString` | The UID or GUID the message was sent to | +| `Timestamp` | `int64` | When the receipt was generated | +| `ReceiptType` | `FString` | `delivered` or `read` | +| `DeliveredAt` | `int64` | Unix-ms when delivered (`0` if not yet) | +| `ReadAt` | `int64` | Unix-ms when read (`0` if not yet) | +| `MessageSender` | `FString` | UID of the original message's sender | + +### Mark As Read + + + +Call the **Mark As Read Async** node, passing a message you received. This sends a read receipt to its sender. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Message | `FCometChatMessage` | The message to mark as read | + +**On Success** fires with **no payload**. + +Wire **On Failure** → `Break FCometChatError` here too: if marking read fails (say the socket dropped), keep the message flagged unread locally and retry on reconnect rather than showing it as read. + + +```cpp +#include "AsyncActions/CometChatMarkAsReadAction.h" + +void AMyActor::MarkRead(const FCometChatMessage& Message) +{ + auto* Action = UCometChatMarkAsReadAction::MarkAsReadAsync(this, Message); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnMarkedRead); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnMarkedRead() +{ + // No payload — the read receipt has been sent to the message's sender +} +``` + + + +### Mark As Delivered + + + +Call the **Mark As Delivered Async** node, passing a message you received, to send a delivery receipt. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Message | `FCometChatMessage` | The message to mark as delivered | + +**On Success** fires with **no payload**. + + +```cpp +#include "AsyncActions/CometChatMarkAsDeliveredAction.h" + +void AMyActor::MarkDelivered(const FCometChatMessage& Message) +{ + auto* Action = UCometChatMarkAsDeliveredAction::MarkAsDeliveredAsync(this, Message); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnMarkedDelivered); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnMarkedDelivered() +{ + // No payload — the delivery receipt has been sent +} +``` + + + +### Mark As Unread + + + +Call the **Mark As Unread Async** node, passing a message, to flag its conversation as unread again. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Message | `FCometChatMessage` | The message from which to mark the conversation unread | + +**On Success** returns the affected `FCometChatConversation` with its refreshed `UnreadMessageCount`. + + +```cpp +#include "AsyncActions/CometChatMarkAsUnreadAction.h" + +void AMyActor::MarkUnread(const FCometChatMessage& Message) +{ + auto* Action = UCometChatMarkAsUnreadAction::MarkAsUnreadAsync(this, Message); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnMarkedUnread); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnMarkedUnread(const FCometChatConversation& Conversation) +{ + UE_LOG(LogTemp, Log, TEXT("Unread again: %d"), Conversation.UnreadMessageCount); +} +``` + + + + +These nodes **send** receipts and set status on demand. For the **real-time** receipt delegates (`OnMessagesDelivered`, `OnMessagesRead`) that fire automatically as the other user's client acknowledges your messages, see [Delivery & Read Receipts](/sdk/unreal/delivery-read-receipts). + + --- ## Real-Time: Incoming Messages @@ -170,12 +538,18 @@ To receive messages as they arrive (without polling), bind to the `OnMessageRece +**Use case**: keep an open chat live without polling. + 1. Get a reference to the **CometChat Subsystem** 2. Drag off and search for **On Message Received** -3. Use **Bind Event** to connect it to a custom event -4. The custom event receives an `FCometChatMessage` parameter - -Bind this **before** calling Login so you don't miss any messages. +3. Use **Bind Event** to connect it to a custom event — do this **before** calling Login so you don't miss anything +4. The custom event receives an `FCometChatMessage`. In the handler: + - **Skip your own echo** — ignore the message if `Sender Uid` is the logged-in user + - **De-duplicate** — ignore it if you've already shown that message `Id` (a live message can overlap fetched history) + - **Route** — only append if it belongs to the open chat (`Receiver Type` + `Conversation Id`) + - **Append** the row and scroll to the bottom + +`On Message Received` has no failure pin — it's a push delegate. Handle connection drops with the connection-status delegates instead. ```cpp @@ -211,6 +585,149 @@ void AMyActor::HandleNewMessage(const FCometChatMessage& Message) The `OnMessageReceived` delegate fires for **all** conversations — both 1:1 and group. Use `ReceiverType` to distinguish between them, and `ConversationId` to route messages to the right chat window. +### Merging History with Live Messages + +```mermaid +flowchart LR + H["Fetched history"] --> M["Merge & sort by SentAt"] + R["OnMessageReceived fires"] --> D{"Id already shown?"} + D -->|"Yes"| X["Skip (duplicate)"] + D -->|"No"| B{"Belongs to open chat?"} + B -->|"No"| X + B -->|"Yes"| M + M --> UI["Render in scroll box"] + + style H fill:#333,stroke:#666,color:#fff + style R fill:#333,stroke:#666,color:#fff + style M fill:#444,stroke:#888,color:#fff + style D fill:#555,stroke:#999,color:#ccc + style B fill:#555,stroke:#999,color:#ccc + style X fill:#3a2a2a,stroke:#844,color:#fcc + style UI fill:#444,stroke:#888,color:#fff +``` + +### Reference: sample UI + +The plugin ships a ready-made `UCometChatGroupChatBox` widget. These two trimmed excerpts show the same paths this page covers — history load and live append. + + +Both excerpts call the **native C++ SDK** (`Sub->GetSdk()->...`) directly, which delivers results on a **background thread**. In your own code, prefer the async nodes above plus the `OnMessageReceived` / `OnTextMessageReceived` delegates — they're Blueprint-friendly and already marshal to the game thread for you. + + +```cpp +// (a) History load — CometChatGroupChatBox.cpp, UCometChatGroupChatBox::DoLoadHistory() +// Native API: the callback runs off the game thread, so hop back before touching UI. +Sub->GetSdk()->get_group_messages(TCHAR_TO_UTF8(*GroupGuid), MessageLimit, /*before_id*/ 0, + [this](const std::string& err, const chatsdk::PaginatedMessages& result) + { + AsyncTask(ENamedThreads::GameThread, [this, err, result]() + { + if (err.empty()) + { + for (const auto& m : result.messages) + AddMessageToList(CometChatConversions::ToUnreal(m)); // append in order + ScrollToBottom(); + } + // else: SetState(EChatState::Error, ...) to show a retry state + }); + }); +``` + +```cpp +// (b) Real-time append — CometChatGroupChatBox.cpp, UCometChatGroupChatBox::HandleTextMessageReceived() +// Bound to UCometChatSubsystem::OnTextMessageReceived, which fires on the game thread. +void UCometChatGroupChatBox::HandleTextMessageReceived(const FCometChatMessage& Message) +{ + if (Message.SenderUid == CurrentUserUid) return; // skip your own echo + if (MessageIds.Contains(Message.Id)) return; // de-dup vs. fetched history + + // Route: only append if this message belongs to the open group + if (Message.ReceiverType == TEXT("group") && Message.ConversationId.Contains(GroupGuid)) + { + AddMessageToList(Message); // records Message.Id in MessageIds, then adds the row + ScrollToBottom(); + } +} +``` + + +`AddMessageToList()` is what records each `Message.Id`, so the `MessageIds.Contains(...)` guard above is what keeps a live message from being drawn twice when it overlaps the tail of your fetched history. + + +--- + +## Handling Failures + +Every async node exposes an **On Failure** pin (Blueprint) / `OnFailure` delegate (C++) that hands you an `FCometChatError`: + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `Code` | `FString` | Machine-readable code, e.g. `ERR_SDK`, `ERR_UNKNOWN`, or a server code | +| `Message` | `FString` | Human-readable description — safe to log | +| `Details` | `FString` | Extra context when the SDK provides it | + +For a one-off failure, log `Message` and show a retry affordance. For history loads it's worth retrying **transient** network errors automatically, with backoff: + +```cpp +// AMyActor.h: int32 PendingHistoryAttempt = 0; FString GroupGuid; +#include "AsyncActions/CometChatGetGroupMessagesAction.h" +#include "TimerManager.h" + +void AMyActor::LoadGroupHistory(int32 Attempt) +{ + PendingHistoryAttempt = Attempt; // remembered so the failure handler can back off + + auto* Action = UCometChatGetGroupMessagesAction::GetGroupMessagesAsync(this, GroupGuid, 30, 0); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnGroupMessagesFetched); + Action->OnFailure.AddDynamic(this, &AMyActor::OnHistoryFetchFailed); + Action->Activate(); +} + +void AMyActor::OnHistoryFetchFailed(const FCometChatError& Error) +{ + // Code is an FString surfaced from the SDK/server (e.g. "ERR_INTERNET_UNAVAILABLE"). + UE_LOG(LogTemp, Warning, TEXT("History fetch failed [%s]: %s"), *Error.Code, *Error.Message); + + const bool bTransient = + Error.Code == TEXT("ERR_INTERNET_UNAVAILABLE") || Error.Code.Contains(TEXT("NETWORK")); + + if (!bTransient || PendingHistoryAttempt >= 4) + { + ShowHistoryErrorState(); // show a "couldn't load history" panel with a Retry button + return; + } + + // Exponential backoff: 0.5s, 1s, 2s, 4s + const float Delay = 0.5f * FMath::Pow(2.0f, static_cast(PendingHistoryAttempt)); + FTimerHandle RetryHandle; + GetWorldTimerManager().SetTimer(RetryHandle, + [this]() { LoadGroupHistory(PendingHistoryAttempt + 1); }, Delay, false); +} +``` + + +Always cap retries (here, four attempts) and only retry **transient** errors. Retrying a permanent failure — bad GUID, not a member, revoked auth — just loops. When you give up, surface a manual **Retry** button instead. + + +--- + +## Best Practices + + +**Do** +- **Bind `On Message Received` before you call Login** so you never miss a message that arrives during startup. +- **De-duplicate by message `Id`.** A real-time message can overlap the tail of your fetched history — skip it if that `Id` is already on screen. +- **Route by `ConversationId` / `ReceiverType`.** The delegate fires for every conversation; only append when the message belongs to the chat that's open. +- **Page history with `NextCursor` + `HasMore`** instead of pulling everything at once. + + + +**Avoid** +- **Don't poll.** Re-fetching with `Get ... Messages Async` on a timer wastes requests and adds latency — use the real-time delegate for new messages and fetch only for history and backfill. +- **Don't sort by arrival.** Merge fetched history and live messages, then order by `SentAt` before rendering, so a delayed fetch can't shuffle the timeline. +- **Don't hand-marshal what's already safe.** The CometChat delegates fire on the game thread, so handlers bound to them can touch widgets directly. If *you* spin off async work (image decode, disk cache), that's when you must return to the game thread before updating UI. + + --- ## Next Steps diff --git a/sdk/unreal/reference.mdx b/sdk/unreal/reference.mdx index 6906f30d1..25d57fc3a 100644 --- a/sdk/unreal/reference.mdx +++ b/sdk/unreal/reference.mdx @@ -17,7 +17,11 @@ These are called directly on `UCometChatSubsystem`. | `ConfigureWithSettings` | Config | `AppId: FString`, `Settings: FCometChatAppSettings` | `void` | Initialize with advanced settings (custom hosts, subscription type, manual connection). | | `IsLoggedIn` | Auth | — | `bool` | Returns `true` if a user is currently authenticated. | | `GetLoggedInUser` | Auth | — | `FCometChatUser` | Returns the currently logged-in user's profile. | +| `GetUserAuthToken` | Auth | — | `FString` | Returns the current session's auth token. | | `GetConnectionStatus` | Connection | — | `ECometChatConnectionState` | Returns the current WebSocket connection state. | +| `GetActiveCall` | Calls | — | `FCometChatCall` | Returns the current active call (empty if none). | +| `HasActiveCall` | Calls | — | `bool` | Returns `true` if there is an active call. | +| `GetConversationUpdateSettings` | Conversations | — | `FCometChatConversationUpdateSettings` | Returns which message categories update a conversation's state. | | `StartTyping` | Typing | `Indicator: FCometChatTypingIndicator` | `void` | Notify that the local user started typing. | | `EndTyping` | Typing | `Indicator: FCometChatTypingIndicator` | `void` | Notify that the local user stopped typing. | | `SendTransientMessage` | Messaging | `Message: FCometChatTransientMessage` | `void` | Send an ephemeral message that is not persisted. | @@ -29,23 +33,61 @@ These are called directly on `UCometChatSubsystem`. Each node is a latent Blueprint action with **On Success** and **On Failure** output pins. In C++, create via the static factory method, bind delegates, and call `Activate()`. -### Authentication +### Configuration & Authentication | Node | Parameters | Success Output | Description | | ---- | ---------- | -------------- | ----------- | -| **Login Async** | `Uid: FString`, `AuthKey: FString` | — | Authenticate a user with Auth Key. | -| **Login With Auth Token Async** | `AuthToken: FString` | — | Authenticate a user with a pre-generated Auth Token. | +| **Configure Async** | `AppId: FString`, `Region: FString` | `FCometChatUser` | Initialize the SDK. On success returns the restored session user (empty `Uid` if none). | +| **Configure With Settings Async** | `AppId: FString`, `Settings: FCometChatAppSettings` | `FCometChatUser` | Initialize with advanced settings (custom hosts, subscription type, manual connection). | +| **Login Async** | `Uid: FString`, `AuthKey: FString` | `FCometChatUser` | Authenticate a user with an Auth Key. | +| **Login With Auth Token Async** | `AuthToken: FString` | `FCometChatUser` | Authenticate a user with a pre-generated Auth Token. | | **Logout Async** | — | — | Log out the current user and disconnect. | +### Connection + +| Node | Parameters | Success Output | Description | +| ---- | ---------- | -------------- | ----------- | +| **Connect Async** | — | — | Manually establish the WebSocket connection. | +| **Disconnect Async** | — | — | Manually close the WebSocket connection (stays logged in). | +| **Ping Async** | — | — | Ping the server to verify connectivity. | + ### Messaging | Node | Parameters | Success Output | Description | | ---- | ---------- | -------------- | ----------- | | **Send Message Async** | `ReceiverUid: FString`, `Text: FString` | `FCometChatMessage` | Send a 1:1 text message. | | **Send Group Message Async** | `Guid: FString`, `Text: FString` | `FCometChatMessage` | Send a text message to a group. | +| **Send Media Message Async** | `Message: FCometChatMessage` | `FCometChatMessage` | Send an image/video/audio/file message (build an `FCometChatMessage` with `Type` + `Attachment`). | +| **Send Custom Message Async** | `Message: FCometChatMessage` | `FCometChatMessage` | Send a custom-typed message with `CustomData` JSON (game events, gifts, pings). | +| **Edit Message Async** | `Message: FCometChatMessage` | `FCometChatMessage` | Edit a message's text (set `Id` + new `Text`). | +| **Delete Message Async** | `MessageId: int64` | `FCometChatMessage` | Delete a message; returns the tombstoned message with `DeletedAt` set. | | **Get Messages Async** | `Uid: FString`, `Limit: int32` | `TArray` | Fetch message history for a 1:1 conversation. | | **Get Group Messages Async** | `Guid: FString`, `Limit: int32`, `BeforeMessageId: int32` | `TArray`, `FCometChatPagination` | Fetch paginated message history for a group. | -| **Fetch Messages Async** | `Request: FCometChatMessagesRequest` | `TArray` | Fetch messages with full filter/pagination control. | +| **Fetch Previous Messages Async** | `Request: FCometChatMessagesRequest` | `TArray` | Fetch older messages with full filter/pagination control. | +| **Fetch Next Messages Async** | `Request: FCometChatMessagesRequest` | `TArray` | Fetch newer messages with full filter/pagination control. | +| **Get Message Details Async** | `MessageId: int64` | `FCometChatMessage` | Fetch a single message by ID. | + +### Receipts + +| Node | Parameters | Success Output | Description | +| ---- | ---------- | -------------- | ----------- | +| **Mark As Read Async** | `Message: FCometChatMessage` | — | Mark a message (and earlier) as read. | +| **Mark As Unread Async** | `Message: FCometChatMessage` | `FCometChatConversation` | Mark a conversation unread from this message. | +| **Mark As Delivered Async** | `Message: FCometChatMessage` | — | Mark a message as delivered. | +| **Mark As Interacted Async** | `MessageId: int64`, `ElementId: FString` | — | Record an interaction with an interactive-message element. | +| **Get Receipts Async** | `MessageId: int64` | `TArray` | Fetch per-user delivery/read receipts for a message. | + +### Reactions + +| Node | Parameters | Success Output | Description | +| ---- | ---------- | -------------- | ----------- | +| **Add Reaction Async** | `MessageId: int64`, `Reaction: FString` | `FCometChatMessage` | React to a message with an emoji. | +| **Remove Reaction Async** | `MessageId: int64`, `Reaction: FString` | `FCometChatMessage` | Remove your reaction from a message. | +| **Fetch Reactions Async** | `Request: FCometChatReactionsRequest` | `TArray` | Fetch the individual users who reacted to a message. | + + +`FCometChatMessage.Reactions` (a `TArray`) already gives per-emoji totals and whether the logged-in user reacted — enough to render reaction pills. Only call **Fetch Reactions Async** when you need the list of *who* reacted. + ### Users @@ -53,37 +95,113 @@ Each node is a latent Blueprint action with **On Success** and **On Failure** ou | ---- | ---------- | -------------- | ----------- | | **Get User Async** | `Uid: FString` | `FCometChatUser` | Fetch a user's profile. | | **Fetch Users Async** | `Request: FCometChatUsersRequest` | `TArray` | Fetch users with search, filters, and pagination. | +| **Update Current User Async** | `User: FCometChatUser` | `FCometChatUser` | Update the logged-in user's own profile. | +| **Block Users Async** | `UIDs: TArray` | — | Block one or more users. | +| **Unblock Users Async** | `UIDs: TArray` | — | Unblock one or more users. | +| **Fetch Blocked Users Async** | `Request: FCometChatBlockedUsersRequest` | `TArray` | Fetch blocked users. | +| **Get Online User Count Async** | — | `int32` | Get the count of currently-online users in the app. | +| **Create User Async** | `User: FCometChatUser`, `ApiKey: FString` | `FCometChatUser` | Admin: create a user (requires REST API Key — server-side only). | +| **Update User Async** | `User: FCometChatUser`, `ApiKey: FString` | `FCometChatUser` | Admin: update any user (requires REST API Key — server-side only). | ### Groups | Node | Parameters | Success Output | Description | | ---- | ---------- | -------------- | ----------- | -| **Create Group Async** | `Name: FString`, `MemberIds: TArray` | `FCometChatGroup` | Create a new group with initial members. | -| **Join Group Async** | `Guid: FString` | — | Join an existing group. | +| **Create Group Async** | `Group: FCometChatGroup` | `FCometChatGroup` | Create a new group (set `Name` + `Type`); the caller becomes owner. | +| **Join Group Async** | `GUID: FString`, `GroupType: FString`, `Password: FString` | `FCometChatGroup` | Join a group (`Password` only for password groups). | | **Leave Group Async** | `Guid: FString` | — | Leave a group. | +| **Delete Group Async** | `GUID: FString` | — | Delete a group (owner only, irreversible). | +| **Update Group Async** | `Group: FCometChatGroup` | `FCometChatGroup` | Update a group's name/description/icon/type/metadata. | +| **Get Group Async** | `GUID: FString` | `FCometChatGroup` | Fetch a single group's details. | | **Fetch Groups Async** | `Request: FCometChatGroupsRequest` | `TArray` | Fetch groups with search, filters, and pagination. | +| **Get Joined Groups Async** | — | `TArray` | Get the GUIDs of groups the logged-in user belongs to. | +| **Get Online Group Member Count Async** | `GUIDs: TArray` | — | Get online member counts for the given groups (JSON). | + +### Group Member Management + +| Node | Parameters | Success Output | Description | +| ---- | ---------- | -------------- | ----------- | +| **Fetch Group Members Async** | `Request: FCometChatGroupMembersRequest` | `TArray` | Fetch a group's active members with search, scope filters, and pagination. | +| **Add Members Async** | `Guid: FString`, `Members: TArray` | — | Add one or more users to a group. | +| **Kick Member Async** | `Uid: FString`, `Guid: FString` | — | Remove a member from the group (they can rejoin). | +| **Ban Member Async** | `Uid: FString`, `Guid: FString` | — | Remove a member and block them from rejoining. | +| **Unban Member Async** | `Uid: FString`, `Guid: FString` | — | Lift a ban, allowing the user to join the group again. | +| **Fetch Banned Members Async** | `Request: FCometChatBannedMembersRequest` | `TArray` | Fetch a group's banned members with search, scope filters, and pagination. | +| **Change Scope Async** | `Uid: FString`, `Guid: FString`, `Scope: FString` | — | Change a member's scope (`participant`, `moderator`, `admin`) — used to promote or demote. | +| **Transfer Ownership Async** | `GUID: FString`, `UID: FString` | — | Transfer group ownership to another member (owner only). | + + +**Permission model**: only `admin`/`owner` can promote a member to `admin`; `moderator` and above can promote a `participant` to `moderator` and kick/ban members with a strictly lower scope. The server enforces these rules — always check the logged-in user's own scope (`FCometChatGroup.Scope`, or `owner == UserUid`) before showing kick/ban/promote/demote actions in your UI. + ### Conversations | Node | Parameters | Success Output | Description | | ---- | ---------- | -------------- | ----------- | | **Fetch Conversations Async** | `Request: FCometChatConversationsRequest` | `TArray` | Fetch conversations with filters and pagination. | +| **Get Conversation Async** | `ConversationWith: FString`, `ConversationType: FString` | `FCometChatConversation` | Fetch a single conversation by peer UID/GUID and type. | +| **Delete Conversation Async** | `ConversationWith: FString`, `ConversationType: FString` | — | Delete a conversation for the logged-in user. | +| **Tag Conversation Async** | `ConversationWith: FString`, `ConversationType: FString`, `Tags: TArray` | `FCometChatConversation` | Set tags on a conversation. | +| **Mark Conversation Read Async** | `ConversationWith: FString`, `ConversationType: FString` | — | Mark an entire conversation as read. | +| **Mark Conversation Delivered Async** | `ConversationWith: FString`, `ConversationType: FString` | — | Mark an entire conversation as delivered. | -### Connection +### Unread Counts | Node | Parameters | Success Output | Description | | ---- | ---------- | -------------- | ----------- | -| **Connect Async** | — | — | Manually establish the WebSocket connection. | -| **Disconnect Async** | — | — | Manually close the WebSocket connection. | -| **Ping Async** | — | — | Ping the server to verify connectivity. | +| **Get Unread Count Async** | — | `FString` (JSON) | Total unread counts across all conversations. | +| **Get Unread Count For User Async** | `UID: FString` | `FString` | Unread count for one 1:1 conversation. | +| **Get Unread Count For Group Async** | `GUID: FString` | `FString` | Unread count for one group. | +| **Get Unread Count All Users Async** | — | `FString` | Unread counts for all 1:1 conversations. | +| **Get Unread Count All Groups Async** | — | `FString` | Unread counts for all groups. | + + +For a conversation-list UI, the per-conversation `UnreadMessageCount` returned by **Fetch Conversations Async** is usually more convenient than these JSON-string endpoints. + + +### Calls + +| Node | Parameters | Success Output | Description | +| ---- | ---------- | -------------- | ----------- | +| **Initiate Call Async** | `Call: FCometChatCall` | `FCometChatCall` | Start a call (build an `FCometChatCall` with the receiver + type). | +| **Accept Call Async** | `SessionId: FString` | `FCometChatCall` | Accept an incoming call. | +| **Reject Call Async** | `SessionId: FString`, `Status: FString` | `FCometChatCall` | Reject/decline an incoming call. | +| **End Call Async** | `SessionId: FString` | `FCometChatCall` | End/hang up an active call. | + +### AI + +| Node | Parameters | Success Output | Description | +| ---- | ---------- | -------------- | ----------- | +| **Ask Bot Async** | `ReceiverId: FString`, `ReceiverType: FString`, `BotId: FString`, `Question: FString` | `FString` (JSON) | Ask an AI bot a question. | +| **Get Smart Replies Async** | `ReceiverId: FString`, `ReceiverType: FString` | `FString` (JSON) | Get suggested replies for a conversation. | +| **Get Conversation Starter Async** | `ReceiverId: FString`, `ReceiverType: FString` | `FString` (JSON) | Get AI-suggested conversation openers. | +| **Get Conversation Summary Async** | `ReceiverId: FString`, `ReceiverType: FString` | `FString` (JSON) | Get an AI summary of a conversation. | + +### Notifications & Preferences + +| Node | Parameters | Success Output | Description | +| ---- | ---------- | -------------- | ----------- | +| **Register Token Async** | `Token: FString`, `Platform: ECometChatPushPlatform`, `ProviderId: FString` | `FString` | Register a device push token. | +| **Unregister Token Async** | — | — | Unregister the device's push token. | +| **Fetch Preferences Async** | — | `FCometChatNotificationPreferences` | Fetch notification preferences. | +| **Update Preferences Async** | `Preferences: FCometChatNotificationPreferences` | `FCometChatNotificationPreferences` | Update notification preferences. | +| **Reset Preferences Async** | — | `FCometChatNotificationPreferences` | Reset preferences to defaults. | +| **Mute Conversations Async** | `Conversations: TArray` | — | Mute one or more conversations. | +| **Unmute Conversations Async** | `Conversations: TArray` | — | Unmute one or more conversations. | +| **Get Muted Conversations Async** | — | `TArray` | List the user's muted conversations. | ### Moderation | Node | Parameters | Success Output | Description | | ---- | ---------- | -------------- | ----------- | -| **Flag Message Async** | `MessageId: FString`, `FlagDetail: FCometChatFlagDetail` | — | Flag a message for moderation. | +| **Flag Message Async** | `MessageId: int64`, `FlagDetail: FCometChatFlagDetail` | — | Flag a message for moderation review. | | **Get Flag Reasons Async** | — | `TArray` | Fetch available flag/report reasons. | +### Extensions + +| Node | Parameters | Success Output | Description | +| ---- | ---------- | -------------- | ----------- | +| **Call Extension Async** | `Slug: FString`, `Method: FString`, `Endpoint: FString`, `BodyJson: FString` | `FString` | Call a CometChat extension's REST endpoint and return its raw JSON response. | --- @@ -135,6 +253,31 @@ All delegates are `UPROPERTY(BlueprintAssignable)` on `UCometChatSubsystem`. The | `OnGroupMemberScopeChanged` | `FCometChatScopeChangeEvent` | Member scope changed | | `OnMemberAddedToGroup` | `FCometChatAction, FCometChatUser, FCometChatUser, FCometChatGroup` | Member added | +### CallListener + +| Delegate | Payload Type | Fires When | +| -------- | ------------ | ---------- | +| `OnIncomingCallReceived` | `FCometChatCall` | An incoming call arrives | +| `OnOutgoingCallAccepted` | `FCometChatCall` | Your outgoing call is accepted | +| `OnOutgoingCallRejected` | `FCometChatCall` | Your outgoing call is rejected | +| `OnIncomingCallCancelled` | `FCometChatCall` | An incoming call is cancelled by the caller | +| `OnCallEndedMessageReceived` | `FCometChatCall` | A call-ended message arrives | + +### OngoingCallListener + +| Delegate | Payload Type | Fires When | +| -------- | ------------ | ---------- | +| `OnCallUserJoined` | `FCometChatUser` | A user joins the ongoing call | +| `OnCallUserLeft` | `FCometChatUser` | A user leaves the ongoing call | +| `OnCallError` | `FCometChatError` | An error occurs during the call | +| `OnOngoingCallEnded` | `FCometChatCall` | The ongoing call ends | +| `OnCallUserListUpdated` | `TArray` | The participant list changes | +| `OnAudioModesUpdated` | `TArray` | Available audio output modes change | +| `OnRecordingStarted` | `FCometChatUser` | A participant starts recording | +| `OnRecordingStopped` | `FCometChatUser` | A participant stops recording | +| `OnUserMuted` | `FCometChatUserMutedEvent` | A participant is muted | +| `OnCallSwitchedToVideo` | `FCometChatCallSwitchedToVideoEvent` | The call switches from audio to video | + ### ConnectionListener | Delegate | Payload Type | Fires When | @@ -160,6 +303,15 @@ All delegates are `UPROPERTY(BlueprintAssignable)` on `UCometChatSubsystem`. The | -------- | ------------ | ---------- | | `OnAIAssistantEvent` | `FCometChatAIAssistantEvent` | AI assistant event | +### Push Notifications + +Only fire when push is enabled — see [Push Notifications](/sdk/unreal/push-notifications). + +| Delegate | Payload Type | Fires When | +| -------- | ------------ | ---------- | +| `OnPushNotificationReceived` | `FString` (payload JSON) | A push arrives while the game is running | +| `OnPushNotificationTapped` | `FString` (payload JSON) | The player taps a CometChat notification (including a cold start) | + --- ## Structs @@ -305,6 +457,16 @@ All delegates are `UPROPERTY(BlueprintAssignable)` on `UCometChatSubsystem`. The | `Count` | `int32` | Number of times this reaction was used | | `bReactedByMe` | `bool` | Whether the logged-in user reacted | +### FCometChatReaction + +Returned by **Fetch Reactions Async** — one entry per user who reacted, as opposed to `FCometChatReactionCount`'s per-emoji totals. + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Reaction` | `FString` | The reaction emoji/string | +| `ReactedBy` | `FCometChatUser` | The user who reacted | +| `ReactedAt` | `int64` | Unix timestamp when the reaction was added | + ### FCometChatAttachment | Property | Type | Description | @@ -414,6 +576,108 @@ All delegates are `UPROPERTY(BlueprintAssignable)` on `UCometChatSubsystem`. The | `HasMore` | `bool` | Whether more pages exist | | `NextCursor` | `int32` | Cursor for next page | +### FCometChatPresence + +Legacy presence payload (`OnPresenceChanged`). + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Uid` | `FString` | The user whose presence changed | +| `Status` | `ECometChatPresenceStatus` | `Online`, `Offline`, or `Away` | +| `LastActiveAt` | `int64` | Unix timestamp of last activity | + +### FCometChatCall + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `BaseMessage` | `FCometChatMessage` | The underlying call message | +| `SessionId` | `FString` | Unique call session identifier | +| `CallStatus` | `FString` | `initiated`, `ongoing`, `ended`, `rejected`, `busy`, `cancelled`, `unanswered` | +| `Action` | `FString` | Call action type | +| `InitiatedAt` | `int64` | When the call was initiated | +| `JoinedAt` | `int64` | When the local user joined | +| `CallInitiator` | `FCometChatUser` | The user who started the call | +| `CallReceiverUser` | `FCometChatUser` | The receiver (for 1:1 calls) | +| `CallReceiverGroup` | `FCometChatGroup` | The receiver (for group calls) | + +### FCometChatAudioMode + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Mode` | `FString` | Audio output mode (e.g. `speaker`, `earpiece`, `bluetooth`) | +| `bIsSelected` | `bool` | Whether this mode is currently active | + +### FCometChatUserMutedEvent + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `MutedUser` | `FCometChatUser` | The user who was muted | +| `MutedBy` | `FCometChatUser` | The user who performed the mute | + +### FCometChatCallSwitchedToVideoEvent + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `SessionId` | `FString` | The call session | +| `InitiatedBy` | `FCometChatUser` | Who requested the switch to video | +| `AcceptedBy` | `FCometChatUser` | Who accepted the switch | + +### FCometChatNotificationPreferences + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `bUsePrivacyTemplate` | `bool` | Use the app's privacy template | +| `OneOnOne` | `FCometChatOneOnOnePreferences` | 1:1 notification preferences | +| `Group` | `FCometChatGroupPreferences` | Group notification preferences | +| `Mute` | `FCometChatMutePreferences` | Do-not-disturb / mute schedule | + +### FCometChatOneOnOnePreferences + +Each field is `int32` where `1` = notifications on, `0` = off. + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Messages` | `int32` | New 1:1 messages | +| `Replies` | `int32` | Replies | +| `Reactions` | `int32` | Reactions | + +### FCometChatGroupPreferences + +Each field is `int32` (`1` = on, `0` = off): `Messages`, `Replies`, `Reactions`, `MemberLeft`, `MemberAdded`, `MemberJoined`, `MemberKicked`, `MemberBanned`, `MemberUnbanned`, `MemberScopeChanged`. + +### FCometChatMutePreferences + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `DND` | `int32` | Do-not-disturb toggle (`1` = on) | +| `ScheduleJson` | `FString` | JSON mute schedule | + +### FCometChatMutedConversation + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Id` | `FString` | Conversation peer UID or group GUID | +| `Type` | `FString` | `user` or `group` | +| `Until` | `int64` | Unix timestamp to mute until (`0` for indefinite) | + +### FCometChatUnmutedConversation + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Id` | `FString` | Conversation peer UID or group GUID | +| `Type` | `FString` | `user` or `group` | + +### FCometChatConversationUpdateSettings + +Returned by `GetConversationUpdateSettings()` — which message categories bump a conversation's last-message/unread state. + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `bCallActivities` | `bool` | Call activity messages update the conversation | +| `bGroupActions` | `bool` | Group action messages update the conversation | +| `bCustomMessages` | `bool` | Custom messages update the conversation | +| `bMessageReplies` | `bool` | Thread replies update the conversation | + --- ## Request Builder Structs @@ -551,6 +815,14 @@ All delegates are `UPROPERTY(BlueprintAssignable)` on `UCometChatSubsystem`. The | `Approved` | Message has been approved | | `Disapproved` | Message has been rejected | +### ECometChatPushPlatform + +| Value | Description | +| ----- | ----------- | +| `FCM` | Firebase Cloud Messaging (Android) | +| `APNS` | Apple Push Notification service (iOS) | +| `APNS_VOIP` | Apple PushKit VoIP (iOS call notifications) | + --- ## C++ Header Includes diff --git a/sdk/unreal/send-message.mdx b/sdk/unreal/send-message.mdx index 0ce4bc25e..88300d51d 100644 --- a/sdk/unreal/send-message.mdx +++ b/sdk/unreal/send-message.mdx @@ -28,6 +28,8 @@ flowchart LR Send a text message to a specific user by their UID. +**Use case:** A player opens a direct chat with a teammate and fires off a quick "gg" right after a match ends. + @@ -44,6 +46,15 @@ Send a text message to a specific user by their UID. | Text | `FString` | The message body | **On Success** returns an `FCometChatMessage` with the sent message details (including the server-assigned `Id` and `SentAt` timestamp). + +**Node flow:** + +1. `On Text Committed (chat input)` → `Get CometChat Subsystem` +2. `Send Message Async` (Receiver Uid, Text) +3. **On Success** → append the returned `FCometChatMessage` to the chat list, clear the input +4. **On Failure** → `Break FCometChatError` → show a "couldn't send" toast, keep the draft text + +Wire **On Failure** to a custom event that receives an `FCometChatError` (`Code`, `Message`, `Details`) so you can surface the reason and leave the composer editable. ```cpp @@ -66,20 +77,60 @@ void AMyActor::OnMessageSent(const FCometChatMessage& Message) UE_LOG(LogTemp, Log, TEXT("Message sent! ID: %s"), *Message.Id); } -void AMyActor::OnMessageFailed(const FString& Error) +void AMyActor::OnMessageFailed(const FCometChatError& Error) { - UE_LOG(LogTemp, Error, TEXT("Send failed: %s"), *Error); + UE_LOG(LogTemp, Error, TEXT("Send failed: %s"), *Error.Message); } ``` + +Optimistically render the message in a "sending…" state the instant the player commits it, then reconcile on **On Success** by swapping in the server-assigned `Id` and `SentAt`. The chat feels instant even on a slow connection. + + + +Never call **Send Message Async** from **Event Tick**, and don't block the game thread waiting for **On Success** — the node is asynchronous. Drive it from an input or commit event and let the callback update your UI. + + +### Handle a failed send (keep and resend) + +Hold onto the unsent text on **On Failure** so the player can retry without retyping. Clear it only once the send actually succeeds. + +```cpp +FString PendingText; // member: the message awaiting confirmation + +void AMyActor::TrySend(const FString& Text) +{ + PendingText = Text; + auto* Action = UCometChatSendMessageAction::SendMessageAsync(this, TEXT("cometchat-uid-2"), Text); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnSendOk); + Action->OnFailure.AddDynamic(this, &AMyActor::OnSendFailed); + Action->Activate(); +} + +void AMyActor::OnSendOk(const FCometChatMessage& Message) +{ + PendingText.Empty(); // delivered — safe to clear the composer + // append Message to your chat list here +} + +void AMyActor::OnSendFailed(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Warning, TEXT("Send failed [%s]: %s"), *Error.Code, *Error.Message); + // Leave PendingText in the composer and offer a Retry control that calls TrySend(PendingText) again. + ShowResendButton(PendingText); // your own UI helper +} +``` + --- ## Send a Group Message Send a text message to a group by its GUID. +**Use case:** In a guild lobby, a member types into the shared chat input and presses Enter to rally the team before a raid. + @@ -94,6 +145,15 @@ Call the **Send Group Message Async** node. | Text | `FString` | The message body | **On Success** returns an `FCometChatMessage`. + +**Node flow:** + +1. `On Text Committed (chat input)` → `Get CometChat Subsystem` +2. `Send Group Message Async` (Guid, Text) +3. **On Success** → append the returned `FCometChatMessage` to the chat list, clear the input +4. **On Failure** → `Break FCometChatError` → show a "couldn't send" toast, keep the draft text + +Wire **On Failure** to a custom event that receives an `FCometChatError` (`Code`, `Message`, `Details`) so you can tell the player why it didn't send and keep their draft. ```cpp @@ -116,14 +176,207 @@ void AMyActor::OnGroupMessageSent(const FCometChatMessage& Message) UE_LOG(LogTemp, Log, TEXT("Group message sent! ID: %s"), *Message.Id); } -void AMyActor::OnGroupMessageFailed(const FString& Error) +void AMyActor::OnGroupMessageFailed(const FCometChatError& Error) { - UE_LOG(LogTemp, Error, TEXT("Group send failed: %s"), *Error); + UE_LOG(LogTemp, Error, TEXT("Group send failed: %s"), *Error.Message); } ``` +### Reference: sample UI + +The `UCometChatGroupChatBox` widget that ships with the plugin sends group messages straight through the native C++ SDK. Here's the trimmed call from its send handler: + +```cpp +// UCometChatGroupChatBox::OnSendClicked() +// UnrealPlugin/CometChat/Source/CometChat/UI/CometChatGroupChatBox.cpp +Sub->GetSdk()->send_group_message(TCHAR_TO_UTF8(*GroupGuid), TCHAR_TO_UTF8(*Text), + [this](const std::string& err, const chatsdk::Message& msg) + { + // Callback runs off the game thread — hop back before touching UI. + AsyncTask(ENamedThreads::GameThread, [this, err, msg]() + { + if (err.empty()) + { + AddMessageToList(CometChatConversions::ToUnreal(msg)); + ScrollToBottom(); + } + }); + }); +``` + + +This is the native C++ SDK API (`Sub->GetSdk()->send_group_message(...)`), handy when you're writing a custom widget in C++. In game code, prefer the **Send Group Message Async** node / `UCometChatSendGroupMessageAction`: it marshals the callback back to the game thread for you and exposes typed **On Success** / **On Failure** pins. + + + +Append the returned `FCometChatMessage` and clear the composer inside **On Success** — not before you send — so a failed send leaves the player's draft untouched. + + + +Don't send on every keystroke or from **Event Tick**. Send once per committed message; spamming the node floods the group and can trip rate limits. + + +--- + +## Send a Media Message + +Send an image, video, audio clip, or file. You build an `FCometChatMessage`, set its `Type` and `Attachment`, and point the attachment at an already-uploaded, publicly reachable `Url`. + +**Use case:** After a match, a player shares a screenshot of the final scoreboard into the squad chat. + + + +Call the **Send Media Message Async** node with an `FCometChatMessage` you've populated. + +| Field to set | Type | Description | +| ------------ | ---- | ----------- | +| Receiver Uid | `FString` | The recipient's UID (1:1) or group GUID | +| Receiver Type | `FString` | `user` or `group` | +| Type | `FString` | `image`, `video`, `audio`, or `file` | +| Attachment | `FCometChatAttachment` | The media metadata (see table below) | +| Caption | `FString` | *(Optional)* Text shown alongside the media | + +**On Success** returns the sent `FCometChatMessage` (with its server-assigned `Id` and `SentAt`). + +**Node flow:** + +1. `On File Chosen (upload complete)` → `Get CometChat Subsystem` +2. `Make FCometChatMessage` (set `Receiver Uid`, `Receiver Type`, `Type`, `Attachment.Url`) +3. `Send Media Message Async` (Message) +4. **On Success** → append the returned `FCometChatMessage`, show the delivered media +5. **On Failure** → `Break FCometChatError` → show an "upload failed" toast, keep the local preview so the player can retry + +Wire **On Failure** to a custom event that receives an `FCometChatError` (`Code`, `Message`, `Details`) so a broken URL or oversized file surfaces a clear message. + + +```cpp +#include "AsyncActions/CometChatSendMediaMessageAction.h" + +void AMyActor::SendImage() +{ + FCometChatMessage Message; + Message.ReceiverUid = TEXT("cometchat-uid-2"); // or a group GUID + Message.ReceiverType = TEXT("user"); // "user" or "group" + Message.Type = TEXT("image"); // image / video / audio / file + Message.Caption = TEXT("Check out my high score!"); + + Message.Attachment.Name = TEXT("score.png"); + Message.Attachment.Extension = TEXT("png"); + Message.Attachment.MimeType = TEXT("image/png"); + Message.Attachment.Size = 20480; // bytes + Message.Attachment.Url = TEXT("https://cdn.mygame.com/uploads/score.png"); + + auto* Action = UCometChatSendMediaMessageAction::SendMediaMessageAsync(this, Message); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnMediaSent); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnMediaSent(const FCometChatMessage& Message) +{ + UE_LOG(LogTemp, Log, TEXT("Media sent! ID: %s (%s)"), + *Message.Id, *Message.Attachment.Url); +} + +void AMyActor::OnError(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("Send failed: %s"), *Error.Message); +} +``` + + + + +Upload the file to your storage bucket or CDN first, then pass the resulting public `Url`. Show a local thumbnail immediately and swap to the delivered message on **On Success** so the share feels instant. + + + +Media messages need an already-uploaded, publicly reachable `Url`. A local file path (`C:\Users\…` or `/Game/…`) won't resolve for recipients and the send will fail — upload first, then send the returned URL. + + +### FCometChatAttachment + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `Extension` | `FString` | File extension, e.g. `png` | +| `MimeType` | `FString` | MIME type, e.g. `image/png` | +| `Name` | `FString` | Display file name | +| `Size` | `int64` | File size in bytes | +| `Url` | `FString` | Publicly reachable URL of the uploaded file | + +--- + +## Send a Custom Message + +Custom messages carry an arbitrary `Type` and a `CustomData` JSON payload — perfect for game-specific events like gifts, challenges, or location pings that you render with your own bubble. + +**Use case:** A player sends a teammate a "golden sword" gift that pops a custom animated bubble in the recipient's chat. + + + +Call the **Send Custom Message Async** node with a populated `FCometChatMessage`. + +| Field to set | Type | Description | +| ------------ | ---- | ----------- | +| Receiver Uid | `FString` | The recipient's UID (1:1) or group GUID | +| Receiver Type | `FString` | `user` or `group` | +| Type | `FString` | Your custom message type, e.g. `game_gift` | +| Custom Data | `FString` | A JSON string carrying your payload | +| Sub Type | `FString` | *(Optional)* A finer classification within your `Type` | + +**On Success** returns the sent `FCometChatMessage`. + +**Node flow:** + +1. `On Gift Selected` → `Get CometChat Subsystem` +2. `Make FCometChatMessage` (set `Receiver Uid`, `Receiver Type`, `Type` = `game_gift`, `Custom Data` = your JSON) +3. `Send Custom Message Async` (Message) +4. **On Success** → append the returned `FCometChatMessage`, spawn the gift effect locally +5. **On Failure** → `Break FCometChatError` → show a "gift not sent" toast, re-enable the gift button + +Wire **On Failure** to a custom event that receives an `FCometChatError` (`Code`, `Message`, `Details`) so a rejected payload doesn't silently swallow the player's action. + + +```cpp +#include "AsyncActions/CometChatSendCustomMessageAction.h" + +void AMyActor::SendGift() +{ + FCometChatMessage Message; + Message.ReceiverUid = TEXT("cometchat-uid-2"); // or a group GUID + Message.ReceiverType = TEXT("user"); // "user" or "group" + Message.Type = TEXT("game_gift"); // your custom type + Message.SubType = TEXT("golden_sword"); // optional finer classification + Message.CustomData = TEXT("{\"item\":\"golden_sword\",\"qty\":1,\"rarity\":\"legendary\"}"); + + auto* Action = UCometChatSendCustomMessageAction::SendCustomMessageAsync(this, Message); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnCustomSent); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnCustomSent(const FCometChatMessage& Message) +{ + UE_LOG(LogTemp, Log, TEXT("Custom '%s' sent! ID: %s"), *Message.Type, *Message.Id); +} +``` + + + + +Receiving clients get custom messages through the same `OnMessageReceived` delegate — branch on `Message.Type` and parse `Message.CustomData` to spawn the right in-game effect. + + + +Keep `CustomData` a small JSON of ids and enums (item id, quantity, rarity) that the receiver looks up locally — it stays fast to parse and cheap to sync across clients. + + + +Don't stuff large payloads — full inventory dumps, base64-encoded images — into `CustomData`. Oversized custom messages bloat conversation history and slow every client that loads it. Send big assets as **media messages** instead. + + --- ## The FCometChatMessage Struct @@ -145,6 +398,134 @@ For the full struct definition, see [Key Concepts → FCometChatMessage](/sdk/un --- +## Edit a Message + +Update the text of a message you've already sent. Populate an `FCometChatMessage` with the existing message's `Id` and the new `Text`. + +**Use case:** A player fixes a typo in a message they just posted to the group, and everyone sees the correction in place. + + + +Call the **Edit Message Async** node with an `FCometChatMessage`. + +| Field to set | Type | Description | +| ------------ | ---- | ----------- | +| Id | `FString` | The server-assigned ID of the message to edit | +| Text | `FString` | The new message body | + +**On Success** returns the updated `FCometChatMessage` with its `EditedAt` timestamp set. + +**Node flow:** + +1. `On Edit Confirmed` → `Get CometChat Subsystem` +2. `Make FCometChatMessage` (set `Id` = existing message id, `Text` = new body) +3. `Edit Message Async` (Message) +4. **On Success** → update the existing bubble in place, add an "edited" marker +5. **On Failure** → `Break FCometChatError` → show an "edit failed" toast, restore the previous text + +Wire **On Failure** to a custom event that receives an `FCometChatError` (`Code`, `Message`, `Details`) so a rejected edit rolls the bubble back instead of showing a change that never saved. + + +```cpp +#include "AsyncActions/CometChatEditMessageAction.h" + +void AMyActor::EditMyMessage(const FString& MessageId) +{ + FCometChatMessage Message; + Message.Id = MessageId; // the existing message's server ID + Message.Text = TEXT("Edited: good game!"); // the new body + + auto* Action = UCometChatEditMessageAction::EditMessageAsync(this, Message); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnMessageEdited); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnMessageEdited(const FCometChatMessage& Message) +{ + UE_LOG(LogTemp, Log, TEXT("Edited at %lld: %s"), Message.EditedAt, *Message.Text); +} +``` + + + + +Update the bubble in place on **On Success** using the returned `EditedAt`, and show a small "edited" label — there's no need to re-fetch the conversation. + + + +Only the original sender can edit a message, and the edit still round-trips the network. Handle **On Failure** rather than assuming it stuck — roll the bubble back to the previous text if it fails. + + +--- + +## Delete a Message + +Soft-delete a message. The server returns a **tombstoned** copy with `DeletedAt` set, which you can use to render a "This message was deleted" placeholder. + +**Use case:** A player removes a message they posted by mistake, and it collapses to a "This message was deleted" note for everyone. + + + +Call the **Delete Message Async** node. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| Message Id | `int64` | The ID of the message to delete | + +**On Success** returns the tombstoned `FCometChatMessage` with its `DeletedAt` (and `DeletedBy`) fields populated. + +**Node flow:** + +1. `On Delete Confirmed` → `Get CometChat Subsystem` +2. `Delete Message Async` (Message Id) +3. **On Success** → swap the bubble for a "This message was deleted" placeholder (read `DeletedAt` / `DeletedBy`) +4. **On Failure** → `Break FCometChatError` → show a "couldn't delete" toast, leave the bubble as-is + +Wire **On Failure** to a custom event that receives an `FCometChatError` (`Code`, `Message`, `Details`) so a failed delete doesn't leave the UI out of sync with the server. + + +```cpp +#include "AsyncActions/CometChatDeleteMessageAction.h" + +void AMyActor::DeleteMyMessage(int64 MessageId) +{ + auto* Action = UCometChatDeleteMessageAction::DeleteMessageAsync(this, MessageId); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnMessageDeleted); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnMessageDeleted(const FCometChatMessage& Message) +{ + // Message.DeletedAt is now set — swap the bubble for a deleted-placeholder + UE_LOG(LogTemp, Log, TEXT("Deleted at %lld by %s"), + Message.DeletedAt, *Message.DeletedBy); +} +``` + + + + +Delete is a soft-delete: render the tombstoned copy (its `DeletedAt` / `DeletedBy` are set) as a "This message was deleted" placeholder rather than removing the row, so message indices stay stable. + + + +Don't hard-remove the row and assume you're finished — other clients receive the deletion through `OnMessageDeleted` and expect the same placeholder. Re-adding a locally deleted row won't un-delete it on the server. + + + +When you edit or delete a message, every other connected client with that conversation open receives the change in real time through the `OnMessageEdited` / `OnMessageDeleted` subsystem delegates — bind them to update the message bubble in place without re-fetching. See [Real-Time Events](/sdk/unreal/real-time-events). + + +--- + +## Reactions + +Users can react to any message (1:1 or group) with an emoji. Reactions have their own async nodes, per-emoji count summaries, and real-time delegates — see the dedicated [Reactions](/sdk/unreal/reactions) page for the full guide, including the color-emoji font setup for rendering reaction pills. + +--- + ## Next Steps diff --git a/sdk/unreal/setup.mdx b/sdk/unreal/setup.mdx index 604432e83..404eb7245 100644 --- a/sdk/unreal/setup.mdx +++ b/sdk/unreal/setup.mdx @@ -24,10 +24,10 @@ description: "Install the CometChat plugin in your Unreal Engine project." ### Supported Platforms -| Engine Version | Platforms | -| -------------- | --------- | -| UE 5.5.4 | Mac, Windows, iOS, Android | -| UE 5.7.2+ | Mac, Windows, iOS, Android | +| Engine Version | Precompiled binaries available | Additionally supported from source | +| -------------- | ------------------------------ | ---------------------------------- | +| UE 5.5.4 | Mac, Windows | iOS, Android | +| UE 5.7.2+ | Mac | iOS, Android | --- @@ -36,29 +36,34 @@ description: "Install the CometChat plugin in your Unreal Engine project." ### Option 1: Use Precompiled Binaries (No Build Required) - -Copy `Plugins/CometChatSdk/` from the [SDK repository](https://github.com/cometchat/chat-sdk-unreal/tree/v1) into your project's `Plugins/CometChat/` directory. - - - -Download the precompiled binaries for your engine version: + +Precompiled builds are published to Cloudsmith — they are not stored in the SDK repository. ```bash -curl -1sLf -O 'https://dl.cloudsmith.io/public/cometchat/cometchat/raw/versions/1.0.0-beta.2/CometChat-UE5.5-precompiled.zip' +curl -1sLf -O 'https://dl.cloudsmith.io/public/cometchat/cometchat/raw/versions/1.0.0/CometChat-UE5.5-precompiled.zip' ``` ```bash -curl -1sLf -O 'https://dl.cloudsmith.io/public/cometchat/cometchat/raw/versions/1.0.0-beta.2/CometChat-UE5.7-precompiled.zip' +curl -1sLf -O 'https://dl.cloudsmith.io/public/cometchat/cometchat/raw/versions/1.0.0/CometChat-UE5.7-precompiled.zip' ``` - -Extract the zip contents into your project's `Plugins/CometChat/` directory (merging with existing files). + +Extract the zip and copy the folder for your platform into your project's `Plugins/CometChat/` directory, so that `CometChat.uplugin` sits directly inside it: + +| Package | Folder to copy | +| ------- | -------------- | +| `CometChat-UE5.5-precompiled.zip` | `5.5/mac/` or `5.5/windows/` | +| `CometChat-UE5.7-precompiled.zip` | `5.7/` | + + +Each folder is a **complete, self-contained plugin** — it already includes the source, `ThirdParty/` static libraries, and the prebuilt editor binary. Do not merge it with `Plugins/CometChatSdk/` from the SDK repository. + @@ -225,12 +230,29 @@ The plugin provides: - **`UCometChatSubsystem`** — Game instance subsystem for CometChat operations - **Async Blueprint actions** — for login, logout, send/fetch messages, groups, conversations, moderation, and more - **`CometChatEventBridge`** — Real-time event callbacks (40+ delegates organized by listener type) -- **`UCometChatPanel`** — Ready-to-use tabbed chat panel widget (My Groups, Personal, Browse Groups) -- **`UCometChatButton`** — Floating button widget that toggles the chat panel +- **Sample UI widgets** (see below) - **Native C++ chat SDK** via `ThirdParty/chatsdk/` (prebuilt static libraries) --- +## Sample UI Components (reference) + +The plugin ships with ready-made chat widgets. They are **C++ reference implementations** (built in code, not the Blueprint graph) that you can drop into a project as-is or read as a worked example of the SDK in action: + +| Widget | What it is | +| ------ | ---------- | +| `UCometChatGroupChatBox` | A self-contained group/1:1 chat box: configure → login → join → load history → send/receive. The simplest starting point. | +| `UCometChatPanel` | A full tabbed panel (My Groups, Personal, Browse Groups) with search, media, reactions, and group member management. | +| `UCometChatButton` | A floating button that toggles `UCometChatPanel` open and closed. | + + +These widgets call the **native C++ SDK** directly (`GetGameInstance()->GetSubsystem()->GetSdk()->…`), which is a lower-level API than the Blueprint **async action nodes** the rest of these docs use. Prefer the async nodes in your own game code — they are Blueprint-friendly and return typed structs. The widgets are provided for reference and quick starts. Throughout the feature guides, look for **"Reference: sample UI"** notes that point to the exact widget function implementing that feature. + + +Source lives under `Plugins/CometChat/Source/CometChat/UI/` — `CometChatGroupChatBox.cpp`, `CometChatPanel.cpp`, and `CometChatButton.cpp`. + +--- + ## Next Steps Now that the plugin is installed and configured, head to [Authentication](/sdk/unreal/authentication) to log in your first user. diff --git a/sdk/unreal/transient-messages.mdx b/sdk/unreal/transient-messages.mdx index 741df8a95..4f44ad134 100644 --- a/sdk/unreal/transient-messages.mdx +++ b/sdk/unreal/transient-messages.mdx @@ -164,6 +164,56 @@ void AMyActor::HandleTransientMessage(const FCometChatTransientMessage& Message) --- +## Use Case: Live Score / Location Ping + +**Goal:** broadcast a lightweight signal — a live score delta, a map marker, or a player location — to everyone in a match group, updating HUDs instantly without writing anything to history. + +This is a bind-on-receive, fire-on-send flow. Bind the receive delegate once, before Login (see [Real-Time Events](/sdk/unreal/real-time-events)). + + + +**Receive:** + +1. Get a reference to the **CometChat Subsystem** +2. **Bind Event** to **On Transient Message Received** → parse `Data` as JSON, route by a `type` field, update the HUD or marker + +**Send (fire-and-forget):** + +3. On a score/location change, build an `FCometChatTransientMessage` — `ReceiverId` = match GUID, `ReceiverType` = `"group"`, `Data` = `{"type":"score","uid":"p1","score":42}` +4. Call **Send Transient Message** + + +```cpp +void AMyActor::PingScore(const FString& MatchGuid, int32 Score) +{ + UCometChatSubsystem* Chat = GetGameInstance()->GetSubsystem(); + if (!Chat->IsLoggedIn()) return; // no connection → the ping would be dropped + + FCometChatTransientMessage Msg; + Msg.ReceiverId = MatchGuid; + Msg.ReceiverType = TEXT("group"); + Msg.Data = FString::Printf(TEXT("{\"type\":\"score\",\"score\":%d}"), Score); + + Chat->SendTransientMessage(Msg); +} +``` + + + + +`SendTransientMessage` is fire-and-forget void — there is no On Success/On Failure pin. It silently no-ops when you are not connected, so check `IsLoggedIn()` (and, in manual-socket mode, `GetConnectionStatus() == Connected`) before broadcasting. `OnTransientMessageReceived` is a real-time delegate: a dropped socket pauses it, and any pings sent during the gap are gone for good — pair the view with a [connection banner](/sdk/unreal/connection-status). + + + +**Best practice:** reserve transient messages for ephemeral, high-frequency signals — cursor positions, live scores, "looking at this" hints — where dropping a single frame is harmless and nothing needs to be persisted. + + + +**Never** use transient messages for anything that must arrive reliably — purchases, moves in a turn-based game, or chat you expect to see in history. Offline users receive nothing and there is no delivery guarantee or retry. Use a regular message for those. + + +--- + ## Next Steps diff --git a/sdk/unreal/typing-indicators.mdx b/sdk/unreal/typing-indicators.mdx index 3e9437ba3..249b64110 100644 --- a/sdk/unreal/typing-indicators.mdx +++ b/sdk/unreal/typing-indicators.mdx @@ -127,6 +127,90 @@ sequenceDiagram --- +## Use Case: Live "is typing…" Indicator + +**Goal:** show "Alex is typing…" while a member composes, then hide it when they send, blur the composer, or go idle. + +This is a delegate-bind flow on the receive side and a debounced send on the local side. Bind the delegates once, before Login (see [Real-Time Events](/sdk/unreal/real-time-events)). + +**Receive — show the indicator:** + +1. Get a reference to the **CometChat Subsystem** +2. **Bind Event** to **On Typing Started** → add `Sender.Uid` to a "currently typing" set, refresh the label, and (re)start a UI timeout timer +3. **Bind Event** to **On Typing Ended** → remove `Sender.Uid` from the set and refresh the label + +**Send — notify others:** + +4. On the composer's **Text Changed**, if you haven't already flagged the local user as typing, call **Start Typing** once and set the flag +5. (Re)start a short idle timer on each change; when it fires — or on **send** / **focus lost** — call **End Typing** and clear the flag + + +`StartTyping` and `EndTyping` are fire-and-forget void calls — there is no On Success/On Failure pin. They only reach other users while you are connected, so guard them with `IsLoggedIn()` first. Treat `OnTypingStarted`/`OnTypingEnded` as best-effort too: a dropped connection pauses these events, so always keep a receiver-side timeout (so a missed `OnTypingEnded` can't leave a stale "is typing…" on screen) and pair the indicator with a [connection banner](/sdk/unreal/connection-status). + + +### Reference: sample UI + +The bundled `UCometChatGroupChatBox` widget implements this exact debounce + timeout. Trimmed from `CometChatGroupChatBox.cpp`: + +```cpp +// CometChatGroupChatBox.cpp — OnInputChanged(): debounced StartTyping + idle EndTyping +void UCometChatGroupChatBox::OnInputChanged(const FText& Text) +{ + UCometChatSubsystem* Sub = GetChatSubsystem(); + if (!Sub) return; + + const FCometChatTypingIndicator Ind{ GroupGuid, TEXT("group"), TEXT(""), FCometChatUser{} }; + + // Send StartTyping only on the first keystroke of a burst, not on every one + if (!Text.IsEmpty() && !bIsLocalUserTyping) + { + bIsLocalUserTyping = true; + Sub->StartTyping(Ind); + } + + // Reset a 3s idle timer; when it fires (or the box is emptied) send EndTyping + UWorld* World = GetWorld(); + World->GetTimerManager().ClearTimer(LocalTypingEndHandle); + if (!Text.IsEmpty()) + { + World->GetTimerManager().SetTimer(LocalTypingEndHandle, [this, Sub, Ind]() + { + bIsLocalUserTyping = false; + Sub->EndTyping(Ind); + }, 3.0f, false); + } + else + { + bIsLocalUserTyping = false; + Sub->EndTyping(Ind); + } +} + +// CometChatGroupChatBox.cpp — HandleTypingStarted(): receiver arms an auto-clear timeout +void UCometChatGroupChatBox::HandleTypingStarted(const FCometChatTypingIndicator& Indicator) +{ + if (Indicator.ReceiverId != GroupGuid || Indicator.Sender.Uid == CurrentUserUid) return; + + TypingUsers.Add(Indicator.Sender.Uid, + Indicator.Sender.Name.IsEmpty() ? Indicator.Sender.Uid : Indicator.Sender.Name); + UpdateTypingIndicatorUI(); + + // If OnTypingEnded never arrives, OnTypingTimeout() clears the label after TypingTimeoutSeconds + GetWorld()->GetTimerManager().SetTimer(TypingTimeoutHandle, this, + &UCometChatGroupChatBox::OnTypingTimeout, TypingTimeoutSeconds, false); +} +``` + + +**Best practice:** debounce. Send `StartTyping` once when a typing burst begins — not on every keystroke — and always send `EndTyping` on message send, composer blur, and idle timeout. Keep a receiver-side timeout so a missed end event self-heals. + + + +**Avoid** calling `StartTyping` on every `OnTextChanged`/keystroke. It floods the socket, invites rate-limiting (`OnFeatureThrottled`), and makes remote indicators flicker. One `StartTyping` per burst is enough. + + +--- + ## Next Steps diff --git a/sdk/unreal/ui-components.mdx b/sdk/unreal/ui-components.mdx deleted file mode 100644 index c68a88473..000000000 --- a/sdk/unreal/ui-components.mdx +++ /dev/null @@ -1,211 +0,0 @@ ---- -title: "UI Components" -description: "Drop-in chat UI widgets: CometChatPanel (tabbed chat) and CometChatButton (floating toggle)." ---- - -The CometChat Unreal SDK includes two ready-to-use UMG widgets that provide a complete chat experience out of the box. Both are fully styleable and configurable via Blueprint properties. - ---- - -## UCometChatPanel - -A game-style tabbed chat panel with a dark translucent HUD aesthetic. It provides: - -- **Three tabs**: My Groups, Personal, Browse Groups -- **Search**: Filter conversations and groups by name -- **Inline chat view**: Select a conversation to open the chat within the panel -- **Typing indicators**: Shows who is typing in the active chat -- **Avatar loading**: Async avatar image loading with fallback initials -- **Message filters**: Configurable filters for message types and categories - -```mermaid -flowchart LR - A["CometChatPanel"] --> B["Tab: My Groups"] - A --> C["Tab: Personal"] - A --> D["Tab: Browse Groups"] - B --> E["Chat View"] - C --> E - D --> E - - style A fill:#444,stroke:#888,color:#fff - style B fill:#333,stroke:#666,color:#fff - style C fill:#333,stroke:#666,color:#fff - style D fill:#333,stroke:#666,color:#fff - style E fill:#333,stroke:#666,color:#fff -``` - -### Adding to Viewport - - - -1. Create a Widget Blueprint with parent class **CometChat Panel** -2. Set the **App Id**, **Region**, **Auth Key**, and **User Uid** properties -3. Add to viewport via **Create Widget** → **Add to Viewport** - -Or use the `UCometChatButton` widget which manages the panel automatically. - - -```cpp -void AMyHUD::BeginPlay() -{ - Super::BeginPlay(); - - UCometChatPanel* Panel = CreateWidget(GetWorld()); - Panel->AppId = TEXT("YOUR_APP_ID"); - Panel->Region = TEXT("us"); - Panel->AuthKey = TEXT("YOUR_AUTH_KEY"); - Panel->UserUid = TEXT("cometchat-uid-1"); - Panel->AddToViewport(); -} -``` - - - -### Configuration Properties - -#### SDK Config - -| Property | Type | Default | Description | -| -------- | ---- | ------- | ----------- | -| `AppId` | `FString` | — | Your CometChat App ID | -| `Region` | `FString` | `"us"` | App region (`us` or `eu`) | -| `AuthKey` | `FString` | — | Your CometChat Auth Key | -| `UserUid` | `FString` | — | UID of the user to log in as | - -#### Panel Style - -| Property | Type | Default | Description | -| -------- | ---- | ------- | ----------- | -| `PanelWidth` | `float` | `650` | Panel width in pixels | -| `PanelHeight` | `float` | `450` | Panel height in pixels | -| `PanelBackground` | `FLinearColor` | Dark translucent | Background color | -| `AccentColor` | `FLinearColor` | Blue | Primary accent color | -| `TabActiveColor` | `FLinearColor` | Blue | Active tab color | -| `TabInactiveColor` | `FLinearColor` | Dark gray | Inactive tab color | -| `TextColor` | `FLinearColor` | Light gray | Primary text color | -| `SubTextColor` | `FLinearColor` | Medium gray | Secondary text color | -| `FontSize` | `float` | `12` | Base font size | -| `SmallFontSize` | `float` | `10` | Small text font size | -| `CornerRadius` | `float` | `8` | Border corner radius | -| `SidebarWidth` | `float` | `220` | Sidebar/list width | - -#### Message Style - -| Property | Type | Default | Description | -| -------- | ---- | ------- | ----------- | -| `ChatMessageFontSize` | `float` | `10` | Message text size | -| `ChatAvatarSize` | `float` | `24` | Avatar image size | -| `ChatUsernameOwnColor` | `FLinearColor` | Blue | Own username color | -| `ChatUsernameOtherColor` | `FLinearColor` | Gold | Other username color | - -#### Message Filters - -| Property | Type | Default | Description | -| -------- | ---- | ------- | ----------- | -| `bShowTextMessages` | `bool` | `true` | Show text messages | -| `bShowImageMessages` | `bool` | `true` | Show image messages | -| `bShowVideoMessages` | `bool` | `true` | Show video messages | -| `bShowAudioMessages` | `bool` | `true` | Show audio messages | -| `bShowFileMessages` | `bool` | `true` | Show file messages | -| `bShowCustomMessages` | `bool` | `true` | Show custom messages | -| `bShowActionMessages` | `bool` | `false` | Show action messages (join/leave) | -| `bShowCallMessages` | `bool` | `false` | Show call messages | -| `bHideBlockedUserMessages` | `bool` | `false` | Hide messages from blocked users | -| `bHideDeletedMessages` | `bool` | `true` | Hide deleted messages | -| `bHideReplies` | `bool` | `true` | Hide thread replies from main chat | -| `bOnlyWithAttachments` | `bool` | `false` | Only show messages with attachments | -| `bOnlyWithMentions` | `bool` | `false` | Only show messages with mentions | -| `bOnlyWithReactions` | `bool` | `false` | Only show messages with reactions | -| `bOnlyUnread` | `bool` | `false` | Only show unread messages | - ---- - -## UCometChatButton - -A floating circular button that toggles the `UCometChatPanel` open and closed. Add it to your HUD for a one-click chat experience. - -### Adding to Viewport - - - -1. Create a Widget Blueprint with parent class **CometChat Button** -2. Set the **App Id**, **Region**, **Auth Key**, and **User Uid** properties -3. Add to viewport — the button appears as a floating circle -4. Clicking the button opens/closes the full chat panel - - -```cpp -void AMyHUD::BeginPlay() -{ - Super::BeginPlay(); - - UCometChatButton* ChatButton = CreateWidget(GetWorld()); - ChatButton->AppId = TEXT("YOUR_APP_ID"); - ChatButton->Region = TEXT("us"); - ChatButton->AuthKey = TEXT("YOUR_AUTH_KEY"); - ChatButton->UserUid = TEXT("cometchat-uid-1"); - ChatButton->AddToViewport(); -} -``` - - - -### Configuration Properties - -| Property | Type | Default | Description | -| -------- | ---- | ------- | ----------- | -| `AppId` | `FString` | — | Your CometChat App ID | -| `Region` | `FString` | `"us"` | App region | -| `AuthKey` | `FString` | — | Your CometChat Auth Key | -| `UserUid` | `FString` | — | UID of the user to log in as | -| `ButtonSize` | `float` | `56` | Button diameter in pixels | -| `ButtonColor` | `FLinearColor` | Blue | Button background color | -| `ButtonHoverColor` | `FLinearColor` | Light blue | Button hover color | -| `IconColor` | `FLinearColor` | White | Icon/text color | -| `IconFontSize` | `float` | `20` | Icon font size | - -### Blueprint Methods - -| Method | Returns | Description | -| ------ | ------- | ----------- | -| `ToggleChatPanel()` | `void` | Open or close the chat panel programmatically | -| `IsPanelOpen()` | `bool` | Check if the panel is currently visible | - ---- - -## Usage Pattern - -The typical setup is to add `UCometChatButton` to your HUD. It handles creating and managing the `UCometChatPanel` internally: - -```mermaid -flowchart TD - A["Add CometChatButton to Viewport"] --> B["User clicks button"] - B --> C["Panel opens (auto-login + fetch data)"] - C --> D["User chats"] - D --> E["User clicks button again"] - E --> F["Panel closes"] - - style A fill:#333,stroke:#666,color:#fff - style B fill:#444,stroke:#888,color:#fff - style C fill:#333,stroke:#666,color:#fff - style D fill:#333,stroke:#666,color:#fff - style E fill:#444,stroke:#888,color:#fff - style F fill:#333,stroke:#666,color:#fff -``` - - -The panel handles SDK initialization, login, and data fetching automatically when opened. You only need to provide the configuration properties. - - ---- - -## Next Steps - - - - Install the plugin and configure your project. - - - Fine-tune SDK settings and connection management. - - diff --git a/sdk/unreal/users.mdx b/sdk/unreal/users.mdx index d8a73e874..8e5c97639 100644 --- a/sdk/unreal/users.mdx +++ b/sdk/unreal/users.mdx @@ -1,18 +1,18 @@ --- title: "Users" -description: "Fetch user profiles and track user presence in real time." +description: "Fetch, search, update, and block users, track presence, and manage accounts in your Unreal Engine game." +--- + +The CometChat Unreal SDK exposes async nodes for every user operation: fetching a single profile, searching the full user list, updating the logged-in user's own profile, blocking and unblocking, counting who's online, and — from a trusted context — creating and updating accounts with your REST API key. Presence updates arrive in real time through the subsystem. + --- ## Get User -Retrieve a user's profile by their UID. +Retrieve a single user's profile by their UID. - - - - Call the **Get User Async** node. | Parameter | Type | Description | @@ -41,9 +41,37 @@ void AMyActor::OnUserFetched(const FCometChatUser& User) UE_LOG(LogTemp, Log, TEXT("User: %s (%s)"), *User.Name, *User.Status); } -void AMyActor::OnUserFetchFailed(const FString& Error) +void AMyActor::OnUserFetchFailed(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("Fetch user failed: %s"), *Error.Message); +} +``` + + + +**Handle lookup failures.** A missing or deactivated UID comes back on **On Failure** as `ERR_UID_NOT_FOUND`. Branch on `Error.Code` so a stale friend entry shows a clear message instead of a generic error. + + + +Wire **Get User Async → On Failure** into a **Switch on String** on `Error.Code`: + +1. `ERR_UID_NOT_FOUND` → the account doesn't exist or is deactivated; remove the stale entry from your friends list. +2. **Default** → show `Error.Message`; transient network failures land here and are safe to retry. + + +```cpp +void AMyActor::OnUserFetchFailed(const FCometChatError& Error) { - UE_LOG(LogTemp, Error, TEXT("Fetch user failed: %s"), *Error); + if (Error.Code == TEXT("ERR_UID_NOT_FOUND")) + { + // Account removed or deactivated — drop the stale friend entry. + RemoveFriendEntry(); + } + else + { + // Network or unexpected error — safe to retry. + UE_LOG(LogTemp, Error, TEXT("Fetch user failed [%s]: %s"), *Error.Code, *Error.Message); + } } ``` @@ -56,7 +84,279 @@ void AMyActor::OnUserFetchFailed(const FString& Error) | `Uid` | `FString` | Unique user identifier | | `Name` | `FString` | Display name | | `AvatarUrl` | `FString` | URL to the user's avatar image | -| `Status` | `FString` | Current status text | +| `Status` | `FString` | Presence status text (`online` or `offline`) | +| `Role` | `FString` | Role assigned to the user | +| `Metadata` | `FString` | Custom JSON metadata attached to the user | +| `Link` | `FString` | Deep link / profile URL | +| `StatusMessage` | `FString` | Custom status or bio text | +| `LastActiveAt` | `int64` | Unix timestamp of the user's last activity | +| `bHasBlockedMe` | `bool` | `true` if this user has blocked the logged-in user | +| `bBlockedByMe` | `bool` | `true` if the logged-in user has blocked this user | +| `Tags` | `TArray` | Tags associated with the user | +| `DeactivatedAt` | `int64` | Unix timestamp when the account was deactivated (`0` if active) | + +--- + +## Fetch Users + +Retrieve a paginated, filterable list of users — the basis for a friends list, a "start new chat" search, or a lobby roster. + + + + + + + +Call the **Fetch Users Async** node with an `FCometChatUsersRequest` struct. Set `SearchKeyword` to filter by name/UID, `bFriendsOnly = true` to list only the user's friends, or `bHideBlockedUsers = true` to omit users you've blocked. + +**On Success** returns a `TArray`. + +**Use case — friend search.** As the player types in a "Find friends" box, search the directory and show matching profiles with their online status. + +**Blueprint flow** + +1. **Make FCometChatUsersRequest** — set `SearchKeyword` from the text box and `bHideBlockedUsers` = `true`. +2. **Fetch Users Async** — pass the struct. +3. **On Success** (returns `Users`) → build result rows; read each `Status` (or the presence events below) for the online dot. +4. **On Failure** (returns `Error`) → show `Error.Message` and keep the last results on screen so the search box stays usable. + + +```cpp +#include "AsyncActions/CometChatFetchUsersAction.h" + +void AMyActor::SearchUsers(const FString& Keyword) +{ + FCometChatUsersRequest Request; + Request.Limit = 30; + Request.SearchKeyword = Keyword; + Request.bFriendsOnly = false; // set true to only list the user's friends + Request.bHideBlockedUsers = true; // omit users you've blocked + + auto* Action = UCometChatFetchUsersAction::FetchUsersAsync(this, Request); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnUsersFetched); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnUsersFetched(const TArray& Users) +{ + for (const FCometChatUser& User : Users) + { + UE_LOG(LogTemp, Log, TEXT("%s — %s"), *User.Name, *User.Status); + } +} + +void AMyActor::OnError(const FCometChatError& Error) +{ + UE_LOG(LogTemp, Error, TEXT("Request failed: %s"), *Error.Message); +} +``` + + + +### FCometChatUsersRequest + +| Property | Type | Default | Description | +| -------- | ---- | ------- | ----------- | +| `Limit` | `int32` | `30` | Max users to return per page | +| `SearchKeyword` | `FString` | — | Filter users by name/UID substring | +| `SearchIn` | `TArray` | — | Fields to search within (e.g. `name`, `uid`) | +| `UserStatus` | `FString` | — | Filter by status: `online` or `offline` | +| `bHideBlockedUsers` | `bool` | `false` | Exclude users the logged-in user has blocked | +| `bFriendsOnly` | `bool` | `false` | Only return the logged-in user's friends | +| `Roles` | `TArray` | — | Filter by one or more roles | +| `Tags` | `TArray` | — | Filter by user tags | +| `bWithTags` | `bool` | `false` | Include each user's tags in the response | +| `UIDs` | `TArray` | — | Fetch only these specific UIDs | +| `SortBy` | `FString` | — | Field to sort by (e.g. `name`) | +| `SortOrder` | `FString` | — | `asc` or `desc` | +| `Page` | `int32` | `0` | Page number for pagination | + + +**Pagination**: increment `Page` to fetch subsequent pages. If the returned array count is less than `Limit`, you've reached the end of the list. + + +--- + +## Update Current User + +Update the **logged-in user's own profile** — display name, avatar, status message, metadata, and tags. This runs in the user's own authenticated session and needs **no API key**. + + + +Call the **Update Current User Async** node with an `FCometChatUser` struct describing the changes. The `Uid` must be the logged-in user's own UID. + +**On Success** returns the updated `FCometChatUser`. + + +```cpp +#include "AsyncActions/CometChatUpdateCurrentUserAction.h" + +void AMyActor::UpdateMyProfile() +{ + FCometChatUser Me; + Me.Uid = TEXT("cometchat-uid-1"); // must be the logged-in user's UID + Me.Name = TEXT("Sir Lancelot"); + Me.AvatarUrl = TEXT("https://example.com/avatars/lancelot.png"); + Me.StatusMessage = TEXT("Questing for the Grail"); + Me.Tags.Add(TEXT("knight")); + + auto* Action = UCometChatUpdateCurrentUserAction::UpdateCurrentUserAsync(this, Me); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnProfileUpdated); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnProfileUpdated(const FCometChatUser& Result) +{ + UE_LOG(LogTemp, Log, TEXT("Profile updated: %s"), *Result.Name); +} +``` + + + + +Only fields you set are changed. To edit another user's profile — or create accounts — you need admin privileges and your REST API key; see [User Management (Admin)](#user-management-admin) below. + + + +**Best practice — short-lived Auth Tokens.** Log players in with short-lived [Auth Tokens](/sdk/unreal/authentication#login-using-auth-token) minted by your server, and let them edit their own profile through **Update Current User** (no API key). Reserve the API-key **Create User** / **Update User** calls for trusted, server-side contexts. + + +--- + +## Blocking Users + +Block one or more users to stop receiving their messages, unblock them later, and list who is currently blocked. All three take effect for the logged-in user only. + +### Block / Unblock + + + +Call the **Block Users Async** or **Unblock Users Async** node with a `TArray` of UIDs. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| UIDs | `TArray` | The UIDs to block or unblock | + +**On Success** returns an `FString` result summary. + +**Use case — block from a profile.** A player opens another user's profile and taps *Block*; their messages stop arriving and their content can be hidden. + +**Blueprint flow** + +1. **Block Users Async** — pass a one-element array with the target `Uid`. +2. **On Success** → set the user's `bBlockedByMe` locally, hide their content, and swap the button to *Unblock*. +3. **On Failure** (returns `Error`) → show `Error.Message` and leave the button unchanged so the player can retry. + + +```cpp +#include "AsyncActions/CometChatBlockUsersAction.h" +#include "AsyncActions/CometChatUnblockUsersAction.h" + +void AMyActor::BlockUser(const FString& Uid) +{ + auto* Action = UCometChatBlockUsersAction::BlockUsersAsync(this, { Uid }); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnUsersBlocked); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::UnblockUser(const FString& Uid) +{ + auto* Action = UCometChatUnblockUsersAction::UnblockUsersAsync(this, { Uid }); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnUsersBlocked); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnUsersBlocked(const FString& Result) +{ + UE_LOG(LogTemp, Log, TEXT("Block/unblock result: %s"), *Result); +} +``` + + + +### Fetch Blocked Users + + + +Call the **Fetch Blocked Users Async** node with an `FCometChatBlockedUsersRequest` struct. Set `Direction` to `blockedByMe` (users you blocked) or `hasBlockedMe` (users who blocked you). + +**On Success** returns a `TArray`. + + +```cpp +#include "AsyncActions/CometChatFetchBlockedUsersAction.h" + +void AMyActor::FetchBlocked() +{ + FCometChatBlockedUsersRequest Request; + Request.Limit = 30; + Request.Direction = TEXT("blockedByMe"); // or "hasBlockedMe" + + auto* Action = UCometChatFetchBlockedUsersAction::FetchBlockedUsersAsync(this, Request); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnBlockedFetched); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnBlockedFetched(const TArray& Users) +{ + for (const FCometChatUser& User : Users) + { + UE_LOG(LogTemp, Log, TEXT("Blocked: %s"), *User.Name); + } +} +``` + + + +### FCometChatBlockedUsersRequest + +| Property | Type | Default | Description | +| -------- | ---- | ------- | ----------- | +| `Limit` | `int32` | `30` | Max users to return per page | +| `SearchKeyword` | `FString` | — | Filter blocked users by name/UID | +| `Direction` | `FString` | — | `blockedByMe` or `hasBlockedMe` | +| `Page` | `int32` | `0` | Page number for pagination | + + +You don't always need a fetch to know block state: every `FCometChatUser` already carries `bBlockedByMe` (you blocked them) and `bHasBlockedMe` (they blocked you). Read those flags on any user you already have to drive block/unblock buttons and to hide their content. + + +--- + +## Get Online User Count + +Fetch the number of users currently online in your app — handy for a lobby header or presence badge. + + + +Call the **Get Online User Count Async** node. It takes no parameters. + +**On Success** returns an `int32` count. + + +```cpp +#include "AsyncActions/CometChatGetOnlineUserCountAction.h" + +void AMyActor::RefreshOnlineCount() +{ + auto* Action = UCometChatGetOnlineUserCountAction::GetOnlineUserCountAsync(this); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnOnlineCount); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnOnlineCount(int32 Count) +{ + UE_LOG(LogTemp, Log, TEXT("%d users online"), Count); +} +``` + + --- @@ -64,6 +364,13 @@ void AMyActor::OnUserFetchFailed(const FString& Error) Track when users come online, go offline, or go away by binding to the `OnPresenceChanged` delegate on the Subsystem. +**Use case — live status dots.** Keep the green/gray dots next to player names in a friends list or lobby roster updating in real time, without re-fetching users. + +You have two listener styles — bind whichever fits your UI: + +- **On Presence Changed** (`OnPresenceChanged`) delivers a compact `FCometChatPresence` (`Uid`, `Status`, `LastActiveAt`) — ideal for flipping a status dot. +- **On User Online** / **On User Offline** (`OnUserOnline` / `OnUserOffline`) each deliver the full `FCometChatUser`, handy when you also want the latest name or avatar. See [Real-Time Events](/sdk/unreal/real-time-events#userlistener-events). + 1. Get a reference to the **CometChat Subsystem** @@ -122,6 +429,62 @@ Use presence data to show green/gray status dots next to player names in your fr --- +## User Management (Admin) + +Create new accounts and update **any** user's profile from a trusted context. Unlike Update Current User, these operate with admin authority and require your CometChat REST **API Key**. + + + +Call the **Create User Async** or **Update User Async** node with an `FCometChatUser` struct and your REST `Api Key`. + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| User | `FCometChatUser` | The account to create or the profile changes to apply | +| Api Key | `FString` | Your CometChat REST API key (admin credential) | + +**On Success** returns the resulting `FCometChatUser`. + + +```cpp +#include "AsyncActions/CometChatCreateUserAction.h" +#include "AsyncActions/CometChatUpdateUserAction.h" + +void AMyActor::CreatePlayerAccount() +{ + FCometChatUser NewUser; + NewUser.Uid = TEXT("player-9001"); + NewUser.Name = TEXT("New Challenger"); + + auto* Action = UCometChatCreateUserAction::CreateUserAsync( + this, + NewUser, + TEXT("YOUR_REST_API_KEY") // keep this on a trusted/server build only + ); + Action->OnSuccess.AddDynamic(this, &AMyActor::OnUserCreated); + Action->OnFailure.AddDynamic(this, &AMyActor::OnError); + Action->Activate(); +} + +void AMyActor::OnUserCreated(const FCometChatUser& Result) +{ + UE_LOG(LogTemp, Log, TEXT("Created user %s"), *Result.Uid); +} +``` + +**Update User** has the identical shape — call `UCometChatUpdateUserAction::UpdateUserAsync(this, User, TEXT("YOUR_REST_API_KEY"))` with the fields you want to change. + + + + +**Your API key is a secret.** Create User and Update User require your CometChat REST **API Key**, which grants full admin access to your entire app. Only call these from a trusted or server-side context — **never ship the API key inside a distributed game client**, where it can be extracted from the packaged build and abused. For a player editing their own profile, use [Update Current User](#update-current-user) above, which needs no API key. + + + +**Bad practice — admin calls from the client.** Never hard-code your REST **API Key** in Blueprints, `.ini` files, or C++ that ships in the packaged game — anything in the client can be extracted and abused. Run **Create User** / **Update User** (and any other API-key call) from your own backend, and have the game reach them through your server instead of embedding the key. + + +--- + ## Next Steps @@ -129,6 +492,6 @@ Use presence data to show green/gray status dots next to player names in your fr Create, join, and manage groups. - All five real-time delegates in one place. + All the real-time delegates, including presence, in one place.