diff --git a/package.json b/package.json
index 2a22cbb1..cd0fe95c 100644
--- a/package.json
+++ b/package.json
@@ -138,7 +138,8 @@
"btoa",
"Blob",
"Element",
- "Image"
+ "Image",
+ "NotificationEvent"
],
"ignore": [
"dist",
diff --git a/routes/_actions/pushSubscription.js b/routes/_actions/pushSubscription.js
new file mode 100644
index 00000000..6d45a1d8
--- /dev/null
+++ b/routes/_actions/pushSubscription.js
@@ -0,0 +1,89 @@
+import { getSubscription, deleteSubscription, postSubscription, putSubscription } from '../_api/pushSubscription'
+import { store } from '../_store/store'
+import { urlBase64ToUint8Array } from '../_utils/base64'
+
+const dummyApplicationServerKey = 'BImgAz4cF_yvNFp8uoBJCaGpCX4d0atNIFMHfBvAAXCyrnn9IMAFQ10DW_ZvBCzGeR4fZI5FnEi2JVcRE-L88jY='
+
+export async function updatePushSubscriptionForInstance (instanceName) {
+ const { loggedInInstances, pushSubscription } = store.get()
+ const accessToken = loggedInInstances[instanceName].access_token
+
+ if (pushSubscription === null) {
+ return
+ }
+
+ const registration = await navigator.serviceWorker.ready
+ const subscription = await registration.pushManager.getSubscription()
+
+ if (subscription === null) {
+ store.set({ pushSubscription: null })
+ store.save()
+ return
+ }
+
+ try {
+ const backendSubscription = await getSubscription(instanceName, accessToken)
+
+ // Check if applicationServerKey changed (need to get another subscription from the browser)
+ if (btoa(urlBase64ToUint8Array(backendSubscription.server_key).buffer) !== btoa(subscription.options.applicationServerKey)) {
+ await subscription.unsubscribe()
+ await deleteSubscription(instanceName, accessToken)
+ await updateAlerts(instanceName, pushSubscription.alerts)
+ } else {
+ store.set({ pushSubscription: backendSubscription })
+ store.save()
+ }
+ } catch (e) {
+ // TODO: Better way to detect 404
+ if (e.message.startsWith('404:')) {
+ await subscription.unsubscribe()
+ store.set({ pushSubscription: null })
+ store.save()
+ }
+ }
+}
+
+export async function updateAlerts (instanceName, alerts) {
+ const { loggedInInstances } = store.get()
+ const accessToken = loggedInInstances[instanceName].access_token
+
+ const registration = await navigator.serviceWorker.ready
+ let subscription = await registration.pushManager.getSubscription()
+
+ if (subscription === null) {
+ // We need applicationServerKey in order to register a push subscription
+ // but the API doesn't expose it as a constant (as it should).
+ // So we need to register a subscription with a dummy applicationServerKey,
+ // send it to the backend saves it and return applicationServerKey, which
+ // we use to register a new subscription.
+ // https://github.com/tootsuite/mastodon/issues/8785
+ subscription = await registration.pushManager.subscribe({
+ applicationServerKey: urlBase64ToUint8Array(dummyApplicationServerKey),
+ userVisibleOnly: true
+ })
+
+ let backendSubscription = await postSubscription(instanceName, accessToken, subscription, alerts)
+
+ await subscription.unsubscribe()
+
+ subscription = await registration.pushManager.subscribe({
+ applicationServerKey: urlBase64ToUint8Array(backendSubscription.server_key),
+ userVisibleOnly: true
+ })
+
+ backendSubscription = await postSubscription(instanceName, accessToken, subscription, alerts)
+
+ store.set({ pushSubscription: backendSubscription })
+ store.save()
+ } else {
+ try {
+ const backendSubscription = await putSubscription(instanceName, accessToken, alerts)
+ store.set({ pushSubscription: backendSubscription })
+ store.save()
+ } catch (e) {
+ const backendSubscription = await postSubscription(instanceName, accessToken, subscription, alerts)
+ store.set({ pushSubscription: backendSubscription })
+ store.save()
+ }
+ }
+}
diff --git a/routes/_api/oauth.js b/routes/_api/oauth.js
index 4d294bc2..8ab5ecc3 100644
--- a/routes/_api/oauth.js
+++ b/routes/_api/oauth.js
@@ -2,7 +2,7 @@ import { post, paramsString, WRITE_TIMEOUT } from '../_utils/ajax'
import { basename } from './utils'
const WEBSITE = 'https://pinafore.social'
-const SCOPES = 'read write follow'
+const SCOPES = 'read write follow push'
const CLIENT_NAME = 'Pinafore'
export function registerApplication (instanceName, redirectUri) {
diff --git a/routes/_api/pushSubscription.js b/routes/_api/pushSubscription.js
new file mode 100644
index 00000000..a17a1455
--- /dev/null
+++ b/routes/_api/pushSubscription.js
@@ -0,0 +1,26 @@
+import { auth, basename } from './utils'
+import { post, put, get, del } from '../_utils/ajax'
+
+export async function postSubscription (instanceName, accessToken, subscription, alerts) {
+ const url = `${basename(instanceName)}/api/v1/push/subscription`
+
+ return post(url, { subscription: subscription.toJSON(), data: { alerts } }, auth(accessToken))
+}
+
+export async function putSubscription (instanceName, accessToken, alerts) {
+ const url = `${basename(instanceName)}/api/v1/push/subscription`
+
+ return put(url, { data: { alerts } }, auth(accessToken))
+}
+
+export async function getSubscription (instanceName, accessToken) {
+ const url = `${basename(instanceName)}/api/v1/push/subscription`
+
+ return get(url, auth(accessToken))
+}
+
+export async function deleteSubscription (instanceName, accessToken) {
+ const url = `${basename(instanceName)}/api/v1/push/subscription`
+
+ return del(url, auth(accessToken))
+}
diff --git a/routes/_pages/settings/instances/[instanceName].html b/routes/_pages/settings/instances/[instanceName].html
index 61b3a1f6..e0d98c41 100644
--- a/routes/_pages/settings/instances/[instanceName].html
+++ b/routes/_pages/settings/instances/[instanceName].html
@@ -15,6 +15,27 @@
Your browser doesn't support push notifications.
+ {:elseif $notificationPermission === "denied"} +You have denied permission to show notifications.
+ {/if} + +