dwyl / dwyl/PWA-Liveview

Presence via a hook

Open
#31 0 comments 2 reactions 0 assignees View on GitHub
documentation
Dominant language
JavaScript
Stars
33
Forks
4
PR merge metrics
No merged PRs in 30d

Description

[EDITED]

"Almost" straight from the [documentation](https://hexdocs.pm/phoenix/presence.html#usage-with-channels-and-javascript).

Almost because of the bonus of having a reactive banner on each LiveView.

The banner is rendered in each LiveView (just a different "module_id" assign, which is NOT a dom_id)

```elixir

```

> the `user_id` comes from the `mount/3`

The PhoenixComponent "Users" contains a DOM element that will be filled by JS, where you set `phx-udpate="ignore"` since the innerContent is managed by the reactive component.

```elixir
defmodule LiveviewPwaWeb.Users do
use Phoenix.Component

@moduledoc false

attr :user_id, :string, required: true
attr :module_id, :string, required: true

def display(assigns) do
~H"""





User ID:

{@user_id}






"""
end
end
```

Use a Channel

Server-side, you just:

- `Presence.track` on the "user-id" when it mounts (on each navigation to a given page)
- `push` the "user-id" to the client. This is because I wanted to isolate the current user from the other users in the Presence.list
- get the `Presence.list` and `push` it to the connected client(s)

```elixir
defmodule LiveviewPwa.PresenceChannel do
use Phoenix.Channel
alias LiveviewPwa.Presence
require Logger

@moduledoc false

@impl true
def join("proxy:presence", _params, socket) do
send(self(), :after_join)
{:ok, socket}
end

@impl true
def handle_info(:after_join, socket) do
user_id = socket.assigns.user_id
{:ok, _} = Presence.track(socket, user_id, %{id: user_id})
send(self(), :push_users)
{:noreply, socket}
end

def handle_info(:push_users, socket) do
users = Presence.list(socket)
# pass the user_id to the channel
:ok = push(socket, "user", %{from: socket.assigns.user_id})
# push the presence state to the channel
:ok = push(socket, "presence_state", users)
{:noreply, socket}
end
end
```

In the main JS script ("app.js"), call this function that targets given ids (the "module_id" defined above, set in ) to display the presence list.

It contains the client-side logic of the Channel. The difficulty here is that you want to support navigation, meaning your banner with the users logged-in is present and reactive on each page.

- a listener `channel.on("user")` (to detect the current user)
- a listener on navigation (event "phx:page-loading-stop" to a page that uses this channel.
- a `new Presence` object that contains the `presence.onSync` listener to react on Presence changes when the Channel is connected
- a rendering JS component (named `MountUsers` here). It receives the updated "userIDs", the current user ID and the DOM element ID on where to mount)

```js
import { CONFIG } from "@js/main";

export async function setPresence(userSocket, topic, user_token) {
const [{ Presence }, { useChannel }, { MountUsers }] = await Promise.all([
import("phoenix"),
import("@js/user_socket/useChannel"),
import("@js/components/mountUsers"),
]);

const channel = await useChannel(userSocket, topic, { user_token });
const presence = new Presence(channel);

let userID = null;
let userIDs = [];
let usersComponent = null;

// Track user ID sent from backend
channel.on("user", ({ from }) => {
userID = from;
});

// Utility: determine which DOM ID to use
const getTargetEl = () => {
const path = window.location.pathname;
const id =
path === "/yjs"
? CONFIG.NAVIDS.yjs.id
: path === "/map"
? CONFIG.NAVIDS.map.id
: CONFIG.NAVIDS.elec.id;
return document.getElementById(id);
};

// Render or update the component, if possible
const tryRender = () => {
const el = getTargetEl();
if (!el) return;

// Avoid duplicate mounting
if (usersComponent) {
if (el === usersComponent.el) {
console.log("[tryRender] Same element, updating...");
usersComponent.update({ userIDs, userID, el });
return;
} else {
console.log(
"[tryRender] Different element, disposing old, mounting new..."
);
usersComponent.dispose();
usersComponent = MountUsers({ userIDs, userID, el });
}
} else {
console.log("[tryRender] No existing component, mounting...");
usersComponent = MountUsers({ userIDs, userID, el });
}
// usersComponent.el = el;
};

// Sync presence when changed
presence.onSync(() => {
console.log("[presence.onSync]");
userIDs = presence.list((id, _meta) => id);
tryRender();
});

// Handle LiveView navigation
window.addEventListener("phx:page-loading-stop", () => {
console.log("[phx:page-loading-stop]");
tryRender();
});
}
```

where the helper `useChannel` is:

```js
export function useChannel(socket, topic, params) {
return new Promise((resolve, reject) => {
if (!socket) {
reject(new Error("Socket not found"));
return;
}

const channel = socket.channel(topic, params);
channel
.join()
.receive("ok", () => {
console.log(`Joined successfully Channel : ${topic}`);
resolve(channel);
})

.receive("error", (resp) => {
console.log(`Unable to join ${topic}`, resp.reason);
reject(new Error(resp.reason));
});
channel.onClose(() => console.log(`Channel closed: ${topic}`));
});
}
```

The last bit is the reactive JS component called "MountUsers" above. . I used `SolidJS`. One great power is the integration with LiveView. You can return a "disposer" , and possibly any function such as the "update" to mutates the props, thus render selectively. check in "setPresence" how we "dispose" or "udpate".

```js
import { render } from "solid-js/web";
import { For, createSignal, batch } from "solid-js";

export function MountUsers(props) {
console.log("[MountUsers]");

const [users, setUsers] = createSignal(props.userIDs);
const [dom, setDom] = createSignal(props.el);

const dispose =
props.el &&
render(
() => (


{users().length}
Online user(s):  

{(user) => (

{user}

)}


),
dom()
);

const update = (newProps) => {
batch(() => {
setUsers(newProps.userIDs);
setDom(newProps.el);
});
};

return {
update,
dispose,
};
}

```

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by tracing the PresenceChannel, the Users Phoenix component, app.js, useChannel, and MountUsers. Check how the channel tracks users, how presence updates reach the reactive component, and how phx:page-loading-stop handles LiveView navigation. Done means the presence banner renders and stays updated on each relevant LiveView while isolating the current user.

Written by the indexing model from the issue text.

Assessment

Tech stack
elixir, javascript
Domain
full-stack, web-dev
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.