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
htmldivelement, we’re using a WebView within this component. What this component does is use a WebView to display anhtmlfile (we’ll create thehtmlfile in a moment) into which TalkJS’ UI is going to be mounted later on in this tutorial. - The
loadScriptproperty will contain the JavaScript code that is needed to mount TalkJS into the WebView. By passing this property to the WebView’sinjectedJavaScriptproperty, 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 theTalkUIcomponent 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&&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.Userobject, which is needed to create theTalk.Session. - Create an
Inboxand store its object aswindow.ui. - Mount the
Inboxthat is stored inwindow.uiinto thetalkjs-containerdivelement. Thetalkjs-containercan be found in thehtmlfile 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
handlePostScriptInjectionfunction to theTalkUI. This function will be executed whenever the script has been injected and executed into theTalkUI. In order for another script to be executed within theTalkUI, we need to reset theshouldInjectandscriptproperties in the Redux state. This is being done in thehandlePostScriptInjectionfunction.
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 ofUserobjects for the names that are currently selected by the user in theSelectionList. - Setting the JavaScript in the Redux state. The JavaScript that is being set is the generated JavaScript with
getInboxSelectConversationScript(). - Toggling the
shouldInjectproperty in the Redux state. This will cause the script to be injected and executed into theTalkUIcomponent. - Navigating to the
InboxNavigationRouteinside theInboxStackNavigator. This will display theChatInboxScreenContainerto the user and, thus, display theTalkUIwhich 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&&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.paramsis notnull, the header will be displayed. Whenever it is indeednull, the navigation header will beundefined, which in turn will result in the header being hidden. - Pass the
handleMessagefunction as theonMessagehandler for the `TalkUI` component. - The
handleMessagefunction will extract the String value from theeventthat is being passed as a parameter. The String value that will be passed will either betrueorfalse, 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 betrue, if no conversation has been selected the value will befalse. The navigation header should be hidden whenever a conversation has been selected, thus the value beingtrue. - The
toggleNavigationHeader()function sets the navigation params either tonullorundefined, depending on the givenshouldHidevalue. Whenever this function sets the value tonull, 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.