🎉 New! Web Push Notifications for Chatkit. Learn more in our latest blog post.
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Create a file-sharing app with React Native

  • Wern Ancheta

November 1st, 2018
You will need Expo and ngrok installed on your machine. Basic knowledge of React Native is required.

In this tutorial, we’ll build a file-sharing app using React Native and Chatkit.

Prerequisites

Basic knowledge of React Native is required.

We’ll be using Expo, so you need to have it set up on your machine. Aside from that, you also need the Android or iOS Expo client app. This allows you to easily test the app on multiple devices.

The app will have a server component, so we need to expose it to the internet using ngrok. Create a free account before proceeding.

App overview

The app that we’re going to build is a file-sharing app with a one on one chat feature.

First, the user has to log in. The user that they’re chatting with doesn’t necessarily have to be logged in, but they have to share the friend’s username to their friend by some other means, so they can input it when they log in. This allows the app to put them in the same chat room:

Once they’re logged in, they’ll be greeted with the chat screen. Here, the user can send a text or attach a file or image along with it. If an image was attached to the message, it’s previewed in the chat bubble. If it’s any other file type, only the file name is appended to the actual text that was sent:

By default, Chatkit only loads the ten most recent messages. But the app also allows users to load older messages.

When the user clicks on the image icon to the right of the paper clip icon, the image picker will open. This will look different on Android and iOS, but it has the same functions:

On the other hand, if the user clicks on the paper clip icon, they can select any type of file:

Due to restrictions in iOS, the file picker will only be usable on Android. This is because the file picker only picks files from iCloud. But to enable that feature, you need to have an Apple Developer Account so you can enable the iCloud Application Service for your specific app.

When the user clicks on the download icon in the header, the files modal will open. This will show all the files that were shared throughout the chat history and allow users to download it:

When the user clicks on the download icon next to each file, the file will be opened in the phone’s default browser. From here, the user can long press the image to save it. On Android, files opened in the browser will automatically be downloaded:

Here’s what the final output will look like:

You can find the source code of the app in its GitHub repo.

Create a Chatkit app instance

If you haven’t done so already, sign up for a Pusher account and log in. Once that’s done, go to the Chatkit dashboard and create a new app instance:

Once the instance is created, scroll down to the test token provider section and enable it:

The test token provider endpoint is where we will make a request to get a unique token for the user so they can connect to Chatkit. Note that we will only be using this so we could quickly get started with using Chatkit without setting up a lot of things. If you’re working on a production app, it’s best to skip the test token provider altogether. Refer to the Chatkit authentication docs if you need help.

Build the app

The first thing you need to do is initialize a new Expo project:

    expo init RNChatkitFileShare

Once it’s created, go inside the newly created project folder and install all the dependencies:

    cd RNChatkitFileShare
    yarn add @pusher/chatkit-client random-string react-native-gifted-chat react-native-mime-types react-navigation string-hash

Here’s a brief overview of what each one does:

  • @pusher/chatkit-client - the JavaScript library for working with Chatkit.
  • random-string - for generating a random string for filenames.
  • react-native-gifted-chat - for easily implementing the chat UI.
  • react-native-mime-type - for determining the MIME types of the files selected by the user. We supply the file’s MIME type as an additional info when sending an attachment to Chatkit.
  • react-navigation - for navigating between the two screens.
  • string-hash - for generating a hash for usernames.

Once all the dependencies are installed, we can start adding the code. Replace the contents of the App.js file with the following. This uses the Root component where we’ll implement the navigation for the whole app:

    // App.js
    import React from "react";
    import { View } from "react-native";
    import Root from "./Root";

    export default class App extends React.Component {
      render() {
        return (
          <View style={styles.container}>
            <Root />
          </View>
        );
      }
    }

    const styles = {
      container: {
        flex: 1
      }
    };

Next, create a Root.js file. This is where we create the stack navigator for navigating between the screens:

    // Root.js
    import React, { Component } from "react";
    import { createStackNavigator } from "react-navigation";

    import Login from "./app/screens/Login";
    import Chat from "./app/screens/Chat";

    console.ignoredYellowBox = ["Setting a timer"];

    const RootStack = createStackNavigator(
      {
        Login: Login,
        Chat: Chat
      },
      {
        initialRouteName: "Login"
      }
    );

    class Router extends Component {
      render() {
        return <RootStack />;
      }
    }

    export default Router;

Before you proceed with the next section, create an app/screens and app/components directory in the root of the project directory. This is where we’ll be putting the files we’ll be creating.

Login screen

Create an app/screens/Login.js file and put the following. The CHAT_SERVER is the server endpoint which handles the requests for creating users. We’ll be creating the server near the end of the tutorial, leave it as is for now:

    // app/screens/Login.js
    import React, { Component } from "react";
    import {
      View,
      Text,
      TextInput,
      TouchableOpacity,
      ActivityIndicator
    } from "react-native";
    import stringHash from "string-hash";

    const CHAT_SERVER = "YOUR_NGROK_URL/users"; 

Next, create the component and initialize the state:

    export default class Login extends Component {
      static navigationOptions = {
        header: null
      };

      state = {
        username: "",
        friends_username: "",
        is_loading: false // whether to show the animated loader or not 
      };

      // next: add render method
    }

    // last: add styles

Next, render the UI of the app. This will allow the user to enter their username and the username of the friend they want to chat with:

    render() {
      return (
        <View style={styles.container}>
          <View style={styles.main}>
            <View style={styles.fieldContainer}>
              <Text style={styles.label}>Enter your username</Text>
              <TextInput
                style={styles.textInput}
                onChangeText={username => this.setState({ username })}
                value={this.state.username}
              />
            </View>

            <View style={styles.fieldContainer}>
              <Text style={styles.label}>Enter friend's username</Text>
              <TextInput
                style={styles.textInput}
                onChangeText={friends_username =>
                  this.setState({ friends_username })
                }
                value={this.state.friends_username}
              />
            </View>

            {this.state.is_loading && (
              <ActivityIndicator size="small" color="#0064e1" />
            )}

            {!this.state.is_loading && (
              <TouchableOpacity onPress={this.login} style={styles.button}>
                <Text style={styles.buttonText}>Sign in</Text>
              </TouchableOpacity>
            )}
          </View>
        </View>
      );
    }

    // next: add login method

As mentioned earlier, the friend’s username doesn’t need to already exist or can be already in use. The usernames are appended to generate the room name. You’ll see this in action in the server component later on.

Next, add the login method. This makes a request to the server to create the user:

    login = async () => {
      let username = this.state.username;
      let friends_username = this.state.friends_username;
      let user_id = stringHash(username).toString(); // get the hash of the username to come up with the user ID

      // show the loader
      this.setState({
        is_loading: true
      });

      if (username && friends_username) {
        let response = await fetch(CHAT_SERVER, {
          method: "POST",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify({
            user_id: user_id,
            username: username
          })
        });

        // reset the UI
        await this.setState({
          is_loading: false,
          username: "",
          friends_username: ""
        });

        if (response.ok) {
          // navigate to the Chat screen
          this.props.navigation.navigate("Chat", {
            user_id,
            username,
            friends_username
          });
        } else {
          console.log("not ok");
        }
      }
    };

Lastly, add the styles:

    const styles = {
      container: {
        flex: 1,
        alignItems: "center",
        justifyContent: "center",
        padding: 20,
        backgroundColor: "#FFF"
      },
      fieldContainer: {
        marginTop: 20
      },
      label: {
        fontSize: 16
      },
      textInput: {
        height: 40,
        marginTop: 5,
        marginBottom: 10,
        borderColor: "#ccc",
        borderWidth: 1,
        backgroundColor: "#eaeaea",
        padding: 5
      },
      button: {
        alignSelf: "center",
        marginTop: 10
      },
      buttonText: {
        fontSize: 18,
        color: "#05a5d1"
      }
    };

Chat screen

Create an app/screens/Chat.js file and add the following:

    // app/screens/Chat.js
    import React, { Component } from "react";
    import { View, TouchableOpacity, Alert, ActivityIndicator } from "react-native";
    import { GiftedChat, Send } from "react-native-gifted-chat";

    import {Chatkit, TokenProvider} from "@pusher/chatkit-client";

    import randomstring from "random-string"; // for generating filenames

    import * as mime from "react-native-mime-types"; // for determining the mime types of files selected through the file/image picker

    import { Permissions, ImagePicker, DocumentPicker } from "expo"; // for getting permission to access camera roll (for image picker)
    import { Ionicons } from "@expo/vector-icons";

Next, add your Chatkit credentials. You can find these details on the Chatkit app instance you created earlier. Leave the ngrok URL for now, we’ll be adding it later once we run the server:

    const CHATKIT_TOKEN_PROVIDER_ENDPOINT = "YOUR TEST TOKEN PROVIDER ENDPOINT"; // you can find this on the test token provider section
    const CHATKIT_INSTANCE_LOCATOR = "YOUR INSTANCE LOCATOR ID"; // you can find this under the credentials section

    const CHAT_SERVER = "YOUR_NGROK_URL/rooms";

Next, include the DownloadsModal component. This is used for rendering the list of files shared in the chat room:

    import DownloadsModal from "../components/DownloadsModal";

Next, create the Chat component. Start by adding the navigation config. This includes the button to be rendered on the right side of the header. As you’ve seen in the demo earlier, this is the button for showing the downloads modal. We’ll declare the navigation param (viewFiles) inside the component class later. For now, just know that clicking the button will update the state so that the downloads modal will become visible:

    export default class Chat extends Component {
      static navigationOptions = ({ navigation }) => {
        const { params } = navigation.state;

        return {
          headerTitle: `Chat with ${params.friends_username}`, // get the friends_username passed from the login screen
          headerRight: (
            <TouchableOpacity onPress={() => params.viewFiles()}>
              <View style={styles.buttonContainer}>
                <Ionicons name="ios-folder-open" size={25} color="#FFF" />
              </View>
            </TouchableOpacity>
          ),
          headerStyle: {
            backgroundColor: "#333"
          },
          headerTitleStyle: {
            color: "#FFF"
          }
        };
      };

      // next: initialize state

    }

Next, initialize the state:

    state = {
      messages: [], // for storing the messages displayed in the UI
      files: [], // for storing the files displayed in the UI
      is_initialized: false, // whether Chatkit is already initialized or not
      is_loading: false, // whether the app is currently loading something
      is_modal_visible: false, // whether to show the downloads modal or not
      show_load_earlier: false, // whether to show the button for fetching older messages
      is_picking_file: false // whether the user is currently picking an image/file or not
      is_sending: false // whether Chatkit is currently sending a message
    };

    // next: add constructor

Next, add the constructor. This is where we initialize all the variables that we’ll be using throughout this component:

    constructor(props) {
      super(props);
      const { navigation } = this.props;
      const user_id = navigation.getParam("user_id");
      const username = navigation.getParam("username");
      const friends_username = navigation.getParam("friends_username");

      const members = [username, friends_username];
      members.sort(); // arrange alphabetically so it's the same room name all throughout

      this.user_id = user_id;
      this.username = username;
      this.room_name = members.join("-"); // room name is just the combined username and friends username

      this.attachment = null; // the currently attached file
    }

    // next: add componentDidMount

The componentDidMount method is where we connect to Chatkit. The most important part here is the userID that you supply. This should be the same as the user ID that was supplied to the server when the user was created (this was triggered in the login screen earlier):

    async componentDidMount() {
      const { navigation } = this.props;

      const { status } = await Permissions.askAsync(Permissions.CAMERA_ROLL);

      const username = navigation.getParam("username");

      // initialize Chatkit      

      const chatManager = new ChatManager({
          instanceLocator: CHATKIT_INSTANCE_LOCATOR,
          userId: this.user_id,
          tokenProvider: new TokenProvider({ url: CHATKIT_TOKEN_PROVIDER_ENDPOINT })
      });

      // next: add try..catch  
    }

Next is the code for actually connecting the user to Chatkit. Once the user is connected, we make a request to the server to create the room. Once we get a success response, we subscribe the current user to the room so we can subscribe them to the onMessage event. This is one of the event hooks that come with Chatkit so users are notified when specific events happen inside the chat room:

    try {
      let currentUser = await chatManager.connect(); // connect the user to Chatkit
      this.currentUser = currentUser;

      // create the room
      let response = await fetch(CHAT_SERVER, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          user_id: this.user_id,
          room_name: this.room_name
        })
      });

      if (response.ok) { // request was successful
        let room = await response.json();

        this.room_id = room.id.toString();
        // subscribe the user to the room
        await this.currentUser.subscribeToRoom({
          roomId: room.id,
          hooks: {
            onMessage: this.onReceive // executed when a new message is sent in the room
          }
        });

        await this.setState({
          is_initialized: true // hides the animated loader
        });
      }
    } catch (err) {
      console.log("error with chat manager: ", err);
    }

    // set the viewFiles function as a navigation param
    this.props.navigation.setParams({
      viewFiles: this.viewFiles
    });

Next, we render the chat UI. At the top is the animated loader. This becomes visible when Chatkit isn’t initialized yet or when it’s loading previous messages. After that is the actual chat UI. As you can see, it’s very minimal because we’re using Gifted Chat. There are only three required props: messages, onSend, and user. The other props are only used for customizing the chat UI. After that, we render the DownloadsModal:

    render() {
      return (
        <View style={styles.container}>
          {(this.state.is_loading || !this.state.is_initialized) && (
            <ActivityIndicator
              size="small"
              color="#0064e1"
              style={styles.loader}
            />
          )}

          {this.state.is_initialized && (
            <GiftedChat
              messages={this.state.messages}
              onSend={messages => this.onSend(messages)}
              user={{
                _id: this.user_id
              }}
              renderActions={this.renderCustomActions}
              loadEarlier={this.state.show_load_earlier}
              onLoadEarlier={this.loadEarlierMessages}
              renderSend={this.renderSend}
            />
          )}

          <DownloadsModal
            is_visible={this.state.is_modal_visible}
            files={this.state.files}
            closeModal={this.closeModal}
          />
        </View>
      );
    }

Note that the GiftedChat component is responsible for rendering all the chat-related UI. This includes the message bubble, and the text field for entering messages.

When the send button in the chat UI is clicked, the onSend function is executed. The message is automatically passed to it so we use it to construct the actual message to send to Chatkit. If there’s an attachment, we add the attachment property to the message before sending it. You’ll see how we’re setting the value for this.attachment later on:

    onSend([message]) {
      let msg = {
        text: message.text, // the text entered in the text field
        roomId: this.room_id
      };

      this.setState({
        is_sending: true // show the animated loader in place of the send button
      });

      if (this.attachment) {
        let filename = this.attachment.name
          ? this.attachment.name
          : randomstring() + ".jpg"; // images doesn't have file names, any other file type does
        let type = this.attachment.file_type
          ? this.attachment.file_type
          : "image/jpeg"; // assumes that images doesn't have file_type in the attachment object

        // add an attachment to the message
        msg.attachment = {
          file: {
            uri: this.attachment.uri, // the local path to the file that was picked
            type: type, // mime type of the file
            name: `${filename}`
          },
          name: `${filename}`,
          type: this.attachment.type
        };
      }

      // send the message
      this.currentUser.sendMessage(msg).then(() => {
        this.attachment = null; 

        this.setState({
          is_sending: false // hide the animated loader and show the send button (if there's a text in the text field)
        });
      });
    }

An attachment can be a link to a file that’s publicly available in a server, or a local file that needs to be uploaded on Chatkit’s servers. If it’s the latter, then we need to set fetchRequired to true. This means that the current user has to fetch the file first from the Chatkit server in order to have access to it. Also, instead of supplying link (pertains to a public file in a server), we supply a file object which contains various file data (uri, type, and name). This tells the Chatkit client that the file is to be uploaded on Chatkit’s servers.

Next, we add the onReceive function. This gets executed every time the onMessage hook is triggered. It uses the getMessageAndFile function to construct the data that we need to append to the messages and files array in the state:

    onReceive = async data => {
      let { message, file } = await this.getMessageAndFile(data);

      if (file) {
        const { id, name, link, type } = file;
        await this.setState(previousState => ({
          // add the new file
          files: previousState.files.concat({
            id: id,
            name: name,
            link: link,
            type: type
          })
        }));
      }

      await this.setState(previousState => ({
        // add the new message
        messages: GiftedChat.append(previousState.messages, message)
      }));

      if (this.state.messages.length > 9) {
        this.setState({
          show_load_earlier: true 
        });
      }
    };

In the above code, we’re also updating the state to show the button for loading previously sent messages. Why do we need to do this? This is because the onReceive function not only gets executed when a new message is sent in a room whose messages has already been loaded. It also gets executed when the messages are initially loaded. This means that there’s a chance that there are messages older than the ones that are loaded by default when there are already more than nine messages. The same isn’t necessarily true as messages are added, but that’s where we compromise.

Here’s the getMessageAndFile function, it’s responsible for constructing the data which Gifted Chat requires to properly render the messages.


getMessageAndFile = async ({ id, senderId, text, attachment, createdAt }) => {
  let file_data = null;
  let msg_data = {
    _id: id,
    text: text,
    createdAt: new Date(createdAt),
    user: {
      _id: senderId, // the user's ID
      name: senderId, // the user's name
      avatar: "https://png.pngtree.com/svg/20170602/0db185fb9c.png" // the user avatar image
    }
  };

  if (attachment) {
    const { name, type, link } = attachment; // the link where the actual file can be fetched, and the file type



    if (type == "image") {
      msg_data.image = link; // add an image to the message
    } else {
      msg_data.text += `\nattached:\n${name}`; // append the file name to the existing text message
    }

    file_data = {
      id: id, // message ID
      name: name,
      link: link, // actual link to the file
      type: type // file type
    };
  }

  return {
    message: msg_data,
    file: file_data
  };
};

Next is the function for loading older messages. This uses the fetchMessages function provided by Chatkit to get messages after a specific message. By default, this function backward-fetches the messages, so it will select messages older than the one you specified in your initialId. Once we have the older messages, we do the same thing we did in the onReceive function earlier:

    loadEarlierMessages = async () => {
      // show animated loader
      this.setState({
        is_loading: true
      });

      // get the ID of the least recent message that's been loaded
      const earliest_message_id = Math.min(
        ...this.state.messages.map(m => parseInt(m._id))
      );

      try {
        let messages = await this.currentUser.fetchMessages({
          roomId: this.room_id,
          initialId: earliest_message_id, // start fetching after this message
          direction: "older", // backward fetch messages
          limit: 10
        });

        if (!messages.length) {
          this.setState({
            show_load_earlier: false
          });
        }

        let earlier_messages = [];
        let files = [];

        // loop through the messages so we can request for the attached file
        await this.asyncForEach(messages, async msg => {
          let { message, file } = await this.getMessageAndFile(msg);

          earlier_messages.push(message);
          if (file) {
            files.push(file);
          }
        });

        // update the state
        await this.setState(previousState => ({
          messages: previousState.messages.concat(earlier_messages),
          files: previousState.files.concat(files)
        }));
      } catch (err) {
        console.log("error occured while trying to load older messages", err);
      }

      await this.setState({
        is_loading: false
      });
    };

Here’s the asyncForEach function. We needed to use this because the forEach function doesn’t play very well with the async/await pattern. You can read more about it here:

    asyncForEach = async (array, callback) => {
      for (let index = 0; index < array.length; index++) {
        await callback(array[index], index, array);
      }
    };

Next, the renderCustomActions function is used for rendering additional items in the bottom-left portion of the chat UI. In our case, we want to render two icon buttons for picking images and files:

    renderCustomActions = () => {
      if (!this.state.is_picking_file) { // don't render if user is currently picking a file
        let icon_color = this.attachment ? "#0064e1" : "#808080"; // change the color if there's an attachment 

        return (
          <View style={styles.customActionsContainer}>
            <TouchableOpacity onPress={this.openFilePicker}>
              <View style={styles.buttonContainer}>
                <Ionicons name="md-attach" size={23} color={icon_color} />
              </View>
            </TouchableOpacity>

            <TouchableOpacity onPress={this.openImagePicker}>
              <View style={styles.buttonContainer}>
                <Ionicons name="md-image" size={23} color={icon_color} />
              </View>
            </TouchableOpacity>
          </View>
        );
      }

      // show animated loader in place of the buttons if user is currently picking a file
      return (
        <ActivityIndicator size="small" color="#0064e1" style={styles.loader} />
      );
    };

Next, we want to show an animated loader instead of the send button if the app is busy sending a message. Sending a message often takes a few seconds if a file is attached to it, so the main purpose for doing this is to indicate that the app is actually doing something while this process is on going:

    renderSend = props => {
      if (this.state.is_sending) {
        return (
          <ActivityIndicator
            size="small"
            color="#0064e1"
            style={[styles.loader, styles.sendLoader]}
          />
        );
      }

      return <Send {...props} />; // render the default Send button as usual
    };

Next, add the openFilePicker function. This uses Expo’s DocumentPicker for picking the files. As mentioned in the demo earlier, this is only useful when on Android, because there are additional permissions required in iOS:

    openFilePicker = async () => {
      let file = await DocumentPicker.getDocumentAsync();

      // show loader in place of the image and file pickers
      await this.setState({
        is_picking_file: true
      });

      if (file.type == "success") { // user has selected a file if type is success
        this.attachment = {
          name: file.name,
          uri: file.uri,
          type: "file",
          file_type: mime.contentType(file.name) // get mime type
        };

        Alert.alert("Success", "File attached!");
      }

      await this.setState({
        is_picking_file: false
      });
    };

Next, is the openImagePicker function. This uses Expo’s ImagePicker for opening the operating system’s default UI for picking images:

    openImagePicker = async () => {
      let result = await ImagePicker.launchImageLibraryAsync({
        allowsEditing: true, // show the OS's default image editing UI after picking an image
        aspect: [4, 3] // aspect ratio of the image (4 = width, 3 = height)
      });

      await this.setState({
        is_picking_file: true
      });

      if (!result.cancelled) { // user has selected a file if it's not cancelled
        this.attachment = {
          uri: result.uri,
          type: result.type
        };

        Alert.alert("Success", "File attached!");
      }

      await this.setState({
        is_picking_file: false
      });
    };

Here’s the code for opening the downloads modal:

    viewFiles = async () => {
      this.setState({
        is_modal_visible: true
      });
    };

And here’s the code for closing it:

    closeModal = () => {
      this.setState({
        is_modal_visible: false
      });
    };

Lastly, here are the styles for the chat screen. These are mostly for the additional components that we’re rendering in the screen because Gifted Chat already comes with some nice default styling:

    const styles = {
      container: {
        flex: 1
      },
      buttonContainer: {
        padding: 10
      },
      customActionsContainer: {
        flexDirection: "row",
        justifyContent: "space-between"
      },
      loader: {
        paddingTop: 20
      },
      sendLoader: {
        marginRight: 10,
        marginBottom: 10
      }
    };

DownloadsModal component

