Using more Event Listeners with the Nexmo Stitch JavaScript SDK

In this getting started guide we'll cover adding more events to the Conversation we created in the simple conversation with member invites getting started guide. We'll deal with multiple types of events, the ones that come via the conversation, and the ones we send to the conversation.

Concepts

This guide will introduce you to the following concepts.

  • Conversation Events - member:left and text: events that fire on a Conversation when someone does an Action
  • Conversation Actions - actions that trigger events on a Conversation
  • Conversation History - an events object that stores all text Events happening on a conversation

Before you begin

1 - Update the JavaScript App

We will use the application we already created for the second getting started guide. All the basic setup has been done in the previous guides and should be in place. We can now focus on updating the client-side application.

1.1 - Add conversation history

The first thing we're going to do is add history to the existing conversation. We're going to add a method that gets all the events that happened in the conversation and displays conversation history in the message feed.

showConversationHistory(conversation) {
  conversation.getEvents().then((events) => {
    var eventsHistory = ""

    events.forEach((value, key) => {
      if (conversation.members.get(value.from)) {
        const date = new Date(Date.parse(value.timestamp))
        switch (value.type) {
          case 'text:seen':
            break;
          case 'text:delivered':
            break;
          case 'text':
            eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date}: <b>${value.body.text}</b><br>` + eventsHistory
            break;

          case 'member:joined':
            eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date}: <b>joined the conversation</b><br>` + eventsHistory
            break;
          case 'member:left':
            eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date}: <b>left the conversation</b><br>` + eventsHistory
            break;
          case 'member:invited':
            eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date}: <b>invited to the conversation</b><br>` + eventsHistory
            break;

          default:
            eventsHistory = `${conversation.members.get(value.from).user.name} @ ${date}: <b>unknown event</b><br>` + eventsHistory
        }
      }
    })

    this.messageFeed.innerHTML = eventsHistory + this.messageFeed.innerHTML
  })
}

Next, update setupConversationEvents to call the method we just created

setupConversationEvents(conversation) {
  ...

  this.showConversationHistory(conversation)
}

1.2 - Add text:seen events

Now that we've done that, it's time to learn about sending events into a conversation. In order to do that, we'll update the text event listener to acknowledge when a user received a message. We'll update the relevant bit from setupConversationEvents to check if a message in the conversation was not sent by the current user, and then call message.seen(). What this does, is it fires a text:seen event on the conversation.

setupConversationEvents(conversation) {
...
  // Bind to events on the conversation
  conversation.on('text', (sender, message) => {
    console.log('*** Message received', sender, message)
    const date = new Date(Date.parse(message.timestamp))
    const text = `${sender.user.name} @ ${date}: <b>${message.body.text}</b><br>`
    this.messageFeed.innerHTML = text + this.messageFeed.innerHTML

    if (sender.user.name !== this.conversation.me.user.name) {
        message.seen().then(this.eventLogger('text:seen')).catch(this.errorLogger)
    }
  })
...
}

We're going to add a listener as well on the conversation for text:seen, notifying other members in the conversation that their message was seen by other members of the conversation.

setupConversationEvents(conversation) {
...
  conversation.on("text:seen", (data, text) => console.log(`${data.user.name} saw text: ${text.body.text}`))
}

1.3 - Add text:typing events

We're going to update the conversation so that we can see when someone in typing. There are two events that enable us to do so, text:typing:on and text:typing:off, with their corresponding methods on the conversation object that fires them, startTyping() and stopTyping(). We'll fist update setupConversationEvents to listen for those events

setupConversationEvents(conversation) {
...
  conversation.on("text:typing:off", data => console.log(`${data.user.name} stopped typing...`))
  conversation.on("text:typing:on", data => console.log(`${data.user.name} started typing...`))
}

Now we're going to fire those events when the user focuses or blurs the message box. We'll update the setupUserEvents method in order to do that

setupUserEvents() {
...
  this.messageTextarea.addEventListener('focus', () => {
    this.conversation.startTyping().then(this.eventLogger('text:typing:on')).catch(this.errorLogger)
  });
  this.messageTextarea.addEventListener('blur', () => {
    this.conversation.stopTyping().then(this.eventLogger('text:typing:off')).catch(this.errorLogger)
  })
}

1.4 - Leave a conversation

Finally, we'll add the UI for user to leave a conversation. Let's add the button at the top of the messages area.

<section id="messages">
  <button id="leave">Leave Conversation</button>
  ...
</section>

And add the button in the class constructor

constructor() {
...
  this.leaveButton = document.getElementById('leave')
}

We'll then update the setupUserEvents method to trigger conversation.leave() when the user clicks the button

setupUserEvents() {
...
  this.leaveButton.addEventListener('click', () => {
    this.conversation.leave().then(this.eventLogger('member:left')).catch(this.errorLogger)
  })
}

To finish, we're going to add a listener for member:left in the setupConversationEvents method. We'll also add a helper function to handle updating the message feed when a user joins or leaves a conversation. And refactor the member:joined handler to use the new helper function.

memberEventHandler(type) {
  return (member, event) => {
    const date = new Date(Date.parse(event.timestamp))
    console.log(`*** ${member.user.name} ${type} the conversation`)
    const text = `${member.user.name} @ ${date}: <b>${type} the conversation</b><br>`
    this.messageFeed.innerHTML = text + this.messageFeed.innerHTML
  }
}

setupConversationEvents(conversation) {
  ...
  conversation.on("member:joined", this.memberEventHandler('joined'))
  conversation.on("member:left", this.memberEventHandler('left'))
}

1.5 - Open the conversation in two browser windows

Now run index.html in two side-by-side browser windows, making sure to login with the user name jamie in one and with alice in the other. Open the developer tools console and start chatting. You'll see events being logged in the console.

That's it! Your page should now look something like this.

Where next?

Using more Event Listeners with the Nexmo Stitch Android SDK

In this getting started guide we'll demonstrate how to show previous history of a Conversation we created in the simple conversation getting started guide. From there we'll cover how to show when a member is typing and mark text as being seen.

Concepts

This guide will introduce you to Conversation Events. We'll be attaching the MarkedAsSeenListener and SeenReceiptListener listeners to a Conversation, after you are a Member.

Before you begin

  • Ensure you have run through the the first and second quickstarts.
  • Make sure you have two Android devices to complete this example. They can be two emulators, one emulator and one physical device, or two physical devices.

1 - Setup

For this quickstart you won't need to emulate any server side events with the Nexmo CLI. You'll just need to be able to login as both users created in quickstarts 1 and 2.

If you're continuing on from the previous guide you may need to regenerate the users JWTs. See quickstarts 1 and 2 for how to do so.

2 Update the Android App

We will use the application we already created for quickstarts 1 and 2. With the basic setup in place we can now focus on updating the client-side application. We can leave the LoginActivity as is. For this demo, we'll solely focus on the ChatActivity.

2.1 Updating the ChatActivity layout

We're going to be adding some new elements to our chat app so let's update our layout to reflect them. The updated layout should look like so:

<!--activity_chat.xml-->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context="com.nexmo.a3usingevents.ChatActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="16dp"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <TextView
        android:id="@+id/typing_notification"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="Someone is typing"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <EditText
            android:id="@+id/chat_box"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:inputType="textAutoComplete"
            tools:text="This is a sample" />

        <ImageButton
            android:id="@+id/send_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@null"
            android:layout_gravity="center"
            android:src="@drawable/ic_send_black_24dp"/>

    </LinearLayout>

</LinearLayout>

Notice that we've added the RecyclerView as well as a TextView with the id typing_notification. We'll load the messages in the RecyclerView and show a message in the typing_notification TextView when a user is typing.

2.2 Adding the new UI to the ChatActivity

In the previous examples we showed messages by adding to a TextView. For this example we'll show you how to use the Stitch SDK in a RecyclerView. Let's add our new UI elements to the ChatActivity:

//ChatActivity.java
public class ChatActivity extends AppCompatActivity {
    private String TAG = ChatActivity.class.getSimpleName();

    private EditText chatBox;
    private ImageButton sendBtn;
    private TextView typingNotificationTxt;
    private RecyclerView recyclerView;
    private ChatAdapter chatAdapter;

    private ConversationClient conversationClient;
    private Conversation conversation;
    private SubscriptionList subscriptions = new SubscriptionList();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat);

        conversationClient = ((ConversationClientApplication) getApplication()).getConversationClient();
        Intent intent = getIntent();
        String conversationId = intent.getStringExtra("CONVERSATION_ID");
        conversation = conversationClient.getConversation(conversationId);

        recyclerView = (RecyclerView) findViewById(R.id.recycler);
        chatAdapter = new ChatAdapter(conversation);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(ChatActivity.this);
        recyclerView.setAdapter(chatAdapter);
        recyclerView.setLayoutManager(linearLayoutManager);

        chatBox = (EditText) findViewById(R.id.chat_box);
        sendBtn = (ImageButton) findViewById(R.id.send_btn);
        typingNotificationTxt = (TextView) findViewById(R.id.typing_notification);

        sendBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                sendMessage();
            }
        });
    }

    private void sendMessage() {
        conversation.sendText(chatBox.getText().toString(), new RequestHandler<Event>() {
            @Override
            public void onError(NexmoAPIError apiError) {
                logAndShow("Error sending message: " + apiError.getMessage());
            }

            @Override
            public void onSuccess(Event result) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        chatBox.setText(null);
                    }
                });
            }
        });
    }
}

We'll also need to attach the EventListener. We'll do so in attachListeners()

@Override
protected void onResume() {
    super.onResume();
    attachListeners();
}

//ChatActivity.java
private void attachListeners() {
    conversation.messageEvent().add(new ResultListener<Event>() {
        @Override
        public void onSuccess(Event result) {
            chatAdapter.notifyDataSetChanged();
            recyclerView.smoothScrollToPosition(chatAdapter.getItemCount());
        }
    }).addTo(subscriptions);
}

And since we're attaching the listeners we'll need to remove them as well. Let's do that in the onPause part of the lifecycle.

//ChatActivity.java
@Override
protected void onPause() {
    super.onPause();
    subscriptions.unsubscribeAll();
}

2.3 Creating the ChatAdapter and ViewHolder

Our RecyclerView will need a Adapter and ViewHolder. We can use this:

//ChatAdapter.java
public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ViewHolder> {

    private static final String TAG = "ChatAdapter";
    private List<Event> events = new ArrayList<>();

    public ChatAdapter(Conversation conversation) {
        events = conversation.getEvents();
    }

    @Override
    public ChatAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        Context context = parent.getContext();
        LayoutInflater inflater = LayoutInflater.from(context);
        View contactView = inflater.inflate(R.layout.chat_item, parent, false);

        ViewHolder viewHolder = new ViewHolder(contactView);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(ChatAdapter.ViewHolder holder, int position) {
        Text textMessage = events.get(position);
        if (textMessage.getType().equals(EventType.TEXT)) {
            holder.text.setText(textMessage.getText());
        }
    }

    @Override
    public int getItemCount() {
        return events.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        private final TextView text;
        private final ImageView seenIcon;

        public ViewHolder(View itemView) {
            super(itemView);
            text = (TextView) itemView.findViewById(R.id.item_chat_txt);
            seenIcon = (ImageView) itemView.findViewById(R.id.item_chat_seen_img);
        }
    }
}

We'll also need to create a layout for the ViewHolder. Our layout will have a textview to hold the message text. The layout will also have a check mark image that we can make visible or set the visibility to gone depending on if the other users of the chat have seen the message or not. The layout will look like so:

<!-- layout/chat_item.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="horizontal"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/item_chat_txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        tools:text="Hello World!"/>

    <View
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_weight="1"
        />

    <ImageView
        android:id="@+id/item_chat_seen_img"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_done_all_black_24dp"
        android:visibility="gone"/>

</LinearLayout>

2.4 - Show chat history

The chat history should be ready when we start the ChatActivity so we'll fetch the history in LoginActivity before we fire the intent to start the next activity. We'll modify the goToConversation() method in LoginActivity to reflect this.

// LoginActivity.java
private void goToConversation(final Conversation conversation) {
    conversation.updateEvents(null, null, new RequestHandler<Conversation>() {
        @Override
        public void onError(NexmoAPIError apiError) {
            logAndShow("Error Updating Conversation: " + apiError.getMessage());
        }

        @Override
        public void onSuccess(final Conversation result) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Intent intent = new Intent(LoginActivity.this, ChatActivity.class);
                    intent.putExtra("CONVERSATION_ID", conversation.getConversationId());
                    startActivity(intent);
                }
            });
        }
    });
}

Calling updateEvents() on a conversation retrieves the event history. You can pass in two Event ids into the updateEvents() method to tell it to only retrieve events within the timeframe of those IDs. We'll pass null into the first two parameters instead since we want to fetch the whole history of the conversation. Now when we fire the intent and start the ChatActivity we'll have the history of the chat loaded into the RecyclerView.

2.5 - Adding Typing and Seen Listeners

We can add other listeners just like we added our other Listener. The startTyping and stopTyping is used to indicate when a user is currently typing or not. The typingEvent() is used to listen to typing events sent. Finally, the seenEvent() will be used to mark our messages as read. We'll add theses listeners to our attachListeners() method.

//ChatActivity.java
private void attachListeners() {
  ...

  chatBox.addTextChangedListener(new TextWatcher() {
      @Override
      public void beforeTextChanged(CharSequence s, int start, int count, int after) {
          //intentionally left blank
      }

      @Override
      public void onTextChanged(CharSequence s, int start, int before, int count) {
          //intentionally left blank
      }

      @Override
      public void afterTextChanged(Editable s) {
          if (s.length() > 0) {
              sendTypeIndicator(Member.TYPING_INDICATOR.ON);
          } else {
              sendTypeIndicator(Member.TYPING_INDICATOR.OFF);
          }
      }
  });

  conversation.typingEvent().add(new ResultListener<Member>() {
      @Override
      public void onSuccess(final Member member) {
          runOnUiThread(new Runnable() {
              @Override
              public void run() {
                  String typingMsg = member.getTypingIndicator().equals(Member.TYPING_INDICATOR.ON) ? member.getName() + " is typing" : null;
                  typingNotificationTxt.setText(typingMsg);
              }
          });
      }
  }).addTo(subscriptions);

  conversation.seenEvent().add(new ResultListener<Receipt<SeenReceipt>>() {
      @Override
      public void onSuccess(Receipt<SeenReceipt> result) {
          runOnUiThread(new Runnable() {
              @Override
              public void run() {
                  chatAdapter.notifyDataSetChanged();
              }
          });
      }
  }).addTo(subscriptions);
}

private void sendTypeIndicator(Member.TYPING_INDICATOR typingIndicator) {
    switch (typingIndicator){
        case ON: {
            conversation.startTyping(new RequestHandler<Member.TYPING_INDICATOR>() {
                @Override
                public void onSuccess(Member.TYPING_INDICATOR typingIndicator) {
                    //intentionally left blank
                }

                @Override
                public void onError(NexmoAPIError apiError) {
                    logAndShow("Error start typing: " + apiError.getMessage());
                }
            });
            break;
        }
        case OFF: {
            conversation.stopTyping(new RequestHandler<Member.TYPING_INDICATOR>() {
                @Override
                public void onSuccess(Member.TYPING_INDICATOR typingIndicator) {
                    //intentionally left blank
                }

                @Override
                public void onError(NexmoAPIError apiError) {
                    logAndShow("Error stop typing: " + apiError.getMessage());
                }
            });
            break;
        }
    }
}

We can tell the Stitch SDK when a member is typing using TextView's addTextChangedListener. We'll attach a TextWatcher to the chatBox. In the afterTextChanged callback we'll look at the length of the text in the EditText. If the text is greater than 0, we know that the user is still typing. Depending on if the user is typing we'll call sendTypeIndicator() with Member.TYPING_INDICATOR.ON or Member.TYPING_INDICATOR.OFF as an argument. The sendTypeIndicator method just fires either conversation.startTyping() or conversation.stopTyping() By adding a listener to conversation.typingEvent() we can then update our typingNotificationTxt with the correct message of who's typing or set the message to null if no one is typing.

Finally we'll add a Listener to the conversation.seenEvent() so that when an event is marked as seen, we'll update the ChatAdapter and show the events as seen in our UI.

2.6 - Marking Text messages as seen

We'll only want to mark our messages as read when the other user has seen the message. If the user has the app in the background, we'll want to wait until they bring the app to the foreground and they have seen the text message in the RecyclerView in the ChatActivity. To do so, we'll need to mark messages as seen in the ChatAdapter. Let's make the following changes to the ChatAdapter

// ChatAdapter.java
public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ViewHolder> {

    private static final String TAG = "ChatAdapter";
    private Member self;
    private List<Event> events = new ArrayList<>();

    public ChatAdapter(Conversation conversation) {
        self = conversation.getSelf();
        events = conversation.getEvents();
    }

    ...

    @Override
    public void onBindViewHolder(ChatAdapter.ViewHolder holder, int position) {
        if (events.get(position).getType().equals(EventType.TEXT)) {
            final Text textMessage = (Text) events.get(position);
            if (!textMessage.getMember().equals(self) && !memberHasSeen(textMessage)) {
                textMessage.markAsSeen(new RequestHandler<SeenReceipt>() {
                    @Override
                    public void onSuccess(SeenReceipt result) {
                        //Left blank
                    }

                    @Override
                    public void onError(NexmoAPIError apiError) {
                        Log.d(TAG, "mark as seen onError: " + apiError.getMessage());
                    }
                });
            }
            holder.text.setText(textMessage.getMember().getName() + ": " + textMessage.getText());
            if (textMessage.getSeenReceipts().isEmpty()) {
                holder.seenIcon.setVisibility(View.INVISIBLE);
            } else {
                holder.seenIcon.setVisibility(View.VISIBLE);
            }
        }
    }

    private boolean memberHasSeen(Text textMessage) {
      boolean seen = false;
      for (SeenReceipt receipt : textMessage.getSeenReceipts()) {
          if (receipt.getMember().equals(self)) {
              seen = true;
              break;
          }
      }
      return seen;
    }
    ...
}

We've added Member self to our constructor and as a member variable to the ChatAdapter. We've also made some changes to the onBindViewHolder method. Before we start marking something as read, we want to ensure that we're referring to a Text message. That's what the events.get(position).getType().equals(EventType.TEXT) check is doing. We only want to mark a message as read if it the sender of the message is not our self. That's why !textMessage.getMember().equals(self) is there. We also don't want to mark something as read if it's already been marked read. The memberHasSeen method looks up all of the SeenReceipts and will only mark the method as read if the current user hasn't created a SeenReceipt. Then, we only want to show the seenIcon if the message has been marked as read. That's what !textMessage.getSeenReceipts().isEmpty() is for.

Try it out

Run the apps on both of your emulators. On one of them, login with the username "jamie". On the other emulator login with the username "alice" Once you've completed this quickstart, you can run the sample app on two different devices. You'll be able to login as a user, join an existing conversation, chat with users, show a typing indicator, and mark messages as read.

Where next?

Try out Enabling Audio in your App

Using more Event Listeners with the Nexmo Stitch In-App Messaging iOS SDK

In this getting started guide we'll demonstrate how to show previous history of a Conversation we created in the simple conversation getting started guide. From there we'll cover how to show when a member is typing.

1 - Setup

  • Ensure you have run through the the first and second quickstarts.
  • Make sure you have two iOS devices to complete this example. They can be two simulators, one simulator and one physical device, or two physical devices.

2 Update the iOS App

We will use the application we already created for quickstarts 1 and 2. With the basic setup in place we can now focus on updating the client-side application. We can leave LoginViewController.swift alone. For this demo, we'll solely focus on the ChatViewController.swift.

2.1 Updating the app layout

We're going to be adding some new elements to our chat app so let's update our layout to reflect them.

2.1.1 UITableView

Let us start with an instance of UITableView whose cells we will use to display messages from the chat. In our .xcodeproj navigate to ChatViewController scene in Main.storyboard. Let us now delete the instance of textView.

Let us control drag an instance of UITableView onto the scene in the textView's spot. After adding the UITableView to storyboards, let us constrain its leading, trailing and top guides to the surrounding Safe Area respectively. We want to set the leading and trailing space to the Safe Area at 16 points. Let us set the constraint for the top layout guide to the top of the Safe Area layout at zero points.

Do not forget to add a prototype cell. Control drag from the object library to add a prototype cell to the top of our instance of UITableView. To finalize the addition let us name the cell: messageCell because the reusable cells will house messages!

2.1.2 UILabel

Add an instance of UILabel just below our instance of UITableView but above our instance of TextField. Later we will call it typingIndicator so that we show a message in the typingIndicator when a user is typing.

2.2 Adding the new UI to the ChatViewController

In the previous quick starts we showed messages by adding to a TextView. For this example we'll show you how to use the iOS SDK with an instance of UITableView. Let's add our new UI outlets to from the view to their controller ChatViewController.

To create a connection from our instance of UITableView to its controller in ChatViewController.swift we set the delegate or dataSource properties referentially. With Main.storyboard open while simultaneously holding shift option command, click on ChatViewController.swift so that it appears in the assistant editor. Control drag from within the body of UITableView to ChatViewController.swift to declare tableView as an outlet as such:

class ChatViewController: UIViewController {
    // tableView for displaying chat
    @IBOutlet weak var tableView: UITableView!
}

Similarly, let us do the same for our instance of UILabel in the following way:

class ChatViewController: UIViewController {
    // typingIndicatorLabel for typing indications
    @IBOutlet weak var typyingIndicatorLabel: UILabel!
}

2.3 Wiring up the Delegate and Datasource

Our instance of UITableView will need a delegate and dataSource. In viewDidLoad(:) we can use this:

