Direct messages using Phoenix channels

Hi,

I’m trying to create an app which features direct messaging between two users. The users are randomly assigned; you hit a button and you’re randomly placed in a chat with another active user.

I’ve read a few threads about this and think I get the gist of what to do, but I’m struggling to implement. Here’s what I have so far:

First of all, every user joins their own personal channel, which references their ID:

  const personalChannel = socket.channel(`chat:${userAID}`);
  personalChannel.join();

Then, when a user hits the ‘start a chat’ button I make an ajax request to get the ID of a random user, and push a message containing this user’s ID:

personalChannel.push("initiate_convo", {
   id: toUser
});

In Elixir, I attempt to use that message to broadcast to the target user’s channel:

def handle_in("initiate_convo", %{"id" => id}, socket) do
  broadcast!("chat:#{id}", "open_convo", %{
    from: "16",
    room_topic: "chat:16:#{id}"
  })

  {:reply, {:ok, %{}}, socket}
end

Finally, I catch that on the client side, where I intend to open a private channel for the two users:

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

At the moment I get the following error when hitting the ‘start a chat’ button:

(FunctionClauseError) no function clause matching in Phoenix.Channel.assert_joined!/1
(phoenix) lib/phoenix/channel.ex:589: Phoenix.Channel.assert_joined!("chat:12")

But even beyond that, I’m struggling to get to grips with whether my approach is conceptually sound, or whether I’m missing anything?

Thank you!

3 Likes

:wave:

I’m not sure I completely understand your approach, but I’d probably have two channels, one for users to get chat invitations user:<user_id> and one for chat rooms, chat:<sorted_list_of_user_ids>. And the user channel would also be responsible for starting the chat rooms, instead of using ajax.

I had a demo app which used somewhat similar approach. This test illustrates how it used to work.

Some other possibly relevant files:

Not sure how relevant it is to your use case, but might provide some ideas.

hey :wave:

This definitely helps, thanks! Just to make sure I understand, what you’re suggesting is:

Every user joins a channel with their user_id, like:

 def join("user:" <> id, _params, %{assigns: %{id: id}} = socket) do
    {:ok, socket}
 end

Then we also have a chat channel which is joined with two user IDs, e.g. socket.channel("chats:1:2")

So when user A joins the chat channel, the function for joining also broadcasts to user B’s channel (user:2) with a ‘chat started’ message. On the client side, we listen for that message and then also join the chat channel as user B.

Is that about right? Sorry, just want to make sure I properly understand the concept.

1 Like

Is that about right? Sorry, just want to make sure I properly understand the concept.

Yes, I think so.

I think there were a few complications to exchange the keys for message encryption, but I don’t really remember how that was implemented.

1 Like

I also agree with this approach. You will need to make sure that your chat name function is ordered, so the 2 users join the same topic. "chats:1:2" and "chats:2:1" feel similar, but would be different topics.

There are many benefits to the ChatRoomChannel approach. You can separate the logic of chat from chat management, and you can do things like track room presence to know if the other user is still online in your chat or not. You could do those things with a single UserChannel, but it would be more difficult and not give benefit otherwise.

3 Likes

Brilliant, thank you both :smile: I’ve got a solution working now based on your advice