diff --git a/docs.json b/docs.json index 4e87f68d2..713a8f1a8 100644 --- a/docs.json +++ b/docs.json @@ -2698,6 +2698,7 @@ "group": "Message Bubbles", "pages": [ "ui-kit/angular/components/cometchat-text-bubble", + "ui-kit/angular/components/cometchat-card-bubble", "ui-kit/angular/components/cometchat-image-bubble", "ui-kit/angular/components/cometchat-video-bubble", "ui-kit/angular/components/cometchat-audio-bubble", @@ -2764,6 +2765,7 @@ "ui-kit/angular/guides/search-messages", "ui-kit/angular/guides/rich-text-formatting", "ui-kit/angular/guides/custom-message-types", + "ui-kit/angular/guides/card-messages", "ui-kit/angular/guides/custom-text-formatter", "ui-kit/angular/guides/mentions-formatter", "ui-kit/angular/guides/shortcut-formatter", diff --git a/ui-kit/angular/components/cometchat-card-bubble.mdx b/ui-kit/angular/components/cometchat-card-bubble.mdx new file mode 100644 index 000000000..3b1da5a46 --- /dev/null +++ b/ui-kit/angular/components/cometchat-card-bubble.mdx @@ -0,0 +1,238 @@ +--- +title: "Card Bubble" +description: "Angular component that renders a developer Card Message inside a message bubble using the prebuilt CometChat Cards renderer, with pure-forwarded card actions." +--- + +The `CometChatCardBubble` component renders a **developer Card Message** (`message.category === "card"`) as a card bubble inside a conversation. It is the card equivalent of [`CometChatTextBubble`](/ui-kit/angular/components/cometchat-text-bubble): the surrounding [`CometChatMessageBubble`](/ui-kit/angular/components/cometchat-message-bubble) wrapper supplies the container, receipts, reactions, long-press options, reply and thread view — this component only replaces the **content view** with the rendered card. + +## Overview + +The UIKit is a **render-only** consumer of cards: it draws cards delivered by the SDK and forwards card actions to your app. It never parses or mutates the card body, and never sends or creates cards. When a card message arrives, the UIKit routes it to this bubble automatically. + +The component follows a strict **render-only contract**: + +- **Render-only** — the raw card payload from `message.getCard()` is serialized verbatim and handed to the prebuilt `CometChatCardView` renderer (from [`@cometchat/cards-angular`](https://www.npmjs.com/package/@cometchat/cards-angular)) as a `cardJson` string. The UI Kit performs **zero transformation** of the payload. +- **No behavior** — the bubble runs no action logic of its own. When a user taps an interactive element, the renderer's raw action is **pure-forwarded** on two channels (see [Card Actions](#card-actions)); your app owns all behavior. +- **Graceful fallback** — when the payload is empty or invalid, a single-line fallback text is shown instead of an empty bubble (see [Fallback Behavior](#fallback-behavior)). + + + Card rendering (layout, theming, interactive elements) is owned by the prebuilt `@cometchat/cards-angular` renderer library, **not** by the UI Kit. The UI Kit is responsible only for delivering the payload to the renderer and forwarding actions back out. + + +## Automatic Rendering + +In the standard chat flow you do **not** instantiate this component yourself. [`CometChatMessageList`](/ui-kit/angular/components/cometchat-message-list) and `CometChatMessageBubble` route any message with category `"card"` to `CometChatCardBubble` automatically, keyed by **category** (the developer `type` is arbitrary). To handle card actions in this flow, subscribe to the [`ccCardActionClicked`](#card-actions) event bus — no component wiring is required. + +Use the component directly only when you are building a fully custom message renderer. + +## Basic Usage + +```typescript expandable +import { Component } from '@angular/core'; +import { CometChat } from '@cometchat/chat-sdk-javascript'; +import { + CometChatCardBubbleComponent, + MessageBubbleAlignment, +} from '@cometchat/chat-uikit-angular'; + +@Component({ + selector: 'app-card-message', + standalone: true, + imports: [CometChatCardBubbleComponent], + template: ` + + `, +}) +export class CardMessageComponent { + cardMessage!: CometChat.CardMessage; + MessageBubbleAlignment = MessageBubbleAlignment; +} +``` + +### Incoming vs Outgoing Messages + +```typescript expandable +import { Component } from '@angular/core'; +import { CometChat } from '@cometchat/chat-sdk-javascript'; +import { + CometChatCardBubbleComponent, + MessageBubbleAlignment, +} from '@cometchat/chat-uikit-angular'; + +@Component({ + selector: 'app-card-list', + standalone: true, + imports: [CometChatCardBubbleComponent], + template: ` + + + + + + `, +}) +export class CardListComponent { + incomingCard!: CometChat.CardMessage; + outgoingCard!: CometChat.CardMessage; + MessageBubbleAlignment = MessageBubbleAlignment; +} +``` + +## Properties + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `message` | `CometChat.CardMessage` | **required** | The card message to render. The raw payload is read from `message.getCard()`. | +| `alignment` | `MessageBubbleAlignment` | `MessageBubbleAlignment.left` | Bubble alignment. `left` for incoming/receiver messages, `right` for outgoing/sender messages. Kept for parity with the text bubble. | +| `themeMode` | `CometChatCardThemeMode` | `'auto'` | Renderer theme mode. One of `'auto'`, `'light'`, or `'dark'`. Passed straight to `CometChatCardView`. | +| `themeOverride` | `CometChatCardThemeOverride` | `undefined` | Optional renderer theme overrides (colors, spacing, typography for the rendered card). Passed straight to `CometChatCardView`. | + + + `CometChatCardThemeMode` and `CometChatCardThemeOverride` are exported by `@cometchat/cards-angular`. Type these inputs precisely — passing `unknown` will fail strict-template AOT compilation. + + +## Events + +| Event | Payload Type | Description | +|-------|-------------|-------------| +| `onCardAction` | `CardBubbleAction` | Emitted when a user taps an interactive element on the card. Carries the raw renderer action. | + +```typescript +/** Payload emitted by the bubble's onCardAction output. */ +export interface CardBubbleAction { + message: CometChat.CardMessage; // the card message the action originated from + action: unknown; // the renderer's raw discriminated action +} +``` + +## Card Actions + +The Cards renderer is a **pure renderer** — it emits actions through callbacks without executing them. The bubble forwards each action on **both** channels and runs no behavior of its own: + +1. **`(onCardAction)` output** — for apps that render this bubble directly. +2. **`CometChatMessageEvents.ccCardActionClicked` event bus** — for the standard, internally rendered flow where the bubble is created by the UI Kit. This is the recommended channel for the default message list. + + + Act on **one** channel only to avoid double-handling the same tap. In the standard `CometChatMessageList` flow, use the `ccCardActionClicked` bus. + + +### Handling actions via the output + +```typescript expandable +import { Component } from '@angular/core'; +import { CometChat } from '@cometchat/chat-sdk-javascript'; +import { + CometChatCardBubbleComponent, + CardBubbleAction, +} from '@cometchat/chat-uikit-angular'; + +@Component({ + selector: 'app-card-message', + standalone: true, + imports: [CometChatCardBubbleComponent], + template: ` + + `, +}) +export class CardMessageComponent { + cardMessage!: CometChat.CardMessage; + + onCardAction(event: CardBubbleAction): void { + // event.action is the raw renderer action (a CometChatCardAction). + // Your app implements the behavior — the UI Kit performs none. + console.log('Card action on message', event.message.getId(), event.action); + } +} +``` + +### Handling actions via the event bus + +```typescript expandable +import { Injectable, DestroyRef, inject } from '@angular/core'; +import { + CometChatMessageEvents, + ICardActionEvent, +} from '@cometchat/chat-uikit-angular'; +import type { CometChatCardAction } from '@cometchat/cards-angular'; + +@Injectable({ providedIn: 'root' }) +export class CardActionHandler { + private readonly destroyRef = inject(DestroyRef); + + start(): void { + // Subscribe once; auto-unsubscribes when the injector is destroyed. + CometChatMessageEvents.subscribeOnCardActionClicked((event: ICardActionEvent) => { + const action = event.action as CometChatCardAction; + switch (action.type) { + case 'openUrl': + window.open(action.url, '_blank', 'noopener,noreferrer'); + break; + case 'copyToClipboard': + navigator.clipboard?.writeText(action.value); + break; + // ...handle the remaining action types + } + }, this.destroyRef); + } +} +``` + +The full set of action types and a complete reference handler are covered in the [Card Messages guide](/ui-kit/angular/guides/card-messages#handling-card-actions). + +## Fallback Behavior + +When `getCard()` returns nothing drawable (`null`, a blank string, or an empty object), the bubble renders a single line of fallback text instead of an empty card. The fallback is resolved in this order: + +1. `message.getFallbackText()` +2. `message.getText()` +3. Localized `"Card Message"` (the `card_message` localization key) + +This keeps the conversation readable even if a card payload is malformed or a client cannot render it. + +## Customization + +The card's internal layout, colors, and typography are controlled by the renderer through [`themeMode`](#properties) and [`themeOverride`](#properties). The bubble wrapper and fallback line are styled with CSS variables: + +```css expandable +cometchat-card-bubble { + /* Spacing around the card content */ + --cometchat-spacing-2: 8px; + --cometchat-spacing-3: 12px; + + /* Fallback text */ + --cometchat-font-body-regular: 400 14px 'Inter'; + --cometchat-text-color-secondary: #666666; + + /* Bubble surface (incoming) */ + --cometchat-background-color-02: #F5F5F5; + --cometchat-radius-3: 12px; +} +``` + +To restyle the rendered card itself (button colors, header, body), pass a `themeOverride` to the bubble rather than overriding CSS — the card DOM is owned by the renderer. + +## Technical Details + +- **Standalone Component** — import and use independently. +- **Change Detection** — `OnPush` for optimal performance; uses Angular signals for the card payload and fallback. +- **Renderer dependency** — `CometChatCardViewComponent` from `@cometchat/cards-angular`. +- **Callback before schema** — the renderer's `(onAction)` output is bound before `[cardJson]` is assigned, so the action callback is registered before the card schema is rendered. + +## Related + +- **[Card Messages guide](/ui-kit/angular/guides/card-messages)** — the full card rendering implementation: developer cards, agent cards, streaming cards, and the complete action vocabulary. +- **[CometChatMessageBubble](/ui-kit/angular/components/cometchat-message-bubble)** — routes card messages to this bubble. +- **[CometChatMessageList](/ui-kit/angular/components/cometchat-message-list)** — renders card bubbles in the conversation. +- **[Events](/ui-kit/angular/events)** — the `ccCardActionClicked` event reference. diff --git a/ui-kit/angular/events.mdx b/ui-kit/angular/events.mdx index 6475955f7..95062e838 100644 --- a/ui-kit/angular/events.mdx +++ b/ui-kit/angular/events.mdx @@ -54,6 +54,7 @@ Events provide decoupled communication between UIKit components using a publish/ | **ccMessageDeleted** | Triggered when the user successfully deletes a message. | | **ccMessageRead** | Triggered when the sent message is read by the receiver. | | **ccLiveReaction** | Triggered when the user sends a live reaction. | +| **ccCardActionClicked** | Triggered when a user taps an interactive element on a card (developer card or nested agent card). Carries an `ICardActionEvent` with the owning `message` (or `null` for a streaming card), the raw renderer `action`, and the originating `elementId`/`cardJson`. The UIKit forwards the action untouched and runs no behavior. See the [Card Messages guide](/ui-kit/angular/guides/card-messages#handling-card-actions). | ### SDK Listener Events @@ -69,6 +70,7 @@ Events provide decoupled communication between UIKit components using a publish/ | **onMessageEdited** | Emitted when the CometChat SDK listener indicates that a message has been edited. | | **onMessageDeleted** | Emitted when the CometChat SDK listener indicates that a message has been deleted. | | **onTransientMessageReceived** | Emitted when the CometChat SDK listener receives a transient message. | +| **onCardMessageReceived** | Emitted when the CometChat SDK listener receives a developer card message (`category: "card"`). Carries a `CometChat.CardMessage`. The UIKit renders cards but never sends or creates them. | ## CometChatCallEvents diff --git a/ui-kit/angular/guides/card-messages.mdx b/ui-kit/angular/guides/card-messages.mdx new file mode 100644 index 000000000..5ef5a1f09 --- /dev/null +++ b/ui-kit/angular/guides/card-messages.mdx @@ -0,0 +1,241 @@ +--- +title: "Card Messages" +description: "How the Angular UIKit renders Card Messages — developer cards, persisted agent cards, and streaming agent cards — and how to handle card actions." +--- + + + +| Field | Value | +| --- | --- | +| Package | `@cometchat/chat-uikit-angular` | +| Renderer | `@cometchat/cards-angular` (`CometChatCardView`) | +| Components | `CometChatCardBubble`, `CometChatAIAssistantMessageBubble`, `CometChatStreamMessageBubble` | +| Key event | `CometChatMessageEvents.ccCardActionClicked` | +| Required setup | `CometChatUIKit.init(uiKitSettings)` then `CometChatUIKit.login("UID")` | +| Purpose | Render structured, interactive card payloads inside conversations and forward card actions to your app | +| Related | [Card Bubble](/ui-kit/angular/components/cometchat-card-bubble) \| [AI Assistant Chat](/ui-kit/angular/components/cometchat-ai-assistant-chat) \| [Events](/ui-kit/angular/events) | + + + +Card Messages are structured, interactive cards delivered inside conversations. The Angular UIKit renders them through the prebuilt [`@cometchat/cards-angular`](https://www.npmjs.com/package/@cometchat/cards-angular) renderer and forwards every card action back to your application. + + + The UIKit is a **render-only** consumer of cards: it draws cards delivered by the SDK and forwards their actions to your app. It never parses or mutates the card body, and never sends or creates cards. + + +## The render-only contract + +The UIKit is a **render-only** consumer of cards. For every card surface, the rules are identical: + +1. **Pass the payload through unchanged.** The raw card payload is read from the SDK, serialized verbatim (`JSON.stringify`), and handed to `CometChatCardView` as a `cardJson` string. The UIKit performs **no transformation**. +2. **Forward actions, run no behavior.** The renderer emits actions through callbacks. The UIKit forwards them on the `ccCardActionClicked` event bus. Your app implements **all** behavior (open a URL, start a chat, call an API, …). +3. **Fall back gracefully.** When a payload is empty or invalid, a single fallback line is shown instead of a broken card. + +This contract is centralized in a small set of helpers so the developer bubble, the agent bubble, and the streaming bubble all treat payloads identically. + +## Three delivery paths + +A card can reach the conversation in three ways. The UIKit routes each one to the correct renderer automatically. + +| Path | Source | Component | Routed by | +| --- | --- | --- | --- | +| **Developer card** | `CardMessage` with `category: "card"` | [`CometChatCardBubble`](/ui-kit/angular/components/cometchat-card-bubble) | message **category** | +| **Persisted agent card** | `AIAssistantMessage` content block | `CometChatAIAssistantMessageBubble` | element `type: "card"` | +| **Streaming agent card** | Live `card_start` / `card` / `card_end` events | `CometChatStreamMessageBubble` | stream event type | + +### Developer cards + +A message with category `"card"` is routed to `CometChatCardBubble` by [`CometChatMessageBubble`](/ui-kit/angular/components/cometchat-message-bubble), keyed on **category** (the developer `type` is arbitrary). The bubble renders `message.getCard()` and forwards taps on the `ccCardActionClicked` bus. + +In the conversation list, a card message's preview is its `getText()` if present, otherwise the localized `"Card Message"` label. + +To react to incoming developer cards in real time, listen on the message bus: + +```typescript expandable +import { Injectable, DestroyRef, inject } from '@angular/core'; +import { CometChat } from '@cometchat/chat-sdk-javascript'; +import { CometChatMessageEvents } from '@cometchat/chat-uikit-angular'; + +@Injectable({ providedIn: 'root' }) +export class CardListener { + private readonly destroyRef = inject(DestroyRef); + + start(): void { + CometChatMessageEvents.subscribeOnCardMessageReceived( + (card: CometChat.CardMessage) => { + console.log('Card received:', card.getId(), card.getCard()); + }, + this.destroyRef + ); + } +} +``` + +See the [Card Bubble](/ui-kit/angular/components/cometchat-card-bubble) component reference for inputs, events, and theming. + +### Persisted agent cards + +After an AI agent run completes, the persisted `AIAssistantMessage` exposes its content as an **ordered list of blocks** via `getElements()`. `CometChatAIAssistantMessageBubble` renders them in order: + +- a `text` block renders as Markdown (via [`CometChatMarkdownRenderer`](/ui-kit/angular/components/cometchat-markdown-renderer)); +- a `card` block renders through `CometChatCardView`, using the same render-only path as developer cards. + +When a message has no elements (older messages), the bubble falls back to `getText()`. No additional wiring is required — the [AI Assistant Chat](/ui-kit/angular/components/cometchat-ai-assistant-chat) flow instantiates this bubble for you. Taps on a nested agent card are forwarded on the same `ccCardActionClicked` bus. + +### Streaming agent cards + +While an agent run is streaming, cards arrive progressively and are rendered by `CometChatStreamMessageBubble`, which subscribes to the streaming service's `messageStream$`: + +| Stream event | Behavior | +| --- | --- | +| `card_start` | Shows an in-place loader labeled with the event's `executionText`, keyed by `cardId`. | +| `card` | Replaces the loader (correlated by `cardId`) with the rendered card. | +| `card_end` | No-op — the run-complete persisted `AIAssistantMessage` replaces the streamed bubble. | + +Because no persisted message exists yet during streaming, a tap on a streaming card is forwarded with `message: null` on the `ccCardActionClicked` bus; the persisted bubble that follows is the source of truth. + +## Handling card actions + +The Cards renderer emits actions but never executes them. The UIKit forwards each action **untouched** on `CometChatMessageEvents.ccCardActionClicked`. Subscribe **once** at app startup and dispatch by action type. + +### The event payload + +```typescript +export interface ICardActionEvent { + /** + * The owning message — CardMessage (developer) or AIAssistantMessage + * (persisted agent card). null only for a card tapped while the agent run + * is still streaming. + */ + message: CometChat.BaseMessage | null; + /** The renderer's raw discriminated action (CometChatCardAction). */ + action: unknown; + /** The renderer element id that emitted the action (used by customCallback). */ + elementId?: string; + /** The raw card JSON the action originated from (used by customCallback). */ + cardJson?: string; +} +``` + +### Action types + +`action` is a `CometChatCardAction` — a discriminated union (from `@cometchat/cards-angular`) narrowed by its `type`. The UIKit forwards all of them; your app decides what each one does: + +| `type` | Intended behavior | +| --- | --- | +| `openUrl` | Open a URL (in a new tab or a webview). | +| `copyToClipboard` | Copy a value to the clipboard. | +| `downloadFile` | Download a file. | +| `sendMessage` | Send a text message to a user/group (or the current conversation). | +| `apiCall` | Make an HTTP request. | +| `chatWithUser` | Open a one-to-one chat with a user. | +| `chatWithGroup` | Open a group chat. | +| `initiateCall` | Start an audio/video call. | +| `customCallback` | Invoke an app-defined handler, keyed by `callbackId`. | + +### Reference handler + +Subscribe once (a root-provided service is the natural home) and dispatch by `type`. The snippet below mirrors the sample app's reference implementation: + +```typescript expandable +import { Injectable, DestroyRef, inject } from '@angular/core'; +import { CometChat } from '@cometchat/chat-sdk-javascript'; +import { + CometChatMessageEvents, + ICardActionEvent, +} from '@cometchat/chat-uikit-angular'; +import type { CometChatCardAction } from '@cometchat/cards-angular'; + +/** Narrows the discriminated card action union to one variant by its type. */ +type Action = Extract; + +@Injectable({ providedIn: 'root' }) +export class CardActionService { + private readonly destroyRef = inject(DestroyRef); + private started = false; + + /** Subscribe once. Idempotent; auto-unsubscribes when the injector is destroyed. */ + start(): void { + if (this.started) return; + this.started = true; + CometChatMessageEvents.subscribeOnCardActionClicked( + (event) => this.dispatch(event), + this.destroyRef + ); + } + + private dispatch(event: ICardActionEvent): void { + const action = event.action as CometChatCardAction | null | undefined; + if (!action || typeof action !== 'object') return; + + switch (action.type) { + case 'openUrl': + this.openUrl(action); + break; + case 'copyToClipboard': + this.copyToClipboard(action); + break; + case 'chatWithUser': + void this.chatWithUser(action); + break; + // ...handle downloadFile, sendMessage, apiCall, chatWithGroup, + // initiateCall, and customCallback the same way. + default: + break; // Unknown action — the renderer owns the action vocabulary. + } + } + + private openUrl(a: Action<'openUrl'>): void { + if (!a.url) return; + const target = a.openIn === 'webview' ? '_self' : '_blank'; + window.open(a.url, target, 'noopener,noreferrer'); + } + + private copyToClipboard(a: Action<'copyToClipboard'>): void { + if (a.value == null) return; + navigator.clipboard?.writeText(a.value); + } + + private async chatWithUser(a: Action<'chatWithUser'>): Promise { + if (!a.uid) return; + const user = await CometChat.getUser(a.uid); + // Navigate your app to the conversation with `user`. + } +} +``` + +Start the service once your shell is ready — for example from your home component's `ngOnInit`: + +```typescript +export class HomeComponent implements OnInit { + constructor(private cardActions: CardActionService) {} + + ngOnInit(): void { + this.cardActions.start(); + } +} +``` + + + Each tap is forwarded on **one** bus to all subscribers. Subscribe in a single place (a root service) so an action runs exactly once. If you also bind the [`(onCardAction)` output](/ui-kit/angular/components/cometchat-card-bubble#card-actions) on a directly-rendered card bubble, handle the action on only one of the two channels. + + +### `customCallback` actions + +A `customCallback` action carries a `callbackId` (and optional `payload`). Map each `callbackId` to an app-defined handler, and use `event.elementId` / `event.cardJson` / `event.message?.getId()` for context. No server message is sent by the UIKit for a custom callback — it is entirely app-defined. + +## Fallback behavior + +Every card surface resolves a single-line fallback when the payload is empty or invalid: + +1. `getFallbackText()` (when available on the message/element) +2. `getText()` +3. Localized `"Card Message"` + +This guarantees a readable conversation even when a card cannot be rendered. + +## Related + +- **[Card Bubble](/ui-kit/angular/components/cometchat-card-bubble)** — the developer card component reference. +- **[AI Assistant Chat](/ui-kit/angular/components/cometchat-ai-assistant-chat)** — the agent chat experience that renders persisted and streaming agent cards. +- **[Events](/ui-kit/angular/events)** — `ccCardActionClicked` and `onCardMessageReceived` reference.