Skip to content

Instantly share code, notes, and snippets.

@ponty96
Last active May 17, 2016 09:34
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/2282aa6ed6bcadd8cfe31382911f92b0 to your computer and use it in GitHub Desktop.
Save ponty96/2282aa6ed6bcadd8cfe31382911f92b0 to your computer and use it in GitHub Desktop.
Tutorial Draft

In this tutorial we will be implementing a technological stack have been playing with. We are gonna build a simple chat application with the very popular react native + redux and a realtime communication platform Pusher.

React native makes it easy to build mobile apps with native UI and high performance by just wrapping Javascript codes over native modules, you can read more here and Pusher allows you to send messages realtime via their infrastructure to as many listeners as possible;

I would like to assume you have react native set up already on your computer and that you have created an app on Pusher already.Thou its simple, just create an account here and from there create an app

So enough talk, lets get started

$ cd into_working_directory

$ react-native init PusherChatApp

$ cd PusherChatApp

Update your package.json to

{
  "name": "PusherChat",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start"
  },
  "dependencies": {
    "axios": "^0.11.0",
    "moment": "^2.13.0",
    "pusher-js": "git+https://github.com/pusher-community/pusher-websocket-js-iso.git",
    "react": "^0.14.8",
    "react-native": "^0.24.1",
    "react-native-keyboard-spacer": "^0.1.6",
    "react-native-router-flux": "^3.22.23",
    "react-native-swipeout": "^2.0.12",
    "react-redux": "^4.4.5",
    "redux": "^3.5.2",
    "redux-logger": "^2.6.1",
    "redux-thunk": "^2.0.1"
  }
}

$ npm install

Since we will be using redux lets go ahead and create the neccessary folders as seen below

screen shot 2016-05-12 at 12 58 49 pm

At the end of the tutorial we want to have two screens as such:

Coversations Screen:

Conversations Screen

Coversation Screen:

Conversation Screen

So lets go ahead and write some codes, just go ahead and edit the following files:

index.ios.js && index.android.js
import React, {
  AppRegistry,
  Component,
  StyleSheet,
  Text,
  View
} from 'react-native';

import PusherChatApp from './src/app'

AppRegistry.registerComponent('PusherChat', () => PusherChatApp);
src/app.js
import React, { Component, StyleSheet, Dimensions} from 'react-native';
import { Provider } from 'react-redux'
import configureStore from './store/configureStore'

const store = configureStore();

import {Actions, Scene, Router, TabBar,  Modal} from 'react-native-router-flux';
import SplashScreen from './screens/splash-screen'
import ConversationsScreen from './screens/conversations-screen';
import ConversationScreen from './screens/conversation-screen';

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: "#fff",
    },
    tabBarStyle: {
        flex: 1,
        flexDirection: "row",
        backgroundColor: "#95a5a6",
        padding: 0,
        height: 45
    },
    sceneStyle: {
        flex: 1,
        backgroundColor: "#fff",
        flexDirection: "column",
        paddingTop:20
    }
})
export default class PusherChatApp extends Component {
    render() {
        return (
            <Provider store={store}>
            <Router style={styles.container} sceneStyle={styles.sceneStyle}>
                <Scene key="root">
                    <Scene key="conversations_screen" component={ConversationsScreen} hideNavBar={true}/>
                    <Scene key="splash_screen" component={SplashScreen} hideNavBar={true}/>
                    <Scene key="conversation_screen" component={ConversationScreen} hideNavBar={true} />
                </Scene>
            </Router>
                </Provider>
        )

    }
}

So lets start by creating our Conversations screen component; So how this work? When the Component is mounted it dispatches an action to get all the chats stored on the devices AsyncStorage (one could also ajax load all the messages that have been sent to the user since when he was offline) and the chats are passed as props to the Component.

The component uses react-natives in built ListView component to render the list of conversations a user has.

Also in this component we can start a new conversation with a user by clicking an add button that opens a Modal which contains a form to add the username and a start converation message

src/screens/conversations-screen.js
import React, {
    Component,
    View,
    Text,
    StyleSheet,
    Image,
    ListView,
    TouchableHighlight,
    Modal,
    TextInput,
    AsyncStorage
} from 'react-native';

import Button from './../components/button/button'
import { Actions } from 'react-native-router-flux'
import _ from 'lodash';
import {connect} from 'react-redux';
import { startConvo, apiGetChats, newMesage } from './../actions/';
import moment from 'moment'

const styles = StyleSheet.create({
    container: {
        flex: 1
    },
    main_text: {
        fontSize: 16,
        textAlign: "center",
        alignSelf: "center",
        color: "#42C0FB",
        marginLeft: 5
    },
    row: {
        flexDirection: "row",
        justifyContent: "space-between",
        borderBottomWidth: 1,
        borderBottomColor: "#42C0FB",
        marginBottom: 10
    },

    listRow: {
        flexDirection: "row",
        paddingLeft: 10,
        paddingRight: 10,
        marginBottom: 12
    },
    column: {
        flex: 1,
        flexDirection: "column",
        marginLeft: 8,
        borderBottomWidth: 1,
        borderBottomColor: "#42C0FB"
    },
    headerImg: {
        height: 40,
        width: 40
    },
    dp: {
        height: 50,
        width: 50,
        borderRadius: 25
    },
    last_msg: {
        color: "#666",
        fontSize: 13,
        marginTop: 5
    },
    time: {
        color: "#42C0FB",
        fontSize: 12
    },
    innerRow: {
        flexDirection: "row",
        justifyContent: "space-between"
    },
    add_text: {
        fontSize: 16,
        textAlign: "right",
        alignSelf: "center",
        color: "#42C0FB",
        marginRight: 10
    },
    modalContainer: {
        backgroundColor: '#42C0FB',
        flex: 1,
        flexDirection: "column",
        paddingTop: 30
    },
    rowRight: {
        flexDirection: "row",
        justifyContent: "flex-end",
        borderBottomWidth: 1,
        borderBottomColor: "#42C0FB",
        marginBottom: 10
    },
    close_btn: {
        alignSelf: "flex-end",
        borderWidth: 1,
        borderColor: "#fff",
        width: 50,
        height: 30,
        justifyContent: "center",
        marginRight: 15,
    },
    input:{
        borderWidth: 1,
        borderColor: "#fff",
        height:50,
        marginBottom:10,
        marginLeft:8,
        marginRight:8,
        paddingLeft:5
    },
    text: {
        textAlign: "center",
        color:"#fff"
    },
    submit_btn: {
        alignSelf: "center",
        borderWidth: 1,
        borderColor: "#fff",
        width: 100,
        height: 40,
        justifyContent: "center",
        marginRight: 15,
    }
});

const username = 'ponty96';

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

class ConversationsScreen extends Component {
    constructor(props) {
        super(props);
        const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 != r2});
        this.state = {
            conversations: ds,
            modalVisible: false,
            contact_name: "",
            new_contact_message: ""
        }
    }

    componentDidMount(){
        const {dispatch, Chats} = this.props;
        const process_status =  Chats.process_status;

        if(process_status != "isFetching"){
            dispatch(apiGetChats())
        }
        // start pusher websocket client to listen to new websocket events
        newMesage(dispatch)
    }
    componentWillReceiveProps(nextProps){
        const {dispatch, Chats} = nextProps;
        const process_status =  Chats.process_status;
        if(process_status === "completed"){
            // sorting convos by time
            let chats = Chats.chats;
            chats.sort((a,b)=>{
                return moment(b.sent_at).valueOf() - moment(a.sent_at).valueOf();
            });
            const convos = _.uniq(chats, 'convo_id');

            this.setState({
                conversations: this.state.conversations.cloneWithRows(convos)
            })
        }
    }

    closeModal = () => {
        this.setState({modalVisible: false})
    }
    openModal = () => {
        this.setState({modalVisible: true})
    }

    addContact = (name,message) => {
        const {dispatch, Chats} = this.props;
        dispatch(startConvo(name,message));
        this.closeModal();
    }

    renderModal = () => {
        // function that returns a modal view for the user to start a new conversation
        const animated = true;
        const transparent = false;

        return (
            <Modal animated={animated} transparent={transparent} visible={this.state.modalVisible}
                   onRequestClose={() => {this.closeModal()}}>
                <View style={styles.modalContainer}>
                    <View style={styles.rowRight}>
                        <Button
                            style={styles.close_btn}
                            onPress={this.closeModal}>
                            <Text style={styles.text}>Close</Text>
                        </Button>
                    </View>
                    <TextInput
                        style={styles.input}
                        placeholder="Contact Name"
                        onChangeText={(text) =>  this.setState({contact_name:text})}/>

                    <TextInput
                        style={styles.input}
                        placeholder="Say Hello to Contact"
                        onChangeText={(text) =>  this.setState({new_contact_message:text})}/>

                    <Button
                        style={styles.submit_btn}
                        onPress={() => this.addContact(this.state.contact_name, this.state.new_contact_message)}>
                        <Text style={styles.text}>Submit</Text>
                    </Button>
                </View>
            </Modal>
        )
    }
    renderRow = (rowData) => {
        // this function returns the view for each row data in the list view
        return (
            <Button onPress={() =>  Actions.conversation_screen({convo_id:rowData.convo_id})}>
                <View style={styles.listRow}>
                    <Image source={{uri:username != rowData.sender ? rowData.sender_dp : rowData.receiver_dp}}
                           style={styles.dp}/>
                    <View style={styles.column}>
                        <View style={styles.innerRow}>
                            <Text>{ username != rowData.sender ? rowData.sender : rowData.receiver}</Text>
                            <Text style={styles.time}>{rowData.time}</Text>
                        </View>
                        <Text style={styles.last_msg}>{rowData.message}</Text>
                    </View>
                </View>
            </Button>
        )
    }
    render() {
        const modalVisible = this.state.modalVisible;
        return (
            <View style={styles.container}>
                <View style={styles.row}>
                    <Image source={require('./../assets/icon.png')} style={styles.headerImg}/>
                    <Text style={styles.main_text}>Conversations</Text>
                    <TouchableHighlight
                        style={{marginTop:10}}
                        onPress={this.openModal}
                        underlayColor="transparent">
                        <Text style={styles.add_text}>Add</Text>
                    </TouchableHighlight>
                </View>
                <View>
                    { modalVisible ? this.renderModal() : <View></View>}
                </View>
                <ListView
                    renderRow={this.renderRow}
                    dataSource={this.state.conversations}/>
            </View>
        )
    }
}
export default connect(mapStateToProps)(ConversationsScreen)

Now we create our Conversation Screen where all the chat is done, when a row is clicked in the Conversations Screen component we dispatch an Action to navigate to our Conversation Screen where we pass the convo_id as a props.

The Converation Screen is also connected to redux and when its mounted it filters all the chat by the convod_id, sorts them based on time and renders them via the ListView component. It also contains a TextInput Component and a button to send the message which dispatches an action when clicked.

src/screens/conversation-screen.js
import React, { Component, View, Text, StyleSheet, Image, ListView, TextInput, Dimensions} from 'react-native';
import Button from './../components/button/button';
import { Actions } from 'react-native-router-flux';
import KeyboardSpacer from 'react-native-keyboard-spacer';
import { connect } from 'react-redux';
import _ from 'lodash';
import moment from 'moment';
import { apiSendChat, newMesage } from './../actions/';


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

const styles = StyleSheet.create({
    container: {
        flex: 1
    },
    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:"row",
        justifyContent:"space-between"
    },
    textInput:{
        height:50,
        width:(width * 0.85),
        color:"#e8e8e8",
    },
    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:""
        }
    }

    componentDidMount(){
        const {dispatch, Chats} = this.props;
        const process_status =  Chats.process_status;
        const convo_id  = this.props.convo_id;

        if(process_status === "completed"){
            const convos = Chats.chats.filter((val) => {return val.convo_id == convo_id});

            convos.sort((a,b)=>{
                return moment(a.sent_at).valueOf() - moment(b.sent_at).valueOf();
            });
            this.setState({
                conversation: this.state.conversation.cloneWithRows(convos)
            })
        }
    }
    componentWillReceiveProps(nextProps) {
        const {dispatch, Chats} = nextProps;
        const process_status =  Chats.process_status;
        const convo_id  = this.props.convo_id;

        if(process_status === "completed"){
            const convos = Chats.chats.filter((val) => {return val.convo_id == convo_id})
            convos.sort((a,b)=>{
                return moment(a.sent_at).valueOf() - moment(b.sent_at).valueOf();
            });
            this.setState({
                conversation: this.state.conversation.cloneWithRows(convos)
            })
        }

    }

    renderSenderUserBlock(data){
        return (
            <View style={styles.messageBlockRight}>
                <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.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 = () => {
        const convo_id  = this.props.convo_id;
        const receiver = convo_id.substr(0, convo_id.indexOf(username));
        const message = this.state.text;

        const {dispatch, Chats} = this.props;
        dispatch(apiSendChat(receiver,message))

    }

    render() {
        const convo_id  = this.props.convo_id;
        return (
            <View style={styles.container}>
                <View style={styles.row}>
                    <Button
                        style={styles.back_btn}
                        onPress={() => Actions.pop()}>
                        <Image source={require('./../assets/back_chevron.png')} style={styles.back_img}/>
                    </Button>
                    <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}>{ convo_id.substr(0, convo_id.indexOf(username)) }</Text>
                    </View>
                </View>

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

                <View style={styles.input}>
                    <TextInput
                        style={styles.textInput}
                        onChangeText={(text) => this.setState({text:text})}
                        placeholder="Type a message"/>
                    <Button
                        onPress={this.sendMessage}>
                        <Image source={require('./../assets/phone.png')} style={styles.msgAction}/>
                    </Button>
                </View>
                <KeyboardSpacer/>
            </View>
        )
    }


}

export default connect(mapStateToProps)(ConversationScreen)

lets create out redux actions

src/actions/index.js
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 GET_CONVERSATION = "GET_CONVERSATION";
export const IS_FETCHING = "IS_FETCHING";
export const IS_LOADING = "IS_LOADING";
export const IS_ERROR = "IS_ERROR";
export const NEW_MESSAGE = "NEW_MESSAGE";

const add_to_storage = (data) => {
    AsyncStorage.setItem(data.convo_id+data.sent_at, JSON.stringify(data), () => {})
}

const sendChat = (payload) => {
    return {
        type:SEND_CHAT,
        payload:payload
    };
};

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

const newChat = (payload) => {
    return {
        type:NEW_MESSAGE,
        payload:payload
    };
};

const getConv = (payload) => {
    return {
        type:GET_CONVERSATION,
        payload:payload
    };
};

function isLoading(){
    return {
        type:IS_LOADING
    };
};

function isFetching(){
    return {
        type:IS_FETCHING
    };
};

function isError(){
    return {
        type:IS_ERROR
    };
};

const genConvoId = (sender,receiver) =>{
    if(sender < receiver){
        return sender+receiver
    }
    return receiver+sender
}

export function newMesage(dispatch){
    var socket = new Pusher("3c01f41582a45afcd689");
    const channel = socket.subscribe('chat_channel');
    channel.bind('new-message',
        (data) => {
            add_to_storage(data.chat);
            dispatch(newChat(data.chat))
        }
    );
}

export function startConvo(receiver,message){
    const sent_at = moment().format();
    const sender = "ponty96";
    const convo_id = genConvoId(sender,receiver);
    const chat = {sender:sender, receiver:receiver, message:message, convo_id:convo_id,sent_at:sent_at};
    return dispatch => {
        dispatch(isLoading());
        return  axios.get(`http://localhost:5000/chat/${JSON.stringify(chat)}`).then(response =>{
        }).catch(response =>{
            dispatch(isError())
        });
    };
}
export function apiSendChat(receiver,message){
    const sent_at = moment().format();
    const sender = "ponty96";
    const convo_id = genConvoId(sender,receiver);
    const chat = {sender:sender, receiver:receiver, message:message, convo_id:convo_id,sent_at:sent_at};
    return dispatch => {
        dispatch(isLoading());
        return  axios.get(`http://localhost:5000/chat/${JSON.stringify(chat)}`).then(response =>{
        }).catch(response =>{
            dispatch(isError())
        });
    };
};


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

    return dispatch => {
        dispatch(isFetching());
        return AsyncStorage.getAllKeys((err, keys) => {
            AsyncStorage.multiGet(keys, (err, stores) => {
                let 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))
            });
        });
    };
}

lets create our reducer

src/reducers/index.js
import { combineReducers } from 'redux';
import { SEND_CHAT, GET_ALL_CHATS, GET_CONVERSATION, IS_FETCHING, IS_LOADING, IS_ERROR, NEW_MESSAGE} from './../actions'

const Chats = (state = {process_status:"", chats:[]}, actions) => {
    switch(actions.type){
        case IS_LOADING:
            return Object.assign({}, state, {
                process_status:"isLoading"
            });

        case IS_FETCHING:
            return Object.assign({}, state, {
                process_status:"isFetching"
            });

        case GET_ALL_CHATS:
            return Object.assign({}, state, {
                process_status:"completed",
                chats:state.chats.concat(actions.payload)
            });

        case GET_CONVERSATION:
            return Object.assign({}, state, {
                process_status:"completed",
                chats:state.chats.concat(actions.payload)
            });

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

        default:
            return state;
    }
};

const rootReducer = combineReducers({
    Chats
})

export default rootReducer;

and finally our store

src/store/configureStore.js
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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment