Monday, August 9, 2021

Using Shrine to add a notification alert to a dynamic favicon

If you've ever worked with the Trello app, you may have noticed that when you have a new notification, the favicon (the little icon in the browser tab) has a red dot added to it. This really helps grab your attention when you receive a notification, but you are working on another tab.

So when the question "can we do this in our app as well?" came up, at first I said, sure, no problem! After all, its just changing the href link to point to another favicon image, but with the red dot.

But then I remembered we allow users to upload their own favicons... Now there's a interesting challenge. 

Using a Shrine to Add the Alert Dot

In our project we use the Shrine gem to handle file uploads, and MiniMagick for image processing. So I added a new derivative to my favicon uploader:
 
class FaviconUploader < Shrine
  Attacher.derivatives do |original|
    magick = ImageProcessing::MiniMagick.source(original)
    red_dot = Rails.root.join('app', 'assets', 'images', 'favicons', 'overlays', 'alert.png')

    {
        favicon:  magick.resize_to_fill!(64, 64),
        alert:  magick.resize_to_fill(64, 64).composite!(red_dot,
          mode:    "over", 
          gravity: "south-east", 
          offset:  [0, 0])
    }
  end
end


Notice that in the original derivative we call resize_to_fill!, while in the new derivative we call resize_to_fill (without the bang) because the bang, which tells MiniMagick to perform the chained effects, should be on the last method called.


So in the new derivative we first resize the image, and then composite the image with a custom PNG image of a red dot, so that it sits in the bottom-right corner.


Dealing with Favicon Change

Now that we have the images, let's move on to the easy part - changing the favicon to the one with the dot when there are notifications, and changing back when the notifications are cleared.
I handled this by adding a data attribute to the link tags for each of the icons. For example, this is what the link tag for standard non-Apple devices looks like when using the static favicon:

<link rel="icon" type="image/png" sizes="64x64" href="/assets/favicons/favicon-default.png" data-alert-favicon="/assets/favicons/favicon-alert.png" data-default-favicon="/assets/favicons/favicon-default.png">

The link tag for the dynamic favicon is rendered like this:

<% data_attrs = %[data-alert-favicon=#{@main_scope.favicon_url(:alert)} data-default-favicon=#{@main_scope.favicon_url(:favicon)}] %>
<% default_href = @main_scope.favicon_url(:favicon) %>
<link rel="icon" type="image/png" sizes="64x64" href="<%= default_href %>" <%= data_attrs %>>

Then I can call the following function when a notification is added or when the last unread notification is marked as read:

function update_favicon_alert(alert_should_be_on){
  $('[rel="apple-touch-icon"], [rel="icon"]').each(function(){
    new_href = $(this).data(`${alert_should_be_on ? 'alert' : 'default'}-favicon`);
    if (new_href && new_href.length > 0) {
      $(this).attr('href', new_href);
    }
  })
}

That's it!

Conclusion

Shrine is a very powerful tool, and we love using it. It does have a steeper learning curve than the Paperclip gem or even Active Storage, but once you get over the hump, it's a versatile tool with enormous capabilities.

Technically we could also embed the number of notifications (from 1 up to say 9, and for any more show 9+) by having 9 such derivatives, and as before update the link to use the appropriate favicon, but that felt to us a bit cumbersome and very unnecessary.