Skip to content

Instantly share code, notes, and snippets.

@bvsatyaram
Last active December 27, 2015 14:28
Show Gist options
  • Save bvsatyaram/7340248 to your computer and use it in GitHub Desktop.
Save bvsatyaram/7340248 to your computer and use it in GitHub Desktop.
Messaging System
# == Schema Information
#
# Table name: conversation_messages
#
# id :integer not null, primary key
# body :text
# thread_id :integer
# author_id :integer
# created_at :datetime not null
# updated_at :datetime not null
#
class Conversation::Message < ActiveRecord::Base
belongs_to :thread, :class_name => Conversation::Thread.name, :foreign_key => 'thread_id'
belongs_to :author, :class_name => User.name
has_many :subscriptions, :class_name => Conversation::MessageSubscription.name, :foreign_key => 'message_id', :dependent => :destroy
#TODO: Remove redundancy
# This is required to have +through+ has_many association Thread#message_subscriptions, though it's presence is redundant here in this model
has_many :message_subscriptions, :class_name => Conversation::MessageSubscription.name, :foreign_key => 'message_id', :dependent => :destroy
attr_accessor :recipients
end
# == Schema Information
#
# Table name: conversation_message_subscriptions
#
# id :integer not null, primary key
# user_id :integer
# message_id :integer
# archived :boolean default(FALSE)
# unread :boolean default(FALSE)
# created_at :datetime not null
# updated_at :datetime not null
#
class Conversation::MessageSubscription < ActiveRecord::Base
belongs_to :user
belongs_to :message, :class_name => Conversation::Message.name, :foreign_key => 'message_id'
scope :for_user, lambda { |usr| where(:user_id => usr.id) }
scope :archived, lambda { |is_archived| where(:archived => is_archived) }
scope :unread, where(:unread => true)
def mark_read!
self.update_attribute(:unread, false)
end
end
class Conversation::MessageSubscriptionObserver < ActiveRecord::Observer
def after_create(message_subscription)
if message_subscription.unread?
message_subscription.message.thread.subscriptions.for_user(message_subscription.user).first.increment!(:unread_count)
end
end
end
class Conversation::MessagesController < ApplicationController
before_filter :load_new_message, :only => [:new, :create]
def new
@user_data = auto_suggest_user_data
if params[:user_id]
prefill_user = User.find(params[:user_id])
@prefill_user_data = [{:value => prefill_user.id.to_s, :name => prefill_user.name_for_display(current_user)}].to_json
else
@prefill_user_data = [].to_json
end
end
def create
@stand_alone_creation = params[:conversation_message][:thread_id].nil? # Message is sent from profile page (not messages page), without reference to any thread
@message.author = current_user
@save_success = true
body = params[:conversation_message][:body].strip
if body.blank?
@save_success = false
return
else
@message.body = body
end
if @stand_alone_creation
recipients = fetch_recipients_from_string
@thread = fetch_or_create_thread_for_recipients(recipients)
return unless @save_success
else
@thread = Conversation::Thread.find(params[:conversation_message][:thread_id])
recipients = @thread.subscriptions.collect(&:user)
end
recipients.each do |user|
ms = @message.subscriptions.new
ms.user = user
ms.unread = (user != current_user)
end
@thread.messages << @message
@save_success = @thread.save
if @save_success && @stand_alone_creation
@thread_messages = {@thread => @message}
end
end
private
def load_new_message
@message = Conversation::Message.new
end
def fetch_recipients_from_string
#TODO: DRY the code. The same code is used in Gym::MembershipsController
recipients = params[:as_values_recipients].strip
if recipients.blank?
@save_success = false
return
else
recipients = recipients.split(',').reject(&:blank?)
recipients = User.where(:id => recipients)
recipients << current_user
return recipients.flatten.uniq
end
end
def fetch_or_create_thread_for_recipients(recipients)
return if recipients.nil?
thread = recipients.collect{|usr| usr.threads}.reduce(:&).select{|t| t.subscriptions.size == recipients.size}.first
if thread.nil?
thread = Conversation::Thread.new
recipients.each do |usr|
thread.subscriptions << thread.subscriptions.build(:user => usr)
end
@save_success = thread.save
end
return thread
end
end
This gist tries to give an example of how to build a simple, yet robust, messaging system.
We assume that the users are handed by `User` model.
The following models are proposed:
Conversation::Thread
Conversation::ThreadSubscription
Conversation::Message
Conversation::MessageSubscription
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
# == Schema Information
#
# Table name: conversation_threads
#
# id :integer not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
#
class Conversation::Thread < ActiveRecord::Base
has_many :messages, :class_name => Conversation::Message.name, :foreign_key => 'thread_id', :order => 'id DESC', :dependent => :destroy
has_many :message_subscriptions, :class_name => Conversation::MessageSubscription, :through => :messages, :order => 'id DESC', :dependent => :destroy
has_many :subscriptions, :class_name => Conversation::ThreadSubscription, :foreign_key => 'thread_id', :dependent => :destroy
def participants(skip_user = nil)
participants = self.subscriptions.collect(&:user)
participants = participants - [skip_user] unless participants.size == 1
return participants
end
def random_participant(skip_user = nil)
participants(skip_user).sample
end
end
# == Schema Information
#
# Table name: conversation_thread_subscriptions
#
# id :integer not null, primary key
# user_id :integer
# thread_id :integer
# unread_count :integer default(0)
# created_at :datetime not null
# updated_at :datetime not null
#
class Conversation::ThreadSubscription < ActiveRecord::Base
belongs_to :user
belongs_to :thread, :class_name => Conversation::Thread.name, :foreign_key => 'thread_id'
scope :for_user, lambda { |user| where(:user_id => user.id) }
def mark_read!
return if self.unread_count == 0
self.update_attribute(:unread_count, 0)
self.thread.message_subscriptions.for_user(self.user).unread.each(&:mark_read!)
end
end
class Conversation::ThreadsController < ApplicationController
after_filter :mark_thread_as_read, :only => [:show]
#TODO: authorize_resource
def index
@archived = false
@threads = current_user.threads
@thread_messages = {}
@threads.each do |thread|
@thread_messages[thread] = thread.message_subscriptions.for_user(current_user).archived(@archived).first.try(&:message)
end
@threads = @threads.select{|thread| @thread_messages[thread].present?}
@threads = @threads.sort_by{|thread| @thread_messages[thread].id}.reverse
end
def show
@thread = Conversation::Thread.find(params[:id])
@page_title = @thread.participants.collect(&:name).join(', ')
@archived = false
message_ids = @thread.message_subscriptions.for_user(current_user).archived(@archived).collect(&:message_id)
@messages = Conversation::Message.where(:id => message_ids).order(:id)
@new_message = @thread.messages.new
end
private
def mark_thread_as_read
@thread.subscriptions.for_user(current_user).first.mark_read!
end
end
class User < ActiveRecord::Base
has_many :threads, :through => :thread_subscriptions, :class_name => Conversation::Thread, :dependent => :destroy
has_many :thread_subscriptions, :class_name => Conversation::ThreadSubscription, :dependent => :destroy
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment