about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2018-05-19 14:46:47 +0200
committerGitHub <noreply@github.com>2018-05-19 14:46:47 +0200
commit4b94e9c65efd227c58f8be9b3db6b667d403ed3c (patch)
tree32c3e4f22fa78fb6d74062e192ea8cc7c59f135b /app
parent1951ff41b33b264fd8179998648c3cbbf2834cd5 (diff)
Improve payload format of Web Push API now that it's open (#7521)
> Good lord what is happening in there

Previously the contents of the Web Push API payloads closely resembled the structure of JavaScript's [Notification](https://developer.mozilla.org/en-US/docs/Web/API/Notification). But now that the API is open to non-browser apps, and given that there is no required coupling between contents of the payload and a Notification object, here is how I changed the payload:

```json
{ 
  "access_token": "...",
  "preferred_locale": "en",
  "notification_id": "12345",
  "notification_type": "follow",
  "title": "So and so followed you",
  "body": "This is my bio",
  "icon": "https://example.com/avatar.png"
}
```

The title, body and icon attributes are included as a fallback so you can construct a minimal notification if you cannot perform a network request to the API to get more data.
Diffstat (limited to 'app')
-rw-r--r--app/javascript/mastodon/actions/notifications.js1
-rw-r--r--app/javascript/mastodon/locales/ar.json1
-rw-r--r--app/javascript/mastodon/locales/bg.json1
-rw-r--r--app/javascript/mastodon/locales/ca.json1
-rw-r--r--app/javascript/mastodon/locales/co.json1
-rw-r--r--app/javascript/mastodon/locales/de.json1
-rw-r--r--app/javascript/mastodon/locales/defaultMessages.json6
-rw-r--r--app/javascript/mastodon/locales/el.json1
-rw-r--r--app/javascript/mastodon/locales/en.json1
-rw-r--r--app/javascript/mastodon/locales/eo.json1
-rw-r--r--app/javascript/mastodon/locales/es.json1
-rw-r--r--app/javascript/mastodon/locales/eu.json1
-rw-r--r--app/javascript/mastodon/locales/fa.json1
-rw-r--r--app/javascript/mastodon/locales/fi.json1
-rw-r--r--app/javascript/mastodon/locales/fr.json1
-rw-r--r--app/javascript/mastodon/locales/gl.json1
-rw-r--r--app/javascript/mastodon/locales/he.json1
-rw-r--r--app/javascript/mastodon/locales/hr.json1
-rw-r--r--app/javascript/mastodon/locales/hu.json1
-rw-r--r--app/javascript/mastodon/locales/hy.json1
-rw-r--r--app/javascript/mastodon/locales/id.json1
-rw-r--r--app/javascript/mastodon/locales/io.json1
-rw-r--r--app/javascript/mastodon/locales/it.json1
-rw-r--r--app/javascript/mastodon/locales/ja.json1
-rw-r--r--app/javascript/mastodon/locales/ko.json1
-rw-r--r--app/javascript/mastodon/locales/nl.json1
-rw-r--r--app/javascript/mastodon/locales/no.json1
-rw-r--r--app/javascript/mastodon/locales/oc.json1
-rw-r--r--app/javascript/mastodon/locales/pl.json1
-rw-r--r--app/javascript/mastodon/locales/pt-BR.json1
-rw-r--r--app/javascript/mastodon/locales/pt.json1
-rw-r--r--app/javascript/mastodon/locales/ru.json1
-rw-r--r--app/javascript/mastodon/locales/sk.json1
-rw-r--r--app/javascript/mastodon/locales/sl.json1
-rw-r--r--app/javascript/mastodon/locales/sr-Latn.json1
-rw-r--r--app/javascript/mastodon/locales/sr.json1
-rw-r--r--app/javascript/mastodon/locales/sv.json1
-rw-r--r--app/javascript/mastodon/locales/te.json1
-rw-r--r--app/javascript/mastodon/locales/th.json1
-rw-r--r--app/javascript/mastodon/locales/tr.json1
-rw-r--r--app/javascript/mastodon/locales/uk.json1
-rw-r--r--app/javascript/mastodon/locales/zh-CN.json1
-rw-r--r--app/javascript/mastodon/locales/zh-HK.json1
-rw-r--r--app/javascript/mastodon/locales/zh-TW.json1
-rw-r--r--app/javascript/mastodon/service_worker/web_push_locales.js30
-rw-r--r--app/javascript/mastodon/service_worker/web_push_notifications.js178
-rw-r--r--app/models/web/push_subscription.rb21
-rw-r--r--app/serializers/web/notification_serializer.rb163
48 files changed, 212 insertions, 229 deletions
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index 641ad0e14..3f95f6667 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -22,6 +22,7 @@ export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
 
 defineMessages({
   mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
+  group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
 });
 
 const fetchRelatedRelationships = (dispatch, notifications) => {
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 1199c9535..bb4189514 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "الترقيّات:",
   "notifications.column_settings.show": "إعرِضها في عمود",
   "notifications.column_settings.sound": "أصدر صوتا",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "تم",
   "onboarding.next": "التالي",
   "onboarding.page_five.public_timelines": "تُعرَض في الخيط الزمني المحلي المشاركات العامة المحررة من طرف جميع المسجلين في {domain}. أما في الخيط الزمني الموحد ، فإنه يتم عرض جميع المشاركات العامة المنشورة من طرف جميع الأشخاص المتابَعين من طرف أعضاء {domain}. هذه هي الخيوط الزمنية العامة، وهي طريقة رائعة للتعرف أشخاص جدد.",
diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json
index dd747ab3b..aeb4f09da 100644
--- a/app/javascript/mastodon/locales/bg.json
+++ b/app/javascript/mastodon/locales/bg.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Споделяния:",
   "notifications.column_settings.show": "Покажи в колона",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json
index 68fc4a44c..4bf8ad1c6 100644
--- a/app/javascript/mastodon/locales/ca.json
+++ b/app/javascript/mastodon/locales/ca.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Impulsos:",
   "notifications.column_settings.show": "Mostrar en la columna",
   "notifications.column_settings.sound": "Reproduïr so",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Fet",
   "onboarding.next": "Següent",
   "onboarding.page_five.public_timelines": "La línia de temps local mostra missatges públics de tothom de {domain}. La línia de temps federada mostra els missatges públics de tothom que la gent de {domain} segueix. Aquests són les línies de temps Públiques, una bona manera de descobrir noves persones.",
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index 9d8d950a8..8d924f973 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Spartere:",
   "notifications.column_settings.show": "Mustrà indè a colonna",
   "notifications.column_settings.sound": "Sunà",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Fatta",
   "onboarding.next": "Siguente",
   "onboarding.page_five.public_timelines": "A linea pubblica lucale mostra statuti pubblichi da tuttu u mondu nant'à {domain}. A linea pubblica glubale mostra ancu quelli di a ghjente seguitata da l'utilizatori di {domain}. Quesse sò una bona manera d'incuntrà nove parsone.",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index fe5bc4443..84b815d50 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Geteilte Beiträge:",
   "notifications.column_settings.show": "In der Spalte anzeigen",
   "notifications.column_settings.sound": "Ton abspielen",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Fertig",
   "onboarding.next": "Weiter",
   "onboarding.page_five.public_timelines": "Die lokale Zeitleiste zeigt alle Beiträge von Leuten, die auch auf {domain} sind. Das gesamte bekannte Netz zeigt Beiträge von allen, denen von Leuten auf {domain} gefolgt wird. Zusammen sind sie die öffentlichen Zeitleisten, ein guter Weg, um neue Leute zu finden.",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 49b287928..e2f3ac6dd 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -17,6 +17,10 @@
       {
         "defaultMessage": "{name} mentioned you",
         "id": "notification.mention"
+      },
+      {
+        "defaultMessage": "{count} notifications",
+        "id": "notifications.group"
       }
     ],
     "path": "app/javascript/mastodon/actions/notifications.json"
@@ -870,7 +874,7 @@
         "id": "compose_form.hashtag_warning"
       },
       {
-        "defaultMessage": "This toot will only be visible to all the mentioned users.",
+        "defaultMessage": "This toot will only be sent to all the mentioned users. However, the operators of your instance and any receiving instances may see this message.",
         "id": "compose_form.direct_message_warning"
       }
     ],
diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json
index d1421be76..6e8aa0e24 100644
--- a/app/javascript/mastodon/locales/el.json
+++ b/app/javascript/mastodon/locales/el.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 478caaaf8..11589d797 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json
index d5ddc23d4..479fc6c6e 100644
--- a/app/javascript/mastodon/locales/eo.json
+++ b/app/javascript/mastodon/locales/eo.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Diskonigoj:",
   "notifications.column_settings.show": "Montri en kolumno",
   "notifications.column_settings.sound": "Eligi sonon",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Farita",
   "onboarding.next": "Sekva",
   "onboarding.page_five.public_timelines": "La loka tempolinio montras publikajn mesaĝojn de ĉiuj en {domain}. La fratara tempolinio montras publikajn mesaĝojn de ĉiuj, kiuj estas sekvataj de homoj en {domain}. Tio estas la publikaj tempolinioj, kio estas bona maniero por malkovri novajn homojn.",
diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json
index 8aac2d77d..233e46561 100644
--- a/app/javascript/mastodon/locales/es.json
+++ b/app/javascript/mastodon/locales/es.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Retoots:",
   "notifications.column_settings.show": "Mostrar en columna",
   "notifications.column_settings.sound": "Reproducir sonido",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Listo",
   "onboarding.next": "Siguiente",
   "onboarding.page_five.public_timelines": "La línea de tiempo local muestra toots públicos de todos en {domain}. La línea de tiempo federada muestra toots públicos de cualquiera a quien la gente de {domain} siga. Estas son las líneas de tiempo públicas, una buena forma de conocer gente nueva.",
diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json
index e927547e3..773d69287 100644
--- a/app/javascript/mastodon/locales/eu.json
+++ b/app/javascript/mastodon/locales/eu.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index e19b1cd85..957046c27 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "بازبوق‌ها:",
   "notifications.column_settings.show": "نمایش در ستون",
   "notifications.column_settings.sound": "پخش صدا",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "پایان",
   "onboarding.next": "بعدی",
   "onboarding.page_five.public_timelines": "نوشته‌های محلی یعنی نوشته‌های همهٔ کاربران {domain}. نوشته‌های همه‌جا یعنی نوشته‌های همهٔ کسانی که کاربران {domain} آن‌ها را پی می‌گیرند. این فهرست‌های عمومی راه خوبی برای یافتن کاربران تازه هستند.",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index cfe9c4f33..55b9da70a 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Buustit:",
   "notifications.column_settings.show": "Näytä sarakkeessa",
   "notifications.column_settings.sound": "Äänimerkki",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Valmis",
   "onboarding.next": "Seuraava",
   "onboarding.page_five.public_timelines": "Paikallisella aikajanalla näytetään instanssin {domain} kaikkien käyttäjien julkiset julkaisut. Yleisellä aikajanalla näytetään kaikkien instanssin {domain} käyttäjien seuraamien käyttäjien julkiset julkaisut. Nämä julkiset aikajanat ovat loistavia paikkoja löytää uusia ihmisiä.",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index c4a41583c..44464cbc3 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Partages :",
   "notifications.column_settings.show": "Afficher dans la colonne",
   "notifications.column_settings.sound": "Émettre un son",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Effectué",
   "onboarding.next": "Suivant",
   "onboarding.page_five.public_timelines": "Le fil public global affiche les messages de toutes les personnes suivies par les membres de {domain}. Le fil public local est identique, mais se limite aux membres de {domain}.",
diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json
index 1eaaebc53..aa7ba0d1e 100644
--- a/app/javascript/mastodon/locales/gl.json
+++ b/app/javascript/mastodon/locales/gl.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Promocións:",
   "notifications.column_settings.show": "Mostrar en columna",
   "notifications.column_settings.sound": "Reproducir son",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Feito",
   "onboarding.next": "Seguinte",
   "onboarding.page_five.public_timelines": "A liña de tempo local mostra as publicacións públicas de todos en {domain}. A liña de tempo federada mostra as publicacións públicas de todos os que as persoas en {domain} seguen. Estas son as Liñas de tempo públicas, unha boa forma de descubrir novas persoas.",
diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json
index a4bcba8ff..324ea725e 100644
--- a/app/javascript/mastodon/locales/he.json
+++ b/app/javascript/mastodon/locales/he.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "הדהודים:",
   "notifications.column_settings.show": "הצגה בטור",
   "notifications.column_settings.sound": "שמע מופעל",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "יציאה",
   "onboarding.next": "הלאה",
   "onboarding.page_five.public_timelines": "ציר הזמן המקומי מראה הודעות פומביות מכל באי קהילת {domain}. ציר הזמן העולמי מראה הודעות פומביות מאת כי מי שבאי קהילת {domain} עוקבים אחריו. אלו צירי הזמן הפומביים, דרך נהדרת לגלות אנשים חדשים.",
diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json
index bd1dfac4c..0216d0837 100644
--- a/app/javascript/mastodon/locales/hr.json
+++ b/app/javascript/mastodon/locales/hr.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Boostovi:",
   "notifications.column_settings.show": "Prikaži u stupcu",
   "notifications.column_settings.sound": "Sviraj zvuk",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Učinjeno",
   "onboarding.next": "Sljedeće",
   "onboarding.page_five.public_timelines": "Lokalni timeline prikazuje javne postove sviju od svakog na {domain}. Federalni timeline prikazuje javne postove svakog koga ljudi na {domain} slijede. To su Javni Timelineovi, sjajan način za otkriti nove ljude.",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 5059a45d4..795c9b7cd 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Rebloggolások:",
   "notifications.column_settings.show": "Oszlopban mutatás",
   "notifications.column_settings.sound": "Hang lejátszása",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Befejezve",
   "onboarding.next": "Következő",
   "onboarding.page_five.public_timelines": "A helyi idővonal mindenkinek a publikus posztját mutatja a(z) {domain}-n. A federált idővonal mindenki publikus posztját mutatja akit {domain} felhasználói követnek. Ezek a publikus idővonalak, nagyszerű mód új emberek megismerésére.",
diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json
index 7fe723892..2c7496d89 100644
--- a/app/javascript/mastodon/locales/hy.json
+++ b/app/javascript/mastodon/locales/hy.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Տարածածներից՝",
   "notifications.column_settings.show": "Ցուցադրել սյունում",
   "notifications.column_settings.sound": "Ձայն հանել",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Պատրաստ է",
   "onboarding.next": "Հաջորդ",
   "onboarding.page_five.public_timelines": "Տեղական հոսքը ցույց է տալիս {domain} տիրույթից բոլորի հրապարակային թթերը։ Դաշնային հոսքը ցույց է տալիս հրապարակային թթերը բոլորից, ում {domain} տիրույթի մարդիկ հետեւում են։ Սրանք Հրապարակային հոսքերն են՝ նոր մարդկանց բացահայտելու հրաշալի միջոց։",
diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json
index 07b26a0a5..2fcd5c482 100644
--- a/app/javascript/mastodon/locales/id.json
+++ b/app/javascript/mastodon/locales/id.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Boost:",
   "notifications.column_settings.show": "Tampilkan dalam kolom",
   "notifications.column_settings.sound": "Mainkan suara",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Selesei",
   "onboarding.next": "Selanjutnya",
   "onboarding.page_five.public_timelines": "Linimasa lokal menampilkan semua postingan publik dari semua orang di {domain}. Linimasa gabungan menampilkan postingan publik dari semua orang yang diikuti oleh {domain}. Ini semua adalah Linimasa Publik, cara terbaik untuk bertemu orang lain.",
diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json
index a8ab72339..9d429bda8 100644
--- a/app/javascript/mastodon/locales/io.json
+++ b/app/javascript/mastodon/locales/io.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Repeti:",
   "notifications.column_settings.show": "Montrar en kolumno",
   "notifications.column_settings.sound": "Plear sono",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json
index 8bd652864..64baf5440 100644
--- a/app/javascript/mastodon/locales/it.json
+++ b/app/javascript/mastodon/locales/it.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Post condivisi:",
   "notifications.column_settings.show": "Mostra in colonna",
   "notifications.column_settings.sound": "Riproduci suono",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Fatto",
   "onboarding.next": "Prossimo",
   "onboarding.page_five.public_timelines": "La timeline locale mostra i post pubblici di tutti gli utenti di {domain}. La timeline federata mostra i post pubblici di tutti gli utenti seguiti da quelli di {domain}. Queste sono le timeline pubbliche, che vi danno grandi possibilità di scoprire nuovi utenti.",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 8be04cf73..95f8438d5 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "ブースト:",
   "notifications.column_settings.show": "カラムに表示",
   "notifications.column_settings.sound": "通知音を再生",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "完了",
   "onboarding.next": "次へ",
   "onboarding.page_five.public_timelines": "連合タイムラインでは{domain}の人がフォローしているMastodon全体での公開投稿を表示します。同じくローカルタイムラインでは{domain}のみの公開投稿を表示します。",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index fc13a5b5b..bb6f3558c 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "부스트:",
   "notifications.column_settings.show": "컬럼에 표시",
   "notifications.column_settings.sound": "효과음 재생",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "완료",
   "onboarding.next": "다음",
   "onboarding.page_five.public_timelines": "연합 타임라인에서는 {domain}의 사람들이 팔로우 중인 Mastodon 전체 인스턴스의 공개 포스트를 표시합니다. 로컬 타임라인에서는 {domain} 만의 공개 포스트를 표시합니다.",
diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json
index 047583ce2..63a452377 100644
--- a/app/javascript/mastodon/locales/nl.json
+++ b/app/javascript/mastodon/locales/nl.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "In kolom tonen",
   "notifications.column_settings.sound": "Geluid afspelen",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Klaar",
   "onboarding.next": "Volgende",
   "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodonservers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te leren kennen.",
diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json
index 05dae0630..1496afb83 100644
--- a/app/javascript/mastodon/locales/no.json
+++ b/app/javascript/mastodon/locales/no.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Fremhevet:",
   "notifications.column_settings.show": "Vis i kolonne",
   "notifications.column_settings.sound": "Spill lyd",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Ferdig",
   "onboarding.next": "Neste",
   "onboarding.page_five.public_timelines": "Den lokale tidslinjen viser offentlige poster fra alle på {domain}. Felles tidslinje viser offentlige poster fra alle som brukere på {domain} følger. Dette er de offentlige tidslinjene, et fint sted å oppdage nye brukere.",
diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json
index 615af998f..3ae29129f 100644
--- a/app/javascript/mastodon/locales/oc.json
+++ b/app/javascript/mastodon/locales/oc.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Partatges :",
   "notifications.column_settings.show": "Mostrar dins la colomna",
   "notifications.column_settings.sound": "Emetre un son",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Sortir",
   "onboarding.next": "Seguent",
   "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json
index 0df36f969..be5693bc9 100644
--- a/app/javascript/mastodon/locales/pl.json
+++ b/app/javascript/mastodon/locales/pl.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Podbicia:",
   "notifications.column_settings.show": "Pokaż w kolumnie",
   "notifications.column_settings.sound": "Odtwarzaj dźwięk",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Gotowe",
   "onboarding.next": "Dalej",
   "onboarding.page_five.public_timelines": "Lokalna oś czasu zawiera wszystkie publiczne wpisy z {domain}. Globalna oś czasu wyświetla publiczne wpisy śledzonych przez członków {domain}. Są to publiczne osie czasu – najlepszy sposób na poznanie nowych osób.",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 2360c41e7..26ae5ef30 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Compartilhamento:",
   "notifications.column_settings.show": "Mostrar nas colunas",
   "notifications.column_settings.sound": "Reproduzir som",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Pronto",
   "onboarding.next": "Próximo",
   "onboarding.page_five.public_timelines": "A timeline local mostra postagens públicas de todos os usuários no {domain}. A timeline federada mostra todas as postagens de todas as pessoas que pessoas no {domain} seguem. Estas são as timelines públicas, uma ótima maneira de conhecer novas pessoas.",
diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json
index 3ac92dd57..02ffc825c 100644
--- a/app/javascript/mastodon/locales/pt.json
+++ b/app/javascript/mastodon/locales/pt.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Partilhas:",
   "notifications.column_settings.show": "Mostrar nas colunas",
   "notifications.column_settings.sound": "Reproduzir som",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Pronto",
   "onboarding.next": "Próximo",
   "onboarding.page_five.public_timelines": "A timeline local mostra as publicações de todos os utilizadores em {domain}. A timeline global mostra as publicações de todas as pessoas que pessoas em {domain} seguem. Estas são as timelines públicas, uma óptima forma de conhecer novas pessoas.",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 5cf983b83..ca8574578 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Продвижения:",
   "notifications.column_settings.show": "Показывать в колонке",
   "notifications.column_settings.sound": "Проигрывать звук",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Готово",
   "onboarding.next": "Далее",
   "onboarding.page_five.public_timelines": "Локальная лента показывает публичные посты всех пользователей {domain}. Глобальная лента показывает публичные посты всех людей, на которых подписаны пользователи {domain}. Это - публичные ленты, отличный способ найти новые знакомства.",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index 4f7969bd3..cf9ead5e7 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Boosty:",
   "notifications.column_settings.show": "Zobraziť v stĺpci",
   "notifications.column_settings.sound": "Prehrať zvuk",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Koniec",
   "onboarding.next": "Ďalej",
   "onboarding.page_five.public_timelines": "Lokálna časová os zobrazuje verejné správy od všetkých na {domain}. Federovaná časová os zobrazuje verejné správy od všetkých tých, čo následujú užívatrľov {domain} z iných serverov. Tieto sú takzvané Verejné Časové Osi, výborná možnosť ako nájsť a spoznať nových ľudí.",
diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json
index 69b416258..dbad35c34 100644
--- a/app/javascript/mastodon/locales/sl.json
+++ b/app/javascript/mastodon/locales/sl.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json
index 490b3a51a..bc54a0865 100644
--- a/app/javascript/mastodon/locales/sr-Latn.json
+++ b/app/javascript/mastodon/locales/sr-Latn.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Podrški:",
   "notifications.column_settings.show": "Prikaži u koloni",
   "notifications.column_settings.sound": "Puštaj zvuk",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Gotovo",
   "onboarding.next": "Sledeće",
   "onboarding.page_five.public_timelines": "Lokalna lajna prikazuje sve javne statuse od svih na domenu {domain}. Federisana lajna prikazuje javne statuse od svih ljudi koje prate korisnici sa domena {domain}. Ovo su javne lajne, sjajan način da otkrijete nove ljude.",
diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json
index b331f9f48..5cdeb24fc 100644
--- a/app/javascript/mastodon/locales/sr.json
+++ b/app/javascript/mastodon/locales/sr.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Подршки:",
   "notifications.column_settings.show": "Прикажи у колони",
   "notifications.column_settings.sound": "Пуштај звук",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Готово",
   "onboarding.next": "Следеће",
   "onboarding.page_five.public_timelines": "Локална лајна приказује све јавне статусе од свих на домену {domain}. Федерисана лајна приказује јавне статусе од свих људи које прате корисници са домена {domain}. Ово су јавне лајне, сјајан начин да откријете нове људе.",
diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json
index 36b200764..579921046 100644
--- a/app/javascript/mastodon/locales/sv.json
+++ b/app/javascript/mastodon/locales/sv.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Knuffar:",
   "notifications.column_settings.show": "Visa i kolumnen",
   "notifications.column_settings.sound": "Spela upp ljud",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Klart",
   "onboarding.next": "Nästa",
   "onboarding.page_five.public_timelines": "Den lokala tidslinjen visar offentliga inlägg från alla på {domain}. Den förenade tidslinjen visar offentliga inlägg från alla personer på {domain} som följer. Dom här offentliga tidslinjerna är ett bra sätt att upptäcka nya människor.",
diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json
index 038ac6abb..55415da81 100644
--- a/app/javascript/mastodon/locales/te.json
+++ b/app/javascript/mastodon/locales/te.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json
index 8d24395af..243941caf 100644
--- a/app/javascript/mastodon/locales/th.json
+++ b/app/javascript/mastodon/locales/th.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Boosts:",
   "notifications.column_settings.show": "Show in column",
   "notifications.column_settings.sound": "Play sound",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Done",
   "onboarding.next": "Next",
   "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index a377e7b74..c6d27b609 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Boost’lar:",
   "notifications.column_settings.show": "Bildirimlerde göster",
   "notifications.column_settings.sound": "Ses çal",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Tamam",
   "onboarding.next": "Sıradaki",
   "onboarding.page_five.public_timelines": "Yerel zaman tüneli, bu sunucudaki herkesten gelen gönderileri gösterir.Federe zaman tüneli, kullanıcıların diğer sunuculardan takip ettiği kişilerin herkese açık gönderilerini gösterir. Bunlar herkese açık zaman tünelleridir ve yeni insanlarla tanışmak  için harika yerlerdir. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new ",
diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json
index 613d1a00d..2e174e186 100644
--- a/app/javascript/mastodon/locales/uk.json
+++ b/app/javascript/mastodon/locales/uk.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "Передмухи:",
   "notifications.column_settings.show": "Показати в колонці",
   "notifications.column_settings.sound": "Відтворювати звук",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "Готово",
   "onboarding.next": "Далі",
   "onboarding.page_five.public_timelines": "Локальна стрічка показує публічні пости усіх користувачів {domain}. Глобальна стрічка показує публічні пости усіх людей, на яких підписані користувачі {domain}. Це публичні стрічки, відмінний спосіб знайти нових людей.",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index 073dbe6cb..254d078eb 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "当有人转嘟了你的嘟文时:",
   "notifications.column_settings.show": "在通知栏显示",
   "notifications.column_settings.sound": "播放音效",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "出发!",
   "onboarding.next": "下一步",
   "onboarding.page_five.public_timelines": "“本站时间轴”显示的是由本站({domain})用户发布的所有公开嘟文。“跨站公共时间轴”显示的的是由本站用户关注对象所发布的所有公开嘟文。这些就是寻人好去处的公共时间轴啦。",
diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json
index 9334b5d20..9fb8aa2f9 100644
--- a/app/javascript/mastodon/locales/zh-HK.json
+++ b/app/javascript/mastodon/locales/zh-HK.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "轉推你的文章:",
   "notifications.column_settings.show": "在通知欄顯示",
   "notifications.column_settings.sound": "播放音效",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "開始使用",
   "onboarding.next": "繼續",
   "onboarding.page_five.public_timelines": "「本站時間軸」顯示在 {domain} 各用戶的公開文章。「跨站時間軸」顯示在 {domain} 各人關注的所有用戶(包括其他服務站)的公開文章。這些都是「公共時間軸」,是認識新朋友的好地方。",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 0f4d04947..ae917a5ac 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -186,6 +186,7 @@
   "notifications.column_settings.reblog": "轉推:",
   "notifications.column_settings.show": "顯示在欄位中",
   "notifications.column_settings.sound": "播放音效",
+  "notifications.group": "{count} notifications",
   "onboarding.done": "完成",
   "onboarding.next": "下一步",
   "onboarding.page_five.public_timelines": "本地時間軸顯示 {domain} 上所有人的公開貼文。聯盟時間軸顯示 {domain} 上所有人關注的公開貼文。這就是公開時間軸,發現新朋友的好地方。",
diff --git a/app/javascript/mastodon/service_worker/web_push_locales.js b/app/javascript/mastodon/service_worker/web_push_locales.js
new file mode 100644
index 000000000..ce96ae297
--- /dev/null
+++ b/app/javascript/mastodon/service_worker/web_push_locales.js
@@ -0,0 +1,30 @@
+/* @preval */
+
+const fs   = require('fs');
+const path = require('path');
+
+const filtered  = {};
+const filenames = fs.readdirSync(path.resolve(__dirname, '../locales'));
+
+filenames.forEach(filename => {
+  if (!filename.match(/\.json$/) || filename.match(/defaultMessages|whitelist/)) return;
+
+  const content = fs.readFileSync(path.resolve(__dirname, `../locales/${filename}`), 'utf-8');
+  const full    = JSON.parse(content);
+  const locale  = filename.split('.')[0];
+
+  filtered[locale] = {
+    'notification.favourite': full['notification.favourite'] || '',
+    'notification.follow': full['notification.follow'] || '',
+    'notification.mention': full['notification.mention'] || '',
+    'notification.reblog': full['notification.reblog'] || '',
+
+    'status.show_more': full['status.show_more'] || '',
+    'status.reblog': full['status.reblog'] || '',
+    'status.favourite': full['status.favourite'] || '',
+
+    'notifications.group': full['notifications.group'] || '',
+  };
+});
+
+module.exports = JSON.parse(JSON.stringify(filtered));
diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js
index f63cff335..16eaefcbb 100644
--- a/app/javascript/mastodon/service_worker/web_push_notifications.js
+++ b/app/javascript/mastodon/service_worker/web_push_notifications.js
@@ -1,36 +1,32 @@
+import IntlMessageFormat from 'intl-messageformat';
+import locales from './web_push_locales';
+
 const MAX_NOTIFICATIONS = 5;
 const GROUP_TAG = 'tag';
 
-// Avoid loading intl-messageformat and dealing with locales in the ServiceWorker
-const formatGroupTitle = (message, count) => message.replace('%{count}', count);
-
 const notify = options =>
   self.registration.getNotifications().then(notifications => {
-    if (notifications.length === MAX_NOTIFICATIONS) {
-      // Reached the maximum number of notifications, proceed with grouping
+    if (notifications.length >= MAX_NOTIFICATIONS) { // Reached the maximum number of notifications, proceed with grouping
       const group = {
-        title: formatGroupTitle(options.data.message, notifications.length + 1),
-        body: notifications
-          .sort((n1, n2) => n1.timestamp < n2.timestamp)
-          .map(notification => notification.title).join('\n'),
+        title: formatMessage('notifications.group', options.data.preferred_locale, { count: notifications.length + 1 }),
+        body: notifications.sort((n1, n2) => n1.timestamp < n2.timestamp).map(notification => notification.title).join('\n'),
         badge: '/badge.png',
         icon: '/android-chrome-192x192.png',
         tag: GROUP_TAG,
         data: {
           url: (new URL('/web/notifications', self.location)).href,
           count: notifications.length + 1,
-          message: options.data.message,
+          preferred_locale: options.data.preferred_locale,
         },
       };
 
       notifications.forEach(notification => notification.close());
 
       return self.registration.showNotification(group.title, group);
-    } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) {
-      // Already grouped, proceed with appending the notification to the group
-      const group = cloneNotification(notifications[0]);
+    } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) { // Already grouped, proceed with appending the notification to the group
+      const group = { ...notifications[0] };
 
-      group.title = formatGroupTitle(group.data.message, group.data.count + 1);
+      group.title = formatMessage('notifications.group', options.data.preferred_locale, { count: group.data.count + 1 });
       group.body  = `${options.title}\n${group.body}`;
       group.data  = { ...group.data, count: group.data.count + 1 };
 
@@ -40,57 +36,87 @@ const notify = options =>
     return self.registration.showNotification(options.title, options);
   });
 
-const handlePush = (event) => {
-  const options = event.data.json();
-
-  options.body      = options.data.nsfw || options.data.content;
-  options.dir       = options.data.dir;
-  options.image     = options.image || undefined; // Null results in a network request (404)
-  options.timestamp = options.timestamp && new Date(options.timestamp);
-
-  const expandAction = options.data.actions.find(action => action.todo === 'expand');
+const fetchFromApi = (path, method, accessToken) => {
+  const url = (new URL(path, self.location)).href;
 
-  if (expandAction) {
-    options.actions          = [expandAction];
-    options.hiddenActions    = options.data.actions.filter(action => action !== expandAction);
-    options.data.hiddenImage = options.image;
-    options.image            = undefined;
-  } else {
-    options.actions = options.data.actions;
-  }
+  return fetch(url, {
+    headers: {
+      'Authorization': `Bearer ${accessToken}`,
+      'Content-Type': 'application/json',
+    },
 
-  event.waitUntil(notify(options));
+    method: method,
+    credentials: 'include',
+  }).then(res => {
+    if (res.ok) {
+      return res;
+    } else {
+      throw new Error(res.status);
+    }
+  }).then(res => res.json());
 };
 
-const cloneNotification = (notification) => {
-  const clone = {  };
+const formatMessage = (messageId, locale, values = {}) =>
+  (new IntlMessageFormat(locales[locale][messageId], locale)).format(values);
 
-  for(var k in notification) {
-    clone[k] = notification[k];
-  }
+const handlePush = (event) => {
+  const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json();
+
+  // Placeholder until more information can be loaded
+  event.waitUntil(
+    notify({
+      title,
+      body,
+      icon,
+      tag: notification_id,
+      timestamp: new Date(),
+      badge: '/badge.png',
+      data: { access_token, preferred_locale, url: '/web/notifications' },
+    }).then(() => fetchFromApi(`/api/v1/notifications/${notification_id}`, 'get', access_token)).then(notification => {
+      const options = {};
+
+      options.title     = formatMessage(`notification.${notification.type}`, preferred_locale, { name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
+      options.body      = notification.status && notification.status.content;
+      options.icon      = notification.account.avatar_static;
+      options.timestamp = notification.created_at && new Date(notification.created_at);
+      options.tag       = notification.id;
+      options.badge     = '/badge.png';
+      options.image     = notification.status && notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url || undefined;
+      options.data      = { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/web/statuses/${notification.status.id}` : `/web/accounts/${notification.account.id}` };
+
+      if (notification.status && notification.status.sensitive) {
+        options.data.hiddenBody  = notification.status.content;
+        options.data.hiddenImage = notification.status.media_attachments.length > 0 && notification.status.media_attachments[0].preview_url;
+
+        options.body    = undefined;
+        options.image   = undefined;
+        options.actions = [actionExpand(preferred_locale)];
+      } else if (notification.status) {
+        options.actions = [actionReblog(preferred_locale), actionFavourite(preferred_locale)];
+      }
 
-  return clone;
+      return notify(options);
+    })
+  );
 };
 
-const expandNotification = (notification) => {
-  const nextNotification = cloneNotification(notification);
+const actionExpand = preferred_locale => ({
+  action: 'expand',
+  icon: '/web-push-icon_expand.png',
+  title: formatMessage('status.show_more', preferred_locale),
+});
 
-  nextNotification.body    = notification.data.content;
-  nextNotification.image   = notification.data.hiddenImage;
-  nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand');
+const actionReblog = preferred_locale => ({
+  action: 'reblog',
+  icon: '/web-push-icon_reblog.png',
+  title: formatMessage('status.reblog', preferred_locale),
+});
 
-  return self.registration.showNotification(nextNotification.title, nextNotification);
-};
-
-const makeRequest = (notification, action) =>
-  fetch(action.action, {
-    headers: {
-      'Authorization': `Bearer ${notification.data.access_token}`,
-      'Content-Type': 'application/json',
-    },
-    method: action.method,
-    credentials: 'include',
-  });
+const actionFavourite = preferred_locale => ({
+  action: 'favourite',
+  icon: '/web-push-icon_favourite.png',
+  title: formatMessage('status.favourite', preferred_locale),
+});
 
 const findBestClient = clients => {
   const focusedClient = clients.find(client => client.focused);
@@ -99,6 +125,24 @@ const findBestClient = clients => {
   return focusedClient || visibleClient || clients[0];
 };
 
+const expandNotification = notification => {
+  const newNotification = { ...notification };
+
+  newNotification.body    = newNotification.data.hiddenBody;
+  newNotification.image   = newNotification.data.hiddenImage;
+  newNotification.actions = [actionReblog(notification.data.preferred_locale), actionFavourite(notification.data.preferred_locale)];
+
+  return self.registration.showNotification(newNotification.title, newNotification);
+};
+
+const removeActionFromNotification = (notification, action) => {
+  const newNotification = { ...notification };
+
+  newNotification.actions = newNotification.actions.filter(item => item.action !== action);
+
+  return self.registration.showNotification(newNotification.title, newNotification);
+};
+
 const openUrl = url =>
   self.clients.matchAll({ type: 'window' }).then(clientList => {
     if (clientList.length !== 0) {
@@ -124,27 +168,19 @@ const openUrl = url =>
     return self.clients.openWindow(url);
   });
 
-const removeActionFromNotification = (notification, action) => {
-  const actions          = notification.actions.filter(act => act.action !== action.action);
-  const nextNotification = cloneNotification(notification);
-
-  nextNotification.actions = actions;
-
-  return self.registration.showNotification(nextNotification.title, nextNotification);
-};
-
 const handleNotificationClick = (event) => {
   const reactToNotificationClick = new Promise((resolve, reject) => {
     if (event.action) {
-      const action = event.notification.data.actions.find(({ action }) => action === event.action);
-
-      if (action.todo === 'expand') {
+      if (event.action === 'expand') {
         resolve(expandNotification(event.notification));
-      } else if (action.todo === 'request') {
-        resolve(makeRequest(event.notification, action)
-          .then(() => removeActionFromNotification(event.notification, action)));
+      } else if (event.action === 'reblog') {
+        const { data } = event.notification;
+        resolve(fetchFromApi(`/api/v1/statuses/${data.id}/reblog`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'reblog')));
+      } else if (event.action === 'favourite') {
+        const { data } = event.notification;
+        resolve(fetchFromApi(`/api/v1/statuses/${data.id}/favourite`, 'post', data.access_token).then(() => removeActionFromNotification(event.notification, 'favourite')));
       } else {
-        reject(`Unknown action: ${action.todo}`);
+        reject(`Unknown action: ${event.action}`);
       }
     } else {
       event.notification.close();
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index df549c6d3..7da3428fe 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -21,8 +21,8 @@ class Web::PushSubscription < ApplicationRecord
   has_one :session_activation
 
   def push(notification)
-    I18n.with_locale(associated_user.locale || I18n.default_locale) do
-      push_payload(message_from(notification), 48.hours.seconds)
+    I18n.with_locale(associated_user&.locale || I18n.default_locale) do
+      push_payload(payload_for_notification(notification), 48.hours.seconds)
     end
   end
 
@@ -46,16 +46,13 @@ class Web::PushSubscription < ApplicationRecord
     @associated_access_token = if access_token_id.nil?
                                  find_or_create_access_token.token
                                else
-                                 access_token
+                                 access_token.token
                                end
   end
 
   private
 
   def push_payload(message, ttl = 5.minutes.seconds)
-    # TODO: Make sure that the payload does not
-    # exceed 4KB - Webpush::PayloadTooLarge
-
     Webpush.payload_send(
       message: Oj.dump(message),
       endpoint: endpoint,
@@ -70,16 +67,20 @@ class Web::PushSubscription < ApplicationRecord
     )
   end
 
-  def message_from(notification)
-    serializable_resource = ActiveModelSerializers::SerializableResource.new(notification, serializer: Web::NotificationSerializer, scope: self, scope_name: :current_push_subscription)
-    serializable_resource.as_json
+  def payload_for_notification(notification)
+    ActiveModelSerializers::SerializableResource.new(
+      notification,
+      serializer: Web::NotificationSerializer,
+      scope: self,
+      scope_name: :current_push_subscription
+    ).as_json
   end
 
   def find_or_create_access_token
     Doorkeeper::AccessToken.find_or_create_for(
       Doorkeeper::Application.find_by(superapp: true),
       session_activation.user_id,
-      Doorkeeper::OAuth::Scopes.from_string('read write follow'),
+      Doorkeeper::OAuth::Scopes.from_string('read write follow push'),
       Doorkeeper.configuration.access_token_expires_in,
       Doorkeeper.configuration.refresh_token_enabled?
     )
diff --git a/app/serializers/web/notification_serializer.rb b/app/serializers/web/notification_serializer.rb
index 31c703832..f3c4ffc47 100644
--- a/app/serializers/web/notification_serializer.rb
+++ b/app/serializers/web/notification_serializer.rb
@@ -2,168 +2,37 @@
 
 class Web::NotificationSerializer < ActiveModel::Serializer
   include RoutingHelper
-  include StreamEntriesHelper
+  include ActionView::Helpers::TextHelper
+  include ActionView::Helpers::SanitizeHelper
 
-  class DataSerializer < ActiveModel::Serializer
-    include RoutingHelper
-    include StreamEntriesHelper
-    include ActionView::Helpers::SanitizeHelper
+  attributes :access_token, :preferred_locale, :notification_id,
+             :notification_type, :icon, :title, :body
 
-    attributes :content, :nsfw, :url, :actions,
-               :access_token, :message, :dir
-
-    def content
-      decoder.decode(strip_tags(body))
-    end
-
-    def dir
-      rtl?(body) ? 'rtl' : 'ltr'
-    end
-
-    def nsfw
-      return if object.target_status.nil?
-      object.target_status.spoiler_text.presence
-    end
-
-    def url
-      case object.type
-      when :mention
-        web_url("statuses/#{object.target_status.id}")
-      when :follow
-        web_url("accounts/#{object.from_account.id}")
-      when :favourite
-        web_url("statuses/#{object.target_status.id}")
-      when :reblog
-        web_url("statuses/#{object.target_status.id}")
-      end
-    end
-
-    def actions
-      return @actions if defined?(@actions)
-
-      @actions = []
-
-      if object.type == :mention
-        @actions << expand_action if collapsed?
-        @actions << favourite_action
-        @actions << reblog_action if rebloggable?
-      end
-
-      @actions
-    end
-
-    def access_token
-      return if actions.empty?
-      current_push_subscription.associated_access_token
-    end
-
-    def message
-      I18n.t('push_notifications.group.title')
-    end
-
-    private
-
-    def body
-      case object.type
-      when :mention
-        object.target_status.text
-      when :follow
-        object.from_account.note
-      when :favourite
-        object.target_status.text
-      when :reblog
-        object.target_status.text
-      end
-    end
-
-    def decoder
-      @decoder ||= HTMLEntities.new
-    end
-
-    def expand_action
-      {
-        title: I18n.t('push_notifications.mention.action_expand'),
-        icon: full_asset_url('web-push-icon_expand.png', skip_pipeline: true),
-        todo: 'expand',
-        action: 'expand',
-      }
-    end
-
-    def favourite_action
-      {
-        title: I18n.t('push_notifications.mention.action_favourite'),
-        icon: full_asset_url('web-push-icon_favourite.png', skip_pipeline: true),
-        todo: 'request',
-        method: 'POST',
-        action: "/api/v1/statuses/#{object.target_status.id}/favourite",
-      }
-    end
-
-    def reblog_action
-      {
-        title: I18n.t('push_notifications.mention.action_boost'),
-        icon: full_asset_url('web-push-icon_reblog.png', skip_pipeline: true),
-        todo: 'request',
-        method: 'POST',
-        action: "/api/v1/statuses/#{object.target_status.id}/reblog",
-      }
-    end
-
-    def collapsed?
-      !object.target_status.nil? && (object.target_status.sensitive? || object.target_status.spoiler_text.present?)
-    end
-
-    def rebloggable?
-      !object.target_status.nil? && !object.target_status.hidden?
-    end
+  def access_token
+    current_push_subscription.associated_access_token
   end
 
-  attributes :title, :image, :badge, :tag,
-             :timestamp, :icon
-
-  has_one :data, serializer: DataSerializer
-
-  def title
-    case object.type
-    when :mention
-      I18n.t('push_notifications.mention.title', name: name)
-    when :follow
-      I18n.t('push_notifications.follow.title', name: name)
-    when :favourite
-      I18n.t('push_notifications.favourite.title', name: name)
-    when :reblog
-      I18n.t('push_notifications.reblog.title', name: name)
-    end
+  def preferred_locale
+    current_push_subscription.associated_user&.locale || I18n.default_locale
   end
 
-  def image
-    return if object.target_status.nil? || object.target_status.media_attachments.empty?
-    full_asset_url(object.target_status.media_attachments.first.file.url(:small))
-  end
-
-  def badge
-    full_asset_url('badge.png', skip_pipeline: true)
-  end
-
-  def tag
+  def notification_id
     object.id
   end
 
-  def timestamp
-    object.created_at
+  def notification_type
+    object.type
   end
 
   def icon
-    object.from_account.avatar_static_url
+    full_asset_url(object.from_account.avatar_static_url)
   end
 
-  def data
-    object
+  def title
+    I18n.t("notification_mailer.#{object.type}.subject", name: object.from_account.display_name.presence || object.from_account.username)
   end
 
-  private
-
-  def name
-    display_name(object.from_account)
+  def body
+    truncate(strip_tags(object.target_status&.spoiler_text&.presence || object.target_status&.text || object.from_account.note), length: 140)
   end
 end