cometchat-native-customization

SkillCommunication

Customize the CometChat React Native UI Kit without forking — four-tier model: props → request builders → text formatters + message templates → DataSource decorators + event bus.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the cometchat-native-customization skill

What this skill tells your AI

The instructions your AI receives, as published by cometchat/cometchat-skills in skills/cometchat-native-customization/SKILL.md and read by ahel’s review.

Purpose

Teaches Claude how to change the behavior or appearance of the React Native UI Kit without modifying the kit itself. Four tiers, from cheapest to deepest:

Tier 1 — Props            (95% of asks solved here)
Tier 2 — RequestBuilder   (filter what data loads)
Tier 3 — Formatters + Templates   (change how text / messages render)
Tier 4 — DataSource decorators + Events  (last resort, powerful)

Always try Tier 1 first. Escalate only when the tier can't do what the user wants.

Read cometchat-native-components first — the catalog is the source of truth for prop names, slot views, and event listener names that this skill builds on.

Ground truth: docs/ui-kit/react-native/custom-text-formatter-guide.mdx, mentions-formatter-guide.mdx, shortcut-formatter-guide.mdx, url-formatter-guide.mdx, events.mdx, methods.mdx, property-changes.mdx, and the kit's source at packages/ChatUiKit/src/shared/formatters/ and packages/ChatUiKit/src/shared/events/. Official docs: https://www.cometchat.com/docs/ui-kit/react-native/overview · Docs MCP: claude mcp add --transport http cometchat-docs https://www.cometchat.com/docs/mcp (or fetch the URL directly without MCP).


Four-tier triage — pick the right tier before writing any code

When a user says "I want X" for a CometChat component:

If they want to...Use TierCost
Hide a feature (thread option, receipts, edit, etc.)Tier 1 — hide* / *Visibility props1 line of JSX
Customize a subsection (header title, subtitle, avatar, empty state)Tier 1 — <Slot>View prop1 component
Filter what loads (only show online users, exclude blocked, include tags)Tier 2 — *RequestBuilder1 builder
Change how URLs / mentions / hashtags / emojis render inlineTier 3 — textFormattersSubclass of CometChatTextFormatter
Render a custom message type (custom bubble, custom interactive msg)Tier 3 — templates + CometChatMessageTemplate1 template + 1 renderer
React to events from another component ("they deleted a message, now reload my view")Tier 4 — CometChatUIEventHandlerListener
Rewrite how data flows through the kit (custom conversation sorting, override user-fetch logic)Tier 4 — DataSourceDecoratorClass extension

If a user's ask fits Tier 1 but you jumped to Tier 3, you've written 50 lines that a 1-line prop could have replaced. Start low.


Tier 1 — Props (hide / slot views / styles)

cometchat-native-components is the full catalog. Three prop families cover most customization:

1a. hide* / *Visibility flags

Turn features off with a single prop:

<CometChatMessageList
  user={selectedUser}
  hideReplyInThreadOption     // already mandatory — see components § 11
  hideReceipts
  hideReactions={false}
  hideTranslateMessageOption
  hideMessagePrivatelyOption
  hideReplyOption={false}
/>

Full list of hide* props per component: cometchat-native-components. Check there before writing custom code.

1b. <Slot>View props — replace a section

Every component has PascalCase slot props for replacing named sections of its default UI:

<CometChatMessageHeader
  user={selectedUser}
  TitleView={(user, group) => <Text style={styles.customTitle}>{user?.getName()}</Text>}
  SubtitleView={(user, group) => <OnlineStatus user={user} />}
  LeadingView={(user, group) => <CustomAvatar user={user} />}
  TrailingView={(user, group) => <CustomActions user={user} />}
  AuxiliaryButtonView={(user, group) => <CometChatCallButtons user={user} group={group} />}
/>

Slot functions receive the same data the default view would have (typically user, group, or a single entity). They return RN JSX.

For custom views that should match the theme, use useTheme():

import { useTheme } from "@cometchat/chat-uikit-react-native";

function CustomTitle({ user }: any) {
  const theme = useTheme();
  return (
    <Text style={{
      color: theme.color.textPrimary,
      // heading3 is a VARIANT { bold, medium, regular } — read a weight, not a flat field
      // (verified vs uikit-react-native-v5 theme/default/typography.ts)
      fontFamily: theme.typography.heading3.regular.fontFamily,
      fontSize: theme.typography.heading3.regular.fontSize,
    }}>
      {user?.getName()}
    </Text>
  );
}

See cometchat-native-theming § 8 for more on useTheme().

1c. style={{ ... }} prop — nested styling

Each component accepts a nested-object style prop (see cometchat-native-components § 13):

<CometChatConversations
  style={{
    containerStyle: { backgroundColor: "#FAFAFA" },
    itemStyle: {
      avatarStyle: { containerStyle: { borderRadius: 8 } },
    },
  }}
/>

Prefer theme-level changes (via cometchat-native-theming) for app-wide color shifts; use style={{}} only for one-off overrides on a single component instance.


Tier 2 — RequestBuilder filtering

For "I want to show a subset of X", use the matching *RequestBuilder. Never post-filter in-render.

import { CometChat } from "@cometchat/chat-sdk-react-native";

// Only conversations in a specific tag group
<CometChatConversations
  conversationsRequestBuilder={
    new CometChat.ConversationsRequestBuilder()
      .setLimit(20)
      .setUserTags(["premium"])
      .setConversationType(CometChat.RECEIVER_TYPE.USER)
  }
/>

// Only online users, exclude blocked
<CometChatUsers
  usersRequestBuilder={
    new CometChat.UsersRequestBuilder()
      .setLimit(30)
      .setStatus("online")
      .setSearchKeyword("")
      .friendsOnly(false)
  }
/>

// Only groups you've joined
<CometChatGroups
  groupsRequestBuilder={
    new CometChat.GroupsRequestBuilder()
      .setLimit(30)
      .joinedOnly(true)
  }
/>

// Message list — exclude system messages
<CometChatMessageList
  user={user}
  messageRequestBuilder={
    new CometChat.MessagesRequestBuilder()
      .setUID(user.getUid())
      .setLimit(30)
      .setCategories(["message"])   // exclude "call", "action"
      .hideReplies(false)
  }
  hideReplyInThreadOption
/>

Each request builder is chainable. The @cometchat/chat-sdk-react-native exports the builder classes — import them from the SDK, not the UI Kit.

Finding the right method

Request builder methods are documented at cometchat.com/docs/sdk/react-native (or query the docs MCP). Common ones:

BuilderUseful methods
ConversationsRequestBuilder.setLimit(n), .setUserTags([...]), .setGroupTags([...]), .setConversationType(type), .withTags(true), .withUserAndGroupTags(true)
UsersRequestBuilder.setLimit(n), .setStatus("online"), .setSearchKeyword(str), .friendsOnly(bool), .setTags([...]), .setUIDs([...]), .hideBlockedUsers(bool)
GroupsRequestBuilder.setLimit(n), .setSearchKeyword(str), .joinedOnly(bool), .setTags([...]), .setGroupTypes([...])
MessagesRequestBuilder.setUID(uid) / .setGUID(guid), .setLimit(n), .setCategories([...]), .setTypes([...]), .hideReplies(bool), .setTags([...]), .setParentMessageId(id)
GroupMembersRequestBuilder.setLimit(n), .setSearchKeyword(str), .setScopes([...])

Tier 3 — Text formatters + message templates

For "change how text or messages render", Tier 3 is the right level. Two sub-patterns:

3a. Custom text formatter — inline text patterns

CometChatTextFormatter is an abstract base class for matching inline text patterns (hashtags, keywords, emoji shortcodes, custom tags) and replacing them with custom JSX.

import {
  CometChatTextFormatter,
  SuggestionItem,
} from "@cometchat/chat-uikit-react-native";
import { CometChat } from "@cometchat/chat-sdk-react-native";
import { Text, View, StyleSheet } from "react-native";

class HashtagFormatter extends CometChatTextFormatter {
  constructor() {
    super();
    this.setTrackingCharacter("#");              // optional — triggers suggestion list
    this.setRegexPatterns(/\B#(\w+)\b/g);         // takes a SINGLE RegExp, not an array (verified vs CometChatTextFormatter.ts: setRegexPatterns(regexPattern: RegExp))
  }

  // Called for each bubble's text; return string | JSX
  getFormattedText(
    inputText: string | null | React.ReactNode,
  ): string | React.ReactNode {
    if (typeof inputText !== "string") return inputText;
    const parts = inputText.split(/(\B#\w+\b)/g);
    return (
      <Text>
        {parts.map((part, i) =>
          part.match(/^#\w+$/)
            ? <Text key={i} style={styles.hashtag} onPress={() => openHashtag(part)}>{part}</Text>
            : <Text key={i}>{part}</Text>,
        )}
      </Text>
    );
  }

  // Optional — called before a message is sent. Transform the outgoing message.
  handlePreMessageSend(message: CometChat.TextMessage): CometChat.TextMessage {
    // e.g. attach the list of hashtags to the message metadata
    return message;
  }

  // Optional — for suggestion-list support (triggered by `#`)
  search(searchKey: string): void {
    // Fetch matching hashtags from your backend, then:
    // this.setSearchData([{ id: "tag1", title: "#typescript" }]);
  }
}

const styles = StyleSheet.create({
  hashtag: { color: "#2563EB", fontWeight: "600" },
});

Register the formatter by passing it to both CometChatMessageList and CometChatMessageComposer:

const formatters = [
  new CometChatMentionsFormatter(),   // keep the built-in ones
  new CometChatUrlsFormatter(),
  new HashtagFormatter(),             // add yours
];

<CometChatMessageList
  user={selectedUser}
  textFormatters={formatters}
  hideReplyInThreadOption
/>
<CometChatMessageComposer
  user={selectedUser}
  textFormatters={formatters}
/>

3b. Custom message template — entire custom bubble

For rendering a totally custom message type (interactive cards, scheduling, forms), use the kit's CometChatMessageTemplate.

Wrong-namespace trap (verified against kit source): the template class is CometChatMessageTemplate, exported from @cometchat/chat-uikit-react-native (packages/ChatUiKit/src/shared/modals/CometChatMessageTemplate.ts:116; re-exported at src/index.ts:47). There is NO CometChat.MessageTemplate on the Chat SDK — new CometChat.MessageTemplate(...) does not exist. Always import the class from the UI Kit.

The constructor takes a single options object (packages/ChatUiKit/src/shared/modals/CometChatMessageTemplate.ts:213-237). View slots are PascalCase (ContentView, BottomView, BubbleView, HeaderView, LeadingView, StatusInfoView, FooterView, ReplyView); options is lowercase. The options callback signature is (loggedInUser, message, theme, group?) — note the theme argument before group (CometChatMessageTemplate.ts:202-207, invoked at CometChatMessageList.tsx:3484 as template.options(loggedInUser, item, mergedTheme, group)).

import {
  CometChatMessageTemplate,
  CometChatUiKitConstants,
} from "@cometchat/chat-uikit-react-native";

const pollTemplate = new CometChatMessageTemplate({
  type: "poll",
  category: CometChatUiKitConstants.MessageCategoryConstants.custom,
  ContentView: (message, alignment) => (
    <PollBubble message={message} alignment={alignment} />
  ),
  BottomView: (message, alignment) => (
    <PollVoteCounts message={message} />
  ),
  options: (loggedInUser, message, theme, group) => [
    /* CometChatMessageOption[] — custom long-press menu items */
  ],
});

Register by MERGING with the defaults — never replace. The defaults accessor is getAllMessageTemplates(theme, additionalParams?) and it requires the kit theme (packages/ChatUiKit/src/shared/framework/DataSource.ts:168; impl at MessageDataSource.tsx:1732). The data source is reached via either ChatConfigurator.getDataSource() (ChatConfigurator.ts:24) or the equivalent CometChatUIKit.getDataSource() (CometChatUIKit.ts:380). Pass the theme from useTheme():

import {
  ChatConfigurator,
  useTheme,
} from "@cometchat/chat-uikit-react-native";

function Chat({ selectedUser }: any) {
  const theme = useTheme();
  // getAllMessageTemplates REQUIRES the theme arg
  const defaults = ChatConfigurator.getDataSource().getAllMessageTemplates(theme);

  return (
    <CometChatMessageList
      user={selectedUser}
      templates={[pollTemplate, ...defaults]}   // append your type, keep all built-ins
      hideReplyInThreadOption
    />
  );
}

If you pass only templates={[pollTemplate]} you will REPLACE the built-in text/image/video/file/group-action templates and the conversation will render blank for every non-custom message. Always spread ...defaults.

3c. Override an existing type's bubble (text / image)

To change how a built-in type renders (e.g. a custom text bubble), don't write a new type — get the default templates, find the matching one by type, swap its ContentView, and pass the whole array back via templates. This preserves every other type and keeps the type's default options, ReplyView, etc.

import { ChatConfigurator, useTheme, CometChatUiKitConstants } from "@cometchat/chat-uikit-react-native";

function Chat({ selectedUser }: any) {
  const theme = useTheme();
  const templates = ChatConfigurator.getDataSource().getAllMessageTemplates(theme);

  // type values come from CometChatUiKitConstants.MessageTypeConstants
  const textTemplate = templates.find(
    (t) => t.type === CometChatUiKitConstants.MessageTypeConstants.text,
  );
  if (textTemplate) {
    textTemplate.ContentView = (message, alignment) => (
      <MyCustomTextBubble message={message} alignment={alignment} />
    );
  }

  return (
    <CometChatMessageList
      user={selectedUser}
      templates={templates}   // mutated-in-place array — full default set, one view swapped
      hideReplyInThreadOption
    />
  );
}

ContentView's signature is (messageObject: CometChat.BaseMessage, alignment: MessageBubbleAlignmentType) => JSX.Element | null (CometChatMessageTemplate.ts:131-134). Branch on alignment === "left" | "right" if you want incoming-vs-outgoing variants.

3d. Add a Forward-style long-press option to an existing type

A long-press menu item is a CometChatMessageOption — a plain object type (id, title, optional icon, onPress, CustomView, style), defined at packages/ChatUiKit/src/shared/modals/CometChatMessageOption.ts:6-18, exported from src/index.ts:45.

Wrong-namespace trap: there is no CometChatActionsIcon / CometChatActionsView in the RN kit (that is the web kit's class — grep of packages/ChatUiKit/src returns zero hits). In RN, an option is the CometChatMessageOption object literal.

Append yours by overriding the type's template options callback. Call the default-options accessor first, then add your item — append, don't replace:

import {
  ChatConfigurator,
  useTheme,
  CometChatUiKitConstants,
} from "@cometchat/chat-uikit-react-native";
import type { CometChatMessageOption } from "@cometchat/chat-uikit-react-native";

function Chat({ selectedUser }: any) {
  const theme = useTheme();
  const templates = ChatConfigurator.getDataSource().getAllMessageTemplates(theme);

  const textTemplate = templates.find(
    (t) => t.type === CometChatUiKitConstants.MessageTypeConstants.text,
  );
  if (textTemplate) {
    textTemplate.options = (loggedInUser, message, t, group) => {
      // default long-press options for this message (reply, edit, delete, copy, ...)
      const defaults = ChatConfigurator.getDataSource().getMessageOptions(
        loggedInUser, message, t, group,
      );
      const forward: CometChatMessageOption = {
        id: "forward",
        title: "Forward",
        onPress: (msg) => forwardMessage(msg),
      };
      return [...defaults, forward];   // append, keep the built-ins
    };
  }

  return (
    <CometChatMessageList user={selectedUser} templates={templates} hideReplyInThreadOption />
  );
}

The default-options accessor is getMessageOptions(loggedInUser, messageObject, theme, group?, additionalParams?) (DataSource.ts:57-63; impl MessageDataSource.tsx:587). It returns CometChatMessageOption[]. The template's options callback is invoked by the list as template.options(loggedInUser, item, mergedTheme, group) (CometChatMessageList.tsx:3484).

When to use text formatter vs message template

Use formatter (Tier 3a)Use template (Tier 3b)
Change how TEXT inside a bubble renders (hashtags, URLs, mentions, emoji shortcodes)Render a completely different bubble body
Content is still a TextMessageContent is a custom message type (sent via CometChat.sendCustomMessage)
Doesn't need its own long-press optionsNeeds custom message options (vote, claim, accept, etc.)

Tier 4 — DataSource decorators + event bus

When Tiers 1-3 can't do it, you're modifying how data flows through the UI Kit. Two mechanisms:

4a. Event bus — CometChatUIEventHandler

Subscribe to events that UI Kit components emit so your own code can react.

import { CometChatUIEventHandler } from "@cometchat/chat-uikit-react-native";
import { useEffect } from "react";

function AppScreen() {
  useEffect(() => {
    const listenerId = "APP_MESSAGE_LISTENER";

    CometChatUIEventHandler.addMessageListener(listenerId, {
      ccMessageSent: ({ message, status }) => {
        // status === "inProgress" | "sent"
        analytics.track("message_sent", { id: message.getId() });
      },
      ccMessageEdited: ({ message }) => { /* ... */ },
      ccMessageDeleted: ({ message }) => { /* ... */ },
      ccMessageRead: ({ message }) => { /* ... */ },
      ccLiveReaction: ({ reaction }) => { /* ... */ },
    });

    return () => CometChatUIEventHandler.removeMessageListener(listenerId);
  }, []);

  return /* ... */;
}

Event listener API reference

ListenerUse when...
addMessageListenerreacting to any message-related event (sent, edited, deleted, read, reactions)
addConversationListenerreacting to conversation-level events (ccConversationDeleted, ccUpdateConversation)
addUserListenerreacting to user actions (ccUserBlocked, ccUserUnblocked)
addGroupListenerreacting to group lifecycle (ccGroupCreated, ccGroupDeleted, ccGroupLeft, ccGroupMemberScopeChanged, ccGroupMemberKicked, ccGroupMemberBanned, ccGroupMemberJoined, ccGroupMemberAdded, ccOwnershipChanged, etc.)
addCallListenerreacting to call events (onIncomingCallAccepted, onCallEnded, onCallInitiated, etc.)

Every pair has a matching remove*Listener(id)always call it in the cleanup of your useEffect to avoid duplicate listeners on re-render.

Listener ID uniqueness matters. Use a constant per component/feature. Colliding IDs cause only the latest-registered listener to fire.

4b. DataSource decorators

DataSourceDecorator and MessageDataSource wrap the kit's internal data source to override specific methods without forking the whole kit.

When to reach for this: overriding how user data is fetched, how conversations are sorted, adding custom message metadata to every sent message, intercepting attachment uploads.

Minimum pattern:

import {
  DataSource,
  DataSourceDecorator,
  ChatConfigurator,
} from "@cometchat/chat-uikit-react-native";

class MyDataSource extends DataSourceDecorator {
  constructor(source: DataSource) {
    super(source);
  }

  // Override only the method you want to change
  getConversationsRequestBuilder() {
    const builder = super.getConversationsRequestBuilder();
    builder.setUserAndGroupTags(true);
    return builder;
  }

  // To register a custom type kit-wide, override getAllMessageTemplates —
  // it returns the ARRAY. (getMessageTemplate(type, category, theme, ...)
  // returns a SINGLE template-or-null, so don't merge an array there.)
  getAllMessageTemplates(theme: any, additionalParams?: any) {
    const defaults = super.getAllMessageTemplates(theme, additionalParams);
    return [myCustomTemplate, ...defaults];
  }
}

// Register the decorator before init — wraps the default data source
ChatConfigurator.dataSource = new MyDataSource(ChatConfigurator.getDataSource());
await CometChatUIKit.init(settings);

This is an escape hatch, not a first tool. If you find yourself reaching for Tier 4, re-check whether Tier 1 (props) or Tier 3 (templates) could have solved it. Templates + slot views cover most "custom behavior" asks.

4c. Extensions datasource (for extension-like deep behavior)

ExtensionsDataSource is the base class for registering an extension-shaped chunk of behavior (its own composer action + its own bubble + its own data handling) — this is what PollsExtension, StickersExtension, etc. extend internally. You'd only subclass this if you're shipping a reusable feature module across apps.

For a single app, use DataSourceDecorator instead.


5. Sample app reference (when Tiers 1–4 don't have what you need)

If none of Tiers 1–4 covered the user's request, don't immediately conclude they need custom code. The RN UI Kit ships two reference sample apps that compose multiple kit components into common chat UX patterns that aren't shipped as named exports:

Bare RN: https://github.com/cometchat/cometchat-uikit-react-native/tree/v5/examples/SampleApp

Expo: https://github.com/cometchat/cometchat-uikit-react-native/tree/v5/examples/SampleAppExpo

(Use the branch matching your installed UI-Kit major version — confirm via package.json. If the user is on v5, use v5; for v6 use v6. The folder layout is identical between the two flavors — src/components, src/screens, src/utils — so the same lookup table works for both.)

Examples that look like "missing components" but are in the sample app:

User asks forSample app reference path
User / group details screenexamples/SampleApp(Expo)/src/components/CometChatDetails/ (CometChatUserDetails.tsx + group-details inline in the home screen)
Threaded messages screen layoutexamples/SampleApp(Expo)/src/components/CometChatDetails/CometChatThreadedMessages.tsx
Top-level chat shell (tabs + screens + drawer)examples/SampleApp(Expo)/src/components/CometChatHome/ + App.tsx
Multi-tab chat (Chats / Calls / Users / Groups)examples/SampleApp(Expo)/src/components/CometChatTabs/
New conversation modal with user/group pickerexamples/SampleApp(Expo)/src/components/CometChatNewChat/
Search screen (conversations + messages)examples/SampleApp(Expo)/src/components/CometChatSearch/
Call log details / history / recordingsexamples/SampleApp(Expo)/src/components/CometChatCallLog/
App-state / active-chat React contextexamples/SampleApp(Expo)/src/context/AppContext.tsx

Discovery commands (works against either repo flavor):

# List the sample app's components directory via the GitHub API (bare RN)
curl -s "https://api.github.com/repos/cometchat/cometchat-uikit-react-native/contents/examples/SampleApp/src/components?ref=v5" \
  | grep -oE '"name":\s*"[^"]+"' | head -30

# Same, for Expo
curl -s "https://api.github.com/repos/cometchat/cometchat-uikit-react-native/contents/examples/SampleAppExpo/src/components?ref=v5" \
  | grep -oE '"name":\s*"[^"]+"' | head -30

# Fetch a specific component file directly
curl -s "https://raw.githubusercontent.com/cometchat/cometchat-uikit-react-native/v5/examples/SampleApp/src/components/CometChatDetails/CometChatUserDetails.tsx"

You can also use WebFetch on the URLs above. The docs MCP does NOT index the sample apps — fetch them from GitHub directly.

If you find a matching reference implementation:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
105
Forks
2
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
cometchat-native-customization
Source
github.com/cometchat/cometchat-skills