Inviting Members with the Nexmo Stitch JavaScript SDK

In this getting started guide we'll cover creating a second user and inviting them to the Conversation we created in the simple conversation getting started guide. From there we'll list the conversations that are available to the user and upon receiving an invite to new conversations we'll automatically join them.

Concepts

This guide will introduce you to the following concepts.

  • Invites - you can invite users to a conversation
  • Application Events - member:invited events that fire on an Application, before you are a Member of a Conversation
  • Conversation Events - member:joined and text events that fire on a Conversation, after you are a Member

Before you begin

1 - Setup

Note: The steps within this section can all be done dynamically via server-side logic. But in order to get the client-side functionality we're going to manually run through the setup.

1.1 - Create another User

Create another user who will participate within the conversation:

$  nexmo user:create name="alice"

The output of the above command will be something like this:

User created: USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab

That is the User ID. Take a note of it as this is the unique identifier for the user that has been created. We'll refer to this as SECOND_USER_ID later.

1.2 - Generate a User JWT

Generate a JWT for the user. The JWT will be stored to the SECOND_USER_JWT variable. Remember to change the YOUR_APP_ID value in the command.

$ SECOND_USER_JWT="$(nexmo jwt:generate ./private.key sub=alice exp=$(($(date +%s)+86400)) acl='{"paths":{"/v1/users/**":{},"/v1/conversations/**":{},"/v1/sessions/**":{},"/v1/devices/**":{},"/v1/image/**":{},"/v3/media/**":{},"/v1/applications/**":{},"/v1/push/**":{},"/v1/knocking/**":{}}}' application_id=YOUR_APP_ID)"

Note: The above command sets the expiry of the JWT to one day from now.

You can see the JWT for the user by running the following:

$ echo $SECOND_USER_JWT

2 - Update the JavaScript App

We will use the application we already created for the first getting started guide. With the basic setup in place we can now focus on updating the client-side application.

2.1 - Add placeholder UI to list Conversations

Update index.html with a placeholder section to list conversations.

<style>
...
    #conversations {
        display: none;
    }
...
</style>
...
<body>
...
    <section id="conversations">
        <h1>Conversations</h1>
    </section>

We'll also update the constructor method with a reference to the conversations element.

constructor() {
    ...
    this.conversationList = document.getElementById('conversations')
}

2.2 - Update the stubbed out Login

Now, let's update the login workflow to accommodate a second user.

Define a constant with a value of the second User JWT that was created earlier and set the value to the SECOND_USER_JWT that was generated earlier.

<script>
...
const SECOND_USER_JWT = 'SECOND USER JWT';

</script>

Update the authenticate function. We'll return the USER_JWT value if the username is 'jamie' or SECOND_USER_JWT for any other username.

<script>

authenticate(username) {
  return username.toLowerCase() === "jamie" ? USER_JWT : SECOND_USER_JWT;
}
</script>

Next, update the login form handler to show the conversation elements instead of the message elements when the form is submitted.

this.loginForm.addEventListener('submit', (event) => {
    event.preventDefault()
    const userToken = this.authenticate(this.loginForm.children.username.value)
    if (userToken) {
        this.listConversations(userToken)
    }
})

2.3 - Update the JS needed to list the Conversations

In the previous quick start guide we retrieved the conversation directly using a hard-coded YOUR_CONVERSATION_ID. This time we're going to list the conversations that the user is a member, allowing the user to select the conversation they want to join. We're going to delete the joinConversation method and create the listConversations method:

listConversations(userToken) {
  new ConversationClient({
          debug: false
      })
      .login(userToken)
      .then(app => {
          console.log('*** Logged into app', app)
          return app.getConversations()
      })
      .then((conversations) => {
        console.log('*** Retrieved conversations', conversations);

        this.updateConversationsList(conversations)
      })
      .catch(this.errorLogger)
}

We're going to create the updateConversationsList method. It will create the HTML wrapper element for the conversations. The code cycles through the conversations, creating HTML elements for each of them, binding a click event listener to each of them, and then adds the element to the wrapper element. The code checks if there were any conversations added, and if not lists a message and then appends the wrapper element to the UI. Finally, it shows the conversations list and hides the login form.

updateConversationsList(conversations) {
  let conversationsElement = document.createElement("ul");
  for (let id in conversations) {
      let conversationElement = document.createElement("li");
      conversationElement.textContent = conversations[id].display_name;
      conversationElement.addEventListener("click", () => this.setupConversationEvents(conversations[id]));
      conversationsElement.appendChild(conversationElement);
  }

  if (!conversationsElement.childNodes.length) {
      conversationsElement.textContent = "You are not a member of any conversations"
  }

  this.conversationList.appendChild(conversationsElement)
  this.conversationList.style.display = 'block'
  this.loginForm.style.display = 'none'
}

We need to also update setupConversationEvents in order to hide the conversationList and show the messages.

setupConversationEvents(conversation) {
  this.conversationList.style.display = 'none'
  document.getElementById("messages").style.display = "block"
  ...
}

2.4 - Listening for Conversation invites and accepting them

The next step is to add a listener on the application object for the member:invited event. Once we receive an invite, we're going to automatically join the user to that Conversation, and re-login the user in order to update the UI. We're going to update the listConversations method in order to do that.

listConversations(userToken) {

    new ConversationClient({
            debug: false
        })
        .login(userToken)
        .then(app => {
            console.log('*** Logged into app', app)

            app.on("member:invited", (member, event) => {
              //identify the sender and type of conversation.
              if (event.body.cname.indexOf("CALL") != 0 && member.invited_by) {
                console.log("*** Invitation received:", event);

                //accept an invitation.
                app.getConversation(event.cid || event.body.cname)
                  .then((conversation) => {
                    this.conversation = conversation
                    conversation.join().then(() => {
                      var conversationDictionary = {}
                      conversationDictionary[this.conversation.id] = this.conversation
                      this.updateConversationsList(conversationDictionary)
                    }).catch(this.errorLogger)

                  })
                  .catch(this.errorLogger)
              }
            })
            return app.getConversations()
        })
        .then((conversations) => {
            console.log('*** Retrieved conversations', conversations);

            this.updateConversationsList(conversations)

        })
        .catch(this.errorLogger)
}

2.5 - Listening for members who joined a conversation

Update the setupConversationEvents method by adding code to listen for the member:joined event on the conversation and then show a message in the UI about a member joining the conversation:

setupConversationEvents(conversation) {
  ...

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

Your index.html should now look something like this  .

2.6 - 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 login as alice in the other. Focus the browser window where you're logged in with alice.

2.7 - Invite the second user to the conversations

Now you will invite alice to the conversation that you created. In your terminal, run the following command and remember to replace your YOUR_CONVERSATION_ID for the Conversation you created in the first guide and the SECOND_USER_ID with the one you got when creating the User for alice.

$ nexmo member:add YOUR_CONVERSATION_ID action=invite channel='{"type":"app"}' user_id=SECOND_USER_ID

The output of this command will confirm that the user has been added to the "Nexmo Chat" conversation.

Member added: MEM-aaaaaaaa-bbbb-cccc-dddd-0123456789ab

You can also check this by running the following request, replacing YOUR_CONVERSATION_ID:

$ nexmo member:list YOUR_CONVERSATION_ID -v

Where you should see an output similar to the following:

name                                     | user_id                                  | user_name | state
---------------------------------------------------------------------------------------------------------
MEM-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | jamie     | JOINED
MEM-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | alice     | INVITED

Return to the previously opened browser windows so you can see alice has a conversation listed now. You can click the conversation name and proceed to chat between alice and jamie.

Where next?

Inviting Members with the Nexmo Stitch Android SDK

In this getting started guide we'll demonstrate creating a second user and inviting them to the Conversation we created in the simple conversation getting started guide. From there we'll list the conversations that are available to the user and upon receiving an invite to new conversations we'll automatically join them.

Concepts

This guide will introduce you to the following concepts:

  • Invites - How to invite users to a conversation
  • Application Events - Attaching the ConversationInvitedListener to a ConversationClient, before you are a Member of a Conversation
  • Conversation Events - Attaching the MemberJoinedListener and MessageListener listeners to a Conversation, after you are a Member

Before you begin

  • Ensure you have run through the previous guide
  • 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

Note: The steps within this section can all be done dynamically via server-side logic. But in order to get the client-side functionality we're going to manually run through the setup.

1.1 - Create another User

Create another user who will participate within the conversation:

$  nexmo user:create name="alice"

The output of the above command will be something like this:

User created: USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab

That is the User ID. Take a note of it as this is the unique identifier for the user that has been created. We'll refer to this as SECOND_USER_ID later.

1.2 - Generate a User JWT

Generate a JWT for the user. The JWT will be stored to the SECOND_USER_JWT variable. Remember to change the YOUR_APP_ID value in the command.

$ SECOND_USER_JWT="$(nexmo jwt:generate ./private.key sub=alice exp=$(($(date +%s)+86400)) acl='{"paths":{"/v1/users/**":{},"/v1/conversations/**":{},"/v1/sessions/**":{},"/v1/devices/**":{},"/v1/image/**":{},"/v3/media/**":{},"/v1/applications/**":{},"/v1/push/**":{},"/v1/knocking/**":{}}}' application_id=YOUR_APP_ID)"

Note: The above command sets the expiry of the JWT to one day from now.

You can see the JWT for the user by running the following:

$ echo $SECOND_USER_JWT

2 Update the Android App

We will use the application we already created for the first getting started guide. With the basic setup in place we can now focus on updating the client-side application.

2.1 Update the stubbed out Login

Now, let's update the login workflow to accommodate a second user.

Define a variable with a value of the second User JWT that was created earlier and set the value to the SECOND_USER_JWT that was generated earlier.

//LoginActivity.java
public class LoginActivity extends AppCompatActivity {
    private static final String TAG = LoginActivity.class.getSimpleName();
    private String USER_JWT = YOUR_USER_JWT;
    private String SECOND_USER_JWT = YOUR_SECOND_USER_JWT;
    ...
}

Update the authenticate function. We'll return the USER_JWT value if the username is 'jamie' or SECOND_USER_JWT for any other username.

//LoginActivity.java
private String authenticate(String username) {
        return username.toLowerCase().equals("jamie") ? USER_JWT : SECOND_USER_JWT;
    }

    private void login() {
        final EditText input = new EditText(LoginActivity.this);
        final AlertDialog.Builder dialog = new AlertDialog.Builder(LoginActivity.this)
                .setTitle("Enter the username you are logging in as")
                .setPositiveButton("Login", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        String userToken = authenticate(input.getText().toString());
                        loginAsUser(userToken);
                    }
                });

        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.MATCH_PARENT);
        input.setLayoutParams(lp);
        dialog.setView(input);

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                dialog.show();
            }
        });
    }

private void loginAsUser(String token) {
    conversationClient.login(token, new RequestHandler<User>() {
        @Override
        public void onSuccess(User user) {
            showLoginSuccessAndAddInvitationListener(user);
            retrieveConversations();
        }

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

We'll be running this device on two different devices (on an emulator or physical devices), so we'll first ask which user you're logging in as. When you enter a username we'll login with their JWT. We'll also add an invitation listener and retrieve any conversations that user is already a part of. Let's cover what the showLoginSuccessAndAddInvitationListener(user) method will do.

2.2 Listening for Conversation invites and accepting them

The next step is to update the login method to listen on the application with the ConversationInvitedListener. Once we receive an invite, we're going to automatically join the user to that Conversation.

//LoginActivity.java
private void showLoginSuccessAndAddInvitationListener(final User user) {
    conversationClient.invitedEvent().add(new ResultListener<Invitation>() {
        @Override
        public void onSuccess(Invitation result) {
            logAndShow(result.getInvitedBy() + " invited you to their chat");
            result.getConversation().join(new RequestHandler<Member>() {
                @Override
                public void onSuccess(Member result) {
                    goToConversation(result.getConversation());
                }

                @Override
                public void onError(NexmoAPIError apiError) {
                    logAndShow("Error joining conversation: " + apiError.getMessage());
                }
            });
        }
    });
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            loginTxt.setText("Logged in as " + user.getName() + "\nStart chatting!");
        }
    });
}

To respond to events where a user is invited to a conversation, you can add a ResultListener<Invitation> to a invitedEvent() on an instance of ConversationClient. In this example we're going to show that the user was invited, join the conversation, and then navigate to our ChatActivity to participate in that conversation. The ResultListener<Invitation> only has a success callback: onSuccess, which means the user successfully received the invite.

Now let's go back to our retrieveConversations() method:

2.3 List the Conversations and handle selecting one

//LoginActivity.java
private void retrieveConversations() {
    conversationClient.getConversations(new RequestHandler<List<Conversation>>() {
        @Override
        public void onSuccess(List<Conversation> conversationList) {
            if (conversationList.size() > 0) {
                showConversationList(conversationList);
            } else {
                logAndShow("You are not a member of any conversations");
            }
        }

        @Override
        public void onError(NexmoAPIError apiError) {
            logAndShow("Error listing conversations: " + apiError.getMessage());
        }
    });
}

There are two ways to see what conversations a member is a part of. conversationClient.getConversations() retrieves the full list of conversations the logged in user is a Member of asynchronously. If you want retrieve the list of conversations a user is a part of in a synchronous manner, you can call conversationClient.getConversationList()

If there wasn't an error retrieving the list of conversations, we'll check if the user has more than one conversation that they are a part of. If there is more than one conversation, then we'll show a dialog allowing the user to select a conversation to enter. If the user doesn't have any conversations then we'll show a message telling them so.

For now, let's show the list of conversations.

//LoginActivity.java
private void showConversationList(final List<Conversation> conversationList) {
    List<String> conversationNames = new ArrayList<>(conversationList.size());
    for (Conversation convo : conversationList) {
        conversationNames.add(convo.getDisplayName());
    }

    final AlertDialog.Builder dialog = new AlertDialog.Builder(LoginActivity.this)
            .setTitle("Choose a conversation")
            .setItems(conversationNames.toArray(new CharSequence[conversationNames.size()]), new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    goToConversation(conversationList.get(which));
                }
            });

    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            dialog.show();
        }
    });
}

We'll loop through the conversations the user is a member of and then show that list in a AlertDialog. When the user selects on of the already created conversations, we'll go to the ChatActivity with that conversation, passing along the conversation ID in an extra like in our first quickstart.

2.4 - Run the apps

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"

2.5 - Invite the second user to the conversations

Finally, let's invite the user to the conversation that we created. In your terminal, run the following command and remember to replace YOUR_APP_ID and YOUR_CONVERSATION_ID ID of the Application and Conversation you created in the first guide and the SECOND_USER_ID with the one you got when creating the User for alice.

$ nexmo member:add YOUR_CONVERSATION_ID action=invite channel='{"type":"app"}' user_id=SECOND_USER_ID

The output of this command will confirm that the user has been added to the "Nexmo Chat" conversation.

Member added: MEM-aaaaaaaa-bbbb-cccc-dddd-0123456789ab

You can also check this by running the following request, replacing YOUR_CONVERSATION_ID:

$ nexmo member:list YOUR_CONVERSATION_ID -v

Where you should see an output similar to the following:

name                                     | user_id                                  | user_name | state  
---------------------------------------------------------------------------------------------------------
MEM-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | jamie     | JOINED
MEM-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | alice     | INVITED

Return to your emulators so you can see alice has a conversation listed now. You can click the conversation name and proceed to chat between alice and jamie.

Try 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 or receive invites, and chat with users.

Where next?

Try out Quickstart 3

Inviting Members with the Nexmo Stitch iOS SDK

In this getting started guide we'll demonstrate creating a second user and inviting them to the Conversation we created in the simple conversation getting started guide. From there we'll list the conversations that are available to the user and upon receiving an invite to new conversations we'll automatically join them.

Concepts

This guide will introduce you to the following concepts:

  • Invites - How do we invite users to a conversation? By programming a call to .subscription on an instance of ConversationClient to subscribe to events for new invitations.
  • Application Events - By programming a call to .subscription on an instance of ConversationClient to subscribe to events for new members.
  • Conversation Events - By programming a call to .subscription on an instance of ConversationClient to check membership to a collection of conversations.

Before you begin

  • Ensure you have run through the previous guide
  • 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.

Note: We do not currently support any drag & drop UIs yet so we'll build on the last UI.

1 - Setup

Note: The steps within this section can all be done dynamically via server-side logic. But in order to get the client-side functionality we're going to manually run through the setup.

1.1 - Create another User

Create another user who will participate within the conversation:

$  nexmo user:create name="alice"

The output of the above command will be something like this:

User created: USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab

That is the User ID. Take a note of it as this is the unique identifier for the user that has been created. We'll refer to this as SECOND_USER_ID later.

1.2 - Generate a User JWT

Generate a JWT for the user. The JWT will be stored to the SECOND_USER_JWT variable. Remember to change the YOUR_APP_ID value in the command.

$ SECOND_USER_JWT="$(nexmo jwt:generate ./private.key sub=alice exp=$(($(date +%s)+86400)) acl='{"paths":{"/v1/users/**":{},"/v1/conversations/**":{},"/v1/sessions/**":{},"/v1/devices/**":{},"/v1/image/**":{},"/v3/media/**":{},"/v1/applications/**":{},"/v1/push/**":{},"/v1/knocking/**":{}}}' application_id=YOUR_APP_ID)"

Note: The above command sets the expiry of the JWT to one day from now.

You can see the JWT for the user by running the following:

$ echo $SECOND_USER_JWT

2 Update the iOS App

We will use the application we already created for the first getting started guide. With the basic setup in place we can now focus on updating the client-side application.

2.1 Update the stubbed out Login

Now, let's update the login workflow to accommodate a second user.

Define a variable with a value of the second User JWT that was created earlier and set the value to the SECOND_USER_JWT that was generated earlier.

// a stub for holding the value of private.key
struct Authenticate {

    static let userJWT = "USER_JWT"
    static let anotherUserJWT = "SECOND_USER_JWT"

}

Update the authenticate function with an instance of UIAlertController with an action for one user, another for another user.

   // login button
    @IBAction func loginBtn(_ sender: Any) {

        print("DEMO - login button pressed.")

        let alert = UIAlertController(title: "My Alert", message: "This is an alert.", preferredStyle: .alert)

        alert.addAction(UIAlertAction(title: NSLocalizedString("jamie", comment: "First User"), style: .`default`, handler: { _ in
            NSLog("The \"First User\" is here!")

            let token = Authenticate.userJWT

            print("DEMO - login called on client.")

            self.client.login(with: token).subscribe(onSuccess: {

                if let user = self.client.account.user {

                    print("DEMO - login successful and here is our \(user)")

                 }, onError: { [weak self] error in

                self?.statusLbl.isHidden = false

                print(error.localizedDescription)

                let reason: String = {
                    switch error {
                    case LoginResult.failed: return "failed"
                    case LoginResult.invalidToken: return "invalid token"
                    case LoginResult.sessionInvalid: return "session invalid"
                    case LoginResult.expiredToken: return "expired token"
                    case LoginResult.success: return "success"
                    default: return "unknown"
                    }
                }()

                print("DEMO - login unsuccessful with \(reason)")

            }).addDisposableTo(self.client.disposeBag) // Rx does not maintain a memory reference; to make sure that reference is still in place; keep a reference of this object while I do an operation.

                }))

        alert.addAction(UIAlertAction(title: NSLocalizedString("alice", comment: "Second User"), style: .default, handler: { (_) in

            NSLog("The \"Second User\" is here!")

            let token = Authenticate.anotherUserJWT

            print("DEMO - login called on client.")

            self.client.login(with: token).subscribe(onSuccess: {

                if let user = self.client.account.user {
                    print("DEMO - login successful and here is our \(user)")

                  }, onError: { [weak self] error in

                self?.statusLbl.isHidden = false

                print(error.localizedDescription)

                let reason: String = {
                    switch error {
                    case LoginResult.failed: return "failed"
                    case LoginResult.invalidToken: return "invalid token"
                    case LoginResult.sessionInvalid: return "session invalid"
                    case LoginResult.expiredToken: return "expired token"
                    case LoginResult.success: return "success"
                    default: return "unknown"
                    }
                }()

                print("DEMO - login unsuccessful with \(reason)")

            }).addDisposableTo(self.client.disposeBag) // Rx does not maintain a memory reference; to make sure that reference is still in place; keep a reference of this object while I do an operation.

                }))

        DispatchQueue.main.async {

            self.present(alert, animated: true, completion: nil)
        }

    }

We'll be running this device on two different devices (on an iOS simulator and a physical devices), so we'll make sure to log one user on one device, another user on another device. ask which user you're logging in as.

2.2 Listening for Conversation invites and accepting them

The next step is to update the login method to listen to changes in conversations' array on the client.conversation that we will configure to subscribe to events for users invited to a conversation. Once a user receives an invite, we're going to automatically join the user to that Conversation.

 // whenever the conversations array is modified
                    self.client.conversation.conversations.asObservable.subscribe(onNext: { (change) in
                        switch change {
                        case .inserted(let conversations, let reason):
                            switch reason {
                            case .invitedBy(let member):
                                conversations.first?.join().subscribe(onSuccess: { _ in
                                    print("You have joined this conversation: \(String(describing:conversations.first?.uuid))")
                                }, onError: { (error) in
                                    print(error.localizedDescription)
                                })
                            default:
                                break
                            }
                        default:
                            break
                        }
                    })

As soon as the invite is received, the user subscribes to the conversation.

2.3 List the Conversations and handle selecting one

As soon as the user subscribes to a conversation, we check to see whether the user joined the conversation or not.


        // figure out which conversation a member has joined
        _ = self.client.conversation.conversations.filter({ (conversation) -> Bool in
        conversation.members.contains(where: { (member) -> Bool in
        return member.user.isMe && member.state == .joined
        })
    })

We'll loop through the conversations the user is a member to make sure that the user is joined to the desired conversation.

2.4 - Run the apps

Run the apps on both the simulator & device. On one of them, login "jamie". On the other login "alice".

2.5 - Invite the second user to the conversations

Finally, let's invite the user to the conversation that we created. In your terminal, run the following command and remember to replace YOUR_APP_ID and YOUR_CONVERSATION_ID ID of the Application and Conversation you created in the first guide and the SECOND_USER_ID with the one you got when creating the User for alice.

$ nexmo member:add YOUR_CONVERSATION_ID action=invite channel='{"type":"app"}' user_id=SECOND_USER_ID

The output of this command will confirm that the user has been added to the "Nexmo Chat" conversation.

Member added: MEM-aaaaaaaa-bbbb-cccc-dddd-0123456789ab

You can also check this by running the following request, replacing YOUR_CONVERSATION_ID:

$ nexmo member:list YOUR_CONVERSATION_ID -v

Where you should see an output similar to the following:

name                                     | user_id                                  | user_name | state  
---------------------------------------------------------------------------------------------------------
MEM-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | jamie     | JOINED
MEM-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | USR-aaaaaaaa-bbbb-cccc-dddd-0123456789ab | alice     | INVITED

Return to your emulators so you can see alice has a conversation listed now. You can click the conversation name and proceed to chat between alice and jamie.

Try 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 or receive invites, and chat with users.