Commit 89c35d77 authored by Jens-Christian Fischer's avatar Jens-Christian Fischer
Browse files

added presence to web server

parent 90a339c2
......@@ -14,6 +14,7 @@ defmodule Grains do
# worker(Grains.Worker, [arg1, arg2, arg3]),
worker(Grains.Concounter, [0]),
worker(Grains.StatefulMap, []),
supervisor(Grains.Presence, []),
# See
......@@ -3,7 +3,7 @@ defmodule Grains.Mixfile do
def project do
[app: :grains,
version: "0.0.29",
version: "0.0.30",
elixir: "~> 1.2",
elixirc_paths: elixirc_paths(Mix.env),
compilers: [:phoenix, :gettext] ++ Mix.compilers,
defmodule Grains.Presence do
@moduledoc """
Provides presence tracking to channels and processes.
See the [`Phoenix.Presence`](
docs for more details.
## Usage
Presences can be tracked in your channel after joining:
defmodule Grains.MyChannel do
use Grains.Web, :channel
alias Grains.Presence
def join("some:topic", _params, socket) do
send(self, :after_join)
{:ok, assign(socket, :user_id, ...)}
def handle_info(:after_join, socket) do
{:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
online_at: inspect(System.system_time(:seconds))
push socket, "presence_state", Presence.list(socket)
{:noreply, socket}
In the example above, `Presence.track` is used to register this
channel's process as a presence for the socket's user ID, with
a map of metadata. Next, the current presence list for
the socket's topic is pushed to the client as a `"presence_state"` event.
Finally, a diff of presence join and leave events will be sent to the
client as they happen in real-time with the "presence_diff" event.
See `Phoenix.Presence.list/2` for details on the presence datastructure.
## Fetching Presence Information
The `fetch/2` callback is triggered when using `list/1`
and serves as a mechanism to fetch presence information a single time,
before broadcasting the information to all channel subscribers.
This prevents N query problems and gives you a single place to group
isolated data fetching to extend presence metadata.
The function receives a topic and map of presences and must return a
map of data matching the Presence datastructure:
%{"123" => %{metas: [%{status: "away", phx_ref: ...}],
"456" => %{metas: [%{status: "online", phx_ref: ...}]}
The `:metas` key must be kept, but you can extend the map of information
to include any additional information. For example:
def fetch(_topic, entries) do
query =
from u in User,
where: in ^Map.keys(entries),
select: {, u}
users = query |> Repo.all |> Enum.into(%{})
for {key, %{metas: metas}} <- entries, into: %{} do
{key, %{metas: metas, user: users[key]}}
The function above fetches all users from the database who
have registered presences for the given topic. The fetched
information is then extended with a `:user` key of the user's
information, while maintaining the required `:metas` field from the
original presence data.
use Phoenix.Presence, otp_app: :grains,
pubsub_server: Grains.PubSub
defmodule Grains.PresenceChannel do
use Grains.Web, :channel
alias Grains.Concounter
alias Grains.Presence
def join("presence:" <> user_id, _params, socket) do
def join("presence:lobby", _, socket) do
send self(), :after_join
{:ok, socket}
def handle_info(:after_join, socket) do
IO.puts("handle_join: #{socket.assigns.user_uuid}")
Presence.track(socket, socket.assigns.user_uuid, %{
online_at: :os.system_time(:milli_seconds)
push socket, "presence_state", Presence.list(socket)
{:noreply, socket}
......@@ -13,6 +13,8 @@
// to also remove its path from "config.paths.watched".
import "phoenix_html";
import {Socket, Presence} from "phoenix"
import _ from "underscore"
import socket from "./socket"
import SliderPanel from "./slider";
......@@ -22,6 +24,59 @@ import OrientationPanel from "./orientation";
SliderPanel.init(socket, "sliderPanel", ["s1", "s2", "s3"]);
OrientationPanel.init(socket, "orientationPanel");
// Presence
let presences = {}
let formatTimestamp = (timestamp) => {
let date = new Date(timestamp)
return date.toLocaleDateString()
let listBy = (user, {metas: metas}) => {
return {
user: user,
onlineAt: formatTimestamp(metas[0].online_at)
let userList = document.getElementById("UserList")
let render = (presences) => {
userList.innerHTML = Presence.list(presences, listBy)
.map(presence => `
<br><small>online since ${presence.onlineAt}</small>
let room ="presence:lobby", {})
room.on("presence_state", state => {
presences = Presence.syncState(presences, state)
room.on("presence_diff", diff => {
presences = Presence.syncDiff(presences, diff)
console.log("joining presence");
room.join().receive("ok", resp => {
console.log("presence joined");
.receive("error", reason => console.log("presence join failed", reason))
// Import local files
// Local files can be imported directly using relative
......@@ -24,6 +24,13 @@
<%= render @view_module, @view_template, assigns %>
<div class="row">
<div class="col-md-8">
<ul id="UserList" class="list-unstyled">
<li>Loading online users</li>
</div> <!-- /container -->
<script>window.userToken = "<%= assigns[:user_token] %>"</script>
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment