React v6 UIKit : How to handle thread replies

Guide Overview:

This guide demonstrates how to implement thread replies using the CometChat React UI Kit v6. While thread reply functionality isn’t available out of the box, this tutorial will walk you through building a custom component to handle threaded conversations.

Note: This guide can be followed to implement thread replies functionality in v5 UI Kit as well.

Step 1: Set up state to track the selected parent message

To enable thread replies, you first need to store the message that a user chooses to reply to. This message becomes the parent message of the thread.

const [selectedThreadMsg, setSelectedThreadMsg] = useState<CometChat.BaseMessage | null>(null);

Step 2: Handle Thread Reply Click Action

Define a function that will handle the onClick event when a user clicks to view or reply to thread messages:

const getOnThreadRepliesClick = (message: CometChat.BaseMessage) => {
    // Store the message selected for replying in the state
    setSelectedThreadMsg(message);
}

Step 3: Pass the Handler to <CometChatMessageList />

Pass the above function as a prop to the main message list component to enable thread reply clicks:

<CometChatMessageList
    user={selectedUser}
    group={selectedGroup}
    // This triggers when a reply is clicked
    onThreadRepliesClick={getOnThreadRepliesClick}
/>

Step 4: Conditionally Render the Custom Thread Component

To enable threaded messaging, you need to render the custom Thread component only when a message has been selected for replying.

{selectedThreadMsg && (
  <Thread
    selectedThreadMsg={selectedThreadMsg}
    setSelectedThreadMsg={setSelectedThreadMsg}
    selectedUser={selectedUser}
    selectedGroup={selectedGroup}
  />
)}

Step 5: Build the thread component

To implement threaded messaging, you’ll need to assemble several UIKit components, each playing a specific role in displaying and managing thread conversations.

Key Points:

  • CometChatThreadHeader: Displays the parent message at the top of the thread view, giving users context on what they’re replying to.
  • CometChatMessageList: Use with a parentMessageId and a custom messagesRequestBuilder to fetch and display only the replies associated with the selected parent message.
  • CometChatMessageComposer: also configure the composer with the same parentMessageId, this allows users to compose and send replies directly within the thread view.

Thread Component:

import {
  CometChatMessageComposer,
  CometChatMessageList,
  CometChatThreadHeader,
  CometChatUIKit,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";
import "./Thread.css";
interface ThreadProps {
  selectedThreadMsg: CometChat.BaseMessage | null;
  setSelectedThreadMsg: (msg: CometChat.BaseMessage | null) => void;
  selectedUser: CometChat.User | undefined;
  selectedGroup: CometChat.Group | undefined;
}
const Thread = ({
  selectedThreadMsg,
  setSelectedThreadMsg,
  selectedUser,
  selectedGroup,
}: ThreadProps) => {
  // Build message request to fetch thread messages, excluding action messages
  const threadMsgBuilder = () => {
    return new CometChat.MessagesRequestBuilder()
      .setCategories(
        CometChatUIKit.getDataSource()
          .getAllMessageCategories()
          .filter((cat) => cat !== "action")
      )
      .setTypes(CometChatUIKit.getDataSource().getAllMessageTypes())
      .hideReplies(true)
      .setLimit(20)
      .setParentMessageId(selectedThreadMsg ? selectedThreadMsg.getId() : 0);
  };
  return (
    <div className="thread-container">
      {/* Thread header with close button */}
      {selectedThreadMsg && (
        <CometChatThreadHeader
          parentMessage={selectedThreadMsg}
          onClose={() => setSelectedThreadMsg(null)}
        />
      )}
      {/* Thread messages list */}
      <CometChatMessageList
        user={selectedUser}
        group={selectedGroup}
        parentMessageId={selectedThreadMsg?.getId()}
        messagesRequestBuilder={threadMsgBuilder()}
      />
      {/* Composer sends messages as replies in the thread */}
      <CometChatMessageComposer
        user={selectedUser}
        group={selectedGroup}
        parentMessageId={selectedThreadMsg?.getId()}
      />
    </div>
  );
};
export default Thread;

Main Component: Integrate Thread Component

Import and render the <Thread/> component inside your <App/> component to display thread conversations.

import { useState } from "react";
import {
  CometChatMessageComposer,
  CometChatMessageHeader,
  CometChatMessageList,
} from "@cometchat/chat-uikit-react";
import { CometChat } from "@cometchat/chat-sdk-javascript";
import { CometChatSelector } from "../CometChatSelector/CometChatSelector";
import "./home.css";
import "@cometchat/chat-uikit-react/css-variables.css";
import Thread from "../thread/Thread";
function App() {
  const [selectedUser, setSelectedUser] = useState<CometChat.User | undefined>(undefined);
  const [selectedGroup, setSelectedGroup] = useState<CometChat.Group | undefined>(undefined);
  const [selectedCall, setSelectedCall] = useState<CometChat.Call | undefined>(undefined);
  // State to store the selected message to reply
  const [selectedThreadMsg, setSelectedThreadMsg] = useState<CometChat.BaseMessage | null>(null);
  const getOnThreadRepliesClick = (message: CometChat.BaseMessage) => {
    setSelectedThreadMsg(message);
  };
  return (
    <div className="conversations-with-messages">
      <div className="conversations-wrapper">
        <CometChatSelector
          onSelectorItemClicked={(activeItem) => {
            let item = activeItem;
            if (activeItem instanceof CometChat.Conversation) {
              item = activeItem.getConversationWith();
            }
            if (item instanceof CometChat.User) {
              setSelectedUser(item as CometChat.User);
              setSelectedCall(undefined);
              setSelectedGroup(undefined);
            } else if (item instanceof CometChat.Group) {
              setSelectedUser(undefined);
              setSelectedGroup(item as CometChat.Group);
              setSelectedCall(undefined);
            } else if (item instanceof CometChat.Call) {
              setSelectedUser(undefined);
              setSelectedGroup(undefined);
              setSelectedCall(item as CometChat.Call);
            }
          }}
        />
      </div>
      {selectedUser || selectedGroup || selectedCall ? (
        <div className="messages-wrapper">
          <CometChatMessageHeader user={selectedUser} group={selectedGroup} />
          <CometChatMessageList
            user={selectedUser}
            group={selectedGroup}
            onThreadRepliesClick={getOnThreadRepliesClick}
          />
          <CometChatMessageComposer
            user={selectedUser}
            group={selectedGroup}
          />
        </div>
      ) : (
        <div className="empty-conversation">Select Conversation to start</div>
      )}
      {/* Render Thread component when a thread message is selected */}
      {selectedThreadMsg && (
        <Thread
          selectedThreadMsg={selectedThreadMsg}
          setSelectedThreadMsg={setSelectedThreadMsg}
          selectedUser={selectedUser}
          selectedGroup={selectedGroup}
        />
      )}
    </div>
  );
}
export default App;

CometChatSelector component:

import { useEffect, useState } from "react";
import {
  Conversation,
  Group,
  User,
  CometChat,
} from "@cometchat/chat-sdk-javascript";
import {
  CometChatConversations,
  CometChatUIKitLoginListener,
} from "@cometchat/chat-uikit-react";
import "./CometChatSelector.css";

// Define the props for the CometChatSelector component
interface SelectorProps {
  onSelectorItemClicked?: (
    input: User | Group | Conversation,
    type: string
  ) => void;
  onHide?: () => void;
  onNewChatClicked?: () => void;
}

// CometChatSelector component
export const CometChatSelector = (props: SelectorProps) => {
  // Destructure props with a default function for onSelectorItemClicked
  const { onSelectorItemClicked = () => {} } = props;

  // State to store the logged-in user
  const [loggedInUser, setLoggedInUser] = useState<CometChat.User | null>();

  // State to track the currently selected item (it can be a Conversation, User, or Group)
  const [activeItem, setActiveItem] = useState<
    CometChat.Conversation | CometChat.User | CometChat.Group | undefined
  >();

  // useEffect hook to fetch and set the logged-in user
  useEffect(() => {
    const loggedInUser = CometChatUIKitLoginListener.getLoggedInUser();
    setLoggedInUser(loggedInUser);
  }, [loggedInUser]); // Dependency on loggedInUser causes unnecessary re-renders

  return (
    <>
      {/* Render chat conversations if a user is logged in */}
      {loggedInUser && (
        <>
          <CometChatConversations
            activeConversation={
              activeItem instanceof CometChat.Conversation
                ? activeItem
                : undefined
            }
            onItemClick={(e) => {
              setActiveItem(e); // Update the active item when an item is clicked
              onSelectorItemClicked(e, "updateSelectedItem"); // Notify parent component
            }}
          />
        </>
      )}

    </>
  );
};