Building a Twitter Spaces Clone with NativeBase and 100ms

Introduction

As part of our community initiatives at NativeBase, we partnered with 100ms to do a workshop on “Building a Twitter Spaces Clone,” which you can find in the video version. This article is written as a follow-along reading for the same.

B…


This content originally appeared on DEV Community and was authored by NativeBase

Introduction

As part of our community initiatives at NativeBase, we partnered with 100ms to do a workshop on “Building a Twitter Spaces Clone,” which you can find in the video version. This article is written as a follow-along reading for the same.

Bootstrapping the project

One of the great things about using NativeBase is you get templates for all the possible target platforms you might be thinking of building an app, which means the bootstrapping time is reduced drastically. You get an all-configured, basic app that is ready to be extended.

Following the NativeBase Installation guide, we will start by using the “react-native” template, and it's as easy as copy-pasting a few commands described in the instructions.

Building the Screens

The demo app we are building has two screens; you see the home screen when you launch the app. This screen shows you all the live spaces. The card component on this screen is attractive. It has details of showing several things and is of moderate complexity, so let's look at how NativeBase makes building UI's like this almost trivial.

Image description

SpaceCard Component

import React from 'react';
import { Box, Text, HStack, Avatar, Pressable } from 'native-base';

export default function (props) {
  return (
    <Pressable
      w="full"
      bg="fuchsia.800"
      overflow="hidden"
      borderRadius="16"
      onPress={props.onPress}
    >
      <Text px="4" my="4" fontSize="md" color="white">
        Live
      </Text>
      <Text w="80%" pl="4" mb="4" fontSize="xl" color="white">
        Building a Twitter Space Clone in React Native using NativeBase and
        100ms
      </Text>
      <HStack p="4" bg="fuchsia.900" space="4">
        <Box flexDirection="row" justifyContent="center" alignItems="center">
          <Avatar
            size="sm"
            alignSelf="center"
            bg="green.200"
            source={{
              uri: 'https://pbs.twimg.com/profile_images/1188747996843761665/8CiUdKZW_400x400.jpg',
            }}
          >
            VB
          </Avatar>
          <Box ml="4">
            <Text fontSize="sm" color="white">
              Vipul Bhardwaj
            </Text>
            <Text fontSize="sm" color="white">
              SSE @GeekyAnts
            </Text>
          </Box>
        </Box>
        <Box flexDirection="row" justifyContent="center" alignItems="center">
          <Avatar
            size="sm"
            alignSelf="center"
            bg="green.200"
            source={{
              uri: 'https://pbs.twimg.com/profile_images/1188747996843761665/8CiUdKZW_400x400.jpg',
            }}
          >
            HO
          </Avatar>
          <Box ml="4">
            <Text fontSize="sm" color="white">
              Host
            </Text>
            <Text fontSize="sm" color="white">
              SE @100ms
            </Text>
          </Box>
        </Box>
      </HStack>
    </Pressable>
  );
}

Yup, that's it. That's the whole code, isn't that crazy, how easy NativeBase makes everything 🤯.

Let's look at the code in detail and learn about some of the tiny details that make it even more awesome.

Everything is a token

Every component in NativeBase is styled using its comprehensive, professionally designed, and tested Design System, which was created to be extensible to represent the brand identity of your app. This allows you to use tokens available in the NativeBase theme.

And thus, we can use values like w="full", bg="fuchsia.800" , overflow="hidden" , borderRadius="16" all of which are tokens assigned to props. This style of passing styles props as individual values is known as “Utility Props”, which provides great Developer Experience. NativeBase embraces this idea fully, and uses “Utility Props”, instead of the default react-native StyleSheet approach.

Color Modes and Accessibility

NativeBase supports both light and dark mode out of the box, and all the inbuilt components are designed to work with both color modes. But what if you use something other than the default values. With NativeBase, using Pseudo Props this becomes terribly easy.

Let's look at an example, this is the JSX code for the HomeScreen, notice on line 1, we have _light, and _dark. In NativeBase, props that start with an underscore are called pseudo props and they are used to control conditional styling. In the case of light and dark modes, you can use these props to provide styles that will only apply when the color mode is light or dark.

Yes, it's that easy to add support for the dark mode to your components. And on top of that, NativeBase used react-native-aria, and thus all the components are accessible by default, without you needing to do anything extra.

<Box flex="1" _light={{ bg: 'white' }} _dark={{ bg: 'darkBlue.900' }}>
  <VStack space="2" p="4">
    <Heading>Happening Now</Heading>
    <Text>Spaces going on right now</Text>
  </VStack>
  <ScrollView p="4">
    <VStack space="8">
      <SpaceCard
        onPress={() =>
          navigation.navigate('Space', {
            roomID: 'your-room-id-here',
          })
        }
      />   
    </VStack>
  </ScrollView>
</Box>

Adding Functionality

We use the 100ms SDK for react-native, which makes it extremely easy to get your add from just a collection of UI screens with static data to a full-blown functional app. The SDK is easy to set up and the documentation is great.

const fetchToken = async ({ roomID, userID, role }) => {
  const endPoint =
    'https://prod-in.100ms.live/hmsapi/geekyants.app.100ms.live/api/token';

  const body = {
    room_id: roomID,
    user_id: userID,
    role: role,
  };

  const headers = {
    'Content-Type': 'application/json',
    Accept: 'application/json',
  };

  const response = await fetch(endPoint, {
    method: 'POST',
    body: JSON.stringify(body),
    headers,
  });

  const result = await response.json();
  return result;
};

async function joinRoom(hmsInstance, roomID, userID) {
  if (!hmsInstance) {
    console.error('HMS Instance not found');
    return;
  }

  const { token } = await fetchToken({
    roomID,
    userID,
    role: 'speaker',
  });

  const hmsConfig = new HMSConfig({ authToken: token, username: userID });

  hmsInstance.join(hmsConfig);
}

export default function Space({ navigation, route }) {
  const hmsInstance = useContext(HMSContext);
  const [isMute, setMute] = useState(false);
  const [participants, setParticipants] = useState([]);

  const userID = useRef('demouser').current;
  const roomID = useRef(route.params.roomID).current;

  useEffect(() => {
    if (hmsInstance) {
      hmsInstance.addEventListener(HMSUpdateListenerActions.ON_ERROR, (data) =>
        console.error('ON_ERROR_HANDLER', data)
      );

      hmsInstance.addEventListener(
        HMSUpdateListenerActions.ON_JOIN,
        ({ room, localPeer, remotePeers }) => {
          const localParticipant = {
            id: localPeer?.peerID,
            name: localPeer?.name,
            role: localPeer?.role?.name,
            avatar: (
              <Circle w="12" h="12" p="2" bg="blue.600">
                {localPeer?.name?.substring(0, 2)?.toLowerCase()}
              </Circle>
            ),
            isMute: localPeer?.audioTrack?.isMute(),
          };

          const remoteParticipants = remotePeers.map((remotePeer) => {
            return {
              id: remotePeer?.peerID,
              name: remotePeer?.name,
              role: remotePeer?.role?.name,
              avatar: (
                <Circle w="12" h="12" p="2" bg="blue.600">
                  {remotePeer?.name?.substring(0, 2)?.toLowerCase()}
                </Circle>
              ),
              isMute: remotePeer?.audioTrack?.isMute(),
            };
          });

          setParticipants([localParticipant, ...remoteParticipants]);
        }
      );

      hmsInstance.addEventListener(
        HMSUpdateListenerActions.ON_ROOM_UPDATE,
        (data) => console.log('ON ROOM UPDATE', data)
      );

      hmsInstance?.addEventListener(
        HMSUpdateListenerActions.ON_PEER_UPDATE,
        ({ localPeer, remotePeers }) => {
          const localParticipant = {
            id: localPeer?.peerID,
            name: localPeer?.name,
            role: localPeer?.role?.name,
            avatar: (
              <Circle w="12" h="12" p="2" bg="blue.600">
                {localPeer?.name?.substring(0, 2)?.toLowerCase()}
              </Circle>
            ),
            isMute: localPeer?.audioTrack?.isMute(),
          };

          const remoteParticipants = remotePeers.map((remotePeer) => {
            return {
              id: remotePeer?.peerID,
              name: remotePeer?.name,
              role: remotePeer?.role?.name,
              avatar: (
                <Circle w="12" h="12" p="2" bg="blue.600">
                  {remotePeer?.name?.substring(0, 2)?.toLowerCase()}
                </Circle>
              ),
              isMute: remotePeer?.audioTrack?.isMute(),
            };
          });

          setParticipants([localParticipant, ...remoteParticipants]);
        }
      );

      hmsInstance?.addEventListener(
        HMSUpdateListenerActions.ON_TRACK_UPDATE,
        ({ localPeer, remotePeers }) => {
          const localParticipant = {
            id: localPeer?.peerID,
            name: localPeer?.name,
            role: localPeer?.role?.name,
            avatar: (
              <Circle w="12" h="12" p="2" bg="blue.600">
                {localPeer?.name?.substring(0, 2)?.toLowerCase()}
              </Circle>
            ),
            isMute: localPeer?.audioTrack?.isMute(),
          };

          const remoteParticipants = remotePeers.map((remotePeer) => {
            return {
              id: remotePeer?.peerID,
              name: remotePeer?.name,
              role: remotePeer?.role?.name,
              avatar: (
                <Circle w="12" h="12" p="2" bg="blue.600">
                  {remotePeer?.name?.substring(0, 2)?.toLowerCase()}
                </Circle>
              ),
              isMute: remotePeer?.audioTrack?.isMute(),
            };
          });

          setParticipants([localParticipant, ...remoteParticipants]);
        }
      );
    }

    joinRoom(hmsInstance, roomID, userID);
  }, [hmsInstance, roomID, userID]);
}
<>
  <VStack
    p="4"
    flex="1"
    space="4"
    _light={{ bg: "white" }}
    _dark={{ bg: "darkBlue.900" }}
  >
    <HStack ml="auto" alignItems="center">
      <IconButton
        variant="unstyled"
        icon={<HamburgerIcon _dark={{ color: "white" }} size="4" />}
      />
      <Button variant="unstyled">
        <Text fontSize="md" fontWeight="bold" color="red.600">
          Leave
        </Text>
      </Button>
    </HStack>
    <Text fontSize="xl" fontWeight="bold">
      Building a Twitter Space Clone in React Native using NativeBase and 100ms
    </Text>
    <FlatList
      numColumns={4}
      ListEmptyComponent={<Text>Loading...</Text>}
      data={participants}
      renderItem={({ item }) => (
        <VStack w="25%" p="2" alignItems="center">
          {item.avatar}
          <Text numberOfLines={1} fontSize="xs">
            {item.name}
          </Text>
          <HStack alignItems="center" space="1">
            {item.isMute && (
              <Image
                size="3"
                alt="Peer is mute"
                source={require("../icons/mute.png")}
              />
            )}
            <Text numberOfLines={1} fontSize="xs">
              {item.role}
            </Text>
          </HStack>
        </VStack>
      )}
      keyExtractor={(item) => item.id}
    />
  </VStack>
  <HStack
    p="4"
    zIndex="1"
    safeAreaBottom
    borderTopWidth="1"
    alignItems="center"
    _light={{ bg: "white" }}
    _dark={{ bg: "darkBlue.900" }}
  >
    <VStack space="2" justifyContent="center" alignItems="center">
      <Pressable
        onPress={() => {
          hmsInstance.localPeer.localAudioTrack().setMute(!isMute);
          setMute(!isMute);
        }}
      >
        <Circle p="2" borderWidth="1" borderColor="coolGray.400">
          {isMute ? (
            <Image
              size="8"
              key="mic-is-off"
              alt="mic is off"
              resizeMode={"contain"}
              source={require("../icons/mic-mute.png")}
            />
          ) : (
            <Image
              size="8"
              key="mic-is-on"
              alt="mic is on"
              resizeMode={"contain"}
              source={require("../icons/mic.png")}
            />
          )}
        </Circle>
      </Pressable>
      <Text fontSize="md">{isMute ? "Mic is off" : "Mic is on"}</Text>
    </VStack>
    <HStack ml="auto" mr="4" space="5">
      <Image
        size="7"
        alt="Participant Icon"
        source={require("../icons/users.png")}
      />
      <Image
        size="7"
        alt="Emojie Icon"
        source={require("../icons/heart.png")}
      />
      <Image size="7" alt="Share Icon" source={require("../icons/share.png")} />
      <Image
        size="7"
        alt="Tweet Icon"
        source={require("../icons/feather.png")}
      />
    </HStack>
  </HStack>
</>

We first join the room with room id. We fetch the authentication tokens by hitting the URL and creating an HMSConfig object, which we will use to connect to the room. Once we establish a connection, and we will get events based on calls when things happen in the room. For example, when some peer/user joins the room, we will get an event, and based on that, we can change the state of our data, which will lead to changes reflected in the UI. You can read more about it on the SDK and all the details of different things in the SDK documentation (‣)

Final Product

And there we have it, a working demo of a feature minimal Twitter spaces clone, and the fun is just beginning; you can add so many features to extend this and build an incredibly cool and feature-rich app you can use in the real world 🙂.


This content originally appeared on DEV Community and was authored by NativeBase


Print Share Comment Cite Upload Translate Updates
APA

NativeBase | Sciencx (2022-02-02T11:47:55+00:00) Building a Twitter Spaces Clone with NativeBase and 100ms. Retrieved from https://www.scien.cx/2022/02/02/building-a-twitter-spaces-clone-with-nativebase-and-100ms/

MLA
" » Building a Twitter Spaces Clone with NativeBase and 100ms." NativeBase | Sciencx - Wednesday February 2, 2022, https://www.scien.cx/2022/02/02/building-a-twitter-spaces-clone-with-nativebase-and-100ms/
HARVARD
NativeBase | Sciencx Wednesday February 2, 2022 » Building a Twitter Spaces Clone with NativeBase and 100ms., viewed ,<https://www.scien.cx/2022/02/02/building-a-twitter-spaces-clone-with-nativebase-and-100ms/>
VANCOUVER
NativeBase | Sciencx - » Building a Twitter Spaces Clone with NativeBase and 100ms. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/02/02/building-a-twitter-spaces-clone-with-nativebase-and-100ms/
CHICAGO
" » Building a Twitter Spaces Clone with NativeBase and 100ms." NativeBase | Sciencx - Accessed . https://www.scien.cx/2022/02/02/building-a-twitter-spaces-clone-with-nativebase-and-100ms/
IEEE
" » Building a Twitter Spaces Clone with NativeBase and 100ms." NativeBase | Sciencx [Online]. Available: https://www.scien.cx/2022/02/02/building-a-twitter-spaces-clone-with-nativebase-and-100ms/. [Accessed: ]
rf:citation
» Building a Twitter Spaces Clone with NativeBase and 100ms | NativeBase | Sciencx | https://www.scien.cx/2022/02/02/building-a-twitter-spaces-clone-with-nativebase-and-100ms/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.