Implement direct messages (no chat room) using phoenix channels

Hi, I’m creating an app where users, after making a connection, can speak to each other, this is not the classic chat room example as are just direct messages between 2 users. Is there a specific way to implement this using Phoenix Channels? been googling but couldn’t find something useful, from the top of my head I’m just thinking of having a topic like conversation and as subtopic a combination of both users ids, so that the subtopic will be unique for any 2 users combination.
Is this a good approach? or is there a better/cleaner way to implement this?

thanks

2 Likes

You might try to have a common channel for all, filtered conditionaly on handle_out…

Like in this post here

1 Like

Yeah, I also see it this way. Everyone joined one big channel and then communication rely on direct handle_in eg. someone pushes into this channel for topic user:#{user_id} and only that user can read it and in return he uses push or reply, but not broadcast.

I would have a topic like private:user1:user2 and then just have a consistent sort for the users so that you don’t end up with private:user2:user1 also. Maybe use user IDs. That way the messages are contained and you don’t have to worry about another user reading them as much.

I tried this, but the problem is to know when to subscribe to those private channels…

eg. A wants to call B, but B does not know it should connect to the private channel between A and B

Oh yes, then it could be better to just have private:user inbox for every user as said above and only have the user in question be able to connect to it for reading. Just include the sender’s ID/name in the message payload.

In the example given, You can broadcast… but when the message leaves… it is filtered and pushed only to those needed

I like the idea of having a topic like private:user1:user2. When an event occurs, I know I have to connect both users to the private channel, that way I think A wants to call B, but B does not know it should connect to the private channel between A and B wouldn’t be a problem.

That might be working too :slight_smile:

I used the other way around because conditions can also work with groups.

eg sending message to a group of people only.

BTW how do You know an event has occured?

I was just thinking that as I had the idea in my head but haven’t implemented it yet. When an event occurs, I have to notify both users in order to them to connect to the private channel, so I guess for the event notification I can use the approach filtering in handle_out, and then use the user1:user2 approach. I could just filter everything in handle_out but I think having a specific subtopic for a private channel makes more sense conceptually speaking.

So You will notify them with a common channel?

exactly, common channel to notify about the event, and let them subscribe to the private channel

Well, I do the same, I send notification on a common channel.

The difference is I notify only the targeted people I need. In my case, the common channel is already private, as the notification occurs between A and B only.

yeah, it makes sense, and I wouldn’t have to open a new socket for the private channel, but I’m just thinking, if I look at a code where only one common channel is used filtering the receptors in handle_out, I would ask myself, where are the private conversations being managed/handled? until I find out that’s being filtered in handle_out, whereas if I have a specific private channel for direct messages, with only one glance I could answer that question.
Does it make sense?

The best way to accomplish this is to have a “control” channel for each user, such as a UserChannel, a topic like user:[user_id]. When User A wants to start a private conversation with User B, they broadcast to User A’s channel:

socket.endpoint.broadcast!("user:b", "open_convo", %{from: "a", room_topic: "priv_room:a:b"})

Then on the client, User A reacts to the event. This would allow your UI to either decline or accept new convos or always join automatically:


userChannel.on("open_convo", ({from: remoteUserID, room_topic: topic}) => {
  let privRoom = socket.channel(topic)
  privRoom.join()
  ....
})

You want to avoid the handle_out usage where all users join the same topic since filtering any individual convo would be extremely expensive. Hope the helps!

17 Likes

Here is a very simplified code, to show what I mean. It assumes payload contains a message, and a receiver_id. It also assumes You store user_id in your socket

def handle_in("notify", payload, socket) do
  sender_id = socket.assigns.user_id

  # This method is intercepted when going out!
  broadcast! socket, "notify_out", %{
    message: payload.message,
    sender_id: sender_id,
    receiver_id: payload.receiver_id}
  {:noreply, socket}
end
    
intercept ["notify_out"]
def handle_out("notify_out", payload, socket) do
  user_id = socket.assigns.user_id
  if (user_id === payload.sender_id || user_id === payload.receiver_id) do
    push socket, "notify_out", payload
    {:noreply, socket}
  else
    {:noreply, socket}
  end
end

UPDATE: Chris McCord does it better :slight_smile:

2 Likes

so many nice options, I like that!

that is a very good point, gonna try to implement something like you suggested merging with some of kokolegorille suggestions :slight_smile:

thanks everyone!

I’d like to quickly point out that you won’t need a new socket to join a new private channel, you can have multiple (lightweight!) channels multiplexed over a single websocket.

1 Like

Hi, i may have a question, in the direct message only one user should get the message, but in the phoenix pg2 pubsub, all the nodes will get notify of the message, is it true and is there some ways to avoid it? thanks!

Why not just broadcast it to a specific user channel then?