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:
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:
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:
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:
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.
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 ;)
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:
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.