Creating a chat app using TalkJS chat API and React Native

This is an older tutorial. Check out the Getting started guide for React Native.

This tutorial will show you how you can implement private and group chat functionalities into any React Native application by using the TalkJS chat API. 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 Native version

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

NodeJS version

Make sure you're working with NodeJS v10.13.0 or above.

The messaging-app

Our example messaging-app is an application that realizes a use case in which users are able to chat with other users in both private and group chats. We will add working chat functionalities to the base-app within this tutorial and thus create the messaging-app out of it. The base-app is the messaging-app without the chat functionalities: the user can login and select who to start a chat with but the actual chat will not be created within the base-app itself. The base-app is just an app with a User Interface that has no chat functionality, implementing TalkJS will make this app a functional messaging app.

Before this tutorial's implementation (base-app):

After this tutorial's implementation (messaging-app):

This tutorial is meant to demonstrate how you can add chat functionality to any React Native application by using TalkJS. You can perform the steps in this tutorial on our example application, but you should also be able to do them inside your own React Native 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 a React Native example application can be found on the TalkJS GitHub repo.


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 base-app, you can clone a project from our GitHub repository.

- Install all the needed modules: `npm install`

- For Apple devices, start the application on either your mobile device or an emulator as you normally would with a React Native application.

- For Android devices, to start the application on your mobile device, use: `npm run start-android-device`.

To start it on an emulator, use: `npm run start-android-emulator`


Implementing the Inbox

In this section we’re going to create and implement a component that will use the TalkJS’ Inbox.

Creating the TalkUI component

We're going to create a generalized component that will be able to use any of the TalkJS UI modes. The TalkJS UI mode that will be used within this application is the TalkJS Inbox.

Create a file named `TalkUI.native.tsx`, located in `app/components/TalkUI/` and add the following code:

import React, { Component } from 'react';
import { WebView, Platform } from 'react-native';

interface DefaultProps { 
    loadScript: string
}
class TalkUI extends Component<DefaultProps, object> {

    private webView: any;
    
    componentWillUnmount() {
        this.injectJavaScript('window.ui.destroy();');
    }
    
    injectJavaScript(script: string) {
        if (this.webView) {
            this.webView.injectJavaScript(script);
        }
    }
    
    render() {
        return (
             this.webView = r}
                javaScriptEnabled={true}
                domStorageEnabled={true}
                source={ Platform.OS === 'ios' ? require('./talkjs-container.html') : { uri: "file:///android_asset/html/talkjs-container.html" } }
                injectedJavaScript={this.props.loadScript}
            />
        );
    }
}
export default TalkUI;

- Because TalkJS has to be mounted into an `html` `div` element, we’re using a WebView within this component. What this component does is use a WebView to display an `html` file (we’ll create the `html` file in a moment) into which TalkJS’ UI is going to be mounted later on in this tutorial.

- The `loadScript` property will contain the JavaScript code that is needed to mount TalkJS into the WebView. By passing this property to the WebView’s `injectedJavaScript` property, we’re making sure that the JavaScript is being injected and executed into the WebView whenever the WebView has finished loading.

- For example, `this.injectJavaScript('window.ui.destroy();');` will ensure that the TalkJS UI mode is being destroyed whenever the `TalkUI` component is being unmounted. This part will make more sense once we’re a bit further through the tutorial.

Create a file named `talkjs-container.html`, located in `app/components/TalkUI/` and add the following code:

<script>
        (function(t,a,l,k,j,s){
        s=a.createElement('script');s.async=1;s.src="https://cdn.talkjs.com/talk.js";a.head.appendChild(s)
        ;k=t.Promise;t.Talk={v:1,ready:{then:function(f){if(k)return new k(function(r,e){l.push([f,r,e])});l
        .push([f])},catch:function(){return k&amp;&amp;new k()},c:l}};})(window,document,[]);
</script>

<div id="talkjs-container" style="height: 100%; text-align: center;">Loading</div>

This file adds a `div` element into which the TalkJS Inbox will be mounted, as well as the JavaScript bundle that is needed to execute TalkJS-related JavaScript code within the `html`.

Make sure to copy the `talkjs-container.html` to the following folder: `android/app/src/main/assets/html/`, to ensure that the WebView is also able to use the `html` file whenever the app is being run on an Android device.

Mapping Redux state to the ChatInboxScreen

The `ChatInboxScreen` will need access to the current logged in user, which can be extracted from the Redux state.

Since we’re using the Container Pattern within this application, we need to conform to its concepts by changing the class into a Container whenever it’ll access the Redux state.

Navigate to `app/screens/ChatInbox/ChatInboxScreen.native.tsx` and change both its `file` and `class` name to `ChatInboxScreenContainer`. Also, change the exported component at the end of the file to `ChatInboxScreenContainer`.

Navigate to `App.tsx` and modify the `ChatInboxScreen` to `ChatInboxScreenContainer` like the following highlighted code:

...
import ChatInboxScreenContainer from './app/screens/ChatInbox/ChatInboxScreenContainer.native';

...
const InboxStackNavigator = createStackNavigator({
  Inbox: { 
    screen: ChatInboxScreenContainer
    ...
  },
  ...
 });
 ...

Add the following highlighted code to the `ChatInboxScreenContainer.native.tsx`:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';
import { Text } from 'react-native';
import { User } from '../../shared/models/user.model';

interface DefaultProps { 
    currentUser: User
}
class ChatInboxScreenContainer extends Component<DefaultProps, object> {

    render() {
        return (
            We'd like to see chats here!
        );
    }
}

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)(ChatInboxScreenContainer);

The added lines of code give the class access to the currentUser, which is being mapped from the Redux state.

Rendering the TalkUI in ChatInboxScreenContainer

Navigate to `app/screens/ChatInbox/ChatInboxScreenContainer.native.tsx` and add the following highlighted code:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';
import { Text } from 'react-native'; //<-- This unused import can be removed
import { User } from '../../shared/models/user.model';
import TalkUI from '../../components/TalkUI/TalkUI.native';

interface DefaultProps { 
    currentUser: User
}
class ChatInboxScreenContainer extends Component<DefaultProps, object> {

    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)(ChatInboxScreenContainer);

We're now rendering the `TalkUI` component but the `loadScript` value that we're currently passing is not the actual JavaScript that we need to execute.

Create a file named `talk.util.ts`, located in `app/shared/utils/` and add the following code:

import { User } from "../models/user.model";

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

export function getInboxLoadScript(currentUser: User) : string {
    return `Talk.ready.then(function() {
        window.currentUser = new Talk.User({
            id: "` + 'user_' + currentUser.id + `",
            name: "` + currentUser.username + `"
        });
        
        window.talkSession = new Talk.Session({
            appId: "` + APP_ID + `",
            me: window.currentUser
        });
        
        window.ui = window.talkSession.createInbox();
        window.ui.mount(document.getElementById("talkjs-container"));
    });`;
}

This function will generate the JavaScript that is needed to mount the `Inbox` into the WebView.

What this JavaScript does:

- Start a Session for the current logged in user. You can read more about a TalkJS Session here.

- Convert the application's current user object into a `Talk.User` object, which is needed to create the `Talk.Session`.

- Create an `Inbox` and store its object as `window.ui`.

- Mount the `Inbox` that is stored in `window.ui` into the `talkjs-container` `div` element. The `talkjs-container` can be found in the `html` file that we have loaded into the WebView (`talkjs-container.html`).

Please note that the used `id` and `name` for the `window.currentUser` can be changed to anything else that will suit the needs of your application.

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.Creating an account is free while you develop and test integrating TalkJS. Once you have an account go to the TalkJS dashboard and look for your App ID.

Make sure to change the `APP_ID` within `talk.util.ts` to the one displayed in your dashboard.

Passing the correct script to the TalkUI component

Navigate to `app/screens/ChatInbox/ChatInboxScreenContainer.native.tsx` and add the following highlighted code:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';
import { Text } from 'react-native';
import { User } from '../../shared/models/user.model';
import TalkUI from '../../components/TalkUI/TalkUI.native';
import { getInboxLoadScript } from '../../shared/utils/talk.util';

interface DefaultProps { 
    currentUser: User
}
class ChatInboxScreenContainer extends Component<DefaultProps, object> {

    render() {
        return (
            <TalkUI />
        );
    }
}

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)(ChatInboxScreenContainer);

We’ve now added the actual JavaScript that is needed to mount the `TalkJS Inbox`. At this stage of the tutorial, your user is able to login to the app and will see the `Inbox` upon login.

Since the user does not have any previous conversations, the `Inbox` shows the user that he/she currently has no chats, as shown in the image below.

In the next section we're going to add the ability to create a conversation within our application; the user will be able to create both private and group chats with his/her contacts.

If you have executed the steps within this section successfully, your `app/components/TalkUI/TalkUI.native.tsx`, `app/screens/ChatInbox/ChatInboxScreenContainer.native.tsx`, and `app/shared/utils/talk.util.ts` should look like:

`app/components/TalkUI/TalkUI.native.tsx`:

import React, { Component } from 'react';
import { WebView, Platform } from 'react-native';

interface DefaultProps { 
    loadScript: string
}
class TalkUI extends Component<DefaultProps, object> {

    private webView: any;
    
    componentWillUnmount() {
        this.injectJavaScript('window.ui.destroy();');
    }
    
    injectJavaScript(script: string) {
        if (this.webView) {
            this.webView.injectJavaScript(script);
        }
    }
    
    render() {
        return (
             this.webView = r}
                javaScriptEnabled={true}
                domStorageEnabled={true}
                source={ Platform.OS === 'ios' ? require('./talkjs-container.html') : { uri: "file:///android_asset/html/talkjs-container.html" } }
                injectedJavaScript={this.props.loadScript}
            />
        );
    }
}
export default TalkUI;

`app/screens/ChatInbox/ChatInboxScreenContainer.native.tsx`:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';
import { User } from '../../shared/models/user.model';
import TalkUI from '../../components/TalkUI/TalkUI.native';
import { getInboxLoadScript } from '../../shared/utils/talk.util';

interface DefaultProps { 
    currentUser: User
    
}
class ChatInboxScreenContainer extends Component<DefaultProps, object> {

    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)(ChatInboxScreenContainer);

`app/shared/utils/talk.util.ts`:

import { User } from "../models/user.model";
// Change this to your actual AppId which can be
// found in the TalkJS dashboard.
const APP_ID = 'YOUR_APP_ID';

export function getInboxLoadScript(currentUser: User) : string {
    return `Talk.ready.then(function() {
        window.currentUser = new Talk.User({
            id: "` + 'user_' + currentUser.id + `",
            name: "` + currentUser.username + `"
        });
        
        window.talkSession = new Talk.Session({
            appId: "` + APP_ID + `",
            me: window.currentUser
        });
        
        window.ui = window.talkSession.createInbox();
        window.ui.mount(document.getElementById("talkjs-container"));
    });`;
}

Starting a conversation

What we want to achieve is that our user is able to, at any time, select users that he/she wants to start a conversation with. For this to work within TalkJS, we have to generate JavaScript that will create and select the `conversation` into the `Inbox`.

The screen that the user will interact with to create a conversation is the `CreateChatScreenContainer` (see image below).

However, the screen that renders the `TalkUI`, and thus the `Inbox`, is the `ChatInboxScreenContainer`. This means that we will have to pass the generated JavaScript from the `CreateChatScreenContainer` to the `ChatInboxScreenContainer`, as we need the generated JavaScript to be injected and executed into the `TalkUI` component.

We’re going to use Redux to pass the generated JavaScript from the `CreateChatScreenContainer` to the `ChatInboxScreenContainer`. The `CreateChatScreenContainer` will set the generated JavaScript in the Redux state and the `ChatInboxScreenContainer` will receive the JavaScript by mapping the Redux state to its properties.

Modifying the Redux store for TalkJS JavaScript passing

We're adding two properties to the Redux state: a String `script` that’ll hold the JavaScript as its value and a Boolean `shouldInject` that will determine whether the JavaScript should be actually executed.

The `shouldInject` property will be used to allow the same JavaScript to be injected and executed multiple times: whenever the same JavaScript should be executed, the `script` value will not change. This would cause the `ChatInboxScreenContainer` not to re-map the Redux state, as the Redux state hasn't changed in this scenario. By using the `shouldInject` property, the `ChatInboxScreenContainer` will actually re-map the Redux state in this scenario, since the `shouldInject` value would change from `false` to `true` (or the other way around) which means a change in the Redux state.

Redux actions

Create a file named `talk.actions.ts`, located in `app/shared/store/actions/` and add the following code:

export const TOGGLE_SHOULD_INJECT = 'toggleShouldInject';
export const SET_SCRIPT = 'setScript';

export function toggleShouldInject(shouldInject: boolean) {
    return {
        type: TOGGLE_SHOULD_INJECT,
        payload: {
            shouldInject: shouldInject
        }
    };
}

export function setScript(script: string) {
    return {
        type: SET_SCRIPT,
        payload: {
            script: script
        }
    }
}
Redux reducer

Create a file named `talk.reducer.ts`, located in `app/shared/store/reducers/` and add the following code:

import { TOGGLE_SHOULD_INJECT, SET_SCRIPT } from "../actions/talk.actions";

export default function talkReducer(state = { script: '', shouldInject: false }, action: any) {
    switch (action.type) {
        case TOGGLE_SHOULD_INJECT:
            return { ...state, shouldInject: action.payload.shouldInject };
        case SET_SCRIPT:
            return { ...state, script: action.payload.script }
        default:
            return state;
    }
}
Redux store

Navigate to `app/shared/store/store.ts` and add the following highlighted code:

import { combineReducers, createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import authenticationReducer from './reducers/authentication.reducer';
import talkReducer from './reducers/talk.reducer';
  
const allReducers = combineReducers({
    authentication: authenticationReducer,
    talk: talkReducer
});

function configureStore(initialState: object) {
    return createStore(allReducers, initialState, composeWithDevTools(applyMiddleware()));
}

export default configureStore({
    authentication: { currentUser: null },
    talk: {
        script: '',
        shouldInject: false
    }
});

Mapping Talk Redux state to ChatInboxScreenContainer

Navigate to `app/screens/ChatInbox/ChatInboxScreenContainer.native.tsx` and add the following highlighted code:

...
import { toggleShouldInject, setScript } from '../../shared/store/actions/talk.actions';

interface DefaultProps { 
    currentUser: User,
    talkScript: string,
    shouldInjectScript: boolean,
    toggleShouldInject: (shouldInject: boolean) => void,
    setScript: (script: string) => void
}
class ChatInboxScreenContainer extends Component<DefaultProps, object> {

    render() {
        ...
    }
}

const mapStateToProps = (state: any, props: any) => {
    return { 
        currentUser: state.authentication.currentUser,
        talkScript: state.talk.script,
        shouldInjectScript: state.talk.shouldInject  
    }
}
const mapDispatchToProps = (dispatch: Dispatch, props: any) => {
    return bindActionCreators({
        toggleShouldInject: toggleShouldInject,
        setScript: setScript
    }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatInboxScreenContainer);

Allowing the TalkUI component to inject and execute JavaScript

We’re going to pass the `talkScript` and `shouldInjectScript` properties to the `TalkUI` from the `ChatInboxScreenContainer`. Like standard React behaviour, the `TalkUI` will re-render itself whenever the passed properties have changed. This means that the `TalkUI` component will re-render itself each time new JavaScript should be injected and executed.

Because the `TalkUI` component renders a WebView, each re-render will cause a re-render of the WebView as well. Whenever the WebView re-renders itself it will reload the `html` that it should be displaying. Because the TalkJS `Inbox` is mounted into the html within the WebView, a re-render of the WebView means a reload of TalkJS: the TalkJS bundle will be re-downloaded and the `Inbox` will be re-mounted. This is behaviour that we want to avoid: we do not want TalkJS to be reloaded each time new JavaScript has been passed to the `TalkUI` component. As that would deteriorate the UX: the user would see the entire `Inbox` disappear and re-appear whenever JavaScript would be injected and executed.

Therefore, we will override the `shouldComponentUpdate` function in the `TalkUI` component to always return `false`. Because `shouldComponentUpdate` will always return `false`, we will have to inject and execute the needed JavaScript within this function as this will be the only function that will be executed for the `TalkUI` component whenever it has been passed new properties.

Navigate to `app/components/TalkUI/TalkUI.native.tsx` and add the following highlighted code:

...

interface DefaultProps { 
    loadScript: string,
    injectionScript?: string,
    shouldInjectScript?: boolean,
    onScriptInjection?: () => void
}
class TalkUI extends Component<DefaultProps, object> {

    ...
    
    shouldComponentUpdate(nextProps: any) {
        if (nextProps.shouldInjectScript) {
            this.injectJavaScript(nextProps.injectionScript);
            
            if (this.props.onScriptInjection) {
                this.props.onScriptInjection();
            }
        }
        return false;
    }
    
    ...
}
export default TalkUI;

The added code will inject the JavaScript whenever needed without updating the `TalkUI` component itself. The optional `onScriptInjection` property will should make more sense in the next section.

Passing the script to the TalkUI component

Navigate to `app/screens/ChatInbox/ChatInboxScreenContainer.native.tsx` and add the following highlighted code:

...
class ChatInboxScreenContainer extends Component<DefaultProps, object> {

    handlePostScriptInjection = () => {
        this.props.toggleShouldInject(false);
        this.props.setScript('');
    }

    render() {
        return (
            
        );
    }
}

...

In the code that we added above, we are:

- Passing the script and its needed properties to the `TalkUI`.

- Passing the `handlePostScriptInjection` function to the `TalkUI`. This function will be executed whenever the script has been injected and executed into the `TalkUI`. In order for another script to be executed within the `TalkUI`, we need to reset the `shouldInject` and `script` properties in the Redux state. This is being done in the `handlePostScriptInjection` function.

Mapping Redux state to the CreateChatScreenContainer

We’re going to generate the script within the `CreateChatScreenContainer`. To generate the script, the `CreateChatScreenContainer` will need access to some of the properties in the Redux state.

Navigate to `app/screens/CreateChat/CreateChatScreenContainer.native.tsx` and add the following highlighted code:

...
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';
import { setScript, toggleShouldInject } from '../../shared/store/actions/talk.actions';
import { User } from '../../shared/models/user.model';

interface DefaultProps { 
    navigation: NavigationScreenProp<any, any>,
    currentUser: User,
    setScript: (script: string) => void,
    toggleShouldInject: (shouldInject: boolean) => void
}

interface DefaultState {
    ...
}
class CreateChatScreenContainer extends Component<DefaultProps, DefaultState, object> {

    ...
    
    render() {
        ...
    }
}

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

Creating the script generating function

Navigate to `app/shared/utils/talk.util.ts` and add the following code:

export function getInboxSelectConversationScript(participants: User[], conversationId: string) : string {
    let script = `Talk.ready.then(function() {
        conversation = window.talkSession.getOrCreateConversation("` + conversationId + `");    
        conversation.setParticipant(window.currentUser);`;
        
    for (const participant of participants) {
        script += `window.participant` + participant.id + ` = new Talk.User({
            id: "` + 'user_' + participant.id + `", 
            name: "` + participant.username + `"
        });`;
    }
    
    for (const participant of participants) {
        script += 'conversation.setParticipant(window.participant' + participant.id + ');';
    }
    
    script += 'window.ui.select(conversation);';
    script += '});';
    
    return script;
}

This function creates a conversation (if a conversation with the given `conversationId` already exists, the conversation will be retrieved instead of created) with a given `conversationId`, adds all the needed participants to it and selects it in the `Inbox`. This will make the `Inbox` open and display the created conversation.

The `getInboxSelectConversationScript` function generates the following code as a String so it can be injected and executed into the `TalkUI`:

Talk.ready.then(function() {
  // Retrieves or creates the conversation for the given conversationId
  conversation = window.talkSession.getOrCreateConversation(conversationId);    
  
  // Adds the currentuser as a participant
  conversation.setParticipant(window.currentUser);
        
  // Creates Talk.User objects for the given participants, e.g. participant1 - participant3
  window.participant1 = new Talk.User({
      id: 'user_1',
      name: 'user1'
  });  
  window.participant2 = new Talk.User({
      id: 'user_2',
      name: 'user2'
  });
  window.participant3 = new Talk.User({
      id: 'user_3',
      name: 'user3'
  });

  // Adds all other participants to the conversation, e.g. participant1 - participant3
  conversation.setParticipant(window.participant1);
  conversation.setParticipant(window.participant2);
  conversation.setParticipant(window.participant3);
  
  // Selects the retrieved/generated conversation in the used TalkJS UI mode (we're using the Inbox in this tutorial)
  window.ui.select(conversation);
});
Generating the conversationId

The `getInboxSelectConversationScript` function needs a `conversationId`. We're going to add a function that will generate the same `conversationId` for a given list of users, no matter in which order the users are being passed. The function will behave similar to TalkJS’ OneOnOneId.

We will create this behaviour by sorting the list of users, converting the list to a JSON String, and hashing the JSON String.

Navigate to `app/shared/utils/talk.util.ts` and add the following code:

import sha1 from "sha1";

export function generateConversationId(participants: User[], currentUser: User) {
    const allParticipants = [...participants, currentUser];
    const sorted = allParticipants.sort(alphabeticalFilter);
    const json = JSON.stringify(sorted);
    const hash = sha1(json);
    
    return hash.toString().substring(0, 20);
}

const alphabeticalFilter = (a: User, b: User) => { 
    const aLower = a.username.toLowerCase(), bLower = b.username.toLowerCase();
    
    if (aLower < bLower) { return -1; } else if (aLower > bLower) {
        return 1;
    }
    return 0;
};

Make sure to install the imported `sha` node module.

Setting the generated script in Redux state

Navigate to `app/screens/CreateChat/CreateChatScreenContainer.native.tsx` and add the following highlighted code:

...
import { getUserForUsername } from '../../core/modules/user.module';
import { getInboxSelectConversationScript, generateConversationId } from '../../shared/utils/talk.util';

interface DefaultProps { 
    ...
}
interface DefaultState {
    ...
}
class CreateChatScreenContainer extends Component<DefaultProps, DefaultState, object> {
    ...
    
    handleCreateClick = async () => {
        if (this.state.selectedNames.length == 0) {
            return;
        }
        
        const participants = await this.getSelectedParticipants();
        
        this.props.setScript(getInboxSelectConversationScript(participants, generateConversationId(participants, this.props.currentUser)));
        this.props.toggleShouldInject(true);
        this.props.navigation.navigate('Inbox');
    }
    
    render() {
        ...
    }
    
    async getSelectedParticipants() : Promise<User[]> {
      const participants: User[] = [];
      
      for (const participant of this.state.selectedNames) {
          const user = await getUserForUsername(participant);
          participants.push(user);
      }
      return participants;
    }
}
...

With the above added code we are:

- Adding the `getSelectedParticipants()` function. This function retrieves a list of `User` objects for the names that are currently selected by the user in the `SelectionList`.

- Setting the JavaScript in the Redux state. The JavaScript that is being set is the generated JavaScript with `getInboxSelectConversationScript()`.

- Toggling the `shouldInject` property in the Redux state. This will cause the script to be injected and executed into the `TalkUI` component.

- Navigating to the `Inbox` `NavigationRoute` inside the `InboxStackNavigator`. This will display the `ChatInboxScreenContainer` to the user and, thus, display the `TalkUI` which has now opened the desired conversation.

You have now successfully finished this section. Your user is now able to create both private and group chats with his contacts!

If you have executed the steps within this section successfully, your `app/screens/ChatInbox/ChatInboxScreenContainer.native.tsx`, `app/screens/CreateChat/CreateChatScreenContainer.native.tsx`, `app/components/TalkUI/TalkUI.native.tsx`, `app/shared/utils/talk.util.ts`, `app/shared/store/actions/talk.actions.ts`, `app/shared/store/reducers/talk.reducer.ts`, and `app/shared/store/store.ts` should look like:

`app/screens/ChatInbox/ChatInboxScreenContainer.native.tsx`:

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';
import { User } from '../../shared/models/user.model';
import TalkUI from '../../components/TalkUI/TalkUI.native';
import { getInboxLoadScript } from '../../shared/utils/talk.util';
import { toggleShouldInject, setScript } from '../../shared/store/actions/talk.actions';

interface DefaultProps { 
    currentUser: User,
    talkScript: string,
    shouldInjectScript: boolean,
    toggleShouldInject: (shouldInject: boolean) => void,
    setScript: (script: string) => void
}
class ChatInboxScreenContainer extends Component<DefaultProps, object> {

    handlePostScriptInjection = () => {
        this.props.toggleShouldInject(false);
        this.props.setScript('');
    }
    
    render() {
        return (
            
        );
    }
}

const mapStateToProps = (state: any, props: any) => {
    return { 
        currentUser: state.authentication.currentUser,
        talkScript: state.talk.script,
        shouldInjectScript: state.talk.shouldInject 
    }
}
const mapDispatchToProps = (dispatch: Dispatch, props: any) => {
    return bindActionCreators({
        toggleShouldInject: toggleShouldInject,
        setScript: setScript
    }, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatInboxScreenContainer);

`app/screens/CreateChat/CreateChatScreenContainer.native.tsx`:

import React, { Component } from 'react';
import { Text, TouchableOpacity } from 'react-native';
import { NavigationScreenProp } from 'react-navigation';
import CreateChatScreen from './CreateChatScreen.native';
import { connect } from 'react-redux';
import { Dispatch, bindActionCreators } from 'redux';
import { setScript, toggleShouldInject } from '../../shared/store/actions/talk.actions';
import { User } from '../../shared/models/user.model';
import { getUserForUsername } from '../../core/modules/user.module';
import { getInboxSelectConversationScript, generateConversationId } from '../../shared/utils/talk.util';

interface DefaultProps { 
    navigation: NavigationScreenProp<any, any>,
    currentUser: User,
    setScript: (script: string) => void,
    toggleShouldInject: (shouldInject: boolean) => void
}

interface DefaultState {
    selectedNames: string[]
}
class CreateChatScreenContainer extends Component<DefaultProps, DefaultState, object> {

    static navigationOptions = ({navigation}: any) => ({
        headerRight: 
             navigation.state.params.createClickHandler()}>
                Create
            ,
    });
    
    constructor(props: any) {
        super(props);
        this.state = {
            selectedNames: []
        };
    }
    
    handleCreateClick = async () => {
        if (this.state.selectedNames.length == 0) {
            return;
        }
        const participants = await this.getSelectedParticipants();
                
        this.props.setScript(getInboxSelectConversationScript(participants, generateConversationId(participants, this.props.currentUser)));
        this.props.toggleShouldInject(true);
        this.props.navigation.navigate('Inbox');
    }
    
    handleItemSelectionChange = (selectedItems: string[]) => {
        this.setState({
            selectedNames: selectedItems
        });
    }
    
    componentDidMount() {
        this.props.navigation.setParams({
            createClickHandler: this.handleCreateClick
        });
    }
    
    render() {
        return (
            
        );
    }
    
    async getSelectedParticipants() : Promise<User[]> {
      const participants: User[] = [];
      
      for (const participant of this.state.selectedNames) {
          const user = await getUserForUsername(participant);
          participants.push(user);
      }
      return participants;
    }
}

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

`app/components/TalkUI/TalkUI.native.tsx`:

import React, { Component } from 'react';
import { WebView, Platform } from 'react-native';

interface DefaultProps { 
    loadScript: string,
    injectionScript?: string,
    shouldInjectScript?: boolean,
    onScriptInjection?: () => void
}
class TalkUI extends Component<DefaultProps, object> {

    private webView: any;

    shouldComponentUpdate(nextProps: any) {
        if (nextProps.shouldInjectScript) {
            this.injectJavaScript(nextProps.injectionScript);
            
            if (this.props.onScriptInjection) {
                this.props.onScriptInjection();
            }
        }
        return false;
    }
    
    componentWillUnmount() {
        this.injectJavaScript('window.ui.destroy();');
    }
    
    injectJavaScript(script: string) {
        if (this.webView) {
            this.webView.injectJavaScript(script);
        }
    }
    
    render() {
        return (
             this.webView = r}
                javaScriptEnabled={true}
                domStorageEnabled={true}
                source={ Platform.OS === 'ios' ? require('./talkjs-container.html') : { uri: "file:///android_asset/html/talkjs-container.html" } }
                injectedJavaScript={this.props.loadScript}
            />
        );
    }
}
export default TalkUI;

`app/shared/utils/talk.util.ts`:

import { User } from "../models/user.model";
import sha1 from "sha1";
// Change this to your actual AppId which can be
// found in the TalkJS dashboard.
const APP_ID = 'YOUR_APP_ID';

export function getInboxLoadScript(currentUser: User) : string {
    return `Talk.ready.then(function() {
        window.currentUser = new Talk.User({
            id: "` + 'user_' + currentUser.id + `",
            name: "` + currentUser.username + `"
        });
        
        window.talkSession = new Talk.Session({
            appId: "` + APP_ID + `",
            me: window.currentUser
        });
        
        window.ui = window.talkSession.createInbox();
        window.ui.mount(document.getElementById("talkjs-container"));
    });`;
}

export function getInboxSelectConversationScript(participants: User[], conversationId: string) : string {
    let script = `Talk.ready.then(function() {
        conversation = window.talkSession.getOrCreateConversation("` + conversationId + `");    
        conversation.setParticipant(window.currentUser);`;
        
    for (const participant of participants) {
        script += `window.participant` + participant.id + ` = new Talk.User({
            id: "` + 'user_' + participant.id + `", 
            name: "` + participant.username + `"
        });`;
    }
    
    for (const participant of participants) {
        script += 'conversation.setParticipant(window.participant' + participant.id + ');';
    }
    
    script += 'window.ui.select(conversation);';
    script += '});';
    
    return script;
}

export function generateConversationId(participants: User[], currentUser: User) {
    const allParticipants = [...participants, currentUser];
    const sorted = allParticipants.sort(alphabeticalFilter);
    const json = JSON.stringify(sorted);
    const hash = sha1(json);
    
    return hash.toString().substring(0, 20);
}

const alphabeticalFilter = (a: User, b: User) => { 
    const aLower = a.username.toLowerCase(), bLower = b.username.toLowerCase();
    
    if (aLower < bLower) { return -1; } else if (aLower > bLower) {
        return 1;
    }
    return 0;
};

`app/shared/store/actions/talk.actions.ts`:

export const TOGGLE_SHOULD_INJECT = 'toggleShouldInject';
export const SET_SCRIPT = 'setScript';

export function toggleShouldInject(shouldInject: boolean) {
    return {
        type: TOGGLE_SHOULD_INJECT,
        payload: {
            shouldInject: shouldInject
        }
    };
}

export function setScript(script: string) {
    return {
        type: SET_SCRIPT,
        payload: {
            script: script
        }
    }
}

`app/shared/store/reducers/talk.reducer.ts`:

import { TOGGLE_SHOULD_INJECT, SET_SCRIPT } from "../actions/talk.actions";

export default function talkReducer(state = { script: '', shouldInject: false }, action: any) {
    switch (action.type) {
        case TOGGLE_SHOULD_INJECT:
            return { ...state, shouldInject: action.payload.shouldInject };
        case SET_SCRIPT:
            return { ...state, script: action.payload.script }
        default:
            return state;
    }
}

`app/shared/store/store.ts`:

import { combineReducers, createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import authenticationReducer from './reducers/authentication.reducer';
import talkReducer from './reducers/talk.reducer';  

const allReducers = combineReducers({
    authentication: authenticationReducer,
    talk: talkReducer
});

function configureStore(initialState: object) {
    return createStore(allReducers, initialState, composeWithDevTools(applyMiddleware()));
}

export default configureStore({
    authentication: { currentUser: null },
    talk: {
        script: '',
        shouldInject: false
    }
});

Enabling 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 – see our Authentication docs for more information.


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.

Toggling the navigation header

Currently, if the user opens a conversation there are two navigation headers in the application.

This is because TalkJS automatically creates a navigation header with a back button on small and mobile devices whenever a conversation is selected in the `Inbox`. See the image below.

In this section, we’re going to hide the app's navigation header whenever a conversation has been selected within the `Inbox`. See the image below.

Navigate to `app/shared/utils/talk.util.ts` and add the following highlighted code:

...
export function getInboxLoadScript(currentUser: User) : string {
    return `Talk.ready.then(function() {
        window.currentUser = new Talk.User({
            id: "` + 'user_' + currentUser.id + `",
            name: "` + currentUser.username + `",
            configuration: "react_native_app"
        });
        
        window.talkSession = new Talk.Session({
            appId: "` + APP_ID + `",
            me: window.currentUser
        });
        
        window.ui = window.talkSession.createInbox();
        window.ui.on("conversationSelected", (event) => { postMessage((event.conversation) != null); });
        window.ui.mount(document.getElementById("talkjs-container"));
    });`;
}
...

The code that we’ve added adds a function to the `Inbox`’ `conversationSelected` event. Whenever a conversation is selected, the function that we passed within the body (`{ }`) will be executed. Our body will execute the `postMessage` function with a boolean value representing whether the conversation that has been selected is null or not. If the conversation is indeed null, no conversation is selected. You can read more about the `Inbox`' `conversationSelected` event here.

Our `talkjs-container.html` currently has no `postMessage` function, we still have to add it.

The WebView has a property `onMessage` which can be given a function to handle incoming messages. Messages passed through `window.postMessage()` within the JavaScript of the WebView will be sent to the handling function that has been passed to the `onMessage` property.

Because `window.postMessage` is not always successfully initialized whenever the WebView has been loaded, especially on iOS devices, we’re going to add our own `postMessage` function which will poll until `window.postMessage` has been successfully initialized.

Navigate to `app/components/TalkUI/talkjs-container.html` and add the following highlighted code:

<script>
        (function(t,a,l,k,j,s){
        s=a.createElement('script');s.async=1;s.src="https://cdn.talkjs.com/talk.js";a.head.appendChild(s)
        ;k=t.Promise;t.Talk={v:1,ready:{then:function(f){if(k)return new k(function(r,e){l.push([f,r,e])});l
        .push([f])},catch:function(){return k&amp;&amp;new k()},c:l}};})(window,document,[]);

        function postMessage(message) {
            if (window.postMessage.length === 1) window.postMessage(message);
            else setTimeout(postMessage.bind(null, message), 100);
        }
    </script>

Replace the `talkjs-container.html` in `android/app/src/main/assets/html/` with the one that has just been modified (`app/components/TalkUI/talkjs-container.html`).

Modifying the TalkUI component

Navigate to `app/components/TalkUI/TalkUI.native.tsx` and add the following highlighted code:

...

interface DefaultProps { 
    ...
    onMessage?: (event: any) => void
}
class TalkUI extends Component<DefaultProps, object> {

    ...
    render() {
        return (
             this.webView = r}
                javaScriptEnabled={true}
                domStorageEnabled={true}
                source={ Platform.OS === 'ios' ? require('./talkjs-container.html') : { uri: "file:///android_asset/html/talkjs-container.html" } }
                injectedJavaScript={this.props.loadScript}
                onMessage={this.props.onMessage}
            />
        );
    }
}
export default TalkUI;

Adding header toggling to ChatInboxScreenContainer

Navigate to `app/screens/ChatInbox/ChatInboxScreenContainer.native.tsx` and add the following highlighted code:

...
import { NavigationScreenProp } from 'react-navigation';

interface DefaultProps { 
    navigation: NavigationScreenProp<any, any>,
    ...
}
class ChatInboxScreenContainer extends Component<DefaultProps, object> {

    static navigationOptions = ({ navigation }: any) => ({
        header: navigation.state.params ? navigation.state.params.header : undefined
    });
    
    ...
    
    render() {
        return (
            
        );
    }
    
    handleMessage = (event: any) => {
        const message = event.nativeEvent.data;
        this.toggleNavigationHeader((message === 'true'));
    }
    
    toggleNavigationHeader(shouldHide: boolean) {
        this.props.navigation.setParams({ 
            header: shouldHide ? null : undefined
        });
    }
}

...

The above-added code does the following:

- Set a condition for the displaying of the navigation header. Whenever the `navigation.state.params` is not `null`, the header will be displayed. Whenever it is indeed `null`, the navigation header will be `undefined`, which in turn will result in the header being hidden.

- Pass the `handleMessage` function as the `onMessage` handler for the `TalkUI` component.

- The `handleMessage` function will extract the String value from the `event` that is being passed as a parameter. The String value that will be passed will either be `true` or `false`, as the value that is being passed is the result of the code that we added earlier: `postMessage((event.conversation) != null);`. If a conversation has been selected, the value will be `true`, if no conversation has been selected the value will be `false`. The navigation header should be hidden whenever a conversation has been selected, thus the value being `true`.

- The `toggleNavigationHeader()` function sets the navigation params either to `null` or `undefined`, depending on the given `shouldHide` value. Whenever this function sets the value to `null`, the earlier added header displaying condition will hide the navigation header.

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!

Navigate to `app/shared/utils/talk.util.ts` and add the following highlighted code:

...
export function getInboxLoadScript(currentUser: User) : string {
    return `Talk.ready.then(function() {
        window.currentUser = new Talk.User({
            id: "` + 'user_' + currentUser.id + `",
            name: "` + currentUser.username + `",
            email: 'youruser@youruseremail.com',
            phone: 'yourusersphone'
        });
        
        ...
    });`;
}
...

Welcome message

We’re going to add a welcome message for our current user. All we have to do is set the `welcomeMessage` property when instantiating our `Talk.User`.

Navigate to `app/shared/utils/talk.util.ts` and add the following highlighted code:

...
export function getInboxLoadScript(currentUser: User) : string {
    return `Talk.ready.then(function() {
        window.currentUser = new Talk.User({
            id: "` + 'user_' + currentUser.id + `",
            name: "` + currentUser.username + `",
            welcomeMessage: 'Hi there! Whatsup?'
        });
        
        ...
    });`;
}
...

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.