We have seen how to get started with Group Chat(Multi-User Chat) using the Smack and ejabberd server. Now, we will have a look at some features which are as follows.

1. User Typing Status

2. User’s who joined and left the group

Let’s see the above features in detail.

1. User Typing Status :

In single-user chat(One To One) there is a class i.e. ChatStateManager which provides user typing status. But in group chat, there is no such class available. So to achieve user typing status in group chat we need to use Extension. The extension means we can add custom data to the Message class. We can send different custom data(such as ChatState, message sent time etc.) to different extensions(ChatStateExtension, StandardExtensionElement).

Here we will use ChatStateExtension to send user typing status. We are going to pass custom data i.e. ChatState to ChatStateExtension. There are different states of ChatState which are <active/>, <inactive/>, <gone/>, < composing /> and < paused />. Here we will be using only    < composing /> and < paused /> state.

<composing> : This state is used when user is typing.

<paused> : User had been typing but now has stopped.

To use this state’s we strictly have to follow NO REPETITION POLICY which is explained below.

If the user types continuously for a long time (e.g., while composing a lengthy reply), the user SHOULD NOT send more than one standalone <composing/> state in a row. More specifically, a user SHOULD NOT send second instance of state which will be same as previous state (i.e., a standalone state MUST be followed by a different state, not repetition of the same state).

If we doesn’t follow this policy, performance will be hit badly. It may slow down your app’s performance, your chat feature may lag and even it can crash some times. Practically, we can say you will not be able to even send messages.

Let’s start with the implementation of Chat State Status.

1. We will use TextWatcher on EditText.

messageEditText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {

override fun beforeTextChanged(messageBody: CharSequence?, start: Int, count: Int, after: Int) {

override fun onTextChanged(messageBody: CharSequence?, start: Int, before: Int, count: Int) {
fun checkTextLength(messageBody: CharSequence?) {
if (!TextUtils.isEmpty(messageBody)) sendChatState(ChatState.composing)

We will use the below variables in all below functions:

private var multiUserChat: MultiUserChat? = nullvar liveChatState = MutableLiveData()
private val chatStateHandler = Handler(Looper.getMainLooper())
private val NAMESPACE = “http://jabber.org/protocol/chatstates”
private var isComposingChatStateSent: Boolean = false
private var isPausedChatStateSent: Boolean = falseprivate lateinit var username: Stringprivate var disposable: CompositeDisposable = CompositeDisposable()
var xmppServiceGroupDomain: DomainBareJid = JidCreate.domainBareFrom(“domain_name_for_group_chat”)
fun sendChatState(chatState: ChatState) {
if (smackConnection != null && smackConnection!!.isConnected) {
if (!isComposingChatStateSent || ! isPausedChatStateSent) {
val chatStateMessage = Message()

//create empty body message
chatStateMessage.body = null
chatStateMessage.type = Message.Type.groupchat
chatStateMessage.subject = null
chatStateMessage.to = multiUserChat?.room
//the user who is typing i.e. logged in user
chatStateMessage.from = JidCreate.bareFrom(Localpart.from(username), xmppServiceDomain)
//create extension using ChatStateExtension
//this extension will be fetched in incoming message listener and is explained below
val extension = ChatStateExtension(chatState)
if (!isComposingChatStateSent && chatState == ChatState.composing) {
//message sent with composing(typing) event
isComposingChatStateSent = true
//message sent with pause event  after 3 seconds
//code is written below in chatStaePauseRunnable
chatStateHandler.postDelayed(chatStatePausedRunnable, 3000)
} else {
if (!isPausedChatStateSent && chatState == ChatState.paused) {
isPausedChatStateSent = true

disposable.add(Observable.interval(6000L, TimeUnit.MILLISECONDS)
{ t ->
if (isComposingChatStateSent && isPausedChatStateSent) {
isComposingChatStateSent = false
isPausedChatStateSent = false


private val chatStatePausedRunnable = Runnable {

How NO REPETITION POLICY is achieved is explained below

In func sendChatState (ChatState)

  1. As user types its first character, we are sending a message which includes ChatStateExtension with composing event.
  2. Now after sending composing event message we are sending a paused event after delay of 3 seconds. We have considered here 3 seconds, but you can consider according to your needs.
  3. After sending message with paused event we are running a background task which sets Boolean variables to false after 6 seconds. So we can start the same process again of sending Chat States.

So, we can see here we are not sending same event which was sent earlier. This is how we can achieve NO REPETITION POLICY.

After sending chatStateMessage, this message will be captured in chat’s MessageListener.

private val incomingMessageListener = object : MessageListener {
override fun processMessage(message: Message?) {
if (message?.from?.resourceOrEmpty.toString() != username) {
//get extension which we added in message in above function
val elementName = message?.getExtension(NAMESPACE)?.elementName
if (elementName != null) {
val state = ChatState.valueOf(elementName)
if (state == ChatState.composing) {
//if chat state is composing
liveChatState.postValue(message.from.resourceOrEmpty.toString() + ” is typing…”)
} else {
//when chat state is paused

liveChatState contains the value of which the user is typing.

This is how we can implement this important feature in group chat.

2. User’s who joined and left the group:

private var multiUserChat: MultiUserChat? = null

Add ParticipantStatusListener to multiUserChat object.


Now, we will initialize presenceListener.

private val presenceListener = object : ParticipantStatusListener {
override fun joined(participant: EntityFullJid?) {
if (participant!!.resourceOrEmpty.toString() != loggedInUser) {
val presenceMessage = Message()
presenceMessage.body = participant.resourceOrEmpty.toString() + ” joined”
//logic for adding this message to your adapter

override fun left(participant: EntityFullJid?) {
if (participant!!.resourceOrEmpty.toString() != loggedInUser) {
val presenceMessage = Message()
presenceMessage.body = participant.resourceOrEmpty.toString() + ” left”
//logic for adding this message to your adapter

override fun adminRevoked(participant: EntityFullJid?) {

override fun adminGranted(participant: EntityFullJid?) {

override fun moderatorGranted(participant: EntityFullJid?) {

override fun membershipRevoked(participant: EntityFullJid?) {

override fun membershipGranted(participant: EntityFullJid?) {

override fun moderatorRevoked(participant: EntityFullJid?) {

override fun banned(participant: EntityFullJid?, actor: Jid?, reason: String?) {

override fun voiceRevoked(participant: EntityFullJid?) {

override fun nicknameChanged(participant: EntityFullJid?, newNickname: Resourcepart?) {

override fun ownershipRevoked(participant: EntityFullJid?) {

override fun voiceGranted(participant: EntityFullJid?) {

override fun ownershipGranted(participant: EntityFullJid?) {
override fun kicked(participant: EntityFullJid?, actor: Jid?, reason: String?) {

Here, we can see that there are many functions in this listener. But we will focus on first two functions which are joined and left. So we can understand by the names of this functions what they do.

In Joined and left functions we get the participant who joined or left, from this object we can extract user’s name – the code is written in respective method.

This is how we can get the user’s presence status in group i.e. who joined or left.

If you have any further issues or confusion regarding this, please feel free to comment.

If you have idea of app which require Group chat, Feel free to contact us. We’ll help you translate them into reality.

Do you have any product idea or business need?



Offline mobile app development is critical for users to sync their data properly, when offline. Here, we help you learn the process from implementation to data synchroniz

Omnivore POS integration - Ensuring Agility To Your Restaurant Businesses

Omnivore POS integration - Ensuring Agility To Your Restaurant Businesses

Omnivore software offers a point-of-sales integration API making the restaurant system agile, more customer engaging and adaptive to fast changing business environments.

Unit Testing using Mockk.io

Unit Testing using mockK.io in Kotlin

Learn about unit testing using mockk.io in Kotlin.