Ö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.jsiçindeki olay izleyici görüşme penceresinden veriyi alır, bir conversation_id ve mesaj içeriği, sonra client taraftakisend_messagefonksiyonunu tetikler.- Client taraftaki
send_messagefonksiyonu server taraftakisend_messagemetodunu çağırır ve verileri ona gönderir. - Server taraftaki
send_messagemetodu yeni birPrivate::Messagekaydı ü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::Messagekaydı oluştuktan sonraafter_create_commitmetodu tetikleniyor ve arkaplan görevini çağırıyor. Private::MessageBroadcastJob> verilen veriyi işler ve kanal abonelerine yayınlar.- Client tarafta
receivedgö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