In today's episode we're going to modify elm-phoenix-socket so that it has support for Phoenix.Presence. I figured walking through contributing to a library would be useful. This is literally my first ever library contribution in Elm, so we should all learn something! Let's get started.

Project

I started off by chatting with the author of the library and finding out how he would suggest I go about developing an additional feature. He suggested we clone the repo, run elm-reactor, and play around with the examples. I've already pulled it down, so let's just do that:

cd ~/elm/elm-phoenix-socket
elm-reactor

(now visit http://localhost:8000/examples/Chat.elm)

So here's a chat client that's working entirely fine with the existing non-presence hosted phoenix chat app. We'll copy in our chat client for our local phoenix presence-based chat and use that as our example though.

cp ../elm_presence_chat/Main.elm examples/Presence.elm

Now if you run our phoenix app from episode 011.2, you can open this in the reactor and it should be working but using the local files for development purposes to satisfy your import.

Let's go ahead and make a feature branch so that we can keep our code cleanly separated for our eventual Pull Request:

git checkout -b feature/add_phoenix_presence_support

Now let's open up the elm client and the phoenix native js client:

vim src/Phoenix/Socket.elm
# Then - :e ~/elixir/phoenix/web/static/js/phoenix.js

We'll search in phoenix.js for var Presence to find where the presence module begins. It has 2 functions we care about: syncState, which is used to sync a complete state object, typically on load, and syncDiff, which is used for the differences.

We already know that we need to subscribe to events on the channel to handle these two presence messages, so let's go ahead and modify our application to subscribe to the messages and just Debug.log them so we can see what we're dealing with. We'll open up examples/Presence.elm:

initPhxSocket : String -> Phoenix.Socket.Socket Msg
initPhxSocket username =
    Phoenix.Socket.init (socketServer username)
        |> Phoenix.Socket.withDebug
        |> Phoenix.Socket.on "new:msg" "room:lobby" ReceiveChatMessage
        |> Phoenix.Socket.on "presence_state" "room:lobby" HandlePresenceState
        |> Phoenix.Socket.on "presence_diff" "room:lobby" HandlePresenceDiff

So we'll add those two messages:

type Msg
    -- ...
    | HandlePresenceState JE.Value
    | HandlePresenceDiff JE.Value

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        -- ...
        HandlePresenceState raw ->
            model ! []

        HandlePresenceDiff raw ->
            model ! []

Now if you try to join the channel, you'll see these messages getting logged out, because we're using withDebug, and now we've stubbed out places to handle them.

Here's an example of what we want to decode into a PresenceState record type:

{
  "knewter": {
    "metas": [
      "phx_ref": "g20AAAAIxEvUVEnwwz0=",
      "online_at": "{1465, 484686, 28594}",
      "device": "browser"
    ]
  }
}

We can see that the metas will have to be a custom type, because it's defined per implementation. Consequently, I'm assuming our type can be something like this:

import Dict exposing (Dict)


-- Phoenix.Presence types
type alias PresenceState =
    Dict String PresenceStateMetaWrapper


type alias PresenceStateMetaWrapper =
    { metas : List PresenceStateMetaValue }


type alias PresenceStateMetaValue =
    { phx_ref : String
    , online_at : String
    , device : String
    }

Then we want to put this into our model:

type alias Model =
    { newMessage : String
    , messages : List ChatMessage
    , username : String
    , phxSocket : Maybe (Phoenix.Socket.Socket Msg)
    , phxPresences : PresenceState
    }


initialModel : Model
initialModel =
    { newMessage = ""
    , messages = []
    , username = ""
    , phxSocket = Nothing
    , phxPresences = Dict.empty -- Note: when we refactor this to a
      -- Phoenix.Presence module, we'll want something like Phoenix.Presence.init
    }

We'll write the update function to decode the value we get in our message from phoenix:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        -- ...
        HandlePresenceState raw ->
            -- we will write this decoder
            case JD.decodeValue presenceStateDecoder raw of
                Ok presenceState ->
                    let
                        _ =
                            Debug.log "PresenceState" presenceState
                    in
                        --{ model | phxPresences = syncState model.phxPresences presenceState } ! []
                        model ! []

                Err error ->
                    let
                        _ =
                            Debug.log "error" error
                    in
                        model ! []

Next, we need to define the presenceStateDecoder. It basically writes itself based on the types, though it took me around 20 minutes to figure out how those ought to be laid out in conjunction with writing this bit.

-- Our presence state is a dict alias, so we'll use Json.Decode.dict to decode
-- it - this results in a Dict with String keys and the output value of the
-- `presenceStateMetaWrapperDecoder` as the values
presenceStateDecoder : JD.Decoder PresenceState
presenceStateDecoder =
    JD.dict presenceStateMetaWrapperDecoder


-- This decoder just exists to handle the "metas" nesting
presenceStateMetaWrapperDecoder : JD.Decoder PresenceStateMetaWrapper
presenceStateMetaWrapperDecoder =
    JD.object1 PresenceStateMetaWrapper
        ("metas" := JD.list presenceStateMetaValueDecoder)


-- And here we decode the lowest level.  We'll ultimately need to handle this
-- part better since the meta values other than `phx_ref` are variable, but for
-- now we aren't going to bother with making this generalizable until we have
-- something useful to run with.
presenceStateMetaValueDecoder : JD.Decoder PresenceStateMetaValue
presenceStateMetaValueDecoder =
    JD.object3 PresenceStateMetaValue
        ("phx_ref" := JD.string)
        ("online_at" := JD.string)
        ("device" := JD.string)

If you run it now, you should see some messages in the console that begin PresenceState: Dict.fromList... - that implies the decode was successful.

Now, we can launch the web interface that's in the phoenix app, at http://localhost:8000 and log in as a different user. And we can see that we got a new presence diff for travis joining. But we aren't decoding that one yet! And it has a different structure:

{
  "leaves": {},
  "joins": {
    "travis": {
      "metas": [
        {
          "phx_ref": "g20AAAAIBIaaARgeac4=",
          "online_at": "{1465, 503519, 989318}",
          "device": "browser"
        }
      ]
    }
  }
}

So it looks like leaves and joins both contain values of type PresenceState, so let's define a new type for PresenceDiff:

type alias PresenceDiff =
    { leaves : PresenceState
    , joins : PresenceState
    }

All that's left is to build a Json.Decode.Decoder for the presence_diff message, and use it in that update branch. I'm going to leave that as an exercise for you for tomorrow though!

That's it for today.

Summary

In today's episode, we started work on implementing Phoenix.Presence support in elm-phoenix-socket. Tomorrow I'll leave you with an exercise to implement the PresenceDiff decoder. Then we'll implement syncState, syncDiff, and list from phoenix.js and we should be able to track presences in our clients. I hope you enjoyed it. See you soon!

Resources