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

Building a chat app with emoji reactions - Part 2: Building the Android app

  • Neo Ighodaro
June 14th, 2019
For this part of the series, you will need Android Studio 3+ set up on your machine.

Introduction

This is the second part of our series. In the first part, we built the backend for the application. In this part, we will build an Android app. In the app, we will have our user add emoji reactions to the messages of a chat room. When we are done building, your app will look like this:

Prerequisites

To follow the tutorial, you need to have the following:

  • Completed the first part of the series.
  • Have Android Studio v3.x installed on your machine. You can download here.
  • Have a fair knowledge of Kotlin programming language and Android development.

Creating our project

Let us start by creating a new Android project. Open Android Studio and Create New Project. When you are presented with the wizard, choose the Empty Activity template like so:

After that, in the next configuration screen, use the following details:

  • Name - ChatkitEmoji
  • Package name - com.neo.chatkitemoji
  • Language - Kotlin
  • Minimum API level - API 21

After that, select Finish and wait while Android Studio does the work for you.

Building the Android app

By now, your project should be ready. Open your app-module build.gradle file, add these dependencies and sync:

    // File: ./app/build.gradle
    dependencies {
        // [...]

        implementation 'com.android.support:recyclerview-v7:28.0.0'
        implementation 'com.squareup.retrofit2:retrofit:2.5.0'
        implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
        implementation 'com.pusher:pusher-java-client:1.8.0'
        implementation 'com.pusher:chatkit-android:1.2.0'
    }

We just added dependencies for Chatkit, Channels, Retrofit (for network calls) and RecyclerView. Next, we will tweak the styling of our app. Open the colors.xml file and replace it with this:

    <!-- File: ./app/src/main/res/values/colors.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <color name="colorPrimary">#607D8B</color>
        <color name="colorPrimaryDark">#455A64</color>
        <color name="colorAccent">#795548</color>
    </resources>

Before we start implementing the various screens of the app, let us first create some files and do some preliminary setup.

First, create a new class named ChatEmojiApp and paste this snippet:

    // File: ./app/src/main/java/com/neo/chatkitemoji/ChatEmojiApp.kt
    package com.neo.chatkitemoji

    import android.app.Application
    import com.pusher.chatkit.CurrentUser

    class ChatEmojiApp: Application() {
        companion object {
            lateinit var currentUser: CurrentUser
        }
    }

Here, we have an application class where we store a variable that will be used across the whole application - currentUser. This variable will be assigned when login is successful. After creating the class, add it to the AndroidManifest.xml file like this:

    <!-- File: ./app/src/main/AndroidManifest.xml -->
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.neo.chatkitemoji">

      <!-- [...] -->

      <application
          android:name=".ChatEmojiApp"
          >
          <!-- [...] -->
      </application>

      <!-- [...] -->

    </manifest>

Also, add the internet permission in the AndroidManifest.xml file to give the app access to make use of the device’s network connection:

    <!-- File: ./app/src/main/AndroidManifest.xml -->
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.neo.chatkitemoji">

        <uses-permission android:name="android.permission.INTERNET" />

        <!-- [...] -->

    </manifest>

Note: For Android 9+ you need to add the attribute android:usesCleartextTraffic="true``" to the application tag also.

Now, let us create an interface that will be used by Retrofit. Create a new interface named ApiService and paste this in it:

    // File: ./app/src/main/java/com/neo/chatkitemoji/ApiService.kt
    package com.neo.chatkitemoji

    import okhttp3.RequestBody
    import retrofit2.Call
    import retrofit2.http.Body
    import retrofit2.http.POST

    interface ApiService {

        @POST("/users")
        fun createUser(@Body login: RequestBody): Call<String>

        @POST("/updateEmoji")
        fun updateEmoji(@Body login: RequestBody): Call<String>

    }

This interface holds the endpoints we will interact it. The first function - createUser will be used to create new users and the second - updateEmoji will be used to send emoji updates to the server.

Next, create another class named RetrofitInstance and paste this:

    // File: ./app/src/main/java/com/neo/chatkitemoji/RetrofitInstance.kt
    package com.neo.chatkitemoji

    import com.google.gson.GsonBuilder
    import okhttp3.OkHttpClient
    import retrofit2.Retrofit
    import retrofit2.converter.gson.GsonConverterFactory

    class RetrofitInstance {
        companion object {
            val retrofit = Retrofit.Builder()
                .baseUrl("http://10.0.2.2:3000/")
                .addConverterFactory(
                    GsonConverterFactory.create(GsonBuilder().setLenient().create())
                )
                .client(OkHttpClient.Builder().build())
                .build()
                .create(ApiService::class.java)
        }
    }

In this class, we have a static method that returns the ApiService interface we created. This time around, it is wrapped with some retrofit configurations such as the base URL for our requests.

When running apps on the Android simulator, to access the localhost, you need to use this address: 10.0.2.2

Next, we will implement the login screen of the app. Currently, you have a MainActivity and activity_main.xml file. Refactor them to LoginActivity and activity_login.xml respectively.

You can refactor a file by right-clicking the file, then select the RefactorRename option like so:

Now, open the activity_login.xml layout and replace the contents with this:

    <!-- File: ./app/src/main/res/layout/activity_login.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            xmlns:app="http://schemas.android.com/apk/res-auto"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="20dp">
        <EditText
            android:id="@+id/editTextUsername"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"/>
        <Button
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@+id/editTextUsername"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            android:id="@+id/signIn"
            android:text="Sign in"
        />
    </android.support.constraint.ConstraintLayout>

This layout contains an input field for the user to input a username, and a button. When the button is clicked, we will authenticate the user. Now, let us add logic to the LoginActivity class. Open the class and do the following:

Replace your import section with this:

    // File: ./app/src/main/java/com/neo/chatkitemoji/LoginActivity.kt
    import android.content.Intent
    import android.support.v7.app.AppCompatActivity
    import android.os.Bundle
    import com.pusher.chatkit.AndroidChatkitDependencies
    import com.pusher.chatkit.ChatManager
    import com.pusher.chatkit.ChatkitTokenProvider
    import kotlinx.android.synthetic.main.activity_login.*
    import okhttp3.MediaType
    import retrofit2.Call
    import retrofit2.Callback
    import retrofit2.Response
    import org.json.JSONObject
    import okhttp3.RequestBody

After that, replace the onCreate method with this:

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        signIn.setOnClickListener {
            val reqObject = JSONObject()
            reqObject.put("userId", editTextUsername.text.toString())

            val body = RequestBody.create(
                MediaType.parse("application/json; charset=utf-8"),
                reqObject.toString()
            )

            RetrofitInstance.retrofit.createUser(body).enqueue(object:Callback<String>{
                override fun onFailure(call: Call<String>, t: Throwable) {
                }

                override fun onResponse(call: Call<String>, response: Response<String>) {
                    if (response.code()==200){
                        setupChatManager()
                    } else {

                    }
                }
            })
        }
    }

In this method, we make a network request to our server to confirm that the user ID entered is a user that exists on our platform. When the call is successful, we call the setupChatManager method. Create the method inside the same class like so:

    private fun setupChatManager() {
        val chatManager = ChatManager(
            instanceLocator = "CHATKIT_INSTANCE_LOCATOR",
            userId = editTextUsername.text.toString(),
            dependencies = AndroidChatkitDependencies(
                tokenProvider = ChatkitTokenProvider(
                    endpoint = "http://10.0.2.2:3000/token",
                    userId = editTextUsername.text.toString()
                )
            )
        )

        chatManager.connect { result ->
            when (result) {
                is com.pusher.util.Result.Success -> {
                    ChatEmojiApp.currentUser = result.value
                    startActivity(Intent(this@LoginActivity,ChatRoomActivity::class.java))
                }

                is com.pusher.util.Result.Failure -> {

                }
            }
        }
    }

Replace the CHATKIT_INSTANCE_LOCATOR value with the instance locator value from your Chatkit dashboard.

In this method, we initialize the chatManager object with the username inputted by the user, the token endpoint on our server, and the keys from the Chatkit app created in part 1. When the chatManager successfully connects, we save the result of the current user to the ChatEmojiApp.currentUser variable and open the next activity - ChatRoomActivity.

The ChatRoomActivity is where we will subscribe the user to the general room created in the first part. In this activity, we will display a list of messages from the room subscribed to. Create a new empty activity named ChatRoomActivity.

You can create an activity like so:

Now, open the activity_chat_room.xml layout and replace it with this:

    <!-- File: ./app/src/main/res/layout/activity_chat_room.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ChatRoomActivity">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerViewChat"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_marginTop="10dp"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </android.support.constraint.ConstraintLayout>

Since this activity will display a list of messages from a chat room, the layout contains a recycler view to display the messages.

Since we have a recycler view, we need an adapter to manage how items are displayed on the list. Create a new class ChatRoomAdapter and replace the contents with this:

    // File: ./app/src/main/java/com/neo/chatkitemoji/ChatRoomAdapter.kt
    package com.neo.chatkitemoji

    import android.support.v7.widget.LinearLayoutManager
    import android.support.v7.widget.RecyclerView
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.TextView

    class ChatRoomAdapter(val chatMessageClickListener: ChatMessageClickListener): RecyclerView.Adapter<ChatRoomAdapter.ViewHolder>() {

        private var messageList = ArrayList<MessageModel>()

        fun addMessage(model:MessageModel){
            this.messageList.add(model)
            notifyDataSetChanged()
        }

        fun getList() = messageList

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatRoomAdapter.ViewHolder {
            return ViewHolder(
                LayoutInflater.from(parent.context)
                    .inflate(R.layout.chat_list_row, parent, false))
        }

        override fun onBindViewHolder(holder: ChatRoomAdapter.ViewHolder, position: Int) = holder.bind(messageList[position])

        override fun getItemCount() = messageList.size

        inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

            private val username: TextView = itemView.findViewById(R.id.editTextUsername)
            private val message: TextView = itemView.findViewById(R.id.editTextMessage)
            private val recyclerView: RecyclerView = itemView.findViewById(R.id.emojiRecyclerView)

            fun bind(item: MessageModel) = with(itemView) {
                username.text = item.message.userId
                message.text = item.message.text

                val linearLayoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
                recyclerView.layoutManager = linearLayoutManager
                recyclerView.adapter = ChatRoomEmojiListAdapter(item.data)

                this.setOnClickListener {
                    chatMessageClickListener.onMessageClicked(adapterPosition,item)
                }
            }

        }

        interface ChatMessageClickListener {
            fun onMessageClicked(position:Int, item: MessageModel)
        }

    }

This adapter manages how data will be displayed on the ChatRoomActivity. The adapter overrides the following methods:

  • onCreateViewHolder - To know the layout design for each row of the list.
  • getItemCount - to know the size of the list. Here, we just return the size of the array list variable.
  • onBindViewHolder - to bind data to each row of the list.

Apart from the usual override methods, we have some custom methods:

  • addMessage - to easily add an item to the list and refresh the adapter.
  • getList - to return the current version of the list used by the adapter. We need this so that we can modify it later on when a user adds or updates an emoji for a message row.

We also have an interface ChatMessageClickListener which we will use to notify the activity class when the user selects any row. We will send the position of the selected row and the data attached for that row.

In our adapter class, we have some files that are missing. The chat_list_row.xml file, the MessageModel file and the ChatRoomEmojiListAdapter file. The chat_list_row.xml file will show how each row will look like, the MessageModel file is a data class that holds the data for each row, and the ChatRoomEmojiListAdapter file is an adapter class for the recycler view to show emojis in every message row.

Create a new class - MessageModel and paste this:

    // File: ./app/src/main/java/com/neo/chatkitemoji/MessageModel.kt
    package com.neo.chatkitemoji

    import com.pusher.chatkit.messages.Message

    data class MessageModel(val message: Message, val data:ArrayList<EmojiModel>)

    data class EmojiModel (var string :String = "",
                           var count :Int = 1,
                           var userIds :ArrayList<String> = ArrayList())

This class is a data class to show the data available for each message row. This means, apart from the Message object Pusher gives us, we have a list of EmojiModel which will tell us the emoji added, its count and the users that added that emoji.

Next, create a layout chat_list_row.xml and paste this:

    <!-- File: ./app/src/main/res/layout/chat_list_row.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_margin="20dp"
        xmlns:tools="http://schemas.android.com/tools">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/editTextUsername"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="Neo"
            android:textColor="@android:color/black"
            android:textSize="18sp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/editTextMessage"
            app:layout_constraintTop_toBottomOf="@id/editTextUsername"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="Hey there!"
            android:textSize="16sp"/>

        <android.support.v7.widget.RecyclerView
            android:layout_width="0dp"
            android:id="@+id/emojiRecyclerView"
            app:layout_constraintTop_toBottomOf="@id/editTextMessage"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_height="wrap_content">

        </android.support.v7.widget.RecyclerView>

    </android.support.constraint.ConstraintLayout>

From the layout above, we can see that each row will have two textviews (for the name of the sender and the message itself) and a recycler view for showing the emojis.

After that, create a new class named ChatRoomEmojiListAdapter and paste this:

    // File: ./app/src/main/java/com/neo/chatkitemoji/ChatRoomEmojiListAdapter.kt
    package com.neo.chatkitemoji

    import android.support.v7.widget.RecyclerView
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.TextView

    class ChatRoomEmojiListAdapter(var emojiList: ArrayList<EmojiModel>)
        : RecyclerView.Adapter<ChatRoomEmojiListAdapter.ViewHolder>() {

        fun addEmoji(model:EmojiModel){
            this.emojiList.add(model)
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatRoomEmojiListAdapter.ViewHolder {
            return ViewHolder(
                LayoutInflater.from(parent.context)
                    .inflate(R.layout.emoji_item, parent, false))
        }

        override fun onBindViewHolder(holder: ChatRoomEmojiListAdapter.ViewHolder, position: Int) = holder.bind(emojiList.elementAt(position))

        override fun getItemCount(): Int = emojiList.size

        inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

            private val emojiString: TextView = itemView.findViewById(R.id.emojiString)
            private val emojiCount: TextView = itemView.findViewById(R.id.emojiCount)

            fun bind(item: EmojiModel) = with(itemView) {
                emojiString.text =  item.string
                emojiCount.text =  item.count.toString()
            }
        }
    }

This adapter is very similar to the one we created earlier. This one manages the display of emojis for every message row. It displays the emoji itself and the count. This adapter uses the emoji_item.xml layout to implement how each row will look like. Create the layout like so:

    <!-- File: ./app/src/main/res/layout/emoji_item.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_margin="5dp"
        android:layout_height="wrap_content"
        xmlns:app="http://schemas.android.com/apk/res-auto">

        <TextView
            android:id="@+id/emojiString"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/emojiCount"
            app:layout_constraintStart_toEndOf="@id/emojiString"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toTopOf="parent" />

    </android.support.constraint.ConstraintLayout>

Now, let us go back to the ChatRoomActivity to complete our logic. Open the class and replace what you have there with this:

    // File: ./app/src/main/java/com/neo/chatkitemoji/ChatRoomActivity.kt
    package com.neo.chatkitemoji

    import android.app.Activity
    import android.content.Intent
    import android.os.Bundle
    import android.support.v7.app.AppCompatActivity
    import android.support.v7.widget.DividerItemDecoration
    import android.support.v7.widget.LinearLayoutManager
    import com.google.gson.Gson
    import com.pusher.chatkit.CurrentUser
    import com.pusher.chatkit.rooms.RoomListeners
    import com.pusher.client.Pusher
    import com.pusher.client.PusherOptions
    import kotlinx.android.synthetic.main.activity_chat_room.*
    import okhttp3.MediaType
    import okhttp3.RequestBody
    import org.json.JSONObject
    import retrofit2.Call
    import retrofit2.Callback
    import retrofit2.Response

    class ChatRoomActivity : AppCompatActivity(), ChatRoomAdapter.ChatMessageClickListener {
        lateinit var currentUser: CurrentUser

        var currentRow = -1

        private val chatRoomAdapter = ChatRoomAdapter(this)

        private lateinit var options: PusherOptions

        private lateinit var pusher: Pusher

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_chat_room)
            currentUser = ChatEmojiApp.currentUser
            setupRecyclerView()
            subscribeToRoom()
            setupPusher()
        }
    }

In this snippet, we have the Pusher variables which we will initialize much later and a bunch of other variables. In the onCreate method, after setting the layout file, we assign the current user value from the ChatEmojiApp class to our own class variable, then we call some other methods such as:

  • setupRecyclerView - This method is used to set up our recycler view. Create this method in the same class like this:
    private fun setupRecyclerView() {
      with(recyclerViewChat){
        layoutManager = LinearLayoutManager(this@ChatRoomActivity)
        adapter = chatRoomAdapter
        addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
      }
    }

This method initializes the recycler view, assigns the chatRoomAdapter to it and decorates the recycler view to have a vertical line after each item.

  • subscribeToRoom - This is the method where we subscribe the user to a room. Create this method like this:
    private fun subscribeToRoom() {
      currentUser.subscribeToRoom(
        roomId = currentUser.rooms[0].id,
        listeners = RoomListeners(
            onMessage = { message ->
                runOnUiThread {
                    chatRoomAdapter.addMessage(MessageModel(message, ArrayList()))
                }
            }
        ),
        callback = { subscription -> },
        messageLimit = 20
      )
    }

From the snippet above, we subscribed the user to first room on his/her list. Whenever we receive a new message, we add it to the chatAdapter's list. We also limited the message we receive to 20.

  • setupPusher - This method initializes Pusher with your keys from Pusher Channels dashboard. Create the method inside your class like this:
    private fun setupPusher() {
      options = PusherOptions()
      options.setCluster("PUSHER_CHANNELS_CLUSTER")
      pusher = Pusher("PUSHER_CHANNELS_KEY", options)
      pusher.connect()
      pusherSubscribe()
    }

Replace the PUSHER_* placeholders with keys from your Pusher dashboard.

In the snippet above, we initialize our pusher object and connect it. After that, we have another method that helps us to subscribe and listen for updates. After initializing and connecting Pusher, we subscribe to the room’s channel ID in the pusherSubscribe method. Create the method pusherSubscribe like so:

    private fun pusherSubscribe() {
        val channel = pusher.subscribe(currentUser.rooms[0].id)

        channel.bind(
            "emoji-event"
        ) { channelName, eventName, data ->

            val result = JSONObject(data.toString())

            runOnUiThread {

                val chatMessagesIterator = chatRoomAdapter.getList().iterator()

                while (chatMessagesIterator.hasNext()){

                    val messageItem = chatMessagesIterator.next()
                    if (messageItem.message.id.toString() == result.getString("messageId")) {
                        var doesEmojiExist = false
                        val emojiListIterator: MutableIterator<EmojiModel> = messageItem.data.iterator()

                        while (emojiListIterator.hasNext()){
                            val currentValue = emojiListIterator.next()
                            if(result.getString("emoji")== currentValue.string) {
                                // update with new contents
                                doesEmojiExist = true
                                if(result.getString("count").toInt()==0){
                                    emojiListIterator.remove()
                                } else {
                                    currentValue.count = result.getString("count").toInt()
                                    currentValue.userIds = Gson().fromJson(result.getString("userIds"),
                                        ArrayList::class.java) as ArrayList<String>
                                }
                            }
                        }

                        if (!doesEmojiExist){
                            val newModel = EmojiModel()
                            newModel.string = result.getString("emoji")
                            newModel.userIds = Gson().fromJson(result.getString("userIds"),
                                ArrayList::class.java) as ArrayList<String>
                            messageItem.data.add(newModel)
                        }
                    }
                }

                chatRoomAdapter.notifyDataSetChanged()
            }
        }
    }

In this method, we subscribe to the room’s id channel and the emoji-event event. When we get a response from Pusher, we use the message id from the response to compare across the message list to know which message item we will update.

When we find the message item that matches, we loop through all the emojis present for that particular message item to know if the emoji existed before. If it does and the response from Pusher has its count as zero, we remove that emoji from our list otherwise, we just update the count for that emoji on the emoji list of the message item. If the emoji did not exist, we add it as a new emoji to the list.

Finally, we refresh the adapter using the chatRoomAdapter.notifyDataSetChanged() method so that any change to our list will be reflected immediately.

The ChatRoomActivity implements the ChatRoomAdapter.ChatMessageClickListener interface to know when a user selects a message row. The interface requires us to implement a method: onMessageClicked. Add the method inside your class like this:

    override fun onMessageClicked(position:Int,item: MessageModel) {
        currentRow = position
        startActivityForResult(Intent(this,EmojiActivity::class.java),1000)
    }

In this method, we assign the position clicked to our class variable currentRow. This variable will be used subsequently. Next, we start another activity, - EmojiActivity. This activity will list emoji reactions that we can add to a message.

Before we create that activity, let us handle how we will receive results from the EmojiActivity. Add another method like so:

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
      super.onActivityResult(requestCode, resultCode, data)

      if (requestCode==1000 && resultCode == Activity.RESULT_OK){
          var ifEmojiExists = false
          val result = data!!.getStringExtra("result")

          for (item in chatRoomAdapter.getList()[currentRow].data){
              if (item.string == result) {
                  ifEmojiExists = true

                  if(!item.userIds.contains(currentUser.id)){
                      item.userIds.add(currentUser.id)
                      item.count.inc()
                      sendToPusher(result, item.count.inc(), item.userIds)
                  } else {
                      item.userIds.remove(currentUser.id)
                      item.count = if (item.count > 0) item.count.dec() else 0
                      sendToPusher(result,item.count ,item.userIds)
                  }
              }
          }

          if (!ifEmojiExists){
              val userIds = ArrayList<String>()
              userIds.add(currentUser.id)
              sendToPusher(result, 1, userIds)
          }
      }
    }

Whenever you use a startActivityForResult method, you must use an onActivityResult method to handle the result.

In this method, we check if the request code matches what we used in the startActivityForResult. If it does, we loop through the emoji list for that particular message row. We also check if the result gotten from the EmojiActivity (the emoji selected) had already existed before knowing which action to take.

We use the userIds variable to know whether the current user had added that emoji before or not. At every point, we send the updated data (count of the emoji and user list who selected the emoji) using the sendToPusher method.

Create it like this:

    private fun sendToPusher(result: String, count: Int, userIds: ArrayList<String>) {
        val reqObject = JSONObject()

        reqObject.put("messageId",chatRoomAdapter.getList()[currentRow].message.id.toString())
        reqObject.put("roomId", currentUser.rooms[0].id)
        reqObject.put("emoji",result)
        reqObject.put("count",count.toString())
        reqObject.put("userIds", Gson().toJson(userIds))

        val body = RequestBody.create(
          MediaType.parse("application/json; charset=utf-8"),
          reqObject.toString()
        )

        RetrofitInstance.retrofit.updateEmoji(body).enqueue(object: Callback<String> {
            override fun onFailure(call: Call<String>, t: Throwable) {
            }

            override fun onResponse(call: Call<String>, response: Response<String>) {
            }
        })
    }

In this method, we send the current data to our server who broadcasts the update through Pusher Channels.

Finally, let us implement our EmojiActivity . Create a new activity called EmojiActivity. Open the activity_emoji.xml file and replace it with this:

    <!-- File: ./app/src/main/res/layout/activity_emoji.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".EmojiActivity">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/availableEmojiRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>

    </android.support.constraint.ConstraintLayout>

The layout contains another recycler view which will display the available emoji reactions a user can add to a message.

Next, we will create the adapter for the recycler view. Create a new class named AvailableEmojiListAdapter and paste this in the file:

    // File: ./app/src/main/java/com/neo/chatkitemoji/AvailableEmojiListAdapter.kt
    package com.neo.chatkitemoji

    import android.support.v7.widget.RecyclerView
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.TextView

    class AvailableEmojiListAdapter (var emojiList: ArrayList<String>,
                                     var availableEmojiClickListener: AvailableEmojiClickListener)
        : RecyclerView.Adapter<AvailableEmojiListAdapter.ViewHolder>() {

        fun addEmoji(model:String){
            this.emojiList.add(model)
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvailableEmojiListAdapter.ViewHolder {
            return ViewHolder(
                LayoutInflater.from(parent.context)
                    .inflate(android.R.layout.simple_expandable_list_item_1, parent, false))
        }

        override fun onBindViewHolder(holder: AvailableEmojiListAdapter.ViewHolder, position: Int) = holder.bind(emojiList[position])

        override fun getItemCount(): Int = emojiList.size

        inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

            private val emojiString: TextView = itemView.findViewById(android.R.id.text1)

            fun bind(item: String) = with(itemView) {
                emojiString.text =  item

                this.setOnClickListener {
                    availableEmojiClickListener.onEmojiClicked(item)
                }
            }
        }

        interface AvailableEmojiClickListener {
            fun onEmojiClicked(selectedEmoji:String)
        }
    }

In this adapter, we used one of android’s custom layout for the onCreateViewHolder method. We also have a listener to tell us when a user has selected an emoji.

Next, open the EmojiActivity and paste this:

    // File: ./app/src/main/java/com/neo/chatkitemoji/EmojiActivity.kt
    package com.neo.chatkitemoji

    import android.app.Activity
    import android.content.Intent
    import android.support.v7.app.AppCompatActivity
    import android.os.Bundle
    import android.support.v7.widget.GridLayoutManager
    import kotlinx.android.synthetic.main.activity_emoji.*

    class EmojiActivity : AppCompatActivity(), AvailableEmojiListAdapter.AvailableEmojiClickListener {

        private val availableEmojiAdapter = AvailableEmojiListAdapter(ArrayList(), this)

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_emoji)
            setupRecyclerView()
            addEmojis()
        }

        override fun onEmojiClicked(selectedEmoji: String) {
            val resultIntent = Intent()
            resultIntent.putExtra("result",selectedEmoji)
            setResult(Activity.RESULT_OK, resultIntent)
            finish()
        }

        private fun addEmojis() {
            availableEmojiAdapter.addEmoji("\uD83D\uDE09") // wink
            availableEmojiAdapter.addEmoji("\uD83D\uDE20") // angry
            availableEmojiAdapter.addEmoji("\uD83D\uDE02") // face with tears of joy
            availableEmojiAdapter.addEmoji("\uD83D\uDE08") // smiling face with horns
            availableEmojiAdapter.addEmoji("\uD83D\uDC7A") // goblin
        }

        private fun setupRecyclerView() {
            with(availableEmojiRecyclerView){
                layoutManager = GridLayoutManager(this@EmojiActivity,3)
                adapter = availableEmojiAdapter
            }
        }
    }

In this activity, we get feedback when an emoji is selected through the onEmojiClicked method. We set the result of the activity to Activity.RESULT_OK and pass the emoji through an intent back to the calling activity (ChatRoomActivity).

With the addEmojis method, we are able to populate the activity with some emojis which the user can add. Now, we can run our app. Your app should look like this:

Conclusion

In this part, we saw how to use Chatkit and subscribe to a chat room and receive messages. We also saw how we could add emoji reactions to different messages in the chat room. In the final part, we will build the iOS application equivalent.

Feel free to interact with the GitHub repo.

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