Δημιουργώντας μια εφαρμογή συνομιλιών (chat) από το μηδέν με Rails και WebSockets

Ένα βοήθημα που περιγράφει βήμα προς βήμα πως να δημιουργήσετε μια εφαρμογή συνομιλιών (chat) από το μηδέν με χρήση Rails και WebSockets (ActionCable).
This article is also available in english.
Πρόσθεσα νέα σελίδα στο μπλογκ: "Αστεία" για προγραμματιστές

Εισαγωγή

Σε αυτό το βοήθημα θα δημιουργήσουμε μια εφαρμογή συνομιλιών (chat) από το μηδέν χρησιμοποιώντας το framework Ruby on Rails και την τεχνολογία των WebSockets.

Rails chat tutorial gif

Ενημέρωση: Δημοσίευσα άλλο ένα βοήθημα για τη δημιουργία docker image της εφαρμογής του παρόντος, μπορείτε να το βρείτε στα αγγλικά για την ώρα εδώ.

Κώδικας και σχόλια
Μπορείτε να βρείτε τον κώδικα του παρόντος άρθρου στο GitHub.
Για σχόλια, feedback, λάθη κ.λπ. παρακαλώ ανοίξτε ένα issue στο αποθετήριο.

Τι είναι τα WebSockets;

Πρόκειται ουσιαστικά για ένα πρωτόκολλο που επιτρέπει την αμφίδρομη επικοινωνία μεταξύ πελάτη και εξυπηρετητή μιας διαδικτυακής εφαρμογής μέσω μιας και μόνης σύνδεσης TCP μακράς διαρκείας.

The WebSocket protocol enables interaction between a web browser (or other client application) and a web server with lower overheads, facilitating real-time data transfer from and to the server.

This is made possible by providing a standardized way for the server to send content to the client without being first requested by the client, and allowing messages to be passed back and forth while keeping the connection open. In this way, a two-way ongoing conversation can take place between the client and the server.

The communications are done over TCP port number 80 (or 443 in the case of TLS-encrypted connections), which is of benefit for those environments which block non-web Internet connections using a firewall. Similar two-way browser-server communications have been achieved in non-standardized ways using stopgap technologies such as Comet. WebSocket @ Wikipedia

Γιατί να χρησιμοποιήσουμε WebSockets;

Υποθέστε ότι θέλετε να δημιουργήσετε μια ιστοσελίδα που εμφανίζει τις καταστάσεις κάποιων διαδικασιών που βρίσκονται υπό εκτέλεση. Χωρίς τη χρήση WebSockets, θα έπρεπε να κάνετε ένα από τα παρακάτω:

  • να χρησιμοποιήσετε AJAX με Javascript intervals για να ζητάτε και να εμφανίζεται τις τρέχουσες καταστάσεις των διαδικασιών
  • να φορτώνεται ξανά τη σελίδα κάθε x δευτερόλεπτα (<meta http-equiv="refresh" content="x">)
  • να προσθέσετε ένα μήνυμα στη σελίδα τύπου “Οι καταστάσεις δεν ενημερώνονται αυτόματα ¯\_(ツ)_/¯ Πατήστε εδώ για να φορτώσετε ξανά τη σελίδα.”

Και οι τρεις παραπάνω προτάσεις θα κατέληγαν στο να ζητήσουν από τον εξυπηρετητή τις καταστάσεις των διαδικασιών ακόμη και να αυτές δεν είχαν αλλάξει.

Τα WebSockets είναι αυτά που θα μας επιτρέψουν αυτού του είδους η επικοινωνία να λάβει χώρα κατά απαίτηση. Το κόστος αυτής της λειτουργικότητας είναι η διατήρηση TCP συνδέσεων μεταξύ του εξυπηρετητή και όλων των πελατών του (μια για κάθε ανοιγμένη σελίδα στον φυλλομετρητή).

Χτίζοντας την εφαρμογή

Θα χτίσουμε την εφαρμογή χρησιμοποιώντας:

  • Ruby: έκδοση 2.6.2
  • Rails: έκδοση 5.2.3

Στήνοντας το περιβάλλον

Θα εγκαταστήσουμε τις κατάλληλες εκδόσεις της Ruby και του Ruby on Rails.

Εγκατάσταση Ruby

Χρησιμοποιώ το πρόγραμμα rvm για να διαχειρίζομαι τις εκδόσεις της Ruby που εγκαθιστώ στο σύστημά μου. Για να εγκαταστήσετε την Ruby του παρόντος βοηθήματος εκτελέστε:

rvm install ruby 2.6.2

Εγκατάσταση Ruby on Rails

Δημιουργήστε ένα φάκελο στο σύστημά σας με το όνομα rails-chat-tutorial.

Περιηγηθείτε σε αυτόν τον φάκελο και δημιουργήστε τα παρακάτω αρχεία:

.ruby-version

ruby-2.6.2

.ruby-gemset

rails-chat-tutorial

Με αυτά τα δύο αρχεία, ενημερώνουμε τον rvm (Ruby version manager) ότι όταν εργαζόμαστε σε αυτόν τον φάκελο, θέλουμε να γίνεται χρήση της συγκεκριμένης έκδοσης Ruby (.ruby-version) και των συγκεκριμένων βιβλιοθηκών που έχουν εγκατασταθεί στο gemset με το όνομα rails-chat-tutorial (.ruby-gemset).

Τώρα, αν περιηγηθείτε ξανά στον φάκελο θα δείτε κάτι σαν τα παρακάτω:

$ cd .

ruby-2.6.2 - #gemset created /home/iridakos/.rvm/gems/ruby-2.6.2@rails-chat-tutorial
ruby-2.6.2 - #generating rails-chat-tutorial wrappers...........

Εγκαταστήστε την επιθυμητή έκδοση του Ruby on Rails με:

gem install rails -v 5.2.3

Δημιουργία της εφαρμογής

Είμαστε έτοιμοι να δημιουργήσουμε την rails εφαρμογή, στη γραμμή εντολών πληκτρολογήστε:

rails new .

Σημείωση: Δεν προσδιορίσαμε κάποιο όνομα για την εφαρμογή οπότε το framework θα χρησιμοποιήσει το όνομα του φακέλου: rails-chat-tutorial.

Το Rails θα δημιουργήσει όλα τα αρχεία της εφαρμογής και θα εγκαταστήσει τις απαιτούμενες βιβλιοθήκες (gems).

Ας ξεκινήσουμε την εφαρμογή για να επιβεβαιώσουμε ότι όλα είναι εντάξει.

rails server

You should see something like:

=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.1 (ruby 2.6.2-p47), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000
Use Ctrl-C to stop

Ανοίξτε έναν φυλλομετρητή και επισκεφθείτε τη διεύθυνση: http://localhost:3000. Αν βλέπετε το παρακάτω είμαστε εντάξει για να συνεχίσουμε. Αν όχι, δε με ξέρετε δε σας ξέρω :P

Rails new application

Χρήστες και βιβλιοθήκη devise

Θα χρησιμοποιήσουμε την φοβερή λύση της βιβλιοθήκης devise για πιστοποίηση χρηστών.

Προσαρτήστε την παρακάτω απαίτηση βιβλιοθήκης στο τέλος του αρχείου Gemfile βρίσκεται στον κεντρικό φάκελο της εφαρμογής (Rails root directory).

gem 'devise'

Στη γραμμή εντολών, εγκαταστήστε τη νέα βιβλιοθήκη εκτελώντας:

bundle

Ολοκληρώστε την ενσωμάτωση της devise με:

rails generate devise:install

Θα δημιουργήσουμε το μοντέλο που αναπαριστά τους χρήστες μας χρησιμοποιώντας τις γεννήτριες της devise.

Εκτελέστε στη γραμμή εντολών:

rails generate devise User username:string

Σημείωση: προσθέσαμε ένα επιπλέον χαρακτηριστικό username στο μοντέλο (πέραν των όσων εξ ορισμού παρήγαγε η βιβλιοθήκη) ούτως ώστε να έχουμε κάτι πιο φιλικό να εμφανίζουμε για τους χρήστες αντί της διεύθυνσης email.

Ανοίξτε το αρχείο προσαρμογής της βάσης (migration) που βρίσκεται στην τοποθεσία db/migrate/<datetime>_devise_create_users.rb και προσθέστε ένα ευρετήριο μοναδικότητας (unique index) για το χαρακτηριστικό που προσθέσαμε (username)[4]:

  add_index :users,  :username,             unique: true

Βρείτε τη γραμμή που ορίζει τη στήλη username αλλάξτε την ως εξής:

t.string :username, null: false

για να δηλώσουμε το χαρακτηριστικό ως υποχρεωτικό.

Στη συνέχεια, στο μοντέλο User που βρίσκεται στην τοποθεσία app/models/user.rb προσθέστε τον παρακάτω κανόνα επικύρωσης της μοναδικότητας και της υποχρεωτικής παρουσίας του χαρακτηριστικού:

  validates :username, uniqueness: true, presence: true

Τέλος, προσαρμόστε τη βάση με:

rails db:migrate

Δωμάτια και μηνύματα

Κάθε μήνυμα θα εμφανίζεται σε ένα δωμάτιο.

Ας δημιουργήσουμε και τα δύο.

Χρησιμοποιήστε την παρακάτω εντολή για δημιουργήσετε το δωμάτιο - Room:

rails generate resource Room name:string:uniq

και την ακόλουθη εντολή για να δημιουργήσετε το μήνυμα δωματίου - RoomMessage:

rails generate resource RoomMessage room:references user:references message:text

Τώρα θα ορίσουμε τις σχέσεις αυτών των δύο[7].

Ανοίξτε το αρχείο που βρίσκεται στην τοποθεσία app/models/room.rb και προσθέστε την παρακάτω σχέση μέσα στην κλάση:

has_many :room_messages, dependent: :destroy,
                         inverse_of: :room

Ανοίξτε το αρχείο που βρίσκεται στην τοποθεσία app/models/room_message.rb και προσθέστε τις παρακάτω σχέσεις:

belongs_to :user
belongs_to :room, inverse_of: :room_messages

Προσαρμόστε τη βάση με την εντολή:

rails db:migrate

Τώρα μπορούμε να ορίσουμε με τέτοιο τρόπο τις διαδρομές (routes) ώστε τα αιτήματα στην αρχική σελίδα (root requests) να σερβίρονται από την ενέργεια index του ελεγκτή RoomsController (που δημιουργήθηκε αυτόματα όταν εκτελέσαμε την εντολή για τη δημιουργία του πόρου RoomMessage παραπάνω) - RoomsController#index.

Ανοίξτε το αρχείο που βρίσκεται στην τοποθεσία config/routes.rb και αλλάξτε τα περιεχόμενά του στα παρακάτω:

Rails.application.routes.draw do
  devise_for :users

  root controller: :rooms, action: :index

  resources :room_messages
  resources :rooms
end

Επανεκκινήστε τον εξυπηρετητή και προσπαθήστε να περιηγηθείτε στην αρχική σελίδα της εφαρμογής (http://localhost:3000).

Θα δείτε το παρακάτω λάθος, μην ανησυχείτε:

No action index for the RoomsController

Πρέπει να δημιουργήσουμε την ενέργεια index στον ελεγκτή RoomsController. Ανοίξτε τον ελεγκτή που βρίσκεται στην τοποθεσία app/controllers/rooms_controller.rb και αλλάξτε τα περιεχόμενά του στα παρακάτω:

class RoomsController < ApplicationController
  def index
  end
end

Κατόπιν, δημιουργήστε το αρχείο app/views/rooms/index.html.erb και για την ώρα απλά προσθέστε μόνο την παρακάτω επικεφαλίδα:

<h1>Rooms index</h1>

Φορτώστε ξανά την αρχική σελίδα και voilà.

Rooms index

Προσθήκη πιστοποίησης χρηστών

Θέλουμε όλοι οι χρήστες να είναι πιστοποιημένοι πριν να μπορέσουν να συμμετάσχουν σε συνομιλίες οπότε θα προσθέσουμε την παρακάτω γραμμή στην κλάση ApplicationController που βρίσκεται στην τοποθεσία app/controllers/application_controller.rb και από την οποία κληρονομούν οι ελεγκτές μας:

  before_action :authenticate_user!

Αν πλοηγηθούμε εκ νέου στην αρχική σελίδα http://localhost:3000 θα πρέπει να ανακατευθυνθούμε στη σελίδα πιστοποίησης χρηστών [10].

Sign in

Πριν συνεχίσουμε με τα ωραία, ας εξοπλίσουμε πρώτα την εφαρμογή με κάποια καλά χαρακτηριστικά.

Προσθήκη bootstrap

Θα χρησιμοποιήσουμε το Bootstrap και θα το ενσωματώσουμε στην εφαρμογή χρησιμοποιώντας τη βιβλιοθήκη bootstrap-rubygem.

Ακολουθώντας τις οδηγίες της βιβλιοθήκης, προσθέστε τις παρακάτω απαιτήσεις βιβλιοθηκών στο μητρώο των απαιτήσεων Gemfile.

gem 'bootstrap', '~> 4.3.1'
gem 'jquery-rails'

και εκτελέστε την εντολή bundle για να κατέβουν και να εγκατασταθούν.

Αλλάξτε την κατάληξη του αρχείου app/assets/stylesheets/application.css σε scss και αντικαταστήστε τα περιεχόμενά του με:

@import "bootstrap";

Προσθέστε τις ακόλουθες γραμμές στο αρχείο app/assets/javascript/application.js αμέσως πριν τη γραμμή //= require_tree . [9]:

//= require jquery3
//= require popper
//= require bootstrap-sprockets

Προσθήκη simple_form

Θα χρησιμοποιήσουμε αυτή την εξαιρετική βιβλιοθήκη για να δημιουργούμε τις φόρμες μας εύκολα.

Προσθέστε την απαίτηση στο αρχείο Gemfile και εκτελέστε την εντολή bundle για να το εγκαταστήσετε.

gem 'simple_form'

Τέλος, ολοκληρώστε την ενσωμάτωση στην εφαρμογή χρησιμοποιώντας:

rails generate simple_form:install --bootstrap

Σημείωση: Χρησιμοποιήσαμε την οδηγία –bootstrap στην παραπάνω εντολή αφού το bootstrap είναι το CSS framework που θα χρησιμοποιούμε.

Προβολές devise με bootstrap και simple_form

Η βιβλιοθήκη devise χρησιμοποιεί τις δικές τις προβολές (views) για πιστοποιήσεις, καταχωρήσεις νέων χρηστών κ.λπ. Παρόλα αυτά, έχουμε τρόπο να προσαρμόσουμε αυτές τις προβολές και εφόσον επιλέξαμε να χρησιμοποιήσουμε τις βιβλιοθήκες bootstrap και simple_form, μπορούμε να παράξουμε αυτές τις προβολές ούτως ώστε οι επιλογές μας να γίνουν σεβαστές.

Στη γραμμή εντολών σας:

rails generate devise:views

Η προβολή για την πιστοποίηση των χρηστών βρίσκεται στην τοποθεσία app/views/devise/sessions/new.html.erb και για την καταχώρηση χρηστών στην τοποθεσία app/views/devise/registrations/new.html.erb. Ανοίξτε αυτά τα δύο αρχεία και αλλάξτε τις κλάσεις των κουμπιών υποβολής αντικαθιστώντας την παρακάτω γραμμή[6]:

<%= f.button :submit, "Sign up" %>

με

<%= f.button :submit, "Sign up", class: 'btn btn-success' %>

ώστε τα κουμπιά να εμφανίζονται ακολουθώντας το στυλ του bootstrap.

Πριν δούμε τις αλλαγές που κάναμε, ας κάνουμε ένα τελευταίο πράγμα στην προκαθορισμένη δομή των προβολών μας (default layout).

Ανοίξτε το αρχείο app/views/layouts/application.html.erb και αντικαταστήστε τα περιεχόμενά του με τα παρακάτω:

<!DOCTYPE html>
<html>
  <head>
    <title>RailsChatTutorial</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <div class="container">
      <div class="row">
        <div class="col-12">
          <%= yield %>
        </div>
      </div>
    </div>
  </body>
</html>

ώστε να χρησιμοποιούμε το Bootstrap’s πλέγμα (grid) στις προβολές.

Περιηγηθείτε στην αρχική σελίδα http://localhost:3000 και δείτε τι δημιουργήσαμε.

Sign in with Devise and simple form

Ας προσπαθήσουμε να κάνουμε καταχώρηση χρήστη ακολουθώντας το σύνδεσμο Sign up της φόρμας:

Sign up without username

Όπως βλέπετε, δεν υπάρχει πεδίο για τη συμπλήρωση του ονόματος χρήστη (username) που προσθέσαμε όταν δημιουργήσαμε το μοντέλο User. Για να μπορούν οι χρήστες να καταχωρούν το όνομά τους πρέπει:

  • να προσθέσουμε το πεδίο στη φόρμα
  • να ενημερώσουμε τη βιβλιοθήκη devise ώστε να αναγνωρίζει και να δέχεται το νέο χαρακτηριστικό (username) διαφορετικά ο ελεγκτής ApplicationController θα το αγνοεί όταν θα υποβάλλουμε τη φόρμα.

Για να προσθέσουμε το πεδίο στη φόρμα καταχώρησης χρηστών, ανοίξτε το αρχείο app/views/devise/registrations/new.html.erb και προσθέστε αυτές τις γραμμές μεταξύ των πεδίων email και password.

  <%= f.input :username,
              required: true %>

Κατόπιν, ανοίξτε τον ελεγκτή της εφαρμογής app/controllers/application_controller.rb για να ρυθμίσετε καταλλήλως τα επιτρεπόμενα χαρακτηριστικά. Αλλάξτε τα περιεχόμενά του στα παρακάτω:

class ApplicationController < ActionController::Base
  before_action :authenticate_user!

  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:email, :username])
  end
end

Αυτό ήταν, περιηγηθείτε ξανά στην αρχική σελίδα και δοκιμάστε να καταχωρηθείτε ως χρήστες[5].

Sign up with username

Καθάρισμα μη χρησιμοποιούμενων στοιχείων

Δε θα χρησιμοποιήσουμε coffee script ή τα turbolinks οπότε ας αφαιρέσουμε τις σχετικές αναφορές.

Ανοίξτε το Gemfile και αφαιρέστε τις παρακάτω γραμμές:

# Use CoffeeScript for .coffee assets and views
gem 'coffee-rails', '~> 4.2'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'

Ανοίξτε το αρχείο app/assets/javascripts/application.js και αφαιρέστε την παρακάτω γραμμή:

//= require turbolinks

Ανοίξτε το app/views/layouts/application.html.erb και αλλάξτε τις παρακάτω γραμμές[3] από:

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>

σε

    <%= stylesheet_link_tag    'application', media: 'all' %>
    <%= javascript_include_tag 'application' %>

Ελέγξτε ότι ο φάκελος app/assets/javascripts δεν έχει αρχεία με κατάληξη .coffee και αν έχει, αφαιρέστε τα.2

Στη γραμμή εντολών τέλος, εκτελέστε:

rails tmp:cache:clear

για να καθαρίσετε ότι αρχεία coffee scripts είχαν ενδεχομένως ενταχθεί στην cache.

Αυτά. Επανεκκινήστε τον εξυπηρετητή.

Προσθήκη μπάρας πλοήγησης

Για να βελτιώσουμε τη χρηστικότητα της εφαρμογής θα προσθέσουμε μια μπάρα πλοήγησης.

Δημιουργήστε το φάκελο app/views/shared και ένα αρχείο μέσα του με το όνομα _navigation_bar.html.erb. Αυτό θα είναι ένα απόσπασμα (partial) που θα εμφανίζει την μπάρα πλοήγησης και που στη συνέχεια θα προσθέσουμε στην κεντρική δομή των ιστοσελίδων της εφαρμογής (application layout) για να εμφανίζεται σε όλες τις σελίδες. Προσθέστε μέσα του τα παρακάτω:

<nav class="navbar navbar-expand-lg navbar-dark bg-dark justify-content-between">
  <a class="navbar-brand" href="#">Rails chat tutorial</a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#nav-bar-collapse" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>

  <% if current_user %>
    <div class="dropdown">
      <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
        <img class="avatar" src="<%= gravatar_url(current_user) %>">
        <%= current_user.username %>
      </a>

      <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
        <%= link_to 'Logout', destroy_user_session_path, method: :delete, class: 'dropdown-item' %>
      </div>
    </div>
  <% end %>
</nav>

Δώστε προσοχή στη γραμμή gravatar_url(current_user). Αυτή είναι μια βοηθητική μέθοδος που θα χρησιμοποιούμε για να εμφανίζουμε την εικόνα προφίλ (gravatar) των πιστοποιημένων χρηστών. Δεν είναι συστημική μέθοδος, θα την υλοποιήσουμε εμείς αλλά είναι αρκετά απλή.

Ανοίξτε το αρχείο με τις βοηθητικές μεθόδους app/helpers/application_helper.rb και προσθέστε τον παρακάτω κώδικα:

def gravatar_url(user)
  gravatar_id = Digest::MD5::hexdigest(user.email).downcase
  url = "https://gravatar.com/avatar/#{gravatar_id}.png"
end

Σημείωση:

  • Όπως μπορείτε να δείτε το όνομα του χρήστη, η εικόνα προφίλ και ο σύνδεσμος εξόδου από την εφαρμογή θα εμφανίζονται μόνο όταν ο χρήστης έχει συνδεθεί.

Η εικόνα προφίλ έχει μια CSS κλάση avatar. Πρέπει να ορίσουμε αυτήν την κλάση σε κάποιο αρχείο stylesheet της εφαρμογής. Δημιουργήστε ένα αρχείο (στο οποίο θα συγκεντρώσουμε όλες τις CSS κλάσεις που θα χρησιμοποιήσουμε πέραν αυτών που παρέχονται από το bootstrap) με το όνομα app/assets/stylesheets/rails-chat-tutorial.scss.

Για την ώρα προσθέστε τον κανόνα για την εικόνα προφίλ:

.avatar {
  max-height:30px;
  border-radius: 15px;
  width:auto;
  vertical-align:middle;
}

και ανοίξτε το αρχείο application.scss για να το προσθέσετε στο μητρώο stylesheet της εφαρμογής. Προσθέστε τη γραμμή:

@import "rails-chat-tutorial"

Τώρα θα προσθέσουμε το απόσπασμα της μπάρας πλοήγησης που δημιουργήσαμε στην προκαθορισμένη δομή των σελίδων της εφαρμογής. Ανοίξτε το αρχείο app/views/layouts/application.html.erb και αλλάξτε τα περιεχόμενά του στα παρακάτω:

<!DOCTYPE html>
<html>
  <head>
    <title>RailsChatTutorial</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all' %>
    <%= javascript_include_tag 'application' %>
  </head>

  <body>
    <div class="container">
      <div class="row">
        <div class="col-12">
          <%= render partial: 'shared/navigation_bar' %>
          <div class="my-3">
            <%= yield %>
          </div>
        </div>
      </div>
    </div>
  </body>
</html>

Φορτώστε ξανά την αρχική σελίδα και δείτε την μπάρα.

Sign up with navigation bar

Τέλεια. Συμπληρώστε τα επιθυμητά στοιχεία εισόδου σας και υποβάλετε τη φόρμα.

Signed in

Διαχείριση δωματίων

Θα δημιουργήσουμε μια απλή δομή για τα δωμάτια.

  • Μια μικρή στήλη που θα εμφανίζει κάθετα όλα τα διαθέσιμα δωμάτια
  • Μια μεγάλη στήλη που θα περιέχει τα μηνύματα και τη φόρμα υποβολής μηνύματος.

Οι αρχική σελίδα των δωματίων θα έχει τη δεξιά στήλη άδεια, η τελευταία θα εμφανίζεται μόνο όταν ο χρήστης έχει επιλέξει συγκεκριμένο δωμάτιο.

Τέλος, στην αρχική σελίδα των δωματίων θα παρέχουμε και την επιλογή για δημιουργία νέου δωματίου.

Αρχική σελίδα δωματίων

Πρώτα θα πρέπει να φορτώσουμε όλα τα διαθέσιμα δωμάτια στον ελεγκτή RoomsController. Ανοίξτε το αρχείο app/controllers/rooms_controller.rb και αλλάξτε την μέθοδο index όπως παρακάτω:

def index
  @rooms = Room.all
end

Ανοίξτε την προβολή app/views/rooms/index.html.erb και αλλάξτε τα περιεχόμενά της στα παρακάτω[8]:

<div class="row">
  <div class="col-12 col-md-3">
    <div class="mb-3">
      <%= link_to new_room_path, class: "btn btn-primary" do %>
        Create a room
      <% end %>
    </div>

    <% if @rooms.present? %>
      <nav class="nav flex-column">
        <% @rooms.each do |room| %>
          <%= link_to room.name, room_path(room), class: "nav-link room-nav-link" %>
        <% end %>
      </nav>
    <% else %>
      <div class="text-muted">
        The are no rooms
      </div>
    <% end %>
  </div>

  <div class="col">
    <div class="alert alert-primary">
      <h4 class="alert-heading">
        Welcome to the RailsChatTutorial!
      </h4>

      <p>
        We need to talk.
      </p>

      <hr />

      <p>
        You can create or join a room from the sidebar.
      </p>
    </div>
</div>

Αν υπάρχουν δωμάτια, η αριστερή στήλη θα εμφανίζει μια κάθετη πλοήγηση με συνδέσμους που οδηγούν στη σελίδα κάθε δωματίου. Η δεξιά στήλη εμφανίζει ένα απλό μήνυμα καλωσορίσματος στην εφαρμογή.

Room index

Πατώντας το κουμπί Create a room εμφανίζεται ένα αναμενόμενο λάθος καθώς ο ελεγκτής δεν υποστηρίζει ακόμη την ενέργεια δημιουργίας δωματίου.

Δημιουργία και επεξεργασία δωματίου

Θα ορίσουμε τις ενέργειες για τη δημιουργία και την επεξεργασία ενός δωματίου.

Ανοίξτε τον ελεγκτή των δωματίων app/controllers/rooms_controller.rb και αλλάξτε τα περιεχόμενά του ως εξής:

class RoomsController < ApplicationController
  # Loads:
  # @rooms = all rooms
  # @room = current room when applicable
  before_action :load_entities

  def index
    @rooms = Room.all
  end

  def new
    @room = Room.new
  end

  def create
    @room = Room.new permitted_parameters

    if @room.save
      flash[:success] = "Room #{@room.name} was created successfully"
      redirect_to rooms_path
    else
      render :new
    end
  end

  def edit
  end

  def update
    if @room.update_attributes(permitted_parameters)
      flash[:success] = "Room #{@room.name} was updated successfully"
      redirect_to rooms_path
    else
      render :new
    end
  end

  protected

  def load_entities
    @rooms = Room.all
    @room = Room.find(params[:id]) if params[:id]
  end

  def permitted_parameters
    params.require(:room).permit(:name)
  end
end

Σημείωση: φορτώνουμε από πριν τις μεταβλητές που κρατούν τα δωμάτια (@rooms) και το τρέχον δωμάτιο (@room) ώστε να είναι διαθέσιμες σε όλες τις ενέργειες χρησιμοποιώντας την οδηγία before_action :load_entities (action hook).

Θα δημιουργήσουμε μια απλή φόρμα για το μοντέλο Room και θα τη χρησιμοποιήσουμε και για τη δημιουργία αλλά και για την επεξεργασία ενός δωματίου. Δημιουργήστε την προβολή app/views/rooms/_form.html.erb και προσθέστε τα παρακάτω:

<%= simple_form_for @room do |form| %>
  <%= form.input :name %>
  <%= form.submit "Save", class: 'btn btn-success' %>
<% end %>

Στη συνέχεια, δημιουργήστε τις προβολές των ενεργειών new και edit αντίστοιχα:

app/views/rooms/new.html.erb

<h1>
  Creating a room  
</h1>

<%= render partial: 'form' %>

app/views/rooms/edit.html.erb

<h1>
  Editing room <%= @room.name %>
</h1>

<%= render partial: 'form' %>

Ώρα να φτιάξουμε το πρώτο μας δωμάτιο. Από την αρχική σελίδα των δωματίων, πατήστε το κουμπί Create a room

New room

Σώστε το και ορίστε:

Room index with rooms

Προσθέστε την παρακάτω κλάση στο αρχείο app/assets/stylesheets/rails-chat-tutorial.scss για να βελτιώσουμε την εμφάνιση των δωματίων.

.room-nav-link {
  border: 1px solid lighten($primary, 40%);
  background: lighten($primary, 45%);

  & + .room-nav-link {
    border-top: 0 none;
  }
}

Room index with improved rooms

Σημείωση: Θα προσθέσουμε παρακάτω τον σύνδεσμο επεξεργασίας (edit) ενός δωματίου.

Πριν προχωρήσουμε στη σελίδα προβολής ενός δωματίου, θα αλλάξουμε τον κώδικα της αρχικής σελίδας των δωματίων ώστε να μπορούμε να χρησιμοποιήσουμε την αριστερή στήλη και μέσα στη σελίδα ενός δωματίου.

Δημιουργήστε το απόσπασμα προβολής app/views/rooms/_rooms.html.erb με τα εξής περιεχόμενα:

<div class="mb-3">
  <%= link_to new_room_path, class: 'btn btn-primary' do %>
    Create a room
  <% end %>
</div>

<% if @rooms.present? %>
  <nav class="nav flex-column">
    <% @rooms.each do |room| %>
      <%= link_to room.name, room_path(room), class: 'nav-link room-nav-link' %>
    <% end %>
  </nav>
<% else %>
  <div class="text-muted">
    The are no rooms
  </div>
<% end %>

και προσαρμόστε την αρχική σελίδα των δωματίων app/views/rooms/index.html.erb ώστε να το χρησιμοποιεί:

<div class="row">
  <div class="col-12 col-md-3">
    <%= render partial: 'rooms' %>
  </div>

  <div class="col">
    <div class="alert alert-primary">
      <h4 class="alert-heading">
        Welcome to the RailsChatTutorial!
      </h4>

      <p>
        We need to talk.
      </p>

      <hr />

      <p>
        You can create or join a room from the sidebar.
      </p>
    </div>
  </div>
</div>

Σελίδα δωματίου

Προσθέστε την ενέργεια προβολής ενός δωματίου show στον ελεγκτή των δωματίων app/controllers/rooms_controller.rb:

def show
  @room_message = RoomMessage.new room: @room
  @room_messages = @room.room_messages.includes(:user)
end

Σημειώσεις:

  • Δημιουργούμε ένα νέο μήνυμα (@room_message) για να το χρησιμοποιήσουμε στην προβολή όταν θα δημιουργήσουμε τη φόρμα για την υποβολή νέου μηνύματος.
  • Όταν εμφανίζουμε ένα μήνυμα στο δωμάτιο, προσπελαύνουμε την email διεύθυνση του χρήστη που το δημιούργησε για να μπορούμε να υπολογίσουμε το hash της gravatar εικόνας προφίλ του. Χρησιμοποιήσαμε λοιπόν την μέθοδο .includes(:user) στο ερώτημα της βάσης για τα μηνύματα δωματίου @room_messages ώστε να προσκομίζονται και οι χρήστες ταυτόχρονα για να αποφύγουμε το πρόβλημα με τα N+1 ερωτήματα[1].

Δημιουργήστε την προβολή δωματίου app/views/rooms/show.html.erb:

<h1>
  <%= @room.name %>
</h1>

<div class="row">
  <div class="col-12 col-md-3">
    <%= render partial: 'rooms' %>
  </div>

  <div class="col">
    <div class="chat">
      <% @room_messages.each do |room_message| %>
        <%= room_message %>
      <% end %>
    </div>

    <%= simple_form_for @room_message, remote: true do |form| %>
      <div class="input-group mb-3">
        <%= form.input :message, as: :string,
                                 wrapper: false,
                                 label: false,
                                 input_html: {
                                   class: 'chat-input'
                                 } %>
        <div class="input-group-append">
          <%= form.submit "Send", class: 'btn btn-primary chat-input' %>
        </div>
      </div>

      <%= form.input :room_id, as: :hidden %>
    <% end %>
  </div>
</div>

Σημειώσεις:

  • Επαναχρησιμοποιήσαμε το απόσπασμα προβολής app/views/rooms/_rooms.html.erb που δημιουργήσαμε στο προηγούμενο βήμα
  • Προσθέσαμε ένα div με κλάση .chat και σε αυτό θα εμφανίζονται τα μηνύματα του δωματίου
  • Προσθέσαμε μια φόρμα για το μήνυμα @room_message που αρχικοποιήσαμε στον ελεγκτή. Επίσης χρησιμοποιήσαμε την οδηγία remote: true όταν αρχικοποιήσαμε τη φόρμα συνεπώς η υποβολή της θα γίνεται μέσω Ajax.
  • Προσθέσαμε ένα κρυφό πεδίο για το χαρακτηριστικό :room_id ώστε να είναι διαθέσιμο στον ελεγκτή των μηνυμάτων RoomMessagesController κατά την υποβολή της φόρμας

Ας ομορφύνουμε τα στοιχεία της συνομιλίας προσθέτοντας τις παρακάτω γραμμές στο αρχείο app/assets/stylesheets/rails-chat-tutorial.scss:

.chat {
  border: 1px solid lighten($secondary, 40%);
  background: lighten($secondary, 50%);
  height: 50vh;
  border-radius: 5px 5px 0 0;
  overflow-y: auto;
}

.chat-input {
  border-top: 0 none;
  border-radius: 0 0 5px 5px;
}

Περιηγηθείτε σε ένα δωμάτιο για να δούμε τι έχουμε κάνει μέχρι στιγμής.

Room page with chat

Πατώντας το κουμπή Send δε συμβαίνει τίποτα στη σελίδα αλλά αν κοιτάξετε το αρχείο καταγραφής (log) του εξυπηρετητή θα παρατηρήσετε το εξής:

AbstractController::ActionNotFound (The action 'create' could not be found for RoomMessagesController):

Ας το διορθώσουμε.

Δημιουργία μηνυμάτων

Αυτό θα είναι απλό. Το μόνο που έχουμε να κάνουμε είναι να υλοποιήσουμε την ενέργεια της δημιουργίας - create στον ελεγκτή των μηνυμάτων - RoomMessagesController.

app/controllers/room_messages_controller.rb

class RoomMessagesController < ApplicationController
  before_action :load_entities

  def create
    @room_message = RoomMessage.create user: current_user,
                                       room: @room,
                                       message: params.dig(:room_message, :message)
  end

  protected

  def load_entities
    @room = Room.find params.dig(:room_message, :room_id)
  end
end

Σημειώσεις:

  • φορτώνουμε εκ των προτέρων το δωμάτιο χρησιμοποιώντας την παράμετρο room_id που προσθέσαμε προηγουμένως σαν κρυφό πεδίο στη φόρμα του μηνύματος
  • δημιουργούμε ένα νέο μήνυμα θέτοντας του ως χρήστη τον χρήστη που είναι πιστοποιημένος κατά την υποβολή

Αν δοκιμάσετε να υποβάλετε ένα μήνυμα τώρα, και πάλι δε θα δείτε να συμβαίνει κάτι στη σελίδα αλλά στα logs του εξυπηρετητή θα δείτε ότι το μήνυμα δημιουργήθηκε επιτυχώς.

Started POST "/room_messages" for ::1 at 2019-04-04 19:24:33 +0300
Processing by RoomMessagesController#create as JS
  Parameters: {"utf8"=>"✓", "room_message"=>{"message"=>"My first message", "room_id"=>"8"}, "commit"=>"Send"}
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ /home/iridakos/.rvm/gems/ruby-2.6.2@rails-chat-tutorial/gems/activerecord-5.2.3/lib/active_record/log_subscriber.rb:98
  Room Load (0.2ms)  SELECT  "rooms".* FROM "rooms" WHERE "rooms"."id" = ? LIMIT ?  [["id", 8], ["LIMIT", 1]]
  ↳ app/controllers/room_messages_controller.rb:13
   (0.1ms)  begin transaction
  ↳ app/controllers/room_messages_controller.rb:5
  RoomMessage Create (0.7ms)  INSERT INTO "room_messages" ("room_id", "user_id", "message", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["room_id", 8], ["user_id", 1], ["message", "My first message"], ["created_at", "2019-04-04 16:24:33.456641"], ["updated_at", "2019-04-04 16:24:33.456641"]]
  ↳ app/controllers/room_messages_controller.rb:5
   (4.0ms)  commit transaction
  ↳ app/controllers/room_messages_controller.rb:5
No template found for RoomMessagesController#create, rendering head :no_content
Completed 204 No Content in 88ms (ActiveRecord: 5.1ms)

Οι χρήστες περιμένουν το πεδίο του μηνύματος να καθαρίζει μετά την αποστολή ενός μηνύματος. Ας μην τους αφήσουμε να περιμένουν.

Δημιουργήστε ένα αρχείο app/assets/javascripts/room.js και προσθέστε τα παρακάτω:

$(function() {
  $('#new_room_message').on('ajax:success', function(a, b,c ) {
    $(this).find('input[type="text"]').val('');
  });
});

Συνδέουμε το συμβάν ajax:success που πυροδοτείται από το Rails στην επιτυχή υποβολή της φόρμας και το μόνο που κάνουμε είναι ότι καθαρίζουμε τα περιεχόμενα του πεδίου κειμένου του μηνύματος.

Φορτώστε εκ νέου τη σελίδα και προσπαθήστε να υποβάλετε ξανά και δείτε την αλλαγή που μόλις κάναμε. Το πεδίο του μηνύματος θα πρέπει να καθαρίσει μετά την υποβολή της φόρμας.

Εμφάνιση μηνυμάτων

Αν φορτώσετε πάλι τη σελίδα του δωματίου θα δείτε κάτι σαν αυτό:

Room message to string

Ας ομορφύνουμε και τα μηνύματα.

Αντικαταστήστε τα περιεχόμενα της προβολής του δωματίου app/views/rooms/show.html.erb με τα παρακάτω:

<h1>
  <%= @room.name %>
</h1>

<div class="row">
  <div class="col-12 col-md-3">
    <%= render partial: 'rooms' %>
  </div>

  <div class="col">
    <div class="chat">
      <% @room_messages.each do |room_message| %>
        <div class="chat-message-container">
          <div class="row no-gutters">
            <div class="col-auto text-center">
              <img src="<%= gravatar_url(room_message.user) %>" class="avatar" alt="">
            </div>

            <div class="col">
              <div class="message-content">
                <p class="mb-1">
                  <%= room_message.message %>
                </p>

                <div class="text-right">
                  <small>
                    <%= room_message.created_at %>
                  </small>
                </div>
              </div>
            </div>
          </div>
        </div>
      <% end %>
    </div>

    <%= simple_form_for @room_message, remote: true do |form| %>
      <div class="input-group mb-3">
        <%= form.input :message, as: :string,
                                 wrapper: false,
                                 label: false,
                                 input_html: {
                                   class: 'chat-input'
                                 } %>
        <div class="input-group-append">
          <%= form.submit "Send", class: 'btn btn-primary chat-input' %>
        </div>
      </div>

      <%= form.input :room_id, as: :hidden %>
    <% end %>
  </div>
</div>

και προσθέστε τις παρακάτω CSS κλάσεις μέσα στην κλάση .chat:

.chat-message-container {
  padding: 5px;

  .avatar {
    margin: 5px;
  }

  .message-content {
    padding: 5px;
    border: 1px solid $primary;
    border-radius: 5px;
    background: lighten($primary, 10%);
    color: $white;
  }

  & + .chat-message-container {
    margin-top: 10px;
  }
}

Φορτώστε ξανά τη σελίδα. Magic.

Improved display of room messages

WebSockets - ActionCable

Ήρθε η ώρα να χρησιμοποιήσουμε τα WebSockets μέσω της λειτουργικότητας ActionCable του Rails.

Action Cable seamlessly integrates WebSockets with the rest of your Rails application. It allows for real-time features to be written in Ruby in the same style and form as the rest of your Rails application, while still being performant and scalable. It’s a full-stack offering that provides both a client-side JavaScript framework and a server-side Ruby framework. You have access to your full domain model written with Active Record or your ORM of choice. Action Cable Overview @ Ruby on Rails Guides (v5.2.3)

Εγκατάσταση redis

Θα χρησιμοποιήσουμε τον προσαρμογέα (adapter) redis που αποτελεί μια ασφαλή επιλογή για παραγωγικά περιβάλλοντα σε αντίθεση με την επιλογή async.

Πρώτα πρέπει να εγκαταστήσετε την εφαρμογή redis στο σύστημά σας.

Για να το εγκαταστήσω σε Ubuntu χρειάστηκε απλά να εκτελέσω τις παρακάτω εντολές.

sudo apt update
sudo apt install redis-server

Για να ελέγξετε ότι η εγκατάσταση ήταν επιτυχής, βεβαιωθείτε ότι παίρνετε PONG όταν εκτελείτε:

$ redis-cli
127.0.0.1:6379> ping
PONG

Ρύθμιση του ActionCable

Αφου εργαζόμαστε σε περιβάλλον ανάπτυξης (development environment), ανοίξτε το αρχείο ρυθμίσεων του στοιχείου ActionCable config/cable.yml και αντικαταστήστε τα περιεχόμενά του με τα παρακάτω:

development:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: rails-chat-tutorial_development

test:
  adapter: async

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
  channel_prefix: rails-chat-tutorial_production

Σημείωση: προσθέσαμε την επιλογή channel_prefix επειδή:

Additionally, a channel_prefix may be provided to avoid channel name collisions when using the same Redis server for multiple applications Action Cable Overview # Redis Adapter @ Ruby on Rails Guides (v5.2.3)

Τέλος, θα προσθέσουμε την απαίτηση βιβλιοθήκης για τον προσαρμογέα redis στο μητρώο απαιτήσεων Gemfile:

gem 'redis'

Μην ξεχνάτε να εκτελείτε bundle κάθε φορά που αλλάζετε αυτό το αρχείο.

Ρύθμιση devise για πιστοποίηση συνδέσεων WebSocket

Όταν εγκαθίσταται μια σύνδεση WebSocket, δεν έχουμε πρόσβαση στη συνεδρία του χρήστη (user session) αλλά έχουμε πρόσβαση τα cookies του. Έτσι, για να μπορούμε να πιστοποιήσουμε τον χρήστη χρειάζεται να κάνουμε κάποια πράγματα σχετικά με το devise (credits to Greg Molnar).

Δημιουργήστε έναν αρχικοποιητή για τα “warden hooks” με το όνομα config/initializers/warden_hooks.rb και προσθέστε τις παρακάτω γραμμές:

Warden::Manager.after_set_user do |user,auth,opts|
  scope = opts[:scope]
  auth.cookies.signed["#{scope}.id"] = user.id
end

Warden::Manager.before_logout do |user, auth, opts|
  scope = opts[:scope]
  auth.cookies.signed["#{scope}.id"] = nil
end

Επεξήγηση: Προσθέτουμε ένα cookie με το αναγνωριστικό του χρήστη (user id) μετά την επιτυχή πιστοποίηση του και το αφαιρούμε κατά την έξοδό του από την εφαρμογή.

Ρύθμιση της σύνδεση WebSocket

Ανοίξτε το αρχείο 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.signed['user.id'])
        verified_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

Επεξήγηση:

Here identified_by is a connection identifier that can be used to find the specific connection later. Note that anything marked as an identifier will automatically create a delegate by the same name on any channel instances created off the connection. Action Cable Overview # Connection setup @ Ruby on Rails Guides (v5.2.3)

Στη μέθοδο find_verified_user προσπελαύνουμε το cookie που θέσαμε νωρίτερα στον αρχικοποιητή των warden hooks.

Δημιουργία του καναλιού δωματίου

A channel encapsulates a logical unit of work, similar to what a controller does in a regular MVC setup. Action Cable Overview # Channels @ Ruby on Rails Guides (v5.2.3)

Θα δημιουργήσουμε το κανάλι RoomChannel στο οποίο θα εγγράφονται όλες οι σελίδες δωματίων.

Δημιουργήστε το κανάλι με το όνομα app/channels/room_channel.rb και τα εξής περιεχόμενα:

class RoomChannel < ApplicationCable::Channel
  def subscribed
    room = Room.find params[:room]
    stream_for room

    # or
    # stream_from "room_#{params[:room]}"
  end
end

Επεξήγηση:

  • Η μέθοδος subscribed εκτελείται κάθε φορά που εγγράφεται κάποιος στο κανάλι και είναι υπεύθυνη να ρυθμίσει τη ροή μέσω της οποίας τα δεδομένα θα πηγαινοέρχονται.

Θα ρυθμίσουμε τον κώδικα της σελίδα δωματίου αργότερα με τέτοιο τρόπο ώστε κατά την εγγραφή στο κανάλι να αποστέλλεται και η παράμετρος του δωματίου room.

Έχουμε δύο επιλογές:

  • Να χρησιμοποιήσουμε τη μέθοδο stream_for: με αυτό τον τρόπο το Rails αυτόματα παράγει ένα όνομα ροής για το εκάστοτε μοντέλο (room στην περίπτωσή μας), για παράδειγμα: “room:asdfwer234”. Όταν στη συνέχεια θέλουμε να στείλουμε δεδομένα σε αυτή τη ροή, το μόνο που έχουμε να κάνουμε είναι να εκτελέσουμε τη μέθοδο RoomChannel.broadcast_to(room_object, data) όπου το Rails θα ανακτήσει αυτόματα το όνομα της ροής βάσει της παραμέτρου room_object. Με άλλα λόγια, δε χρειάζεται εμείς να υπολογίζουμε το όνομα μιας ροής δωματίου στην οποία θέλουμε να στείλουμε/λάβουμε δεδομένα.
    • αυτή η επιλογή είναι μόνο όταν το κανάλι χειρίζεται εγγραφές (subscriptions) που συνδέονται με μοντέλα, στην περίπτωσή μας κάποιο δωμάτιο
  • Να χρησιμοποιήσουμε τη μέθοδο stream_from: ορίζουμε μόνοι μας το όνομα της ροής και στη συνέχεια, όταν θέλουμε να στείλουμε δεδομένα στη ροή χρησιμοποιούμε: ActionCable.server.broadcast("room_#{a_room_id_here}", data).

Διαβάστε περισσότερα εδώ.

Αναμετάδοση μηνυμάτων

Κάθε φορά που δημιουργείται ένα μήνυμα, θέλουμε να το αναμεταδώσουμε στη ροή του καναλιού για το συγκεκριμένο δωμάτιο. Για να το κάνουμε αυτό, αλλάξτε την ενέργεια δημιουργίας μηνύματος - create του αντίστοιχου ελεγκτή app/controllers/room_messages_controller.rb ως εξής:

def create
  @room_message = RoomMessage.create user: current_user,
                                     room: @room,
                                     message: params.dig(:room_message, :message)

  RoomChannel.broadcast_to @room, @room_message
end

Επεξήγηση: προσθέσαμε τη γραμμή RoomChannel.broadcast_to @room, @room_message που θα αναμεταδώσει στη ροή του καναλιού του δωματίου (όπως εξηγήσαμε προηγουμένως) το μήνυμα @room_message μετασχηματισμένο σε μορφή JSON μέσω της μεθόδου to_json.

Έτσι, στην άλλη πλευρά, στην πλευρά του πελάτη δηλαδή, θα παραλάβουμε την JSON μορφή του μοντέλου RoomMessage. Για να δούμε πως είναι αυτή:

{
  "id":29,
  "room_id":8,
  "user_id":1,
  "message":"My first message",
  "created_at":"2019-04-04T17:09:00.637Z",
  "updated_at":"2019-04-04T17:09:00.637Z"
}

Έχουμε σκοπό να εμφανίσουμε τα μηνύματα στη σελίδα του δωματίου μέσω Javascript και αυτή η πληροφορία που λαμβάνουμε προς το παρόν δεν είναι αρκετή. Μας λείπει η διεύθυνση της εικόνας προφίλ gravatar. Ας την προσθέσουμε.

Ανοίξτε το μοντέλου του χρήστη app/models/user.rb και προσθέστε την παρακάτω μέθοδο.

def gravatar_url
  gravatar_id = Digest::MD5::hexdigest(email).downcase
  "https://gravatar.com/avatar/#{gravatar_id}.png"
end

Έχουμε ήδη υλοποιήσει αυτή τη μέθοδο στις βοηθητικές μεθόδους app/helpers/application_helper.rb αλλά δε θα τη χρειαστούμε πλέον οπότε αφαιρέστε τη.

Ενημερώστε το απόσπασμα της μπάρας πλοήγησης app/views/shared/_navigation_bar.html.erb ώστε ο υπολογισμός της διεύθυνσης της εικόνας προφίλ να γίνεται πλέον από τη μέθοδο που μόλις δημιουργήσαμε:

<img class="avatar" src="<%= current_user.gravatar_url %>">

Ενημερώστε και τη σελίδα δωματίου app/views/rooms/show.html.erb αλλάξτε τη και εκεί επίσης, με:

<img src="<%= room_message.user.gravatar_url %>" class="avatar" alt="">

Τέλος, θα αλλάξουμε το μετασχηματισμό JSON των μηνυμάτων - RoomMessage ώστε να εμπεριέχει και τη διεύθυνση της εικόνας προφίλ:

app/models/room_message.rb

def as_json(options)
  super(options).merge(user_avatar_url: user.gravatar_url)
end

Ας επιβεβαιώσουμε ότι ο μετασχηματισμός είναι επιτυχής:

{
  "id":29,
  "room_id":8,
  "user_id":1,
  "message":"My first message",
  "created_at":"2019-04-04T17:09:00.637Z",
  "updated_at":"2019-04-04T17:09:00.637Z",
  "user_avatar_url":"https://gravatar.com/avatar/02a28db6886d578f75a820b50f2dd334.png"
}

Μια χαρά, προχωράμε.

Εγγραφή στη ροή δωματίου

Θα προσθέσουμε κάποια δεδομένα στη σελίδα των δωματίων ώστε να μπορούμε να τα χρησιμοποιήσουμε μέσω Javascript για να πραγματοποιούμε εγγραφή στις κατάλληλες ροές κάθε φορά που επισκεπτόμαστε ένα δωμάτιο.

Ανοίξτε το αρχείο app/views/rooms/show.html.erb και αλλάξτε τη γραμμή στην οποία ορίζουμε το div chat ως εξής:

<div class="chat" data-channel-subscribe="room" data-room-id="<%= @room.id %>">

Επεξήγηση: Προσθέσαμε δύο χαρακτηριστικά δεδομένων, ένα που ορίζει σε ποιο κανάλι θέλουμε να γραφτούμε (room) και ένα που ορίζει σε ποιο συγκεκριμένο δωμάτιο βρισκόμαστε (@room.id).

Στο τέλος του αρχείου προσθέστε το παρακάτω απόσπασμα κώδικα:

<div class="d-none" data-role="message-template">
  <div class="chat-message-container">
    <div class="row no-gutters">
      <div class="col-auto text-center">
        <img src="" class="avatar" alt="" data-role="user-avatar">
      </div>

      <div class="col">
        <div class="message-content">
          <p class="mb-1" data-role="message-text"></p>

          <div class="text-right">
            <small data-role="message-date"></small>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Αυτό το απόσπασμα θα χρησιμοποιείται σαν πρότυπο (template) για κάθε εισερχόμενο μήνυμα. Κάθε φορά που έρχεται κάποιο μήνυμα θα:

  • κλωνοποιούμε αυτό το απόσπασμα HTML
  • θα αλλάζουμε τις τιμές των κατάλληλων στοιχείων του κλωνοποιημένου πλέον αποσπάσματος και τέλος
  • θα προσαρτούμε τον παραγόμενο HTML κώδικα στο τέλος του div chat.

Τώρα θα δημιουργήσουμε το Javascript που θα είναι υπεύθυνο για τις εγγραφές στις ροές των καναλιών και για τον χειρισμό των εισερχόμενων δεδομένων.

Δημιουργήστε το αρχείο app/assets/javascripts/channels/room_channel.js και προσθέστε τον παρακάτω κώδικα:

$(function() {
  $('[data-channel-subscribe="room"]').each(function(index, element) {
    var $element = $(element),
        room_id = $element.data('room-id')
        messageTemplate = $('[data-role="message-template"]');

    $element.animate({ scrollTop: $element.prop("scrollHeight")}, 1000)        

    App.cable.subscriptions.create(
      {
        channel: "RoomChannel",
        room: room_id
      },
      {
        received: function(data) {
          var content = messageTemplate.children().clone(true, true);
          content.find('[data-role="user-avatar"]').attr('src', data.user_avatar_url);
          content.find('[data-role="message-text"]').text(data.message);
          content.find('[data-role="message-date"]').text(data.updated_at);
          $element.append(content);
          $element.animate({ scrollTop: $element.prop("scrollHeight")}, 1000);
        }
      }
    );
  });
});

Επεξήγηση:

  • Για κάθε στοιχείο που έχει χαρακτηριστικό δεδομένων (data attribute) channel-subscribe με τιμή room
    • Δημιούργησε μια εγγραφή (subscription) στο κανάλι “RoomChannel” περνώντας ως παράμετρο με όνομα room την τιμή του room-id χαρακτηριστικού δεδομένων (θυμηθείτε αυτή τη γραμμή στο κανάλι των δωματίων (RoomChannel): Room.find params[:room])
      • Όταν έρχονται δεδομένα, κλωνοποίησε το πρότυπο και άλλαξε τα περιεχόμενά του βάσει των χαρακτηριστικών του εισερχόμενου αντικειμένου.
      • Πρόσθεσε το νέο περιεχόμενο στο div chat και τέλος
      • Χρησιμοποίησε animation για να εντυπωσιάσεις τους χρήστες μετακινώντας σταδιακά την τρέχουσα θέση του div μηνυμάτων στο κάτω μέρος του (αν δεν έγινε κατανοητό στα ελληνικά, εννοώ να γίνει smooth scrolling στο bottom του div :).

Αυτάααααααα! Μεγάλο άρθρο, κουράστηκε η γάτα.

Tired cat photo

Ευχαριστίες

Ευχαριστώ πολύ για τα σχόλια και το feedback!