One of the fastest ways to build applications has always been Ruby on Rails. Ruby on Rails is a feature-packed web development framework that makes it easy for novices and experts alike to easily stand up an application. When you want to add chat to your application, using Stream Chat makes that task just as easy, and it's quick to do!
In this tutorial, I will describe how to add chat to a React Native application powered by a Ruby on Rails backend.
As always, the full code can be found on GitHub.
Prerequisites
To be successful in building a chat app using Ruby on Rails, you'll want to be sure you have the following before proceeding:
- A Stream account. Visit the Stream Chat Website to get started!
- Ruby
Setting Up the Project
In this tutorial, we'll be building a server (frontend) and a client (backend). You'll need to create a directory to house both of these sections and then cd
into it to install the required dependencies for each of them. All of this can be accomplished using the following commands:
$ mkdir rails-streamchat $ cd rails-streamchat $ npx create-react-app client $ gem install rails $ rails new server --api
Building the Server
The server will expose only one endpoint: /users
. This endpoint will accept a username
and password
which will be validated against the database. If the username doesn’t exist in the database, it regards it as a new user and adds it. As a final step, it connects to Stream Chat and generates a token
that will be used client-side.
The first step is to set up the required files and add dependencies such as bcrypt
and stream-chat
SDK. You can do that by running the following code:
$ cd server $ rake db:create $ rails generate model user moniker:string password:digest` # If rake generate takes long, generated stubs are the problem # You can fix it by "rm -rf bin/ && rake app:update:bin" $ rake db:migrate $ bundle add stream-chat-ruby dotenv-rails bcrypt $ rails generate controller Users
Once everything above succeeds, the next step is to update the generated controller to include the logic for authenticating and creating users. You will need to update the contents of the file located at app/controllers/users_controller.rb
with the following code:
# frozen_string_literal: true require 'stream-chat' class UsersController < ApplicationController def create user = User.find_by(moniker: user_params[:moniker]) if user.nil? user = User.create(user_params) if user.valid? user.save render json: { status: true, user: user, token: chat_token(user.moniker) } return end render json: { status: false, message: 'Could not create an account for the user' } return end unless user.authenticate(user_params[:password]) render json: { status: false, message: 'Invalid password provided' } return end render json: { status: true, user: user, token: chat_token(user.moniker) } end private def chat_token(username) client = StreamChat::Client.new(api_key = Rails.configuration.stream_api_key, api_secret = Rails.configuration.stream_api_secret) token = client.create_token(username) client.update_user({ id: username, name: username }) chan = client.channel('messaging', channel_id: 'rails-chat') chan.create('admin') chan.add_members(['admin', username]) token rescue StandardError => e p e '' end def user_params params.require(:user).permit(:moniker, :password) end end
The next step is to add validation requirements to the User
class so that we are certain only safe data is written to the database. This can be done by updating app/models/user.rb
with the following:
class User < ApplicationRecord has_secure_password validates :moniker, presence: true, uniqueness: true validates :password_digest, presence: true end
The next step will be to connect the controller to a route. Rails has a file that handles all routings, located at config/routes.rb
. That file needs to be updated to contain the following:
# frozen_string_literal: true Rails.application.routes.draw do post '/users', to: 'users#create' end
As a final step, you will need to handle environment variables to safely pass the API key and secret from Stream Chat into the app. If you notice, we already installed a dotenv
gem that is going to help read a .env
file and populate the variables available to the app. Create a .env
file by running:
1$ touch .env
In the newly created .env
file, add the following content:
API_KEY=your_API_KEY
API_SECRET=your_API_SECRET
Please remember to replace "your_API_KEY" and "your_API_SECRET" with the values from your Stream Dashboard.
Once this has been created and populated, you will need to load the .env
file when the app boots up. We can ensure this by editing config/application.rb
to contain the following:
# frozen_string_literal: true require_relative 'boot' require 'rails' require 'dotenv' # Pick the frameworks you want: require 'active_model/railtie' require 'active_job/railtie' require 'active_record/railtie' require 'active_storage/engine' require 'action_controller/railtie' require 'action_mailer/railtie' require 'action_mailbox/engine' require 'action_text/engine' require 'action_view/railtie' require 'action_cable/engine' # require "sprockets/railtie" require 'rails/test_unit/railtie' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) Dotenv.load module Server class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 6.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers # -- all .rb files in that directory are automatically loaded after loading # the framework and any gems in your application. # Only loads a smaller set of middleware suitable for API only apps. # Middleware like session, flash, cookies can be added back manually. # Skip views, helpers and assets when generating a new resource. config.api_only = true config.stream_api_key = ENV['API_KEY'] config.stream_api_secret = ENV['API_SECRET'] end end
That's it! You can now start the server by running the following command:
1$ rails server
Building the Client
The first step to building our front end is to add a few more required dependencies and files to the client application. That can be done using the following commands:
$ yarn add axios @react-native-community/netinfo react-native-document-picker react-native-image-picker $ yarn add stream-chat stream-chat-react-native $ touch Auth.js Chat.js $ cd ios $ pod install
In the newly created Chat.js
file, paste the following:
import React, {Component} from 'react'; import {View} from 'react-native'; import { Chat, Channel, MessageList, MessageInput, } from 'stream-chat-react-native'; export default class ChatView extends Component { render() { const channel = this.props.chatClient.channel('messaging', 'rails-chat'); channel.watch(); return ( <Chat client={this.props.chatClient}> <Channel channel={channel}> <View style={{display: 'flex', height: '100%'}}> <MessageList /> <MessageInput /> </View> </Channel> </Chat> ); } }
The next step is to update Auth.js
to contain the logic for user authentication. That can be done by amending it with the following code:
import React, {Component} from 'react'; import {Button, TextInput, View, StyleSheet} from 'react-native'; export default class Auth extends Component { constructor(props) { super(props); this.state = { moniker: '', password: '', }; } render() { return ( <View style={styles.container}> <TextInput value={this.state.moniker} onChangeText={moniker => this.setState({moniker: moniker.trim()})} placeholder={'Moniker'} style={styles.input} /> <TextInput value={this.state.password} onChangeText={password => this.setState({password: password.trim()})} placeholder={'Password'} style={styles.input} secureTextEntry={true} /> <Button title={'Login'} style={styles.input} onPress={() => { this.props.cb(this.state); }} /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', }, input: { width: 200, height: 44, padding: 10, borderWidth: 1, borderColor: 'black', marginBottom: 10, }, });
The final step will be to update App.js
to contain the code below:
import React, {Component} from 'react'; import {Alert, StyleSheet, View, SafeAreaView} from 'react-native'; import {StreamChat} from 'stream-chat'; import Auth from './Auth'; import Chat from './Chat'; import axios from 'axios'; export default class App extends Component { constructor(props) { super(props); this.state = { isAuthenticated: false, id: '', }; this.chatClient = new StreamChat('API_KEY'); } onLoginCallBack = user => { if (user.moniker.length === 0) { Alert.alert('Login', 'Please provide your moniker'); return; } if (user.password.length === 0) { Alert.alert('Login', 'Please provide your password'); return; } const data = { user: { ...user, }, }; axios .post('http://localhost:3000/users', data) .then(res => { console.log(res.data); if (res.data.token === '') { Alert.alert('Login', 'Error occurred while authenticating you'); return; } this.chatClient.setUser( { id: res.data.user.moniker, username: res.data.user.moniker, image: 'https://stepupandlive.files.wordpress.com/2014/09/3d-animated-frog-image.jpg', }, res.data.token, ); this.setState({ isAuthenticated: true, id: res.data.user._id, }); }) .catch(err => { console.log(err); Alert.alert('Login', 'Could not log you in'); }); }; render() { return ( <SafeAreaView style={{flex: 1}}> <View style={{flex: 1}}> {!this.state.isAuthenticated || this.state.currentUser === null ? ( <View style={styles.container}> <Auth cb={this.onLoginCallBack} /> </View> ) : ( <View style={[{flex: 1}]}> <Chat userID={this.state.id} chatClient={this.chatClient} /> </View> )} </View> </SafeAreaView> ); } } const styles = StyleSheet.create({ container: { flex: 1, }, });
Our front end is now ready! To run the app, execute the following command:
1$ yarn run ios
Wrapping Up
In this tutorial, I have described how to add a chat interface to your Ruby on Rails app using Stream Chat. This can be taken further by implementing a social network with Rails that supports direct messaging; checkout the Stream Chat docs to learn about all the awesome updates you can make to your application.
Thanks for reading, and happy coding!