Platform docs
Auth, users, webhooks & more
Info:
This is beta documentation for Stream Video React Native SDK v2. For the latest stable version, see the latest version (v1).

Chat Integration

Stream's Chat and Video SDKs integrate seamlessly for calling apps with chat or chat apps with calling features.

Installation

This guide uses stream-chat-react-native v10 and stream-chat v10. Install them following the chat SDK's installation guide:

yarn add stream-chat-react-native@10 stream-chat@10

The chat SDK needs React Native 0.79 or later. If your chat code is on an earlier version, follow the upgrade guide first.

Creating and Passing Chat Client

The chat SDK's Quick start explains how the chat client is created and the user connected.

Wrapping the chat client

Create a ChatWrapper component at the app root to provide the client to all children:

src/components/ChatWrapper.tsx
import React, { PropsWithChildren } from "react";
import { ActivityIndicator, StyleSheet, View } from "react-native";
import {
  Chat,
  OverlayProvider,
  Streami18n,
  useCreateChatClient,
} from "stream-chat-react-native";

const STREAM_API_KEY = "your-api-key";
const user = { id: "your-user-id", name: "your-user-name" };
const token = "your-user-token";

const streami18n = new Streami18n({ language: "en" });

export const ChatWrapper = ({ children }: PropsWithChildren) => {
  const chatClient = useCreateChatClient({
    apiKey: STREAM_API_KEY,
    tokenOrProvider: token,
    userData: user,
  });

  if (!chatClient) {
    // Show a loader until the Chat client connects
    return (
      <View style={styles.container}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  return (
    <OverlayProvider i18nInstance={streami18n}>
      <Chat client={chatClient} i18nInstance={streami18n}>
        {children}
      </Chat>
    </OverlayProvider>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: "center", alignItems: "center" },
});

userData is the user to connect: only id is required, and a profile picture goes in image.

Note:

It is essential for you to wrap the GestureHandlerRootView, OverlayProvider and Chat component on the root of your chat integration hierarchy.

GestureHandlerRootView should be wrapped into the root of your application as follows:

App.tsx
import React from "react";
import { StyleSheet } from "react-native";
import { StreamCall, StreamVideo } from "@stream-io/video-react-native-sdk";
import { VideoCallUI } from "src/components/VideoCallUI";
import { GestureHandlerRootView } from "react-native-gesture-handler";

export default function App() {
  // ...

  return (
    <GestureHandlerRootView style={styles.container}>
      <StreamVideo client={client}>
        <StreamCall call={call}>
          <VideoCallUI />
        </StreamCall>
      </StreamVideo>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
});

Adding Chat into Video

Step 1: Wrapping the ChatWrapper

Wrap ChatWrapper with StreamVideo from the Video Calling Tutorial:

App.tsx
import React from "react";
import { StyleSheet } from "react-native";
import { StreamCall, StreamVideo } from "@stream-io/video-react-native-sdk";
import { VideoCallUI } from "./components/VideoCallUI";
import { ChatWrapper } from "./src/components/ChatWrapper";
import { GestureHandlerRootView } from "react-native-gesture-handler";

export default function App() {
  // ...

  return (
    <ChatWrapper>
      <GestureHandlerRootView style={styles.container}>
        <StreamVideo client={client}>
          <StreamCall call={call}>
            <VideoCallUI />
          </StreamCall>
        </StreamVideo>
      </GestureHandlerRootView>
    </ChatWrapper>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
});

Step 2: Creating the Chat window/screen

Use Channel, MessageList, and MessageComposer components. Use call ID as channel ID for distinction. Channel starts watching the channel when it mounts.

Preview of the Chat Screen
src/ChatScreen.tsx
import React, { useMemo } from "react";
import { StyleSheet, View } from "react-native";
import {
  Channel,
  MessageComposer,
  MessageList,
  useChatContext,
} from "stream-chat-react-native";

const CHANNEL_TYPE = "messaging";
const CHANNEL_ID = "your-call-id";

export const ChatScreen = () => {
  const { client } = useChatContext();
  const channel = useMemo(
    () => client.channel(CHANNEL_TYPE, CHANNEL_ID),
    [client],
  );

  return (
    <View style={styles.wrapper}>
      <Channel channel={channel}>
        <MessageList />
        <MessageComposer />
      </Channel>
    </View>
  );
};

const styles = StyleSheet.create({
  wrapper: { flex: 1 },
});
Note:

When the chat screen sits below a navigation header, pass the header's height to Channel as keyboardVerticalOffset and topInset, so the composer and the attachment picker stay clear of it. Keyboard and insets explains both.

Step 3: Open the chat window

Create custom CallControls with ChatButton. The onPressHandler opens ChatScreen.

Preview of the call controls with chat button
src/components/VideoCallUI.tsx
import React, { useCallback } from "react";
import { SafeAreaView, StyleSheet, View } from "react-native";
import {
  CallContent,
  CallControlsButton,
  HangUpCallButton,
  ToggleAudioPublishingButton,
  ToggleCameraFaceButton,
  ToggleVideoPublishingButton,
} from "@stream-io/video-react-native-sdk";

type ChatButtonProps = {
  onPressHandler?: () => void;
};

export const ChatButton = ({ onPressHandler }: ChatButtonProps) => {
  return (
    <CallControlsButton onPress={onPressHandler}>
      {/* Your Chat Icon here */}
    </CallControlsButton>
  );
};

export type CallControlsComponentProps = {
  onChatOpenHandler?: () => void;
  onHangupCallHandler?: (err?: Error) => void;
};

export const CallControlsComponent = ({
  onChatOpenHandler,
  onHangupCallHandler,
}: CallControlsComponentProps) => {
  return (
    <View style={styles.callControlsWrapper}>
      <ChatButton onPressHandler={onChatOpenHandler} />
      <ToggleVideoPublishingButton />
      <ToggleAudioPublishingButton />
      <ToggleCameraFaceButton />
      <HangUpCallButton onHangupCallHandler={onHangupCallHandler} />
    </View>
  );
};

export const VideoCallUI = () => {
  const onChatOpenHandler = useCallback(() => {
    // handle how to open the Chat window/screen.
    // You can use this to navigate to the chat screen, open a bottom sheet modal, etc.
  }, []);

  const CustomControlsComponent = useCallback(() => {
    return <CallControlsComponent onChatOpenHandler={onChatOpenHandler} />;
  }, [onChatOpenHandler]);

  return (
    <SafeAreaView style={styles.container}>
      <CallContent CallControls={CustomControlsComponent} />
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: "#272A30" },
  callControlsWrapper: {
    flexDirection: "row",
    justifyContent: "space-evenly",
    paddingVertical: 12,
    zIndex: 2,
    backgroundColor: "#272A30",
  },
});

Step 4: Showing unread count indicator for new chats

Watch the channel and read the current user's unread count from the channel's state. The chat client keeps the count up to date while the channel is watched, and resets it when the user reads the messages.

Preview of the unread count in call controls
src/hooks/useUnreadCount.tsx
import { useEffect, useMemo } from "react";
import type { ChannelStateData } from "stream-chat";
import { useChatContext, useStateStore } from "stream-chat-react-native";
import { useCall } from "@stream-io/video-react-native-sdk";

const CHANNEL_TYPE = "messaging";

const readSelector = (state: ChannelStateData) => ({ read: state.read });

/**
 * Returns the current user's unread message count in the call's channel.
 */
export const useUnreadCount = () => {
  const { client } = useChatContext();
  const call = useCall();
  const callId = call?.id;

  const channel = useMemo(
    () => (callId ? client.channel(CHANNEL_TYPE, callId) : undefined),
    [client, callId],
  );

  // Watch the channel, so the client keeps its state up to date
  useEffect(() => {
    if (!channel) return;
    channel.watch().catch((error) => {
      console.warn("Failed to watch the channel", error);
    });
  }, [channel]);

  const { read } = useStateStore(channel?.state, readSelector) ?? {};

  if (!client.userId || !read) return 0;
  return read[client.userId]?.unread_messages ?? 0;
};
Note:

client.channel() returns the same Channel instance to every component that asks for that channel, including the chat screen. The hook therefore doesn't call Channel.stopWatching() when it un-mounts: that would stop watching the channel for the chat screen too. Channel.watch() rejects when the client isn't connected, so handle its error.

Pass unread count via unreadBadgeCountIndicator in CallControls:

src/components/VideoCallUI.tsx
import React, { useCallback } from "react";
import { SafeAreaView, StyleSheet, Text, View } from "react-native";
import {
  CallContent,
  CallControlsButton,
  HangUpCallButton,
} from "@stream-io/video-react-native-sdk";
import { useUnreadCount } from "../hooks/useUnreadCount";

export const BadgeCountIndicator = ({
  count,
}: {
  count: number | undefined;
}) => {
  // Don't show badge if count is 0 or undefined
  if (!count) return null;

  return (
    <View style={styles.badge}>
      <Text style={styles.badgeText}>{count}</Text>
    </View>
  );
};

type ChatButtonProps = {
  onPressHandler?: () => void;
  unreadBadgeCount?: number;
};

export const ChatButton = ({
  onPressHandler,
  unreadBadgeCount,
}: ChatButtonProps) => {
  return (
    <CallControlsButton onPress={onPressHandler}>
      {/* Your Chat Icon here */}
      <BadgeCountIndicator count={unreadBadgeCount} />
    </CallControlsButton>
  );
};

export type CallControlsComponentProps = {
  onChatOpenHandler?: () => void;
  onHangupCallHandler?: (err?: Error) => void;
  unreadBadgeCount?: number;
};

export const CallControlsComponent = ({
  onChatOpenHandler,
  onHangupCallHandler,
  unreadBadgeCount,
}: CallControlsComponentProps) => {
  return (
    <View style={styles.callControlsWrapper}>
      {/* ... */}
      <ChatButton
        onPressHandler={onChatOpenHandler}
        unreadBadgeCount={unreadBadgeCount}
      />
      <HangUpCallButton onHangupCallHandler={onHangupCallHandler} />
    </View>
  );
};

export const VideoCallUI = () => {
  const unreadBadgeCount = useUnreadCount();
  const onChatOpenHandler = useCallback(() => {
    // handle how to open the Chat window/screen.
    // You can use this to navigate to the chat screen, open a modal, etc.
  }, []);

  const CustomControlsComponent = useCallback(() => {
    return (
      <CallControlsComponent
        onChatOpenHandler={onChatOpenHandler}
        unreadBadgeCount={unreadBadgeCount}
      />
    );
  }, [onChatOpenHandler, unreadBadgeCount]);

  return (
    <SafeAreaView style={styles.container}>
      <CallContent CallControls={CustomControlsComponent} />
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1 },
  callControlsWrapper: {
    flexDirection: "row",
    justifyContent: "space-evenly",
    paddingVertical: 12,
  },
  badge: {
    borderRadius: 10,
    paddingHorizontal: 5,
    paddingVertical: 2,
    backgroundColor: "#20E070",
  },
  badgeText: { fontSize: 12, fontWeight: "bold", color: "#000000" },
});

Adding Video into Chat

Add video calling to chat for messenger-like functionality.

Channel list screen
Channel screen
Incoming Call
Outgoing Call
Active Call

Use ChatWrapper from earlier setup.

Step 1: Setting up navigation

Use React Navigation for navigation.

Installing the dependencies

Install native stack navigator packages:

yarn add @react-navigation/native @react-navigation/elements @react-navigation/native-stack react-native-screens react-native-safe-area-context

Install pods:

npx pod-install
Info:

You can also follow the Getting started guide to install the React Navigation dependencies effectively.

Wrapping the App in NavigationContainer

Wrap with NavigationContainer for state management:

App.tsx
import React from "react";
import { StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { NavigationContainer } from "@react-navigation/native";

export default function App() {
  // ...

  return (
    <GestureHandlerRootView style={styles.container}>
      <NavigationContainer>{/* Rest of the code */}</NavigationContainer>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
});

Step 2: Creating the screens

Create three screens:

  • Channel list screen - Lists all channels
  • Channel screen - Messages, composer, and call button
  • Thread screen - Replies to a message

The channel screen gets the channel's cid as a route param, and the thread screen gets the thread's id. Set up screen types:

src/types.ts
export type NavigationStackParamsList = {
  ChannelListScreen: undefined;
  ChannelScreen: { cid: string };
  ThreadScreen: { threadId: string };
};

Channel List Screen:

Preview of the Channel List screen

The ChannelList component renders a ChannelPaginator: it holds the filters and sort order of the list. Register it on the client's channel manager, which keeps the list up to date:

src/screens/ChannelListScreen.tsx
import React, { useEffect, useMemo } from "react";
import { StyleSheet, View } from "react-native";
import { ChannelPaginator } from "stream-chat";
import type { ChannelFilters, SortParamRequest } from "stream-chat";
import { ChannelList, useChatContext } from "stream-chat-react-native";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import type { NavigationStackParamsList } from "src/types";

const sort: SortParamRequest[] = [
  { field: "last_message_at", direction: -1 },
  // Your sort options
];

type ChannelListScreenProps = NativeStackScreenProps<
  NavigationStackParamsList,
  "ChannelListScreen"
>;

export function ChannelListScreen({ navigation }: ChannelListScreenProps) {
  const { client } = useChatContext();

  const paginator = useMemo(() => {
    const filters: ChannelFilters = {
      type: "messaging",
      members: { $in: [client.userId ?? ""] },
      // Your filters
    };
    return new ChannelPaginator({ client, filters, id: "my-channels", sort });
  }, [client]);

  useEffect(() => {
    client.channelManager.insertPaginator({ paginator });
    return () => {
      client.channelManager.removePaginator(paginator);
      paginator.dispose();
    };
  }, [client, paginator]);

  return (
    <View style={styles.container}>
      <ChannelList
        onSelect={(channel) => {
          navigation.navigate("ChannelScreen", { cid: channel.cid });
        }}
        paginator={paginator}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
});

Channel Screen:

Preview of the Channel screen

The channel list's paginator keeps the channel in client.channelManager, so the channel screen looks it up by its cid. Opening a message's replies navigates to the thread screen with the thread's id:

src/screens/ChannelScreen.tsx
import React, { useEffect } from "react";
import { useHeaderHeight } from "@react-navigation/elements";
import {
  Channel,
  MessageComposer,
  MessageList,
  useChatContext,
  useOverlayContext,
} from "stream-chat-react-native";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import type { NavigationStackParamsList } from "src/types";
import { Platform, StyleSheet, View } from "react-native";

type ChannelScreenProps = NativeStackScreenProps<
  NavigationStackParamsList,
  "ChannelScreen"
>;

export function ChannelScreen({ navigation, route }: ChannelScreenProps) {
  const { client } = useChatContext();
  const channel = client.channelManager.get(route.params.cid);
  const headerHeight = useHeaderHeight();
  const { overlay } = useOverlayContext();

  useEffect(() => {
    navigation.setOptions({
      gestureEnabled: Platform.OS === "ios" && overlay === "none",
    });
  }, [navigation, overlay]);

  if (!channel) return null;

  return (
    <Channel
      channel={channel}
      keyboardVerticalOffset={headerHeight}
      topInset={headerHeight}
    >
      <View style={styles.container}>
        <MessageList
          onThreadSelect={(thread) => {
            navigation.navigate("ThreadScreen", { threadId: thread.id });
          }}
        />
        <MessageComposer />
      </View>
    </Channel>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
});

Uses Channel, MessageList, MessageComposer.

Thread Screen:

The client keeps a thread it opened, so the thread screen looks it up by its id. Thread takes the same keyboardVerticalOffset and topInset as Channel:

src/screens/ThreadScreen.tsx
import React from "react";
import { useHeaderHeight } from "@react-navigation/elements";
import {
  MessageComposer,
  MessageList,
  Thread,
  useChatContext,
} from "stream-chat-react-native";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import type { NavigationStackParamsList } from "src/types";

type ThreadScreenProps = NativeStackScreenProps<
  NavigationStackParamsList,
  "ThreadScreen"
>;

export function ThreadScreen({ route }: ThreadScreenProps) {
  const { client } = useChatContext();
  const thread = client.threads.get(route.params.threadId);
  const headerHeight = useHeaderHeight();

  if (!thread) return null;

  return (
    <Thread
      keyboardVerticalOffset={headerHeight}
      thread={thread}
      topInset={headerHeight}
    >
      <MessageList />
      <MessageComposer />
    </Thread>
  );
}

Set up Native Stack Navigator:

App.tsx
import React from "react";
import { StyleSheet } from "react-native";
import { NavigationStackParamsList } from "./src/types";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { NavigationContainer } from "@react-navigation/native";
import { ChannelListScreen } from "./src/screens/ChannelListScreen";
import { ChannelScreen } from "./src/screens/ChannelScreen";
import { ThreadScreen } from "./src/screens/ThreadScreen";

const Stack = createNativeStackNavigator<NavigationStackParamsList>();

const Messenger = () => {
  return (
    <Stack.Navigator>
      <Stack.Screen name="ChannelListScreen" component={ChannelListScreen} />
      <Stack.Screen name="ChannelScreen" component={ChannelScreen} />
      <Stack.Screen name="ThreadScreen" component={ThreadScreen} />
    </Stack.Navigator>
  );
};

export default function App() {
  // ...

  return (
    <GestureHandlerRootView style={styles.container}>
      <NavigationContainer>
        <Messenger />
      </NavigationContainer>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
});

Step 3: Wrapping the ChatWrapper

Wrap ChatWrapper to provide chat client:

App.tsx
import React from "react";
import { StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { NavigationContainer } from "@react-navigation/native";
import { ChatWrapper } from "./src/components/ChatWrapper";
// ...

export default function App() {
  // ...

  return (
    <GestureHandlerRootView style={styles.container}>
      <NavigationContainer>
        <ChatWrapper>
          <Messenger />
        </ChatWrapper>
      </NavigationContainer>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
});

Step 4: Creating and passing the Video Wrapper

Create a video wrapper per the Video Call Tutorial:

src/components/VideoWrapper.tsx
import React, { PropsWithChildren, useEffect, useState } from "react";
import {
  StreamVideo,
  StreamVideoClient,
} from "@stream-io/video-react-native-sdk";

const STREAM_API_KEY = "your-api-key";
const user = { id: "your-user-id", name: "your-user-name" };
const token = "your-user-token";

export const VideoWrapper = ({ children }: PropsWithChildren) => {
  const [videoClient, setVideoClient] = useState<StreamVideoClient>();

  useEffect(() => {
    const _videoClient = new StreamVideoClient({
      apiKey: STREAM_API_KEY,
      user,
      token,
    });

    setVideoClient(_videoClient);

    return () => {
      _videoClient.disconnectUser().catch((error) => {
        console.error("Failed to disconnect", error);
      });
      setVideoClient(undefined);
    };
  }, []);

  if (!videoClient) return null;

  return (
    <StreamVideo client={videoClient} language={"en"}>
      {children}
    </StreamVideo>
  );
};

Wrap VideoWrapper:

App.tsx
import React from "react";
import { StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { NavigationContainer } from "@react-navigation/native";
import { ChatWrapper } from "./src/components/ChatWrapper";
import { VideoWrapper } from "./src/components/VideoWrapper";
// ...

export default function App() {
  // ...

  return (
    <GestureHandlerRootView style={styles.container}>
      <NavigationContainer>
        <ChatWrapper>
          <VideoWrapper>
            <Messenger />
          </VideoWrapper>
        </ChatWrapper>
      </NavigationContainer>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
});

Step 5: Passing the call to StreamCall

Use useCalls for pending calls and StreamCall to provide call context:

src/components/Calls.tsx
import React, { useCallback, useEffect } from "react";
import { StreamCall, useCalls } from "@stream-io/video-react-native-sdk";
import { Alert } from "react-native";

export const Calls = () => {
  const calls = useCalls();

  const handleMoreCalls = useCallback(() => {
    const lastCallCreatedBy = calls[1].state.createdBy;
    Alert.alert(
      `Incoming call from ${
        lastCallCreatedBy?.name ?? lastCallCreatedBy?.id
      }, only 1 call at a time is supported`,
    );
  }, [calls]);

  // Reset the state of the show variable when there are no calls.
  useEffect(() => {
    if (calls.length > 1) {
      handleMoreCalls();
    }
  }, [calls.length, handleMoreCalls]);

  const firstCall = calls[0];

  if (!firstCall) return null;

  return <StreamCall call={firstCall}>{/* Rest of the code */}</StreamCall>;
};
Note:

We currently support one call at a time, and therefore we show an alert, if a user gets multiple calls while they are in the call already. You can handle it in a different way.

This is the reason why we pass calls[0] to the StreamCall component, for now.

Step 6: Using the RingingCallContent

Use RingingCallContent to handle Incoming, Outgoing, CallPreparing, and CallContent states:

src/components/Calls.tsx
import React from "react";
import { StyleSheet, View } from "react-native";
import {
  RingingCallContent,
  StreamCall,
  useCalls,
} from "@stream-io/video-react-native-sdk";

export const Calls = () => {
  const calls = useCalls();

  // ...

  const firstCall = calls[0];

  if (!firstCall) return null;

  return (
    <StreamCall call={firstCall}>
      <View style={StyleSheet.absoluteFill}>
        <RingingCallContent />
      </View>
    </StreamCall>
  );
};

Render Calls after the navigator, inside VideoWrapper. It fills the screen, so a call shows above the chat screens:

App.tsx
import React from "react";
import { StyleSheet } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { NavigationContainer } from "@react-navigation/native";
import { ChatWrapper } from "./src/components/ChatWrapper";
import { VideoWrapper } from "./src/components/VideoWrapper";
import { Calls } from "./src/components/Calls";
// ...

export default function App() {
  // ...

  return (
    <GestureHandlerRootView style={styles.container}>
      <NavigationContainer>
        <ChatWrapper>
          <VideoWrapper>
            <Messenger />
            <Calls />
          </VideoWrapper>
        </ChatWrapper>
      </NavigationContainer>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
});

Step 7: Adding a call button to the Channel Header

Add a call button to the channel screen's header with the headerRight option of Stack.Screen. It gets the channel's cid from the route params:

Preview of the Channel Screen Header
<Stack.Screen
  name="ChannelScreen"
  component={ChannelScreen}
  options={({ route }) => ({
    headerRight: () => <CreateCallButton cid={route.params.cid} />,
  })}
/>

CreateCallButton implementation. It rings every member of the channel:

src/components/CreateCallButton.tsx
import React, { useCallback } from "react";
import { Pressable, StyleSheet } from "react-native";
import { useChatContext } from "stream-chat-react-native";
import {
  type MemberRequest,
  useStreamVideoClient,
} from "@stream-io/video-react-native-sdk";

export const CreateCallButton = ({ cid }: { cid: string }) => {
  const { client } = useChatContext();
  const videoClient = useStreamVideoClient();

  const createCallHandler = useCallback(async () => {
    const channel = client.channelManager.get(cid);
    if (!videoClient || !channel) return;

    const members = Object.values(channel.state.members).map<MemberRequest>(
      (member) => ({ user_id: member.user_id! }),
    );

    try {
      // use a unique call ID, such as a UUID
      const call = videoClient.call("default", "random-call-id");
      await call.getOrCreate({
        ring: true,
        data: {
          // more timeout to cancel the call automatically so that it works when callee's app is in quit state
          settings_override: {
            ring: {
              auto_cancel_timeout_ms: 30000,
              incoming_call_timeout_ms: 5000,
            },
          },
          custom: { channelCid: cid },
          members,
        },
      });
    } catch (error) {
      console.log("Failed to createCall", error);
    }
  }, [client, cid, videoClient]);

  return (
    <Pressable onPress={createCallHandler} style={styles.icon}>
      {/* Call icon component */}
    </Pressable>
  );
};

const styles = StyleSheet.create({
  icon: { height: 18, width: 18 },
});

Pressing the call button sends an outgoing call. Recipients can accept/reject via the Incoming call screen.

Further Steps

Use this template for chat-primary apps with video features.

Related guides: