WhatsApp UI in React Native (part 1)
Tags: reactnative, ios, android, tutorial
Published: 2023-01-18 10:03:39

Billion of us rely on WhatsApp every day.

WhatsApp's simple interface is a a huge part of their success.

But how hard is it to build WhatsApp's UI in React Native? 

Let's find out!


![Preview of the app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xkv45u0p9z0xmb441pfc.gif)



You can see the [full code for this project here ](https://github.com/jackbridger/whatsapp-chat-ui-react-native/tree/article-part-1)

We also have part two and three
* [Part 2 - Setting up a chat backend](https://dev.to/nsmet/chat-backend-with-supabase-socketio-55nn)
* [Part 3 - Using real data in our app ](https://dev.to/jacksbridger/whatsapp-in-react-native-part-3-1gmf)

## Setup
I'm using Expo for this project. 
I recommend checking out the Expo docs but you should follow something like the below:

```
$ npm install --global expo-cli && npx create-expo-$app my-app
$ cd my-app
$ npx expo install react-dom react-native-web @expo/webpack-config
$ npx expo start
```

## Screens & components 

### Screens
We will have two main screens, and two placeholder screens. 

* **screens/Chats/Chats.tsx** - this is a list of Chat conversations. I put it in a folder along with its styling file, Chats.styles.ts
* **screens/Chat/Chat.tsx** - this is the screen for a conversation. I put it in a folder along with its styling file, Chat.styles.ts
* **screens/Status.tsx** - this is a placeholder for the status tab - we won't work on it today
* **screens/Calls.tsx** - this is a placeholder for the status tab - we won't work on it today

### Components

* **components/ChatMessages/ChatMessages.tsx** - this is the list of all the chat messages in any given chat conversation
* **components/ConversationPreview/ConversationPreview.tsx** - This is the preview of a message that you see in the list of all your chat conversations
* **components/MessageBubble/MessageBubble.tsx** - this is an individual message within a chat conversation
* **components/SendButton/SendButton.tsx** - This is the whole section where you send a message: including the text input and the actual button to send the message.


## Navigation - Chats / Status / Calls
The central navigation feature for Whatsapp is navigating between Chats and Calls (+ Status but who uses that?!) 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/coztblg7nvkh0qlzpzn5.gif)

To get these cool tabs, we can use:

```ts
import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";
```

This gets us most of the way without any styling.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wanv4ik5nu3x7zl5coem.gif)

But before we start styling, we still need to think about navigation between screens.

For example, we need a list of all the chat conversations and then we need to be able to navigate to that specific chat conversation.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/713mg0w50y1je6un077i.gif)

But we don't want each chat conversations to be in their own tab. They need to be within the chat conversations tab. 
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aivx6njx2lg6bnlielsz.png)

And we need to be able to see other chats with the back button.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/smv13cyxnira4dsp4wco.gif)

So we need another StackNavigator.

The initial way I thought about structuring the navigation was having a navigator nested within each of the tabs like this:

TopTabsNavigator
-----ChatNavigator
------------ChatMessage
-----StatusNavigator
-----CallsNavigator

But then you end up seeing your Top Tabs at every level. And we only want them on that home screen. 

So instead we can create a **StackNavigator** at the root and render our **TopTabNavigator** as the default Screen within it. 

Then within our **TopTabsNavigator**, the default Screen is our **Chats** screen.


#### _One potential point of confusion_

_One thing that could be confusing is that our Conversation Chat screen is within our Root Navigator._

_That's because when someone clicks on a conversation preview in our Chats list, they are navigating from one screen in the Root stack (Chats in TopTabsNavigator), to another screen in the Root stack._

Finally, our **RootNavigator** is wrapped in a **NavigationContainer** and this is exported as a **Navigation** component so we can render it inside App.tsx.

```ts
// navigation/index.tsx
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import * as React from "react";

import TopTabNavigator from "./TopTabNavigator";
import Chat from "../screens/Chat/Chat";
import CallsScreen from "../screens/Calls";
import StatusScreen from "../screens/Status";
import ModalScreen from "../screens/ModalScreen";
import { RootStackParamList } from "../types";

export default function Navigation() {
  return (
    <NavigationContainer>
      <RootNavigator />
    </NavigationContainer>
  );
}
const Stack = createNativeStackNavigator<RootStackParamList>();

function RootNavigator() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Root" component={TopTabNavigator} />
      <Stack.Screen name="Chat" component={Chat} />
      <Stack.Group screenOptions={{ presentation: "modal" }}>
        <Stack.Screen name="Modal" component={ModalScreen} />
      </Stack.Group>
    </Stack.Navigator>
  );
}

```

```ts
// navigation/TopTabNavigator.tsx
import * as React from "react";
import { createMaterialTopTabNavigator } from "@react-navigation/material-top-tabs";

import ChatsScreen from "../screens/Chats/Chats";
import StatusScreen from "../screens/Status";

import { RootTabParamList, RootTabScreenProps } from "../types";

const TopTab = createMaterialTopTabNavigator<RootTabParamList>();
export default function TopTabNavigator() {
  return (
    <TopTab.Navigator tabBarPosition="top" initialRouteName="Chats">
      <TopTab.Screen
        name="Chats"
        component={ChatsScreen}
        options={({ navigation }: RootTabScreenProps<"Chats">) => ({
          title: "Chats",
        })}
      />
      <TopTab.Screen
        name="Status"
        component={StatusScreen}
        options={{
          title: "Status",
        }}
      />
      <TopTab.Screen
        name="Calls"
        component={CallsScreen}
        options={{
          title: "Calls",
        }}
      />
    </TopTab.Navigator>
  );
}

```

In our App.tsx file, we want to return a **Navigation** component like this:

```ts
// App.tsx
import Navigation from "./navigation";

export default function App() {
  return (
    <SafeAreaProvider>
      <Navigation />
    </SafeAreaProvider>
  );
}
```
To style the navigation, you can play around with screenOptions to get the WhatsApp-y look. 

I used the following options, to get it looking pretty close.

```ts
    <TopTab.Navigator
      tabBarPosition="top"
      initialRouteName="Chats"
      screenOptions={{
        tabBarIndicatorStyle: {
          backgroundColor: Colors.light.white,
        },
        tabBarInactiveTintColor: Colors.light.inactiveGreen,
        tabBarActiveTintColor: Colors.light.white,
        tabBarLabelStyle: { fontSize: 16, fontWeight: "bold" },
        tabBarStyle: {
          backgroundColor: Colors.light.darkGreen,
        },
      }}
    >
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/04b2vxbzojhxfe0g25qq.gif)

We can also add some options to the "Root" screen to make the  header look a bit more WhatsApp-y too.

```ts
      <Stack.Screen
        name="Root"
        component={TopTabNavigator}
        options={{
          headerShown: true,
          headerTitle: "WhatsApp",
          headerStyle: {
            backgroundColor: Colors.light.darkGreen,
          },
          headerTitleStyle: {
            fontWeight: "bold",
            color: Colors.light.white,
            fontSize: 20,
          },
          headerTitleAlign: "left",
        }}
      />
```
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ztf431ju49vhnsmtax12.gif)

## Rough Data structure of Whatsapp

This is mostly a frontend exercise but we need some kind of simple data. 

I laid mine out like this:

```ts
export type Conversation = {
  id: string; // a unique id of conversation
  messages: Message[]; // messages sent
  users: number[]; // ids of users in the conversation
  title: string; e.g. "Nick Cage" or "30th birthday"
};

export type Message = {
  text: string; // message contents
  time: Date; // timestamp of send time
  userID: number; // userID of sender
  id: string; // unique of of message
};
```
You can see an example of [fake data I generated here](https://github.com/jackbridger/whatsapp-chat-ui-react-native/blob/main/data/messages.ts), I saved it in data/messages 

## State management

I found it a lot easier to build this with some global state management.

To do this I used react's context to manage:
* conversations - (also includes the messages)
* sendMessage - this adds messages to the conversation and sorts them
* setCurrentConversation - this keeps track of which conversation you pressed on 
* getCurrentConversation - this tells you which conversation to load

To build this I set up a ConversationsProvider that contains the values above that I need globally

```ts
export const ConversationsContext = createContext<ConversationsContextType>({
  conversations: startingConversations,
  sendMessage: () => {},
  getCurrentConversation: () => {
    return { id: "", messages: [], users: [], title: "" };
  },
  setCurrentConversation: (id) => {},
});

export const ConversationsProvider = ({ children }) => {
  const [conversations, setConversations] = useState<ConversationType[]>(
    sortConversations(startingConversations)
  );
  const [currConversation, setCurrConversation] = useState<ConversationType>();

  const getCurrentConversation = () => {
    if (currConversation) return currConversation;
    else return { id: "", messages: [], users: [], title: "" };
  };

  const setCurrentConversation = (id: string) => {
    const currentConvo = conversations.filter((conv) => conv.id === id)[0];
    setCurrConversation(currentConvo);
  };

  function sendMessage(
    newMsg: string,
    thisConversationID: string,
    userID: number,
    setNewMsg: (msg: string) => void,
    isTyping: boolean,
    setIsTyping: (isTyping: boolean) => void
  ) {
    if (isTyping) {
      setNewMsg("");
      setIsTyping(false);
      if (currConversation && currConversation.id === thisConversationID) {
        setCurrConversation((prevConvo: ConversationType) => {
          return {
            ...prevConvo,
            messages: [...prevConvo.messages, formatMessage(newMsg, userID)],
          };
        });
      }
      setConversations((previousConversations: ConversationType[]) => {
        const allConversations = previousConversations.map(
          (conversation: ConversationType) => {
            if (conversation.id === thisConversationID) {
              return {
                ...conversation,
                messages: [
                  ...conversation.messages,
                  formatMessage(newMsg, userID),
                ],
              };
            }
            return conversation;
          }
        );
        return sortConversations(allConversations);
      });
    }
  }
  return (
    <ConversationsContext.Provider
      value={{
        conversations,
        sendMessage,
        getCurrentConversation,
        setCurrentConversation,
      }}
    >
      {children}
    </ConversationsContext.Provider>
  );
};

```

### Sending a message
When a message is sent, its appended on to the end of the messages array within the relevant conversation.

It also clears the state of the message box so it becomes empty again.  

### sorting conversations

I wrote a function to sort conversations and I run it every time a message is sent


```ts
const sortConversations = (conversations: ConversationType[]) => {
  return conversations.sort((a, b) => {
    const lastMessageA = a.messages[a.messages.length - 1];
    const lastMessageB = b.messages[b.messages.length - 1];
    return lastMessageB.time.getTime() - lastMessageA.time.getTime();
  });
};

```
### Accessing the state

To access this global state, I wrap the rest of my app in the **ConversationsProvider**

```ts
export default function App() {
  return (
    <ConversationsProvider>
      <SafeAreaProvider>
        <Navigation />
      </SafeAreaProvider>
    </ConversationsProvider>
  );
}
```

And then I can access this state with:

```ts
const { conversations, sendMessage } = useContext(ConversationsContext);
```

## List of chat conversations

Our chat conversations will work great as a **FlatList**. But instead of **FlatList** we're doing to use **FlashList** from Shopify, which is said to have better performance than **FlatList** (but I've not verified this).

```ts
// screens/Chats/Chats.tsx
import { View } from "react-native";
import { FlashList } from "@shopify/flash-list";

import ConversationPreview from "../../components/ConversationPreview/ConversationPreview";
import { Conversation, RootTabScreenProps } from "../../types";
import conversations from "../../data/messages";
import styles from "./Chats.styles";

interface ConversationItemProps {
  item: Conversation;
}
export default function ChatsScreen({}: RootTabScreenProps<"Chats">) {
  const renderConversationPreview = (props: ConversationItemProps) => {
    const { item } = props;
    return <ConversationPreview key={item.id} conversation={item} />;
  };
  return (
    <View style={styles.mainContainer}>
      <FlashList
        data={conversations}
        renderItem={renderConversationPreview}
        keyExtractor={(item) => item.id}
        estimatedItemSize={40}
      />
    </View>
  );
}

```
I estimated the size as being 40 items but feel free to adjust this. **FlashList** perform more effectively by knowing roughly how many items you will have.

I kept the styling fairly light.

```ts
// screens/Chats/Chats.styles.ts
import { StyleSheet } from "react-native";
import Colors from "../../constants/Colors";

export default StyleSheet.create({
  mainContainer: {
    backgroundColor: Colors.light.white,
    height: "100%",
    paddingTop: "3%",
  },
});
```
In our **FlashList**, we render out a **ConversationPreview** for each of our conversations. It looks something like this. 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nsgxkuttsmizh72eov20.png)

## ConversationPreview

In ConversationPreview, I'm doing a few fun things.

### Text previews

We can create the '...' eliptical effect by fetching the last message from the conversation and then setting the following options in our text component. 
```
numberOfLines={1}
ellipsizeMode="tail"
```
### Profile images 
We are setting profile image by the conversation id - they're saved on an images object which we store the assets on. 

```ts
const images: {
  [key: string]: any;
} = {
  "101": require("./images/101.jpeg"),
  "102": require("./images/102.jpeg"),
  "103": require("./images/103.jpeg"),
  "104": require("./images/104.jpeg"),
  "105": require("./images/105.jpeg"),
};

export default images;

```

### Times & notifications

We use the dayjs library for getting the timestamp of our messages and we're hard coding a notification as default for now. 

```ts
// components/ConversationPreview/ConversationPreview.tsx
import { Image, Text, View, TouchableOpacity } from "react-native";
import { useNavigation } from "@react-navigation/native";
import { useContext } from "react";
import dayjs from "dayjs";

import { ConversationType } from "../../types";
import styles from "./ConversationPreview.styles";
import { ConversationsContext } from "../../context/conversationContext";
import images from "../../assets/index";

interface ConversationPreviewProps {
  conversation: ConversationType;
}

interface ChatRouteParams {
  conversation: ConversationType;
}

export default function ConversationPreview(props: ConversationPreviewProps) {
  const { conversation } = props;
  const { setCurrentConversation } = useContext(ConversationsContext);
  const navigation = useNavigation();
  const profileImg = images[conversation.id];

  const chatRouteParams: ChatRouteParams = {
    conversation,
  };

  const _onPress = () => {
    setCurrentConversation(conversation.id);
    navigation.navigate("Chat", chatRouteParams);
  };

  return (
    <TouchableOpacity onPress={_onPress} style={styles.messageContainer}>
      <View style={styles.imgAndMsgSubContainer}>
        <Image style={styles.profileImg} source={profileImg} />
        <View>
          <Text style={styles.msgTitle}>{conversation.title}</Text>
          <Text
            numberOfLines={1}
            ellipsizeMode="tail"
            style={styles.msgPreview}
          >
            {conversation.messages[conversation.messages.length - 1].text}
          </Text>
        </View>
      </View>
      <View style={styles.msgDataContainer}>
        <View style={styles.msgDataSubContainer}>
          <Text style={styles.timeText}>
            {dayjs(
              conversation.messages[conversation.messages.length - 1].time
            ).format("HH:mm")}
          </Text>
          <View style={styles.numberOfMsgsContainer}>
            <Text style={styles.numberOfMsgsText}>2</Text>
          </View>
        </View>
      </View>
    </TouchableOpacity>
  );
}

```

```ts
// components/ConversationPreview/ConversationPreview.styles.ts
import { StyleSheet } from "react-native";
import Colors from "../../constants/Colors";
import { Dimensions } from "react-native";
const windowHeight = Dimensions.get("window").height;
const windowWidth = Dimensions.get("window").width;

export default StyleSheet.create({
  messageContainer: {
    width: windowWidth * 0.97,
    height: windowHeight / 15,
    flexDirection: "row",
    justifyContent: "space-around",
  },
  imgAndMsgSubContainer: {
    flexDirection: "row",
    width: "90%",
    height: "100%",
    alignItems: "center",
  },
  profileImg: {
    width: 50,
    height: 50,
    marginRight: 10,
    marginLeft: 10,
    borderRadius: 50,
  },
  msgTitle: {
    fontWeight: "bold",
    color: Colors.light.offBlack,
  },
  msgPreview: {
    color: Colors.light.offBlack,
    width: "80%",
  },
  msgDataContainer: {
    flexDirection: "row",
    alignItems: "center",
  },
  msgDataSubContainer: {
    flexDirection: "column",
    alignItems: "center",
  },
  timeText: {
    color: Colors.light.brightGreen,
  },
  numberOfMsgsContainer: {
    backgroundColor: Colors.light.brightGreen,
    height: 20,
    width: 20,
    borderRadius: 50,
    padding: 0,
    margin: 0,
    alignItems: "center",
    justifyContent: "center",
  },
  numberOfMsgsText: {
    color: Colors.light.white,
    fontWeight: "bold",
    padding: 0,
    margin: 0,
  },
});

```
#### Pressing a message

When we press on a message preview, we navigate to the Chat Screen and we set the current conversation.

```ts
  const chatRouteParams: ChatRouteParams = {
    conversation,
  };

  const _onPress = () => {
    setCurrentConversation(conversation.id);
    navigation.navigate("Chat", chatRouteParams);
  };
```

## Chat

Our messages are displayed on the **Chat.tsx** screen

### Background image

We set the background image as an image I found online. 
```ts
<ImageBackground
        style={styles.backgroundImg}
        source={require(whatsappBackgroundImg)}
        resizeMode="cover"
>
```
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yca73ly8cxtsxxu8vvxb.png)

We used it with the **<ImageBackground>** component from **react-native**

### ChatMessages

The **ChatMessages** component is where we display all the messages in this conversation.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c592jvr6pkp7ui1tty5h.png)

This component is a **FlashList** that renders all the messages as MessageBubbles. 

```ts
      <FlashList
        inverted
        data={[...messages].reverse()}
        renderItem={renderMessageBubble}
        estimatedItemSize={40}
      />
```

#### Position of ChatMessages

When someone opens the keyboard, our **ChatMessages** need to move up above the keyboard (same as our **SendButton** below).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4kd0c1sxen55n4uwye08.gif)

But also, when the input box expands as message size increases, ChatMessages need to move up too.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0fjfeoad5mq081jlwkcw.gif)


I probably overcomplicated this (let me know in the comments), but I solved it like this:

```ts
// components/ChatMessages/ChatMessages.tsx
<View
      style={{
        height:
          windowHeight * 0.8 -
          keyBoardOffsetHeight * 0.95 -
          getMessageHeightOffset(heightOfMessageBox, windowHeight),
      }}
    >
      <FlashList
        inverted
        data={[...messages].reverse()}
        renderItem={renderMessageBubble}
        estimatedItemSize={40}
      />
    </View>
```

and with the **getMessageBoxHeightOffset** function
```ts
// helpers/getMessageBoxHeightOffset.ts
const getMessageHeightOffset = (
  heightOfMessageBox: number,
  windowHeight: number
): number => {
  const maxHeightOfMessageBox = windowHeight * 0.3;
  if (heightOfMessageBox > maxHeightOfMessageBox) {
    return maxHeightOfMessageBox - windowHeight * 0.05;
  }
  if (heightOfMessageBox > 24) {
    return heightOfMessageBox - windowHeight * 0.035;
  }
  return 0;
};

export default getMessageHeightOffset;

```

#### MessageBubble
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d2i7ah8znc2yux1c2nxn.png)

There is quite a bit of styling in this component. And the styling depends on whether it is from a recipient or sender.

We just establish if I sent the message by checking if it matched my user id. 

```ts
const isMyMessage = message.userID === 1;
```

Then we adjust the styling based on that

```ts
import { View, Text } from "react-native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import dayjs from "dayjs";

import { MessageDataType } from "../../types";
import styles from "./MessageBubble.styles";

export default function MessageBubble(props: MessageDataType) {
  const { message } = props;
  const isMyMessage = message.userID === 1;
  const isMessageRead = false;
  return (
    <View
      style={{
        ...styles.messageContainer,
        alignSelf: isMyMessage ? "flex-start" : "flex-end",
        backgroundColor: isMyMessage ? "#fcfcfc" : "#dfffc7",
        borderTopLeftRadius: isMyMessage ? 0 : 5,
        borderTopRightRadius: isMyMessage ? 5 : 0,
      }}
    >
      <View
        style={{
          ...styles.leftMessageArrow,
          display: isMyMessage ? "flex" : "none",
        }}
      ></View>
      <Text
        style={{
          ...styles.messageText,
          left: isMyMessage ? 0 : 10,
        }}
      >
        {message.text}
      </Text>
      <View
        style={{
          ...styles.timeAndReadContainer,
          left: isMyMessage ? 0 : 10,
        }}
      >
        <Text style={styles.timeText}>
          {dayjs(message.time).format("HH:mm A")}
        </Text>
        <View>
          {isMessageRead ? (
            <MaterialCommunityIcons name="read" size={16} color="#5bb6c9" />
          ) : (
            <MaterialCommunityIcons name="check" size={16} color="grey" />
          )}
        </View>
        <View
          style={{
            ...styles.rightMsgArrow,
            display: isMyMessage ? "none" : "flex",
          }}
        ></View>
      </View>
    </View>
  );
}

```

### SendButton

The sendButton is a fun one. 

#### position of the sendButton

The Whatsapp sendButton starts at the bottom, but when someone opens the keyboard, it moves upwards.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4kd0c1sxen55n4uwye08.gif)

So we need the style to be absolute and dependent on whether the keyboard is open. 

```ts
bottom: Math.max(keyBoardOffsetHeight, windowHeight * 0.02),
```

To get this keyBoardOffsetHeight I wrote a hook

```ts
// components/SendButton/SendButton.tsx
const keyBoardOffsetHeight = useKeyboardOffsetHeight();
``` 

```ts
// helpers/useKeyboardOffsetHeight.tsx
import { useEffect, useState } from "react";
import { Keyboard } from "react-native";

export default function useKeyboardOffsetHeight(): number {
  const [keyBoardOffsetHeight, setKeyboardOffsetHeight] = useState(0);

  useEffect(() => {
    const keyboardWillShowListener = Keyboard.addListener(
      "keyboardWillShow",
      (e) => {
        setKeyboardOffsetHeight(e.endCoordinates.height);
      }
    );
    const keyboardWillHideListener = Keyboard.addListener(
      "keyboardWillHide",
      () => {
        setKeyboardOffsetHeight(0);
      }
    );

    return () => {
      keyboardWillHideListener.remove();
      keyboardWillShowListener.remove();
    };
  }, []);

  return keyBoardOffsetHeight;
}

```

#### Typing = icon change animation

When we start typing, WhatsApp changes the voice record icon to the send icon.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bavsnufeidv69u8ytoby.gif)

Switching the icons is easy - we just keep an **isTyping** flag. It's initialised in Chat.tsx because its shared across components.

But for it to look smooth, we want an animation between the icons so we can use **react-native-reanimated** library

```ts
<Transitioning.View ref={ref} transition={msgTypeTransition}>
              {isTyping ? (
                <Ionicons name="send" size={16} color={Colors.light.white} />
              ) : (
                <FontAwesome5
                  name="microphone"
                  size={16}
                  color={Colors.light.white}
                />
              )}
            </Transitioning.View>

const msgTypeTransition = (
  <Transition.Together>
    <Transition.Out type="scale" durationMs={100} />
    <Transition.Change interpolation="easeInOut" />
    <Transition.In type="scale" durationMs={100} />
  </Transition.Together>
);
```

If you enjoyed, this check out parts two and three
* [Part 2 - Setting up a chat backend](https://dev.to/nsmet/chat-backend-with-supabase-socketio-55nn)
* [Part 3 - Using real data in our app ](https://dev.to/jacksbridger/whatsapp-in-react-native-part-3-1gmf)

I also host a podcast on [developer tools](https://scalingdevtools.com/).