cometchat-native-placement-9f8c36 — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited cometchat-native-placement-9f8c36 (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Teaches Claude the five canonical placement patterns for putting chat inside a React Native app. Each pattern specifies:
@react-navigation/* (or Expo Router)Ground truth: docs/ui-kit/react-native/react-native-conversation.mdx, react-native-one-to-one-chat.mdx, react-native-tab-based-chat.mdx, their expo-*.mdx equivalents, and the examples/SampleApp/ + examples/SampleAppExpo/ sample apps.
Read `cometchat-native-core` and `cometchat-native-components` before this skill — the provider wrapper chain and component catalog are prerequisites.
Use this table to pick a placement. If the user says "add chat to my app" without specifying where, ask them what they're building.
| User intent | Recommended placement | Experience |
|---|---|---|
| Messaging app (WhatsApp / Telegram / Signal style) | Conversations stack — list → tap → full-page messages screen | Two-pane-equivalent on mobile |
| SaaS / marketplace / e-commerce with chat as a feature | Stack screen — dedicated /chat or /messages route | Full-page chat inside the app |
| Support app or focused 1-to-1 | Stack screen (single thread) — no conversation list, go straight into one chat | Single thread |
| Full messaging hub with calls / users / groups | Bottom tabs — Chats / Users / Groups / Calls tabs + stack screen for message view | Tab-based messenger |
| Occasional chat overlay from a non-chat screen | Modal — present from anywhere, dismiss to return | Modal |
| Inline comments / contextual chat | BottomSheet — swipe up from a screen section | Sheet |
| Chat embedded inside an existing screen (e.g. a support tab next to product details) | Embedded — CometChat components inside a parent layout | Embedded |
┌───────────────────────────────────┐
│ ← Hiking Group ⋮ │ ← CometChatMessageHeader
├───────────────────────────────────┤
│ │
│ ╭──────────╮ │
│ │ Message │ │
│ ╰──────────╯ │ ← CometChatMessageList
│ │
│ ╭──────────╮ │
│ │ Reply │ │
│ ╰──────────╯ │
│ │
├───────────────────────────────────┤
│ + Type a message... ▶ │ ← CometChatMessageComposer
└───────────────────────────────────┘┌───────────────────────────────────┐
│ ← Hiking Group ⋮ │ ← header
├───────────────────────────────────┤
│ │
│ (messages) │
│ │
├───────────────────────────────────┤
│ Chats Users Groups Calls │ ← bottom tab bar
└───────────────────────────────────┘ ┌─────────────────┐
│ ═══ Chat ✕ │ ← drag handle + close
├─────────────────┤
│ │
│ (messages) │
│ │
├─────────────────┤
│ Type message ▶ │
└─────────────────┘
(parent screen dimmed behind)parent screen visible at top ─────
┌─────────────────┐
│ ═══ (handle) │
│ Hiking Group │
├─────────────────┤
│ (messages) │
├─────────────────┤
│ Type message ▶ │
└─────────────────┘┌───────────────────────────────────┐
│ Product details │
│ [product image + specs] │
├───────────────────────────────────┤
│ Contact seller │ ← section heading
│ ┌────────────────────────────┐ │
│ │ (CometChatMessageHeader) │ │
│ │ (CometChatMessageList) │ │ ← embedded chat
│ │ (CometChatMessageComposer) │ │
│ └────────────────────────────┘ │
└───────────────────────────────────┘The most common pattern — chat lives in its own screen, pushed via @react-navigation/native-stack.
Two screens: list + messages.
// ConversationsScreen.tsx
import { CometChatConversations, CometChatUiKitConstants } from "@cometchat/chat-uikit-react-native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
export function ConversationsScreen({ navigation }: { navigation: NativeStackNavigationProp<any> }) {
return (
<CometChatConversations
onItemPress={(conversation) => {
const type = conversation.getConversationType();
if (type === CometChatUiKitConstants.ConversationTypeConstants.user) {
navigation.navigate("Messages", { user: conversation.getConversationWith() });
} else {
navigation.navigate("Messages", { group: conversation.getConversationWith() });
}
}}
/>
);
}// MessagesScreen.tsx
import { View } from "react-native";
import {
CometChatMessageHeader,
CometChatMessageList,
CometChatMessageComposer,
} from "@cometchat/chat-uikit-react-native";
export function MessagesScreen({ route, navigation }: any) {
const { user, group } = route.params ?? {};
return (
<View style={{ flex: 1 }}>
<CometChatMessageHeader user={user} group={group} onBack={() => navigation.goBack()} showBackButton />
<CometChatMessageList user={user} group={group} hideReplyInThreadOption />
<CometChatMessageComposer user={user} group={group} />
</View>
);
}// AppNavigator.tsx
import { createNativeStackNavigator } from "@react-navigation/native-stack";
const Stack = createNativeStackNavigator();
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Conversations" component={ConversationsScreen} />
<Stack.Screen name="Messages" component={MessagesScreen} />
</Stack.Navigator>For support chat, marketplace "Contact seller", or any focused 1-to-1 where the target user/group is known in advance.
export function SupportChatScreen() {
const [agent, setAgent] = useState<CometChat.User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
CometChat.getUser("support-agent-uid")
.then((user) => {
setAgent(user);
setLoading(false);
})
.catch(() => setLoading(false));
}, []);
if (loading) return <ActivityIndicator style={{ flex: 1 }} />;
if (!agent) return <Text style={{ padding: 16 }}>Support unavailable. Try again shortly.</Text>;
return (
<View style={{ flex: 1 }}>
<CometChatMessageHeader user={agent} />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</View>
);
}<View style={{ flex: 1 }}> so the composer sits at the bottom and the list fills the middle.CometChatMessageHeader's onBack should call navigation.goBack(). Set showBackButton explicitly so the header knows to render it.KeyboardAvoidingView on iOS or android:windowSoftInputMode="adjustResize" on Android. The framework patterns (cometchat-native-expo-patterns, cometchat-native-bare-patterns) cover the platform-specific wiring.For full-featured messengers with distinct entry points per content type.
// TabsNavigator.tsx
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
const Tab = createBottomTabNavigator();
const Stack = createNativeStackNavigator();
function MainTabs() {
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="Chats" component={ConversationsScreen} />
<Tab.Screen name="Users" component={UsersScreen} />
<Tab.Screen name="Groups" component={GroupsScreen} />
<Tab.Screen name="Calls" component={CallLogsScreen} />
</Tab.Navigator>
);
}
export function AppNavigator() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Main" component={MainTabs} />
<Stack.Screen name="Messages" component={MessagesScreen} />
</Stack.Navigator>
);
}Each tab screen pushes to a shared Messages stack screen with the selected entity:
export function UsersScreen({ navigation }: any) {
return (
<CometChatUsers onItemPress={(user) => navigation.navigate("Messages", { user })} />
);
}
export function GroupsScreen({ navigation }: any) {
return (
<CometChatGroups onItemPress={(group) => navigation.navigate("Messages", { group })} />
);
}
export function CallLogsScreen() {
return <CometChatCallLogs />;
}@react-navigation/bottom-tabs. The Messages screen is OUTSIDE the tab navigator (at the stack level) so it presents full-screen without the tab bar.CometChatCallLogs only works when @cometchat/calls-sdk-react-native is installed. Omit the Calls tab if the project doesn't use calling.For occasional chat that doesn't belong in the primary navigation. Two approaches — native RN <Modal> or react-navigation's presentation: "modal".
Cleaner — the modal is a regular stack screen with a modal presentation option.
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="ChatModal"
component={ChatModalScreen}
options={{ presentation: "modal" }}
/>
</Stack.Navigator>function ChatModalScreen({ navigation }: any) {
const [agent, setAgent] = useState<CometChat.User | null>(null);
useEffect(() => { CometChat.getUser("support-agent").then(setAgent); }, []);
if (!agent) return null;
return (
<View style={{ flex: 1 }}>
<CometChatMessageHeader user={agent} onBack={() => navigation.goBack()} showBackButton />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</View>
);
}
// Trigger from anywhere:
<Button title="Contact support" onPress={() => navigation.navigate("ChatModal")} />iOS gets the native modal slide-up. Android shows a fade-in full-screen by default — if you need a swipe-to-dismiss feel, use the BottomSheet pattern instead.
<Modal> componentFor lightweight one-off modals that don't need a separate route.
import { Modal, Pressable, View } from "react-native";
const [visible, setVisible] = useState(false);
<Modal visible={visible} animationType="slide" onRequestClose={() => setVisible(false)}>
<SafeAreaView style={{ flex: 1 }}>
<View style={{ flex: 1 }}>
<CometChatMessageHeader user={agent} onBack={() => setVisible(false)} showBackButton />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</View>
</SafeAreaView>
</Modal>Works fine but bypasses navigation state — deep links and back-button handling need extra work.
Native-feel swipe-up chat overlaid on a parent screen. Two library options; pick one based on the project's existing navigation:
| Library | When to use |
|---|---|
@gorhom/bottom-sheet | Most flexible + most common. Good for partial-height sheets with snap points. |
@cometchat/chat-uikit-react-native's CometChatBottomSheet | Lightweight. Good if the project doesn't already depend on @gorhom/bottom-sheet. |
import BottomSheet, { BottomSheetView } from "@gorhom/bottom-sheet";
import { useRef, useMemo } from "react";
function ProductScreen({ product }: any) {
const sheetRef = useRef<BottomSheet>(null);
const snapPoints = useMemo(() => ["25%", "90%"], []);
const [agent, setAgent] = useState<CometChat.User | null>(null);
useEffect(() => {
CometChat.getUser(product.sellerUid).then(setAgent);
}, [product.sellerUid]);
return (
<View style={{ flex: 1 }}>
<ProductDetails product={product} />
<Button title="Contact seller" onPress={() => sheetRef.current?.expand()} />
<BottomSheet ref={sheetRef} snapPoints={snapPoints} index={-1} enablePanDownToClose>
<BottomSheetView style={{ flex: 1 }}>
{agent && (
<>
<CometChatMessageHeader user={agent} />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</>
)}
</BottomSheetView>
</BottomSheet>
</View>
);
}import { CometChatBottomSheet } from "@cometchat/chat-uikit-react-native";
const sheetRef = useRef<any>(null);
<CometChatBottomSheet ref={sheetRef}>
<View style={{ flex: 1, height: "100%" }}>
<CometChatMessageHeader user={agent} />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</View>
</CometChatBottomSheet>
<Button title="Chat" onPress={() => sheetRef.current?.show()} />@gorhom/bottom-sheet has keyboardBehavior + keyboardBlurBehavior props. Without them the composer gets covered by the keyboard on iOS. Use keyboardBehavior="interactive" + keyboardBlurBehavior="restore".<GestureHandlerRootView style={{ flex: 1 }}> at the root (already required by the UI Kit — see cometchat-native-core § 3).flex: 1 + height: "100%" on the inner View so the message list expands to fill the sheet.Chat inside an existing screen, not its own route.
export function ProductDetailScreen({ product }: any) {
const [agent, setAgent] = useState<CometChat.User | null>(null);
useEffect(() => { CometChat.getUser(product.sellerUid).then(setAgent); }, [product.sellerUid]);
return (
<ScrollView style={{ flex: 1 }} keyboardShouldPersistTaps="handled">
<ProductImages images={product.images} />
<ProductSpecs product={product} />
<View style={{ marginTop: 24 }}>
<Text style={{ fontSize: 18, fontWeight: "600", padding: 16 }}>Chat with seller</Text>
<View style={{ height: 480 }}>
{agent && (
<>
<CometChatMessageHeader user={agent} />
<CometChatMessageList user={agent} hideReplyInThreadOption />
<CometChatMessageComposer user={agent} />
</>
)}
</View>
</View>
</ScrollView>
);
}ScrollView without a bounded height, the list collapses to zero height. Wrap in a <View style={{ height: NNN }}> or flex container with an explicit height.ScrollView, the message list's internal scroll competes with the parent's scroll. Consider the single-thread-as-stack-screen pattern instead if the chat is a primary UX.keyboardShouldPersistTaps="handled" on the parent ScrollView + KeyboardAvoidingView at the root help.Usually the embedded pattern is the wrong default — prefer a Modal or BottomSheet trigger from a button on the screen, which gives users a dedicated surface for chatting.
These apply to ALL placement patterns. Violating any of them causes integration bugs or destroys the existing navigation.
cometchat-native-core § 3). Re-wrapping per screen causes duplicate init + login, dropped WebSockets, and a 2–3-second flicker on first mount.index.js (or Expo entry). Missing this import silently disables swipe gestures in the composer, bottom sheet, and attachment drawer.CometChatThreadHeader + scoped list + scoped composer with parentMessageId). Drawer / modal / bottom sheet / embedded / stack-screen placements without a thread panel must include the flag — otherwise "Reply in Thread" shows in the message menu and silently does nothing.user and group expect CometChat.User and CometChat.Group instances — not bare UID strings. Fetch via CometChat.getUser(uid) / CometChat.getGroup(guid) in a useEffect and gate the render on the resolved object.flex: 1, height: N, or inside a flex layout with flex: N), components collapse to zero height and render empty. This is THE most common "why is my chat blank" bug.translate-x-* / translate-y-* / scale-* / rotate-* utilities if using NativeWind). transform creates a new containing block for position: "absolute" descendants, which reparents CometChat's absolute-positioned overlays (emoji picker, action sheet, reactions popover) and makes them misalign. In RN this is less common than web (RN has no position: fixed) but the same rule applies to any position: absolute pickers. Animate right / left / top / bottom offsets instead.| Skill | When to route |
|---|---|
cometchat-native-core | Always first — init, login, provider wrapper chain |
cometchat-native-components | For component prop details — always |
cometchat-native-placement | This skill — picking + wiring a placement |
cometchat-native-expo-patterns | Expo-specific integration (app.json, permissions, Expo Router) |
cometchat-native-bare-patterns | Bare RN (pod install, native modules, privacy manifest) |
cometchat-native-theming | Customize colors / typography / dark mode |
cometchat-native-features | Calls, extensions, AI — the "add a feature" flow |
cometchat-native-customization | Custom slot views, text formatters, events |
cometchat-native-production | Server-side auth tokens |
cometchat-native-troubleshooting | Blank chat / gestures not working / keyboard covering composer / pod install fails |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.