Add buyer-seller chat into a marketplace with React

Note: this is an old tutorial. We recommend that you instead use our React SDK – try it out with our Getting Started guides.

This tutorial will show you how you can implement a buyer-seller chat for online marketplaces, as well as a user to user chat, by creating a React chat, or in other words using TalkJS to add chat into a React app. We will show you how to implement TalkJS into an already existing application to give more context to the implementation. We’ll talk more about this existing application further throughout this tutorial.


First things first

Prerequisites

React version

This application has been written in React version 16.5.2. Make sure to run this application with the aforementioned React version or above to assure it'll function as intended.

NodeJS version

Make sure you're working with NodeJS v8.11.4 or above.

The marketplace

Our marketplace is an application that realizes a simplified use case of a marketplace. In this marketplace, users are able to log in and view product listings:

Before this tutorial's implementation:

After this tutorial's implementation:

Click on an image to open its example application live.
This tutorial is meant to demonstrate how you can add chat functionality to any React application by using TalkJS. You can perform the steps in this tutorial on our example marketplace, but you should also be able to do them inside your own react application right away.

Our example application has been written in TypeScript with Redux and the Container Pattern, it is expected that the reader is familiar with these concepts.

Source code for both marketplace applications can be found on our GitHub repo.

Chat functionalities will be added to the following pages within our marketplace: user profile, product page & inbox page.


Let's get started

Starting the application

Start the application into which you're going to add chat functionalities.

If you're adding chat functionalities to our marketplace application, you can clone the project from its GitHub repository.

Start the application:

npm install
npm start

If our marketplace has started successfully, your console should show you something similar to the following image:

Now that your application is running, navigate to http://localhost:3000/ in your browser.

Install the TalkJS JavaScript SDK

The very first thing that we should do is install the TalkJS JavaScript SDK into our project:

npm install talkjs --save

Active Session

The first thing you should do for TalkJS to properly work within your application is to start a Session for the current logged in user. As long as you have an active Session running, your user will be able to receive desktop notifications. We will, therefore, make sure that the Session is running on each page of our application, even on ones on which our user is not able to read or write messages. In the docs you can read more about a TalkJS Session.

TalkUtils

We’re going to create a utility class that we’ll be able to use in multiple locations within our application.

Create a file in src/shared/utils/ called talk.util.ts, fill it with the following code:

import * as Talk from 'talkjs';
import { User } from '../models/user.model';

// Change this to your actual AppId which can be
// found in the TalkJS dashboard.
export const appId = 'YOUR_APP_ID';

export async function createTalkUser(applicationUser: User) : Promise {
    await Talk.ready;
    
    return new Talk.User({
            id: applicationUser.id,
            name: applicationUser.username,
            photoUrl: applicationUser.profilePictureUrl
         });
}

We’re going to help you grab your AppID in just a moment, we’ll first explain why the utility class contains the TalkUtils#createTalkUser function.

Various functions within the TalkJS JavaScript SDK will require an instance of the TalkJS User class. The TalkUtils#createTalkUser function will return an instance of the TalkJS User class for a given User class of our application, you could look at it as a converting function.

The TalkJS JavaScript SDK is loaded asynchronously. By working with asynchronous functions as well, we're making sure that all TalkJS-related code is non-blocking within our application and that we're following the I/O standards (I/O functions being asynchronous).

AppID

In order for TalkJS to work within your application, your application should have an App ID, which you can find in the TalkJS dashboard.

Create an account — for free while in a test environment — at TalkJS.

Then, go to the TalkJS dashboard and look for your App ID.

Make sure to change the appId within the TalkUtils to the one displayed in your dashboard.

Session initialization

There are two places within our application in which we’ll have to initialize the Session. To make sure that we can easily initialize and grab this Session from anywhere within our application, we’ll create a file that contains both its initialization- and getter function.

Create a file in src/shared/talk/ called talk-session.ts, fill it with the following code:

import * as Talk from 'talkjs';
import { Deferred } from "../utils/deferred.util";
import { User } from '../models/user.model';
import { appId, createTalkUser } from '../utils/talk.util';

const sessionDeferred = new Deferred();

export async function initialize(user: User) {
    await Talk.ready;
    
    sessionDeferred.resolve(new Talk.Session({
        appId: appId,
        me: await createTalkUser(user)
    }));
}

export function get() : Promise {
    return sessionDeferred.promise;
}

The two scenarios in which we’ll have to initialize the Session are when the user just logged in and when the user visits our application and is still logged in from a previous application instance.

Whenever our user is already logged into our application and visits a Component that has to make use of our Session, there’s a possibility that our Session is still being initialized, while the Component is already trying to use the Session. This can cause all sorts of issues so we’re going to fix this by making sure that the application is able to wait for the Session to be active.

What we want to achieve is that we’re able to call code similar to:

await talkSession.get();

Without having to poll for the Session until it is active. This means we need to create a promise that resolves when the session has loaded.

A common way to create a promise is by using a Deferred, which is a little object that lets you return a promise and resolve it later. The example code includes a helper class for this.

We’re creating it upon construction:

const sessionDeferred = new Deferred();

When we initialize the Session, we’ll resolve the sessionDeferred with the Session value:

sessionDeferred.resolve(new Talk.Session(...));

We can then await the current Session like this anywhere in our application:

import * as talkSession from 'src/shared/talk/talk-session';

await talkSession.get();

To make sure that the Session is actually being initialized when we’re calling the talkSession#get function, we’ll have to call its initialization on both the application load and after a successful login.

Successful login

Navigate to src/pages/Login/LoginPageContainer.tsx and add the following highlighted code:

import * as talkSession from '../../shared/talk/talk-session';

async performLoginAttempt(username: string) {
    ...
    if (loginAttemptUser) {
        ...
        talkSession.initialize(user);
    } else {
      ...
    }
  }
Application load

We need to consider the scenario in which a user is already logged in when they load the application. As of now, our application only initializes the Session whenever our user logs in. This means that with the aforementioned scenario, our user is logged in while there is no active Session running. As you're aware, it is important that there's always an active Session running when our user is logged in. We should, therefore, make sure that a Session will be initialized for the already logged in user in this scenario. We’re able to do this by making sure that if the application starts, the Session will be initialized as well.

Navigate to src/index.tsx and add the following highlighted code:

import * as talkSession from './shared/talk/talk-session';

async function setCurrentLoggedInUser() {
  ...
  
  if (user) {
    ...
    talkSession.initialize(user);
  }
  ...
}

You have now successfully made sure there’s always an active Session running for your logged in user.

Your src/shared/utils/talk.util.ts, src/shared/talk/talk-session.ts, src/index.tsx, and src/pages/Login/LoginPageContainer.tsx should look like:talk.util.ts:

import * as Talk from 'talkjs';
import { User } from '../models/user.model';

// Change this to your actual AppId which can be
// found in the TalkJS dashboard.
export const appId = 'YOUR_APP_ID';

export async function createTalkUser(applicationUser: User) : Promise {
    await Talk.ready;
    
    return new Talk.User({
            id: applicationUser.id,
            name: applicationUser.username,
            photoUrl: applicationUser.profilePictureUrl
         });
}

talk-session.ts:

import * as Talk from 'talkjs';
import { Deferred } from "../utils/deferred.util";
import { User } from '../models/user.model';
import { appId, createTalkUser } from '../utils/talk.util';

const sessionDeferred = new Deferred();

export async function initialize(user: User) {
    await Talk.ready;
    
    sessionDeferred.resolve(new Talk.Session({
        appId: appId,
        me: await createTalkUser(user)
    }));
}

export function get() : Promise {
    return sessionDeferred.promise;
}

index.ts:

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import './index.css';
import registerServiceWorker from './registerServiceWorker';
import { Provider } from 'react-redux';
import store from './shared/store/store';
import AppContainer from './components/app/AppContainer';
import { getCurrentUser } from './core/modules/authentication.module';
import { initializeProductMocks } from './core/modules/product.module';
import { login, toggleLoading as toggleIsLoadingAuthentication } from './shared/store/actions/authentication.actions';
import * as talkSession from './shared/talk/talk-session';

setCurrentLoggedInUser();

async function setCurrentLoggedInUser() {
  const user = await getCurrentUser();
  
  if (user) {
    store.dispatch(login(user));
    talkSession.initialize(user);
  }
  
  store.dispatch(toggleIsLoadingAuthentication(false));
}

ReactDOM.render(
  
    
  ,
  document.getElementById('root') as HTMLElement
);

registerServiceWorker();
initializeProductMocks();

LoginPageContainer.tsx:

import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';
import { toast } from 'react-toastify';
import { User } from 'src/shared/models/user.model';
import Login from 'src/pages/Login/LoginPage';
import { login } from 'src/core/modules/authentication.module';
import { login as loginStore } from 'src/shared/store/actions/authentication.actions';
import * as talkSession from '../../shared/talk/talk-session';

interface DefaultProps { 
  loginStore: (user: User) => void
}

class LoginContainer extends React.Component<DefaultProps, object> {

  async performLoginAttempt(username: string) {
    const loginAttemptUser = await login(username);
    
    if (loginAttemptUser) {
        toast.success('Successful login', {
          position: toast.POSITION.BOTTOM_RIGHT
        });
        
        const user = {...loginAttemptUser, products: [], addProduct: () => Function}
        this.props.loginStore(user);
        talkSession.initialize(user);
    } else {
      toast.error('Incorrect credentials', {
        position: toast.POSITION.BOTTOM_RIGHT
      });
    }
  }
  
  handleLoginAttempt = (username: string) => {
    this.performLoginAttempt(username);
  } 
    
  public render() {
    return (<LoginPage onLoginAttempt={this.handleLoginAttempt} />);
  }
}

const mapStateToProps = (state: any, props: any) => {
  return { };
}

const mapDispatchToProps = (dispatch: Dispatch, props: any) => {
  return bindActionCreators({ loginStore: loginStore }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer);

Chat Popup

In this chapter, we’re going to allow buyers to communicate directly with the vendor of a product by using a TalkJS Chat Popup.

This is what a Chat Popup looks like:

Preloading

Navigate to the product page of a watch: http://localhost:3000/products/3.

The first thing we should do is make sure that the chat between our user and the product’s vendor is ready before our user actually tries to open this chat, to enhance user experience.

We’re going to do this by preloading the chat whenever the product page is being loaded.

TalkUtils

Add the following function to src/shared/utils/talk.util.ts:

export async function getOrCreateConversation(session: Talk.Session, currentUser: User, otherUser: User) {   
    const currentTalkUser = await createTalkUser(currentUser);
    const otherTalkUser = await createTalkUser(otherUser);
    
    const conversationBuilder = session.getOrCreateConversation(Talk.oneOnOneId(currentTalkUser, otherTalkUser));
    conversationBuilder.setParticipant(currentTalkUser);
    conversationBuilder.setParticipant(otherTalkUser);
    
    return conversationBuilder;
}

This function creates a ConversationBuilder for a given Session and two users whom the conversation should be created between. We're going to need this function's functionality a few more times within our application, hence creating this function.

ProductPage

Navigate to src/pages/Product/ProductPage.tsx and add the following highlighted code:

import * as Talk from 'talkjs';

interface DefaultProps { 
  ...
  talkSession: Talk.Session | null,
  talkConversation: Talk.ConversationBuilder | null
}

class ProductPage extends React.Component<DefaultProps, object> {

  private chatPopup: Talk.Popup;
  
  preloadChatPopup() {
    if (!this.props.talkSession || !this.props.talkConversation) {
      return;
    }
    
    this.chatPopup = this.props.talkSession.createPopup(this.props.talkConversation);
    this.chatPopup.mount({ show: false });
  }
  
  componentDidUpdate() {
    this.preloadChatPopup();
  }
  
  ...
}

What we're doing here is:

- Making sure that this component expects a TalkJS Session and Conversation from its parent container, ProductPageContainer, by adding them to the DefaultProps (we're using the Container Pattern, which ‘forces’ us to receive both the Session and Conversation objects from the parent container, named ProductPageContainer).

- Adding a private property of the type Talk.Popup. This is the Popup that we’re preloading and the one that we want to display to our user (we’ll do this in the next paragraph).

- Adding the preloading function and calling it from the componentDidUpdate lifecycle hook (we’re using this particular lifecycle hook as there's a possibility of the Session or Conversation being undefined when the component mounts because these are still being initialized by the parent container. The moment the parent container initialized them, they will be passed as props to this component, hence this component updating).

All our preloading function does is call the Session#createPopup function with the given Conversation and mount it. As we’d like to preload our Popup before displaying it to our user, we’re using the show: false property within the ChatPopup#mount function.

ProductPageContainer

We still need to pass both the Session and the Conversation to the ProductPage.

Navigate to src/pages/Product/ProductPageContainer.tsx and add the following highlighted code:

import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';

import * as Talk from 'talkjs';
import * as talkSession from '../../shared/talk/talk-session';
import { getOrCreateConversation } from 'src/shared/utils/talk.util';

interface DefaultProps { currentUser: User }
interface DefaultState { 
  product: Product | null,
  talkSession: Talk.Session | null,
  talkConversation: Talk.ConversationBuilder | null
}

class ProductPageContainer extends React.Component<DefaultProps, DefaultState, object> {
  constructor(...) {
    ...
    
    this.state = {
      product: null,
      talkSession: null,
      talkConversation: null
    }
  }
  
  async componentDidMount() {
    ...
    
    const session = await talkSession.get();
    const conversation = await getOrCreateConversation(session, this.props.currentUser, product.vendor);
    
    //It is important that this #setState is being called separately from the one that's being called to set the product above.
    this.setState({
      talkSession: session,
      talkConversation: conversation
    });
  }
  
  public render() {
    ...
    return (<ProductPage
              onVendorClick={this.handleVendorClick}
              product={this.state.product}
            />
          );          
  }
}

const mapStateToProps = (state: any, props: any) => {
  return { currentUser: state.authentication.currentUser };
}

const mapDispatchToProps = (dispatch: Dispatch, props: any) => {
  return bindActionCreators({ }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(ProductPageContainer);

We're doing a couple of things here:

- Mapping our current logged in user from our Redux State to the props of this component. We will not go more in-depth about this part as that is outside the scope of this tutorial.

- Grabbing the active Session, creating a Conversation and adding both of these to this component's State.

- Passing both the active Session and Conversation to the ProductPage component.

Displaying

Add the following function to the src/pages/Product/ProductPage.tsx:

handleChatButtonClick = () => {
    if (!this.chatPopup) return;
    
    this.chatPopup.show();
  }

This function calls the ChatPopup#show function, which displays our preloaded Popup. We're going to call this method in just a moment.

ProductPage

Add the following code to the ProductPage.tsx to call the ProductPage#handleChatButtonClick on button click:

...
class ProductPage extends React.Component<DefaultProps, object> {
  ...

  handleChatButtonClick = () => {
    ...
  }
    
  public render() {
    const product = this.props.product;
    
    return ( 
      <div>
        <div id="product-information-container">
          <div>
            <ProductCard 
              product={product}
            />
          </div>
        </div>
        <hr />
        <div id="vendor-information-container">
          <div>
            <div>Vendor</div>
          </div>
          <div>
            <button id="chat-btn" type="button" onClick={this.handleVendorClick}> 
               Chat with {product.vendor.username} 
            </button>
          </div>
        </div>
      </div>
    </div> 
    ); 
  }  
} 

export default ProductPage;

Styling

To conform with the styling choices of our application, we’re going to allow each user to have his/her own chat button color on the `ProductPage`. We’re going to do this by using a new model called “ChatPreferences”. This model will be created because we’ll add another property to it lateron in this tutorial as well.

This styling part of the tutorial is not essential for TalkJS to be implemented within your own application.

ChatPreferences model

Create the following file src/shared/models/chat-preferences.model.ts and fill it with the following code:

export class ChatPreferences {
    chatButtonColorHex: string;
    chatWelcomeMessage: string;
    
    constructor(chatButtonColorHex: string, chatWelcomeMessage: string) {
        this.chatButtonColorHex = chatButtonColorHex;
        this.chatWelcomeMessage = chatWelcomeMessage;
    }
}
User model

Add the ChatPreferences model to the User model by adding the following code to user.model.ts:

import { Product } from 'src/shared/models/product.model';
import { ChatPreferences } from './chat-preferences.model';

export class User {
    id: number;
    username: string;
    products: Product[];
    profilePictureUrl: string;
    chatPreferences: ChatPreferences;
    
    constructor(id: number, username: string, profilePictureUrl: string, chatPreferences: ChatPreferences) {
        this.id = id;
        this.username = username;
        this.profilePictureUrl = profilePictureUrl;
        this.chatPreferences = chatPreferences;
        this.products = [];
    }
    
    addProduct(product: Product) {
        this.products.push(product);
        const unReferencedUser = new User(this.id, this.username, this.profilePictureUrl, this.chatPreferences);
        
        product.setVendor(unReferencedUser);
    }
}
Updating mocks

Navigate to src/core/mocks/users.mock.ts and add a ChatPreference object for each mock object:

...
export const USERS: User[] = [
    new User(4, 'John', process.env.PUBLIC_URL + '/assets/images/users/john.jpg', 
new ChatPreferences("#1D1F1E")),
    new User(2, 'Clarke', process.env.PUBLIC_URL + '/assets/images/users/clarke.jpeg', 
new ChatPreferences("#FF69B4")),
    new User(3, 'Ethan', process.env.PUBLIC_URL + '/assets/images/users/ethan.jpeg', 
new ChatPreferences("#CD905A")),
    new User(8, 'Max', process.env.PUBLIC_URL + '/assets/images/users/max.jpeg', 
new ChatPreferences("#00AAB4")),
    new User(5, 'Liam', process.env.PUBLIC_URL + '/assets/images/users/liam.jpeg', 
new ChatPreferences("#9290A1")),
    new User(6, 'Lindsey', process.env.PUBLIC_URL + '/assets/images/users/lindsey.jpeg', 
new ChatPreferences("#37D1DB")),
    new User(7, 'Mary', process.env.PUBLIC_URL + '/assets/images/users/mary.jpeg', 
new ChatPreferences("#D3C445")),
    new User(1, 'Jason', process.env.PUBLIC_URL + '/assets/images/users/jason.jpeg', 
new ChatPreferences("#FBBEBB")),
    new User(9, 'Paul', process.env.PUBLIC_URL + '/assets/images/users/paul.jpeg', 
new ChatPreferences("#08DFB4")),
    new User(10, 'Tom', process.env.PUBLIC_URL + '/assets/images/users/tom.jpeg', 
new ChatPreferences("#FF69B4")),
    new User(11, "Vanessa", process.env.PUBLIC_URL + '/assets/images/users/vanessa.jpeg', 
new ChatPreferences("#FF69B4"))
];
ProductPage

We can now add the styling to our chat button, add the following line of code to the src/pages/Product/ProductPage.tsx:

...
class ProductPage extends React.Component<DefaultProps, object> {
  ...
    
  public render() {
    const product = this.props.product;
    return (
        <div>
            <hr />
            <div id="vendor-information-container">
                <div>Vendor</div>
                <div>
                    <button id="chat-btn" type="button"> Chat with {product.vendor.username} </button>
                </div> 
            </div>
        </div>
        ); 
    } 
} 
export default ProductPage;

You have now successfully added the Chat Popup to your application. The src/shared/utils/talk.util.ts , src/pages/Product/ProductPage.tsx, and src/pages/Product/ProductPageContainer.tsx should look like: talk.util.ts:

import * as Talk from 'talkjs';
import { User } from '../models/user.model';

// Change this to your actual AppId which can be
// found in the TalkJS dashboard.
export const appId = 'YOUR_APP_ID';

export async function createTalkUser(applicationUser: User) : Promise>Talk.User> {
    await Talk.ready;
    
    return new Talk.User({
            id: applicationUser.id,
            name: applicationUser.username,
            photoUrl: applicationUser.profilePictureUrl
         });
}

export async function getOrCreateConversation(session: Talk.Session, currentUser: User, otherUser: User) {   
    const currentTalkUser = await createTalkUser(currentUser);
    const otherTalkUser = await createTalkUser(otherUser);
    
    const conversationBuilder = session.getOrCreateConversation(Talk.oneOnOneId(currentTalkUser, otherTalkUser));
    conversationBuilder.setParticipant(currentTalkUser);
    conversationBuilder.setParticipant(otherTalkUser);
    
    return conversationBuilder;
}

ProductPageContainer.tsx:

import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';
import { User } from 'src/shared/models/user.model';
import routerHistory from '../../shared/router-history/router-history';
import ProductPage from './ProductPage';
import Loading from '../Loading/LoadingPage';
import { getIdFromURL } from 'src/shared/utils/url.util';
import { getProduct } from 'src/core/modules/product.module';
import { Product } from 'src/shared/models/product.model';
import * as Talk from 'talkjs';
import * as talkSession from '../../shared/talk/talk-session';
import { getOrCreateConversation } from 'src/shared/utils/talk.util';

interface DefaultProps { currentUser: User }

interface DefaultState { 
  product: Product | null,
  talkSession: Talk.Session | null,
  talkConversation: Talk.ConversationBuilder | null
}

class ProductPageContainer extends React.Component<DefaultProps, DefaultState, object> {

  constructor(props: any) {
    super(props);
    
    this.state = {
      product: null,
      talkSession: null,
      talkConversation: null
    }
  }
  
  handleIncorrectIdParameter() {
    routerHistory.replace('/404');
  }
  
  handleVendorClick = (vendor: User) => {
    routerHistory.push('/users/' + vendor.id);
  }
  
  async componentDidMount() {
    /* Product Page Product Loading */
    const productId = getIdFromURL('/products/');
    
    if (!productId) {
      this.handleIncorrectIdParameter();
      return;
    }
    const product = await getProduct(productId);
    this.setState({
        product: product
    });

    const session = await talkSession.get();
    const conversation = await getOrCreateConversation(session, this.props.currentUser, product.vendor);
    
    this.setState({
      talkSession: session,
      talkConversation: conversation
    });
  }
  
  public render() {
    if (!this.state.product) {
      return <Loading/>;;
    }
    
    return (<ProductPage 
              onVendorClick={this.handleVendorClick}
              product={this.state.product}
            />);
  }
}

const mapStateToProps = (state: any, props: any) => {
  return { currentUser: state.authentication.currentUser };
}

const mapDispatchToProps = (dispatch: Dispatch, props: any) => {
  return bindActionCreators({ }, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(ProductPageContainer);

ProductPage.tsx:

import * as React from 'react';
import './styles.css';
import { Product } from 'src/shared/models/product.model';
import ProductCard, { Size as ProductCardSize } from '../../components/ProductCard/ProductCard';
import UserCard, { Color, Size as UserCardSize } from '../../components/UserCard/UserCard';
import { User } from 'src/shared/models/user.model';
import * as Talk from 'talkjs';

interface DefaultProps { 
  product: Product,
  onVendorClick: (vendor: User) => void,
  talkSession: Talk.Session | null,
  talkConversation: Talk.ConversationBuilder | null
}

class ProductPage extends React.Component<DefaultProps, object> {
  private chatPopup: Talk.Popup;
    
  preloadChatPopup() {
    if (!this.props.talkSession || !this.props.talkConversation) {
      return;
    }
    
    this.chatPopup = this.props.talkSession.createPopup(this.props.talkConversation);
    this.chatPopup.mount({ show: false });
  }
  
  componentDidUpdate() {
    this.preloadChatPopup();
  }
  
  handleVendorClick = (vendor: User) => {
    this.props.onVendorClick(vendor);
  }

  handleChatButtonClick = () => {
    if (!this.chatPopup) return;
    
    this.chatPopup.show();
  }
    
  public render() {
    const product = this.props.product;
    
    return (
        <div>
            <div id="product-information-container">
                <hr />
                <div id="vendor-information-container">
                    <div>Vendor</div>
                    <div>
                        <button id="chat-btn" type="button"> Chat with {product.vendor.username} </button>
                    </div>
                 </div>
             </div>
         </div>
         ); 
    } 
} 

export default ProductPage;


Chatbox

In this chapter, we're going to make sure that a user is able to send a vendor a message from his/her profile page, by using a TalkJS Chatbox.

This is what a Chatbox looks like:

Chatbox component

We’re going to create a Chatbox component so that we're able to render a Chatbox within any of our components, as we'd like to render it in the UserProfile component.

Create a file named Chatbox.tsx, located in src/components/Chatbox/ and add the following code:

import * as React from 'react';
import * as Talk from 'talkjs';
import { ChatboxOptions } from 'talkjs/types/talkjs/published/UIOptions';

interface DefaultProps extends ChatboxOptions {
    loadingMessage?: string,
    height?: number,
    minWidth?: number,
    
    session: Talk.Session | null,
    conversation: Talk.ConversationBuilder | Talk.Conversation | null
}
class Chatbox extends React.Component<DefaultProps, object> {
  private container: any;
  private chatbox: Talk.Chatbox;
  
  async initialize() {
    if (!this.props.session || !this.props.conversation) {
        return;
    }
    
    const chatboxOptions = {...this.props};
    delete chatboxOptions.loadingMessage;
    delete chatboxOptions.height;
    delete chatboxOptions.minWidth;
    delete chatboxOptions.session;
    delete chatboxOptions.conversation;
    
    this.chatbox = this.props.session.createChatbox(this.props.conversation, chatboxOptions);
    this.chatbox.mount(this.container);
  }
  
  componentDidUpdate() {
    this.initialize();
  }
  
  componentWillUnmount() {
    if (this.chatbox) {
      this.chatbox.destroy();
    }
  }
    
  public render() {
    return (

this.container = container}>; {this.props.loadingMessage}

); } } export default Chatbox;

What we're doing here is:

- Making sure that this component expects a Talk Session and Conversation in its `DefaultProps`, as well as some other optional properties.

- Adding a private property called container. This property will have a reference to the container div-element into which we’ll mount the TalkJS Chatbox.

- Initializing and mounting the TalkJS Chatbox by calling the Session#createChatbox function with the given Conversation.

- Destroying the TalkJS Chatbox and its event listeners when the Chatbox component is going to be unmounted.

UserProfilePage

Navigate to src/pages/UserProfile/UserProfilePage.tsx and add the following highlighted code:

...
import * as Talk from 'talkjs';
import Chatbox from '../../components/Chatbox/Chatbox'; 

interface DefaultProps { 
  ...
  talkSession: Talk.Session | null,
  talkConversation: Talk.ConversationBuilder | null
}

class UserProfilePage extends React.Component<DefaultProps, object> {
    
  public render() {
    ...
    
    });
    return (
        <div id="UserProfile">
            <div id="personal-information-container">
                <hr />
                <div id="chat-container">
                    <div>Chat</div>
                    <div id="talkjs-chatbox"></div>
                    <hr />

                    <div id="owned-products-container">
                        <div>Products</div>
                        <div id="products-row">{productCards}</div>
                    </div>
                 </div>
             </div>
        </div> 
       );
    }  
} 
export default UserProfilePage;

What we're doing here is:

- Making sure that this component expects a Talk Session and Conversation in its DefaultProps, as we’ll have to pass these to the Chatbox component that we’re rendering.

- Adding some HTML such as a divider and container div-element called chat-container. (refer to the final source of the marketplace application to see the accessory CSS). This is simply being done to conform to the application's general layout.

- Rendering the Chatbox component.

UserProfilePageContainer

Navigate to src/pages/UserProfile/UserProfilePageContainer.tsx and add the following highlighted code:

...
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';

import * as Talk from 'talkjs';
import * as talkSession from '../../shared/talk/talk-session';
import { getOrCreateConversation } from 'src/shared/utils/talk.util';

interface DefaultProps { currentUser: User }
interface DefaultState { 
  ...
  talkSession: Talk.Session | null,
  talkConversation: Talk.ConversationBuilder | null
}

class UserProfilePageContainer extends React.Component<DefaultProps, DefaultState, object> {
  constructor(props: any) {
    super(props);
    
    this.state = {
      ...
      talkSession: null,
      talkConversation: null
    };
  }
  
  async componentDidMount() {
    ...
    
    const session = await talkSession.get();
    const conversation = await getOrCreateConversation(session, this.props.currentUser, profileUser);
    
    //It is important that this #setState is being called separately from the one that's being called to set the profileUser above.
    this.setState({
      talkSession: session,
      talkConversation: conversation
    });
  }
    
  public render() {
    ...
    
    return ();
  }
}

const mapStateToProps = (state: any, props: any) => {
  return { currentUser: state.authentication.currentUser };
}
const mapDispatchToProps = (dispatch: Dispatch, props: any) => {
  return bindActionCreators({ }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(UserProfilePageContainer);

We're doing a couple of things here:

- Mapping our current logged in user from our Redux State to the props of this component. We’ve already done this once in our ProductPageContainer.

- Grabbing the active Session, creating a Conversation and adding both of these to this component's State.

- Passing both the active Session and Conversation to the UserProfilePage component.

You have now successfully added the Chatbox to your application.

The src/components/Chatbox/Chatbox.tsx, src/pages/UserProfile/UserProfilePage.tsx, and src/pages/UserProfile/UserProfilePageContainer.tsx should look like:Chatbox.tsx:

import * as React from 'react';
import * as Talk from 'talkjs';
import { ChatboxOptions } from 'talkjs/types/talkjs/published/UIOptions';

interface DefaultProps extends ChatboxOptions {
    loadingMessage?: string,
    height?: number,
    minWidth?: number,
    
    session: Talk.Session | null,
    conversation: Talk.ConversationBuilder | Talk.Conversation | null
}
class Chatbox extends React.Component<DefaultProps, object> {
  private container: any;
  private chatbox: Talk.Chatbox;
  
  async initialize() {
    if (!this.props.session || !this.props.conversation) {
        return;
    }
    
    const chatboxOptions = {...this.props};
    delete chatboxOptions.loadingMessage;
    delete chatboxOptions.height;
    delete chatboxOptions.minWidth;
    delete chatboxOptions.session;
    delete chatboxOptions.conversation;
    
    this.chatbox = this.props.session.createChatbox(this.props.conversation, chatboxOptions);
    this.chatbox.mount(this.container);
  }
  
  componentDidUpdate() {
    this.initialize();
  }
  
  componentWillUnmount() {
    if (this.chatbox) {
      this.chatbox.destroy();
    }
  }
    
  public render() {
    return (
        <div id="Chatbox">this.container = container}> {this.props.loadingMessage}</div>
        ); 
    } 
} 

export default Chatbox;

 UserProfilePage.tsx:

import * as React from 'react';
import './styles.css';
import { User } from 'src/shared/models/user.model';
import UserCard, { Color, Size as UserCardSize } from '../../components/UserCard/UserCard';
import ProductCard, { Size as ProductCardSize } from '../../components/ProductCard/ProductCard';
import { Product } from 'src/shared/models/product.model';
import * as Talk from 'talkjs';
import Chatbox from '../../components/Chatbox/Chatbox'; 

interface DefaultProps { 
  profileUser: User,
  handleProductclick: (product: Product) => void,
  talkSession: Talk.Session | null,
  talkConversation: Talk.ConversationBuilder | null
}

class UserProfilePage extends React.Component<DefaultProps, object> {

  handleProductClick = (product: Product) => {
    this.props.handleProductclick(product);
  }
    
  public render() {
    const user = this.props.profileUser;
    const productCards = user.products.map(product => {
      return <div className="col-sm-3" key={product.id}>
          <ProductCard
            product={product} 
            onClick={this.handleProductClick}
          />
      </div>
    }); 
    
    return (
        <div>
            <div id="personal-information-container">
                <hr />
                <div id="chat-container">
                    <div>Chat</div></div>
                    <div id="talkjs-chatbox">
                </div>
                <hr />
                <div id="owned-products-container">
                    <div>Products</div>         
                    <div id="products-row">{productCards}</div>
                </div>
            </div>
        </div>
        ); 
    } 
} 

export default UserProfilePage;

UserProfilePageContainer.tsx:

import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';
import * as Talk from 'talkjs';
import * as talkSession from '../../shared/talk/talk-session';
import { getOrCreateConversation } from 'src/shared/utils/talk.util';
import routerHistory from '../../shared/router-history/router-history';
import UserProfilePage from './UserProfilePage';
import LoadingPage from '../Loading/LoadingPage';
import { Product } from 'src/shared/models/product.model';
import { getIdFromURL } from 'src/shared/utils/url.util';
import { getUser } from 'src/core/modules/user.module';
import { User } from 'src/shared/models/user.model';

interface DefaultProps { currentUser: User }

interface DefaultState { 
  profileUser: User | null,
  talkSession: Talk.Session | null,
  talkConversation: Talk.ConversationBuilder | null
}

class UserProfilePageContainer extends React.Component<DefaultProps, DefaultState, object> {

  constructor(props: any) {
    super(props);
    
    this.state = {
      profileUser: null,
      talkSession: null,
      talkConversation: null
    };
  }
  
  handleProductClick = (product: Product) => {
    routerHistory.push('/products/' + product.id);
  }
  
  handleIncorrectIdParameter() {
    routerHistory.replace('/404');
  }
  
  async componentDidMount() {
    /* Profile Page User Loading */
    const userId = getIdFromURL('/users/');
    if (!userId) {
      this.handleIncorrectIdParameter();
      return;
    }
    
    const profileUser = await getUser(userId);
    this.setState({
      profileUser: profileUser
    });

    const session = await talkSession.get();
    const conversation = await getOrCreateConversation(session, this.props.currentUser, profileUser);
    
    this.setState({
      talkSession: session,
      talkConversation: conversation
    });
  }
    
  public render() {
    if (!this.state.profileUser) {
      return 
    }
    
    return ();
  }
}
const mapStateToProps = (state: any, props: any) => {
  return { currentUser: state.authentication.currentUser };
}
const mapDispatchToProps = (dispatch: Dispatch, props: any) => {
  return bindActionCreators({ }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(UserProfilePageContainer);

Inbox

Finally, let’s make sure that our user is able to view and send messages in previously opened conversations by using the TalkJS Inbox, which is a component that displays all the conversations that the user is part of. The inbox will be the messaging center of our application.

This is what an Inbox looks like:

Since the TalkJS inbox has been designed to act as a messaging center of an application, TalkJS users typically implement the TalkJS Inbox within a separate page. So that's what we’ll do as well.

Inbox component

Just like we did with the Chatbox, we're going to create an Inbox component.

Create a file named Inbox.tsx, located in src/components/Inbox/ and add the following code:

import * as React from 'react';
import * as Talk from 'talkjs';
import { InboxOptions } from 'talkjs/types/talkjs/published/UIOptions';

interface DefaultProps extends InboxOptions {
    loadingMessage?: string,
    height?: number,
    width?: number,
    
    session: Talk.Session | null
}
class Inbox extends React.Component<DefaultProps, object> {
  private container: any;
  private inbox: Talk.Inbox;
  
  async initialize() {
    if (!this.props.session) {
        return;
    }
    
    const inboxOptions = {...this.props};
    delete inboxOptions.loadingMessage;
    delete inboxOptions.height;
    delete inboxOptions.width;
    delete inboxOptions.session;
    
    this.inbox = this.props.session.createInbox(inboxOptions);
    this.inbox.mount(this.container);
  }
  
  componentDidUpdate() {
    this.initialize();
  }
  
  componentWillUnmount() {
    if (this.inbox) {
      this.inbox.destroy();
    }
  }
    
  public render() {
    return (

this.container = container}> {this.props.loadingMessage}

); } } export default Inbox;

InboxPage

We’re going to create a new page, the InboxPage. This page will render the Inbox component that we just made.

Navigate to src/pages/Inbox/, create InboxPage.tsx and fill it with the following code:

import * as React from 'react';
import * as Talk from 'talkjs';
import Inbox from '../../components/Inbox/Inbox';

interface DefaultProps {
  talkSession: Talk.Session | null
}

class InboxPage extends React.Component<DefaultProps, object> {
    
  public render() {
    return (

); } } export default InboxPage;

InboxPageContainer

Our InboxPage is expecting a Talk Session in its properties. Because our application is using the Container Pattern, we’ll have to conform to this pattern by creating an InboxPageContainer.

Create a file named InboxPageContainer.tsx in the same directory as the InboxPage. Fill it with the following code:

import * as React from 'react';
import * as Talk from 'talkjs';
import InboxPage from './InboxPage';
import * as talkSession from '../../shared/talk/talk-session';

interface DefaultProps { }

interface DefaultState { 
  talkSession: Talk.Session | null
}
class InboxPageContainer extends React.Component<DefaultProps, DefaultState, object> {
  
  constructor(props: any) {
    super(props);
    
    this.state = {
        talkSession: null
    };
  }
  
  async componentDidMount() {
    const session = await talkSession.get();
    
    this.setState({
      talkSession: session
    });
  }
    
  public render() {
    return (
        
    );
  }
}
export default InboxPageContainer;
Styling

We’ll have to add some styling to the InboxPage to make it visually more appealing.

Create a file named styles.css in the src/pages/Inbox/ directory. Fill it with the following code:

.InboxPage {
    margin-top: 40px !important;
}

Import the styles.css in the InboxPage like this:

...
import './styles.css';

...
class InboxPage extends React.Component<DefaultProps, object> {
    
  public render() {
    ...
  }
}
export default InboxPage;
Routing

To ensure that our user is able to visit the InboxPage, we’ll have to add some routing.

Navigate to src/app/App.tsx and add the following code:

...

const InboxPage = Loadable({ loader: () => import('../../pages/Inbox/InboxPageContainer'), loading: Loading });

interface DefaultProps { 
  ...
}
class App extends React.Component<DefaultProps, object> {

  public render() {
    ...
    
    return (
        <div>
            <header></header>
            <div id="body-content"></div>
        </div>
        ); 
    } 
} 

export default App;

You have now successfully added the Inbox to your application.

The src/components/Inbox/Inbox.tsx, src/pages/Inbox/InboxPage.tsx, src/pages/Inbox/InboxPageContainer.tsx, and src/components/app/App.tsx should look like:Inbox.tsx:

import * as React from 'react';
import * as Talk from 'talkjs';
import { InboxOptions } from 'talkjs/types/talkjs/published/UIOptions';

interface DefaultProps extends InboxOptions {
    loadingMessage?: string,
    height?: number,
    width?: number,
    
    session: Talk.Session | null
}
class Inbox extends React.Component<DefaultProps, object> {
  private container: any;
  private inbox: Talk.Inbox;
  
  async initialize() {
    if (!this.props.session) {
        return;
    }
    
    const inboxOptions = {...this.props};
    delete inboxOptions.loadingMessage;
    delete inboxOptions.height;
    delete inboxOptions.width;
    delete inboxOptions.session;
    
    this.inbox = this.props.session.createInbox(inboxOptions);
    this.inbox.mount(this.container);
  }
  
  componentDidUpdate() {
    this.initialize();
  }
  
  componentWillUnmount() {
    if (this.inbox) {
      this.inbox.destroy();
    }
  }
    
  public render() {
    return (
        <div id="Inbox" ref={container => this.container = container}>
            {this.props.loadingMessage}
        </div>
        ); 
    }
}

export default Inbox;

InboxPage.tsx:

import * as React from 'react';
import './styles.css';
import * as Talk from 'talkjs';
import Inbox from '../../components/Inbox/Inbox';

interface DefaultProps {
  talkSession: Talk.Session | null
}
class InboxPage extends React.Component<DefaultProps, object> {
    
  public render() {
    return (
        <div>
            <Inbox 
                loadingMessage='Loading chats...'
                session={this.props.talkSession}
            />
        </div>
        );
    } 
}

export default InboxPage;

InboxPageContainer.tsx:

import * as React from 'react';
import * as Talk from 'talkjs';
import InboxPage from './InboxPage';
import * as talkSession from '../../shared/talk/talk-session';

interface DefaultProps { }
interface DefaultState { 
  talkSession: Talk.Session | null
}

class InboxPageContainer extends React.Component<DefaultProps, DefaultState, object> {
  
  constructor(props: any) {
    super(props);

    this.state = {
        talkSession: null
    };
  }
  
  async componentDidMount() {
    const session = await talkSession.get();
    
    this.setState({
      talkSession: session
    });
  }
    
  public render() {
    return (
        <InboxPage 
            talkSession={this.state.talkSession} 
        />
        );
    }
}
export default InboxPageContainer;

App.tsx:

import * as React from 'react';
import { Router, Route, Switch } from 'react-router-dom';
import * as Loadable from 'react-loadable';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import routerHistory from '../../shared/router-history/router-history';
import Loading from 'src/pages/Loading/LoadingPage';
import NavbarContainer from 'src/components/Navbar/NavbarContainer';
import GuestRoute from '../GuestRoute/GuestRoute';
import ProtectedRoute from '../ProtectedRoute/ProtectedRoute';

const Home = Loadable({ loader: () => import('../../pages/Home/HomePageContainer'), loading: Loading });
const Login = Loadable({ loader: () => import('../../pages/Login/LoginPageContainer'), loading: Loading });
const ProductList = Loadable({ loader: () => import('../../pages/ProductList/ProductListPageContainer'), loading: Loading });
const ProductPage = Loadable({ loader: () => import('../../pages/Product/ProductPageContainer'), loading: Loading });
const UserList = Loadable({ loader: () => import('../../pages/UserList/UserListPageContainer'), loading: Loading });
const UserProfile = Loadable({ loader: () => import('../../pages/UserProfile/UserProfilePageContainer'), loading: Loading });
const InboxPage = Loadable({ loader: () => import('../../pages/Inbox/InboxPageContainer'), loading: Loading });
const Error404 = Loadable({ loader: () => import('../../pages/Error404/Error404Page'), loading: Loading });

interface DefaultProps { 
  hasLoggedInUser: boolean,
  isLoadingAuthentication: boolean
}
class App extends React.Component<DefaultProps, object> {

  public render() {
    const defaultProtectedRouteProps = {
      isAuthenticated: this.props.hasLoggedInUser,
      isLoadingAuthentication: this.props.isLoadingAuthentication,
      redirectionPath: '/login'
    }
    
    return (
        <div>
            <header></header>
            <div id="body-content"></div>
        </div>
        ); 
    }
} 

export default App;

Authentication

Before publishing your application, you need to ensure that authentication is enabled to prevent malicious users from hijacking accounts. This requires adding a few lines of code to your backend, which is outside the scope of this tutorial. Read more about authentication.


Finishing touches

Congratulations, you have implemented TalkJS into an existing application! However, if you want, you can add some finishing touches to make the user experience better.

Enabling file & location sharing

In this chapter, we’re going to allow our users to share both files and their location in any chat.

TalkJS Dashboard

The first thing we have to do is create a custom configuration in the TalkJS dashboard.Log into the TalkJS dashboard and navigate to the configurations section.

Create a new configuration by clicking on the plus button. You can give the configuration any name, we're going for demo_default.

We’re able to enable both file and location sharing by enabling their checkboxes.

Enable the following checkboxes:

TalkUtils

To enable the configuration that we just created for all our users, all we have to do is add this configuration to the TalkUtils#createTalkUser function.

Add the following highlighted code to TalkUtils#createTalkUser:

export async function createTalkUser(applicationUser: User) : Promise {
  await Talk.ready;
  
  return new Talk.User({
          id: applicationUser.id,
          name: applicationUser.username,
          photoUrl: applicationUser.profilePictureUrl,
          configuration: 'demo_default'
       });
}

Make sure to use the configuration name that you chose yourself in the TalkJS dashboard.You have now successfully enabled file and location sharing in your application.

Enabling email and SMS-notifications

Enabling email and SMS-notifications is really easy within TalkJS. All you have to do is pass TalkJS the users' phone number and/or email address and TalkJS will handle the rest!

Add the following highlighted code to TalkUtils#createTalkUser:

export async function createTalkUser(applicationUser: User) : Promise {
  await Talk.ready;
  
  return new Talk.User({
          id: applicationUser.id,
          name: applicationUser.username,
          photoUrl: applicationUser.profilePictureUrl,
          email: 'youruser@youruseremail.com',
          phone: 'yourusersphone'
       });
}

Read more about notifications.

Welcome message

We’re going to add personal welcome messages for each user in our application.

ChatPreferences

Navigate to src/shared/models/chat-preferences.model.ts and add the following highlighted code:

export class ChatPreferences {
    ...
    chatWelcomeMessage: string;
    
    constructor(..., chatWelcomeMessage: string) {
        ...
        this.chatWelcomeMessage = chatWelcomeMessage;
    }
}
Mock users

Navigate to src/core/mocks/users.mock.ts and make sure to add a welcome message for each mock user, as follows:

new User(4, 'John', process.env.PUBLIC_URL + '/assets/images/users/john.jpg', new ChatPreferences("#1D1F1E", 
"Hi! Any questions? Let me know how I can help"))
TalkUtils

Add the following highlighted code to TalkUtils#createTalkUser:

export async function createTalkUser(applicationUser: User) : Promise {
  await Talk.ready;
  
  return new Talk.User({
          id: applicationUser.id,
          name: applicationUser.username,
          photoUrl: applicationUser.profilePictureUrl,
          welcomeMessage: applicationUser.chatPreferences.chatWelcomeMessage
       });
}

You have now successfully added welcome messages to your application.

Destroying Popups

You may have noticed that if you open a Popup with a vendor and then navigate to the Inbox page, the Popup is still visible. If the user is at the Inbox page, there is no need to have any Popups open as these conversations can be opened through the Inbox itself.

We will, therefore, write some code that will make sure that any active Popups will be destroyed whenever the Inbox page is being visited by our user.

We’ll have to save all the Popups that are being opened until we should destroy them, which is when the InboxPage is being displayed.

TalkUtils

Add the following highlighted code to the src/shared/utils/talk.util.ts:

...
let loadedPopups: Talk.Popup[] = [];

export function addPopup(popup: Talk.Popup) {
    loadedPopups.push(popup);
}

export function destroyAllPopups() {
    if (loadedPopups.length > 0) {
        loadedPopups.forEach(p => p.destroy());
        loadedPopups = [];
    }
}
...
ProductPage

Add the following highlighted line of code to the TalkUtils#preloadChatPopup function:

import { addPopup } from 'src/shared/utils/talk.util';

preloadChatPopup() {
    ...
    addPopup(this.chatPopup);
  }
InboxPageContainer

Add the following highlighted line of code to the InboxPageContainer#componentDidMount lifecycle hook:

import { destroyAllPopups } from 'src/shared/utils/talk.util';

async componentDidMount() {
    ...
   destroyAllPopups();
  }

There are a lot more things you can customize about TalkJS, such as custom chat UI themes, different languages and so on.

Take a look at our documentation to read more about our customization possibilities.

If you have a question, don't hesitate to drop by our support chat.