I am trying to set up a messaging system similar to facebook where you have a list of messages sorted by conversations between two users (it is not important for multiple recipients at the moment, but maybe if I'd used a smarter design then it can be easily implemented in the future. I don't think it would be easy to add to what I currently have.) I have something kind of working, but I am unable to implement a few features. I am pretty new to rails / web programming, so any and all tips/hints/solutions is much appreciated.
So I have three relevant models: User, Conversation, and Messages. User has many conversations, Conversation has many messages and belongs to Users, and Message belongs to Conversation. Okay, so this is what the models look like:
User: has relevant fields ID:int, username:string
class User < ActiveRecord::Base
has_many :conversations, :class_name => "Conversation", :finder_sql =>
proc {
"SELECT * FROM conversations " +
"WHERE conversations.user1_id = #{id} OR conversations.user2_id = #{id} " +
"ORDER BY conversations.updated_at DESC" }
Conversation: has relevant fields: ID:int, user1_id:int, user2_id:int, user1_deleted:boolean, user2_deleted:boolean, created_at:datetime, updated_at:datetime
class Conversation < ActiveRecord::Base
has_many :messages
belongs_to :participant_one, :class_name => "User", :foreign_key => :user1_id
belongs_to :participant_two, :class_name => "User", :foreign_key => :user2_id
private
def self.between(user1, user2)
c = Conversation.arel_table
Conversation.where(c[:user1_id].eq(user1).and(c[:user2_id].eq(user2)).or(c[:user1_id].eq(user2).and(c[:user2_id].eq(user1))))
Message: has relevant fields: id:int, conversation_id:int, author_id:int, content:text, created_at:datetime, updated_at:datetime
class Message < ActiveRecord::Base
belongs_to :conversation, :touch => true
I'm not really sure if I need participant_one and participant_two, but I use
def conversation_partner(conversation)
conversation.participant_one == current_user ? conversation.participant_two : conversation.participant_one
end
in a ConversationHelper so that in views, I can show the other participant.
So this basically works. But one of the complications I have is that I do not really distinguish the users very well in the Conversation, a user can be in either the user1 field or the user2 field. So I need to constantly look for the user in one or the other field, e.g. in the finder_sql of the User has_many declaration. Also, when I create a new message, I first search to see if there's a Conversation parameter, or if there isn't one, see if there's a conversation between the two users, and if not, then create a new conversation. (You can either send a message from the conversation index (like a reply), or the current_user can be viewing the another user and click on the "send this user a message" link. The messagecontroller looks like this, and uses that self.between method in the Conversation model:
class MessagesController < ApplicationController
before_filter :get_user
before_filter :find_or_create_conversation, :only => [:new, :create]
def new
@message = Message.new
end
def create
@message = @conversation.messages.build(params[:message])
@message.author_id = current_user.id
if @message.save
redirect_to user_conversation_path(current_user, @conversation), :notice => "Message sent!"
else
redirect_to @conversation
end
end
private
def get_user
@user = User.find(params[:user_id])
end
def find_or_create_conversation
if params[:conversation_id]
@conversation = Conversation.find(params[:conversation_id])
else
@conversation = Conversation.between(@user.id, current_user.id).first or @conversation = Conversation.create!(:user1_id => current_user.id, :user2_id => @user.id)
end
end
(my routes look like this:)
resources :users do
resources :conversations, :only => [:index, :create, :show, :destroy] do
resources :messages, :only => [:new, :create]
end
resources :messages, :only => [:new]
end
So now, I am having problems trying to set the user1_deleted or user2_deleted flags. (and similarly if/when i implement a read/up-to-date flag). The problem is that because the same User can have many conversations, but he can either be the user1 or the user2, it becomes difficult to find him. I was thinking I can do something like this in the Conversation model:
def self.active(user)
Conversation.where(which_user?(user) + "_deleted = ?", false)
end
def self.which_user?(user)
:user1_id == user ? 'user1' : 'user2'
end
But then you can't run it an entire conversation unless you iterate through each of the User's conversation one by one, because sometimes he is user1, and sometimes he is user2. Should I ditch this whole approach and try a new design? If so, does anyone a possible approach that would be more elegant/perform better/actually work and still meet the same needs?
This is a pretty long question, so I appreciate anyone willing to wade through all this with me. Thanks.
The mobile application is using the MQTT protocol. The messenger on the webpage on the other hand, along with the login page communicate through a standard HTTPS protocol as the remaining part of the application.
The Messenger Profile API allows you to set, update, retrieve, and delete properties from the Page Messenger Profile.
Facebook Messenger is a mobile app that enables chat, voice and video communications between the social media site's web-based messaging and smartphones.
Messages sent with the Messenger Platform are classified as one of three different message types. Each message type has different policies and guidelines for what types of content and under what conditions they can be sent.
kindofgreat,
This question intrigued me a bit, so I spent a couple hours experimenting, and here are my findings. The result is an app where any number of users can participate in a conversation.
I went with a data model that has an intermediate model between User
and Conversation
called UserConveration
; it's a join model and holds data about the state of a user and a conversation together (namely, whether the conversation is read, deleted, etc.)
The implementation is on GitHub, and you can see a diff of the code I wrote (versus the code that was automatically generated, to keep all the cruft out) at https://github.com/BinaryMuse/so_association_expirement/compare/53f2263...master.
Here are my models, stripped down to only the associations:
class User < ActiveRecord::Base
has_many :user_conversations
has_many :conversations, :through => :user_conversations
has_many :messages, :through => :conversations
end
class UserConversation < ActiveRecord::Base
belongs_to :user
belongs_to :conversation
has_many :messages, :through => :conversation
delegate :subject, :to => :conversation
delegate :users, :to => :conversation
end
class Conversation < ActiveRecord::Base
has_many :user_conversations
has_many :users, :through => :user_conversations
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :user
belongs_to :conversation
end
And here's what the database looks like:
create_table "conversations", :force => true do |t|
t.string "subject"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "messages", :force => true do |t|
t.integer "user_id"
t.integer "conversation_id"
t.text "body"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "user_conversations", :force => true do |t|
t.integer "user_id"
t.integer "conversation_id"
t.boolean "deleted"
t.boolean "read"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "users", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
The basic idea is to present the user with a "conversation," when in reality behind the scenes we're managing UserConversation
s for all the users involved in a conversation. See especially the method create_user_conversations
on the UserConversation
model, which is responsible for creating an entry in the join table for every user associated with a conversation.
There are also lots of has_many :through
and delegates
calls in the models to make it as painless as possible to get the data we want... e.g. instead of @user_conversation.conversation.subject
you can use @user_conversation.subject
; the same goes for the messages
attribute.
I realize it's quite a bit of code, so I encourage you to get the source and play around with it. It all works, other than "deleting" conversations (I didn't bother with this, but marking messages as read/unread does work). Be aware that you have to be "signed in" as a user to perform some operations, e.g. creating a new conversation, etc. You can sign in by clicking on a user from the main page and choosing "Sign In as This User."
One other thing to keep in mind is that the URLs say "conversation" to keep things nice for the user, even though the controller in use is the "UserConversations" controller--check out the routes file.
If you have any longer/in-depth questions, feel free to contact me via GitHub or via the contact details on my StackOverflow profile.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With