Here’s the code for the DownloadsModal component. As you’ve seen earlier, this component accepts a visibility setting, the files you want to render, and the method for closing the modal:

    // app/screens/DownloadsModal.js
    import React, { Component } from "react";
    import { Modal, View, Text, TouchableOpacity } from "react-native";
    import { MaterialIcons } from "@expo/vector-icons";

    import DownloadsList from "./DownloadsList";

    const DownloadsModal = ({ is_visible, files, closeModal }) => {
      return (
        <Modal
          animationType="slide"
          transparent={false}
          visible={is_visible}
          onRequestClose={() => {
            console.log("modal closed");
          }}
        >
          <View style={styles.modalContainer}>
            <View style={styles.modalMain}>
              <View style={styles.modalHeader}>
                <Text style={styles.modalHeaderText}>Files</Text>
                <TouchableOpacity onPress={closeModal}>
                  <MaterialIcons name="close" size={25} color="#333" />
                </TouchableOpacity>
              </View>

              <DownloadsList files={files} />
            </View>
          </View>
        </Modal>
      );
    };

    const styles = {
      modalContainer: {
        marginTop: 22
      },
      modalMain: {
        padding: 10
      },
      modalHeader: {
        flexDirection: "row",
        justifyContent: "space-between",
        marginBottom: 15
      },
      modalHeaderText: {
        fontSize: 20
      }
    };

    export default DownloadsModal;

DownloadsList component

The DownloadsList component is the one that actually renders the list of files. It renders the list using the FlatList component. When the download button beside a file is clicked, it uses the Linking library to open the link on the system’s default browser. From there, the user can download the file:

    // app/components/DownloadsList.js
    import React, { Component } from "react";
    import {
      View,
      Text,
      FlatList,
      TouchableOpacity,
      Image,
      Linking
    } from "react-native";

    import { Ionicons } from "@expo/vector-icons";

    export default class DownloadsList extends Component {
      render() {
        const { files } = this.props;

        return (
          <View>
            <FlatList
              data={files}
              renderItem={this.renderItem}
              keyExtractor={(item, index) => item.id.toString()}
              contentContainerStyle={styles.list}
            />
          </View>
        );
      }

      renderItem = ({ item }) => {
        return (
          <View style={styles.listItem}>
            <View style={styles.filenameContainer}>
              {item.type == "image" && (
                <Image
                  style={styles.image}
                  source={{ uri: item.link }}
                  resizeMode={"contain"}
                />
              )}

              {item.type != "image" && (
                <Image
                  style={styles.image}
                  source={require("../../assets/placeholder.png")}
                  resizeMode={"contain"}
                />
              )}

              <Text style={styles.filename}>{item.name.substr(0, 20)}...</Text>
            </View>

            <TouchableOpacity onPress={this.downloadFile.bind(this, item)}>
              <View style={[styles.buttonContainer, styles.buttonDownload]}>
                <Ionicons name="md-download" size={28} color="#4591f3" />
              </View>
            </TouchableOpacity>
          </View>
        );
      };

      downloadFile = async item => {
        Linking.openURL(item.link);
      };
    }

    const styles = {
      list: {
        justifyContent: "center"
      },
      listItem: {
        flexDirection: "row",
        justifyContent: "space-between",
        borderBottomWidth: 1,
        borderBottomColor: "#ccc"
      },
      filenameContainer: {
        flexDirection: "row",
        alignItems: "center",
        justifyContent: "center"
      },
      filename: {
        paddingLeft: 10
      },
      image: {
        width: 50,
        height: 50
      },
      buttonContainer: {
        padding: 10
      },
      buttonDownload: {
        alignSelf: "center"
      }
    };

Server component

At this point, we can now proceed with the server component. Start by creating a server folder in the root directory of the project:

    mkdir server

Once created, navigate inside the folder and initialize a new npm project:

    cd server
    npm init

Once the package.json file is created, install the packages that we need:

    npm install --save @pusher/chatkit-server body-parser cors express

