Working with Twilio Coversations

Complaining about docs has been a global rant for now but then not everyone has
the time and man power to write documentation, I don't know why Twilio skipped
on this but the only way to figure out working with Conversations is the
incomplete documentation about the basic flow of the events from their
documentation and the somewhat documented types on their typescript generated
typedoc reference.

Now, I actually got done with the usecase pretty well since I've worked with it
before and knew the mistakes I made before so it was easy this time and hardly
took a day or so to get the additional features with integrations to be done,
but I got feedback on a discord channel regarding a helper library I built for
twilio conversations and the feedback was

While I understand what your library is trying to do, I don't think it's easy
to look for documentation on the original twilio reference since most of the
time you don't really know what is to be used for what.

There's more to the feedback but this made sense, since the first time I worked
with programmable chat and conversations, I had to take reference from the then
available example repositories using these services. Which is generally a
secondary approach to learning something. The primary approach for most people
is referencing the documentation or stack overflow.

Anyway, since this comment then got a few more supporting comments regarding
similar issues, we are going to try to have a base flow of what to look for when
working with a simple chat app and then a few additions in case you work with a
more complex one.

Note: There's actual code sample after the theory is done for reference, if
just reading doesn't help you

Nomenclature

clearing this will help us with the remaining of the explanation

These are the 3 basic resources you need to work with for a very simple chat
app, and each of these can be manipulated more to create something more complex
once you understand the limitation of each.

Twilio Client and Initialization

The basic functionality will require you to get a twilio client library and then
listening to various events on that client. There's a good amount of debate on
when you should be initializing the client, we'll get to the simplest options in
a bit but let's get to the flow of working with the client.

  1. You request a server for the auth token with the needed twilio grants
  2. You use this token to initialize the client
  3. You add listeners to handle the conversation events.

That's the flow for init of client, you will generally initialize the client
at the soonest possible time in your app based on a few variables involved.

Conversations and Listing them

While, a lot of backend developers would just create the token and hand it over
to you for you to add listeners for everything that twilio provides, it's
generally easier for the backend to do the conversation listing for the app
initialization stage.

This helps with a few things, in most cases the conversations are going to be 1
on 1 and you'd need more than just the conversation name, your app might need
the user's full details, the profile pics, etc etc , which "surprisingly" the
backend has faster access too. So here's the things I'd recommend that you get
from the backend for the initial chat list.

This helps the client/frontend to make one initial call to get the list of
conversations to show and it's faster to render than doing something like

  1. Wait for twilio client to connect
  2. get conversation list
  3. check the identity for each conversation.
  4. retrieve user details from the identity of each conversation.
  5. fetch avatar url using the user details

This doesn't mean the frontend won't add listeners. Once you are connected to
the client, you are better off handling the events listed below on the frontend.

And there's a ton more of these events for message and conversation, there's
also typing events if you wish to add that to the conversation list as well,
you'll have to modify how your chat works for this which is next.

Conversations and sending messages

