Edit 2: as u/Pickford pointed out, another good idea could be to check out issues of the library on GitHub I just want to make a motivational post for my fellow Adders.
I really appreciate all the replies, and will check out all the tips and tricks you guys are referring to, I'm really overwhelmed by how nice and helpful you all are, thank you! I'm a self-taught, intermediate-level “developer” by my reckoning who can't seem to build anything meaningful.
I hit periods where I am struggling so much that I'm extremely frustrated and angry. I often have to reread a single paragraph from my book or whatever online course I am using for hours straight.
I really try to figure things out on my own before asking for help because I want to understand what's going on for myself but at times I have to give in and ask someone more experienced to explain. I just get frustrated when I can't understand the concepts despite a lot of effort.
I genuinely enjoy coding, but I think I hate the learning process. I become so frustrated that I often have to take a break due to mental fatigue and that creates inconsistencies in my schedule.
I graduated with the CIS degree in programming back in June. Rails out of the box doesn't help you much with that, so patterns emerge that help to organize things.
Here's some obvious patterns that most Rails apps have: Service Objects You initialize service object, and it has a single public method like run or execute.
It may change the state of the database, send emails, etc. Decorators I think the idea is that methods from this class are mixed into object that is passed in.
Honestly, I've been treating decorators like pseudo presenters that are not used in the views. It's clearly not a decorator pattern, but it's not really a presenter in a sense that it will provide outputs to be rendered on the screen.
99% of the time you'd just handle record validation and saving from the controller, but sometimes things get hairy, so you'd do something like this: It initializes with some inputs and then has a pile of public methods you can call.
There's a class that figures out “next work day”. I'm working with Rails apps for ages now, but organizing code is literally the hardest thing that I have to deal with.
I would like to share with you my first ruby gem (still not published). This gem is a Redis connection pool that accepts `idle_timeout` and kills any connection that has been idle for more than a specific period of time.
There are plenty tutorials online which show how to create your first app. This tutorial will go a step further and explain line-by-line how to create a more complex Ruby on Rails application.
I think it is entirely possible for a total beginner to complete this guide. But keep in mind that this tutorial covers some topics which are beyond the basics.
So if you are a total beginner, it’s going to be harder, because your learning curve is going to be pretty steep. I will provide links to resources where you could get some extra information about every new concept we touch.
I assume that you have already set up your basic Ruby on Rails development environment. At first, it was okay, but after some time I got tired of overcoming mystical obstacles which were caused by Windows.
I had to keep figuring out hack ways to make my applications work. Overcoming those obstacles didn’t give me any valuable skills or knowledge.
I was just spending my time by duct taping Windows 10 setup. I chose to use Vagrant to create a development environment and Putty to connect to a virtual machine.
It is a popular choice among Ruby on Rails community. If you haven’t created any Rails apps with PostgreSQL yet, you may want to check this tutorial.
Navigate to a newly created directory by running the command: Run this line in your command prompt to generate a new controller.
But usually I like to define the home page inside the PagesController. Which means that all methods defined inside these class are going to be available across all our controllers.
We’ll define a public method named index, so it can be callable as an action: Let’s define a route, so when we open our root page of the website, Rails knows which controller and its action to call.
As you remember an action is just a public method, so pages#index says “call the PagesController and its public method (action) index.” If we went to our root path http://localhost:3000, the index action would be called.
Inside this file we can write our regular HTML+ Embedded Ruby code. Just write something inside the file, so we could see the rendered template in the browser.
Now when we go to http://localhost:3000, we should see something like this instead of the default Rails information page. If you don’t already know, when we generate a new application, a new local git repository is initialized.
If you are still running the application, restart the Rails server to make sure that new gems are available. This is necessary in order to use Bootstrap library in Rails, also it allows us to use Sass features.
For the navigation bar we’ll use Bootstrap’s naval component as the starting point and then quite modify it. We will store our navigation bar inside a partial template.
We’re doing this because it’s better to keep every component of the app in separate files. It allows testing and manage app’s code much easier.
Also, we can reuse those components in other parts of the app, without duplicating the code. Now copy and paste naval component from Bootstrap docs and save the file.
Inside the file we see the following method: To quickly learn the differences between ERA syntax, checkout this Stack Overflow answer.
In Home page section we set the route to recognize the root URL. So whenever we send a GET request to go to a root page, PagesController‘index action gets called.
We should change the navigation bar’s name from Brand to coalfield. Because this method allows us to easily generate URI paths.
Open a command prompt and navigate to the project’s directory. Remember that whenever you don’t quite understand how a particular method works, just Google it, and you will probably find its documentation with an explanation.
Sometimes documentations are poorly written, so you might want to Google a little more, and you might find a blog or a Stack Overflow answer, which would help. In this case we pass a string as our first argument to add the element’s value, the second argument is needed for a path, this is where routes helps us to generate it.
In this case we needed to add navbar-brand class to keep our Bootstrap powered navigation bar to function. From what I know there aren’t any strong conventions on how to structure your style sheets in Rails.
We’ve just created a new git branch and automatically switched to it. The reason for doing this is that we can isolate our currently functional version (master branch) and write a new code inside a project’s copy, without being afraid to damage anything.
Once we are complete with the implementation, we can just merge changes to the master branch. App/assets/stylesheets/partials/layout/navigation.Sopwith these lines of code we change naval’s background and links color.
As you may have noticed, a selector is nested inside another declaration block. !important is used to strictly override default Bootstraps styles.
The last thing which you may have noticed is that instead of a color name, we use a Sass variable. The reason for this is that we are going to use this color multiple times across the app.
App/assets/stylesheets/application.scythe reason for importing variables.scss file at the top is to make sure that the variables are defined before we use them. We want to make sure that the navigation bar is always visible, even when we scroll down.
Why don’t we give this feature to the navigation bar right now? Also, we want to have coalfield to be to the Bootstrap Grid System’s left side boundaries.
Right now it is to the viewport’s left side boundaries, because our class is currently container-fluid. If you go to http://localhost:3000, you see that the Home page text is hidden under the navigation bar.
Since our app goal is to let users meet like-minded people, we have to make sure that posts’ authors can be identified. We could create our own authentication system, but that would require a lot of effort.
Just like with any other gem, to set it up we’ll follow its documentation. By installing Devise gem, we not only get the back-end functionality, but also default views.
Also don’t forget that a lot of same questions come to other people’s minds. There’s a high chance that you’ll find the answer by Googling too.
Let’s start with the login page, because in our case this is going to be a more straightforward implementation. With the registration page, due to our wanted feature, an extra effort will be required.
Take a look how arguments inside the methods are styled. Usually code lines shouldn’t be longer than 80 characters, it improves readability.
In order to use bootstrap forms in Rails we’ve to add a bootstrap_form gem. On smaller devices the form will take full screen’s width.
I think doing it this way helps to track changes and understand how the code has evolved. To do that we need to know where the action, which gets called when we go to login page, is located.
If we navigated to localhost:3000/users/sign_up, we would see the default Devise sign up page. But as mentioned above, the sign-up page will require some extra effort.
First, we have to add an extra column to the users table. We could create a new migration file and use a change_table method to add an extra column.
We can just define a new column straight inside the devise_create_users migration file and then recreate the database. Navigate to db/migrate and open the *CREATION_DATE*_devise_create_users.Rb file and add t.string :name, null: false, default: “" inside the create_table method.
We added a new column to the users table and altered the schema.Rb file. Or we can create a new file, specify the controller and the methods that we want to modify.
Controllers/registrations_controller.this code overwrites the sign_up_paras and account_update_paras methods to accept the :name attribute. Now inside our routes we have to specify this controller, so these methods could be overwritten.
Inside the file add the following CSS code: If you don’t know what the rem unit is, you may want to read this tutorial.
We don’t want to see a label text on bootstrap forms, that’s why we set this: It is a good idea to connect them all together, so users could navigate through the website effortlessly.
We’ll put links to sign up and login pages on the navigation bar. This will lead to a file with lots of code, which is hard to manage and test.
On one part elements will be shown all the time, no matter what the screen size is. On the other part of the navigation bar, elements will be shown only on bigger screens and collapsed on the smaller ones.
Inside this directory create a new partial _header.html.era file. Inside the navigation directory, create another partial file named _collapsible_elements.html.era.
First, at the second line I changed the element’s id to navbar-collapsible-content. To trigger this function there’s the button with the data-target attribute inside the _header.html file.
This will show different links based on if a user is signed in, or not. Leaving logic, such as if else statements inside views isn’t a good practice.
Views should be pretty “dumb” and just spit the information out, without “thinking” at all. The last thing to note inside the file is payment and mobile-menu CSS classes.
The purpose of these classes is to control how links are displayed on different screen sizes. Assets/stylesheets/responsive/mobile.SCSI you aren’t familiar with CSS media queries, read this.
When we were working on the _collapsible_elements.html.era file, I mentioned that Rails views is not the right place for logic. We’ll extract logic from Rails views and put it inside the helpers' directory.
App/helpers/navigation_helper.Ruby default Rails loads all helper files to all views. Personally I do not like this, because methods’ names from different helper files might clash.
We don’t have the NavigationController, so helper methods defined inside the NavigationHelper module won’t be available anywhere. If you aren’t familiar with loading and including files, read through this article to get an idea what is going to happen.
We’re going to split the content inside the if else statements into partials. Now cut the content from _collapsible_elements.html.era file’s if else statements and paste it to the corresponding partials.
We’re going to do this for the rest of the guide, whenever we encounter logic inside a view file. By doing this we’re making a favor to ourselves, testing and managing the app becomes much easier.
Even thought there aren’t many features yet, but we already have to spend some time by manually testing the app if we want to make sure that everything works. What a frustration would be to check that everything works fine, every time we did code changes.
I’ve found that it’s a different case than it is with testing frameworks. But I’ve noticed that after apps become larger, controlling sample data with fixtures becomes tough.
Helper Models Jobs Design Patterns Any other logic written by me Besides logic, I wrap my app with acceptance tests using Capybara, to make sure that all app’s features are working properly by simulating a user’s interaction.
That’s what I test in my personal apps, because it fully satisfies my needs. As many Rails developers say, controllers and views shouldn’t contain any logic.
In my opinion, user simulation tests are enough and efficient for views and controllers. So I think that simulation tests are enough to make sure that gems work properly too.
But sometimes you aren’t sure how the completed feature is going to look like and what kind of output to expect. So in those cases, test first and implementation later approach doesn’t really work.
Always read the documentation first, when you want to implement something you don’t know how to. Now you can always come back to this section and check majority of the configurations in one place.
Rather than jumping from one place to another and putting gems with configurations like puzzle pieces together. Let’s commit the changes and finally get our hands dirty with tests.
Spec/helpers/navigation_helper_spec.Rb require ‘ rails _helper' gives us access to all testing configurations and methods. Spec/helpers/navigation_helper_spec.BTO learn more about the context and it, read the basic structure docs.
Inside the corresponding context, those hooks (methods) run before each tests. Next, we’ll need some sample data to perform our tests.
Factory_girl gem gives us ability to add sample data very easily, whenever we need it. Also, it provides a good quality doc, so it makes the overall experience pretty pleasant.
Inside the factories' directory create a new file users.Rb and add the following code: Spec/factories/users within our specs, we can easily create new users inside the test database, whenever we need them, using factory_girl gem’s methods.
For the comprehensive guide how to define and use factories, checkout the factory_girl gem’s docs. By reading docs, you can see that with every additional User record, n value gets incremented by one.
In the feature specs we write code which simulates a user’s interaction with an app. Feature specs are powered by the capybara gem.
We have included those helper methods inside the rails _helper.Rb file previously. Inside the user directory create a new file named sign_up_spec.Rb.
Spec/features/user/sign_up_spec.Rowe simulate a user navigating to the sign up page, filling the form, submitting the form and finally, we expect to see the #user-settings element which is available only for logged-in users. Specs branch isn’t needed anymore.
Now let’s create some associations between User, Category and Post models. Now we’ve to define data columns and associations inside the migrations files.
To display posts on the home page, at first we need to have them inside the database. Creating data manually is boring and time-consuming.
To seed data, using the seeds.Rb file, run a command Since in our app the PagesController is responsible for the homepage, we’ll need to query data inside the pages_controller.Rb file’s index action.
Inside the index action retrieve some records from the posts table. If you aren’t familiar with ruby variables, read this guide.
This will render all posts, which were retrieved inside the index action. Views/posts/_post.html.RBI’ve used a bootstrap card component here to achieve the desired style.
Routes.rehear I’ve used a method to declare routes for index, show, new, edit, create, update and destroy actions. Then I’ve declared some custom collection routes to access pages with multiple Post instances.
With every page change and browser refresh we also recolor posts randomly too. Inside the randomColorSet() function you can see predefined color schemes.
Mouseenter and mouse leave event handlers are going to be needed in the future for posts in specific pages. When you’ll hover on a post, it will slightly change its bottom border’s color.
Assets/stylesheets/partials/posts.vassals inside the mobile.scss add the following code to fix too large text issues on smaller screens: I want to be able to click on a post and see its full content, without going to another page.
Views/posts/_modal.html.this is just a slightly modified bootstrap’s component to accomplish this particular task. Render this partial at the top of the home page’s template.
Finally, with the last line of code we make the modal window visible. But before adding CSS, let’s do a quick management task inside the stylesheets' directory.
Controllers/posts_controller.Rb I'm interested button redirects to a selected post. By sending a GET request to get a post, rails calls the show action.
It allows the container to be colored white across the full browser’s height, no matter if there is enough of content inside the element or not. 100vh-50px is required to subtract navigation bar’s height, otherwise the container would be stretched too much by 50px.
If you click on the'm interested button now, you will be redirected to a page which looks similar to this: We’ll add extra features to the show.html.era template later.
Instead of manually checking that this functionality, of modal window appearance and redirection to a selected post, works, wrap it all with specs. I start by going to the home page, click on the post, expect to see the popped up modal window, click on the'm interested button, and finally, expect to be redirected to the post’s page and see its content.
So after it was clicked on a post, testing framework expects to see a visible modal window. If you don’t care if a user sees an element or not, and you just care about an element’s presence in the DOM, pass an additional visible: false argument.
Start by updating the home page’s side menu. Split file’s content into partials, otherwise it will get noisy very quickly.
Cut #side-menu and #main-content elements, and paste them into separate partial files. Views/pages/index/_main_content.html.surrender those partial files inside the home page’s template.
Views/pages/index/side_menu/_no_login_required_links.html.rehear we simply added links to specific branches of posts. Previously we’ve added nested collection routes inside the resources :posts declaration.
We haven’t set up actions inside the PostsController, nor we created any templates for it. Inside the PostsController, define hobby, study, and team actions.
This method will return data for the specific page, depending on the action’s name. Controllers/posts_controller.bright now get_posts method just retrieves any 30 posts, not specific to anything, so we could move on and focus on further development.
Inside the posts' directory create a _branch.html.era file. Next, a _create_new_post partial is rendered to display a link, which will lead to a page, where a user could create a new post.
Create this partial file inside a new branch directory: Posts/branch/_create_new_post.html.rehear we’ll use a create_new_post_partial_path helper method to determine which partial file to render.
If the paras is empty, it means that none categories were selected by a user, which means that currently the default value all is selected. In our case we generate different paths, depending on the current controller’s action.
Next, inside the _branch.html.era file we render posts and call the no_posts_partial_path helper method. Since you cannot pass an empty string to the render method, I pass a path to an empty partial instead, in occasions where I don’t want to render anything.
Now create a _no_posts.html.era partial for the message inside the branch directory. Create templates for hobby, study and team actions.
Inside them we’ll render the _branch.html.era partial file and pass specific local variables. We’ve done quite a lot of work to create these branch pages.
In these branch pages we want to have different posts’ design. In branch pages let’s create a list design, so a user could see more posts and browse through them more efficiently.
Inside the _post.html.era partial file add the following line of code: Posts/_post.html.rehear we call the post_format_partial_path helper method to decide which post design to render, depending on the current path.
Posts/post/_branch_page.html.Erato decide which partial file to render, define the post_format_partial_path helper method inside the posts_helper.Rb Helpers/posts_helper.the post_format_partial_path helper method won’t be available in the home page, because we render posts inside the home page’s template, which belongs to a different controller.
We’ll use scopes inside the Post model to make queries chainable and some conditional logic inside the controller (we will extract it into service object in the next section to make the code cleaner). Start by defining scopes inside the Post model.
Make sure that the default_scope works correctly by wrapping it with a spec. Controllers/posts_controller.bras I’ve mentioned a little earlier, logic, just like in views, isn’t really a good place in controllers.
So we’ll extract the logic out of this method in the upcoming section. Depending on a user request, data gets queried differently using scopes.
Inside the Post model, define those scopes: Models/post.the joins method is used to query records from the associated tables.
When you click on the next link, it redirects you to another page with older posts. Instead of redirecting to another page with older posts, we can make an infinite scrolling functionality, similar to the Facebook’s and Twitter’s feed.
You just scroll down and without any redirection and page reload, older posts are appended to the bottom of the list. Whenever a user reaches the bottom of the page, AJAX request is sent to get data from the next page and that data gets appended to the bottom of the list.
First check if pagination is present, if there are any more posts to render. Finally, if all conditions successfully pass, load data from the next page using the script() function.
This partial file appends newly retrieved posts to the list. Create this file to append new posts and update the pagination element.
Helpers/posts_helper.rehear the next_page method from the will_paginate gem is used, to determine if there are any more posts to load in the future or not. Posts/posts_pagination_page/_remove_pagination.js.orbit you go to any of the branch pages and scroll down, older posts should be automatically appended to the list.
Also, we no longer need to see the pagination menu, so hide it with CSS. Spec/helpers/post_helper_spec.rehear I’ve used a test double to simulate the posts instance variable and its chained method next_page.
We can also write feature specs to make sure that posts are successfully appended, after you scroll down. The check_posts_count method is defined to reduce the amount of code the file has.
Currently, on the home page we can only see few random posts. Modify the home page, so we could see a few posts from all branches.
Define instance variables inside the PagesController’s index action. Controllers/pages_controller.Rowe have the no_posts_partial_path helper method from before, but we should modify it a little and make it more reusable.
Also, inside the _branch.html.era file pass the posts instance variable to the no_posts_partial_path method as an argument. Service objects As I’ve mentioned before, if you put logic inside controllers, they become complicated very easily and a huge pain to test.
Controllers/posts_controller.bit has a lot of conditional logic which I want to remove by using services. It’s very simple, we just pass data which we want to process and call a defined method to get a desired return value.
In ruby, we pass data to Classs initialize method, in other languages it’s known as the constructor. And then inside the class, we just create a method which will handle all defined logic.
A fortunate thing about design patterns, like services, is that it’s easy to write unit tests for it. We can simply write specs for the call method and test each of its conditions.
Controllers/posts_controller.inside the new action, we define some instance variables for the form to create new posts. Inside the creation action’s post instance variable, we create a new Post object and fill it with data, using the post_params method.
We don’t want to allow for not signed-in users to have access to a page where they can create new posts. We’ll need this method across other controllers too, so define it inside the application_controller.Rb file.
Inside the posts' directory, create a new.html.era file: Attributes of the fields are defined and the collection_select method is used to allow to select one of the available categories.
Spec/requests/posts/new_spec.bras mentioned in the documentation, request specs provide a thin wrapper around the integration tests. The include Warden::Test::Helpers line is required in order to use login_as method.
We can even add some more request specs for the pages which we created previously. Spec/requests/posts/branches_spec.this way we check that all branch pages’ templates successfully render.
To make sure that a user is able to create a new post, write feature specs to test the form. Finally, we want to make sure that all fields are filled correctly.
We could create a simple mailbox system, which would be much easier and faster to develop. Real time communication is much more exciting to develop and comfortable to use.
Fortunately, Rails has Action Cables which makes real time features’ implementation relatively easy. The core concept behind the Action Cables is that it uses a Sockets Protocol instead of HTTP.
And the core concept of Sockets is that it establishes a client-server connection and keeps it open. This means that no page reloads are required to send and receive additional data.
We could name them PrivateConversation and PrivateMessage, but you can quickly encounter a little problem. To avoid chaotic structure inside directories, we can use a name spacing technique.
This is required to add prefix to database tables’ names, so models could be recognized. Personally I don’t like keeping those files inside the models' directory, I prefer to specify a table’s name inside a model itself.
Models/private/message.the private.Rb file, inside the models' directory, is no longer needed, you can delete it. This allows to use custom names for our associations and make sure that name spaced models get recognized.
Another use case of the class_name method would be to create a relation to itself, this is useful when you want to differentiate same model’s data by creating some kind of hierarchies or similar structures. A data column in a table is only created on the belongs_to association’s side, but to make the column recognizable, we’ve to define the foreign_key with same values on both models.
Define data tables inside the migration files: Db/migrate/CREATION_DATE_create_private_messages.inside the body data column, a message’s content is going to be stored.
We have a place to store data for private conversations, but that’s pretty much it. As mentioned in previous sections, personally I like to create a basic visual side of a feature and then write some logic to make it functional.
Once you have a user interface, it’s easier to start breaking down a problem into smaller steps, because you know what should happen after a certain event. This allows to understand and navigate through the source code more intuitively.
In a case of our app, it makes sense that you want to contact a person which has similar interests to yours. A convenient place for this functionality is inside a single post’s page.
Inside the posts/show.html.era template, create a form to initiate a new conversation. Spec/helpers/posts_helper_spec.Rowe’ll define the message_has_been_sent instance variable inside the PostsController in just a moment, it will determine if an initial message to a user was already sent, or not.
Create partial files, corresponding to the leave_message_partial_path helper method, inside a new contact_user directory Before writing specs, define a private_conversation factory, because we’ll need sample data inside the test database.
Private method is going to make sure that the conversation’s id isn’t added inside the session yet. Controllers/private/conversations_controller.band lastly, we’ll need access to the conversation inside the views, so convert the conversation variable into an instance variable.
Then create the last missing partial file for the conversation’s new message form Now let’s create a feature that after a user sends a message through an individual post, the conversation window gets rendered on the app.
This partial file’s purpose is to add a conversation window to the app. Private/conversations/_open.js.this callback partial file is going to be reused in multiple scenarios.
Then we expand the window and autofocus the message form. At the bottom of the file, the positionChatWindows() function is called to make sure that all conversations’ windows are well positioned.
We keep track of the viewport’s width with an event listener. Once there is enough of free space for a hidden conversation window, the app displays it again.
And add CSS to style conversations’ windows Assets/stylesheets/partials/conversation_window.session might notice that there are some classes that haven’t been defined yet in any HTML file.
That’s because the future files, we’ll create in the views' directory, are going to have shared CSS with already existent HTML elements. Instead of jumping back and forth to CSS files multiple times after we add any minor HTML element, I have included some classes, defined in future HTML elements, right now.
Previously we’ve saved an id of a newly created conversation inside the session. It’s time to take an advantage of it and keep the conversation window opened until a user closes it or destroys the session.
Define the helper method inside the Private::MessagesHelper We are going to have an infinite scroll mechanism for messages, similar to the one we have in posts’ pages.
Private/messages/load_more_messages/window/_add_link_to_messages.js.this file is going to update the link which loads previous messages. But, if we tried to go to the app and opened a conversation window, we wouldn’t see any rendered messages.
When we open a conversation window for the first time, we want to see the most recent messages. We can program the conversation window in a way that once it gets expanded, the load more messages link gets triggered, to load the most recent messages.
That’s the path, we defined to the load previous messages link. In our case we include extra methods to our controller from the module.
The reason why it is stored inside the module is that we’ll use this exact same method in another controller a little later. To avoid code duplication, we make the method more reusable.
I’m kidding :D. This is an independent application, and we can create our app however we like it. If you don’t like concerns, there are a bunch of other ways to create reusable methods.
Controllers/concerns/messages.rehear we require the active_support/concern and then extend our module with ActiveSupport::Concern, so Rails knows that it is a concern. After the get_messages method sets all necessary instance variables, the index action responds with the _load_more_messages.js.era partial file.
If you try to contact a user now, a conversation window will be rendered with a message, you sent, inside. Real time functionality with Action Cable Conversations’ windows look pretty neat already.
The first thing which we should do is create a Socket connection and subscribe to a specific channel. Luckily, Socket connections are already covered by default Rails configuration.
Now we need a private conversation channel to subscribe to. Inside the generated Private::ConversationChannel, we see subscribed and unsubscribed methods.
With the subscribed method a user creates a connection to the channel. With the unsubscribed method a user, obviously, destroys the connection.
It means that we can send and receive data from the server without restarting the connection or refreshing a browser, man! Let’s make the conversation window’s new message form functional.
The send_message function is going to call a send_message method on the server side, which will take care of creating a new message. Also take a note, the event handler is on a submit button, but on the conversation window we don’t have any visible submit buttons.
The server side method should be defined inside the Private::ConversationChannel. The data parameter, which we get from the passed argument, is a nested hash.
So to reduce this nested complexity into a single hash, the each_with_object method is used. It won’t show up on the conversation window instantly yet, only when you refresh the website.
It would show up, but we haven’t set anything to broadcast newly created messages to a private conversation’s channel. But before we continue and commit changes, quickly recap how the current messaging system works.
There is an after_create_commit callback method, which runs whenever a new model’s record gets created. We haven’t defined the Private::MessageBroadcastJob, so currently specs would raise an undefined constant error.
Now inside the job, process the given data and broadcast it to channel’s subscribers. Also, we pass some additional key-value pairs to properly display the message.
You can change a window’s color, make it blink, or whatever you want to. We’ll use these functions for both type of chats, private and group.
The messenger is going to be a separate way to open conversations. To prevent a lot of small changes in the future, I’ve included cases with the messenger right now.
Both users, the sender and the recipient, should receive and get displayed new messages on the DOM. We’ll add additional ways to render conversations’ windows in just a moment.
And then create an OrderConversationsService to take care of conversations’ querying and ordering. In the future we’ll mash private and group conversations together, and sort them by their latest messages.
Again, if we didn’t use the includes method, we would experience an N + 1 query problem. When you evaluate values in the opposite way, b a , it sorts an array in descending order.
We haven’t defined the all_by_user scope inside the Private::Conversation model yet. Now inside views, we have access to an array of ordered conversations.
Whenever a user clicks on any of them, a conversation window gets rendered on the app. Inside one component, elements are displayed constantly.
So inside the navigation’s header, where components are visible all the time, we’re going to create a drop down menu of conversations. As usually, to prevent having a large view file, split it into multiple smaller ones.
Open the navigation’s _header.html.era file and replace its content with the following: On smaller devices we’re going to display an icon, instead of the name of the application.
There is a helper method nav_header_content_partials, which returns an array of partials’ paths. Spec/helpers/navigation_helper_spec.now create necessary files to display drop down menus on the navigation bar.
Layouts/navigation/header/dropdowns/_conversation.html.this where we use the all_conversations instance variable, defined inside the controller before, and render links to open conversations. We’ll need to create two different versions of links for private and group conversations.
First define the conversation_header_partial_path helper method inside the NavigationHelper So you have to comment out the group conversation’s part in specs for a while to avoid failure.
Inside specs create a shared directory with a conversations_helper_spec.Rb file to test the private_conv_seen_status helper method. Spec/helpers/shared/conversations_helper_spec.when a link to a conversation is clicked, the Private::Conversation controller’s open action gets called.
The navigation bar right now is messy, we have to take care of its design. To style the drop down menus, add CSS to the navigation.scss file.
By clicking on any of the menu links, a conversation window should appear on the app Also notice that instead of the coalfield logo we have the home page icon now.
We’ll create a messenger which will be opened instead of a conversation window. To avoid these failures, inside the rails _helper.Rb, change wait time somewhere between 5 and 10 seconds.
Program the app in a way that when a conversation window is opened or clicked, its messages get marked as seen. Also note that currently we only see highlighted unseen conversations when the drop down menu is expanded.
Make sure that everything works as we expect by writing the specs. To stay in touch with people, you met on the app, you have to be able to add them to contacts.
Define associations, validation and a method to find a contact record by providing users’ ids. Private conversation’s window update The way users are going to be able to send and accept contact requests is through a private conversation’s window.
Later we’ll add an extra way to accept requests through a navigation bar’s drop down menu. This is where we’ll keep extra options for a private conversation’s window.
Private/conversations/conversation/heading/_add_user_to_contacts.html.ERBA the bottom of the _heading.html.era file, render the option to add the conversation’s opposite user to contacts: Because we’re going to display options on the conversation window’s heading element, we have to make sure that additional options fit perfectly on the heading.
Spec/helpers/private/conversations_helper_spec.previously, we used current_user and recipient let methods only within a private scope’s context. So cut and place them outside private scope’s context.
At the top of the.panel-body element, render a partial file which will show an extra message window to accept or decline a contact request Implement design changes and take care of styling issues which appear due to extra elements on the conversation window.
The expanded window has an option to add a user to contacts. Actually, you can send and accept a contact request right now by clicking an icon on the conversation’s header or clicking the Add to contact link.
Models/user.RBA join table is going to be used to track who belongs to which group conversation Models/group/message.Rowe’ll store users’ ids who have seen a message into an array.
To create and manage objects, such as array, inside a database column, a serialize method is used. A default scope, to minimize the amount of queries, and some validations are added.
In fact, styling and some parts are going to be in common between both types of conversations. Start by creating a controller and a basic user interface.
Controllers/group/conversations_controller.their is some complexity involved in creating a new group conversation, so we’ll extract it into a service object. If you recall, we have them in the Private::ConversationsController too, but this time it stores group conversations’ ids into the session.
We’ll create this interface as an option on the private conversation’s window soon. Before doing that, make sure that the service object functions properly by covering it with specs.
Currently, we only take care of private conversations inside the ApplicationController. Only private conversations are being ordered and only their ids, after a user opens them, are available across the app.
Controllers/application_controller.because conversations’ ordering happens with a help of the OrderConversationsService, we’ve to update this service Order_conversations_service.previously we only had the private conversations array, and we sorted it by the latest messages’ creation dates.
Layouts/application.html.recreate the partial file to render group conversations’ windows one by one: We have a mechanism how group conversations are created and rendered on the app.
Helpers/group/messages_helper.again, this method, just like on the private side, is going to become more “intelligent”, once we develop the messenger. Obviously, this is not a thrilling thing, because once you destroy the session, there is no way to open the same conversation again.
Group/conversations/_open.js.elbow we’re able to open conversations by clicking on navigation bar’s drop down menu links. The app will try to render messages, but we haven’t created any templates for them.
This little information allows us to create extra features, like show to conversation participants who have seen messages, etc. But in our case, we’ll use this information to determine if a current user has seen a message, or not.
Also, we’ll need helper methods from the Shared module. The process of achieving this feature is going to be pretty similar to what we did with private conversations.
Channels/group/conversation_channel.this time we check if a user belongs to a conversation, before establishing the connection, with the belongs_to_conversation method. In private conversations we streamed from a unique channel, by providing the current_user ’s id.
With this loop we connect a user to all its group conversations’ channels. This method on the server side makes sure that a user really belongs to a provided conversation.
When you think about it, we could have just created this loop on the server side and wouldn’t have to deal with all this confirmation process. But here’s a reason why we pass an id of a conversation from the client side.
The passable conversation’s id allows us to effortlessly achieve that. In the upcoming section we’ll create a unique channel for every user to receive notifications in real time.
When new users will be added to a group conversation, we’ll call the subToGroupConversationChannel function, through their unique notification channels, and connect them to the group conversation channel. We wouldn’t have any way to connect new users to a conversation channel dynamically.
Controllers/group/conversations_controller.recreate the Group::AddUserToConversationService, which is going to take care that a selected user will be added to a conversation Users are able to communicate one on one, or if they need, they can build an entire chat room with multiple people.
On mobile screens instead of opening a conversation window, the app will load the messenger. On bigger screens, users could choose where to chat, on the conversation window or on the messenger.
If the messenger is going to fill the whole browser’s window, it should be more comfortable to communicate. Since we’ll use the same data and models, we just need to open conversations in a different environment.
Generate a new controller to handle requests to open a conversation inside the messenger. The purpose of this action is to go from any page straight to the messenger and render a selected conversation.
On smaller screens users are going to chat through messenger instead of conversation windows. In just a moment, we’ll switch links for smaller screens to open conversations inside the messenger.
Messengers/_private_conversation.html.this file will render a private conversation inside the messenger. Also notice that we reuse some partials from the private conversation views.
When we go to the messenger, it’s better to not see drop down menus on the navigation bar. We don’t want to render conversation windows inside the messenger, otherwise it would look chaotic.
A conversation window and the messenger at the same time to chat with the same person. At first, forbid conversations’ windows to be rendered on the messenger’s page.
To control it, remember how conversations’ windows are rendered on the app. If users are inside the messenger’s page, they will get an empty array and no conversations’ windows will be rendered.
Next, create an alternative partial file for navigation’s header, so drop down menus won’t be rendered. Inside the NavigationHelper, we’ve defined the nav_header_content_partials helper method before.
Create a messenger.scss file inside the partials directory Assets/stylesheets/responsive/desktop.scssWhen we click on a conversation to open it, we want to be able to load previous messages somehow.
In this section we’ll put our energy on enhancing those vital features. Let’s make users aware whenever they get a contact request update or joined to a group conversation.
Add CSS to style and position the contact requests’ drop down menu: On the navigation bar, we can see a drop down menu for contact requests now.
We have the notifications channel and the job to broadcast contact requests’ updates. Now we need to create a connection on the client side, so users could send and receive data in real time.