From 2da4767623f918ec486d446c464d4558556282bf Mon Sep 17 00:00:00 2001 From: Karina Kwiatek Date: Sun, 11 Sep 2022 20:42:40 +0200 Subject: [PATCH] Add JS for subscribing to and receiving push notifications --- app/javascript/packs/application.ts | 2 + .../retrospring/features/webpush/dismiss.ts | 7 +++ .../retrospring/features/webpush/enable.ts | 59 +++++++++++++++++++ .../retrospring/features/webpush/index.ts | 28 +++++++++ app/views/inbox/_push_settings.haml | 5 ++ app/views/layouts/inbox.html.haml | 1 + config/locales/frontend.en.yml | 9 +++ config/locales/views.en.yml | 2 + public/service_worker.js | 12 ++++ 9 files changed, 125 insertions(+) create mode 100644 app/javascript/retrospring/features/webpush/dismiss.ts create mode 100644 app/javascript/retrospring/features/webpush/enable.ts create mode 100644 app/javascript/retrospring/features/webpush/index.ts create mode 100644 app/views/inbox/_push_settings.haml create mode 100644 public/service_worker.js diff --git a/app/javascript/packs/application.ts b/app/javascript/packs/application.ts index e6a85b3e..4d292a2e 100644 --- a/app/javascript/packs/application.ts +++ b/app/javascript/packs/application.ts @@ -15,6 +15,7 @@ import initModeration from 'retrospring/features/moderation'; import initMemes from 'retrospring/features/memes'; import initLocales from 'retrospring/features/locales'; import initFront from 'retrospring/features/front'; +import initWebpush from 'retrospring/features/webpush'; start(); document.addEventListener('DOMContentLoaded', initAnswerbox); @@ -28,6 +29,7 @@ document.addEventListener('DOMContentLoaded', initModeration); document.addEventListener('DOMContentLoaded', initMemes); document.addEventListener('turbo:load', initLocales); document.addEventListener('turbo:load', initFront); +document.addEventListener('DOMContentLoaded', initWebpush); window['Stimulus'] = Application.start(); const context = require.context('../retrospring/controllers', true, /\.ts$/); diff --git a/app/javascript/retrospring/features/webpush/dismiss.ts b/app/javascript/retrospring/features/webpush/dismiss.ts new file mode 100644 index 00000000..48e17885 --- /dev/null +++ b/app/javascript/retrospring/features/webpush/dismiss.ts @@ -0,0 +1,7 @@ +export function dismissHandler (event: Event): void { + event.preventDefault(); + + const sender: HTMLButtonElement = event.target as HTMLButtonElement; + sender.closest('.push-settings').classList.add('d-none'); + localStorage.setItem('dismiss-push-settings-prompt', 'true'); +} diff --git a/app/javascript/retrospring/features/webpush/enable.ts b/app/javascript/retrospring/features/webpush/enable.ts new file mode 100644 index 00000000..a17b6f2c --- /dev/null +++ b/app/javascript/retrospring/features/webpush/enable.ts @@ -0,0 +1,59 @@ +import { get, post } from '@rails/request.js'; +import I18n from "retrospring/i18n"; +import { showNotification } from "utilities/notifications"; + +export function enableHandler (event: Event): void { + event.preventDefault(); + + try { + installServiceWorker() + .then(subscribe) + .then(async subscription => { + return Notification.requestPermission().then(permission => { + if (permission != "granted") { + return; + } + + post('/ajax/web_push', { + body: { + subscription + }, + contentType: 'application/json' + }).then(async response => { + const data = await response.json; + + if (data.success) { + new Notification(I18n.translate("frontend.push_notifications.subscribe.success.title"), { + body: I18n.translate("frontend.push_notifications.subscribe.success.body") + }); + } else { + new Notification(I18n.translate("frontend.push_notifications.fail.title"), { + body: I18n.translate("frontend.push_notifications.fail.body") + }); + } + }); + }); + }); + } catch (error) { + console.error("Failed to set up push notifications", error); + showNotification(I18n.translate("frontend.push_notifications.setup_fail")); + } +} + +async function installServiceWorker(): Promise { + return navigator.serviceWorker.register("/service_worker.js", { scope: "/" }); +} + +async function getServerKey(): Promise { + const response = await get("/ajax/webpush/key"); + const data = await response.json; + return Buffer.from(data.key, 'base64'); +} + +async function subscribe(registration: ServiceWorkerRegistration): Promise { + const key = await getServerKey(); + return await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: key + }); +} diff --git a/app/javascript/retrospring/features/webpush/index.ts b/app/javascript/retrospring/features/webpush/index.ts new file mode 100644 index 00000000..8b438699 --- /dev/null +++ b/app/javascript/retrospring/features/webpush/index.ts @@ -0,0 +1,28 @@ +import registerEvents from 'retrospring/utilities/registerEvents'; +import { enableHandler } from './enable'; +import { dismissHandler } from "./dismiss"; + +export default (): void => { + const swCapable = 'serviceWorker' in navigator; + if (swCapable) { + document.body.classList.add('cap-service-worker'); + } + + const notificationCapable = 'Notification' in window; + if (notificationCapable) { + document.body.classList.add('cap-notification'); + } + + if (swCapable && notificationCapable) { + navigator.serviceWorker.getRegistration().then(registration => { + if (!registration && localStorage.getItem('dismiss-push-settings-prompt') == null) { + document.querySelector('.push-settings').classList.remove('d-none'); + } + }) + + registerEvents([ + {type: 'click', target: '[data-action="push-enable"]', handler: enableHandler, global: true}, + {type: 'click', target: '[data-action="push-dismiss"]', handler: dismissHandler, global: true}, + ]); + } +} diff --git a/app/views/inbox/_push_settings.haml b/app/views/inbox/_push_settings.haml new file mode 100644 index 00000000..65c9e0db --- /dev/null +++ b/app/views/inbox/_push_settings.haml @@ -0,0 +1,5 @@ +.card.push-settings.d-none + .card-body + = t(".description") + %button.btn.btn-primary{ data: { action: "push-enable" } }= t("voc.y") + %button.btn{ data: { action: "push-dismiss" } }= t("voc.n") diff --git a/app/views/layouts/inbox.html.haml b/app/views/layouts/inbox.html.haml index e87d6cbb..b54b82b5 100644 --- a/app/views/layouts/inbox.html.haml +++ b/app/views/layouts/inbox.html.haml @@ -2,6 +2,7 @@ .row .col-sm-10.col-md-10.col-lg-9.mx-auto = render 'inbox/actions', delete_id: @delete_id, disabled: @disabled, inbox_count: @inbox_count + = render 'inbox/push_settings' = render 'layouts/messages' = yield diff --git a/config/locales/frontend.en.yml b/config/locales/frontend.en.yml index 0e734fd6..3ca04e3c 100644 --- a/config/locales/frontend.en.yml +++ b/config/locales/frontend.en.yml @@ -49,6 +49,15 @@ en: confirm: title: "Are you sure?" text: "This will mute this user for everyone." + push_notifications: + subscribe: + success: + title: Push notifications enabled! + body: You will now receive push notifications for new questions on this device. + fail: + title: Failed to subscribe to push notifications + body: Please try again later + setup_fail: Failed to set up push notifications. Please try again later. report: confirm: title: "Are you sure you want to report this %{type}?" diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 7d427b81..552e7821 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -231,6 +231,8 @@ en: share: heading: "Share" button: "Share on %{service}" + push_settings: + description: "Want to receive push notifications for new questions on your device?" layouts: feedback: heading: "Feedback" diff --git a/public/service_worker.js b/public/service_worker.js new file mode 100644 index 00000000..f9fa4973 --- /dev/null +++ b/public/service_worker.js @@ -0,0 +1,12 @@ +self.addEventListener('push', function (event) { + if (event.data) { + const notification = event.data.json(); + console.log(event.data); + + event.waitUntil(self.registration.showNotification(notification.title, { + body: notification.body + })); + } else { + console.error("Push event received, but it didn't contain any data.", event); + } +});