Pushing notifications to your browser with LiveView

Hey guys,

I have a pattern I’ve been using on a few projects and I just wanted to share :slight_smile:

Quite often I want to notify a user if something has happened when the browser window is in the background. For my use case, the js Notification API is perfect. This is what I do;

in app.js;

let Hooks = {}

function sendNotification(title, message) {
  if(Notification.permission === "granted") {
    try {
      new Notification(title, {body: message, requireInteraction: false});
    } catch (e) {
      console.debug("notifcation error: " + e)
    }
  }
}

Hooks.Notification = {
  mounted(){
    if(Notification.permission === "default") {
      Notification.requestPermission();
    }
    this.handleEvent("notification", ({title, message}) => sendNotification(title, message));
  }
}

Note that this will ask the user for the notification on mount, which might not be what you want. You could easily have a button that pushes to the client with an event that asks for notification.

To hook it in your need to add the hooks to your liveSocket call like;

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

then on your live view;

<div id="notification" phx-hook="Notification"></div>

And finally in your live view supporting code eg a handle info callback;

{:noreply, push_event(socket, "notification", %{title: title, message: message})}

:slight_smile:

There are lots of enhancements you can do like only send these notifications if the browser is in the background eg. watch for phx-window-blur and phx-window-focus events and set some state so you know when it’s in the background. You could also enhance it by using a service worker and sending the notifcations via gcm/apns but that was an overkill for my use case.

Overall it’s been a nice pattern - I love when i’m uploading a file or processing some work and get a notification when it’s complete.

Cheers,
Anko

18 Likes

I noticed something really interesting about this. If your browser doesn’t have focus, Safari and Chrome will disconnect the websocket after about 10 minutes.

A way around this is to push an event from the server to the client, waking it up. phoenix keepalives seem to be from client to server so they do not work for this.

This is what I do;

  @impl true
  def handle_info(:ping_client, socket) do
    IO.puts "ping_client" 

    Process.send_after(self(), :ping_client, 60_000 * 5)
    {:noreply, push_event(socket, "server_ping", %{})}
  end

and in my mount make sure you start with a

if connected? socket do
  Process.send_after(self(), :ping_client, 5000) # time doesn't matter so much
end

Maybe pings should go from server to client? @josevalim @chrismccord

Has anyone else noticed this?

related:

Perhaps we should have a configuration for it. But after 10 minutes unfocused… it probably makes sense to disconnect until focused again? Otherwise you always risk consuming battery on devices, etc.

2 Likes

It’s not that they are closing the websocket directly; they are stopping the timers running on unfocussed pages which is stopping the client side ping which is closing the websocket.

I agree a configuration would be a good compromise. It’s hard to know the right approach but it seems like getting notifications would be a use case where we don’t mind the battery drain as much - the point of them is to alert you when the web page is not in focus. I guess if it’s abused the browser developers will actively disconnect websockets when they have lost focus.

You could also argue that the reconnect work is more expensive than just keeping the websocket open!

Are we reconnecting immediately or only when the user focus again?

It seems to have some backoff but it doesn’t need user focus. I need to perform more testing.

@Anko FWIW there’s a PR out there related to Chrome disconnects: Schedule heartbeat on reply to avoid intensive throttling by mcrumm · Pull Request #4309 · phoenixframework/phoenix · GitHub

3 Likes