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

Implement Chatkit roles and permissions in a React Native chat app - Part 2: Setting permissions on the client

  • Wern Ancheta
June 27th, 2019
You will need a good understanding of React Native, and Node 11+ and Yarn 1+ installed on your machine.

In this tutorial of the two-part series, we will update an existing React Native chat app and implement the roles and permissions on it. We will use the Node.js web app we created on the first part to create the rooms, users, roles, and assign them to each user.

This is the last part of a two-part series on implementing Chatkit roles and permissions in a React Native chat app. In part one of this tutorial you built your own web app for setting the roles and permissions that will be used in the mobile app.

Prerequisites

This tutorial has the same requirements as part one.

Additionally, you’ll also need to have an ngrok account for exposing the server to the internet.

App overview

As mentioned earlier, we will be implementing roles and permissions in an existing React Native chat app. The app is built with Chatkit and React Native Gifted Chat. It has all the basic features of a chat app:

  • Public and private rooms.
  • Sending a message.
  • Attaching image files.
  • Loading older messages.
  • Typing indicators.
  • User presence indicator (whether the users in the room are offline or online).

In this tutorial, we will be adding the following features:

  • Joining public rooms.
  • Adding users to a room.
  • Removing users from a room.
  • Limiting what the user can do based on their assigned role - new room members cannot attach files to a message, only room leaders can add or remove users from a room.

Here’s what the final output will look like:

You can view the code on this GitHub repo.

Setting up the roles and permissions

Before we proceed to actually update the app, we first need to set up the users which we will be using for testing the app. Go ahead and run the server:

cd server
yarn start
open http://localhost:5000/user-console

Create the following rooms:

Room Is Private?
General No
Street Photography No
2A Yoyo Yes

Next, create the following roles:

Role Permissions
new_room_member room:messages:get, message:create, room:typing_indicator:create, file:get.
room_member add file:create to new_room_member permissions.
room_leader add room:members:add and room:members:remove to room_member permissions.

Lastly, create the following users and assign them to their respective rooms and role:

User Room Role
Shu Takada 2A Yoyo room_leader
Shinji Saito 2A Yoyo room_member
David Harvey Street Photography room_leader
Sara Hylton Street Photography room_member
Your Name General room_member

Note that we will still make use of the default roles assigned by Chatkit to each user. That’s going to be their globally scoped role. What you just created are room-scoped roles, which means that they’re only applicable to the user if they’re currently inside that specific room.

Bootstrapping the app

Picking up from where we left off on part one, switch to the chatkit-roles-web branch, and install the dependencies:

    git clone https://github.com/anchetaWern/RNChatkitRoles
    cd RNChatkitRoles
    git checkout chatkit-roles-web
    yarn
    react-native eject
    react-native link react-native-gesture-handler
    react-native link react-native-document-picker
    react-native link react-native-fs
    react-native link react-native-config
    react-native link react-native-vector-icons
    react-native link rn-fetch-blob

Next, we need to update the Android manifest file to allow the app to read from the external storage. This allows React Native Document Picker to pick files from the device’s external storage:

    // android/app/src/main/AndroidManifest.xml
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.rnchatkitroles">
      <uses-permission android:name="android.permission.INTERNET" />
      <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
      ...
    </manifest>

Next, update the android/app/build.gradle file and include the gradle file for React Native Config:

    apply from: "../../node_modules/react-native/react.gradle"
    apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle" // add this

Lastly, update your .env file with your Chatkit credentials:

    CHATKIT_INSTANCE_LOCATOR_ID="YOUR CHATKIT APP INSTANCE (omit v1:us1:)"
    CHATKIT_SECRET_KEY="YOUR CHATKIT SECRET"

Updating the app

Now we’re ready to update the app. One of the new functionality that we’re going to add is the ability to join a room. Currently, users are only able to enter a room that they’re already a member of.

Rooms screen

Start by importing the Alert package. We’ll use it to inform the user when they join a public room which they’re not already a member of:

    // src/screens/Rooms.js
    import { View, Text, FlatList, Button, Alert } from "react-native";

Next, update the renderRoom() method so it renders a Join button instead of the Enter button if the user isn’t already a member of the room:

    renderRoom = ({ item }) => {
      return (
        <View style={styles.list_item}>
          <Text style={styles.list_item_text}>{item.name}</Text>
          {
            item.joined &&
            <Button title="Enter" color="#0064e1" onPress={() => {
              this.enterChat(item);
            }} />
          }
          {
            !item.joined &&
            <Button title="Join" color="#484848" onPress={() => {
              this.joinRoom(item);
            }} />
          }
        </View>
      );
    }

Update the server so that it adds the joined property to the rooms. chatkit.getUserRooms() returns the rooms that the specified user is already a member of. chatkit.getUserJoinableRooms() returns the public rooms that the user isn’t a member of. We simply add a joined property to the results that we’re getting from both and combine the resulting array:

    // server/index.js
    app.post("/rooms", async (req, res) => {
      const { user_id } = req.body;
      try {
        const rooms = await chatkit.getUserRooms({
          userId: user_id
        });
        rooms.map((item) => {
          item.joined = true;
          return item;
        });

        const joinable_rooms = await chatkit.getUserJoinableRooms({
          userId: user_id
        });
        joinable_rooms.map((item) => {
          item.joined = false;
          return item;
        });

        const all_rooms = rooms.concat(joinable_rooms);

        res.send({ rooms: all_rooms });
      } catch (get_rooms_err) {
        console.log("error getting rooms: ", get_rooms_err);
      }
    });

Going back to the app, create the method for handling the event for when the Join button is clicked:

    // src/screens/Chat.js
    joinRoom = async (room) => {
      try {
        const response = await axios.post(`${CHAT_SERVER}/user/join`, { room_id: room.id, user_id: this.user_id });

        Alert.alert("Joined Room", `You are now a member of [${room.name}]`);
        this.goToChatScreen(response, room);

      } catch (join_room_err) {
        console.log("error joining room: ", join_room_err);
      }
    }

Here’s the code for joining a room. We do three things here. First, we add the user to the room. Second, we assign the role of new_room_member to the user. This is important if want to limit what newly joined users can do in a room. Because if we don’t assign a room-scoped role, the default roles will be used instead. The last thing we do is return the object which will determine what the user can or cannot do in the app. Since we know that this user just joined, we can simply hard code the values:

    // server/index.js
    app.post("/user/join", async (req, res) => {
      const { room_id, user_id } = req.body;
      try {
        await chatkit.addUsersToRoom({
          roomId: room_id,
          userIds: [user_id]
        });

        await chatkit.assignRoomRoleToUser({
          userId: user_id,
          name: 'new_room_member',
          roomId: room_id
        });

        const permissions_bool = {
          is_new_room_member: true, // user is a new member
          is_room_member: false,
          is_room_leader: false
        };

        res.send(permissions_bool);
      } catch (user_permissions_err) {
        console.log("error getting user permissions: ", user_permissions_err);
      }
    });

Going back to the app, also add the event handler for when the Enter button is clicked:

    enterChat = async (room) => {
      try {
        const response = await axios.post(`${CHAT_SERVER}/user/permissions`, { room_id: room.id, user_id: this.user_id });
        this.goToChatScreen(response, room);

      } catch (get_permissions_err) {
        console.log("error getting permissions: ", get_permissions_err);
      }
    };

Here’s the code for getting the user permissions:

    // server/index.js
    app.post("/user/permissions", async (req, res) => {
      const { room_id, user_id } = req.body;
      try {
        const roles = await chatkit.getUserRoles({ userId: user_id });
        const role = roles.find(role => role.room_id == room_id);
        const permissions = (role) ? role.permissions : [];

        const permissions_bool = getPermissions(permissions);

        res.send(permissions_bool);
      } catch (user_permissions_err) {
        console.log("error getting user permissions: ", user_permissions_err);
      }
    });

The above code makes use of the getPermissions() function which we haven’t added yet. We can add it right after the call for setting the template location for Mustache.

First, we declare the arrays which specify the permissions for each role. We then sort it and convert it into a string so it can be easily compared with the current user’s permissions:

    app.set('view engine', 'mustache');
    app.set('views', __dirname + '/views');

    // add these:
    const new_room_member_permissions = ['room:messages:get', 'message:create', 'room:typing_indicator:create', 'file:get'];
    const room_member_permissions = new_room_member_permissions.concat(['file:create']);
    const room_leader_permissions = room_member_permissions.concat(['room:members:add', 'room:members:remove']);

    const new_room_member_permissions_str = JSON.stringify(new_room_member_permissions.sort());
    const room_member_permissions_str = JSON.stringify(room_member_permissions.sort());
    const room_leader_permissions_str = JSON.stringify(room_leader_permissions.sort());

    function getPermissions(permissions) {
      const permissions_str = JSON.stringify(permissions.sort());

      const is_new_room_member = (new_room_member_permissions_str === permissions_str);
      const is_room_member = (room_member_permissions_str === permissions_str);
      const is_room_leader = (room_leader_permissions_str === permissions_str);
      return { is_new_room_member, is_room_member, is_room_leader };
    }

Going back to the app, here’s the goToChatScreen() method, as the name suggests, it’s responsible for redirecting the user to the Chat screen. Both the /user/join and /user/permissions route returns the boolean values which represent the role of the user who logged in. We simply pass these values to the Chat screen as a navigation param:

    // src/screens/Rooms.js
    goToChatScreen = (response, room) => {
      const { is_room_leader, is_room_member, is_new_room_member } = response.data;

      this.props.navigation.navigate("Chat", {
        user_id: this.user_id,
        room_id: room.id,
        room_name: room.name,
        is_room_leader,
        is_room_member,
        is_new_room_member
      });
    }

Chat screen

Let’s proceed to update the Chat screen. This is where we add the functionality for adding and removing users from the room, as well as disabling the file attachment feature for new members of the room.

Start by importing the additional packages that we need:

    // src/screens/Chat.js
    import { View, Text, ActivityIndicator, FlatList, TouchableOpacity, Alert, Button } from "react-native"; // add Button
    import axios from "axios"; // add this
    import RNPickerSelect from "react-native-picker-select"; // add this

Next, update the header bar to render the button for adding a new user if the current user has the permission of a room leader:

    headerRight: (
      <View style={styles.header_right}>
        <TouchableOpacity style={styles.header_button_container} onPress={params.showUsersModal}>
          <View style={styles.header_button}>
            <Text style={styles.header_button_text}>Users</Text>
          </View>
        </TouchableOpacity>

        {
          params.is_room_leader &&
          <TouchableOpacity style={styles.header_button_container} onPress={params.showAddUserModal}>
            <View style={styles.header_button}>
              <Text style={styles.header_button_text}>Add User</Text>
            </View>
          </TouchableOpacity>
        }
      </View>
    ),

Next, initialize the state values for updating the visibility of the modal for adding users and the ID of the user to add:

    state = {
      is_users_modal_visible: false,

      // add these:
      is_add_user_modal_visible: false,
      user_to_add: '' // ID of the user to add
    }

Next, update the constructor so it gets the boolean values representing the current user’s permission:

    constructor(props) {
      super(props);
      const { navigation } = this.props;

      this.user_id = navigation.getParam("user_id");
      this.room_id = navigation.getParam("room_id");


      // add these
      this.is_room_leader = navigation.getParam("is_room_leader");
      this.is_room_member = navigation.getParam("is_room_member");
      this.is_new_room_member = navigation.getParam("is_new_room_member");

      this.modal_types = {
        users: 'is_users_modal_visible',
        add_user: 'is_add_user_modal_visible' // add this
      };
    }

Next, update componentDidMount() so it sets the showAddUserModal() method as a navigation param:

    async componentDidMount() {
      this.props.navigation.setParams({
        showUsersModal: this.showUsersModal,
        showAddUserModal: this.showAddUserModal // add this
      });

      // next: get list of users for this Chatkit instance
    }

Next, we check if the current user has the permission of a room leader. If they have, we make a request to get the list of all users for this Chatkit instance. This data is used for populating the dropdown for picking a user to add to the current room. We’re making use of the React Native Picker Select package to easily implement a dropdown. It requires an array of objects with label and value properties. We set the user’s name as the label and the user’s id as the value:

    try {
      // ..

      await this.currentUser.subscribeToRoomMultipart({
        roomId: this.room_id,
        hooks: {
          // ..
        }
      });

      // add these:
      let user_options = [];
      if (this.is_room_leader) {
        const response = await axios.get(`${CHAT_SERVER}/get-users`);
        const { users } = response.data;

        user_options = users.map((item) => {
          return {
            label: item.name,
            value: item.id
          };
        });
      }

      // update this:
      await this.setState({
        is_initialized: true,
        room_users: this.currentUser.users,
        users: user_options // add this
      });

    } catch (chat_mgr_err) {
      console.log("error with chat manager: ", chat_mgr_err);
    }

Here’s the method for showing the modal for adding new users to the room:

    showAddUserModal = () => {
      this.setState({
        is_add_user_modal_visible: true
      });
    }

Next, update the render() method to extract the new values that we have on the state:

    const {
      typing_user,

      // add these:
      is_add_user_modal_visible,
      users,
      user_to_add
    } = this.state;

Under the code for rendering the users modal, add the code for rendering the modal for adding new users to the room:

    {
      room_users &&
      <Modal isVisible={is_users_modal_visible}>
         ...
      </Modal>
    }

    {
      this.is_room_leader &&
      <Modal isVisible={is_add_user_modal_visible}>
        <View style={styles.modal}>
          <View style={styles.modal_header}>
            <Text style={styles.modal_header_text}>Add User</Text>
            <TouchableOpacity onPress={this.hideModal.bind(this, 'add_user')}>
              <Icon name={"close"} size={20} color={"#565656"} style={styles.close} />
            </TouchableOpacity>
          </View>

          <View style={styles.modal_body}>
            <Text style={styles.label}>Enter username</Text>
            {
              users &&
              <RNPickerSelect
                items={users}
                onValueChange={value => {
                  this.setState({
                    user_to_add: value,
                  });
                }}
                value={user_to_add}
              />
            }
            <View style={styles.button_container}>
              <Button title="Add" color="#0064e1" onPress={this.addUserToRoom} />
            </View>
          </View>
        </View>
      </Modal>
    }

Here’s the code for adding a user to the room. user_to_add contains the ID of the user selected from the picker. We use this along with the room_id to add the user to the room:

    addUserToRoom = async () => {
      const { user_to_add } = this.state;
      try {
        await this.currentUser.addUserToRoom({
          userId: user_to_add,
          roomId: this.room_id
        });

        Alert.alert('User Added', 'User was successfully added to the room.');

        this.setState({
          is_add_user_modal_visible: false
        });

      } catch (add_user_to_room_err) {
        console.log("error adding user to room: ", add_user_to_room_err);
      }
    }

Next, we limit new room members from uploading files. renderCustomActions() is responsible for rendering custom buttons on the left of the text field for entering a message. So we simply return null if the current user is a new room member:

    renderCustomActions = () => {
      // add this
      if (this.is_new_room_member) {
        return null; // prevents new room members from attaching files
      }

      if (!this.state.is_picking_file) {
        // ...
      }

      return (
        <ActivityIndicator size="small" color="#0064e1" style={styles.loader} />
      );
    }

Next, update the renderUser() method. This method is responsible for rendering the list of users in the users modal (the one where you can see whose offline and online in the room). We update it so it renders a button for removing a user from the room if the current user is a room leader:

    renderUser = ({ item }) => {
      const online_status = item.presenceStore[item.id];

      return (
        <View style={styles.list_item_body}>
          <View style={styles.list_item}>
            <View style={styles.inline_contents}>
              ...
            </View>
            {
              this.is_room_leader &&
              <View>
                <Button title="Remove" color="#d73a49" onPress={() => this.removeUserFromRoom(item.id)} />
              </View>
            }
          </View>
        </View>
      );
    }

Here’s the code for removing a user from the room. This makes use of Chatkit’s removeUserFromRoom() method to remove the user with the userId you passed from the room:

    removeUserFromRoom = async (id) => {
      try {
        await this.currentUser.removeUserFromRoom({
          userId: id,
          roomId: this.room_id
        });

        Alert.alert('Removed User', 'User was successfully removed from the room.');
      } catch (remove_user_err) {
        console.log("error removing user from room: ", remove_user_err);
      }
    }

The Chatkit server SDK also has methods for adding and removing users from a room. You can also use it because it will also trigger the hooks for when a user joins a room (onUserJoined) or leaves a room (onUserLeft). But it’s preferrable to execute an operation in the client side if there’s a method for it. The main advantage is that it will be faster because you’re making the request directly to Chatkit’s server instead of having your own server act as a middleman for making the request.

Running the app

At this point, you can now run the app.

If you haven’t done so already, start by running the server:

    cd server
    yarn start

Expose the server to the internet using ngrok:

    ./ngrok http 5000

Update src/screens/Login.js, src/screens/Rooms.js, and src/screens/Chat.js to use the ngrok HTTPS URL:

    const CHAT_SERVER = "YOUR NGROK HTTPS URL";

Finally, run the app:

    react-native run-android
    react-native run-ios

You can now test if the roles and permissions are implemented by the app by logging in using the users you’ve created earlier. For example, you can log in with your name (only if you created a new user for it earlier), and join the Street Photography room. Since you just joined, the new_room_member role is assigned to you and you won’t be able to attach files to a message. You can then access the web app and change your role to room_member to enable attaching of files. Lastly, you can change your role to room_leader to enable adding of new users to the room and removing existing users.

Conclusion

In this tutorial, you learned how to implement Chatkit roles and permissions to a React Native chat app. As you have seen, it’s only a matter of setting up the roles and permissions on the server. From there, you can simply fetch the permissions based on the room that the user is currently at, and selectively rendering UI components based on those permissions.

You can view the code on this GitHub repo.

Clone the project repository
  • Chat
  • JavaScript
  • Node.js
  • React Native
  • Android
  • iOS
  • 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.