Skip to content

Instantly share code, notes, and snippets.

@ponty96
Last active August 5, 2016 14:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ponty96/23318ade93d4339298131772ed9fe203 to your computer and use it in GitHub Desktop.
Save ponty96/23318ade93d4339298131772ed9fe203 to your computer and use it in GitHub Desktop.

React-Native Pusher Chat

For some months now, whilst working on React-native, I stumbled upon Pusher which is a good tool for building applications which have a need for real-time message broadcasting. I'll be showing you a bit of what you could use Pusher to do along with React-Native.

In this tutorial you will be building a simple chat application with Pusher, React-native and Redux as a state container. Before we continue, I’d love to point out that this article assumes a fairly good experience with the use of node.js, react, react-native and es6.

Pusher

Pusher is a real-time communication platform used to send broadcast messages to listeners subscribed to a channel. Listeners subscribe to a channel and the messages are broadcast to the channel and all the listeners receive the messages.

You will first need to create an account and then install the Pusher npm module with the following command:

Under the App Keys section of your Pusher project, note the app_id, key, and secret values.

npm install pusher-js -g

React Native

React Native is a framework for building rich, fast and native mobile apps with the same principles used for building web apps with [React.js]React (for me) presents a better way to build UIs and is worth checking out for better understanding of this tutorial and to make your front-end life a lot easier. If you have not used React Native before, SitePoint has a lot of tutorials, including a Quick Tip to get you started.

Redux

Redux is a simple state container (the simplest I've used so far) that helps keep state in React.js (and React Native) applications using unidirectional flow of state to your UI components and back from your UI component to the Redux state tree. For more details, watch this awesome video tutorials by the man who created Redux. You will learn a lot of functional programing principles in Javascript and it will make you see Javascript in a different light.

Build a Backend

The app needs a web backend to send chat messages to, and to serve as the point from where chat messages are broadcast to all listeners. You will build this with Express.js, a minimalist web framework running on node.js.

You need to have Node 4.0 or higher versions installed on your PC. Then in order to do the following; Start the Node Project, Install Express Install Pusher, run the following commands respectively:

npm init
npm install express -g
npm install pusher
  • Create a folder for the project called ChatServer
  • Inside it an index.js file
  • In index.js, require the necessary libraries and create an express app to run at port 5000.
var express = require('express');
var Pusher = require('pusher');
var app = express();

app.set('port', (process.env.PORT || 5000));
  • Create your own instance of the Pusher library by passing to it the app_id, key, and secret values
...

const pusher = new Pusher({
   appId: 'YOUR PUSHER APP_ID HERE',
   key: 'YOUR PUSHER KEY HERE',
   secret: 'YOUR PUSHER SECRET HERE',
})
  • Create an endpoint. The endpoint receives chat messages and sends them to pusher to broadcast them to all listeners on the chat channel.
...

app.get('/chat/:chat', function(req,res) {
  const chat_data = JSON.parse(req.params.chat);
  pusher.trigger('chat_channel', 'new-message', {chat:chat_data});
});

app.listen(app.get('port'), function() {
  console.log('Node app is running on port', app.get('port'));
});
  • Our index.js file should look like
var express = require('express');
var Pusher = require('pusher');
var app = express();

app.set('port', (process.env.PORT || 5000));

const pusher = new Pusher({
   appId: 'YOUR PUSHER APP_ID HERE',
   key: 'YOUR PUSHER KEY HERE',
   secret: 'YOUR PUSHER SECRET HERE',
})

app.get('/chat/:chat', function(req,res) {
  const chat_data = JSON.parse(req.params.chat);
  pusher.trigger('chat_channel', 'new-message', {chat:chat_data});
});

app.listen(app.get('port'), function() {
  console.log('Node app is running on port', app.get('port'));
});

Mobile App

Let’s now turn to the mobile app:

Open up a terminal and run the following commands to create a new React Native project:

react-native init PusherChat
cd PusherChat
  • Install Dependencies. The app will require the following dependencies:
    • Axios - For Promises and async requests to the backend.
    • AsyncStorage - For storing chat messages locally.
    • Moment - For setting the time each chat message is sent and arrange messages based on this time.
    • Pusher-js - For connecting to pusher.
    • Redux - The state container
    • Redux-thunk - A simple middleware that helps with action dispatching.
    • React-redux - React bindings for Redux.

You should have already installed pusher-js earlier, and AsyncStorage is part of React native. Install the rest by running:

npm install --save redux redux-thunk moment axios react-redux

Build Redux Actions

Now you are ready to build the chat app, but you start by building the actions that the application will perform. With Redux you have to create application action types, because when you want to change the state of your app (either a message 'received' or 'sent') you need to tell the application's state manager (known as the reducer) how (Action type) and what (Payload or Data) to use to update the state.

We Start by doing the following:

  • Create a new file in src/actions/index.js and add the following
import axios from 'axios'
import { AsyncStorage } from 'react-native'
import moment from 'moment'
import Pusher from 'pusher-js/react-native'


export const SEND_CHAT = 'SEND_CHAT'
export const GET_ALL_CHATS = 'GET_ALL_CHATS'
export const RECEIVE_MESSAGE = ' RECEIVE_MESSAGE'
  • Create Helper functions

You also need helper functions that encapsulate and return the appropriate action_type when called, so that when you want to perform an action on your store, you dispatch the helper functions and pass their payloads to them:

const getChats = (payload) => {
  return { type: GET_ALL_CHATS,
    payload,
  }
}

const newMessage = (payload) => {
  return {
    type: RECEIVE_MESSAGE,
    payload,
  }
}
  • Pusher Listener

You also need a function that subscribes to pusher and listens for new messages. For every new message this function receives, add it to the device AsyncStorageand dispatch a new message action so that the application state is updated.

// function for adding messages to AsyncStorage
const addToStorage = (data) => {
  AsyncStorage.setItem(data.convo_id + data.sentAt, JSON.stringify(data), () => {})
}


// function that listens to pusher for new messages and dispatches a new
// message action
export function receiveMessage(dispatch) {
  const socket = new Pusher('3c01f41582a45afcd689')
  const channel = socket.subscribe('chat_channel')
  channel.bind('new-message',
  (data) => {
    addToStorage(data.chat)
    dispatch(newMessage(data.chat))
  }
)
}
  • Api Message Sender

You also have a function for sending chat messages. This function expects two parameters, the sender and message. In an ideal chat app you should know the sender via the device or login, but for this input the sender:

export function apiSendChat(sender, message) {
  const sentAt = moment().format()
  const chat = { sender, message, sentAt }
  axios.get(`http://localhost:5000/chat/${JSON.stringify(chat)}`)
}
  • AsyncStorage Onload Message Fetcher

Finally is a function that gets all the chat messages from the device's AysncStorage. This is needed when first opening the chat app. It loads all the messages the moment the app is open and as makes them available for the component to render by dispatching the getChats helper function

export function apiGetChats() {
  // get from device async storage and not api

  return dispatch => {
    return AsyncStorage.getAllKeys((err, keys) => {
      AsyncStorage.multiGet(keys, (error, stores) => {
        const chats = []
        stores.map((result, i, store) => {
          // get at each store's key/value so you can work with it
          chats.push(JSON.parse(store[i][1]))
        })
        dispatch(getChats(chats))
      })
    })
  }
}

Build Redux Reducer

The next step is to create the reducer. The easiest way to understand what the reducer does is to think of it as a bank cashier that performs actions on your bank account based on whatever slip (Action Type) you present to them. If you present them a withdrawal slip (Action Type) with a set amount (payload) to withdraw (action), they remove the amount (payload) from your bank account (state). You can also add money (action + payload) with a deposit slip (Action Type) to your account (state).

In summary, the reducer is a function that affects the application state based on the action dispatched and the action contains its type and payload. Based on the action type the reducer knows how best to affect the state of the application.

  • Create a new file called src/reducers/index.js and add the following:
import { combineReducers } from 'redux';
import { SEND_CHAT, GET_ALL_CHATS, RECEIVE_MESSAGE } from './../actions'

// THE REDUCER

const Chats = (state = { chats: [] }, actions) => {
  switch (actions.type) {
    case GET_ALL_CHATS:
      return Object.assign({}, state, {
        process_status: 'completed',
        chats: state.chats.concat(actions.payload),
      });

    case SEND_CHAT:
    case RECEIVE_MESSAGE:
      return Object.assign({}, state, {
        process_status: 'completed',
        chats: [...state.chats, actions.payload],
      });

    default:
      return state;
  }
};

const rootReducer = combineReducers({
  Chats,
})

export default rootReducer;

Build Redux Store

Next create the store. Continuing the bank cashier analogy, the store is like the warehouse where all bank accounts (states) are stored. For now you have one state, Chats, and have access to it whenever you need it.

  • Create a new src/store/configureStore.js file and add the following:

We are adding some middlewares -> redux-thunk and redux-logger(redux-thunk allows you to dispatch functions as actions - redux-logger logs every update made to the store; The previous store state and the current store state.)

import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import rootReducer from '../reducers'

const createStoreWithMiddleware = applyMiddleware(
	thunkMiddleware,
	createLogger()
)(createStore)

export default function configureStore(initialState) {
  const store = createStoreWithMiddleware(rootReducer, initialState)
  return store
}

Build Chat Component

Now let's create the main chat component that renders all the chat messages and allows a user to send a chat message by inputting their message. This component uses the React Native ListView and this is because of the following:

- It helps us render each chat message in row
- Allows us to build the look and feel of each row 
- Gives us scrolling out of the box when needed
  • Create a new src/screens/conversationscreen.js file and add the following:
import React,
{ Component,
  View,
  Text,
  StyleSheet,
  Image,
  ListView,
  TextInput,
  Dimensions,
} from 'react-native'
import KeyboardSpacer from 'react-native-keyboard-spacer'
import { connect } from 'react-redux'
import moment from 'moment'
import { apiSendChat, receiveMessage, apiGetChats } from './../actions/'


const { width } = Dimensions.get('window')

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 10,
  },
  main_text: {
    fontSize: 16,
    textAlign: 'center',
    alignSelf: 'center',
    color: '#42C0FB',
    marginLeft: 5,
  },
  row: {
    flexDirection: 'row',
    borderBottomWidth: 1,
    borderBottomColor: '#42C0FB',
    marginBottom: 10,
    padding: 5,
  },
  back_img: {
    marginTop: 8,
    marginLeft: 8,
    height: 20,
    width: 20,
  },
  innerRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  back_btn: {},
  dp: {
    height: 35,
    width: 35,
    borderRadius: 17.5,
    marginLeft: 5,
    marginRight: 5,
  },
  messageBlock: {
    flexDirection: 'column',
    borderWidth: 1,
    borderColor: '#42C0FB',
    padding: 5,
    marginLeft: 5,
    marginRight: 5,
    justifyContent: 'center',
    alignSelf: 'flex-start',
    borderRadius: 6,
    marginBottom: 5,
  },
  messageBlockRight: {
    flexDirection: 'column',
    backgroundColor: '#fff',
    padding: 5,
    marginLeft: 5,
    marginRight: 5,
    justifyContent: 'flex-end',
    alignSelf: 'flex-end',
    borderRadius: 6,
    marginBottom: 5,
  },
  text: {
    color: '#5c5c5c',
    alignSelf: 'flex-start',
  },
  time: {
    alignSelf: 'flex-start',
    color: '#5c5c5c',
    marginTop: 5,
  },
  timeRight: {
    alignSelf: 'flex-end',
    color: '#42C0FB',
    marginTop: 5,
  },
  textRight: {
    color: '#42C0FB',
    alignSelf: 'flex-end',
    textAlign: 'right',
  },
  input: {
    borderTopColor: '#e5e5e5',
    borderTopWidth: 1,
    padding: 10,
    flexDirection: 'column',
    justifyContent: 'space-between',
  },
  textInput: {
    height: 30,
    width: (width * 0.85),
    color: '#e8e8e8',
    borderTopColor: '#e5e5e5',
    borderTopWidth: 1,
  },
  msgAction: {
    height: 29,
    width: 29,
    marginTop: 13,
  }
})
const username = 'ponty96'

