Önceki bölümü okumadan buraya geldiyseniz hiç kasmayın geri dönün
- ORTA SEVİYE BİR RAİLS TUTORIAL - 1 (Kurulum, ilk stiller)
- ORTA SEVİYE BİR RAİLS TUTORIAL - 2 (Kullanıcı girişi - Bootstrap CSS)
- ORTA SEVİYE BİR RAİLS TUTORIAL - 3 (Blog altyapısı)
- ORTA SEVİYE BİR RAİLS TUTORIAL - 4 (JavaScript stiller)
- ORTA SEVİYE BİR RAİLS TUTORIAL - 5 (Kolleksiyon sayfaları)
- ORTA SEVİYE BİR RAİLS TUTORIAL - 6 (Ana sayfa güncellemesi)
Anlık mesajlaşma
Kullanıcılar gönderi yayınlayabiliyor veya diğer kullanıcıların gönderilerini okuyabiliyor. Fakat diğerleri ile iletişim kurma imkanları yok. Kolay ve hızlı olsun diye basit bir mail kutusu kurabiliriz. Ama bu birileriyle iletişim kurmanın çok eski bir yolu. Anlık mesajlaşma hem kullanım olarak rahat hem de geliştirme için daha heyecan verici.
Neyseki Rails gerçek zaman özelliklerini kolayca uygulayabilen Action Cables‘a sahip. Ana prensibi HTTP protokolü yerine WebSockets protokolü kullanması. WebSockets’in ana konsepti bir client-server bağlantısı kurar ve onu açık tutar. Bunun anlamı ilave veri alışverişi yaparken sayfa yeniden yüklenmeyecektir.
Bu kısımın amacı 2 kullanıcı arasında özel görüşme ve mesajlaşma imkanı sunmak.
Modellerin belirlenmesi
Gereken modelleri tanımlayarak başlayalım. İki farklı modele ihtiyacımız var. Biri özel görüşmeler diğeri özel mesajlar için. Şöyleki bir kişi ile mesajlaşmaya başladığınızda onunla bir görüşme başlatırsınız. Görüşme esnasında birçok mesaj alınır verilir. Mesajlar görüşmelere aittir. Bu modellere sırasıyla
PrivateConversation
ve PrivateMessage
diyebiliriz. Fakat bu isimlendirme bize kısa zamanda karışıklıklar olarak geri dönebilir. Her şey yolunda gitse bile models klasörünün nasıl görüneceğini hayal edelim. Aynı ilk kelimeye sahip 2 model oluşturduk. Klasörün içi zor yönetilebilir hale gelebilir.
Klasörler içinde bu kaotik yapılanmadan korunmak için biraz düzenleme yapmalıyız.
Nasıl göründüğüne bakalım. Normal yapıda olunca görüşmelerin modeli
Bunun yerine namespace kullanarak isimlendirme yaparsak model adımız
Genellikle Rails geliştirme prosesini kolaylaştırır. Bu şekilde namespace olarak ifade edilen modelleri üretmek istediğimizde modeli içine koyacağımız klasörü belirtmemiz yeterli olacaktır.
ayrıca
Eğer
Bir modelin içinde tablo adını belirtmek için
Artık
Bir kullanıcı bir çok görüşmeye sahip olabilir ve görüşmeler de bir çok mesaja sahiptirler. Bu ilişkileri modellerin içinde tanımlayalım:
Burada
Görüşmeler iki kullanıcı arasında olacaktır. Bu iki kullanıcı mesajı gönderen
Şimdi migrasyonlar içinde tablo tanımlamalarını yapalım:
Migrasyonları kaydettikten sonra çalıştıralım ve tablolar veritabanımıza eklensin:
Nasıl göründüğüne bakalım. Normal yapıda olunca görüşmelerin modeli
PrivateConversation
adını alacak ve dosyası da private_conversation.rb
olacaktır.
models/private_conversation.rb
Bunun yerine namespace kullanarak isimlendirme yaparsak model adımız
Private::Conversation
olacak ve private
klasörü altında conversation.rb
adında bir dosyada bulunacaktır.models/private/conversation.rb
Bu daha kullanışlı olacaktır. Tüm private
ilk kelimesiyle başlayan modelleri models
klasörü altında birbirinden ayırmaya çalışacağımıza private
klasörü altında toplamış oluruz.
Private::Conversation
modelini oluşturmak için konsolda şu komutu girelim:
rails g model private/conversation
ayrıca
Private::Message
modelini de üretelim:
rails g model private/message
Eğer
models
klasörüne bakarsak private.rb
adında bir dosya görürüz. Bu veritabanı tablolarına isim verirken önlerine private_
ilk kelimesi getirmek için kullanılır. Bu dosyayı burada tutacağımıza her modelin kendi doyasında tablo adını tam olarak girelim ve bu dosyaya gerek kalmasın, kafa karıştırmasın.
self.table_name=
yönergesini kullanırız. Şimdi model dosyalarını şöyle değiştirelim:
app/models/private/conversation.rb
class Private::Conversation < ApplicationRecord
self.table_name = "private_conversations"
end
app/models/private/message.rb
class Private::Message < ApplicationRecord
self.table_name = "private_messages"
end
Artık
models
klasöründeki private.rb
dosyasına ihtiyacımız yok, onu da silelim gitsin.
app/models/private/conversation.rb
...
has_many :messages,
class_name: "Private::Message",
foreign_key: :conversation_id
belongs_to :sender, foreign_key: :sender_id, class_name: "User"
belongs_to :recipient, foreign_key: :recipient_id, class_name: "User"
...
app/models/private/message.rb
...
belongs_to :user
belongs_to :conversation,
foreign_key: :conversation_id,
class_name: "Private::Conversation"
...
Burada
class_name
metodu kullanarak bağlı modelin adını net belirttik. Namespace ile ifade ettiğimiz modellere ulaşılmasını bu şekilde model adını ayrıca belirterek sağlıyoruz. Aynı şekilde User
modeline bağlı recipient
ve sender
sütunları da özel olarak class_name
metodu ile hangi tabloya bağlı olduklarını belirtmemize gerek vardır.
foreign_key
tablolar arasındaki bağlantı yapılırken diğer tablodaki id
sütunu ile eşleşecek olan sütunun adıdır. Veritabanı kullananlar bilir, bu sütun belongs_to
tarafındaki model içinde tanımlanır ve karşı gelen tabloda bu sütundaki değere sahip id
değeri olan kayıt ya da kayıtlar eşleştirilir.
sender
ve mesajı alan recipient
sütunları ile ifade edilir. Bunlara user1
ve user2
isimleri de verebilirdik. Fakat bu şekil isimlendirerek görüşmeyi başlatanın kim olduğunu da biliyoruz, sender
sütunundaki kullanıcı görüşmeyi başlatan kişidir.
db/migrate/2020xxxxxxxxx_create_private_conversations.rb
class CreatePrivateConversations < ActiveRecord::Migration[5.1]
def change
create_table :private_conversations do |t|
t.integer :recipient_id
t.integer :sender_id
t.timestamps
end
add_index :private_conversations, :recipient_id
add_index :private_conversations, :sender_id
add_index :private_conversations, [:recipient_id, :sender_id], unique: true
end
end
db/migrate/2020xxxxxxxxx_create_private_messages.rb
class CreatePrivateMessages < ActiveRecord::Migration[5.1]
def change
create_table :private_messages do |t|
t.text :body
t.references :user, foreign_key: true
t.belongs_to :conversation, index: true
t.boolean :seen, default: false
t.timestamps
end
end
end
body
sütununda mesajın içeriği bulunacak. İlişkileri tanımlamak ve referans eden id sütunlarını tanımlamak yerine burada references
metodunu kullandık. Bu satır user_id adında bir sütun ekler ve bununla User
tablosunun id
sütununa bağlanır, ve tabloya foreign_key olarak bildirilir. belongs_to
metodu da aslında references
metodunun aynısı, sadece anlaşılabilirlik açısından hangisini istersek onu kullanırız. Bu satırda da conversation_id
adında bir satır eklenerek Conversation
tablosunun id
sütununa bağlanıyor ve bu sütun tablonun bir index‘i olarak belirtiliyor. En son eklenen seen
sütunu da tahmin edersiniz mesajın karşı tarafından görüldüğünü bildirecek.
rails db:migrate
Gerçek zamanlı olmayan bir mesaj penceresi
Özel görüşme verilerini saklayacak bir yerimiz oldu, fakat daha çok iş var. Nereden başlayalım? Önce basit kullanıcı arabirimi oluşturalım, sonra işlevselliğini artırmak için lojik ekleyelim. Eğer arabirimimiz olursa problemleri küçük parçalar halinde tek tek işleye işleye gidebiliriz, bütünü düşünmek çok daha zor olur.
Görüşmeler için arabirimi oluşturmaya
Private::Conversations
kontrolörü nü oluşturarak başlayalım. Madem namespace kullanmaya başladık tüm uygulamada ona sadık kalalım.
rails g controller private/conversations
Rails jeneratörler çok yeteneklidir. Aynı modellerde olduğu gibi tüm görselleri , herşeyi namespace’e uygun olarak oluşturur. Zaten bu yetenekler daha sonra bir çok framework tarafından esin kaynağı olarak kullanılmıştır.Yeni görüşme başlatmak
Yeni bir görüşmeyi başlatmak için bir yönteme ihtiyacımız var. Bizim uygulamamızda kendimizle benzer ilgi alanları olan kişilerle görüşme gerçekleştirmek istiyoruz. Buna başlamak için en uygun yerlerden biri kullanıcının bir gönderisinin yayınlandığı sayfa.
Bu sayfanın görsel kalıbı olan
posts/show.html.erb
içine bunu eklemek için bir form ekleyelim. Formun amacı yeni bir görüşmeyi başlatmak. <p><%= @post.content %></p>
satırının altına şunu ekleyelim:
app/views/posts/show.html.erb
...
<%= render contact_user_partial_path %>
...
Bu yardımcı metodu
posts_helper.rb
dosyası içinde tanımlayalım.
app/helpers/posts.helper.rb
...
def contact_user_partial_path
if user_signed_in?
@post.user.id != current_user.id ? "posts/show/contact_user" : "shared/empty_partial"
else
"posts/show/login_required"
end
end
...
show
klasörü ve içinde gereken parça görsel kalıplarını oluşturalım.
app/views/posts/show/_contact_user.html.erb
<div class="contact-user">
<%= render leave_message_partial_path %>
</div>
app/views/posts/show/_login_required.html.erb
<div class="text-center">
Kullanıcı ile iletişim kurmak için <%= link_to "Login", login_path %> olmanız gerekir
</div>
posts_helper.rb
içinde leave_message_partial_path
metodunu tanımlayalım. Bu ekstra metod tanımlamalar falan sırf kodumuzu daha okunabilir ve kolay müdahale edilebilir yapmak için.
app/helpers/posts.helper.rb
...
def leave_message_partial_path
if @message_has_been_sent
"posts/show/contact_user/already_in_touch"
else
"posts/show/contact_user/message_form"
end
end
...
Burada
@message_has_been_sent
değişkeni bize daha önce mesaj gönderilmiş olduğu bilgisini verecek. Mesaj gönderilmişse already_in_touch.html.erb
dosyasını , gönderilmemişse message_form.html.erb
dosyasını parça görsel olarak yayınlayacak.
Bu
@message_has_been_sent
oluşum değişkenini PostsController
içinde tanımlayacağız.
leave_message_partial_path
metodu içinde gerek duyulan iki parça görselini contact_user
alt klasörü içinde tanımlayalım.
app/views/posts/show/contact_user/_already_in_touch.html.erb
<div class="contacted-user">
Bu kullanıcı ile halihazırda temas kurdunuz
</div>
app/views/posts/show/contact_user/_message_form.html.erb
<%= form_tag({controller: "private/conversations", action: "create"},
method: "post",
remote: true) do %>
<%= hidden_field_tag(:post_id, @post.id) %>
<%= text_area_tag(:message_body,
nil,
rows: 3,
class: "form-control",
placeholder: "Kullanıcıya bir mesaj gönder") %>
<%= submit_tag("Mesaj gönder", class: "btn send-message-to-user") %>
<% end %>
Şimdi
PostsController
‘in show
aksiyonuna ilave yapalım:
app/controllers/posts.controller.rb
...
def show
@post = Post.find(params[:id])
if user_signed_in?
@message_has_been_sent = conversation_exist?
end
end
...
Kontrolörün
private
bölümüne conversation_exist?
metodunu ekleyelim
app/controllers/posts.controller.rb
...
private
def conversation_exist?
Private::Conversation.between_users(current_user.id, @post.user.id).present?
end
...
between_users
metodu iki kullanıcı arasında olan görüşmeleri sorguluyor. Bu metodu Private::Conversation
modeli içinde bir scope olarak tanımlayalım.
app/models/private/conversation.rb
...
scope :between_users, -> (user1_id, user2_id) do
where(sender_id: user1_id, recipient_id: user2_id).or(
where(sender_id: user2_id, recipient_id: user1_id))
end
...
Private::Conversatios
kontrolörü içine create
aksiyonu tanımlamak var.
app/controllers/private/conversations_controller.rb
...
def create
recipient_id = Post.find(params[:post_id]).user.id
conversation = Private::Conversation.new(sender_id: current_user.id,
recipient_id: recipient_id)
if conversation.save
Private::Message.create(user_id: recipient_id,
conversation_id: conversation.id,
body: params[:message_body])
respond_to do |format|
format.js {render partial: "posts/show/contact_user/message_form/success"}
end
else
respond_to do |format|
format.js {render partial: "posts/show/contact_user/message_form/fail"}
end
end
end
...
Burada mevcut kullanıcı ile gönderi yazarı arasında yeni bir görüşme başlatıyoruz. Eğer herşey yolunda giderse mevcut kullanıcı tarafından yazılmış bir mesaj oluşturulacak ve bu başarı bir mesaj ile kullanıcıya bildirilecek. Bildirimlerde JavaScript parça dosyalar kullanıyoruz. Bu parça dosyaları oluşturalım:
app/views/posts/show/contact_user/message_form/_success.js.erb
$(".contact-user").replaceWith('\
<div class="contact-user">\
<div class="contacted-user">Mesaj gönderildi</div>\
</div>');
app/views/posts/show/contact_user/message_form/_fail.js.erb
$(".contact-user").replaceWith('<div>Mesaj gönderilemedi</div>');
Route’lar eksik kaldı.
Private::Conversations
ve Private::Messages
kontrolörleri için route’ları tanımlayalım:
config/routes.rb
namespace :private do
resources :conversations, only: [:create] do
member do
post :close
end
end
resources :messages, only: [:index, :create]
end
Sadece birkaç aksiyona ihtiyacımız var, bu durumlarda
only
metodu çok faydalıdır. namespace
metodu da buradaki gibi namespace ile oluşturduğumuz kontrolörler için route tanımlamalarında kullanılır.
branch_page.scss
dosyasına ilave yapalım.
app/assets/stylesheets/partials/posts/branch_page.scss
...
.send-message-to-user {
background-color: $navbarColor;
padding: 10px;
color: white;
border-radius: 10px;
margin-top: 10px;
&:hover {
background-color: black;
color: white;
}
}
.contact-user {
text-align: center;
}
.contacted-user {
display: inline-block;
border-radius: 10px;
padding: 10px;
background-color: $navbarColor;
color: white;
}
...
Şimdi tek gönderi sayfasını açtığımızda şöyle bir görüntü olacaktır.
Bir mesaj yazıp gönderdiğimizde ise şöyle bir görüntü olacaktır.
Daha önce temas kurduğumuz bir kullanıcının gönderisine baktığımızda da şöyle bir görüntü olacaktır:
Bir görüşme penceresi yayınlamak
Açılmış olan görüşmeleri oturum bilgisi içinde saklayacağız. Böylece kullanıcı görüşmeyi sonlandırana ya da oturumu bitirene kadar görüşmeler açık kalacak.
Private::ConversationsController
‘ın create
aksiyonu içinde görüşmenin kaydedilmesini sağlayan add_to_conversations
metodu çağrısı ekleyelim. Sonra da metodu private
bölümünde tanımlayalım.
app/controllers/private/conversations_controller.rb
def create
...
else
respond_to do |format|
format.js {render partial: "posts/show/contact_user/message_form/fail"}
end
end
add_to_conversations unless already_added?
end
private
def add_to_conversations
session[:private_conversations] ||= []
session[:private_conversations] << @conversation.id
end
Bu görüşme id’sini oturum bilgileri içine ekleyecektir.
already_added?
metodu ise görüşmenin id’sinin şimdiye kadar oturum bilgilerine eklenmediğinden emin olmak içindir. Bunu da kontrolörün private
bölümüne ekleyelim.
app/controllers/private/conversations_controller.rb
...
def already_added?
session[:private_conversations].include?(@conversation.id)
end
...
Bir fark daha, burada
conversation
değişkenini daha sonra görsellerde kullanabilmek amacıyla oluşum değişkenine çevirdik (başına @ karakteri ekledik). Bunu da tüm create
metodu içinde kullanıldığı yerlere uygulayalım.
app/views/private/conversations/_conversation.html.erb
<% @recipient = private_conv_recipient(conversation) %>
<% @is_messenger = false %>
<li class="conversation-window" id="pc<%= conversation.id %>"
data-pconversation-user-name="<%= @recipient.name %>"
data-turbolinks-permanent>
<div class="panel panel-default" data-pconversation-id="<%= conversation.id %>">
<%= render "private/conversations/conversation/heading",
conversation: conversation %>
<!-- Görüşme penceresi içeriği -->
<div class="panel-body">
<%= render "private/conversations/conversation/messages_list",
conversation: conversation %>
<%= render "private/conversations/conversation/new_message_form",
conversation: conversation,
user: user %>
</div><!-- panel-body -->
</div>
</li><!-- conversation-window -->
Burada
private_conv_recipient
metodu ile görüşmenin alıcısını bulduk. Yardımcı metodlar içinde bu metodu tanımlayalım.
app/helpers/private/conversations_helper.rb
module Private::ConversationsHelper
def private_conv_recipient(conversation)
conversation.opposed_user(current_user)
end
end
opposed_user
metodu kullanılıyor. Bunu tanımlamak için Private::Conversation
modeline ekleyelim.
app/models/private/conversation.rb
def opposed_user(user)
user == recipient ? sender : recipient
end
Bu metod bir görüşmede karşı kişinin id’sini buluyor.
Tekrar
_conversation.html.erb
dosyasına dönelim ve gerekli olan parça görselleri hazırlayalım.
app/views/private/conversations/conversation/_heading.html.erb
<div class="panel-heading conversation-heading">
<span class="contact-name-notif"><%= @recipient.name %></span>
</div>
<!-- görüşmeyi bitir butonu -->
<%= link_to "X",
close_private_conversation_path(conversation),
class: "close-conversation",
title: "Close",
remote: true,
method: :post %>
app/views/private/conversations/conversation/_messages_list.html.erb
<div class="messages-list">
<%= render load_private_messages(conversation), conversation: conversation %>
<div class="loading-more-messages">
<i class="fa fa-spinner" aria-hidden="true"></i>
</div>
<!-- mesajlar -->
<ul>
</ul>
</div>
Private::ConversationsHelper
yardımcı dosyası içinde load_private_messages
metodunu tanımlayalım.
app/helpers/private/conversations_helper.rb
...
# eğer görüşmede gösterilmeyen mesajlar varsa yüklemek için buton ekle
def load_private_messages(conversation)
if conversation.messages.count > 0
"private/conversations/conversation/messages_list/link_to_previous_messages"
else
"shared/empty_partial"
end
end
...
Bu önceki mesajları göstermek için bir link ekleyecektir. Karşı gelen parça dosyasını yeni ekleyeceğimiz
messages_list
klasörü içine ekleyelim.
app/views/private/conversations/conversation/messages_list/_link_to_previous_messages.html.erb
<%= link_to "Mesajları yükle",
private_messages_path(:conversation_id => conversation.id,
:messages_to_display_offset => @messages_to_display_offset,
:is_messenger => @is_messenger),
class: "load-more-messages",
remote: true %>
Görüşme penceresi tüm uygulama genelinde erişilebilir olacaktır.
Private::ConversatiosHelper
içindeki yardımcı dosyaların tüm uygulama genelinde erişilebilir olması için ApplicationHelper
içine şu ilaveyi yapıyoruz.
app/helpers/application_helper.rb
module ApplicationHelper
include NavigationHelper
include PostsHelper
include Private::ConversationsHelper
end
Sonra yeni mesaj formu için son parça dosyayı da tanımlayalım.
Bu formu ileride daha işlevsel yapacağız.
Şimdi kullanıcı gönderi izleme ekranından bir mesaj gönderdiğinde görüşme penceresini uygulamada yayınlanmasını sağlayalım
Bu parça dosyanın amacı uygulamaya görüşme penceresini eklemek. Parça dosyayı tanımlayalım.
Bu parça görev dosyası bir çok senaryoda kullanılacak. Aynı pencereyi birçok defa açmayı engellemek için, pencereyi yayınlamadan evvel uygulamada halihazırda aktif olup olmadığı kontrol edilmeli. Sonra pencere genişletilip yazı alanına odaklanmalı. Dosyanın en sonunda
Şimdi
JavaScript ile arada veri alışverişi için cookie ya da benzer şeylere ait kendi fonksiyonlarımızı yazmaya çalışmak yerine gon gem’i kullanabiliriz. Bu gem’in orjinal kullanımı veriyi server tarafından JavaScript tarafına göndermek üzerine. Fakat JavaScript değişkenlerini uygulama genelinde erişilebilir yapmak için de çok faydalıdır. Bu gem’i uygulamamıza ekleyelim.
satırlarını
ile gem’i uygulamamıza dahil ederiz.
Bu gem biraz uyuz sayfalarımızda kullanılabilir olması için
Ekran genişliğini izleyen bir olay izleyicimiz var. Bir görüşme pencere genişliğinin sol tarafına yaklaşınca, görüşme gizleniyor. Yeterince boş alan oluştuğunda da otomatik olarak görünebilir oluyor. Bir sayfayı ziyaret ettiğimizde yukarıdaki fonksiyonlar çalışarak görüşme pencerelerinin doğru pozisyonda olmasını sağlıyor.
Bootstrap panel elemanını kullanarak görüşme pencerelerini kolayca genişletip daraltabiliyoruz. Default olarak hepsi daraltılmış görüntüde olacak ve etkileşim yapılamayacaktır. Bunları durum değiştirebilir hale getirmek için
Stiller için yeni bir
Gördüğümüz kadarıyla bu tutorialı hazırlayan arkadaşın daha planları çok. Şimdiye kadar yerleştirmediğimiz elemanların da stilleri var burada.
Daha önce yeni başlatılan görüşmenin id’sini oturum bilgisi içinde saklamıştık. Şimdi onu kullanarak görüşme penceresini kullanıcı kapatana kadar ya da oturumum bitirene kadar açık tutacağız.
Sonra da
Şimdi görsele sıra geldi. Her sayfada çıkması için
Parça görsel dosyamız için
Şimdi yeni mesaj gönderirsek şöyle bir görünümde mesaj pencersini görmeliyiz.
Ancak daha bitmedi. Mesajları görmek için yapılacaklar var.
Kapatma butonu tıklanınca bu aksiyon çağrılır. Aksiyon oturum bilgileri içindeki conversation_id’yi siler ve bir JavaScript dosya ile cevap gönderir. Bu JavaScript dosyanın adı aksiyon ile aynı olacaktır. Bu dosyayı tanımlayalım.
Bu script ile ilgili görüşmenin penceresi DOM’dan çıkarılır (yani yok edilir) ve geri kalan görüşme pencereleri yerleşimi tekrar dizayn edilir.
Tabi bu yardımcı metodun tüm sayfalarda erişilebilir olması için
Şimdi mesajları listeye yükleyecek bir komponente ihityacımız var. Ayrıca bu komponent kullanıcı yukarı doğru scroll ettikçe geride mesaj kalmayana kadar daha önceki mesajları da listeye eklemeye devam edecek. Mesajlar için de daha önce gönderiler listesinde yaptığımıza benzer bir “sonsuz scroll” mekanızması kurmalıyız.
Metod çok basit görünüyor. Sadece bir parça dosyanın path’ini dönüyor. Bu dosya ileride daha gelişecek ve mesajlaşma sistemimize ekstra özellikler vereceğiz. Şu anda bu yardımcı metodlara hiçbir yerden erişemeyiz. Bu yüzden
Yukardan bakınca sadece modül adını vermek yetiyordu ama alt klasör içinden bakınca
Metodda
Bu kod önceki mesajları listenin başına ekler, sonra aynı şeyi tekrar eder. Gelelim
Karşı gelen yeni klasörleri
Bu dosya önceki mesajlar için olan linki güncelleyecektir. Önceki mesajlar eklendikten sonra daha önceki mesajları yükleyecek şekilde link yenilenecektir.
Şimdi sistemimiz hazır, önceki mesajlar listenin üst tarafına gelecek. Fakat uygulamaya gidip baktığımızda hiç bir mesajın pencereye gelmediğini görürüz. Neden? Çünkü mesajları yüklemek için olan link hiç bir şeyi tetiklemiyor. Bir görüşme penceresi açtığımızda en son mesajları görmek isteriz. Görüşme penceresini de bu şekilde davranacak şekilde yönlendirebiliriz. Önceki mesajları yükleme periyodunun ilkini başlatırız ve arkası gelir.
Bir olay izleme kodu yazarak kullanıcı görüşme penceresinin üst kısmına scroll ile yaklaştığında daha fazla mesajı yüklemek için linki tıklatalım. Yeni dosyayı ekleyelim.
Daha fazla mesaj yükleme linki tıklanınca
Kontrolörü ve aksiyonunu oluşturalım.
Burada
Burada yazarın bir fikrini size aktarayım, şöyle demiş: Bazı insanların
Modülü tanımlayalım.
Burada
Son olarak , mesajları listenin üstüne ekledikten sonra loading ikonunu yok etmeliyiz.
Şimdi
Gerekli parça dosyayı da tanımlayalım.
Çalıştrımaya kalkınca yazarın
Şimdi geride başka mesaj kalmadığı için önceki mesajlara olan link ve loading ikonu kaldırılıyor.
Eğer şimdi bir mesaj gönderirsek görüşme penceresi içinde mesajımızla açılır. AJAX istemleriyle mesajları gösterebiliyoruz artık.
İlk yapmamız gereken bir WebSocket bağlantısı oluşturmak ve özel bir kanala bağlamak. Şanslıyız ki WebSocket bağlantıları default Rails konfigürasyonu tarafından kapsanmıştır.
Connection default olarak kurulur. İhtiyacımız olan abone olacağımız bir görüşme kanalı. Namespace ile bir kanal oluşturmak için konsolda şunu girelim:
Bu metodların içeriğine şunları yazalım:
Burada kullanıcının kendisine ait bir kanalı olmasını istiyoruz. Bir kanaldan bir kullanıcı veri alıp verebilir. Kullanıcının id’si unique olduğundan kanal adına onu katarak kanalın da unique olmasını sağlıyoruz.
Bu server tarafındaki bir bağlantı. Şimdi client tarafta da bir bağlantı üretmeliyiz.
Client tarafta bağlantının bir oluşumunu tanımlamak için biraz JavaScript yazmamız gerekecek. Gerçekte Rails kanal üreteci kullandığımızda onu üretti.
Server’ı tekrar başlattığımda WebSocket bağlantısı yapılmaya çalışılırken
Bu değişikliği yapalım ve server’ı tekrar başlatıp login olduğumuzda server log’da şunu görürüz.
Bağlantıyı gerçekleştirdik. Gerçek zamanlı iletişimin merkezini oturttuk. Artık sürekli açık bir client-server bağlantımız var. Bunun anlamı bağlantıyı yeniden yapmadan veya sayfayı yenilemeden server ile veri alışverişi yapabileceğiz. Eğer düşünürsek bu çok güçlü bir özellik. Buradan itibaren mesajlaşma sistemini bu bağlantı üzerinden devam ettireceğiz.
Görüşme penceresinin yeni mesaj formunu çalışır hale getirelim.
Fonksiyon yeni mesaj formundan verileri alıp
Diikat edersek olay işleyici bir submit butonu olayı. Ama bizim görüşme penceremizde bir submit butonu yok olabilir. Böyle tercih edilmiş. Burada klavyeden enter basıldığında submit butonunun tıklanmasını sağlayacağız. Bu fonksiyon ileride başka özellikler için de kullanılacak. Bu yüzden
Dosyaya kısaca bakarsak , önce scrollbarı yukarıdan uzaklaştırarak istenmeyen bir eski mesaj yüklemesinin önüne geçiliyor. Arkasından enter tuşu basılması ile submit butonu tıklanıyor ve mesaj alanı tekrar boşaltılıyor.
Bu metod yeni mesaj üretimini gerçekleştirecek. Burada
Eğer yeni bir mesaj göndermeye kalkarsak yeni mesaj kaydı otomatik olarak üretilir. Şu anda mesaj listesinde gösterilmez sayfayı yenilemek gerekir. Gösterilmesi için yeni eklenen mesajları yayınlayacak bişey yapmalıyız. Fakat öncesinde şu anda mesaj sisteminin nasıl çalıştığına bir bakalım.
Gördüğümüz gibi kayıt oluşturulmasından sonra
Şimdi yeni üretilen mesajı özel görüşmenin kanalında yayınlayacak olan bu arkaplan görevini yazabiliriz. Konsolda şunu girelim
Burada bir mesaj yayınlıyoruz ve kanalın her iki abonesine de gönderiyoruz. Ayrıca mesajın sağlıklı gösterildiğinden emin olmak için bazı key-value çiftleri de gönderiyoruz. Şimdi mesaj taraflara gönderildi de hala listeye gelmez. Çünkü client taraf görselleri için henüz bir şey yapmadık. Bir kanalda bir veri yayınlandığında karşıdaki client’da
Arada görülmemiş mesaj olan pencereye ilgi çekilecek bir yere sadece yorum yazarak sonraya bıraktık.
Burada
Bu fonksiyonları koymak için ortak kullanıldığı belli olsun diye
Kodda mesajlaşma olarak bilinen bir sayfadan bahsediliyor. Bu görüntü şu anda uygulamada mevcut değil. Mesajlaşma , görüşmeleri yapmak için başka bir yol olarak uygulamamız daha sonra eklenecek. İleride bir sürü küçük küçük ilave yaparken konudan kopmamak adına şimdi kodun tamamı verilmiş.
Hepsi bu kadar gerçek zamanlı işlevsellik çalışıyor olmalı. Gönderen de alan da her iki kullanıcı da yeni mesajları görebiliyor olmalı.
Ama burada sanırım farkettiniz can sıkıcı bir konu var. Biz açık olan görüşme pencerelerinin bilgisini oturum bilgileri içine sakladığımızdan dolayı oturumu sonlandırdığımızda görüşme pencereleri yok oluyor ve tekrar geri getiremiyoruz. Ancak yeni görüşme başlatınca oturum bilgilerine o görüşme penceresi ekleniyor. Bu sorunu çözmek için ileride yol yapılacak. Ama ben geçici olarak kayıtlı görüşmeleri siliyorum ve sıfırdan yeni görüşme başlatıyorum. Bunu nasıl yapacağımızı kısaca anlatayım. Önce konsolda Rails consol uygulamasını çalıştırıyoruz.
Sonra gidip sayfadan kullanıcı ile yeni görüşme açıyorum. Ancak bu da çözüm olmadı çünkü karşı tarafı da göreyim dediğimde onun oturumunu başka bir tarayıcıda açınca bu sefer ona mesajlar gelmedi. Bu sefer pansuman tedbirimi geliştirmeye karar verdim. Önce gittim ana yerleşim dosyasına bir link ekleyerek görüşmeleri getirmesi için bir aksiyon çağırdım.
Linki koyduk ama routes.rb içinde bunu tanımlamazsak route yok hatası verecek. routes.rb en alta şunu ekledim
Şimdi
İlk önce oturum açmış olan kullanıcının kayıtlı görüşmeler içinde gönderen ya da alan tarafında olduğu tüm görüşmeleri çekiyorum. Bu arada bu pansuman tedbir olduğu için kullanıcı giriş yaptı mı yapmadı mı bakmıyorum, giriş yapmış kabul ediyorum. Sonra bu görüşmelerin her birini yukarıdaki create aksiyonundan kopyaladığım şekil bir kodla oturuma dahil ediyorum. Oh çok şükür, artık kapanmış tüm görüşme pencereleri bir tıkla emrimde. En son olarak da ana sayfaya yönlendiriyorum, yoksa tarayıcı adresinde saçma bir şey yazacak.
Aksak topal mesajlar karşıya gitmeye başladı.
Şimdi mesajlar karşı tarafa ve bize nasıl geliyor kısaca üzerinden geçelim.
app/views/private/conversations/conversation/_new_message_form.html.erb
<form class="send-private-message">
<input type="hidden" name="conversation_id" value="<%= conversation.id %>">
<input type="hidden" name="user_id" value="<%= user.id %>">
<textarea name="body" rows="3" class="form-control" placeholder="Bir mesaj yazın"></textarea>
<input type="submit" class="btn btn-success send-message">
</form>
Bu formu ileride daha işlevsel yapacağız.
Şimdi kullanıcı gönderi izleme ekranından bir mesaj gönderdiğinde görüşme penceresini uygulamada yayınlanmasını sağlayalım
_success.js.erb
dosyası içine şu ilaveyi yapalım:
app/views/posts/show/contact_user/message_form/_success.js.erb
$(".contact-user").replaceWith('\
<div class="contact-user">\
<div class="contacted-user">Mesaj gönderildi</div>\
</div>');
<%= render "private/conversations/open" %>
Bu parça dosyanın amacı uygulamaya görüşme penceresini eklemek. Parça dosyayı tanımlayalım.
app/views/private/conversations/_open.js.erb
var conversation = $("body").find("[data-pconversation-id='" +
"<%= @conversation.id %>" + "']");
var chat_windows_count = $(".conversation-window").length + 1;
if (conversation.length !== 1) {
$("body").append("<%= j(render 'private/conversations/conversation',\
conversation: @conversation,\
user: current_user) %>");
conversation = $("body").find("[data-pconversation-id='" +
"<%= @conversation.id %>" + "']");
}
// pencereler açılır-küçülür olacak
// yeni pencereyi açmak için heading tıklamasını aktif et
// üretildikten sonra otomatik açılsın
$(".conversation-window:nth-of-type(" + chat_windows_count + ")\
.conversation-heading").click();
// pencereyi tıklayarak mesajı okundu olarak işaretle
// bu da sonra lazım olacak
setTimeout(function(){
$(".conversation-window:nth-of-type(" + chat_windows_count + ")").click();
}, 1000);
// yazı kısmına odaklan
$("conversation-window:nth-of-type(" + chat_windows_count + ")\
form\
textarea").focus();
// tüm görüşme pencerelerini pozisyonla
positionChatWindows();
Bu parça görev dosyası bir çok senaryoda kullanılacak. Aynı pencereyi birçok defa açmayı engellemek için, pencereyi yayınlamadan evvel uygulamada halihazırda aktif olup olmadığı kontrol edilmeli. Sonra pencere genişletilip yazı alanına odaklanmalı. Dosyanın en sonunda
positionChatWindows()
fonksiyonu çağırılarak tüm pencerelerin düzgün yerleştirilmesi sağlanıyor. Eğer bunu yapmazsak tüm pencereler aynı yerde yayınlanır ve bu kullanışsız olur.
assets
klasörü altında görüşme pencerelerinin görülebilirliğini ve pozisyonlarını kontrol eden bir dosya ekleyelim.
app/assets/javascripts/conversations/position_and_visibility.js
// Sayfa yüklemesinden sonra
$(document).on("turbolinks:load", function(){
chat_windows_count = $(".conversation-window").length;
// eğer son görülebilir chat penceresi ayarlanmadıysa ve görüşme penceresi mevcutsa
// son görülebilir chat penceresi değişkenini ayarla
if (gon.last_visible_chat_window == null && chat_windows_count > 0) {
gon.last_visible_chat_window = chat_windows_count;
}
// eğer gon.hidden_chats değişkeni yoksa tanımla
if (gon.hidden_chats == null) {
gon.hidden_chats = 0;
}
window.addEventListener("resize", hideShowChatWindow);
positionChatWindows();
hideShowChatWindow();
});
function positionChatWindows() {
chat_windows_count = $(".conversation-window").length;
// eğer yeni görüşme penceresi eklenmişse,
// onu son görülebilir chat penceresi olarak işaretle
// böylece hideShowChatWindow fonksiyonu onu
// ekran genişliğine göre gösterip gizleyebilir
if (gon.hidden_chats + gon.last_visible_chat_window !== chat_windows_count) {
if (gon.hidden_chats == 0) {
gon.last_visible_chat_window = chat_windows_count;
}
}
// yeni chat penceresi eklendiğinde onu listenin en sol başına yerleştir
for(i=0; i<chat_windows_count; i++) {
var right_position = i * 410;
var chat_window = i + 1;
$(".conversation-window:nth-of-type(" + chat_window + ")")
.css("right", "" + right_position + "px");
}
}
// Eğer son görüşme penceresi ekranın sol kenarına yakınsa onu gizler
function hideShowChatWindow() {
// eğer görüşme penceresi yoksa fonksiyonu durdur
if ($(".conversation-window").length < 1) {
return;
}
// en soldaki pencerenin koordinatlarını al
var offset = $(".conversation-window:nth-of-type(" + gon.last_visible_chat_window + ")").offset();
// görüşme penceresi left koordinatı 50 den küçükse onu gizle
if (offset.left < 50 && gon.last_visible_chat_window !== 1) {
$(".conversation-window:nth-of-type(" + gon.last_visible_chat_window + ")")
.css("display", "none");
gon.hidden_chats++;
gon.last_visible_chat_window--;
}
// eğer en soldaki pencere koordinatı 550 den büyükse
// ve gizlenmiş görüşme varsa onu göster
if (offset.left > 550 && gon.hidden_chats !== 0) {
gon.hidden_chats--;
gon.last_visible_chat_window++;
$(".conversation-window:nth-of-type(" + gon.last_visible_chat_window + ")")
.css("display", "initial");
}
}
JavaScript ile arada veri alışverişi için cookie ya da benzer şeylere ait kendi fonksiyonlarımızı yazmaya çalışmak yerine gon gem’i kullanabiliriz. Bu gem’in orjinal kullanımı veriyi server tarafından JavaScript tarafına göndermek üzerine. Fakat JavaScript değişkenlerini uygulama genelinde erişilebilir yapmak için de çok faydalıdır. Bu gem’i uygulamamıza ekleyelim.
gem 'gon'
gem 'rabl-rails'
satırlarını
Gemfile
dosyasına ekledikten sonrabundle
ile gem’i uygulamamıza dahil ederiz.
Bu gem biraz uyuz sayfalarımızda kullanılabilir olması için
application.html.erb
genel görsel kalıp dosyamızda <head>
kısmına şu satırı eklememiz gerekiyor.
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>Yeni</title>
<%= csrf_meta_tags %>
<%= include_gon(:init => true) %>
...
Ekran genişliğini izleyen bir olay izleyicimiz var. Bir görüşme pencere genişliğinin sol tarafına yaklaşınca, görüşme gizleniyor. Yeterince boş alan oluştuğunda da otomatik olarak görünebilir oluyor. Bir sayfayı ziyaret ettiğimizde yukarıdaki fonksiyonlar çalışarak görüşme pencerelerinin doğru pozisyonda olmasını sağlıyor.
Bootstrap panel elemanını kullanarak görüşme pencerelerini kolayca genişletip daraltabiliyoruz. Default olarak hepsi daraltılmış görüntüde olacak ve etkileşim yapılamayacaktır. Bunları durum değiştirebilir hale getirmek için
javascripts
klasörü içinde toggle_window.js
adında bir dosya tanımlayalım.
app/assets/javascripts/conversations/toggle_window.js
// Sayfa yüklemesinden sonra
$(document).on("turbolinks:load", function(){
// görüşmenin heading kısmı tıklanınca
$("body").on("click",
".conversation-heading, conversation-heading-full",
function(e){
e.preventDefault();
var panel = $(this).parent();
var panel_body = panel.find(".panel-body");
var messages_list = panel.find(".messages-list");
panel_body.toggle(100, function(){});
});
});
Stiller için yeni bir
conversation_window.scss
dosyası tanımlayalım.
app/assets/stylesheets/partials/conversation_window.scss
textarea {
resize: none;
}
.panel {
margin: 0;
border: none !important;
}
.panel-heading {
border-radius: 0;
}
.panel-body {
position: relative;
display: none;
padding: 0 0 5px 0;
}
.conversation-window, .new_chat_window {
min-width: 400px;
max-width: 400px;
position: fixed;
bottom: 0;
right: 0;
list-style-type: none;
}
.conversation-heading, .conversation-heading-full, .new_chat_window {
background-color: $navbarColor !important;
color: white !important;
height: 40px;
border: none !important;
a {
color: white !important;
}
}
.conversation-heading, .conversation-heading-full {
padding: 0 0 0 15px;
width: 360px;
display: inline-block;
vertical-align: middle;
line-height: 40px;
}
.close-conversation, .add-people-to-chat, .add-user-to-contacts, .contact-request-sent {
color: white;
float: right;
height: 40px;
width: 40px;
font-size: 20px;
font-size: 2.0rem;
border: none;
background-color: $navbarColor;
}
.close-conversation, .add-user-to-contacts {
text-align: center;
vertical-align: middle;
line-height: 40px;
font-weight: bold;
}
.close-conversation {
&:hover {
border: none;
background-color: white;
color: $navbarColor !important;
}
&:visited, &:focus {
color: white;
}
}
.form-control[disabled] {
background-color: $navbarColor;
}
.send-private-message, .send-group-message {
textarea {
border-radius: 0;
border: none;
border-top: 1px solid rgba(0, 0, 0, 0.2);
}
}
.loading_svg {
display: none;
}
.loading_svg {
text-align: center;
}
.messages-list {
z-index: 1;
min-height: 300px;
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
ul {
padding: 0;
}
}
.message-received, .message-sent {
max-width: 300px;
word-wrap: break-word;
z-index: 1;
}
.message-sent {
position: relative;
background-color: white;
border: 1px solid rgba(0,0,0, 0.5);
border-radius: 5px;
margin: 5px 5px 5px 50px;
padding: 10px;
float: right;
}
.message-recived {
background-color: $backgroundColor;
border-color: #eeeeee;
border-radius: 5px;
margin: 5px 50px 5px 5px;
padding: 10px;
float: left;
}
.messages-date {
width: 100%;
text-align: center;
border-bottom: 1px solid rgba(0,0,0, 0.2);
line-height: 1px;
line-height: 0.1rem;
margin: 20px 0 20px;
span {
background: #fff;
padding: 0 10px;
}
}
.loading-more-messages {
font-size: 20px;
font-size: 2.0rem;
padding: 10px 0;
text-align: center;
}
.send-message {
display: none;
}
Gördüğümüz kadarıyla bu tutorialı hazırlayan arkadaşın daha planları çok. Şimdiye kadar yerleştirmediğimiz elemanların da stilleri var burada.
Daha önce yeni başlatılan görüşmenin id’sini oturum bilgisi içinde saklamıştık. Şimdi onu kullanarak görüşme penceresini kullanıcı kapatana kadar ya da oturumum bitirene kadar açık tutacağız.
ApplicationController
içinde bir filtre tanımlayalım.
app/controllers/application_controller.rb
before_action :opened_conversations_windows
Sonra da
openened_conversations_windows
metodunu ekleyelim
app/controllers/application_controller.rb
def opened_conversations_windows
if user_signed_in?
# açık olan görüşmeler
session[:private_conversations] ||= []
@private_conversations_windows = Private::Conversation.includes(:recipient, :messages)
.find(session[:private_conversations])
else
@private_conversations_windows = []
end
end
includes
metodu bahsi geçen veritabanı tablosu ile bağlantılı olan diğer tablolardan da bağlantılı verilerin çekileceğini gösterir. Böylece bu sorgunun cevabında sadece görüşmelere ait satırları değil o görüşmenin alıcıları (recipients) ve mesajlarına (messages) ait tüm verileri toplamış oluyoruz. Onlar için bir daha ayrıca sorgu yapmamıza gerek kalmıyor. Bu klasik N + 1 sorgu problemi gibi oluyor. Tek bir sorguda mesajları almazsak ilave yapacağımız sorgula her mesaj için ayrı tetiklenecektir. 100 mesaj için 100 sorgu yapacağımıza bir tek başlangıçta yapılan sorguyla herhangi sayıdaki mesajı alıyoruz. Uygulama ve veritabanı performansı için bunlara dikkat etmek gerekiyor.
application.html.erb
görsel kalıbına ekleyeceğiz. yield
metodu satırı altına şunu ekleyelim:
app/views/layouts/application.html.erb
<%= render "layouts/application/private_conversations_windows" %>
Parça görsel dosyamız için
layouts/application
klasörünü ve içine _private_conversations_windows.html.erb
dosyasını ekleyelim.
app/views/layouts/application/_private_conversations_windows.html.erb
<% @private_conversations_windows.each do |conversation| %>
<%= render partial: "private/conversations/conversation",
locals: { conversation: conversation,
user: current_user } %>
<% end %>
Şimdi yeni mesaj gönderirsek şöyle bir görünümde mesaj pencersini görmeliyiz.
Ancak daha bitmedi. Mesajları görmek için yapılacaklar var.
Görüşmeyi Bitirmek
Görüşmenin “close” butonu halihazırda çalışmıyor. Fakat herşeyimiz hazır.
Private::ConversationsController
içinde bir close
aksiyonu tanımlayalım.
app/controllers/private/conversations_controller.rb
...
def close
@conversation_id = params[:id].to_i
session[:private_conversations].delete(@conversation_id)
respond_to do |format|
format.js
end
end
private
...
Kapatma butonu tıklanınca bu aksiyon çağrılır. Aksiyon oturum bilgileri içindeki conversation_id’yi siler ve bir JavaScript dosya ile cevap gönderir. Bu JavaScript dosyanın adı aksiyon ile aynı olacaktır. Bu dosyayı tanımlayalım.
app/views/private/conversations/close.js.erb
$("body")
.find("[data-pconversation-id='" + "<%= @conversation_id %>" + "']")
.parent()
.remove();
positionChatWindows();
Bu script ile ilgili görüşmenin penceresi DOM’dan çıkarılır (yani yok edilir) ve geri kalan görüşme pencereleri yerleşimi tekrar dizayn edilir.
Mesajları Göstermek
Şu anda mesajlar listesinde sadece bir loading ikonu var, ama mesajımız görünmüyor. Sebebi daha mesajlar için görsel kalıp dosyaları oluşturmadık.
views/private
klasörü altında messages
klasörü ekleyelim ve içine bir parça görseli ekleyelim. (Ben anlamadım niye mesajlar için kontrolör üretmiyor da manual yapıyor. Vardır bir bildiği)
app/views/private/messages/_message.html.erb
<%= render private_message_date_check(message, previous_message),
locals: { message: message } %>
<li title="<%= message.created_at.to_s(:time) %>">
<div class="row">
<div class="<%= sent_or_received(message, user) %> <%= seen_or_unseen(message) %>">
<%= message.body %>
</div>
</div>
</li>
private_message_date_check
yardımcı metodu mesajın bir önceki mesajla aynı günde yayınlanmasını kontrol ediyor (whatsapp gibi aynı gün mesajları gruplanacak). Eğer farklı günse tarih belirten bir satır ekliyor görüntüye. Bu yardımcı metodu Private::MessagesHelper
içinde tanımlayalım.
app/helpers/private/messages_helper.rb
module Private::MessagesHelper
def private_message_date_check(message, previous_message)
if defined?(previous_message) && previous_message.present?
# eğer mesajlar aynı gün içinde yapılmadıysa
if previous_message.created_at.to_date != message.created_at.to_date
@message = message
"private/messages/message/new_date"
else
"shared/empty_partial"
end
else
"shared/empty_partial"
end
end
end
Tabi bu yardımcı metodun tüm sayfalarda erişilebilir olması için
ApplicationHelper
içinde Private::MessagesHelper
bağlamalıyız.
include Private::MessagesHelper
Yeni message
klasörünü ekleyerek _new_date.html.erb
parça görselini tanımlayalım.
app/views/private/messages/message/_new_date.html.erb
<div class="messages-date">
<span><%= @message.created_at.to_date %></span>
</div>
_message.html.erb
dosyamıza geri dönersek sent_or_received
ve seen_or_unseen
adında iki yardımcı metodumuz daha olduğunu görürüz. Bunlar farklı koşullarda farklı sınıflar geri dönerler. Bu metodları da Private::MessagesHelper
içinde tanımlayalım.
app/helpers/private/messages_helper.rb
...
def sent_or_received(message, user)
user.id == message.user_id ? "message-sent" : "message-received"
end
def seen_or_unseen(message)
message.seen == false ? "unseen" : ""
end
...
Şimdi mesajları listeye yükleyecek bir komponente ihityacımız var. Ayrıca bu komponent kullanıcı yukarı doğru scroll ettikçe geride mesaj kalmayana kadar daha önceki mesajları da listeye eklemeye devam edecek. Mesajlar için de daha önce gönderiler listesinde yaptığımıza benzer bir “sonsuz scroll” mekanızması kurmalıyız.
views/private/messages
klasörü içinde _load_more_messages.js.erb
adında bir dosya ekleyelim.
app/views/private/messages/_load_more_messages.js.erb
<% @id_type = "pc" %>
<%= render append_previous_messages_partial_path %>
<%= render replace_link_to_private_messages_partial_path %>
@id_type
oluşum değişkeni görüşme için bir tip belirler. Şimdilik sadece bire bir görüşmelerimiz var ama ileride grup görüşmeleri de olacak , bu değişken onun için konmuş. app/helpers
klasörü altında shared
adında yeni bir klasör ekleyelim ve messages_helper.rb
adında yeni bir yardımcı dosya ekleyelim.
app/helpers/shared/messages_helper.rb
module Shared::MessagesHelper
def append_previous_messages_partial_path
"shared/load_more_messages/window/append_messages"
end
end
Metod çok basit görünüyor. Sadece bir parça dosyanın path’ini dönüyor. Bu dosya ileride daha gelişecek ve mesajlaşma sistemimize ekstra özellikler vereceğiz. Şu anda bu yardımcı metodlara hiçbir yerden erişemeyiz. Bu yüzden
Private::MessagesHelper
içinde bu modüle başvuru yapalım.
app/helpers/private/messages_helper.rb
module Private::MessagesHelper
require "shared/messages_helper"
include Shared::MessagesHelper
...
Yukardan bakınca sadece modül adını vermek yetiyordu ama alt klasör içinden bakınca
require
ile modülü içeren dosyaya bağlanmak gerekti.
app/views/shared
klasörü altında yeni klasörlerden bahsediliyor. Bunları da ekleyelim.
app/views/shared/load_more_messages/window
Sonra da _append_messages.js.erb
dosyasını tanımlayalım.
app/views/shared/load_more_messages/window/_append_messages.js.erb
// geçici olarak daha mesaj yükle linkini kaldır
// böylece yeni mesajlar getirilirken tıklanamasın
$("#<%= @id_type %><%= @conversation.id %> .load-more-messages")
.replaceWith("<span class='load-more-messages'></span>");
// önceki messajları yayınla
$("#<%= @id_type %><%= @conversation.id %> .messages-list ul")
.prepend('<%= j render "#{@type}/conversations/messages" %>');
// Yeni mesajlar eklendikten sonra scrollbar üzerinde boşluk ekle
$('#<%= @id_type %><%= @conversation.id %> .messages-list').scrollTop(400);
Bu kod önceki mesajları listenin başına ekler, sonra aynı şeyi tekrar eder. Gelelim
_load_more_messages.js.erb
içindeki diğer metoda. Private::MessagesHelper
içine bu metodu ekleyelim.
app/helpers/private/messages_helper.rb
...
def replace_link_to_private_messages_partial_path
"private/messages/load_more_messages/window/add_link_to_messages"
end
...
Karşı gelen yeni klasörleri
app/views/private/messages
klasörü altına ekleyelim
app/views/private/messages/load_more_messages/window/_add_link_to_messages.js.erb
$("#<%= @id_type %><%= @conversation.id %> .load-more-messages")
.replaceWith('\
<%= j render partial: "private/conversations/conversation/messages_list/link_to_previous_messages",\
locals: {conversation: @conversation } %>\
');
Bu dosya önceki mesajlar için olan linki güncelleyecektir. Önceki mesajlar eklendikten sonra daha önceki mesajları yükleyecek şekilde link yenilenecektir.
Şimdi sistemimiz hazır, önceki mesajlar listenin üst tarafına gelecek. Fakat uygulamaya gidip baktığımızda hiç bir mesajın pencereye gelmediğini görürüz. Neden? Çünkü mesajları yüklemek için olan link hiç bir şeyi tetiklemiyor. Bir görüşme penceresi açtığımızda en son mesajları görmek isteriz. Görüşme penceresini de bu şekilde davranacak şekilde yönlendirebiliriz. Önceki mesajları yükleme periyodunun ilkini başlatırız ve arkası gelir.
toggle_windows.js
dosyası içindeki toggle
fonksiyonu tam bu işi yapacak. Fonksiyonu düzenleyelim.
app/assets/javascripts/conversations/toggle_window.js
...
panel_body.toggle(100, function(){
var messages_visible = $("ul", this).has("li").length;
// mesaj listesi boşsa ilk 10 mesajı yükle
if (!messages_visible && $(".load-more-messages", this).length) {
$(".load-more-messages", this)[0].click();
}
});
...
Bir olay izleme kodu yazarak kullanıcı görüşme penceresinin üst kısmına scroll ile yaklaştığında daha fazla mesajı yüklemek için linki tıklatalım. Yeni dosyayı ekleyelim.
app/assets/javascripts/conversations/messages_infinite_scroll.js
$(document).on("turbolinks:load ajax:complete", function() {
var iScrollPos = 0;
var isLoading = false;
var currentLoadingIcon;
$(document).ajaxComplete(function() {
isLoading = false;
// loading ikonunu sakla
if (currentLoadingIcon !== undefined) {
currentLoadingIcon.hide();
}
});
$(".messages-list", this).scroll(function() {
var iCurScrollPos = $(this).scrollTop();
if (iCurScrollPos > iScrollPos) {
// aşağı scroll ediyor
} else {
// yukarı scroll ediyor
if (iCurScrollPos < 300 && isLoading == false && $(".load-more-messages", this).length) {
// 10 mesaj daha yükleyen linki tıkla
$(".load-more-messages", this)[0].click();
isLoading = true;
// loading ikonunu bul ve görünür yap
currentLoadingIcon = $(".load-more-messages", this);
currentLoadingIcon.show();
}
}
iScrollPos = iCurScrollPos;
});
});
Daha fazla mesaj yükleme linki tıklanınca
Private::MessagesController
‘ın index
aksiyonu çağırılır. Linki tanımlarken böyle tanımlamıştık. _link_to_previous_messages.html.erb
dosyasına bakabilirsiniz.
app/controllers/private/messages_controller.rb
class Private::MessagesController < ActionController::Base
include Messages
def index
get_messages("private", 10)
@user = current_user
@is_messenger = params["is_messenger"]
respond_to do |format|
format.js { render partial: "private/messages/load_more_messages" }
end
end
end
Burada
Messages
modülünden metodları içerdik. Bu modülü controllers/concerns
klasörü içine koyacağız. ActiveSupport::Concern daha sonra diğer sınıflar içinde kullanacağımız modülleri koymak için olan yerlerden biri. Yukarıdaki kontrolör kodunda bulunan get_messages
metodu bu modülden geliyor. Böyle yapmamızın sebebi aynı metodu daha sonra başka bir kontrolör içinde de kullanacağız.
ActiveSupport::Concern
‘den şikayet ettiklerini ve kullanılmaması gerektiğini söylediklerini gördüm. Bu insanları benimle octagonda dövüşmeye davet ediyorum. Şaka yaptım :D, bu bağımsız bir uygulama ve nasıl istersek öyle yaparız. Eğer concern
sevmiyorsanız tekrar kullanılan metodlar için başka yollar da var.
app/controllers/concerns/messages.rb
require "active_support/concern"
module Messages
extend ActiveSupport::Concern
def get_messages(conversation_type, messages_amount)
# stringi sabite çevirerek fonksiyonun model çağırmasını dinamik hale getirmiş
# aşağıdan Private::Conversation şeklinde model ismi çıkacak
model = "#{conversation_type.capitalize}::Conversation".constantize
@conversation = model.find(params[:conversation_id])
# görüşmenin önceki mesajlarını çek
@messages = @conversation.messages.order(created_at: :desc)
.limit(messages_amount)
.offset(params[:messages_to_display_offset].to_i)
# bir başka mesajları getir için bir değişken ayarla
@messages_to_display_offset = params[:messages_to_display_offset].to_i + messages_amount
@type = conversation_type.downcase
# eğer görüşmede son mesajlar da çekilmişse değişkeni 0 yap
# böylece daha fazla mesaj yükleme linki yok olur
if @conversation.messages.count < @messages_to_display_offset
@messages_to_display_offset = 0
end
end
end
Burada
active_support/concern
içerip modülümüzü ActiveSupport::Concern
ile genişlettiğimiz için Rails artık modülü bir concern olarak tanıyacaktır.
contantize
metodu ile modül tanımını dinamik olarak bir stringden oluşturduk. Şu anda Private::Conversation
modülü çıkacak ileride gruplar için Group::Conversation
olduğunu da göreceğiz inşallah.
get_messages
metodu tüm oluşum değişkenlerini ayarladıktan sonra index
aksiyonumuz _load_more_messages.js.erb
parça dosyasıyla çağrıya dönüş yapıyor.
_load_more_messages.js.erb
dosyasının en altına şunu ekleyelim.
app/views/private/messages/_load_more_messages.js.erb
...
<%= render remove_link_to_messages %>
Şimdi
Shared::MessagesHelper
içinde remove_link_to_messages
metodunu tanımlayalım.
app/helpers/shared/messages_helper.rb
...
# eğer başka önceki mesaj kalmamışsa
def remove_link_to_messages
if @is_messenger == "false"
if @messages_to_display_offset != 0
"shared/empty_partial"
else
"shared/load_more_messages/window/remove_more_messages_link"
end
else
"shared/empty_partial"
end
end
...
Gerekli parça dosyayı da tanımlayalım.
app/views/shared/load_more_messages/window/_remove_more_messages_link.js.erb
$("#<%= @id_type %><%= @conversation.id %> .loading-more-messages")
.replaceWith("");
$("#<%= @id_type %><%= @conversation.id %> .load-more-messages")
.replaceWith("");
Çalıştrımaya kalkınca yazarın
private/conversations/_messages.html.erb
dosyasını eklemeyi unuttuğunu gördüm. Bu dosyayı gruplar için daha sonra yazılan benzerinden üretmeye çalışayım.
app/views/private/conversations/_messages.html.erb
<% previous_message = nil %>
<% @messages.reverse.each do |message| %>
<%= render partial: 'private/messages/message',
locals: { user: message.user,
conversation_id: @conversation.id,
message: message,
previous_message: previous_message } %>
<% previous_message = message %>
<% end %>
Şimdi geride başka mesaj kalmadığı için önceki mesajlara olan link ve loading ikonu kaldırılıyor.
Eğer şimdi bir mesaj gönderirsek görüşme penceresi içinde mesajımızla açılır. AJAX istemleriyle mesajları gösterebiliyoruz artık.
Action Cable ile gerçek zaman işlevselliği
Görüşme pencereleri oldukça güzel görünüyor. Bazı güzel işlevleri de var. Fakat en önemli özelliği eksik - gerçek zamanda mesaj gönderip almak.
Action Cable bize görüşme pencereleri için gereken gerçek zaman işlevlerini tanımlamakta yardımıcı olur.
app/channels/application_cable
klasörü içine bakarsak channel.rb
ve connection.rb
dosyalarını görürüz. Connection
sınıfı yetkilendirmeleri yönetir ve Channel
sınıfı tüm kanallar arasında paylaşılan lojiği içeren üst sınıftır.
rails g channel private/conversation
Üretilen Private::ConversationChannel
tanımında subscribed
ve unsubscribed
metodlarını görürüz. subscribed
metodu ile kullanıcı kanala bir bağlantı yapar. unsubscribed
metodu ile de kullanıcı kanala bağlantısını yok eder.
app/channels/private/conversation_channel.rb
class Private::ConversationChannel < ApplicationCable::Channel
def subscribed
stream_from "private_conversations_#{current_user.id}"
end
def unsubscribed
stop_all_streams
end
end
Burada kullanıcının kendisine ait bir kanalı olmasını istiyoruz. Bir kanaldan bir kullanıcı veri alıp verebilir. Kullanıcının id’si unique olduğundan kanal adına onu katarak kanalın da unique olmasını sağlıyoruz.
Bu server tarafındaki bir bağlantı. Şimdi client tarafta da bir bağlantı üretmeliyiz.
Client tarafta bağlantının bir oluşumunu tanımlamak için biraz JavaScript yazmamız gerekecek. Gerçekte Rails kanal üreteci kullandığımızda onu üretti.
app/assets/javascripts/channels/private
klasörüne gidelim. Rails default dosyayı CoffeeScript
olarak oluşturmuş, biz burada JavaScript kullanacağız dosyanın adını conversation.js
olarak değiştirelim ve içeriği şöyle düzenleyelim:
app/assets/javascripts/channels/private/conversation.js
App.private_conversation = App.cable.subscriptions.create("Private::ConversationChannel", {
connected: function() {},
disconnected: function() {},
received: function(data) {}
});
Server’ı tekrar başlattığımda WebSocket bağlantısı yapılmaya çalışılırken
conversation_channel.rb
dosyasında subscribed
metodu içinde current_user
değişkeninin tanınmadığını gördüm (undefined variable error). Yazar burada bir şeyi atlamış. İnternette bir araştırma yapınca Devise gem kullanan yetkilendirmeli sitelerde Action Cable ile bağlantılar yapılacaksa connection.rb
içinde current_user
değişkeninin şöyle elde edildiğini gördüm.
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 = env['warden'].user
verified_user
else
reject_unauthorized_connection
end
end
end
end
Bu değişikliği yapalım ve server’ı tekrar başlatıp login olduğumuzda server log’da şunu görürüz.
Bağlantıyı gerçekleştirdik. Gerçek zamanlı iletişimin merkezini oturttuk. Artık sürekli açık bir client-server bağlantımız var. Bunun anlamı bağlantıyı yeniden yapmadan veya sayfayı yenilemeden server ile veri alışverişi yapabileceğiz. Eğer düşünürsek bu çok güçlü bir özellik. Buradan itibaren mesajlaşma sistemini bu bağlantı üzerinden devam ettireceğiz.
Görüşme penceresinin yeni mesaj formunu çalışır hale getirelim.
assets/javascripts/channels/private/conversation.js
dosyasına en alta şunları ilave edelim:
app/assets/javascripts/channels/private/conversation.js
...
$(document).on("submit", ".send-private-message", function(e) {
e.preventDefault();
var values = $(this).serializeArray();
App.private_conversation.send_message(values);
$(this).trigger("reset");
});
Fonksiyon yeni mesaj formundan verileri alıp
send_message
fonksiyonuna gönderiyor. send_message
fonksiyonu server tarafında send_message
metodunu çalıştıracak ve yeni mesaj üretilmesini yönetecek.
assets/javascripts/conversations
klasörü altında conversation.js
adında bir dosya ekleyelim.
app/assets/javascripts/conversations/conversation.js
$(document).on("turbolinks:load", function() {
// görüşme penceresi scrollbar'ında üste bir aralık bırak
$(".messages-list").scrollTop(500);
// enter basılınca bir mesaj gönder ve mesaj penceresini orjinaline döndür
$(document).on("keydown",
".conversation-window, .conversation",
function(event) {
if (event.keyCode === 13) {
// eğer yazı varsa
if ($.trim($("textarea", this).val())) {
$(".send-message", this).click();
event.target.value = "";
event.preventDefault();
}
}
});
});
function calculateUnseenConversations() {
var unseen_conv_length = $("#conversations-menu").find(".unseen-conv").length;
if (unseen_conv_length) {
$("#unseen-conversations").css("visibility", "visible");
$("#unseen-conversations").text(unseen_conv_length);
} else {
$("#unseen-conversations").css("visibility", "hidden");
$("#unseen-conversations").text("");
}
}
Dosyaya kısaca bakarsak , önce scrollbarı yukarıdan uzaklaştırarak istenmeyen bir eski mesaj yüklemesinin önüne geçiliyor. Arkasından enter tuşu basılması ile submit butonu tıklanıyor ve mesaj alanı tekrar boşaltılıyor.
private_conversation
nesnesi içinde send_message
fonksiyonu ekleyerek başlayalım. received
görev fonksiyonu altına şunu ekleyelim:
app/assets/javascripts/channels/private/conversation.js
received: function(data) {},
send_message: function(message) {
return this.perform("send_message", {
message: message
});
}
...
received
fonksyonu satır sonuna “,” eklemeyi unutmayın. Bu fonksiyon server tarafında send_message
metodunu çağırır ve message değerini ona gönderir. Server tarafı metodu Private::ConversationChannel
sınıfı içinde tanımlanmalı. Metodu tanımlayalım:
app/channels/private/conversation_channel.rb
...
def send_message(data)
message_params = data["message"].each_with_object({}) do |el, hash|
hash[el["name"]] = el["value"]
end
Private::Message.create(message_params)
end
...
Bu metod yeni mesaj üretimini gerçekleştirecek. Burada
data
parametresi içinde gelen şey içiçe şeklinde bir hash. Bunu tek boyut has yapmak için each_with_object
metodunu kullandık.
- Kullanıcı yeni mesaj formunu doldurur ve mesajı gönderir
javascripts/channels/private/conversation.js
içindeki olay izleyici görüşme penceresinden veriyi alır, bir conversation_id ve mesaj içeriği, sonra client taraftakisend_message
fonksiyonunu tetikler.- Client taraftaki
send_message
fonksiyonu server taraftakisend_message
metodunu çağırır ve verileri ona gönderir. - Server taraftaki
send_message
metodu yeni birPrivate::Message
kaydı üretir.
Yeni mesajı yayınlamak
Yeni mesajı ürettik şimdi onu ilgili kanalda bir şekilde yayınlamamız gerekiyor. Active Record Görev Fonksiyonları bize modeller için çok kullanışlı metodlarla yardım eder.
after_create_commit
adında bir görev fonksiyonu var, bir modelde yeni bir kayıt oluşturulduğunda çalışır. Private::Message
model dosyası içinde şunu ekleyelim:
app/models/private/message.rb
...
after_create_commit do
Private::MessageBroadcastJob.perform_later(self, previous_message)
end
def previous_message
previous_message_index = self.conversation.messages.index(self) - 1
self.conversation.messages[previous_message_index]
end
...
Gördüğümüz gibi kayıt oluşturulmasından sonra
Private::MessageBroadcastJob.preform_later
metodu çağırılıyor. İyi de bu ne? Bu bir arkaplan görevi, arkaplanda dönen işleri yönetir. Bizim istediğimiz anlarda belli operasyonları çalıştırır. Belirli bir olayın hemen arkasından olabileceği gibi , belli zaman sonra ya da başka bir olayın arkasından gerçekleştirilebilir. Active Job temellerini okuyabilirsiniz.
rails g job private/message_broadcast
Üretilen dosya içinde perform
metodunu görürüz. Bir görevi çağırdığımızda default olarak bu metod çalışır. Şimdi görev içinde verileri işleyelim ve kanalın abonelerine iletelim.
app/jobs/private/message_broadcast_job.rb
class Private::MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message, previous_message)
sender = message.user
recipient = message.conversation.opposed_user(sender)
broadcast_to_sender(sender, recipient, message, previous_message)
broadcast_to_recipient(sender, recipient, message, previous_message)
end
private
def broadcast_to_sender(sender, recipient, message, previous_message)
ActionCable.server.broadcast(
"private_conversations_#{sender.id}",
message: render_message(message, previous_message, sender),
conversation_id: message.conversation_id,
recipient_info: recipient)
end
def broadcast_to_recipient(sender, recipient, message, previous_message)
ActionCable.server.broadcast(
"private_conversations_#{recipient.id}",
recipient: true,
sender_info: sender,
message: render_message(message, previous_message, recipient),
conversation_id: message.conversation_id)
end
def render_message(message, previous_message, user)
ApplicationController.render(
partial: "private/messages/message",
locals: { message: message,
previous_message: previous_message,
user: user }
)
end
end
Burada bir mesaj yayınlıyoruz ve kanalın her iki abonesine de gönderiyoruz. Ayrıca mesajın sağlıklı gösterildiğinden emin olmak için bazı key-value çiftleri de gönderiyoruz. Şimdi mesaj taraflara gönderildi de hala listeye gelmez. Çünkü client taraf görselleri için henüz bir şey yapmadık. Bir kanalda bir veri yayınlandığında karşıdaki client’da
received
görev fonksiyonu tetiklenir. İşte burada verileri DOM’a ekleyerek görünmesini sağlayacağız. received
fonksiyonu içine şunları ekleyelim:
app/assets/javascripts/channels/private/conversation.js
received: function(data) {
// Görüşmeler menüsü listesinde bu görüşmeye link varsa
// o linki görüşmeler menüsünde en üste taşı
var conversation_menu_link = $("#conversations-menu ul")
.find("#menu-pc" + data["conversation_id"]);
if (conversation_menu_link.length) {
conversation_menu_link.prependTo("#conversations-menu ul");
}
// değişkenleri ayarla
var conversation = findConv(data["conversation_id"], "p");
var conversation_rendered = ConvRendered(data["conversation_id"], "p");
var messages_visible = ConvMessagesVisiblity(conversation);
if (data["recipient"] == true) {
// yeni mesaj alındıktan sonra "görülmedi" olarak işaretle
$("#menu-pc" + data["conversation_id"]).addClass("unseen-conv");
// Eğer görüşme penceresi mevcutsa
if (conversation_rendered) {
if (!messages_visible) {
// Görüşme penceresi stilini değiştirerek görülmemiş mesajı belirt
// Bir sınıf falan ekleyerek bunu gerçekleştir
}
conversation.find(".messages-list").find("ul").append(data["message"]);
}
calculateUnseenConversations();
}
else {
conversation.find("ul").append(data["message"]);
}
if (conversation.length) {
// yeni mesaj eklendikten sonra pencerede aşağıya scroll et
var messages_list = conversation.find(".messages-list");
var height = messages_list[0].scrollHeight;
messages_list.scrollTop(height);
}
},
Arada görülmemiş mesaj olan pencereye ilgi çekilecek bir yere sadece yorum yazarak sonraya bıraktık.
Burada
findConv
, ConvRendered
ve ConvMessagesVisibility
fonksiyonları kullanılmış. Bu fonksiyonları hem özel hem grup görüşmelerde kullanacağız.shared
adında bir klasör ekleyelim.
app/assets/javascripts/channels/shared
Bu klasör altında conversation.js
dosyasını ekleyelim.
app/assets/javascripts/channels/shared/conversation.js
// DOM içinde bir görüşmeyi bulur
function findConv(conversation_id, type) {
// mesajlaşmada bir açık görüşme varsa
var messenger_conversation = $("body .conversation");
if (messenger_conversation.length) {
// mesajlaşmada açık görüşme var
return messenger_conversation;
} else {
// görüşme bir popup pencerede açık
var data_attr = "[data-" + type + "conversation-id='" +
conversation_id + "']";
var conversation = $("body").find(data_attr);
return conversation;
}
}
// bir görüşme penceresinin yayınlanmış ve tarayıcıda görünüyor olmasını teyit eder
function ConvRendered(conversation_id, type) {
// eğer mevcut görüşme mesajlaşmada açılmışsa
if ($("body .conversation").length) {
// görüşme mesajlaşmada açılmış
// o zaman görünür demektir
return true;
} else {
// görüşme popup pencerede açılmış
// görüşme bir popup pencerede açık
var data_attr = "[data-" + type + "conversation-id='" +
conversation_id + "']";
var conversation = $("body").find(data_attr);
return conversation.is(":visible");
}
}
function ConvMessagesVisiblity(conversation) {
// eğer görüşme mesajlaşmada açıksa
if ($("body .conversation").length) {
// görüşme , mesajlaşmada açık
// öyleyse görünebilir vaziyettedir
return true;
} else {
// görüşme popup pencerede açık
// pencerenin toplanmış mı genişletilmiş mi olduğuna bak
var visiblity = conversation
.find(".panel-body")
.is("visible");
return visiblity;
}
}
Kodda mesajlaşma olarak bilinen bir sayfadan bahsediliyor. Bu görüntü şu anda uygulamada mevcut değil. Mesajlaşma , görüşmeleri yapmak için başka bir yol olarak uygulamamız daha sonra eklenecek. İleride bir sürü küçük küçük ilave yaparken konudan kopmamak adına şimdi kodun tamamı verilmiş.
Hepsi bu kadar gerçek zamanlı işlevsellik çalışıyor olmalı. Gönderen de alan da her iki kullanıcı da yeni mesajları görebiliyor olmalı.
Ama burada sanırım farkettiniz can sıkıcı bir konu var. Biz açık olan görüşme pencerelerinin bilgisini oturum bilgileri içine sakladığımızdan dolayı oturumu sonlandırdığımızda görüşme pencereleri yok oluyor ve tekrar geri getiremiyoruz. Ancak yeni görüşme başlatınca oturum bilgilerine o görüşme penceresi ekleniyor. Bu sorunu çözmek için ileride yol yapılacak. Ama ben geçici olarak kayıtlı görüşmeleri siliyorum ve sıfırdan yeni görüşme başlatıyorum. Bunu nasıl yapacağımızı kısaca anlatayım. Önce konsolda Rails consol uygulamasını çalıştırıyoruz.
rails console
Sonra Rails konsolda görüşmeleri listeletiyoruz.Private::Conversation.all
irb(main):014:0> Private::Conversation.all
Private::Conversation Load (2.0ms) SELECT "private_conversations".* FROM "private_conversations" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Private::Conversation id: 7, recipient_id: 7, sender_id: 1, created_at: "2020-05-17 11:15:19", updated_at: "2020-05-17 11:15:19">,
#<Private::Conversation id: 8, recipient_id: 2, sender_id: 1, created_at: "2020-05-19 09:57:29", updated_at: "2020-05-19 09:57:29">]>
Bu örnekte id değeri 7 ve 8 olan 2 kayıt bulundu. Kısaca bunları siliyorum.Private::Conversation.delete(7)
Private::Conversation.delete(8)
Sonra gidip sayfadan kullanıcı ile yeni görüşme açıyorum. Ancak bu da çözüm olmadı çünkü karşı tarafı da göreyim dediğimde onun oturumunu başka bir tarayıcıda açınca bu sefer ona mesajlar gelmedi. Bu sefer pansuman tedbirimi geliştirmeye karar verdim. Önce gittim ana yerleşim dosyasına bir link ekleyerek görüşmeleri getirmesi için bir aksiyon çağırdım.
app/views/layouts/application.html.erb
...
<body>
<%= render "layouts/navigation" %>
<%= yield %>
<%= render "layouts/application/private_conversations_windows" %>
<%= link_to "Görüşmeleri yükle",
action: "refresh",
controller: "private/conversations" %>
</body>
...
Linki koyduk ama routes.rb içinde bunu tanımlamazsak route yok hatası verecek. routes.rb en alta şunu ekledim
config/routes.rb
...
resources :messages, only: [:index, :create]
end
# olan görüşmeleri getirme linki
get "private/conversations/refresh"
end
Şimdi
conversations_controller.rb
içine ekleyeceğim aksiyon ile kullanıcının dahil olduğu görüşmeleri getirebilirim.
app/controllers/private/conversations_controller.rb
...
def refresh
convs = Private::Conversation.where("sender_id = ? or recipient_id = ?", current_user.id, current_user.id)
for conv in convs
@conversation = conv
add_to_conversations unless already_added?
end
redirect_to "/"
end
...
İlk önce oturum açmış olan kullanıcının kayıtlı görüşmeler içinde gönderen ya da alan tarafında olduğu tüm görüşmeleri çekiyorum. Bu arada bu pansuman tedbir olduğu için kullanıcı giriş yaptı mı yapmadı mı bakmıyorum, giriş yapmış kabul ediyorum. Sonra bu görüşmelerin her birini yukarıdaki create aksiyonundan kopyaladığım şekil bir kodla oturuma dahil ediyorum. Oh çok şükür, artık kapanmış tüm görüşme pencereleri bir tıkla emrimde. En son olarak da ana sayfaya yönlendiriyorum, yoksa tarayıcı adresinde saçma bir şey yazacak.
Aksak topal mesajlar karşıya gitmeye başladı.
Şimdi mesajlar karşı tarafa ve bize nasıl geliyor kısaca üzerinden geçelim.
- Yeni
Private::Message
kaydı oluştuktan sonraafter_create_commit
metodu tetikleniyor ve arkaplan görevini çağırıyor. Private::MessageBroadcastJob
> verilen veriyi işler ve kanal abonelerine yayınlar.- Client tarafta
received
görev fonksiyonu çalışarak veriyi DOM’a ekler.
Bu kadarıyla keselim ve bu yazıyı yayınlayalım çünkü çok uzadı. Sonraki bölümde mesajlaşma yapısını devam edeceğiz inşallah. O zamana kadar kalın sağlıcakla..
Hiç yorum yok:
Yorum Gönder