dwyl / phoenix-liveview-chat-example

đź’¬ Step-by-step tutorial creates a Chat App using Phoenix LiveView including Presence, Authentication and Style with Tailwind CSS
https://liveview-chat-example.fly.dev/
131 stars 13 forks source link

Going further: set up a private chat between users #83

Open ndrean opened 1 year ago

ndrean commented 1 year ago

image

Question: How do you create a private chat between two users?

I did not find a way to get all subscribers.

When you look at the code for Phoenix.PubSub.subscribe:

# Phoenix.PubSub.subscribe
def subscribe(pubsub, topic, opts \\ [])
      when is_atom(pubsub) and is_binary(topic) and is_list(opts) do
   case Registry.register(pubsub, topic, opts[:metadata]) do
  {:ok, _} -> :ok

# Phoenix.PubSub.unsubscribe
def unsubscribe(pubsub, topic) when is_atom(pubsub) and is_binary(topic) do
  Registry.unregister(pubsub, topic)

Dwyl/I use Phoenix.Endpoint instead of Phoenix.PubSub and the PubSub is started with:

{Phoenix.PubSub, name: MyApp.PubSub},

and the standard configuration is:

config :live_map, MyAppWeb.Endpoint,
  pubsub_server: MyApp.PubSub,

We don't use the "atom" form" . Does this start a registry?

I don't see anything:


iex> Registry.count(MyAppPubSub)
# 12 ????
iex> Registry.keys(MyApp.PubSub, self())
# []  ?????
ndrean commented 1 year ago

Sidenote (not from me)

PubSub: allow processes to communicate with each other between a node or multiple nodes. This is important in a distributed world, where server A has a message for a user that’s connected to server B. The message is dispatched over PubSub and the process on server B picks it up for delivery to the user.

Tracker: Tracker is a CRDT-based library to keep a list of information in-sync across a cluster. This information could really be anything, although it’s commonly something related to connected users or other ephemeral types of information. When you write an implementation for Tracker, you have to provide some callbacks like handle_diff 14.

Presence: Implementation of Tracker that works out of the box with Phoenix Channels. You don’t really have to do anything for this other than tie your channels to a Presence module—it’s all built for you already. There is also some JS provided by Phoenix framework for handling Presence on the frontend as well.

Presence is really well suited when you want the list of presence information to be sent to your clients. The quintessential example of this is “who’s online in this chat room” type of information. If you don’t want to broadcast the presence information to clients, then Tracker may be better because you will avoid doing excess work (costs CPU) that you don’t need to do.

PubSub is really separate here. It enables communication, but it doesn’t do anything regarding keeping stateful information distributed in your cluster. It’s not as easy as “broadcast a message that someone joined the channel” because distributed systems introduce a whole host of problems (what happens if a server disconnects, there is a networking issue, etc). Tracker (and therefore Presence) is designed to work in these real-world conditions.

ndrean commented 1 year ago

A drawing is easier to follow I believe: set up N(N+1)/2 one-to-one channels (subscriptions) between users. You need to monitor subscriptions, meaning unsubscribe on deconnection, otherwise you have multiple rendering.

Untitled-2022-11-28-1733(2)

The idea is when a user mounts, he "pre"-build N-1 channels with every connected user, and the topic follows the convention #{user_id}-#{another_user_id}". This way, we have N-1 one-to-one connections. A user can now push a notification to another user and send a private message. Tested with 3 users (thanks to Dwyl's excellent login tools)

However, I wanted to be able to check the existing subscriptions.

Edit 1: you can indeed get all the topics a user subscribed to with:

Registry.keys(MyApp.PubSub, self())

(the error was MyAppWeb.Pubsub instead of MyApp.PubSub .... ^^)

Edit2: I learnt that you can't pattern match on an empty map. Use guard clause Example: to get get "leaves" events, do:

def handle_info(%{event: "presence", payload: %{joins: joins}, socket) when joins == %{}