function mapStateToProps(state) {
  return {
    Chats: state.Chats,
    dispatch: state.dispatch
  }
}

class ConversationScreen extends Component {

  constructor(props) {
    super(props)
    const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 != r2})
    this.state = {
      conversation: ds,
      text: '',
      username: ''
    }
  }
  componentDidMount() {
    const  { dispatch } = this.props
    dispatch(apiGetChats())
    receiveMessage(dispatch)
  }
  componentWillReceiveProps(nextProps) {
    const { dispatch, Chats } = nextProps
    const chats = Chats.chats
    chats.sort((a, b) => {
      return moment(a.sent_at).valueOf() - moment(b.sent_at).valueOf()
    })
    this.setState({
      conversation: this.state.conversation.cloneWithRows(chats)
    })

  }

  renderSenderUserBlock(data) {
    return (
      <View style={styles.messageBlockRight>
        <Text style={styles.textRight}>
          { data.sender }
        </Text>
        <Text style={styles.textRight}>
          {data.message}
        </Text>
        <Text style={styles.timeRight}> {moment(data.time).calendar()}</Text>
      </View>
    )
  }
  renderReceiverUserBlock(data) {
    return (
      <View style={styles.messageBlock}>
        <Text style={styles.text}>
          {data.sender}
        </Text>
        <Text style={styles.text}>
          {data.message}
        </Text>
        <Text style={styles.time}> {moment(data.time).calendar()} </Text>
      </View>
    )
  }
  renderRow = (rowData) => {
    return (
      <View>
        {rowData.sender === username ? this.renderSenderUserBlock(rowData) : this.renderReceiverUserBlock(rowData)}
      </View>
    )
  }

  sendMessage = (e) => {
    if(e.nativeEvent.key === 'Enter') {
      const message = this.state.text
      const username =  this.state.username
      apiSendChat(username, message)
    }
  }

  render() {
    return (
      <View style={styles.container}>
        <View style={styles.row}>
          <View style={styles.innerRow}>
            <Image source={{uri:'https://avatars3.githubusercontent.com/u/11190968?v=3&s=460'}} style={styles.dp}/>
            <Text style={styles.main_text}>GROUP CHAT</Text>
          </View>
        </View>

        <ListView
          renderRow={this.renderRow}
          dataSource={this.state.conversation}/>

        <View style={styles.input}>
          <TextInput
            style={styles.textInput}
            onChangeText={(text) => this.setState({username:text})}
            placeholder='Username'/>
        </View>
        <View style={styles.input}>
          <TextInput
            style={styles.textInput}
            onChangeText={(text) => this.setState({text:text})}
            placeholder='Type a message'
            onKeyPress={this.sendMessage}/>
        </View>
        <KeyboardSpacer/>
      </View>
    )
  }


}

export default connect(mapStateToProps)(ConversationScreen)

React Native gives you a lifecycle function, componentWillReceiveProps(nextProps) called whenever the component is about to receive new properties (props) and it's in this function you update the state of the component with chat messages. The renderSenderUserBlock function renders a chat message as sent by the user and the renderReceiverUserBlock function renders a chat message as received by the user. The sendMessage function is called when the user presses the enter key on their keyboard. This function gets the message and username of the sender from the ConversationScreen Component State

Connect to Store

You need to pass the state to the application components. To do this you will use the react-redux library. This allows you to connect the components to redux and access the application state.

React-Redux provides you with 2 things:

  1. A 'Provider' component which allows you to pass the store to it as a property.
  2. A 'connect' function which allows the component to connect to redux. It passes the redux state which the component connects to as properties for the Component.

Finally create app.js in the src/ folder to tie everything together:

import React, { Component} from 'react-native';
import { Provider } from 'react-redux'
import configureStore from './store/configureStore'
import ConversationScreen from './screens/conversation-screen';

const store = configureStore();

export default class PusherChatApp extends Component {
  render() {
    return (
      <Provider store={store}>
        <ConversationScreen />
      </Provider>
    )
  }
}```

### App Entry Point

Finally we'll update our application entry points so as to render the components we just built.
To do this, we'll edit our index.ios.js and index.android.js files to

```javascript
 import React, {
   AppRegistry,
   Component,
 } from 'react-native';

 import PusherChatApp from './src/app'

 AppRegistry.registerComponent('PusherChat', () => PusherChatApp);
 

Conclusion

This is just a basic app and to hack away in React-Native and Redux I'll advice you start from  Code Cartoons.

You can find the complete project on GitHub.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment