Skip to content

Commit

Permalink
Added cursor presence tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
Mehdi Necibi committed Oct 23, 2023
1 parent b318c70 commit 37973e8
Show file tree
Hide file tree
Showing 13 changed files with 366 additions and 35 deletions.
22 changes: 18 additions & 4 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,29 @@
// Include phoenix_html to handle method=PUT/DELETE in forms and buttons.
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import topbar from "../vendor/topbar"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})

let Hooks = {};

Hooks.TrackClientCursor = {
mounted() {
document.addEventListener('mousemove', (e) => {
const x = (e.pageX / window.innerWidth) * 100; // in %
const y = (e.pageY / window.innerHeight) * 100; // in %
this.pushEvent('cursor-move', { x, y });
});
}
};


let liveSocket = new LiveSocket("/live", Socket, { hooks: Hooks, params: { _csrf_token: csrfToken } })

// Show progress bar on live navigation and form submits
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

Expand Down
1 change: 1 addition & 0 deletions lib/cluster_test/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ defmodule ClusterTest.Application do
# Start a worker by calling: ClusterTest.Worker.start_link(arg)
# {ClusterTest.Worker, arg},
# Start to serve requests, typically the last entry
ClusterTestWeb.Presence,
ClusterTestWeb.Endpoint,
{Cluster.Supervisor, [topologies, [name: ClusterTest.ClusterSupervisor]]}
]
Expand Down
11 changes: 11 additions & 0 deletions lib/cluster_test_web/channels/presence.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule ClusterTestWeb.Presence do
@moduledoc """
Provides presence tracking to channels and processes.
See the [`Phoenix.Presence`](https://hexdocs.pm/phoenix/Phoenix.Presence.html)
docs for more details.
"""
use Phoenix.Presence,
otp_app: :cluster_test,
pubsub_server: ClusterTest.PubSub
end
26 changes: 0 additions & 26 deletions lib/cluster_test_web/components/layouts/app.html.heex
Original file line number Diff line number Diff line change
@@ -1,29 +1,3 @@
<header class="px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between border-b border-zinc-100 py-3 text-sm">
<div class="flex items-center gap-4">
<a href="/">
<img src={~p"/images/logo.svg"} width="36" />
</a>
<p class="bg-brand/5 text-brand rounded-full px-2 font-medium leading-6">
v<%= Application.spec(:phoenix, :vsn) %>
</p>
</div>
<div class="flex items-center gap-4 font-semibold leading-6 text-zinc-900">
<a href="https://twitter.com/elixirphoenix" class="hover:text-zinc-700">
@elixirphoenix
</a>
<a href="https://github.com/phoenixframework/phoenix" class="hover:text-zinc-700">
GitHub
</a>
<a
href="https://hexdocs.pm/phoenix/overview.html"
class="rounded-lg bg-zinc-100 px-2 py-1 hover:bg-zinc-200/80"
>
Get Started <span aria-hidden="true">&rarr;</span>
</a>
</div>
</div>
</header>
<main class="px-4 py-20 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} />
Expand Down
18 changes: 14 additions & 4 deletions lib/cluster_test_web/controllers/page_controller.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
defmodule ClusterTestWeb.PageController do
use ClusterTestWeb, :controller

def home(conn, _params) do
# The home page is often custom made,
# so skip the default app layout.
render(conn, :home, layout: false)
def index(conn, _params) do
session = conn |> get_session()

case session do
%{"user" => _user} ->
conn
|> redirect(to: "/cursors")

_ ->
conn
|> put_session(:user, ClusterTestWeb.Names.generate())
|> configure_session(renew: true)
|> redirect(to: "/cursors")
end
end
end
99 changes: 99 additions & 0 deletions lib/cluster_test_web/live/cursor_positions_live/index.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
defmodule ClusterTestWeb.CursorPositionsLive.Index do
alias ClusterTestWeb.Presence

use ClusterTestWeb, :live_view

@clustertest "clustertest"

@impl true
def mount(_params, %{"user" => user}, socket) do
Presence.track(self(), @clustertest, socket.id, %{
socket_id: socket.id,
x: 50,
y: 50,
name: user
})

ClusterTestWeb.Endpoint.subscribe(@clustertest)

initial_users =
Presence.list(@clustertest)
|> Enum.map(fn {_, data} -> data[:metas] |> List.first() end)

updated =
socket
|> assign(:user, user)
|> assign(:users, initial_users)
|> assign(:socket_id, socket.id)

{:ok, updated}
end

@impl true
def handle_event("cursor-move", %{"x" => x, "y" => y}, socket) do
key = socket.id
payload = %{x: x, y: y}

metas =
Presence.get_by_key(@clustertest, key)[:metas]
|> List.first()
|> Map.merge(payload)

Presence.update(self(), @clustertest, key, metas)

{:noreply, socket}
end

@impl true
def handle_info(%{event: "presence_diff", payload: _payload}, socket) do
users =
Presence.list(@clustertest)
|> Enum.map(fn {_, data} -> data[:metas] |> List.first() end)

updated =
socket
|> assign(users: users)
|> assign(socket_id: socket.id)

{:noreply, updated}
end

@impl true
def render(assigns) do
~H"""
<.header>
Listing Cursor positions
</.header>
<div>
<div>Current node: <%= node() %></div>
<div>Other nodes: <%= "[#{Enum.join(Node.list(), ",")}]" %></div>
<div>
<ul class="list-none" id="cursors" phx-hook="TrackClientCursor">
<%= for user <- @users do %>
<li
style={"color: deeppink; left: #{user.x}%; top: #{user.y}%"}
class="flex flex-col absolute pointer-events-none whitespace-nowrap overflow-hidden"
>
<svg
version="1.1"
width="25px"
height="25px"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 21 21"
>
<polygon fill="black" points="8.2,20.9 8.2,4.9 19.8,16.5 13,16.5 12.6,16.6" />
<polygon fill="currentColor" points="9.2,7.3 9.2,18.5 12.2,15.6 12.6,15.5 17.4,15.5" />
</svg>
<span style="background-color: deeppink;" class="mt-1 ml-4 px-1 text-sm text-white">
<%= user.name %>
</span>
</li>
<% end %>
</ul>
</div>
</div>
"""
end
end
17 changes: 17 additions & 0 deletions lib/cluster_test_web/names.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule ClusterTestWeb.Names do
def generate do
title = ~w(Sir Sr Prof Saint Ibn Lady Madam Mistress Herr Dr) |> Enum.random()

name =
[
~w(B C D F G H J K L M N P Q R S T V W X Z),
~w(o a i ij e ee u uu oo aj aa oe ou eu),
~w(b c d f g h k l m n p q r s t v w x z),
~w(o a i ij e ee u uu oo aj aa oe ou eu)
]
|> Enum.map(fn l -> Enum.random(l) end)
|> Enum.join()

"#{title} #{name}"
end
end
4 changes: 3 additions & 1 deletion lib/cluster_test_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ defmodule ClusterTestWeb.Router do
scope "/", ClusterTestWeb do
pipe_through :browser

get "/", PageController, :home
get "/", PageController, :index

live "/cursors", CursorPositionsLive.Index, :index
end

# Other scopes may use custom stacks.
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ defmodule ClusterTest.MixProject do
{:phoenix_live_view, "~> 0.20.1"},
{:floki, ">= 0.30.0", only: :test},
{:phoenix_live_dashboard, "~> 0.8.2"},
{:phoenix_pubsub, "~> 2.0"},
{:esbuild, "~> 0.7", runtime: Mix.env() == :dev},
{:tailwind, "~> 0.2.0", runtime: Mix.env() == :dev},
{:swoosh, "~> 1.3"},
Expand Down
12 changes: 12 additions & 0 deletions priv/repo/migrations/20231023203353_create_cursor_positions.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule ClusterTest.Repo.Migrations.CreateCursorPositions do
use Ecto.Migration

def change do
create table(:cursor_positions) do
add :x, :integer
add :y, :integer

timestamps(type: :utc_datetime)
end
end
end
61 changes: 61 additions & 0 deletions test/cluster_test/cursor_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
defmodule ClusterTest.CursorTest do
use ClusterTest.DataCase

alias ClusterTest.Cursor

describe "cursor_positions" do
alias ClusterTest.Cursor.CursorPositions

import ClusterTest.CursorFixtures

@invalid_attrs %{y: nil, x: nil}

test "list_cursor_positions/0 returns all cursor_positions" do
cursor_positions = cursor_positions_fixture()
assert Cursor.list_cursor_positions() == [cursor_positions]
end

test "get_cursor_positions!/1 returns the cursor_positions with given id" do
cursor_positions = cursor_positions_fixture()
assert Cursor.get_cursor_positions!(cursor_positions.id) == cursor_positions
end

test "create_cursor_positions/1 with valid data creates a cursor_positions" do
valid_attrs = %{y: 42, x: 42}

assert {:ok, %CursorPositions{} = cursor_positions} = Cursor.create_cursor_positions(valid_attrs)
assert cursor_positions.y == 42
assert cursor_positions.x == 42
end

test "create_cursor_positions/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Cursor.create_cursor_positions(@invalid_attrs)
end

test "update_cursor_positions/2 with valid data updates the cursor_positions" do
cursor_positions = cursor_positions_fixture()
update_attrs = %{y: 43, x: 43}

assert {:ok, %CursorPositions{} = cursor_positions} = Cursor.update_cursor_positions(cursor_positions, update_attrs)
assert cursor_positions.y == 43
assert cursor_positions.x == 43
end

test "update_cursor_positions/2 with invalid data returns error changeset" do
cursor_positions = cursor_positions_fixture()
assert {:error, %Ecto.Changeset{}} = Cursor.update_cursor_positions(cursor_positions, @invalid_attrs)
assert cursor_positions == Cursor.get_cursor_positions!(cursor_positions.id)
end

test "delete_cursor_positions/1 deletes the cursor_positions" do
cursor_positions = cursor_positions_fixture()
assert {:ok, %CursorPositions{}} = Cursor.delete_cursor_positions(cursor_positions)
assert_raise Ecto.NoResultsError, fn -> Cursor.get_cursor_positions!(cursor_positions.id) end
end

test "change_cursor_positions/1 returns a cursor_positions changeset" do
cursor_positions = cursor_positions_fixture()
assert %Ecto.Changeset{} = Cursor.change_cursor_positions(cursor_positions)
end
end
end
Loading

0 comments on commit 37973e8

Please sign in to comment.