Tag: chat

Juggernaut Rails Chat – część VII (emotikonki i dźwięk)

Jeśli dobrnąłeś tutaj i masz działający czat to gratuluję!  W tej, już na pewno ostatniej części zajmiemy się rzeczą może nie aż tak istotną, za to fajną. Dorobimy emotikonki oraz dźwięk nadejścia wiadomości.

Emotikonki

Zacznijmy od emotków. Sprawa wygląda dość prosto. Musimy zastępować tylko emotki tekstowe, takie jak: ":)" wersjami graficznymi czyli po prostu plikami png.

Aby to zrobić napisałem niewielką klasę w libs/:
Klasa emoticoner.rb

class Emoticoner
  ICONS = {
    :smile => {:string => [":)", ";)["], :icon => "smile.png"},
    :happy => {:string => [":D", ";D", ":d", ";d"], :icon => "happy.png"},
    :sad => {:string => [":(", ";("], :icon => "sad.png"},
    :tongue => {:string => [":p", ";p", ":P", ";P"], :icon => "tongue.png"},
  }

  PATH = '/images/emoticons'

  def self.emoticate(msg)
    ICONS.each do |name, row_data|
      row_data[:string].each do |ico|
        msg = msg.gsub(ico, "<img src=\"#{PATH}/#{row_data[:icon]}\" alt=\"\" />")
      end
    end
    msg
  end
end

Jak widzimy, podajemy w stałej ICONS klucz a następnie tablicę "buziek" które mają zostać zastąpione, oraz info na co.
W metodzie statycznej emoticate następuje ta zamiana i zwrócenie już poprawnie wypełnionego ikonkami tekstu.

W stałej PATH przechowujemy informację na temat ścieżki do plików z ikonkami. Teraz wystarczy dodać include w kontrolerze chat:

require ('emoticoner.rb')

I zastąpić przypisanie z parametru do zmiennej msg, następującym kodem:

    msg = Emoticoner.emoticate(h(params[:chat_input]))

Oczywiście do (w moim wypadku) katalogu /images/emoticons wrzucamy pliki z buźkami.

Dźwięk wiadomości

Dorobienie dźwięku wiadomości także nie jest trudne.

Zamieszczamy poniższy kod javascript w kodzie naszej strony:

function DHTMLSound(surl) {
  document.getElementById("dummyspan").innerHTML=
    "<embed src='"+surl+"' hidden=true autostart=true loop=false>";
}

Dodajemy po tagu otwierającym body, taki oto kod html (u nas w layoucie rooms.erb:

<span id=dummyspan></span>

Do akcji send_data dopisujemy jako ostatni element renderu do juggernauta:

      page << "DHTMLSound('/sounds/notice.wav');"

I pamiętamy aby umieścić plik z dzwiękiem w katalogu do którego wskazujemy :)

Mamy juz i emotikonki i dźwięki.

Poniżej zamieszczam kod źródłowy całego projektu. Działa na pewno, ponieważ to na jego bazie robiłem printscreeny :)

Źródełka

Cały zamieszczony tutaj kod możesz dowolnie wykorzystywać w celach edukacyjnych.

Części tutorialu:

  1. Juggernaut Rails Chat – część I (czym jest Juggernaut i jak go zainstalować)
  2. Juggernaut Rails Chat – część II (design)
  3. Juggernaut Rails Chat – część III (rejestracja i logowanie)
  4. Juggernaut Rails Chat – część IV (zarządzanie pokojami)
  5. Juggernaut Rails Chat – część V (łączenie użytkowników z pokojami)
  6. Juggernaut Rails Chat – część VI (odpalamy Juggernaut i nasz chat)
  7. Juggernaut Rails Chat – część VII (emotikonki i dźwięk)

Juggernaut Rails Chat – część VI (odpalamy Juggernaut i nasz chat)

Witam w ostatniej (istotnej) części tutoriala w ktorym tworzymy czat. Oto do czego doszliśmy w przeciągu poprzednich 5 części:

  • prosty layout + logo
  • model użytkownika
  • model pokoi
  • relacja między użytkownikami i pokojami
  • rejestracja użytkowników
  • logowanie użytkowników
  • uwierzytelnianie użytkowników
  • zarządzanie (tworzenie/usuwanie/edycja) pokoi
  • ochrona zasobów przeznaczonych tylko dla administratora
  • do tego wszystkiego CSS i xHTML

W tej części zajmiemy się tym do czego zmierzaliśmy od samego początku. W końcu uruchomimy nasz czat i napiszemy pierwsze wiadomości :)

Instalacja pluginu

Mając zainstalowany gem, musimy zainstalować plugin w naszym kodzie:

ruby script/plugin install http://juggernaut.rubyforge.org/svn/trunk/juggernaut

Następnie w pliku juggernaut.yml w katalogu głównym naszego systemu (nie w app) wpisujemy:


:allowed_ips:
 - 127.0.0.1
:store_messages: true
:port: 5001

:logout_connection_url: http://localhost:3000/chat/juggernaut_connection_logout
:subscription_url:  http://localhost:3000/chat/juggernaut_subscription

Podaliśmy w nim ustawienia dla serwera rozgłoszeniowego:

  • allowed_ips - z jakich adresów można się łączyć z serwerem
  • store_messages - czy ma zapamiętywać wysyłane wiadomości
  • port - port na jakim ma pracować serwer rozgłoszeniowy
  • logout_connection_url - do jakiego kontrolera i akcji ma trafic callback w momencie zamkniecia socketa
  • subscription_url - gdzie ma trafić callback z otwarcia socketa

Teraz w config/juggernaut_hosts.yml musimy poinformować railsy gdzie Juggernaut pracuje:

:hosts:
  - :port: 5001
    :host: 127.0.0.1
    :public_host: 127.0.0.1
    :public_port: 5001

Tego chyba nie trzeba tłumaczyć ;)

Kontroler czatu

W zasadzie zrobiliśmy wszystko co trzeba żeby uruchomić juggernauta, jednak nie mamy czego rozgłaszać. W tym celu utworzymy kontroler czat który będzie odpowiadał za wybór pokoju (akcja index) oraz za obsługę pokoju (room). Link do pokoju będzie parametrem w URLu.

class ChatController < ApplicationController
  protect_from_forgery :only => [:index]
  before_filter :authorize, :except => [:juggernaut_connection_logout, :juggernaut_subscription]
  before_filter :check_room, :only => [:rooms, :send_data]

  def index
    @rooms = ChatRoom.find(:all)
    render :layout => 'chat'
  end

  def rooms
    render :layout => 'rooms'
  end

  def send_data
    # Stosujemy h ponieważ userzy nie mogą przesyłać niczego poza tekstem
    msg = h(params[:chat_input])

    render :juggernaut => {:channels => [@room.id], :type => :send_to_channels} do |page|
      date = "[#{Time.now.strftime("%d-%m-%y %H:%M")}] "
      nick = "<div id=\"nick\" style=\"color: #{session[:color]}\">#{@user.login.capitalize}</div>"
      page.insert_html :bottom, 'chat_data', date+nick+': '+msg+"<br/>"
      page << 'var objDiv = document.getElementById("chat_data"); objDiv.scrollTop = objDiv.scrollHeight;'
    end
    render :nothing => true
  end

  def juggernaut_connection_logout
    room = ChatRoom.find(params[:channels].first.to_i)
    user = User.find(params[:client_id])
    ChatRoomsUser.disconnect(user, room)
    users_list = ''
    room.users.each { |u| users_list+= "<li>#{u.login.capitalize}</li>" }
    render :juggernaut => {:channels => [params[:channels].first.to_i], :type => :send_to_channels} do |page|
      page.replace_html 'users_list', users_list
    end

    render :nothing => true, :status => 200
  end

  def juggernaut_subscription
    room = ChatRoom.find(params[:channels].first.to_i)
    user = User.find(params[:client_id])

    ChatRoomsUser.connect(user, room)

    users_list = ''
    room.users.each { |u| users_list+= "<li>#{u.login.capitalize}</li>" }
    render :juggernaut => {:channels => [params[:channels].first.to_i], :type => :send_to_channels} do |page|
      page.replace_html 'users_list', users_list
    end

    render :nothing => true, :status => 200

  end

  private

  def check_room
    @room = ChatRoom.find_by_link(params[:id])
    if @room.nil?
      flash[:error] = "Wybrany pokój nie istnieje"
      redirect_to :action => :index
    end
  end

end

Kod na pierwszy rzut oka może wydawać się skomplikowany, jednak już go omawiam:

  before_filter :authorize, :except => [:juggernaut_connection_logout, :juggernaut_subscription]
  before_filter :check_room, :only => [:rooms, :send_data]

Dlaczego nie autoryzujemy dwóch juggernautowych metod? Ponieważ juggernaut nie jest użytkownikiem i sam nie przeszedłby autoryzacji. Są inne metody zabezpieczenia tych akcji, jednak tutorial miał być prosty więc nie przesadzajmy ;)
To samo tyczy się sprawdzania czy pokój którego żądamy istnieje. W filtrze tym który jest na samym końcu kontrolera, sprawdzamy czy taki pokój jaki mamy jako parametr istnieje. Jeśli nie, to przenosimy usera do listy pokoi. Jeśli istnieje to niech sobie czatuje :)

Tak jak wspomniałem wyżej, akcja index wyświetla tylko listę pokoi. Akcja rooms w zasadzie ma za zadanie wyświetlenie odpowiedniego widoku w danym pokoju. Do widoków jednak dojdziemy za chwil parę.

Cała magia dzieje się w akcji send_data:

  def send_data
    # Stosujemy h ponieważ userzy nie mogą przesyłać niczego poza tekstem
    msg = h(params[:chat_input])

    render :juggernaut => {:channels => [@room.id], :type => :send_to_channels} do |page|
      date = "[#{Time.now.strftime("%d-%m-%y %H:%M")}] "
      nick = "<div id=\"nick\" style=\"color: #{session[:color]}\">#{@user.login.capitalize}</div>"
      page.insert_html :bottom, 'chat_data', date+nick+': '+msg+"<br/>"
      page << 'var objDiv = document.getElementById("chat_data"); objDiv.scrollTop = objDiv.scrollHeight;'
    end
    render :nothing => true
  end

Najpierw zapisujemy sobie to co user przesłał ajaxem (tekst z params[:chat_input]), pozbywając się przy okazji ewentualnych niebezpiecznych fragmentów (h).
Następnie renderujemy do juggernauta, podając jako parametry kanał (zmienna @room została zainicjowana w filtrze) i typ który rozgłoszenia.
Następnie wpisujemy co ma być wysłane i co ma się z tym stać. Tutaj jest dość prosto - pobieramy znacznik czasu w formacie [DD-MM-YY HH:MM] żeby wyświetlić go przed wiadomością. Kolorujemy nick wartością losową którą wygenerowaliśmy w czasie logowania, dołączamy do tego tekst wiadomości i w zasadzie tyle.
Reszta to wstawienie htmla na końcu zawartości diva z tekstem z czatu (chat_data) oraz ustawienie suwaga na dole. Domyślnie gdy dodajemy za dużo zawartości, suwak zostanie u góry diva. Kod z ostatniej linijki sprawi że suwak ten będzie na dole po otrzymaniu wiadomości. Na samym końcu renderujemy z railsów nic, ponieważ przesłaniem do klienta zajmuje się serwer rozgłoszeniowy.

W zasadzie tyle :) prawda że proste?

Callbacki które wywoływane są gdy ktoś otworzy socket lub go zamknie, są praktycznie takie same, omówię więc tylko jeden:

  def juggernaut_subscription
    room = ChatRoom.find(params[:channels].first.to_i)
    user = User.find(params[:client_id])

    ChatRoomsUser.connect(user, room)

    users_list = ''
    room.users.each { |u| users_list+= "<li>#{u.login.capitalize}</li>" }
    render :juggernaut => {:channels => [params[:channels].first.to_i], :type => :send_to_channels} do |page|
      page.replace_html 'users_list', users_list
    end

    render :nothing => true, :status => 200

  end

Tutaj jest akcja obsługująca callback wykonywany po utworzeniu socketa. Znajdujemy pokój w którym jesteśmy oraz usera który się zgłosił (po listę parametrów przekazywanych z Juggernauta - patrz docsy Juggernauta). Mamy ID kanału oraz ID klienta które dostarcza nam Juggernaut. Następnie łączymy ze sobą usera i pokój, metodą utworzoną w poprzedniej części tutoriala. Później składamy listę loginów userów i zamieniamy ją z poprzednią. Robimy tak ponieważ w momencie podłączenia się do pokoju pojawia się nowy user więc trzeba zaktualizować listę.

Callback przy tworzeniu socketa ma jeszcze jedną własność. Mianowicie jeśli nie zwrócimy mu statusu 200 zamknie socket. Można to wykorzystać tworząc np pokoje dla wybranych osób, bądź pisząc systemy z jakimiś zaawansowanymi metodami weryfikacji. Nam tego nie potrzeba więc 200 zwracamy zawsze.

juggernaut_connection_logout działa praktycznie tak samo. Aktualizuje listę gości pokoju w momencie gdy ktoś przerwie połączenie. Rozłącza także usera zamiast go podłączać.

Wygląd czatu

Pozostało nam opracować jeszcze tylko wygląd czatu. Będzie się on troszkę różnił od tego który stworzyliśmy w części drugiej, ponieważ zrobimy taką małą konsolę błędów. Jednak sama koncepcja niewiele się zmieni. Oprócz layoutu main który już mamy, potrzebujemy jeszcze dwa inne. Nazywają się trochę niefortunnie ale z lenistwa już nie zmienię.

chat.erb - layout którego używamy w akcji index do wyświetlania listy dostępnych pokoi:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
  PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xml:lang="pl" lang="pl" >
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <meta name="language" content="pl"/>
    <meta name="description" content="Racer - Rails Chatter" />
    <%= stylesheet_link_tag 'chat' %>
    <title>Racer - Rails Chatter</title>
  </head>
  <body>
    <div id="contener">
      <div id="logo"></div>
      <div id="menu">
        Zalogowany jako: <%= @user.login.capitalize %>
        <%= link_to "Wyloguj", :controller => :main, :action => :logout %>
      </div>
      <% if flash[:error] %>
        <%= "<div id=\"error\">#{flash[:error]}</div>" %>
      <% end %>
      <% if flash[:message] %>
        <%= "<div id=\"message\">#{flash[:message]}</div>" %>
      <% end %>
      <%= yield %>
    </div>
  </body>
</html>

I CSS do niego:


#logo {
    background: url('/images/racer_logo.png') no-repeat;
    width: 430px; height: 220px; float: left;
}

#menu {
    float: right; width: 400px; font-size: 12px;
    margin-top: 200px; text-align: right;
}

#menu a {
    text-decoration: none; margin-left: 40px;
}

#contener {
    margin:0 auto;
    width: 1000px;
}

#chat_data, #console_text {
    width: 100%; height:300px;
    overflow: auto; border: 1px solid black;
    float: left;
    font-size: 13px;
}

#nick {
    display: inline;
}

#chat {
    width: 65%; height: 400px;
    border: 1px solid black; float: left;
    padding: 10px;
}

#options {
    width: 30%; height: 400px; float: right; border: 1px solid green;
    padding: 10px;
}

#console {
    width: 98%; height: 200px; float: left; border: 1px solid red;
    padding: 10px; margin-top: 10px;
}

#console_text {
    height: 150px;
}

#chat_input {
    width: 500px; border: 1px solid black;
}

#chats_list {
    width: 100%; float: left; border: 1px solid black;
    margin-top: 50px; background: #ffffff;
    padding: 20px;
    -moz-border-radius: 24px;
    -webkit-border-radius: 24px;
}
#users li {
    list-style: none;
}
#users {
    height: 340px; overflow: auto;
}

Tego cssa uzywac będziemy także w ponizszym layoucie który jest szkieletem do wyswietlania pokoju rozmów:

rooms.erb:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
  PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xml:lang="pl" lang="pl" >
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <meta name="language" content="pl"/>
    <meta name="description" content="Racer - Rails Chatter" />
    <%= stylesheet_link_tag 'chat' %>
    <title>Racer - Rails Chatter</title>
    <%= javascript_include_tag :defaults, :juggernaut, "actions" %>
    <script>
      function send(obj) {
        new Ajax.Request('/chat/send_data/<%= @room.link%> ', {
        asynchronous: true,
        evalScripts:  true,
        onComplete:   function(){
            $('chat_input').value = '';
        },
        parameters:Form.serialize(obj)
    });
    return false;
    }
    </script>

  </head>
  <body>
    <span id=dummyspan></span>
    <div id="contener">
      <div id="logo"></div>
      <div id="menu">
        Zalogowany jako: <%= @user.login.capitalize %>
        <%= link_to "Pokoje", :controller => :chat %>
        <%= link_to "Wyloguj", :controller => :main, :action => :logout %>
      </div>
      <%= yield %>
    </div>
    <%=
      # this will render the javascript that will 'cause listening to listen_to_channel channel
      juggernaut(:channels => [@room.id], :client_id => @user.id)
    %>
  </body>
</html>

Ten layout ma kilka dość specyficznych rzeczy. Pierwszą jest dołączenie pliku js "actions". Jego kod zamieszczę tutaj za moment - chociaż na dobrą sprawę nic w nim nie ma niezbędnego - dwa kawałki kodu do obsługi konsoli błędów do debuggingu. Sam chat działałby bez niego tak samo.

Dołączamy też plik JS juggernauta oraz metodę:

      function send(obj) {
        new Ajax.Request('/chat/send_data/<%= @room.link%> ', {
        asynchronous: true,
        evalScripts:  true,
        onComplete:   function(){
            $('chat_input').value = '';
        },
        parameters:Form.serialize(obj)
    });
    return false;
    }

która odpowiada za ajaxowe requesty do railsów z wiadomością która ma być wysłana. Dzięki (znowu) inicjowaniu zmiennej @room na poziomie filtra mamy ją dostępną i tutaj. Po pozytywnym przesłaniu ajaxem wiadomości do rozgłoszenia w pokoju, pole wprowadzania wiadomości jest czyszczone. Tyle ;)

Dalej tuż przed tagiem zamknięcia body, mamy:

    <%=
      juggernaut(:channels => [@room.id], :client_id => @user.id)
    %>

Ustawiamy tutaj nasłuch na kanał i przedstawiamy się.

Poniżej zawartość pliku actions.js:

window.console = {
    firebug: '1.0',
    log:     function() {
        $('console_text').innerHTML = arguments[0] + "\n" + $('console_text').innerHTML;
    },
    clear:   function() {
        $('console_text').innerHTML = '';
    }
}

function alert() {
    console.log(arguments[0])
}

Do tego wszystkiego mamy dwa widoki kontrolera chat:

index.erb:

<div id="chats_list">
  <h3>Wybierz pokój</h3>
  <ul id="chats">
    <% @rooms.each do |room| %>
    <li><%= link_to room.name, :controller => :chat, :action => :rooms, :id => room.link %></li>
    <% end %>
  </ul>
</div>

W którym za bardzo nie ma co tłumaczyć, oraz widok rooms.erb:

<div id="chat">
  <fieldset id="chat_data_wrapper">
    <legend>
      Rozmowa - <%= @room.name %> ------ <button onclick="$('chat_data').innerHTML=''">Wyczyść</button>
    </legend>
    <div id="chat_data"></div>
  </fieldset>
  <br/>
  <form action="/chat/send_data" method="post" onsubmit="javascript: send(this.parentNode); return false;">
    <input id="chat_input" name="chat_input" size="20" type="text" value="" />
    <input type="button" onclick="javascript: send(this.parentNode)" value="Wyślij" name="commit" />
  </form>
</div>

<div id="options">
  <fieldset id="chat_data_wrapper">
    <legend>
      Użytkownicy
    </legend>
    <div id="users">
      <ul id="users_list"></ul>
    </div>
  </fieldset>
</div>

<div id="console">
  <fieldset id="console_wrapper">
    <legend>
      Konsola błędów ------
      <button onclick="console.clear()">Wyczyść</button>
    </legend>
    <textarea id="console_text"></textarea>
  </fieldset>
</div>

W tym widoku mamy dwukrotnie metodę send, ponieważ możemy wpisać wiadomość w polu i dać enter lub nacisnąć wyślij. W obu przypadkach wykona się metoda send która ajaxem puści naszą wiadomość do Railsów które rozgłoszą ją za pośrednictwem Juggernauta do innych gości w pokoju. Mamy też listę użytkowników która załaduje się dynamicznie - dlatego niczego na początku w niej nie ma. Mamy też naszą mini konsolę błędów.

I to by było na tyle :)

Pozostaje nam uruchomić Railsy oraz Juggernauta. Railsy uruchamiamy standardowo - zaś Juggernauta tak:
Na ubuntu musimy sobie do patha wyeksportować do Patha to:

export PATH=$PATH:/var/lib/gems/1.8/bin

A następnie z naszego katalogu głównego wywołujemy:

juggernaut -c juggernaut.yml

I otrzymujemy piękne:

Starting Juggernaut server 0.5.8 on port: 5001...

Wchodzimy na nasz czat i korzystamy dowoli :)

Części tutorialu:

  1. Juggernaut Rails Chat – część I (czym jest Juggernaut i jak go zainstalować)
  2. Juggernaut Rails Chat – część II (design)
  3. Juggernaut Rails Chat – część III (rejestracja i logowanie)
  4. Juggernaut Rails Chat – część IV (zarządzanie pokojami)
  5. Juggernaut Rails Chat – część V (łączenie użytkowników z pokojami)
  6. Juggernaut Rails Chat – część VI (odpalamy Juggernaut i nasz chat)
  7. Juggernaut Rails Chat – część VII (emotikonki i dźwięk)

Kod źródłowy znajduje się na końcu VII części w której opisuję jak zrobić emotikonki oraz dźwięk przyjścia wiadomości.
Poniżej printscreen z działania czatu.

chat

Copyright © 2024 Closer to Code

Theme by Anders NorenUp ↑