diff --git a/docs.json b/docs.json index cb43da08b..8bc161d73 100644 --- a/docs.json +++ b/docs.json @@ -568,6 +568,8 @@ ] }, "ui-kit/react/components/thread-header", + "ui-kit/react/components/pinned-messages", + "ui-kit/react/components/saved-messages", "ui-kit/react/components/message-information", "ui-kit/react/components/reactions", "ui-kit/react/components/reaction-list", @@ -612,6 +614,8 @@ "ui-kit/react/guide-block-unblock-user", "ui-kit/react/guide-group-chat-setup", "ui-kit/react/guide-threaded-messages", + "ui-kit/react/guide-pin-and-save-messages", + "ui-kit/react/guide-custom-text-formatter", "ui-kit/react/guide-search-messages", "ui-kit/react/guide-new-chat-creation", "ui-kit/react/guide-message-privately" diff --git a/ui-kit/react/components-overview.mdx b/ui-kit/react/components-overview.mdx index 6c68f2c50..0a446eac9 100644 --- a/ui-kit/react/components-overview.mdx +++ b/ui-kit/react/components-overview.mdx @@ -125,6 +125,8 @@ All feature components are imported from `@cometchat/chat-uikit-react`. | `CometChatMessageComposer` | Rich text input with attachments, emoji, voice recording, and formatting | [Message Composer](/ui-kit/react/components/message-composer) | | `CometChatMessageBubble` | Message bubble container with alignment, receipts, options, and reactions | [Message Bubble](/ui-kit/react/components/message-bubble) | | `CometChatThreadHeader` | Parent message bubble and reply count for threaded conversations | [Thread Header](/ui-kit/react/components/thread-header) | +| `CometChatPinnedMessages` | Panel of the messages pinned in a conversation | [Pinned Messages](/ui-kit/react/components/pinned-messages) | +| `CometChatSavedMessages` | The current user's saved messages across all conversations | [Saved Messages](/ui-kit/react/components/saved-messages) | | `CometChatMessageInformation` | Message delivery and read receipt details panel | [Message Information](/ui-kit/react/components/message-information) | | `CometChatReactions` | Reaction chips bar with reactor list popover | [Reactions](/ui-kit/react/components/reactions) | | `CometChatReactionList` | Full reactor list with tabs and pagination | [Reaction List](/ui-kit/react/components/reaction-list) | diff --git a/ui-kit/react/components/conversations.mdx b/ui-kit/react/components/conversations.mdx index a40324853..70248541d 100644 --- a/ui-kit/react/components/conversations.mdx +++ b/ui-kit/react/components/conversations.mdx @@ -52,6 +52,7 @@ description: "Scrollable list of recent one-on-one and group conversations for t "hideGroupType": { "type": "boolean", "default": false }, "hideUnreadCount": { "type": "boolean", "default": false }, "hideDeleteConversation": { "type": "boolean", "default": false }, + "hidePinConversation": { "type": "boolean", "default": false }, "showSearchBar": { "type": "boolean", "default": true }, "showScrollbar": { "type": "boolean", "default": false } }, @@ -85,6 +86,11 @@ description: "Scrollable list of recent one-on-one and group conversations for t "name": "ui:conversation/deleted", "payload": "{ conversation: CometChat.Conversation }", "description": "Conversation deleted from list" + }, + { + "name": "ui:conversation/pin-changed", + "payload": "{ conversation: CometChat.Conversation, pinned: boolean }", + "description": "Emitted by the row's Pin/Unpin action; also received to keep the list ordering in sync" } ], "sdkListeners": [ @@ -155,6 +161,7 @@ The component handles: - Real-time updates (new messages, typing indicators, presence changes) - Search filtering - Selection mode (single/multiple) +- Pin conversation — pin a chat to the top of the list, with a pin indicator on the row --- @@ -306,6 +313,7 @@ UI events this component publishes: | Event | Payload | Fires when | | --- | --- | --- | | `ui:conversation/deleted` | `{ conversation }` | User deletes a conversation | +| `ui:conversation/pin-changed` | `{ conversation, pinned }` | User pins or unpins a conversation from the row menu | ### Events Received @@ -320,6 +328,7 @@ UI events this component subscribes to (published by other components): | `ui:conversation/updated` | `{ conversation }` | Updates the conversation in the list | | `ui:conversation/read` | `{ conversationId }` | Resets unread count by conversation ID | | `ui:conversation/deleted` | `{ conversation }` | Removes the conversation from the list | +| `ui:conversation/pin-changed` | `{ conversation, pinned }` | Optimistically re-orders the list when a chat is pinned/unpinned | | `ui:group/created` | `{ group }` | Adds the new group conversation to the list | | `ui:group/deleted` | `{ group }` | Removes the group conversation from the list | | `ui:group/left` | `{ group }` | Removes the group conversation from the list | @@ -342,6 +351,14 @@ These SDK listeners are attached internally. The component updates its state aut ## Customization +### Pin Conversation + +When pinning is enabled for your app (the `features.ux.conversations.pinned.enabled` app setting), each row's context menu carries a **Pin / Unpin** action, and pinned chats sort to the top of the list with a pin indicator on the row. This is wired out of the box — no props required. + +Pins are personal to each user, and your app can cap how many a user may pin through the `features.ux.conversations.pinned.limit` app setting; when a user reaches the cap, the kit shows a toast naming the limit. To remove the menu action entirely, set [`hidePinConversation`](#hidepinconversation). See [Core Features → Pin & Save](/ui-kit/react/core-features#pin-and-save-messages). + +A conversation can also be **system-pinned** app-wide (even when empty). System pins always sort above user pins and cannot be unpinned from the UI. + ### View Props Use view props to replace sections of the default UI while keeping the component's behavior intact: @@ -1039,6 +1056,17 @@ Hide the delete option in the conversation item's context menu. --- +### hidePinConversation + +Remove the built-in **Pin / Unpin** option from the conversation item's context menu. See [Pin Conversation](#pin-conversation) for the feature overview. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### showSearchBar Whether to show the search bar. Set to `false` to hide it entirely. @@ -1210,6 +1238,7 @@ Overrides survive component updates because the component never sets inline styl | Unread badge | `.cometchat-conversations .cometchat-badge` | | Subtitle text | `.cometchat-conversations .cometchat-conversations__subtitle-text` | | Status indicator | `.cometchat-conversations .cometchat-status-indicator` | +| Pin indicator (pinned row) | `.cometchat-conversations__item-pin-indicator` | | Read receipts | `.cometchat-conversations .cometchat-receipts-read` | | Active item | `.cometchat-conversations__list-item-active .cometchat-list-item` | | Typing indicator | `.cometchat-conversations__subtitle-typing` | diff --git a/ui-kit/react/components/message-bubble.mdx b/ui-kit/react/components/message-bubble.mdx index ff35e8223..27c409363 100644 --- a/ui-kit/react/components/message-bubble.mdx +++ b/ui-kit/react/components/message-bubble.mdx @@ -114,8 +114,33 @@ Message bubbles are **self-extracting**: each bubble takes the SDK `message` and | Receipts | Outgoing + `!hideReceipts` | `message.getReadAt()` / `getDeliveredAt()` | | Edited indicator | `message.getEditedAt()` truthy | "(edited)" text | | Thread replies | `message.getReplyCount() > 0` + `!hideThreadView` | Reply count button | +| Pinned indicator | `message.getPinnedAt()` is set | Pin icon in the status-info area | +| Saved indicator | `message.getSavedAt()` is set | Bookmark icon in the status-info area | | Context menu | `options.length > 0` + `!disableInteraction` | Hover/click | +## Pinned and Saved Indicators + +When a message is pinned (visible to everyone in the conversation) or saved (personal to the current user), the bubble shows a small indicator in its status-info area alongside the timestamp — a pin icon for pinned, a bookmark icon for saved. A message can carry both at once. + +This is automatic: the bubble derives each indicator from the message's own state (`getPinnedAt()` / `getSavedAt()`), and it updates live as messages are pinned, unpinned, saved, and unsaved. No props are required. + + +**Live Preview** — a bubble carrying both the pinned and saved indicators. + +[Open in Storybook ↗](https://storybook.cometchat.io/react/?path=/story/components-bubbles-message-bubble--pinned-and-saved) + + + + +To restyle or hide the indicators, target their selectors (see [CSS Selectors](#css-selectors)). To learn how users pin and save messages, see the [Pin & Save Messages guide](/ui-kit/react/guide-pin-and-save-messages). + ## GlobalConfig Integration `hideReceipts` reads from `GlobalConfigContext` when the prop is not explicitly set: @@ -313,5 +338,8 @@ Render-prop overrides for each region of the bubble. Semantics for all of them: | Body | `.cometchat-message-bubble__body` | | Content | `.cometchat-message-bubble__body-content-view` | | Status info | `.cometchat-message-bubble__body-status-info-view` | +| Pin/save indicator (base) | `.cometchat-message-bubble__status-info-view-indicator` | +| Pinned indicator | `.cometchat-message-bubble__status-info-view-indicator--pinned` | +| Saved indicator | `.cometchat-message-bubble__status-info-view-indicator--saved` | | Receipts | `.cometchat-receipts` | | Thread button | `.cometchat-message-bubble__thread-button` | diff --git a/ui-kit/react/components/message-composer.mdx b/ui-kit/react/components/message-composer.mdx index 6cb59d304..642056a6e 100644 --- a/ui-kit/react/components/message-composer.mdx +++ b/ui-kit/react/components/message-composer.mdx @@ -89,6 +89,7 @@ description: "Rich text input with attachments, emoji, voice recording, mentions "emojiButtonIconView": "ReactNode", "sendButtonView": "ReactNode", "auxiliaryButtonView": "ReactNode", + "toolbarTrailingView": "ReactNode", "headerView": "ReactNode" }, "callbacks": { @@ -383,6 +384,7 @@ Use view props to replace sections of the default UI while keeping the component | `emojiButtonIconView` | `ReactNode` | Icon inside the emoji button | | `sendButtonView` | `ReactNode` | Content inside the send button | | `auxiliaryButtonView` | `ReactNode` | Additional buttons in the actions area | +| `toolbarTrailingView` | `ReactNode` | Content at the trailing end of the rich-text formatting toolbar | | `headerView` | `ReactNode` | Header area above the input | #### attachmentOptions @@ -436,6 +438,33 @@ function ComposerCustomAuxiliary({ chatUser }: { chatUser: CometChat.User }) { } ``` +#### toolbarTrailingView + +Render a node at the trailing end of the rich-text formatting toolbar, after the built-in buttons and an auto-inserted separator. This is the place for a button that drives a custom formatter from `textFormatters` — for example, a color button that wraps the selected text. + +```tsx +import { CometChatMessageComposer } from "@cometchat/chat-uikit-react"; +import { ColorFormatter } from "./formatters/ColorFormatter"; +import { ColorButton } from "./components/ColorButton"; + +function ComposerWithColor({ chatUser }: { chatUser: CometChat.User }) { + return ( + } + /> + ); +} +``` + + +The toolbar (and therefore the trailing view) only renders when the rich-text editor is enabled — pass `enableRichTextEditor`. For a complete walkthrough that builds the button and formatter, see the [Custom Text Formatter guide](/ui-kit/react/guide-custom-text-formatter). + + +Your node is rendered as-is with **no wrapper class** — style it yourself. It sits inside `.cometchat-formatting-toolbar`, after an auto-inserted `.cometchat-formatting-toolbar__separator`. + #### sendButtonView Replace the send button. @@ -513,7 +542,7 @@ Override design tokens on the component selector: All props are optional unless noted. -View slot props (`headerView`, `sendButtonView`, `auxiliaryButtonView`, `attachmentButtonIconView`, `voiceRecordingButtonIconView`, `emojiButtonIconView`) are convenience props available only on the flat API. In compound composition mode, use the corresponding sub-components directly or pass children to them. +View slot props (`headerView`, `sendButtonView`, `auxiliaryButtonView`, `toolbarTrailingView`, `attachmentButtonIconView`, `voiceRecordingButtonIconView`, `emojiButtonIconView`) are convenience props available only on the flat API. In compound composition mode, use the corresponding sub-components directly or pass children to them. --- @@ -1064,6 +1093,17 @@ Custom ReactNode for additional buttons rendered in the actions area before the --- +#### toolbarTrailingView + +Custom ReactNode rendered at the trailing end of the rich-text formatting toolbar, after the built-in buttons and an auto-inserted separator. Only shown when the rich-text editor and its toolbar are enabled. Intended for a button that drives a custom formatter from `textFormatters`. See the [Custom Text Formatter guide](/ui-kit/react/guide-custom-text-formatter). + +| | | +| --- | --- | +| Type | `ReactNode` | +| Default | `undefined` | + +--- + #### headerView Custom ReactNode for the header area above the input (replaces edit/reply preview + validation). diff --git a/ui-kit/react/components/message-header.mdx b/ui-kit/react/components/message-header.mdx index 5a507f97f..915cd0bcb 100644 --- a/ui-kit/react/components/message-header.mdx +++ b/ui-kit/react/components/message-header.mdx @@ -48,6 +48,7 @@ description: "Toolbar displaying conversation details with avatar, name, presenc "onSummaryClick": "() => void", "onVoiceCallClick": "(entity: CometChat.User | CometChat.Group) => void", "onVideoCallClick": "(entity: CometChat.User | CometChat.Group) => void", + "onPinnedMessagesClicked": "() => void", "onError": "((error: CometChat.CometChatException) => void) | null" }, "visibility": { @@ -57,7 +58,8 @@ description: "Toolbar displaying conversation details with avatar, name, presenc "showConversationSummaryButton": { "type": "boolean", "default": false }, "enableAutoSummaryGeneration": { "type": "boolean", "default": false }, "hideVoiceCallButton": { "type": "boolean", "default": false }, - "hideVideoCallButton": { "type": "boolean", "default": false } + "hideVideoCallButton": { "type": "boolean", "default": false }, + "hidePinnedMessagesOption": { "type": "boolean", "default": false } }, "viewSlots": { "leadingView": "ReactNode", @@ -300,6 +302,7 @@ function ChatApp() { | `onSummaryClick` | `() => void` | User clicks the AI summary button | | `onVoiceCallClick` | `(entity: CometChat.User \| CometChat.Group) => void` | User clicks the voice call button | | `onVideoCallClick` | `(entity: CometChat.User \| CometChat.Group) => void` | User clicks the video call button | +| `onPinnedMessagesClicked` | `() => void` | User clicks "Pinned messages" in the overflow menu | | `onError` | `((error: CometChat.CometChatException) => void) \| null` | SDK error occurs | ### Events Emitted @@ -676,6 +679,17 @@ Hide the video call button. --- +### hidePinnedMessagesOption + +Remove the "Pinned messages" entry from the overflow menu. The entry only appears when pinning is enabled for your app (the `features.ux.messages.pinned.enabled` app setting) **and** `onPinnedMessagesClicked` is provided; use this prop to hide it even then. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### callSettingsBuilder Custom call settings builder for ongoing call sessions. Passed to the OngoingCall component when a call is started. Falls back to `GlobalConfig.callSettingsBuilder`, then the built-in default. @@ -764,6 +778,17 @@ Callback when the video call button is clicked. When provided, overrides the def --- +### onPinnedMessagesClicked + +Callback when the user clicks "Pinned messages" in the overflow menu. Provide it to open a [`CometChatPinnedMessages`](/ui-kit/react/components/pinned-messages) panel for the current conversation. The menu entry only appears when pinning is enabled for your app (the `features.ux.messages.pinned.enabled` app setting) and this callback is set. + +| | | +| --- | --- | +| Type | `() => void` | +| Default | `undefined` | + +--- + ### onError Callback when an SDK error occurs during call operations or other internal processes. diff --git a/ui-kit/react/components/message-list.mdx b/ui-kit/react/components/message-list.mdx index 936115211..6dd81a95b 100644 --- a/ui-kit/react/components/message-list.mdx +++ b/ui-kit/react/components/message-list.mdx @@ -82,6 +82,11 @@ description: "Scrollable message feed with plugin-based bubble rendering, reacti "optionToggles": { "hideReplyOption": { "type": "boolean", "default": false }, "hideReplyInThreadOption": { "type": "boolean", "default": false }, + "hidePinMessageOption": { "type": "boolean", "default": false }, + "hideUnpinMessageOption": { "type": "boolean", "default": false }, + "hideSaveMessageOption": { "type": "boolean", "default": false }, + "hideUnsaveMessageOption": { "type": "boolean", "default": false }, + "hideThreadSubscriptionOption": { "type": "boolean", "default": false }, "hideEditMessageOption": { "type": "boolean", "default": false }, "hideDeleteMessageOption": { "type": "boolean", "default": false }, "hideCopyMessageOption": { "type": "boolean", "default": false }, @@ -483,6 +488,30 @@ These SDK listeners are attached internally. The component updates its state aut ## Customization +### Pin and Save Options + +When pinning and saving are enabled for your app (the `features.ux.messages.pinned.enabled` and `features.ux.messages.saved.enabled` app settings), `CometChatMessageList` automatically adds **Pin**, **Unpin**, **Save**, and **Unsave** to the message context menu — no props required. Who can pin is enforced by the SDK based on the conversation and the user's role (in a group, owners, admins, and moderators can pin). + +Use the `hide*` props to remove any of these options: + +```tsx + +``` + +| Prop | Removes | +| --- | --- | +| `hidePinMessageOption` | Pin | +| `hideUnpinMessageOption` | Unpin | +| `hideSaveMessageOption` | Save | +| `hideUnsaveMessageOption` | Unsave | +| `hideThreadSubscriptionOption` | Subscribe / unsubscribe thread | + +See the [Pin & Save Messages guide](/ui-kit/react/guide-pin-and-save-messages) for the full screen, and [Core Features → Pin & Save](/ui-kit/react/core-features#pin-and-save-messages) for enabling the features. + ### View Props Use view props to replace sections of the default UI while keeping the component's behavior intact: @@ -962,6 +991,61 @@ Hide the "Reply in Thread" option from the message context menu. --- +### hidePinMessageOption + +Hide the "Pin" option from the message context menu. Pinning must also be enabled for your app (the `features.ux.messages.pinned.enabled` app setting). + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideUnpinMessageOption + +Hide the "Unpin" option from the message context menu. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideSaveMessageOption + +Hide the "Save" option from the message context menu. Saving must also be enabled for your app (the `features.ux.messages.saved.enabled` app setting). + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideUnsaveMessageOption + +Hide the "Unsave" option from the message context menu. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideThreadSubscriptionOption + +Hide the thread subscription (subscribe/unsubscribe) option from the message context menu. See [Threaded Messages → Thread Subscription](/ui-kit/react/guide-threaded-messages#thread-subscription). + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### hideEditMessageOption Hide the "Edit" option from the message context menu. diff --git a/ui-kit/react/components/pinned-messages.mdx b/ui-kit/react/components/pinned-messages.mdx new file mode 100644 index 000000000..56ff11058 --- /dev/null +++ b/ui-kit/react/components/pinned-messages.mdx @@ -0,0 +1,534 @@ +--- +title: "Pinned Messages" +sidebarTitle: "Pinned Messages" +description: "Displays the messages pinned in a conversation, with quick unpin and per-message options." +--- + + +```json +{ + "component": "CometChatPinnedMessages", + "package": "@cometchat/chat-uikit-react", + "import": "import { CometChatPinnedMessages } from \"@cometchat/chat-uikit-react\";", + "description": "Displays the messages pinned in a conversation, scoped to a user or group, with quick unpin and per-message options.", + "cssRootClass": ".cometchat-pinned-messages", + "primaryOutput": { + "prop": "onItemClick", + "type": "(message: CometChat.BaseMessage) => void" + }, + "props": { + "data": { + "user": { + "type": "CometChat.User", + "note": "The 1:1 conversation whose pins to show. Mutually exclusive with group." + }, + "group": { + "type": "CometChat.Group", + "note": "The group conversation whose pins to show. Mutually exclusive with user." + }, + "messagesRequestBuilder": { + "type": "CometChat.MessagesRequestBuilder", + "default": "undefined" + }, + "quickOptionsCount": { + "type": "number", + "default": "1", + "note": "How many inline quick actions to show on a row before overflow." + }, + "textFormatters": { + "type": "CometChatTextFormatter[]", + "default": "undefined" + } + }, + "visibility": { + "hideCloseButton": { "type": "boolean", "default": false }, + "hideCopyMessageOption": { "type": "boolean", "default": false }, + "hideMessageInfoOption": { "type": "boolean", "default": false }, + "hideUnpinMessageOption": { "type": "boolean", "default": false }, + "hideSaveMessageOption": { "type": "boolean", "default": false }, + "hideUnsaveMessageOption": { "type": "boolean", "default": false }, + "hideFlagMessageOption": { "type": "boolean", "default": false }, + "hideMessagePrivatelyOption": { "type": "boolean", "default": false }, + "hideTranslateMessageOption": { "type": "boolean", "default": false } + }, + "callbacks": { + "onItemClick": "(message: CometChat.BaseMessage) => void", + "onClose": "() => void" + }, + "viewSlots": { + "itemView": "(message: CometChat.BaseMessage) => ReactNode", + "headerView": "ReactNode", + "emptyView": "ReactNode", + "errorView": "ReactNode", + "loadingView": "ReactNode" + } + }, + "events": { + "emitted": [], + "emittedNote": "The component itself publishes nothing, but the per-message actions it renders (via the shared message options) publish UI events such as ui:message/pin-changed and ui:message/save-changed that keep other surfaces in sync.", + "received": [ + { + "name": "message/pinned", + "payload": "{ message }", + "description": "Adds the message to the list (network-confirmed)" + }, + { + "name": "ui:message/pin-changed", + "payload": "{ message, pinned }", + "description": "Optimistic local flip: adds on pin, removes on unpin" + }, + { + "name": "message/unpinned", + "payload": "{ message }", + "description": "Removes the message from the list" + }, + { + "name": "ui:message/save-changed", + "payload": "{ message, saved }", + "description": "Refreshes the row so the Save/Unsave option and glyph update" + } + ] + } +} +``` + + +## Overview + +`CometChatPinnedMessages` renders the messages pinned in a single conversation. Scope it to a `user` or a `group`, and it fetches that conversation's pins, keeps them in sync as messages are pinned and unpinned in real time, and surfaces per-message actions (unpin, save, copy, info, and more). It is typically mounted as a panel that opens from the [Message Header](/ui-kit/react/components/message-header)'s pinned-messages action. + + +**Live Preview** — interact with the default pinned messages panel. + +[Open in Storybook ↗](https://storybook.cometchat.io/react/?path=/story/components-messages-pinned-messages--default) + + + + +The component handles: +- Fetching the conversation's pinned messages +- Real-time updates when a message is pinned or unpinned +- An overflow menu of per-message options (unpin, save, copy, info, and more) +- Empty, loading, and error states + + +Pinning must be enabled for your app through the `features.ux.messages.pinned.enabled` app setting for pins to load. See [Core Features](/ui-kit/react/core-features#pin-and-save-messages). + + +--- + +## Usage + +### Flat API + +Pass the `user` or `group` whose pins you want to show. + +```tsx +import { CometChat } from "@cometchat/chat-sdk-javascript"; +import { CometChatPinnedMessages } from "@cometchat/chat-uikit-react"; + +function PinnedPanel({ + group, + onClose, +}: { + group: CometChat.Group; + onClose: () => void; +}) { + return ; +} +``` + +### Compound Composition + +For full layout control, compose the sub-components under `Root`. Omit any sub-component to drop it. + +```tsx +import { CometChatPinnedMessages } from "@cometchat/chat-uikit-react"; + + + + + + + + +``` + +### Opening from the Message Header + +Wire the panel to the header's pinned-messages action with `onPinnedMessagesClicked`. See the [Pin & Save Messages guide](/ui-kit/react/guide-pin-and-save-messages) for the full screen. + +```tsx +import { useState } from "react"; +import { + CometChatMessageHeader, + CometChatPinnedMessages, +} from "@cometchat/chat-uikit-react"; + +function ChatWithPins({ group }: { group: CometChat.Group }) { + const [showPins, setShowPins] = useState(false); + + return ( + <> + setShowPins(true)} + /> + {/* ...message list + composer... */} + {showPins && ( + setShowPins(false)} /> + )} + + ); +} +``` + +--- + +## Filtering + +Pass a `messagesRequestBuilder` to control which pinned messages are fetched — for example, to change the page size. Call `setPinned(true)` on the builder: it is what scopes the request to pinned messages, and without it the request is an ordinary history read. + +```tsx +import { CometChat } from "@cometchat/chat-sdk-javascript"; +import { CometChatPinnedMessages } from "@cometchat/chat-uikit-react"; + + +``` + + +The component re-asserts `setPinned(true)` and the `user`/`group` conversation scope on whatever builder you pass, so those are safe even if you omit them — but keep `setPinned(true)` in your code to make the intent explicit. Don't set a different conversation scope on the builder. + + +--- + +## Actions and Events + +### Callback Props + +| Prop | Signature | Fires when | +| --- | --- | --- | +| `onItemClick` | `(message: CometChat.BaseMessage) => void` | A pinned row is clicked (e.g. jump to the message in the list) | +| `onClose` | `() => void` | The close button is clicked | + +### Events + +The component itself publishes nothing, but the per-message actions it renders (through the shared message options) publish UI events — such as `ui:message/pin-changed` and `ui:message/save-changed` — that keep other surfaces in sync. + +It subscribes to the kit event bus and updates its list automatically. Each pin/save has two events: an optimistic `ui:` flip fired the moment the action succeeds locally, and the network-confirmed SDK event that follows: + +| Event | Payload | Behavior | +| --- | --- | --- | +| `ui:message/pin-changed` | `{ message, pinned }` | Optimistic: adds on pin, removes on unpin | +| `message/pinned` / `message/unpinned` | `{ message }` | Network-confirmed add / remove | +| `ui:message/save-changed` | `{ message, saved }` | Refreshes the row so the Save/Unsave option and glyph update | + +See the [Event System](/ui-kit/react/event-system#pin-and-save) for the full list. + +--- + +## Customization + +### Per-message Options + +Each pinned row exposes its actions in an overflow menu, with the first few surfaced inline as quick actions. Use the `hide*` props to trim the menu: + +```tsx + +``` + +Adjust how many actions appear inline (before the overflow menu) with `quickOptionsCount`: + +```tsx + +``` + +### View Props + +Replace parts of the UI while keeping the component's behavior: + +```tsx + } + emptyView={
Nothing pinned yet
} +/> +``` + +| Slot | Type | Replaces | +| --- | --- | --- | +| `itemView` | `(message: CometChat.BaseMessage) => ReactNode` | A pinned row | +| `headerView` | `ReactNode` | The panel header | +| `emptyView` | `ReactNode` | The empty state | +| `errorView` | `ReactNode` | The error state | +| `loadingView` | `ReactNode` | The loading state | + +### Compound Composition + +Use sub-components for full layout control: + +| Sub-component | Description | Flat API equivalent | +| --- | --- | --- | +| `Root` | Context provider and container | — | +| `Header` | Panel header with title and close button | `headerView` | +| `List` | The scrollable list of pinned rows | — | +| `Item` | A single pinned row (`message`, `index`) | `itemView` | +| `EmptyState` | Shown when nothing is pinned | `emptyView` | +| `ErrorState` | Shown on a load error | `errorView` | +| `LoadingState` | Shown while loading | `loadingView` | + +### CSS Styling + +Override design tokens on the component selector: + +```css +.cometchat-pinned-messages { + --cometchat-background-color-01: #ffffff; + --cometchat-text-color-primary: #141414; +} +``` + +--- + +## Props + +Provide either `user` or `group` (not both). All other props are optional. + + +View slot props (`itemView`, `headerView`, `emptyView`, `errorView`, `loadingView`) are convenience props on the flat API. In compound composition mode, use the corresponding sub-components directly. + + +--- + +### user + +The 1:1 conversation whose pinned messages to show. Mutually exclusive with `group`. + +| | | +| --- | --- | +| Type | `CometChat.User` | +| Default | `undefined` | + +--- + +### group + +The group conversation whose pinned messages to show. Mutually exclusive with `user`. + +| | | +| --- | --- | +| Type | `CometChat.Group` | +| Default | `undefined` | + +--- + +### messagesRequestBuilder + +Customize the request used to fetch pinned messages (for example, the page size). + +| | | +| --- | --- | +| Type | `CometChat.MessagesRequestBuilder` | +| Default | `undefined` | + +--- + +### quickOptionsCount + +How many per-message actions to show inline on a row before the rest collapse into the overflow menu. + +| | | +| --- | --- | +| Type | `number` | +| Default | `1` | + +--- + +### textFormatters + +Text formatters applied when rendering the pinned message previews. See [Text Formatters](/ui-kit/react/plugins/text-formatters). + +| | | +| --- | --- | +| Type | `CometChatTextFormatter[]` | +| Default | `undefined` | + +--- + +### hideCloseButton + +Hide the close button in the panel header. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideUnpinMessageOption + +Remove the unpin action from the per-message options. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideSaveMessageOption / hideUnsaveMessageOption + +Remove the save / unsave actions from the per-message options. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideCopyMessageOption + +Remove the copy action from the per-message options. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideMessageInfoOption + +Remove the message-info action from the per-message options. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideFlagMessageOption + +Remove the flag action from the per-message options. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideMessagePrivatelyOption + +Remove the "message privately" action from the per-message options. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideTranslateMessageOption + +Remove the translate action from the per-message options. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### onItemClick + +Callback when a pinned row is clicked. + +| | | +| --- | --- | +| Type | `(message: CometChat.BaseMessage) => void` | +| Default | `undefined` | + +--- + +### onClose + +Callback when the close button is clicked. + +| | | +| --- | --- | +| Type | `() => void` | +| Default | `undefined` | + +--- + +### className + +Additional CSS class for the root element. + +| | | +| --- | --- | +| Type | `string` | +| Default | `undefined` | + +--- + +## CSS Selectors + +| Target | Selector | +| --- | --- | +| Root container | `.cometchat-pinned-messages` | +| Header | `.cometchat-pinned-messages__header` | +| Header title | `.cometchat-pinned-messages__header-title` | +| Header close button | `.cometchat-pinned-messages__header-close` | +| List | `.cometchat-pinned-messages__list` | +| Row | `.cometchat-pinned-messages__item` | +| Row header (pinned-by) | `.cometchat-pinned-messages__row-header` | +| Row header name | `.cometchat-pinned-messages__row-header-name` | +| Empty state | `.cometchat-pinned-messages__empty` | +| Empty title | `.cometchat-pinned-messages__empty-title` | +| Empty subtitle | `.cometchat-pinned-messages__empty-subtitle` | +| Error state | `.cometchat-pinned-messages__error` | +| Loading shimmer | `.cometchat-pinned-messages__shimmer` | +| Message-info overlay | `.cometchat-pinned-messages__message-info-overlay` | +| Message-info panel | `.cometchat-pinned-messages__message-info-panel` | + +--- + +## Next Steps + + + + Build a full pin/save screen end to end + + + The user's saved messages across conversations + + + Open the pinned-messages panel from the header + + + Toggle the pin/unpin message options + + diff --git a/ui-kit/react/components/saved-messages.mdx b/ui-kit/react/components/saved-messages.mdx new file mode 100644 index 000000000..596e07a2b --- /dev/null +++ b/ui-kit/react/components/saved-messages.mdx @@ -0,0 +1,386 @@ +--- +title: "Saved Messages" +sidebarTitle: "Saved Messages" +description: "Displays the messages the current user has saved across all conversations." +--- + + +```json +{ + "component": "CometChatSavedMessages", + "package": "@cometchat/chat-uikit-react", + "import": "import { CometChatSavedMessages } from \"@cometchat/chat-uikit-react\";", + "description": "A standalone screen that lists the messages the current user has saved across all of their conversations. The host app mounts it directly; it is not opened from a built-in header menu.", + "cssRootClass": ".cometchat-saved-messages", + "primaryOutput": { + "prop": "onItemClick", + "type": "(message: CometChat.BaseMessage) => void" + }, + "props": { + "data": { + "messagesRequestBuilder": { + "type": "CometChat.MessagesRequestBuilder", + "default": "undefined" + }, + "textFormatters": { + "type": "CometChatTextFormatter[]", + "default": "undefined" + } + }, + "visibility": { + "hideCloseButton": { "type": "boolean", "default": false }, + "hideUnsaveMessageOption": { "type": "boolean", "default": false } + }, + "callbacks": { + "onItemClick": "(message: CometChat.BaseMessage) => void", + "onClose": "() => void" + }, + "viewSlots": { + "itemView": "(message: CometChat.BaseMessage) => ReactNode", + "headerView": "ReactNode", + "emptyView": "ReactNode", + "errorView": "ReactNode", + "loadingView": "ReactNode" + } + }, + "events": { + "emitted": [], + "emittedNote": "The component itself publishes nothing, but the unsave action it renders (via the shared message options) publishes ui:message/save-changed, which keeps other surfaces in sync.", + "received": [ + { + "name": "ui:message/save-changed", + "payload": "{ message, saved }", + "description": "Optimistic local flip: adds on save, removes on unsave" + }, + { + "name": "message/saved", + "payload": "{ message }", + "description": "Adds the message to the list (network-confirmed)" + }, + { + "name": "message/unsaved", + "payload": "{ message }", + "description": "Removes the message from the list (network-confirmed)" + } + ] + } +} +``` + + +## Overview + +`CometChatSavedMessages` lists every message the current user has saved, across all of their conversations. Unlike pinned messages, saves are personal and conversation-agnostic — so this component takes no `user`/`group`; it always shows the signed-in user's own saves and keeps them in sync in real time. + +It is a **standalone screen**: the UI Kit does not open it from any built-in header or menu. Mount it wherever your app wants a "Saved" destination — a route, a tab, or a side panel. + + +**Live Preview** — interact with the default saved messages screen. + +[Open in Storybook ↗](https://storybook.cometchat.io/react/?path=/story/components-messages-saved-messages--default) + + + + +The component handles: +- Fetching the current user's saved messages +- Real-time updates when a message is saved or unsaved +- An unsave action per row +- Empty, loading, and error states + + +Saving must be enabled for your app through the `features.ux.messages.saved.enabled` app setting for saves to load. See [Core Features](/ui-kit/react/core-features#pin-and-save-messages). + + +--- + +## Usage + +### Flat API + +```tsx +import { CometChatSavedMessages } from "@cometchat/chat-uikit-react"; + +function SavedScreen({ onClose }: { onClose: () => void }) { + return ; +} +``` + +### Compound Composition + +For full layout control, compose the sub-components under `Root`. Omit any sub-component to drop it. + +```tsx +import { CometChatSavedMessages } from "@cometchat/chat-uikit-react"; + + + + + + + + +``` + +### Mounting as a Screen + +Because there is no built-in trigger, wire it into your own navigation — for example, a button that toggles a panel: + +```tsx +import { useState } from "react"; +import { CometChatSavedMessages } from "@cometchat/chat-uikit-react"; + +function AppShell() { + const [showSaved, setShowSaved] = useState(false); + + return ( + <> + + {showSaved && setShowSaved(false)} />} + + ); +} +``` + +--- + +## Filtering + +Pass a `messagesRequestBuilder` to customize the fetch (for example, the page size). Call `setSaved(true)` on the builder: it is what scopes the request to the current user's saved messages, and without it the request is an ordinary history read. + +```tsx +import { CometChat } from "@cometchat/chat-sdk-javascript"; +import { CometChatSavedMessages } from "@cometchat/chat-uikit-react"; + + +``` + + +The component re-asserts `setSaved(true)` on whatever builder you pass, so it is safe even if you omit it — but keep it in your code to make the intent explicit. + + +--- + +## Actions and Events + +### Callback Props + +| Prop | Signature | Fires when | +| --- | --- | --- | +| `onItemClick` | `(message: CometChat.BaseMessage) => void` | A saved row is clicked (e.g. open the source conversation) | +| `onClose` | `() => void` | The close button is clicked | + +### Events + +The component itself publishes nothing, but the unsave action it renders (through the shared message options) publishes `ui:message/save-changed`, which keeps other surfaces in sync. + +It subscribes to the kit event bus and updates its list automatically — an optimistic `ui:` flip the moment a save/unsave succeeds locally, then the network-confirmed SDK event: + +| Event | Payload | Behavior | +| --- | --- | --- | +| `ui:message/save-changed` | `{ message, saved }` | Optimistic: adds on save, removes on unsave | +| `message/saved` / `message/unsaved` | `{ message }` | Network-confirmed add / remove | + +See the [Event System](/ui-kit/react/event-system#pin-and-save) for the full list. + +--- + +## Customization + +### Per-message Options + +Each saved row exposes an unsave action. Hide it with `hideUnsaveMessageOption`: + +```tsx + +``` + + +Saved rows are list items rather than full message bubbles, so they carry only the unsave action — there is no full per-message options menu here. + + +### View Props + +Replace parts of the UI while keeping the component's behavior: + +```tsx + } + emptyView={
You haven't saved anything yet
} +/> +``` + +| Slot | Type | Replaces | +| --- | --- | --- | +| `itemView` | `(message: CometChat.BaseMessage) => ReactNode` | A saved row | +| `headerView` | `ReactNode` | The panel header | +| `emptyView` | `ReactNode` | The empty state | +| `errorView` | `ReactNode` | The error state | +| `loadingView` | `ReactNode` | The loading state | + +### Compound Composition + +Use sub-components for full layout control: + +| Sub-component | Description | Flat API equivalent | +| --- | --- | --- | +| `Root` | Context provider and container | — | +| `Header` | Panel header with title and close button | `headerView` | +| `List` | The scrollable list of saved rows | — | +| `Item` | A single saved row (`message`, `index`) | `itemView` | +| `EmptyState` | Shown when nothing is saved | `emptyView` | +| `ErrorState` | Shown on a load error | `errorView` | +| `LoadingState` | Shown while loading | `loadingView` | + +### CSS Styling + +Override design tokens on the component selector: + +```css +.cometchat-saved-messages { + --cometchat-background-color-01: #ffffff; + --cometchat-text-color-primary: #141414; +} +``` + +--- + +## Props + +All props are optional. + + +View slot props (`itemView`, `headerView`, `emptyView`, `errorView`, `loadingView`) are convenience props on the flat API. In compound composition mode, use the corresponding sub-components directly. + + +--- + +### messagesRequestBuilder + +Customize the request used to fetch saved messages (for example, the page size). + +| | | +| --- | --- | +| Type | `CometChat.MessagesRequestBuilder` | +| Default | `undefined` | + +--- + +### textFormatters + +Text formatters applied when rendering the saved message previews. See [Text Formatters](/ui-kit/react/plugins/text-formatters). + +| | | +| --- | --- | +| Type | `CometChatTextFormatter[]` | +| Default | `undefined` | + +--- + +### hideCloseButton + +Hide the close button in the panel header. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### hideUnsaveMessageOption + +Remove the unsave action from each saved row. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + +### onItemClick + +Callback when a saved row is clicked. + +| | | +| --- | --- | +| Type | `(message: CometChat.BaseMessage) => void` | +| Default | `undefined` | + +--- + +### onClose + +Callback when the close button is clicked. + +| | | +| --- | --- | +| Type | `() => void` | +| Default | `undefined` | + +--- + +### className + +Additional CSS class for the root element. + +| | | +| --- | --- | +| Type | `string` | +| Default | `undefined` | + +--- + +## CSS Selectors + +| Target | Selector | +| --- | --- | +| Root container | `.cometchat-saved-messages` | +| Header | `.cometchat-saved-messages__header` | +| Header title | `.cometchat-saved-messages__header-title` | +| Header close button | `.cometchat-saved-messages__header-close` | +| List | `.cometchat-saved-messages__list` | +| Row | `.cometchat-saved-messages__item` | +| Row sender | `.cometchat-saved-messages__item-sender` | +| Row preview | `.cometchat-saved-messages__item-preview` | +| Row subtitle | `.cometchat-saved-messages__item-subtitle` | +| Row media-type icon | `.cometchat-saved-messages__item-subtitle-icon` (`--image`, `--video`, `--audio`, `--file`) | +| Row unsave button | `.cometchat-saved-messages__item-unsave` | +| Empty state | `.cometchat-saved-messages__empty` | +| Empty title | `.cometchat-saved-messages__empty-title` | +| Empty subtitle | `.cometchat-saved-messages__empty-subtitle` | +| Error state | `.cometchat-saved-messages__error` | +| Loading shimmer | `.cometchat-saved-messages__shimmer` | + +--- + +## Next Steps + + + + Build a full pin/save screen end to end + + + The messages pinned in a conversation + + + Toggle the save/unsave message options + + + Customize colors, fonts, and spacing + + diff --git a/ui-kit/react/components/thread-header.mdx b/ui-kit/react/components/thread-header.mdx index c217723ca..903e94e90 100644 --- a/ui-kit/react/components/thread-header.mdx +++ b/ui-kit/react/components/thread-header.mdx @@ -35,12 +35,14 @@ description: "Displays the parent message bubble and reply count for threaded co "hideReceipts": { "type": "boolean", "default": false }, "hideDate": { "type": "boolean", "default": false }, "hideReplyCount": { "type": "boolean", "default": false }, + "hideThreadSubscriptionToggle": { "type": "boolean", "default": false }, "showScrollbar": { "type": "boolean", "default": false } }, "callbacks": { "onClose": "() => void", "onSubtitleClicked": "() => void", "onParentDeleted": "() => void", + "onThreadSubscriptionChange": "(subscribed: boolean) => void", "onError": "((error: CometChat.CometChatException) => void) | null" }, "viewSlots": { @@ -117,6 +119,7 @@ The component handles: - Rendering the parent message as a bubble - Real-time reply count updates via SDK listeners - Parent message edit/delete detection +- A thread subscription (subscribe/unsubscribe) bell for group threads - Close button and keyboard (Escape) dismissal --- @@ -233,6 +236,7 @@ This component does not support request builders or filtering. It renders a sing | `onClose` | `() => void` | Close button clicked or Escape pressed | | `onSubtitleClicked` | `() => void` | Sender name / subtitle is clicked | | `onParentDeleted` | `() => void` | Parent message is deleted (thread should close) | +| `onThreadSubscriptionChange` | `(subscribed: boolean) => void` | User subscribes to or unsubscribes from the thread via the bell | | `onError` | `((error: CometChat.CometChatException) => void) \| null` | SDK error occurs | ### Events Emitted @@ -327,6 +331,26 @@ Available sub-components: | `ParentBubble` | Parent message bubble | `disableInteraction`, `messageSentAtDateTimeFormat`, `className` | `messageBubbleView` | | `ReplyCount` | Reply count with divider | `showDivider`, `className` | — | +### Thread Subscription + +In a **group** thread, the header shows a bell that lets the user subscribe to or unsubscribe from the thread, so they can receive updates about new replies even when they aren't actively viewing it. The bell reflects the current subscription state and toggles it on click, with an optimistic UI update and a toast on failure. + +The bell renders automatically for group threads. It never appears in a 1:1 thread. To remove it, set `hideThreadSubscriptionToggle`; to react to changes, pass `onThreadSubscriptionChange`. + +```tsx + + console.log(subscribed ? "Subscribed to thread" : "Unsubscribed from thread") + } +/> +``` + + +The same subscribe/unsubscribe action is also available as a message-context-menu option in `CometChatMessageList` (`hideThreadSubscriptionOption`). For the underlying hook and events, see [Threaded Messages → Thread Subscription](/ui-kit/react/guide-threaded-messages#thread-subscription). + + ### CSS Styling Override design tokens on the component selector: @@ -398,6 +422,17 @@ Hide the reply count section below the parent bubble. --- +### hideThreadSubscriptionToggle + +Hide the subscribe/unsubscribe (subscription) bell. The bell only renders for group threads to begin with — it never shows in a 1:1 thread. + +| | | +| --- | --- | +| Type | `boolean` | +| Default | `false` | + +--- + ### separatorDateTimeFormat Custom date/time format for the date chip shown above the parent bubble. @@ -464,6 +499,17 @@ Callback when the parent message is deleted (thread should close). --- +### onThreadSubscriptionChange + +Callback when the user subscribes to or unsubscribes from the thread via the subscription bell. Receives the new subscription state. + +| | | +| --- | --- | +| Type | `(subscribed: boolean) => void` | +| Default | `undefined` | + +--- + ### onError Callback when an SDK error occurs. @@ -522,6 +568,9 @@ Custom subtitle view below the title. | Body timestamp | `.cometchat-thread-header__body-timestamp` | | Reply count | `.cometchat-thread-header__reply-count` | | Reply count divider | `.cometchat-thread-header__reply-count-divider` | +| Subscription bell wrapper | `.cometchat-thread-header__subscription-wrapper` | +| Subscription bell button | `.cometchat-thread-header__subscription-button` | +| Subscription bell icon | `.cometchat-thread-header__subscription-icon` (`--on` subscribed, `--off` not subscribed) | --- diff --git a/ui-kit/react/core-features.mdx b/ui-kit/react/core-features.mdx index b7b22b315..e9d56d92a 100644 --- a/ui-kit/react/core-features.mdx +++ b/ui-kit/react/core-features.mdx @@ -9,7 +9,7 @@ description: "Overview of CometChat React UI Kit core features, including messag | --- | --- | | Package | `@cometchat/chat-uikit-react` | | Required setup | Wrap app in `CometChatProvider` with valid credentials — must complete before rendering any component | -| Core features | Instant Messaging, Media Sharing, Read Receipts, Mark as Unread, Typing Indicator, User Presence, Reactions, Mentions, Quoted Reply, Search, Threaded Conversations, Moderation, Report Message, Group Chat | +| Core features | Instant Messaging, Media Sharing, Read Receipts, Mark as Unread, Typing Indicator, User Presence, Reactions, Mentions, Quoted Reply, Pin & Save Messages, Search, Threaded Conversations, Moderation, Report Message, Group Chat | | Key components | `CometChatConversations` → [Conversations](/ui-kit/react/components/conversations), `CometChatMessageList` → [Message List](/ui-kit/react/components/message-list), `CometChatMessageComposer` → [Message Composer](/ui-kit/react/components/message-composer), `CometChatMessageHeader` → [Message Header](/ui-kit/react/components/message-header), `CometChatUsers` → [Users](/ui-kit/react/components/users), `CometChatGroups` → [Groups](/ui-kit/react/components/groups), `CometChatGroupMembers` → [Group Members](/ui-kit/react/components/group-members) | | CSS class prefix | `.cometchat-` | | Theming | Override CSS variables on `.cometchat` class. See [Theming](/ui-kit/react/theming) | @@ -165,6 +165,42 @@ Quoted Replies is a robust feature provided by CometChat that enables users to q | [Message List](/ui-kit/react/components/message-list) | [Message List](/ui-kit/react/components/message-list) supports replying to messages via the "Reply" option. Users can select "Reply" on a message to open the composer with the quoted reply pre-filled, maintaining context. | | [Message Composer](/ui-kit/react/components/message-composer) | [Message Composer](/ui-kit/react/components/message-composer) shows the quoted reply above the input field, providing context for the response. | +## Pin and Save Messages + +CometChat lets users **pin** important messages so they're highlighted for everyone in a conversation, and **save** messages privately to a personal list that spans every conversation. Pinned and saved messages show an indicator on the bubble, and dedicated panels let users review them. + + +**Live Preview** — the pinned messages panel for a conversation. + +[Open in Storybook ↗](https://storybook.cometchat.io/react/?path=/story/components-messages-pinned-messages--default) + + + + +| Components | Functionality | +| --- | --- | +| [Message List](/ui-kit/react/components/message-list#pin-and-save-options) | Adds Pin, Unpin, Save, and Unsave to the message options menu when the features are enabled. | +| [Pinned Messages](/ui-kit/react/components/pinned-messages) | A panel of the messages pinned in a conversation, opened from the [Message Header](/ui-kit/react/components/message-header). | +| [Saved Messages](/ui-kit/react/components/saved-messages) | A personal screen of the current user's saved messages across all conversations. | +| [Conversations](/ui-kit/react/components/conversations#hidepinconversation) | Lets users pin a whole conversation to the top of their list. | + +Your app can also cap how many items a user may pin or save through three app settings: + +| App setting | Caps | +| --- | --- | +| features.ux.messages.pinned.limit | Pinned messages per conversation | +| features.ux.messages.saved.limit | Saved messages per user | +| features.ux.conversations.pinned.limit | Pinned conversations per user | + +The UI Kit reads these settings at login. When a user reaches a cap, the kit shows a toast that names the exact limit — no extra handling required. For a full walkthrough, see the [Pin & Save Messages guide](/ui-kit/react/guide-pin-and-save-messages). + ## Group Chat CometChat facilitates Group Chats, allowing users to have conversations with multiple participants simultaneously. This feature is crucial for team collaborations, group discussions, social communities, and more. diff --git a/ui-kit/react/event-system.mdx b/ui-kit/react/event-system.mdx index a2b35e12e..8c1a3d9db 100644 --- a/ui-kit/react/event-system.mdx +++ b/ui-kit/react/event-system.mdx @@ -119,6 +119,25 @@ These events originate from the CometChat SDK (network). They fire when other us | `message/deleted` | `{ message: BaseMessage }` | Message was deleted | | `message/moderated` | `{ message: BaseMessage }` | Message was moderated | +### Pin and Save + +| Event Type | Payload | When | +| --- | --- | --- | +| `message/pinned` | `{ message: BaseMessage }` | A message was pinned in the conversation | +| `message/unpinned` | `{ message: BaseMessage }` | A message was unpinned | +| `message/saved` | `{ message: BaseMessage }` | The current user saved a message | +| `message/unsaved` | `{ message: BaseMessage }` | The current user unsaved a message | + +`CometChatPinnedMessages` and `CometChatSavedMessages` subscribe to these to keep their lists in sync in real time. See the [Pin & Save Messages guide](/ui-kit/react/guide-pin-and-save-messages). + +### Thread Subscription + +| Event Type | Payload | When | +| --- | --- | --- | +| `thread/subscription-changed` | `{ parentMessageId: number, subscribed: boolean }` | The server acknowledged a subscribe/unsubscribe on a thread | + +This is the network-acknowledged event. For the optimistic local flip published the moment a user clicks the bell, see [`ui:thread/subscription-changed`](#thread) below. + ### Receipt Events | Event Type | Payload | When | @@ -192,6 +211,15 @@ These events are published by UI Kit components for local cross-component commun | `ui:message/deleted` | `{ message }` | MessageList (after delete) | | `ui:message/read` | `{ message }` | MessageList (mark as read) | +### Pin and Save (optimistic) + +Published by the message options the moment a pin/save succeeds locally, before the network echo. Pinned/Saved panels and the message list listen to these so every surface flips together. + +| Event Type | Payload | Published by | +| --- | --- | --- | +| `ui:message/pin-changed` | `{ message, pinned }` | Message options (pin / unpin action) | +| `ui:message/save-changed` | `{ message, saved }` | Message options (save / unsave action) | + ### Composer Commands | Event Type | Payload | Published by | @@ -208,6 +236,7 @@ These events are published by UI Kit components for local cross-component commun | `ui:conversation/read` | `{ conversationId }` | MessageList | | `ui:conversation/updated` | `{ conversation }` | MessageList | | `ui:conversation/deleted` | `{ conversation }` | Conversations (delete action) | +| `ui:conversation/pin-changed` | `{ conversation, pinned }` | Conversations (pin / unpin action; optimistic re-order) | | `ui:active-chat/changed` | `{ user?, group?, message?, unreadMessageCount? }` | MessageList (on load) | ### User & Group Actions @@ -231,6 +260,7 @@ These events are published by UI Kit components for local cross-component commun | --- | --- | --- | | `ui:thread/opened` | `{ parentMessage }` | MessageList (thread option) | | `ui:thread/closed` | — | ThreadHeader | +| `ui:thread/subscription-changed` | `{ parentMessageId, subscribed }` | ThreadHeader / MessageList (optimistic subscribe-unsubscribe flip) | ### Call Actions diff --git a/ui-kit/react/guide-custom-text-formatter.mdx b/ui-kit/react/guide-custom-text-formatter.mdx new file mode 100644 index 000000000..c30c20db1 --- /dev/null +++ b/ui-kit/react/guide-custom-text-formatter.mdx @@ -0,0 +1,159 @@ +--- +title: "Custom Text Formatter" +sidebarTitle: "Custom Text Formatter" +description: "Build a minimal color formatter, bind it to a toolbar button in the composer, and render the result in read-only message bubbles." +--- + +## Goal + +By the end of this guide you will have a **color formatter**: a toolbar button in the composer that wraps the selected text in a color marker, and a formatter that renders that marker as colored text everywhere the message appears — in the composer, in the message list, conversation subtitle, pinned and saved messages. + +A text formatter has two jobs, and this guide covers both: + +1. **Rendering** — turn a marker in the raw message text into styled HTML wherever the message is displayed (`format()`). +2. **Authoring** — give users a way to produce that marker. Here, a button in the composer's `toolbarTrailingView` wraps the current selection. + + +For the full formatter reference — the built-in Markdown, Mentions, and URL formatters and the complete `CometChatTextFormatter` API — see [Text Formatters](/ui-kit/react/plugins/text-formatters). This guide is the minimal, task-focused version. + + +## Prerequisites + +- Completed the [Integration Guide](/ui-kit/react/integration-react) +- A chat screen using `CometChatMessageList` and `CometChatMessageComposer` + +## Step 1: The Formatter + +Extend `CometChatTextFormatter`. The one method that matters for rendering is `format()`: it receives the raw message text and returns HTML. Our marker is `{color=VALUE}...{/color}`, and we turn it into a colored ``. + +_File: src/formatters/ColorFormatter.ts_ + +```typescript +import { CometChatTextFormatter } from "@cometchat/chat-uikit-react"; + +/** Matches {color=#e5484d}text{/color} — a CSS color, then the wrapped text. */ +const COLOR_REGEX = /\{color=(#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\}([\s\S]*?)\{\/color\}/g; + +export class ColorFormatter extends CometChatTextFormatter { + readonly id = "color-formatter"; + override priority = 20; // after markdown (10), before mentions/URLs + + getRegex(): RegExp { + return COLOR_REGEX; + } + + format(text: string): string { + this.originalText = text ?? ""; + this.formattedText = this.originalText.replace( + this.getRegex(), + (_match, color: string, inner: string) => + `${inner}`, + ); + return this.formattedText; + } +} +``` + + +`format()` must store `originalText`, set `formattedText`, and return the formatted string — the pipeline relies on those fields. Keep it fast: it runs on every text message render. + + +## Step 2: The Toolbar Button + +The composer's `toolbarTrailingView` renders a node at the end of the rich-text toolbar. Put a button there that wraps the user's current selection in the color marker. + +_File: src/components/ColorButton.tsx_ + +```tsx +export function ColorButton({ color = "#e5484d" }: { color?: string }) { + function wrapSelection() { + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return; + + const range = selection.getRangeAt(0); + const selected = range.toString(); + range.deleteContents(); + range.insertNode( + document.createTextNode(`{color=${color}}${selected}{/color}`), + ); + selection.removeAllRanges(); + } + + return ( + + ); +} +``` + + +`onMouseDown={(e) => e.preventDefault()}` is the key detail — without it, clicking the button moves focus out of the editor and clears the selection before your handler runs. + + +## Step 3: Wire It Into the Composer + +Register the formatter with `textFormatters` and mount the button with `toolbarTrailingView`. The toolbar (and therefore the trailing view) only renders when the rich-text editor is enabled, so pass `enableRichTextEditor`. + +_File: ChatScreen.tsx_ + +```tsx +import { CometChatMessageComposer } from "@cometchat/chat-uikit-react"; +import { ColorFormatter } from "./formatters/ColorFormatter"; +import { ColorButton } from "./components/ColorButton"; + +} +/> +``` + +Now: the user selects text, clicks 🎨, and the input becomes `Hello {color=#e5484d}world{/color}`. On send, that raw text is stored on the message. + +## Step 4: Render It Everywhere the Message Appears + +The marker only becomes color when a surface runs the formatter. Read-only surfaces — like the message list, conversations, pinned/saved panels — call `format()` to produce the bubble HTML. Register the same formatter on each surface where the message can show up. + +_File: ChatScreen.tsx_ + +```tsx +import { + CometChatMessageList, + CometChatPinnedMessages, +} from "@cometchat/chat-uikit-react"; +import { ColorFormatter } from "./formatters/ColorFormatter"; + + + +{/* The same message can appear pinned — format it there too. */} + +``` + + +A formatter is only applied where you register it. If you add `textFormatters` to the composer but not the message list, the author sees the marker but readers see raw `{color=...}` text. Register it on every surface that displays the message. + + +## How It Round-Trips + +``` +Composer (author) Wire format Bubble (reader) +───────────────── ─────────── ─────────────── +select "world" Hello {color=#e5484d} Hello world +click 🎨 → world{/color} → (in red, via format()) +``` + +The marker is plain text on the message, so it survives storage and delivery untouched; each display surface turns it into color independently through the formatter you registered. + +## Next Steps + +- [Text Formatters](/ui-kit/react/plugins/text-formatters) — the built-in formatters and the full `CometChatTextFormatter` API +- [Message Composer → toolbarTrailingView](/ui-kit/react/components/message-composer#toolbartrailingview) — the toolbar slot in detail +- [Message Bubble](/ui-kit/react/components/message-bubble) — how bubbles render message content diff --git a/ui-kit/react/guide-pin-and-save-messages.mdx b/ui-kit/react/guide-pin-and-save-messages.mdx new file mode 100644 index 000000000..278150228 --- /dev/null +++ b/ui-kit/react/guide-pin-and-save-messages.mdx @@ -0,0 +1,254 @@ +--- +title: "Pin & Save Messages" +sidebarTitle: "Pin & Save Messages" +description: "Let users pin important messages for everyone in a conversation and save messages privately for themselves." +--- + +## Goal + +By the end of this guide you will have a chat screen where users can **pin** a message so it's highlighted for everyone in the conversation, open a panel of all pinned messages, and **save** a message privately to their own list — with a dedicated "Saved" screen to review saves across every conversation. + +Pin and save are two separate concepts: + +| | Pin | Save | +| --- | --- | --- | +| **Visible to** | Everyone in the conversation | Only the current user | +| **Scope** | One conversation | All conversations | +| **Surfaced by** | `CometChatPinnedMessages` (per conversation) | `CometChatSavedMessages` (a personal screen) | +| **Opened from** | The message header's pinned-messages action | Your own navigation (no built-in trigger) | + +## Prerequisites + +- Completed the [Integration Guide](/ui-kit/react/integration-react) +- A running `CometChatProvider` setup with valid credentials +- An existing chat screen using `CometChatMessageHeader`, `CometChatMessageList`, and `CometChatMessageComposer` +- **Pin messages** and **Save messages** enabled for your app through the `features.ux.messages.pinned.enabled` and `features.ux.messages.saved.enabled` app settings. See [Core Features → Pin & Save](/ui-kit/react/core-features#pin-and-save-messages). + + +The pin/unpin and save/unsave options only appear in the message options menu when the corresponding feature is enabled for your app. The UI Kit reads that setting at login, so no extra wiring is needed to show or hide the options. + + +## Step 1: The Message Options + +Once the features are enabled, `CometChatMessageList` automatically adds **Pin**, **Unpin**, **Save**, and **Unsave** to the message options menu — no props required. You only need the `hide*` props if you want to remove one: + +_File: ChatScreen.tsx_ + +```tsx +import { CometChatMessageList } from "@cometchat/chat-uikit-react"; + + +``` + +Who can pin is enforced by the SDK based on the conversation and the user's role (group owners, admins, and moderators can pin in a group). See the [Message List options](/ui-kit/react/components/message-list#pin-and-save-options). + +## Step 2: Open the Pinned Messages Panel + +`CometChatMessageHeader` exposes a pinned-messages action in its overflow menu. Wire `onPinnedMessagesClicked` to show `CometChatPinnedMessages`, scoped to the same `user`/`group`. + +_File: ChatScreen.tsx_ + +```tsx +import { useState } from "react"; +import { CometChat } from "@cometchat/chat-sdk-javascript"; +import { + CometChatMessageHeader, + CometChatMessageList, + CometChatMessageComposer, + CometChatPinnedMessages, +} from "@cometchat/chat-uikit-react"; + +function ChatScreen({ group }: { group: CometChat.Group }) { + const [showPins, setShowPins] = useState(false); + + return ( +
+
+ setShowPins(true)} + /> +
+ +
+ +
+ + {showPins && ( +
+ setShowPins(false)} + onItemClick={() => setShowPins(false)} + /> +
+ )} +
+ ); +} +``` + + +The pinned-messages action only appears in the header when pinning is enabled (the `features.ux.messages.pinned.enabled` app setting) **and** you provide `onPinnedMessagesClicked`. Use `hidePinnedMessagesOption` on the header to remove it explicitly. + + +## Step 3: Add a "Saved" Screen + +Saves are personal and span every conversation, so `CometChatSavedMessages` takes no `user`/`group` and is **not** opened from a built-in menu. Mount it wherever your app wants a "Saved" destination — a route, a tab, or a panel toggled from your own button. + +_File: AppShell.tsx_ + +```tsx +import { useState } from "react"; +import { CometChatSavedMessages } from "@cometchat/chat-uikit-react"; + +function AppShell() { + const [showSaved, setShowSaved] = useState(false); + + return ( + <> + + + {showSaved && ( + setShowSaved(false)} /> + )} + + ); +} +``` + +## Step 4: Pinned & Saved Indicators + +Pinned and saved messages render an indicator on the bubble in the main message list, so users can see a message's status inline. This is automatic — no configuration needed. See [Message Bubble → Pinned & Saved indicators](/ui-kit/react/components/message-bubble#pinned-and-saved-indicators). + + +**Live Preview** — a bubble carrying both the pinned and saved indicators. + +[Open in Storybook ↗](https://storybook.cometchat.io/react/?path=/story/components-bubbles-message-bubble--pinned-and-saved) + + + + +## Step 5: Limits + +Your app can cap how many messages may be pinned or saved. These caps are configured as app settings in the dashboard: + +| Setting | Caps | +| --- | --- | +| features.ux.messages.pinned.limit | Pins per conversation | +| features.ux.messages.saved.limit | Saves per user | +| features.ux.conversations.pinned.limit | Pinned conversations per user (see [Conversations](/ui-kit/react/components/conversations#hidepinconversation)) | + +When a user hits a cap, the UI Kit shows a toast explaining the limit — you don't need to handle the error yourself. The kit reads these settings at login so the toast can name the exact cap. + +## Complete Example + +_File: App.tsx_ + +```tsx +import { useState } from "react"; +import { CometChat } from "@cometchat/chat-sdk-javascript"; +import { + CometChatProvider, + CometChatConversations, + CometChatMessageHeader, + CometChatMessageList, + CometChatMessageComposer, + CometChatPinnedMessages, + CometChatSavedMessages, +} from "@cometchat/chat-uikit-react"; + +function ChatWithPinSave() { + const [user, setUser] = useState(null); + const [group, setGroup] = useState(null); + const [showPins, setShowPins] = useState(false); + const [showSaved, setShowSaved] = useState(false); + + function handleConversationClick(conversation: CometChat.Conversation) { + setShowPins(false); + const entity = conversation.getConversationWith(); + if (entity instanceof CometChat.User) { + setUser(entity); + setGroup(null); + } else if (entity instanceof CometChat.Group) { + setGroup(entity); + setUser(null); + } + } + + return ( +
+ {/* Conversations sidebar */} +
+ + +
+ + {/* Main message panel */} +
+ {(user || group) && ( + <> + setShowPins(true)} + /> +
+ +
+ + + )} +
+ + {/* Pinned messages panel */} + {showPins && (user || group) && ( +
+ setShowPins(false)} + onItemClick={() => setShowPins(false)} + /> +
+ )} + + {/* Saved messages screen */} + {showSaved && ( +
+ setShowSaved(false)} /> +
+ )} +
+ ); +} + +function App() { + return ( + + + + ); +} + +export default App; +``` + +## Next Steps + +- [Pinned Messages](/ui-kit/react/components/pinned-messages) — configure the pinned-messages panel +- [Saved Messages](/ui-kit/react/components/saved-messages) — configure the saved-messages screen +- [Message List](/ui-kit/react/components/message-list) — toggle the pin/save message options +- [Conversations](/ui-kit/react/components/conversations#hidepinconversation) — let users pin whole conversations diff --git a/ui-kit/react/guide-threaded-messages.mdx b/ui-kit/react/guide-threaded-messages.mdx index ced528be6..75a88ebd1 100644 --- a/ui-kit/react/guide-threaded-messages.mdx +++ b/ui-kit/react/guide-threaded-messages.mdx @@ -207,6 +207,53 @@ function App() { export default App; ``` +## Thread Subscription + +In a **group** thread, users can *subscribe* to the thread to keep getting updates about new replies even when they aren't actively viewing it, and *unsubscribe* to stop. The UI Kit surfaces this in two places, both wired out of the box: + +- A **bell toggle** in `CometChatThreadHeader` (group threads only — it never appears in a 1:1 thread). +- A **subscribe / unsubscribe option** in the message context menu of `CometChatMessageList`. + +Both reflect the current subscription state, flip it optimistically on click, and show a toast if the server rejects the change. No wiring is required to make them work. + +### Reacting to changes + +Pass `onThreadSubscriptionChange` to the thread header to run your own logic when the user subscribes or unsubscribes: + +```tsx + setThreadedMessage(null)} + onThreadSubscriptionChange={(subscribed) => + console.log(subscribed ? "Subscribed to thread" : "Unsubscribed from thread") + } +/> +``` + +### Hiding the controls + +- `hideThreadSubscriptionToggle` on [`CometChatThreadHeader`](/ui-kit/react/components/thread-header#hidethreadsubscriptiontoggle) removes the bell. +- `hideThreadSubscriptionOption` on [`CometChatMessageList`](/ui-kit/react/components/message-list#hidethreadsubscriptionoption) removes the context-menu option. + +### Driving it yourself + +For custom UI, the `useThreadSubscription` hook exposes the same state and toggle: + +```tsx +import { useThreadSubscription } from "@cometchat/chat-uikit-react"; + +function SubscribeButton({ parentMessageId }: { parentMessageId: number }) { + const { isSubscribed, toggle } = useThreadSubscription(parentMessageId); + return ( + + ); +} +``` + +Subscription changes are also broadcast on the [event bus](/ui-kit/react/event-system): `ui:thread/subscription-changed` (the optimistic local flip) and `thread/subscription-changed` (the server acknowledgement). + ## Next Steps - [Thread Header](/ui-kit/react/components/thread-header) — customize the thread header appearance