// assignment of delegate to our ChatViewController
tableView.delegate = self
// assignment of dataSource to our ChatViewController
tableView.dataSource = self

Designating ChatViewController as delegate for the UITableView means that the ChatViewController agrees to act on behalf of the UITableView to take care of whatever delegate methods are required for our instance of UITableView. Similarly, designating ChatViewController as the dataSource means that the ChatViewController agrees to act on behalf of the UITableView to handle methods required for funneling data into the UITableView. Accordingly, we must now program these methods. This is called 'conforming'.

2.3.1 Programming Delegate and Datasource

If you followed the steps in 2.3, then you should immediately receive a warning saying that "Type ChatViewController.swift does not conform to the protocol UITableViewDataSource". If you do, great! It means our instance of tableView is configured to its controller. Let's make it conform to the protocol now!

In order to make it conform to the UITableViewDataSource protocol we will make use of one of Swift's powerful features: an extension. Down below the class's closing bracket for its declaration, declare an extension for ChatViewController.

Since this extension conforms to UITableViewDelegate, we program it thus:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 0
}

Since the last remaining required method for conforming to the protocol for UITableViewDataSource is cellForRowAt, we will add the method in the following way:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cellWithIdentifier", for: indexPath)
    return cell;
}

Implementing these two methods should remedy the error mentioned earlier. Both methods--numberOfRowsInSection and cellForRowAt, however, are boilerplate. In the next section we configure these methods to interact directly with our instance of ConversationClient to show chat history.

2.4 - Show a chat's history

Let us reconfigure the boilerplate code with properties from our instance of the conversation client.

2.4.1 numberOfRowsInSection

Let's start numberOfRowsInSection. We access the conversations property on conversation that we passed through performSegue(withIdentifier:sender) from the LoginViewController.swift. On the events property, which conforms to Swift's CollectionType, there is a property for .count, which returns the number of messages in a chat's history. It happens like so:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return conversation!.events.count
}

Our instance of tableView ought to return as many rows now as there are events in our instance of conversation, whereas earlier it returned none. If it does, we are halfway there! The next step is to configure cellForRowAt to display the events as messages in the prototype cell's textLabel.text property. We do it by downcasting an event per the row in indexPath as TextEvent that is assigned to the value of constant called message. With message containing the value for each row's messages, we assign it to the value for cell.textLabel?.text. It happens like so:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cellWithIdentifier", for: indexPath)

    let message = conversation?.events[indexPath.row] as? TextEvent

    cell.textLabel?.text = message?.text

    return cell;
}

The next step is to ensure that our instance of tableView updates so in sendBtn(:) we call tableView.reload().

Calling tableView.reload() on a conversation retrieves the event history. Now when we trigger a segue and open the ChatViewController.swift, we'll have the history of the chat loaded in our instance of UITableView.

2.5 - Adding Typing and Seen Listeners

We can add other listeners just like we added our other for subscribing to text events. Our next listener follows from the .members property as opposed to the events property on conversation. Whereas the latter is a collection of events, the former is a collection of members so we can loop through each one of the members with one of Swift's higher order functions like .forEach to subscribe to make a call to a handler. The handler then takes care of who is or is not typing.

conversation!.members.forEach { member in
    member.typing
        .mainThread
        .subscribe(onSuccess: { [weak self] in self?.handleTypingChangedEvent(member: member, isTyping: $0) })
}

With our listener configured, we will programhandleTypingChangedEvent(member:, isTyping:) to figure out whether a member is typing, determine how many members are typing, as well as provide text to be displayed in our instance of UILabel for displaying the typing indications. It happens like so:

func refreshTypingIndicatorLabel(){
    if !whoIsTyping.isEmpty {
        var caption = whoIsTyping.joined(separator: ", ")

        if whoIsTyping.count == 1 {
            caption += " is typing..."
        } else {
            caption += " are typing..."
        }

        typyingIndicatorLabel.text = caption

    } else {
        typyingIndicatorLabel.text = ""

    }
}        

The last step is to add a property called whoIsTyping to our ChatViewController.swift file. We will declare to be of type Set<> whose elements, all of whom are unique, will be of type String so that no member may be duplicated by virtue of the strong typing on the data structure itself:

// a set of unique members typing
private var whoIsTyping = Set<String>()

With these three sets of code in place, the typing indicator updates who is typing when!

Trying it out

Once you've completed this quickstart, you can run the sample app on two different devices. You'll be able to login as a user, join an existing conversation, chat with users, and show a typing indicator.