Here’s a brief overview of what each one does:

  • @pusher/chatkit-server - the Node.js library for working with Chatkit.
  • body-parser - for parsing the request body into something we can manipulate.
  • cors - for allowing requests from the app.
  • express - for spinning up a server.

Next, create the server.js file. This is where we will put all the server-related code. Once created, paste the following code to it. This will import all the packages we need and initialize Chatkit and the server:

    // server/server.js
    const express = require("express");
    const bodyParser = require("body-parser");
    const cors = require("cors");
    const Chatkit = require("@pusher/chatkit-server");

    const app = express();
    const instance_locator_id = "YOUR INSTANCE LOCATOR ID";
    const chatkit_secret = "YOUR CHATKIT SECRET";

    // connect to Chatkit
    const chatkit = new Chatkit.default({
      instanceLocator: `v1:us1:${instance_locator_id}`,
      key: chatkit_secret
    });

    // configure the server
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(bodyParser.json());
    app.use(cors());

Next, add a root route. This is only used for testing whether the server is working or not:

    app.get("/", (req, res) => {
      res.send("all green!");
    });

Next, add the route which receives the requests for creating a user when a user logs in. This creates a user account for identifying a specific user. We’re wrapping it inside a try..catch because we’re expecting it to fail if the id is already used previously. This is true if the user has already logged in previously. If that’s the case, then it’s not considered as an error:

    app.post("/users", async (req, res) => {
      const { user_id, username } = req.body;

      try {
        let user = await chatkit.createUser({
          id: user_id,
          name: username
        });

        res.sendStatus(200);
      } catch (err) {
        if (err.error === "services/chatkit/user_already_exists") {
          console.log("user already exists!");
          res.sendStatus(201);
        } else {
          console.log("error occurred: ");
          let statusCode = err.error.status;
          if (statusCode >= 100 && statusCode < 600) {
            res.status(statusCode);
          } else {
            res.status(500);
          }
        }
      }
    });

Next, add the route for creating rooms. We don’t really have a database for storing room names so we store it in an array instead. The only downside of this is if the server restarts, previously created rooms will be considered as new, which then creates a duplicate room. The Chatkit library for Node.js doesn’t really have an API for checking if a room already exists, so this is the only option we have for a simple implementation:

    let rooms = [];
    app.post("/rooms", async (req, res) => {
      const { user_id, room_name } = req.body;
      let room_data = rooms.find(room => {
        return room_name == room.name;
      });

      if (!room_data) {
        let room = await chatkit.createRoom({
          creatorId: user_id,
          name: room_name
        });

        rooms.push({
          id: room.id, // id is generated by Chatkit's servers
          name: room_name
        });

        res.send(room);
      } else {
        res.send(room_data);
      }
    });

Lastly, expose the server to port 3000:

    const PORT = 3000;
    app.listen(PORT, err => {
      if (err) {
        console.error(err);
      } else {
        console.log(`Running on ports ${PORT}`);
      }
    });

Running the app

Now we’re ready to run the app:

    expo start
    node server/server.js

Next, go to the folder where you have downloaded ngrok, authenticate with your ngrok token, and expose the port where the server is running:

    ./ngrok authtoken YOUR_NGROK_TOKEN
    ./ngrok http 3000

Lastly, update the app/screens/Login.js and app/screens/Chat.js file to replace YOUR_NGROK_URL with the HTTPS URL provided by ngrok:

    // app/screens/Login.js
    const CHAT_SERVER = "YOUR_NGROK_URL/users";
    // app/screens/Chat.js
    const CHAT_SERVER = "YOUR_NGROK_URL/rooms";

Conclusion

That’s it! In this tutorial, you learned how to use Chatkit to create a file-sharing app. Along the way, you learned how easy it is to implement a chat UI using React Native Gifted Chat, as well as how to use the file attachment features in Chatkit.

You can find the source code of this tutorial on its GitHub repo.

Clone the project repository
  • Android
  • Chat
  • iOS
  • JavaScript
  • Node.js
  • React Native
  • Chatkit

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.