This is also something that would initially load data from the backend while the
listeners get added. The initial data would involve you sending the backend your
participant id and getting the last 4-5 messages and listing them, unless you
plan to something like slack and keep the past N messages of each channel in a
cache in the indexedDB (for web) or some sqlite in the mobile apps, which is
also fine if you ask me , though let's say we don't want to do that for now, we
can have the backend send through the last few messages, avatar data (if you
aren't caching that), etc.

and then we have the listeners that we would like to listen to.

Done with the boring parts, based on the above you update your state with the
needed data to show the message. As for handling typing and read, unread message
You have to update the last read message time in the conversation which is on
the conversation object with the method setAllMessagesRead or
updateLastReadMessageIndex , unless you trigger one of these messages the
getUnreadMessagesCount method on the conversation will always return null.

Now for handling typing indicators, that's very simple and actually documented
so I could redirect, but since I've already written so much. You use the method
typing on the conversation instance, which will trigger an event lasting a
duration of 3-4 seconds , unless another typing event is triggered, so you can
add this to your input's keypress event and as long the person is actually
typing, this event keeps firing thus letting you to show that the person is
actually typing.

Finally sending the message is as simple as calling the sendMessage method on
the conversation instance with the text message or a media message. Do use the
attributes optional parameter for the sendMessage function to add unqiue
trackable values if needed.

all the reading aside, let's see it in example. I'll be using my helper library
to reduce my work here

// app-init.js

import {
  createClient,
  onInit,
  onTokenAboutToExpire,
} from "@barelyhuman/twilio-conversations";

let client;

export async function initializeTwilio() {
  if (client) return client;

  const token = await fetchTwilioToken();
  client = createClient(token);

  onInit(() => {
    console.log("Twilio client , connected");
  });

  onTokenAboutToExpire((ttl) => {
    // not using the ttl, but it's there if you need it
    fetchTwilioToken().then((token) => client.updateToken(_nextToken));
  });
}
// chat-list.js
import { onInit, onMessageAdded } from "@barelyhuman/twilio-conversations";
import { initializeTwilio } from "../app-init.js";

async function ChatList() {
  const conversationList = fetchConversations();

  // render the list on screen
  render(conversationList, {
    onClick: (conversation) =>
      navigateTo("Chat", { conversation: conversation.sid }),
  });

  const client = await initializeTwilio();
  // if the client isn't connected wait for it to happen
  if (client.connectionState !== "connected") {
    onInit(() => {
      rerenderChatList();
    });
  }

  // add the needed listeners
  onMessageAdded((message) => {
    const conversationSId = message.conversation.sid;
    const _convToUpdate = conversationList.find(
      (x) => x.sid === conversationSId
    );
    _convToUpdate.lastMessage = message;

    // update state with the new element for the specific item in the list
    updateRenderForKey(_convToUpdate.sid, _convToUpdate);
  });
}
// Chat.js

import { findConversations } from "@barelyhuman/twilio-conversations";
import { initializeTwilio } from "../app-init.js";

async function Chat(conversation) {
  const existingChatMessages = fetchMessages(conversation);

  render(existingChatMessages, {
    onSend: (text) => {},
  });

  const client = await initializeTwilio();
  // if the client isn't connected wait for it to happen
  if (client.connectionState !== "connected") {
    onInit(() => {
      rerenderChatList();
    });
  }

  const conversationResource = findConversations(conversation);

  // this is different based as it's from the helper library
  const {
    conversation,
    onMessageAdded: onMessageAddedToConv,
    onTypingStarted: onTypingStartedInConv,
    onTypingEnded: onTypingEndedInConv,
  } = conversationResource;

  // update render since we have the resource now
  render(existingChatMessages, {
    onTextChange: (text) => {
      conversation.typing();
    },
    onSend: (text) => {
      const _formattedMessage = {
        id: message.sid,
        text: message.body,
        status: "pending",
        user: {
          id: myUserId,
        },
      };
      conversation.sendMessage(text);
    },
  });

  // add the needed listeners
  const { unsubscribe: unsubMessageAdd } = onMessageAddedToConv((message) => {
    const _formattedMessage = {
      id: message.sid,
      text: message.body,
      status: "sent",
      user: {
        id: message.author,
      },
    };

    // update state with the new element for the specific item in the list , as it's status is now changed
    updateRenderForKey(message.id, _formattedMessage);
  });

  const { unsubscribe: unsubTypingStart } = onTypingStartedInConv(
    (participant) => {
      isTyping = true;
    }
  );

  const { unsubscribe: unsubTypingEnd } = onTypingEndedInConv((participant) => {
    isTyping = false;
  });

  // clear the listeners before your componenet unmounts to avoid overloading the event listeners
  onComponentDestroy(() => {
    unsubMessageAdd();
    unsubTypingStart();
    unsubTypingEnd();
  });
}

Everything you see here is doable with the most frameworks and the actual twilio
library, the helper only created a global context to be able to use it the
client even when it's not importable and to be able to chain the client
functions as needed. I repeat you can do everything I've done above with the
twilio library
. No, the above code won't work anywhere since it's pseudocode
and there's doesn't exist a framework that has the above mentioned render
functions the way I used them, these are just examples to be read as examples.