# Chat Integration

Embed video calling into a chat application with ring call support.

## Best Practices

- Use the same API key for both chat and video clients - no separate apps needed.
- Use unique call IDs for ring calls (UUID recommended).
- Filter `useCalls()` by `call.ringing` to show pending calls only.
- Use `call.isCreatedByMe` to distinguish incoming vs outgoing calls.
- Handle all calling states: `RINGING`, `JOINING`, `JOINED`.
- Clean up call instances properly with `call.leave()` in useEffect cleanup.

See the [demo application](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/messenger-clone) for complete implementation.

## Project setup and prerequisites

Make sure you have the following prerequisites checked:

1. [Registered Stream account](https://getstream.io/signup/?product=video)
2. Have an app created in the [Stream's dashboard](https://getstream.io/signin/?product=video) to obtain app API key and secret.
3. Initiate the project (you can follow our [introductory tutorial setup guide](https://getstream.io/video/sdk/react/tutorial/video-calling/#step-1---set-up-your-project-and-credentials))
4. Have installed the Stream video and chat SDKs in the project:

```shell
npm install @stream-io/video-react-sdk@beta stream-chat-react stream-chat
```

```shell
yarn add @stream-io/video-react-sdk@beta stream-chat-react stream-chat
```

>
> **Tip:** When implementing a ring call scenario as we do in this demo, it is important to have a good understanding of our **ring call lifecycle**. You can learn more about the topic in the [Joinging & Creating Calls guide](https://getstream.io/video/docs/react/v2/guides/joining-and-creating-calls/).
>

## App boilerplate

We have prepared a [demo application](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/messenger-clone) to accompany this guide. We do not aim to explain the whole demo application source code. The demo application will serve us to demonstrate the main concepts behind the video-in-chat integration.

>
> **Note:** To initiate chat and video clients you are encouraged to use the same API key. The user tokens should be generated with the same secret. **There is no need to create separate apps for chat and video.**
>

note

## Initiating chat and video clients

```tsx
import type { UserResponse } from "stream-chat";
import { Chat } from "stream-chat-react";
import { StreamVideo, StreamVideoClient } from "@stream-io/video-react-sdk";
import { Channel } from "./components/Channel";
import { Sidebar } from "./components/Sidebar";
import { Video } from "./components/Video";
import { useCreateChatClient } from "./hooks";

import { useState } from "react";

const Root = ({
  apiKey,
  user,
  userToken,
}: {
  apiKey: string;
  user: UserResponse;
  userToken: string;
}) => {
  const chatClient = useCreateChatClient({
    apiKey,
    tokenOrProvider: userToken,
    userData: user,
  });
  const [videoClient, setVideoClient] = useState<StreamVideoClient>();

  useEffect(() => {
    const _client = new StreamVideoClient({ apiKey, user, token: userToken });
    setVideoClient(_client);

    return () => {
      _client.disconnectUser();
      setVideoClient(undefined);
    };
  }, []);

  if (!chatClient || !videoClient) return null;

  return (
    <Chat client={chatClient}>
      <StreamVideo client={videoClient}>
        <Sidebar user={user} />
        <Channel />
        <Video />
      </StreamVideo>
    </Chat>
  );
};
```

## Initiating a ring call

In the ring call scenario we recommend to first create a call without immediately joining it. Use the `Call` method `getOrCreate()` to accomplish this. An example can be found in [`CreateCallButton` component](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/messenger-clone/src/components/CreateCallButton/CreateCallButton.ts) in the demo app:

```tsx {16,19-21}
import { useCallback } from "react";
import {
  type MemberRequest,
  useStreamVideoClient,
} from "@stream-io/video-react-sdk";
import { useChannelStateContext } from "stream-chat-react";
import { LocalPhone } from "@mui/icons-material";
import { meetingId } from "../../utils/meetingId";

export const CreateCallButton = () => {
  const videoClient = useStreamVideoClient();
  const { channel } = useChannelStateContext();

  const createCall = () => {
    videoClient?.call("default", meetingId()).getOrCreate({
      ring: true,
      data: {
        custom: { channelCid: channel.cid },
        members: Object.values(channel.state.members).map<MemberRequest>(
          (member) => ({ user_id: member.user_id! }),
        ),
      },
    });
  };

  const disableCreateCall = !videoClient;
  return (
    <button
      className="rmc__button rmc__button--green"
      disabled={disableCreateCall}
      onClick={createCall}
    >
      <LocalPhone />
    </button>
  );
};
```

>
> **Note:** There is a flexibility in what channel members will be included in the call. And so the call can be a 1:1 or a group call. In our implementation we include all channel members.
>

## Handling the ring call states

Once a ring call is initiated, call members start to receive ring call events (`call.created`, `call.accepted`, `call.rejected`, `call.ended`) over the WebSocket maintained by the video client. The video client updates the **calls pool** state and **calling state** of individual affected `Call` in response to these events.

### Observing the calls pool state

The array of all `Call` objects representing created pending (not accepted, rejected, neither ended) calls is continuously updated in response to each new call creation. You can use this array of `Call` objects to display **incoming** or **outgoing calls** in your application's UI. In our app the top-level [`Video`](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/messenger-clone/src/components/Video/Video.tsx) component observes the changes using the `useCalls` hook and re-renders the UI to reflect the changes:

```tsx {5}
import { StreamCall, useCalls } from "@stream-io/video-react-sdk";
import { CallPanel } from "./CallPanel";

export const Video = () => {
  const calls = useCalls().filter((call) => call.ringing);
  return (
    <>
      {calls.map((call) => (
        <StreamCall call={call} key={call.cid}>
          <CallPanel />
        </StreamCall>
      ))}
    </>
  );
};
```

>
> **Note:** To identify an outgoing call, use the `call.isCreatedByMe` flag.
>

In our demo app, we reflect the incoming calls state in channel list. Channel preview shows buttons to accept or reject the incoming call. The outgoing call or incoming call in an active channel is represented by a [`CallPanel` component](https://getstream.io/video/docs/react/v2/ui-components/call/ringing-call/) floating above the chat UI:

![Image of incoming calls in channel preview](https://getstream.io/docs-assets/images/a152e72f6ce1.png)

This is done by embedding [custom component `ChannelPreviewCallControls`](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/messenger-clone/src/components/ChannelPreview/ChannelPreviewCallControls.tsx) in a [`ChannelPreview` component](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/messenger-clone/src/components/ChannelPreview/ChannelPreview.tsx):

```tsx {12}
import {
  AcceptCallButton,
  CallingState,
  CancelCallButton,
  useCall,
  useCallStateHooks,
} from "@stream-io/video-react-sdk";
import { useChatContext } from "stream-chat-react";

export const ChannelPreviewCallControls = () => {
  const { channel: activeChannel } = useChatContext();
  // the Call instance is passed down from StreamCallProvider located in StreamCall
  const call = useCall();
  const { useCallCallingState } = useCallStateHooks();
  const callingState = useCallCallingState();

  const callingToActiveChannel =
    activeChannel && call && activeChannel.cid === call.state.custom.channelCid;

  const isRinging = callingState === CallingState.RINGING;

  if (call && isRinging && !callingToActiveChannel) {
    return (
      <div className="rmc__channel-preview__call-controls">
        <AcceptCallButton onClick={() => call.join()} />
        <CancelCallButton
          onClick={(e) => {
            const reason = call.isCreatedByMe ? "cancel" : "decline";
            call.leave({ reject: true, reason });
          }}
        />
      </div>
    );
  }
  return null;
};
```

### Observing the state of a specific call

Each call can pass through different states. The call calling state is made available through `useCallCallingState` hook. Therefore, our [custom `CallPanel` component](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/messenger-clone/src/Video/CallPanel.tsx) displays different UI (pending call, active call) based on the information provided by the hook:

```tsx {3,20,31,34,53}
import {
  RingingCall,
  CallingState,
  SpeakerLayout,
  useCall,
  ScreenShareButton,
  SpeakingWhileMutedNotification,
  ToggleAudioPublishingButton,
  ToggleVideoPublishingButton,
  CancelCallButton,
  useCallStateHooks,
} from "@stream-io/video-react-sdk";
import { useChatContext } from "stream-chat-react";
import { useState } from "react";
import { useDraggable } from "../../hooks";

export const CallPanel = () => {
  const call = useCall();
  const { useCallCallingState, useCallCustomData } = useCallStateHooks();
  const callingState = useCallCallingState();
  const customData = useCallCustomData();

  const { channel: activeChannel } = useChatContext();
  const [panelElement, setPanelElement] = useState<HTMLDivElement | null>(null);
  useDraggable(panelElement);

  if (!call) return null;

  const callingToActiveChannel = activeChannel?.cid === customData.channelCid;

  if (CallingState.RINGING === callingState && !callingToActiveChannel)
    return null;

  if (callingState === CallingState.JOINED) {
    return (
      <div
        className="str-video__call-panel rmc__call-panel-wrapper"
        ref={setPanelElement}
      >
        <SpeakerLayout />
        <div className="rmc__active-call-controls">
          <ScreenShareButton />
          <SpeakingWhileMutedNotification>
            <ToggleAudioPublishingButton />
          </SpeakingWhileMutedNotification>
          <ToggleVideoPublishingButton />
          <CancelCallButton />
        </div>
      </div>
    );
  }

  if ([CallingState.RINGING, CallingState.JOINING].includes(callingState)) {
    return (
      <div className="rmc__call-panel-wrapper" ref={setPanelElement}>
        <RingingCall />
      </div>
    );
  }

  return null;
};
```

## Terminating a call

What call termination means depends on the perspective:

1. A user can reject an incoming pending call.
2. A user can end / cancel own outgoing pending call.
3. A call participant (who joined a call) can leave a call.

In case of the group call scenario, a single user rejecting an incoming call or participant leaving a call does not terminate the call for anybody else. However, a user who initiated a pending call will end it for everybody if it has not been joined by others yet. Call is ended also, if the last member rejected the incoming call.

A user who rejected or left a call can re-join the same call again as long as the call has not been ended. The call cannot be re-joined only if ended.

So depending on the context, we associate a bit different click handlers with the button, that terminates the call for the current user:

**Reject an incoming call**
Our [`RingingCallControls`](https://getstream.io/video/docs/react/v2/ui-components/call/ringing-call/#ringing-call-controls) attaches the following callback to the [`CancelCallButton`](https://getstream.io/video/docs/react/v2/ui-components/call/call-controls/#cancelcallbutton) button that rejects the incoming call:

```tsx
<CancelCallButton
  onClick={() => call.leave({ reject: true, reason: "decline" })}
/>
```

**End/Cancel an outgoing call**
Our [`RingingCallControls`](https://getstream.io/video/docs/react/v2/ui-components/call/ringing-call/#ringing-call-controls) attaches the following callback to the [`CancelCallButton`](https://getstream.io/video/docs/react/v2/ui-components/call/call-controls/#cancelcallbutton) button that cancels the outgoing call:

```tsx
<CancelCallButton
  onClick={() => call.leave({ reject: true, reason: "cancel" })}
/>
```

**Leave already joined call**
The default callback of the [`CancelCallButton`](https://getstream.io/video/docs/react/v2/ui-components/call/call-controls/#cancelcallbutton) button is pure `call.leave()` without any parameters. Again the [`RingingCallControls`](https://getstream.io/video/docs/react/v2/ui-components/call/ringing-call/#ringing-call-controls) as well as the demo's active call panel components make use of this by not overriding the default `onClick`:

```tsx
<CancelCallButton />
```

## Adding default styles

In order the default styles that come with both the chat and video SDKs can be applied, we import them into the file [`index.scss`](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/messenger-clone/src/style/index.scss):

```scss
@layer default-chat-sdk {
  @import "stream-chat-react/dist/scss/v2/index.scss";
}

@import "@stream-io/video-react-sdk/dist/css/styles.css"
  layer(default-video-sdk);
```

File [`index.scss`](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/messenger-clone/src/style/index.scss) is then imported into [`App.tsx`](https://github.com/GetStream/stream-video-js/tree/main/sample-apps/react/messenger-clone/src/App.tsx). Additionally, we add [`StreamTheme`](https://getstream.io/video/docs/react/v2/ui-components/video-theme/) component to wrap the app in an element carrying CSS class `str-video`. This will make sure all the variables and styles are applied to the child components:

```tsx {9,23,31}
import type { UserResponse } from "stream-chat";
import { Chat } from "stream-chat-react";
import { StreamTheme, StreamVideo } from "@stream-io/video-react-sdk";
import { Channel } from "./components/Channel";
import { Sidebar } from "./components/Sidebar";
import { Video } from "./components/Video";
import { useCreateChatClient } from "./hooks";

import "./styles/index.scss";

const Root = ({
  apiKey,
  user,
  userToken,
}: {
  apiKey: string;
  user: UserResponse;
  userToken: string;
}) => {
  // create clients and connect users

  return (
    <StreamTheme as="main" className="main-container">
      <Chat client={chatClient}>
        <StreamVideo client={videoClient}>
          <Sidebar user={user} />
          <Channel />
          <Video />
        </StreamVideo>
      </Chat>
    </StreamTheme>
  );
};
```

---

For the most recent version of this documentation, visit [https://getstream.io/video/docs/react/v2/advanced/chat-with-video/](https://getstream.io/video/docs/react/v2/advanced/chat-with-video/).