Using Action Cable for Private Messaging & Presence Indicators on React/Rails app

Alberto Carreras
Jan 11 · 7 min read

This article builds on Dakota Lillie’s great article about how to implement Action Cable with React.

Private live messaging is a cool feature for any app. Adding a visual indicator that notifies when other users are currently connected might also be a great feature for your project. However, these features require that your client gets notifications without submitting HTTP requests; therefore, there must be a permanent connection between your client and the server. Here is were Websockets come to the rescue— for more information about Websockets, read the abovementioned article.

In one of my first projects — SUPP, a web app for connecting users based on proximity and common interests — building a live private messaging was indispensable for the MVP and also wanted to add live indicators informing when a user was connected to facilitate immediate communications.

If this is your first approach to Websockets, I recommend stopping here and reading Dakota Lillie’s article on how to set up the basic WebSocket infrastructure for an open chat in Rails. You can also read Action Cable’s documentation.

In the following post, we will create more documentation on how successfully use Action Cable for private messaging and presence indicators on React (with React-Redux) & Rails apps. Action Cable is the native built-in implementation of WebSockets in Rails. For facilitating the implementation of the Action Cable functionality into our React front-end, we will use the React-actioncable-provider module.

We will discuss Action Cable solutions for creating private chat rooms between users and presence indicators. Finally, we will propose a structured way of implementing Action Cable in your React application using “cable components”.

1. Action Cable and Authenticated Users

The first consideration to have when implementing Action Cable in Rails is what authentication solution we will be using. In the documentation, the 3.1.1 Connection Setup section details how to create a WebSocket connection and identify the user that is requesting a connection.

# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user

def connect
self.current_user = find_verified_user
end

private
def find_verified_user
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end

This current_user is necessary when you want each user to stream_from private channels instead of from open/public channels like conversation_channel or chat_#{params[:room]}. The section 6.2 Example 2: Receiving New Web Notifications on the documentation shows you an example of a private channel that only the connected user can stream from or listen to.

# app/channels/web_notifications_channel.rb
class WebNotificationsChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end

Many users can subscribe to any public channels (chat rooms) and stream from them. Now, subscribing to private channels for each authenticated user allows that new conversations and messages can be broadcasted to and, therefore, received by only the authenticated user itself.

However, at this point, you might be wondering how to find_verified_user using cookies. When I set up my Rails backend, I implemented an authentication system using used JWT tokens instead of cookies. The section 8.3 Notes from the documentation says:

The WebSocket server doesn’t have access to the session, but it has access to the cookies. This can be used when you need to handle authentication. You can see one way of doing that with Devise in this article.

A great solution is to use HTTP-Only cookies for passing the JWT token between the client and server. Follow the following implementation of JWT-tokens in Rails + React (post by Sophie DeBenedetto).

If you want to implement Devise as well -unfortunately, the article link is broken- you can find many tutorials like this one. Devise, as its documentation explains, is a flexible authentication solution for Rails based on Warden, which is a general Rack authentication framework. This means that under the hood Devise uses Warden for authentication. Devise tries to get the currently logged-in user using env['warden'].user and, if it is not found, it returns reject_unauthorized_connection which prohibits broadcasting.

Therefore, the connection.rb file uses warden like this:

# app/channels/application_cable/connection.rbmodule ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user

def connect
self.current_user = find_verified_user
end

private
def find_verified_user
if verified_user = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
end

[NOTE] If you want to implement Action Cable in an existing app so you can play with it, you have already implemented other JWT-authentication solutions and don’t want to upgrade your code, you can always follow one of the following quick fixes.

  1. If your JWT token is saved in localStorage, generate and pass a cookie from the frontend right after login/signing in. You can use the following function to pick up the JWT token from localStorage and save it as a cookie.
saveTokenAsCookie() {   document.cookie = 'X-Authorization=' + this.getToken() + '; path=/';}

This cookie will be available when establishing the WebSocket connection in the server. To access the JWT token from it, the connection file should look like this:

# app/channels/application_cable/connection.rbdef find_verified_user
if current_user = User.find_by(id: JWT.decode(cookies["X- Authorization"],"", false)[0]['sub'])
current_user
else
reject_unauthorized_connection
end
end

This fix 1) has security issues and 2) works in local but might incur in cross-domain issues in deployment as well.

2. Pass the userIdwhen connecting to the WebSocket URL.

<ActionCableProvider url={API_WS_ROOT+`?user=${this.props.userId}`}>

On the server side, the connection file should look like this:

# app/channels/application_cable/connection.rbdef find_verified_user
if current_user = User.find_by(id: request.params[:user])
current_user
else
reject_unauthorized_connection
end
end

This fix has security issues as well.

2. Rails Database Schema

For the following example, the Rails database follows a schema where

  • a user has many conversations through user_conversations
  • a user has many messages
  • a conversation has many (2) users through user_conversations
  • a conversation has many messages
  • a message belongs to a conversation and a user
  • a user_conversation belongs to a conversation and a user

3. Private messaging

Now that we have the authenticated user and the database structure, we can focus on building the websockets for private chats between two users.

  • [On Rails] Set up the conversation channel.

If you followed the Dakota Lillie’s article for creating the conversations channel, now change it as follows:

class ConversationsChannel < ApplicationCable::Channel
def subscribed
#createing a generic channel where all users connect
# stream_from "conversations_channel"

# creating a private channel for each user
stream_from "current_user_#{current_user.id}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
end

Here, current_user.id is provided by connection.rb in the line identified_by :current_user.

  • [On React] Post your new conversations.

When you post a new conversation, make sure you include the user_id from the sender and receiver users. For instance, if you click on a nickname or avatar button to start a conversation with that specific user, a handleClick() function could look something like this:

//StartChatButton.jsfunction fetchToWebsocket(route, bodyData) {
fetch(`${API_ROOT}/${route}`, {
method: 'POST',
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
// Only, if we are saving JWT token in localStorage
"Authorization": `Bearer ${localStorage.getItem("token")}`
// },
body: JSON.stringify(bodyData)
})
}
function handleClick() {
let body = {
title: "PRIVATE",
sender_id: props.user_sender_id,
receiver_id: props.user_receiver_id
};

// If the conversation already exists, execute exit function or do nothing. Otherwise, fetch conversation to WebSockets.
if (conversationExists(props.user_receiver_id)) {
props.exit();
}
else {
fetchToWebsocket("conversations", body);
props.exit();
}
};

The body variable contains both users participating in the conversation. The variable will be used in the server as params to create a new conversation belonging to both users.

Check if a conversation with the same receiver user already exists, so you don’t duplicate conversations. The example code calls a helper function conversationExists(props.user_receiver_id) which will check if there is another existing conversation with the same user in store.

  • [On Rails] Create new conversations and broadcast them.

On the conversations_controllerfile create a new Conversation and two new UserConversations, one for each user on the conversation. Serialize the conversation so it includes each of the users’ information. Then, broadcast the conversation to the sender and receiver private channel.

  • [On React]. Finally, listen to a private conversation channel with a “cable component”.

react-actioncable-provider provides an ActionCable component which listens to the conversation channel for broadcasted data. Upon reception of a new conversation, the function handleReceivedConversation double checks that the user is one of the users in the conversation before appending the conversation using the appendNewConversation action.

4. Presence indicators

  • [On Rails] Set up the presence channel.

This time, the presence channel will be a unique, static channel. However, upon subscription, the user’s active state will change to true, and upon unsubscription to false. Those changes will be broadcasted to all subscribers to the presence_chanel. When requesting nearby users to the server API, the users will contain presence data (true/false). Now, with this user key-values, you can implement visual indicators that change when a user (dis)connects.

  • [On React]. Finally, listen to the presence channel with a “cable component”.

react-actioncable-provider provides an ActionCable component which listens to the presence channel for broadcasted data. Upon reception of a new (dis)connected user, the function handleReceivedActiveUser checks for the type of action -DO (disconnected) or CO (connected), finds the user by id, and change the presence from true to false or vice-versa.

Where to place the cable components

Finally, place the cable components (presenceCable and conversationsCable) inside a component that will only render once after login/signing in. In case of placing the cable component inside a component that gets rendered more than once, the handleReceived() functions will execute as many times as the component is rendered.

//HomeContainer.jsconst HomeContainer = (props) => {return (
<div className="home-container">
<PresenceCable />
<ConversationsCables />
<Other components />
</div>
);
};

NOTE on WebSocket deployment

For Websocket deployment, read the following article Real-Time Rails: Implementing WebSockets in Rails 5 with Action Cable (by Sophie DeBenedetto).

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade