Hello! Finally, I have finished my final capstone project at Flatiron School! I have learned a lot in my time with Flatiron, I am proud of some of my work, but some of it I’m not so proud of. Particularly, my Phase 2 React project where I tasked myself to create a chat application. I had not learned any backend at that point, my knowledge of React was very basic, so my project did not work how I wanted it too. A user could send a message, but another user would have to refresh and re-send a GET request to see the message. It became more of a forum app, which was fine… but not what I wanted.
I not only wanted to redeem my idea, but give it a theme, make something my friends would use.
I introduce,
The requirements for this project involves implementing 4 models, and include a many to many relationship. My 4 models are User, Character, Campaign, and Message.
SQL diagram:
The many to many relationships I have here are campaign and user through character. User has many characters, has many campaigns through characters Campaign has many characters, and has many users through characters.
Full CRUD was implemented on the character model, so a user has full capability to create, read, update, and destroy character objects.
On the front end side, I created 7 client-side routes
“/” - home
“/campaigns” - list of all campaigns.
“/campaigns/id/” - Campaign show page, where the chatroom is.
“/campaigns/new” - Form to create a new campaign.
“/mycharacters” - List of users existing characters.
“/signup” - Sign up page
“/login” - Login page
I had to make sure there was password protection, authentication, validations and error handling. And I had to implement something that was not taught in the curriculum. The whole point of this project was to be able to have a living chatroom, the reason my last chat application didn’t work is because I was never taught about web sockets. There are numerous socket frameworks out there I could use, but I decided to dive into Action Cable.
What is Action Cable?
Action Cable is a WebSocket framework for rails applications. It allows for real-time features to be implemented, and provides bi-directional communication between client and server.
The way it works is you have a connection object that is instantiated each time a WebSocket is accepted by the server. The client of the WebSocket connection is called a consumer. The consumer can subscribe to multiple channels, which is the logical unit of work. Consumers who are subscribed to a channel are known as ‘subscribers’. Streams are then provided to channel the routes that broadcasts to their subscribers.
Let me show you how I implemented it:
I first needed to generate a channel. You can do some in rails like so
Rails g channel
The channel I generated is CampaignChannel.
First, let's make the connection.
Within channels/application_cable/connection.rb:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = @current_user
end
end
end
Identified_by is what designates the connection identifier. In my application_controller I had declared a variable for current_user in my authorize method I had used to designate my identifier.
def authorize
@current_user = User.find_by(id: session[:user_id])
render json: { errors: ["Not authorized"] }, status: :unauthorized unless @current_user
end
Now in my channels/campaign_channel.rb I needed to add some logic; where do the subscribers stream from, and what the channel broadcasts when data is being received?
class CampaignChannel < ApplicationCable::Channel
def subscribed
stream_from "campaign_#{params[:campaign_id]}"
end
def unsubscribed
stop_all_streams
end
def receive(data)
ActionCable.server.broadcast("campaign_#{params[:campaign_id]}", data)
end
end
When a consumer is subscribed, it is streaming through the CampaignChannel by the campaigns id. So when the user enters the campaign with an id 1, the subscriber will be streaming through ‘campaign_1’. When data is being received from a subscriber, action cable will broadcast that data through the specified subscribed channel.
Now for the front end side of things, how do I get this to actually work?
First I needed to make the connection, and create a consumer. The way I went about this, is using reacts ‘useContext’ hook. I created a new file called cable.js within my contexts directory and added the following logic:
import React from "react";
import ActionCable from "actioncable";
const CableContext = React.createContext();
function CableProvider({ children }) {
const actionCableUrl = process.env.NODE_ENV === 'production' ? 'wss://roleplay-chat.onrender.com.com/cable' : 'ws://localhost:3000/cable'
const CableApp = {}
CableApp.cable = ActionCable.createConsumer(actionCableUrl)
return <CableContext.Provider value={CableApp}>{children}</CableContext.Provider>;
}
export { CableContext, CableProvider };
Then in my index.js I imported this file as CableProvider, and wrapped it around my app folder.
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from "react-router-dom";
import { UserProvider } from './contexts/UserContext'
import { CableProvider } from './contexts/cable';
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<CableProvider>
<UserProvider>
<App />
</UserProvider>
</CableProvider>
</BrowserRouter>
);
And finally within my CurrentCampaign component I imported cableContext (my consumer) and created a new channel for the consumer to subscribe to.
const newChannel = cableContext.cable.subscriptions.create({
channel: "CampaignChannel",
campaign_id: campaign.id
},
{
received: (data) => setMessages([...messages, data]),
connected() {
console.log("Connected to CampaignChannel, room: " + campaign.id)
}
})
Now what’s going to happen is when a user sends a message, a POST request will be made to save that message to the database, and the channel will stream the data through the WebSocket.
function sendSocketData(data) {
newChannel.send({
id: data.id,
key: data.id,
body: data.body,
character: playerCharacter
})
}
const handleSubmit = async (e) => {
e.preventDefault();
e.target.message.value = "";
await fetch("/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
key: message.id,
body: message,
campaign_id: campaign.id,
character_id: playerCharacter.id,
}),
}).then((r) => {
if (r.ok) {
r.json().then((data) => sendSocketData(data))
} else {
r.json().then((err) => setErrors(err.errors))
}
});
};
That’s about it. For those interested, here is the link to the repository:
https://github.com/fusion407/Roleplay-chat
I had a great time working on this project, although I found action cable a bit complicated and tedious to learn, it was a very satisfying outcome. Happy coding!
Top comments (0)