🎉 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

Building ephemeral media messaging for Android

  • Neo Ighodaro

August 13th, 2019
You will need to have Android Studio 3.4+, Node and NPM installed on your machine.

Introduction

The purpose of this tutorial is to show you how to build a chat app with Pusher Chatkit that implements ephemeral messages. These are simply messages that last for a short while. In our case, the messages will disappear after the other user has seen the message.

The messages will be erased from the chatroom and all connected devices and no record of the conversion will be kept. This is great for privacy. For this chat app, we will focus on sending images.

When you finish building, your app will look like this:

Prerequisites

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

  • Have Android Studio 3.4+ installed on your machine. You can download here.
  • Have a fair knowledge of Kotlin programming language and Android development.
  • Node.js and NPM installed on your machine. Install here.
  • A Chatkit account. Create here.
  • A device for testing, you can also use the Android emulator.

Setting up our Chatkit instance

Go to your Chatkit dashboard and create a new instance, name it chatkit-ephemeral-message

After creating your instance, navigate to the console tab and create two users. In our own case, we created neo and joe. We did this to reduce some complexity from our app. Still in the console tab, create a new room named general and add the two users to the room.

Setting up our backend

We will build a basic server that will provide an authentication token for our users. We will build this with Node.js. Follow the following instructions to setup your server.

Create a new folder called chatkit-ephemeral-message. Inside the folder, create a package.json file and paste this into the file:

    // File: package.json
    {
      "name": "chatkit-ephemeral-message",
      "version": "1.0.0",
      "main": "index.js",
      "dependencies": {
        "@pusher/chatkit-server": "^1.1.0",
        "body-parser": "^1.18.3",
        "express": "^4.17.1"
      }
    }

This file contains the description of our project. It also contains the dependencies we will make use of too. After that, create an index.js file and paste this snippet inside it:

    // File: index.js
    const express = require('express');
    const bodyParser = require('body-parser');
    const Chatkit = require('@pusher/chatkit-server');
    const app = express();

    const chatkit = new Chatkit.default({
      instanceLocator: 'CHATKIT_INSTANCE_LOCATOR',
      key:'CHATKIT_SECRET_KEY'
    });

    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));

    app.post('/token', (req, res) => {
      const result = chatkit.authenticate({
        userId: req.query.user_id
      });

      res.status(result.status).send(result.body);
    });

    app.post('/delete-message', (req, res) => {
      const { messageId: id, timer } = req.body;

      setTimeout(() => chatkit.deleteMessage({ id }), timer);
      res.end()
    });

    const server = app.listen(3000, () => {
      console.log(`Express server running on port ${server.address().port}`);
    });

This file contains endpoints for our API server. Here, we have specified two endpoints - /token, which will provide a token to a Chatkit user, and /delete-message, which deletes a message from the room.

Replace the CHATKIT_SECRET_KEY and CHATKIT_INSTANCE_LOCATOR with the real keys from your Pusher Chatkit dashboard.

Install the dependencies for your backend by running this command:

    $ npm install

And then run your server with this command:

    $ node index.js

Your server is now running on localhost port 3000.

We will use ngrok to setup our server so that we can get a temporary live URLL. We need a live URL because we will be testing with a real device.

To get started, head to ngrok’s dashboard and create an account. After that, follow the getting started guide found on your dashboard. You will be required to set up an auth token on your machine before you can start an HTTP tunnel. When you have completed the process, you should have something like this:

Copy out your URL as you will soon need it.

Creating our Android project

Let’s create the Android app right away. Open Android Studio and create a new project. You should see a wizard like this:

Select the Empty Activity template and select Next. After that, you will be required to enter the application details:

In our case, the project will be named ChatkitEphemeralMessage. Also, select the Use androidx.* artifacts option as this is the new naming conventions in Android. Click finish when you are done.

Now, Android Studio will take some time to prepare your project for you, you will have a default MainActivity created for you by the time it is done.

Building our app

As usual, the first thing we will add is our dependencies. Open your app-module build.gradle file and add these among your dependencies:

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

        implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha06'
        implementation 'com.pusher:chatkit-android:1.3.3'
    }

Here, we added a recyclerview dependency since we will need to make use of lists and the chatkit dependency for the chat functionality. After this, sync the radle files so that the new dependencies will be downloaded for you.

Next, we need to create some helper classes to help us while developing. First, create a new interface named ApiInterface. Paste this in the file:

    // File: app/src/main/java/com/neo/chatkitephemeralmessage/ApiInterface.kt

    import okhttp3.ResponseBody
    import retrofit2.Call
    import retrofit2.http.*

    interface ApiInterface {
        @POST("delete-message")
        fun deleteMessage(@Body body: DeleteMessage): Call<ResponseBody>
    }

This file is an interface to be used by Retrofit. It contains the endpoints to be accessed in this app. The request body for the deleteMessage method uses a custom data class. Create the class like so:

    // File: app/src/main/java/com/neo/chatkitephemeralmessage/DeleteMessage.kt
    import com.google.gson.annotations.SerializedName

    data class DeleteMessage(

       @field:SerializedName("messageId")
       val messageId: Int? = 0,

      @field:SerializedName("timer")
       val timer: Int? = 0
    )

After that, we will create a utility class that will help us perform some operations. Create a class named ChatkitApp and paste this snippet:

    // File: app/src/main/java/com/neo/chatkitephemeralmessage/ChatkitApp.kt

    import android.graphics.Bitmap
    import android.graphics.BitmapFactory
    import android.util.Log
    import android.widget.Toast
    import com.pusher.chatkit.CurrentUser
    import com.pusher.chatkit.messages.multipart.NewPart
    import com.pusher.util.Result
    import okhttp3.ResponseBody
    import retrofit2.Call
    import retrofit2.Callback
    import retrofit2.Response
    import java.io.File
    import java.net.HttpURLConnection
    import java.net.URL

    class ChatkitApp {
        companion object {
            lateinit var currentUser: CurrentUser
            const val BASE_URL = "NGROK_HTTPS_BASE_URL" // with trailing back slash
            const val INSTANCE_LOCATOR = "CHATKIT_INSTANCE_LOCATOR"
            val TAG = ChatkitApp::class.java.simpleName
        }

        fun sendMessage(text: String, imageUrl: String?) {
            if (!imageUrl.isNullOrEmpty()) {
                ChatkitApp.currentUser.sendMultipartMessage(
                        roomId = ChatkitApp.currentUser.rooms[0].id,
                        parts = listOf(
                                NewPart.Inline(text, "text/plain"),
                                NewPart.Attachment(
                                        type = "image/jpeg",
                                        file = File(imageUrl).inputStream(),
                                        name = "myImage.jpg",
                                        customData = mapOf("source" to "camera")
                                )
                        ),
                        callback = { result ->
                        }
                )
            }
        }

        fun deleteMessage(messageId: Int) {
            val api = ApiClient.client.create(ApiInterface::class.java)
            api?.deleteMessage(DeleteMessage(messageId, 10000))?.enqueue(object : Callback<ResponseBody> {
                override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
                }

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

        fun getBitmapFromUrl(imageUrl: String?): Bitmap? {
            if (imageUrl == null) {
                return null
            }
            return try {
                val url = URL(imageUrl)
                val connection = url.openConnection() as HttpURLConnection
                connection.doInput = true
                connection.connect()
                val input = connection.inputStream
                BitmapFactory.decodeStream(input)
            } catch (e: Exception) {
                Log.e(TAG, e.message)
                null
            }
        }

    }

Replace the CHATKIT_INSTANCE_LOCATOR holder with the instance locator from your dashboard. Also replace the BASE_URL with your ngrok HTTPS URL

We created this class so that we can use these objects and helper methods globally across the app. In this class, we have a method that can help the current user send and delete messages. We also have a method that gets a bitmap from a URL. The bitmap is what we will set as the image.

In the ChatkitApp class, we used an ApiClient object which is not yet available. Create a new class name ApiClient and paste this:

    // File: app/src/main/java/com/neo/chatkitephemeralmessage/ApiClient.kt

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

    object ApiClient {

        private var retrofit: Retrofit? = null
        val client: Retrofit
            get() {
                val gson = GsonBuilder()
                    .setLenient()
                    .create()

                if (retrofit == null) {
                    retrofit = Retrofit.Builder()
                        .baseUrl(ChatkitApp.BASE_URL)
                        .addConverterFactory(GsonConverterFactory.create(gson))
                        .client(OkHttpClient())
                        .build()
                }
                return retrofit!!
            }
    }

This object will help provide us with an instance of Retrofit which we can use across the app. Since we are going to make use of the external storage, we need to request permission for that. Open your AndroidManifest.xml file and add this permission:

    <!-- File: app/src/main/AndroidManifest.xml -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

Now that we have finished with the helper classes, we will start designing our screens. When the project was generated, a MainActivity file was created for you. We will design the login page in this class. Open the activity_main.xml file and replace the file with this snippet:

    <!-- File: app/src/main/res/layout/activity_main.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.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="12dp"
            tools:context=".MainActivity">

        <EditText
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:id="@+id/username"
                android:hint="Enter username"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"/>

        <Button android:layout_width="match_parent"
                android:text="login"
                android:id="@+id/loginButton"
                app:layout_constraintTop_toBottomOf="@+id/username"
                android:layout_height="wrap_content"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

This layout file has an EditText field which we will use to collect the user’s username and a Button which will trigger a login action. The logic for this layout will be added in the MainActivity class.

Open the class and replace it with this snippet:

    // File: app/src/main/java/com/neo/chatkitephemeralmessage/MainActivity.kt

    import android.Manifest
    import android.content.Intent
    import android.content.pm.PackageManager
    import android.os.Bundle
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.app.ActivityCompat
    import androidx.core.content.ContextCompat
    import com.pusher.chatkit.AndroidChatkitDependencies
    import com.pusher.chatkit.ChatManager
    import com.pusher.chatkit.ChatkitTokenProvider
    import kotlinx.android.synthetic.main.activity_main.*

    class MainActivity : AppCompatActivity() {

        private val MY_PERMISSIONS_READ_EXTERNAL_STORAGE = 1000

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            loginButton.setOnClickListener { setupChatManager() }
            checkPermission()
        }

        private fun setupChatManager() {
          val chatManager = ChatManager(
                instanceLocator = ChatkitApp.INSTANCE_LOCATOR,
                userId = username.text.toString(),
                dependencies = AndroidChatkitDependencies(
                        tokenProvider = ChatkitTokenProvider(
                                endpoint = "${ChatkitApp.BASE_URL}token",
                                userId = username.text.toString()
                        ),
                        context = this.applicationContext
                )
          )

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

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

      }

      private fun checkPermission() {
        if (ContextCompat.checkSelfPermission(this,
                        Manifest.permission.READ_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {

            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                            Manifest.permission.READ_EXTERNAL_STORAGE)) {

            } else {
                // No explanation needed, we can request the permission.
                ActivityCompat.requestPermissions(this,
                        arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
                        MY_PERMISSIONS_READ_EXTERNAL_STORAGE)

            }
        } else {
            // Permission has already been granted
        }
      }

    }

In this class, specifically in the onCreateMethod, we attach a listener to the button so that when the button is clicked, the setupChatManager() method is called. In this method, we use our token endpoint, Chatkit app instance locator and the username to setup a chatManager and connect it. When the connection is successful, we save the result and open the ChatRoomActivity.

While creating a chatManager instance, we passed our token endpoint and the instance locator for our Chatkit app. Make sure you replace the instance locator holder with the instance locator on your dashboard.

We also have the checkPermission() method to get permission to access the users device images since we will be working with images.

Next, create another activity named ChatRoomActivity. Use the EmptyActivity template when creating it. After creating the activity, a layout file - activity_chat_room.xml will be generated for you. Open the activity_chat_room.xml file and paste this:

    <!-- File: app/src/main/res/layout/activity_chat_room.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.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"
        tools:context=".ChatRoomActivity">

        <androidx.recyclerview.widget.RecyclerView
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toTopOf="@id/addImageButton"
            android:id="@+id/recyclerViewMessages"
            android:layout_width="match_parent"
            android:layout_height="0dp"/>

        <Button
            android:id="@+id/addImageButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="Upload Image"
            android:textAllCaps="false"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
             />

    </androidx.constraintlayout.widget.ConstraintLayout>

This layout will show a list of messages, hence the need for the RecyclerView. We also have an Button to enable us send new messages to the group.

Next, we will create an adapter class for the RecyclerView to manage the messages from the room. Create a new class named ChatRoomAdapter and paste this:

    // File: app/src/main/java/com/neo/chatkitephemeralmessage/ChatRoomAdapter.kt
    import android.util.Log
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.ImageView
    import android.widget.TextView
    import androidx.recyclerview.widget.RecyclerView
    import com.pusher.chatkit.messages.multipart.Message
    import com.pusher.chatkit.messages.multipart.Payload


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

        private var messageList = ArrayList<Message>()
        private var recyclerView: RecyclerView? = null
        val chatkitApp = ChatkitApp()

        override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
            super.onAttachedToRecyclerView(recyclerView)
            this.recyclerView = recyclerView
        }

        fun addMessage(message: Message) {
            val index = messageList.indexOfFirst { it.id == message.id }
            if (index >= 0) {
                messageList[index] = message
            } else {
                if (message.sender.id != ChatkitApp.currentUser.id) {
                    deleteMessage(message.id)
                }
                messageList.add(message)
                recyclerView?.scrollToPosition(messageList.size - 1)
            }

           // checks if message contains DELETED, remove from list
            when (val data = message.parts[0].payload) {
                is Payload.Inline -> {
                    if (data.content.equals("DELETED", false)){
                        messageList.remove(message)
                        notifyDataSetChanged()
                    }
                }
            }
            notifyDataSetChanged()
        }

        private fun deleteMessage(messageId: Int) {
            chatkitApp.deleteMessage(messageId)
            notifyDataSetChanged()
        }

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

        override fun onBindViewHolder(holder: 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 image: ImageView = itemView.findViewById(R.id.image)

            fun bind(item: Message) {

                username.text = item.sender.name
                try {
                    image.visibility = View.VISIBLE
                    when (val payload = item.parts[1].payload) {
                        is Payload.Attachment -> {
                            payload.url { result ->
                                when (result) {
                                    is com.pusher.util.Result.Success -> {
                                        val url = result.value
                                       image.setImageBitmap(chatkitApp.getBitmapFromUrl(url))               

                                    }
                                }

                            }
                        }
                    }

                } catch (e: Exception) {
                    image.visibility = View.GONE
                }

            }

        }

    }

This adapter is in charge of displaying the messages in the activity. The adapter overrides the following methods:

onCreateViewHolder

To know the layout design for each row of the list. Here, we are using a custom layout - chat_list_row. Create a new layout named chat_list_row.xml and paste this in the file:

    <!-- File: app/src/main/res/layout/chat_list_row.xml -->
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.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">
        <ImageView
            android:id="@+id/image"
            android:layout_width="0dp"
            android:layout_height="150dp"
            android:scaleType="centerCrop"
            android:layout_marginTop="4dp"
            tools:background="@color/colorPrimary"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/editTextUsername"
             />

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/editTextUsername"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            tools:text="Neo"
            android:textColor="@android:color/black"
            android:textSize="15sp"/>
    </androidx.constraintlayout.widget.ConstraintLayout>

getItemCount

To know the size of the list. Here, we just return the size of the array list object.

onBindViewHolder

To bind data to each row of the list. In this app, we are only dealing with text messages and so, the bind method of the ViewHolder class only checks when it is a text message.

We have a custom method addMessage to help us add item to the adapter’s list and refresh it and also retractMessage, this is what deletes messages.

Now, let us finish things up in the ChatRoomActivity. Open the class and replace it with this:

    // File: app/src/main/java/com/neo/chatkitephemeralmessage/ChatRoomActivity.kt
    import android.app.Activity
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.view.View
    import android.view.inputmethod.InputMethodManager
    import android.widget.EditText
    import androidx.recyclerview.widget.DividerItemDecoration
    import androidx.recyclerview.widget.LinearLayoutManager
    import com.pusher.chatkit.rooms.RoomListeners
    import kotlinx.android.synthetic.main.activity_chat_room.*
    import android.content.Intent
    import android.provider.MediaStore
    import android.util.Log
    import android.widget.Button
    import android.widget.ImageButton
    import java.io.IOException

    # Replace the bundle ID 
    import com.neo.chatkitephemeralmessage.ChatkitApp.Companion.currentUser

    class ChatRoomActivity : AppCompatActivity() {

        private val chatRoomAdapter = ChatRoomAdapter()
        private var imagePath : String? = null
        private val GALLERY_REQUEST_CODE = 10001
        val chatkitApp = ChatkitApp()

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_chat_room)
            setupRecyclerView()
            subscribeToRoom()
            setupButtonListener()
        }

    }

This snippet contains all the imports we need and the class definition itself. Inside the class, we override the onCreate method provided by the Android SDK. Inside this method, we call other methods that you will now create:

setupRecyclerView

This method will set up our RecyclerView as the name implies. Create the method inside the class like so:

    private fun setupRecyclerView() {
      with(recyclerViewMessages){
        layoutManager = LinearLayoutManager(this@ChatRoomActivity)
        adapter = chatRoomAdapter
        addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
      }
    }

In this method, we assign a layout manager and adapter to the recyclerViewMessages, and we also decorate it to show a divider after every item.

subscribeToRoom

Create the method like so:

    private fun subscribeToRoom() {
      currentUser.subscribeToRoomMultipart(
        roomId = currentUser.rooms[0].id,
        listeners = RoomListeners(
          onMultipartMessage = {
            runOnUiThread {
              chatRoomAdapter.addMessage(it)
            }
          }
        ),
        messageLimit = 20, // Optional
        callback = { subscription -> }
      )
    }

This method will subscribe us to the general room which is the user’s first room and when a new message comes in, we tell the adapter about it.

setupButtonListener

This method will add a listener to the button so that when it is clicked, we can send a new message. Create the method like so:

    private fun setupButtonListener() {
      addImageButton.setOnClickListener{
        val intent = Intent(Intent.ACTION_PICK)
        intent.type = "image/*"

        val mimeTypes = arrayOf("image/jpeg", "image/png")
        intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)

        startActivityForResult(intent, GALLERY_REQUEST_CODE)
      }
    }

Here, when the user clicks the upload button, we send an implicit intent to start a gallery picker and we handle the result in the onActivityResult method.

Override the method inside your class like so:

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

    public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        // Result code is RESULT_OK only if the user selects an Image
        if (resultCode == Activity.RESULT_OK)
            when (requestCode) {
                GALLERY_REQUEST_CODE -> {

                    val filePath = data?.data
                    try {
                        val filePathColumn = arrayOf(MediaStore.Images.Media.DATA)
                        val cursor = contentResolver.query(filePath, filePathColumn, null, null, null)
                        cursor!!.moveToFirst()
                        val columnIndex = cursor.getColumnIndex(filePathColumn[0])
                        val imgDecodableString = cursor.getString(columnIndex)
                        cursor.close()
                        imagePath = imgDecodableString
                        Log.e("TAG", imgDecodableString)
                        chatkitApp.sendMessage("", imagePath)
                        imagePath = ""
                    } catch (e: IOException) {
                        e.printStackTrace()
                    }
                }
            }
    }

This methods receives the image selected, gets the absolute path of the image and adds the image as a new message.

Now, our application is complete. You can run it, and you will have something like so:

Conclusion

In this tutorial, we have successfully created an Android app that sends media messages, and deletes it 10 seconds after it is received by the other user. Most of this features are highly customisable and the codebase can be optimised further.

Here is the code to the application on GitHub.

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