JavaScript

Notification API – sposób na powiadomienia w (nie)dalekiej przyszłości?

Przeważnie lubimy być powiadamiani o różnych wydarzeniach. Powiadomienia pełnią rolę stricte informacyjną lub zachęcają nas do konkretnego działania: odpisanie na wiadomość, kliknięcie itd. W moim przypadku najczęściej powiadamia mnie Skype czy inne aplikacje dla Modern UI jak kalendarz no i mój smartfon.

Zarówno sam Modern UI jak i smartfony mają, czy raczej miały, przewagę nad stronami w kwestii powiadomień gdyż potrafią wyświetlić je „na interfejsie”. Powoli przeglądarki zaczynają do tego dojrzewać. W Google Chrome zaimplementowano nawet osobny zasobnik na powiadomienia. Mozilla Firefox za to sztywno trzyma się specyfikacji W3C, która nadal jest w fazie working draft.

Notification API pozwala nam na wykorzystanie funkcji powiadomień w nadzwyczaj prosty sposób. Google Chrome posiada dodatkowo własny obiekt do korzystania z powiadomień, jednak nie będę go omawiał w tym wpisie. Nie ma takiej potrzeby, gdyż rozwiązanie oparte o API od W3C działa wystarczająco dobrze.

Stworzyłem demo (UWAGA: dźwięk powiadomień), które pokaże w jaki sposób możemy wykorzystać Notification API na przykładzie powiadomień z jakimi spotykamy się na portalach społecznościach. Kod do pobrania znajdziecie, jak zawsze, na GitHub.

HTML

<!DOCTYPE html> <html>     <head>         <meta charset="utf-8">         <meta http-equiv="X-UA-Compatible" content="IE=edge">         <title>Social Notifications</title>         <meta name="viewport" content="width=device-width, initial-scale=1">          <link href='http://fonts.googleapis.com/css?family=Droid+Sans' rel='stylesheet' type='text/css'>         <link rel="stylesheet" href="css/app.css">     </head>     <body>                  <div id="stats">             <ul>                 <li>                     <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">                         <path id="user-icon" d="M377.055,170.75c0,66.275-53.726,120-120,120s-120-53.725-120-120c0-66.273,53.726-120,120-120                             S377.055,104.477,377.055,170.75z M356.101,296.41c-27.968,22.084-62.633,34.34-99.046,34.34c-36.835,0-71.839-12.543-99.934-35.051                             C83.067,322.875,50,417.877,50,461.25h412C462,418.25,428.678,323.926,356.101,296.41z"/>                     </svg>                     <span id="friends-counter"></span>                     <button id="friends-add">+</button>                 </li>                 <li>                     <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">                         <path id="speech-icon" d="M256,65.353c-108.81,0-206,73.248-206,173.431c0,35.533,12.684,70.421,35.135,97.493                             C86.083,368,67.583,413.5,50.918,446.647c44.665-8.147,108.165-26.147,136.963-43.95C346.438,441.636,462,343.677,462,238.783                             C462,138.051,364.132,65.353,256,65.353z M180.527,270.028c-15.188,0-27.5-12.312-27.5-27.5s12.312-27.5,27.5-27.5                             s27.5,12.312,27.5,27.5S195.716,270.028,180.527,270.028z M262.527,270.028c-15.188,0-27.5-12.312-27.5-27.5s12.312-27.5,27.5-27.5                             s27.5,12.312,27.5,27.5S277.716,270.028,262.527,270.028z M343.527,270.028c-15.188,0-27.5-12.312-27.5-27.5s12.312-27.5,27.5-27.5                             s27.5,12.312,27.5,27.5S358.716,270.028,343.527,270.028z"/>                     </svg>                     <span id="msg-counter"></span>                     <button id="msg-add">+</button>                 </li>                 <li>                     <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">                         <path id="info-icon" d="M255.998,50.001C142.229,50.001,50,142.229,50,255.999c0,113.771,92.229,206,205.998,206                             c113.771,0,206.002-92.229,206.002-206C462,142.229,369.77,50.001,255.998,50.001z M289.025,377.242h-64.05V226.944h64.05V377.242z                             M257,196.884c-19.088,0-34.563-15.476-34.563-34.564c0-19.088,15.475-34.563,34.563-34.563c19.09,0,34.562,15.476,34.562,34.563                             C291.562,181.409,276.09,196.884,257,196.884z"/>                     </svg>                     <span id="info-counter"></span>                     <button id="info-add">+</button>                 </li>             </ul>         </div>          <div id="contribution">             <p><a href="http://michal.zalecki.pl">Michał Załęcki</a> for <a href="http://webroad.pl">WEBroad.pl</a><br />Icons: <a href="http://iconmonstr.com/">iconmonstr</a></p>         </div>          <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>         <script src="js/app.js"></script>     </body> </html>

Jak zapewne zauważyłeś w demo korzystam z grafiki wektorowej. Jeżeli jesteś zainteresowany używaniem SVG na stronie pewnie zaciekawi Cię wpis, który poświęciłem wykonaniu takiego pliku i wykorzystaniu go w prostej animacji: Prosta animacja z wykorzystaniem SVG i JavaScript.

CSS

html {   font-size: 16px; }  body {   background: #093161;   margin: 0;   color: #E2E9ED;   font-family: 'Droid Sans', sans-serif; } body a {   color: #ffcc00;   text-decoration: none;   -webkit-transition: color 300ms;   -moz-transition: color 300ms;   -o-transition: color 300ms;   transition: color 300ms; } body a:hover, body a:focus {   color: #e6b800; } body a:active {   color: #cca300; }  #stats {   text-align: center; } #stats ul {   list-style: none;   margin: 8rem 0 0 0;   padding: 0; } #stats li {   -webkit-border-radius: 50%;   -moz-border-radius: 50%;   -ms-border-radius: 50%;   -o-border-radius: 50%;   border-radius: 50%;   padding: 1rem;   display: inline-block;   margin: 0 2rem;   background: #ffcc00;   position: relative; } #stats span, #stats button {   position: absolute;   top: 7.375rem;   left: 7.375rem;   -webkit-border-radius: 50%;   -moz-border-radius: 50%;   -ms-border-radius: 50%;   -o-border-radius: 50%;   border-radius: 50%;   background: #ffcc00;   width: 4rem;   height: 4rem;   line-height: 4rem;   color: #093161;   font-size: 1.5rem;   display: block; } #stats button {   top: -1.25rem;   left: -1.25rem;   border: none;   cursor: pointer;   padding: 0;   font-size: 2.375rem;   outline: none; } #stats span {   -webkit-transition: -webkit-transform 300ms;   -moz-transition: -moz-transform 300ms;   -o-transition: -o-transform 300ms;   transition: transform 300ms;   backface-visibility: hidden;   font-weight: bold; } #stats span.rotate {   -webkit-transform: rotateX(360deg);   -moz-transform: rotateX(360deg);   -ms-transform: rotateX(360deg);   -o-transform: rotateX(360deg);   transform: rotateX(360deg); } #stats svg {   width: 8rem;   height: 8rem;   fill: #093161;   position: relative;   z-index: 2; }  #contribution {   text-align: center;   padding-top: 4rem;   font-size: 1.3125rem; }  @media (max-width: 720px) {   html {     font-size: 12px;   } } @media (max-width: 520px) {   html {     font-size: 10px;   } } @media (max-width: 430px) {   html {     font-size: 8px;   } } @media (max-width: 340px) {   html {     font-size: 6px;   } }

JavaScript

;(function ($, window, document, undefined) {     "use strict";  /************* #1 *************/  var $friends_add = $('#friends-add'); var $friends_counter = $('#friends-counter').text(0);  var $msg_add = $('#msg-add'); var $msg_counter = $('#msg-counter').text(0);  var $info_add = $('#info-add'); var $info_counter = $('#info-counter').text(0);  var audioSupport = "Audio" in window;  if (audioSupport)     var notifyMp3 = new Audio('mp3/notify.mp3');  /************* #2 *************/  var OnNotifyReady = function(callback) {     this.permission = false;     this.support    = "Notification" in window;      var _this = this;      var init = function() {         if(!_this.support) return;          if (Notification.permission === "granted") {             _this.permission = true;             callback();         }          else if (Notification.permission !== 'denied') {             Notification.requestPermission(function (permission) {                                  if(!('permission' in Notification))                     Notification.permission = permission;                  if (permission === "granted") {                     _this.permission = true;                     callback();                 }                 else                     _this.permission = false;             });         }     };      init(); };  /************* #3 *************/  function friendNotification() {     var i = parseInt($friends_counter.toggleClass('rotate').text(), 10);     $friends_counter.text(++i);     var n = new Notification("One more friend!",                                 {icon: 'img/user-icon.png',                                 body: 'Lorem ipsum dolor sit amet, consectetur.'});     n.onshow = function(){         console.log('Friend notification's displayed.');     };     n.onclick = function(){         console.log('Friend notification's clicked.');     };     n.onclose = function(){         console.log('Friend notification's closed.');     };     n.onerror = function(){         console.log('Something goes wrong with friend notification.');     };     if (audioSupport)         notifyMp3.play(); }  function msgNotification() {     var i = parseInt($msg_counter.toggleClass('rotate').text(), 10);     $msg_counter.text(++i);     var n = new Notification("I've message for you!",                                 {icon: 'img/speech-icon.png',                                 body: 'Labore, sapiente, necessitatibus ratione blanditiis quibusdam hic dolorem consequuntur illum laudantium sunt architecto saepe ipsa accusantium.'});     if (audioSupport)         notifyMp3.play(); }  function infoNotification() {     var i = parseInt($info_counter.toggleClass('rotate').text(), 10);     $info_counter.text(++i);     var n = new Notification("Breaking News!",                                 {icon: 'img/info-icon.png',                                 body: 'Labore, sapiente, necessitatibus ratione blanditiis.'});     if (audioSupport)         notifyMp3.play(); }  function notificationsNotSupported() {     alert("Your browser doesn't support notification. Try with Mozilla Firefox or Google Chrome."); }  /************* #4 *************/  $friends_add.on('click', function(){     if(!new OnNotifyReady(friendNotification).support)         notificationsNotSupported(); });  $msg_add.on('click', function(){     if(!new OnNotifyReady(msgNotification).support)         notificationsNotSupported(); });  $info_add.on('click', function(){     if(!new OnNotifyReady(infoNotification).support)         notificationsNotSupported(); });  }(jQuery, this, this.document));

1. Definicja potrzebnych zmiennych

Na początek definiujemy potrzebne zmienne. Kwestię dźwięku w powiadomieniach rozwiążemy tworząc obiekt Audio jeżeli jest wspierany przez przeglądarkę.

2. Dodzwaniamy obiektem?

Tworzenie osobnej klasy tylko po to by wysłać powiadomienie? Na pierwszy rzut oka, pewnie i drugi, przerost formy nad treścią, ale pozwala to na sprytne obejście pewnego problemu. Jaki to problem? Analizując kod w ciemno możemy odpowiedzieć, że chodzi o uprawnienia.

Diabeł tkwi w szczegółach. By wyświetlić powiadomienie nie możemy po prostu zapytać o to użytkownika w momencie wejścia na stronę, to by było za proste. Pytanie o uprawnienia musi wystąpić w wyniku zdarzenia click. Zwróć na to uwagę gdy będziemy patrzyli jak wygląda to w przypadku Gmaila.

Dodatkowo Google Chrome komplikuje sprawę nie implementując właściwości permission.

3. Powiadamiamy

Trzy funkcje friendNotification, msgNotification i infoNotification odpowiadają za powiadomienia odnoście nowej znajomości, nowej wiadomości i jakiejś innej, bliżej nieokreślonej informacji. Nie ukrywam, że Facebook był tu pewną inspiracją.

Wszystkie trzy funkcje inkrementują licznik powiadomień odpowiedniego typu, wyświetlają powiadomienie i odtwarzają dźwięk (R.I.P. Headphone Users).

W konstruktorze powiadomienia przekazujemy tytuł i opcjonalnie w drugim parametrze inne opcje. Pełny opis obiektu powiadomienia znajdziecie na stronie MDN.

Ostatnia funkcja notificationsNotSupported wyświetla komunikat gdy powiadomienia nie są wspierane.

4. Reagujemy na zdarzenia

Teraz możemy przyjrzeć się działaniu instancji klasy OnNotifyReady. Pozwala ona na stworzenie prostego warunku jednocześnie wywołując funkcję przekazaną w parametrze w momencie uzyskania uprawnień lub od razu jeżeli uprawnienia zostały już przyznane. Obiekt nie jest nam do niczego potrzebny po sprawdzeniu warunku więc nie ma sensu przypisywać go do zmiennej i zaśmiecać pamięci.

Praktyczne podejście do tematu w wykonaniu Google

Powiadomienia są już wykorzystywane w środowisku produkcyjnym m. in. w usłudze Gmail. Po wejściu do skrzynki jesteśmy pytani czy chcemy korzystać z powiadomień czatu. Odpowiednie powiadomienia możemy włączyć również dla wiadomości email.

Działa? Działa!

Podsumowanie

Notification API nie jest może znaczącą zmianą jaką wprowadza HTML5 jak IndexedDB, ale jednocześnie jest na tyle proste w użyciu, że z powodzeniem możemy je zaimplementować bez użycia dodatkowej abstrakcji.

Przedstawione rozwiązanie jest sposobem zaprezentowania niektórych możliwości Notification API. Nie ulega wątpliwości, że nie jest to rozwiązanie, które można przenieść do wersji produkcyjnej jakiegoś serwisu, kwestia bezpieczeństwa nie była tutaj kluczowa.

Przydatne linki

komentarzy 5

  • Awatar
    Tomasz Piątek

    5 lutego 2014 15:05

    Całkiem fajne :) kiedyś się przyda na pewno :)

    Odpowiedz
  • Awatar
    Comandeer

    5 lutego 2014 16:40

    >Notification API nie jest może znaczącą zmianą jaką wprowadza HTML5 jak IndexedDB
    IMO więcej się znajdzie zastosowań dla powiadomień niźli dla IndexedDB ;)
    Skoro już chcesz się bawić w ładne opakowanie tego klasami, to polecam zrobić klasę Notifier, która zarządzałaby ładnie wszystkimi powiadomieniami + np. implementowałaby jakieś queue. Następnie poszczególny rodzaj powiadomień byłby odpowiednią klasą potomną z odpowiednimi metodami (albo po prostu wywołanie Notifier przyjmowałoby inne wartości). Wówczas całość sprowadziłaby się do czegoś postaci Notifier.push(notka); i już – ładne zunifikowane rozwiązanie
    co do odpalania dźwięku – czemu nie jest to jako część callbacku, np onshow? Miałoby to wówczas większy sens. ogólnie nie podoba mi się aktualna klasa OnNotifyReady – jej nazwa wskazuje, że to event handler – a raczej tak nie jest (to już raczej NotifyPermissionPrompt). no i w obecnej postaci niespecjalnie ułatwia ;)
    btw repo na gh jest puste

    Odpowiedz
    • Awatar
      Michał Załęcki

      5 lutego 2014 18:42

      > IMO więcej się znajdzie zastosowań dla powiadomień niźli dla IndexedDB ;)
      Nie powiedziałbym. Notification API pozwala… stworzyć powiadomienie, na IndexedDB na upartego można oprzeć logikę biznesową. Potraktuje to jako uszczypliwość w kontekście durnego API IndexedDB.

      > Skoro już chcesz się bawić w ładne opakowanie tego klasami
      Nie przypominam sobie bym coś takiego proponował. Nie jestem zwolennikiem pakowanie każdej nowej funkcjonalności w biblioteki – nie po to powstają API. Moje rozwiązanie rozwiązuje mój, jeden problem – cel osiągnięty. Wszystko można zawsze zrobić inaczej, lepiej lub gorzej.

      > czemu nie jest to jako część callbacku, np onshow
      Bo to zmieni działanie w Chrome? Odpal parę powiadomień, konsola prawdę Ci powie. Dźwięk odpalałby się po zamknięciu powiadomienia jeżeli w kolejce czekałoby kolejne, a nie w momencie wystąpienia jakiejś akcji (np. nowa wiadomość, zaproszenie itd.)

      > ogólnie nie podoba mi się aktualna klasa OnNotifyReady
      Nie jest to najbardziej fortunna nazwa, ale: 1) Zaczyna się wielką literą więc widać, że to nie event handler, a przynajmniej nie tak jak sobie to możemy wyobrażać, jeżeli znamy popularne konwencje. 2) NotifyPermissionPrompt sugeruje, że jest to pytanie, ale zwraca wartość true/false jak prompt()? Nie, bo jest asynchroniczne. Tak czy inaczej nazwa klasy pozostaje, przynajmniej dla mnie, w kategoriach kosmetyki.

      Odpowiedz
      • Awatar
        Comandeer

        5 lutego 2014 19:29

        >Notification API pozwala… stworzyć powiadomienie, na IndexedDB na upartego można oprzeć logikę biznesową.
        z tym, że większość stron w Sieci wciąż raczej potrzebuje tylko czegoś tak nieskomplikowanego, jak powiadomienia – prawdziwych webappów jest na tyle mało, że mało kto wykorzysta IndexedDB. o to mi chodziło

        >Nie jestem zwolennikiem pakowanie każdej nowej funkcjonalności w biblioteki – nie po to powstają API.
        umówmy się: czyste API, w formie, w jakiej serwuje nam je W3C, są po prostu niestrawne w większości zastosowań. dlatego ja wolę mieć ładną klasę na tym, która a) dawałaby mi sensowny sposób na wykorzystanie API b) rozwiązywałaby crossbrowserowe problemy/dawała polyfill

        >Bo to zmieni działanie w Chrome?
        jak dla mnie takie działanie byłoby poprawne – dźwięk w momencie pojawienia się danego powiadomienia. osobiście nie uznaję tego za błąd. ale ok, jeśli chodzi Ci o odpalenie danej rzeczy przy wystąpieniu akcji a nie powiadomienia – wówczas obecne rozwiązanie jest lepsze

        >Zaczyna się wielką literą więc widać, że to nie event handler, a przynajmniej nie tak jak sobie to możemy wyobrażać, jeżeli znamy popularne konwencje.
        owszem, konwencja Crockforda mówi, że to konstruktor, ale w specce DOM pisze również, że handlerem nie musi być funkcja a obiekt ;) wówczas po pobieżnym rzucie okiem na kod, dalej wygląda to na event handler :P

        >NotifyPermissionPrompt sugeruje, że jest to pytanie, ale zwraca wartość true/false jak prompt()? Nie, bo jest asynchroniczne
        faktycznie, NotifyPermissionRequest brzmiałoby lepiej w tym kontekście

        Odpowiedz

Zostaw odpowiedź