Unread messages counter

2.2k views Asked by At

I am creating a simple chatting app on rails 4. The controllers, models and views are created but the functionality is still incomplete. I have 2 tables in my database, conversations and messages. The conversation table holds two fields, sender id and receiver id. And the messages table holds 3 fields, body, user id and read(defaults to 0 meaning not read).

Models:

class Conversation < ActiveRecord::Base

    belongs_to :sender, :foreign_key => :sender_id, :class_name => "User"
    belongs_to :reciever, :foreign_key => :reciever_id, :class_name => "User"

    has_many :messages, :dependent => :destroy

    validates_uniqueness_of :sender_id, :scope => :reciever_id

    scope :involving, lambda { |user_id|
        where("sender_id = ? OR reciever_id = ?", user_id, user_id)
    }

    scope :between, lambda { |sender_id, reciever_id|
        where("(sender_id = ? AND reciever_id = ?) OR (sender_id = ? AND reciever_id = ?)", sender_id, reciever_id, reciever_id, sender_id)
    }

    def other_interlocutor(user_id)
        if sender.id == user_id
            return reciever.id
        else
            return sender.id
        end
    end
end

class Message < ActiveRecord::Base
  belongs_to :conversation
  belongs_to :user

  validates_presence_of :conversation_id, :user_id, :body

end

What I am trying to do is to create a real-time functionality of receiving unread messages count whenever someone receives a new message. I am using private pub to create the chat between users.

I have a user model which contains this function:

def unread_messages_count
    unread_messages = 0
    # puts "Putting self conversations ! #{self.conversations.first}"
    conversations = Conversation.involving(self.id)
    conversations.each do |conversation|
        unread_messages += conversation.messages.where(:read => 0, :user_id => conversation.other_interlocutor(self.id)).count
    end
    return unread_messages = unread_messages == 0 ? nil : unread_messages
end

I have one page where all user's conversations are listed and one a conversation is clicked all the messages related to that conversation are listed too. On the same page I have subscribed every conversation_messages_path to create separate channels for every conversation. Whenever a message is sent a create.js.erb file is rendered where I publish to those subscribed channels:

<% publish_to conversation_messages_path(@conversation.id) do %>
    $("#conversations_link").text("<%= current_user.unread_messages_count %> Conversations");
    $("#messages").append("<%= escape_javascript render(:partial => 'message', :locals => { :message => @message })%>");
<% end %>

The $("#conversation_link") is where I want to show the unread messages count.

Currently, the unread messages count is returning back the wrong count and the navbar is only updated when the conversation.sender_id messages the receiver.

My unread message counter is not returning the correct number of unread messages. I don't know how to fix it. What's wrong in my code? Thanks.

1

There are 1 answers

8
max On

I would argue that your domain modeling is really off.

The whole idea of a conversation is that the parties involved take turns being the the sender and recipient. What your have modeled is a monologue.

A monologue is a speech delivered by one person, or a long one-sided conversation that makes you want to pull your hair out from boredom. The Greek root word monologos translates to “speaking alone,” and that's a monologue: one person doing all the talking.

The domain model you end up with here should look something like this:

database diagram

Its the message that is actually linked to two users (or more): the sender and the recipient. For the sake of simplicity i'm sticking to 1:1 messaging here (vs group chats where a message may belong to many recipients).

class Message
  belongs_to :recipient, class_name: 'User'
  belongs_to :sender, class_name: 'User'
end

class User
  has_many :sent_messages, 
           class_name: 'Message', 
           foreign_key: 'sender_id' 
  has_many :messages, foreign_key: 'recipient_id'
end

Note that we need to tells Rails the class and the foreign key when it cannot be derived from the association name.

Instead of having a boolean read field you might want to consider using a enum to denote the status of the message.

Enums are basically an integer column which maps to a list of symbols.

class Message
  enum :status, [:unread, :read] 
  belongs_to :recipient, class_name: 'User'
  belongs_to :sender, class_name: 'User'
  belongs_to :conversation
end

Enums give you scopes like:

Message.unread
Message.read

And conditionals such as:

message.unread?
message.read?

And it makes it very straight forward if you want to add more states such as :archieved or :trashed.

Armed with this you don't need your unread_messages_count monstrosity. Which will eat tons of memory since you are pulling records out of the database just to count the associated records.

current_user.messages.unread.size

Also we should define the relation between User and Conversation correctly:

class Conversation
  has_many :messages
  has_and_belongs_to_many :users
end

class Users
  # ..
  has_and_belongs_to_many :conversations
end

This has_and_belongs_to_many relation would store users and converstations in the users_conversations join table. You can use the following generator to create the join table migration:

rails generate migration users_conversations

Added:

Using session[:user_id] in your views is a really bad code smell. You are tightly coupling your authentication logic all over your application.

Instead create a helper:

class SessionsHelper
  def current_user
    @current_user ||= User.find(session[:user_id])
  end

  def user_signed_in?
     !current_user.nil?
  end 
end

Other parts of the application should not know that you store the current user in session[:user_id] just that there is a current_user.