Add webpush front-end support
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -84,6 +84,10 @@ export const CONFIG = gql`
|
||||
instanceFeeds {
|
||||
enabled
|
||||
}
|
||||
webPush {
|
||||
enabled
|
||||
publicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -160,3 +164,14 @@ export const TIMEZONES = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const WEB_PUSH = gql`
|
||||
query {
|
||||
config {
|
||||
webPush {
|
||||
enabled
|
||||
publicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
13
js/src/graphql/webPush.ts
Normal file
13
js/src/graphql/webPush.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export const REGISTER_PUSH_MUTATION = gql`
|
||||
mutation RegisterPush($endpoint: String!, $auth: String!, $p256dh: String!) {
|
||||
registerPush(endpoint: $endpoint, auth: $auth, p256dh: $p256dh)
|
||||
}
|
||||
`;
|
||||
|
||||
export const UNREGISTER_PUSH_MUTATION = gql`
|
||||
mutation UnRegisterPush($endpoint: String!) {
|
||||
unregisterPush(endpoint: $endpoint)
|
||||
}
|
||||
`;
|
||||
@@ -2,33 +2,33 @@
|
||||
|
||||
import { register } from "register-service-worker";
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready() {
|
||||
console.log(
|
||||
"App is being served from cache by a service worker.\n" +
|
||||
"For more details, visit https://goo.gl/AFskqB"
|
||||
);
|
||||
},
|
||||
registered() {
|
||||
console.log("Service worker has been registered.");
|
||||
},
|
||||
cached() {
|
||||
console.log("Content has been cached for offline use.");
|
||||
},
|
||||
updatefound() {
|
||||
console.log("New content is downloading.");
|
||||
},
|
||||
updated() {
|
||||
console.log("New content is available; please refresh.");
|
||||
},
|
||||
offline() {
|
||||
console.log(
|
||||
"No internet connection found. App is running in offline mode."
|
||||
);
|
||||
},
|
||||
error(error) {
|
||||
console.error("Error during service worker registration:", error);
|
||||
},
|
||||
});
|
||||
}
|
||||
// if (process.env.NODE_ENV === "production") {
|
||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||
ready() {
|
||||
console.log(
|
||||
"App is being served from cache by a service worker.\n" +
|
||||
"For more details, visit https://goo.gl/AFskqB"
|
||||
);
|
||||
},
|
||||
registered() {
|
||||
console.log("Service worker has been registered.");
|
||||
},
|
||||
cached() {
|
||||
console.log("Content has been cached for offline use.");
|
||||
},
|
||||
updatefound() {
|
||||
console.log("New content is downloading.");
|
||||
},
|
||||
updated() {
|
||||
console.log("New content is available; please refresh.");
|
||||
},
|
||||
offline() {
|
||||
console.log(
|
||||
"No internet connection found. App is running in offline mode."
|
||||
);
|
||||
},
|
||||
error(error) {
|
||||
console.error("Error during service worker registration:", error);
|
||||
},
|
||||
});
|
||||
// }
|
||||
|
||||
@@ -11,6 +11,7 @@ import { CacheableResponsePlugin } from "workbox-cacheable-response";
|
||||
import { ExpirationPlugin } from "workbox-expiration";
|
||||
|
||||
import { precacheAndRoute } from "workbox-precaching";
|
||||
import { IPushNotification } from "./types/push-notification";
|
||||
|
||||
// Use with precache injection
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -75,3 +76,25 @@ registerRoute(
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
self.addEventListener("push", async (event: any) => {
|
||||
const payload = event.data.json() as IPushNotification;
|
||||
console.log("received push", payload);
|
||||
const options = {
|
||||
title: payload.title,
|
||||
body: payload.body,
|
||||
icon: "/img/icons/android-chrome-512x512.png",
|
||||
badge: "/img/icons/badge-128x128.png",
|
||||
timestamp: new Date(payload.timestamp),
|
||||
lang: payload.locale,
|
||||
data: {
|
||||
dateOfArrival: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
self.registration.showNotification(payload.title, options)
|
||||
);
|
||||
});
|
||||
|
||||
64
js/src/services/push-subscription.ts
Normal file
64
js/src/services/push-subscription.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import apolloProvider from "@/vue-apollo";
|
||||
import ApolloClient from "apollo-client";
|
||||
import { NormalizedCacheObject } from "apollo-cache-inmemory";
|
||||
import { WEB_PUSH } from "../graphql/config";
|
||||
import { IConfig } from "../types/config.model";
|
||||
|
||||
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
|
||||
for (let i = 0; i < rawData.length; ++i) {
|
||||
outputArray[i] = rawData.charCodeAt(i);
|
||||
}
|
||||
return outputArray;
|
||||
}
|
||||
|
||||
export async function subscribeUserToPush(): Promise<PushSubscription | null> {
|
||||
const client = apolloProvider.defaultClient as ApolloClient<NormalizedCacheObject>;
|
||||
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const { data } = await client.mutate<{ config: IConfig }>({
|
||||
mutation: WEB_PUSH,
|
||||
});
|
||||
|
||||
if (data?.config?.webPush?.enabled && data?.config?.webPush?.publicKey) {
|
||||
const subscribeOptions = {
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(
|
||||
data?.config?.webPush?.publicKey
|
||||
),
|
||||
};
|
||||
const pushSubscription = await registration.pushManager.subscribe(
|
||||
subscribeOptions
|
||||
);
|
||||
console.log(
|
||||
"Received PushSubscription: ",
|
||||
JSON.stringify(pushSubscription)
|
||||
);
|
||||
return pushSubscription;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function isSubscribed(): Promise<boolean> {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
return (await registration.pushManager.getSubscription()) !== null;
|
||||
}
|
||||
|
||||
export async function unsubscribeUserToPush(): Promise<string | undefined> {
|
||||
console.log("performing unsubscribeUserToPush");
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
console.log("found registration", registration);
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
console.log("found subscription", subscription);
|
||||
if (subscription && (await subscription?.unsubscribe()) === true) {
|
||||
console.log("done unsubscription");
|
||||
return subscription?.endpoint;
|
||||
}
|
||||
console.log("went wrong");
|
||||
return undefined;
|
||||
}
|
||||
@@ -98,4 +98,8 @@ export interface IConfig {
|
||||
instanceFeeds: {
|
||||
enabled: boolean;
|
||||
};
|
||||
webPush: {
|
||||
enabled: boolean;
|
||||
publicKey: string;
|
||||
};
|
||||
}
|
||||
|
||||
7
js/src/types/push-notification.ts
Normal file
7
js/src/types/push-notification.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface IPushNotification {
|
||||
title: string;
|
||||
body: string;
|
||||
url: string;
|
||||
timestamp: string;
|
||||
locale: string;
|
||||
}
|
||||
@@ -15,6 +15,19 @@
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
<div class="setting-title">
|
||||
<h2>{{ $t("Participation notifications") }}</h2>
|
||||
</div>
|
||||
<b-button v-if="subscribed" @click="unsubscribeToWebPush()">{{
|
||||
$t("Unsubscribe to WebPush")
|
||||
}}</b-button>
|
||||
<b-button
|
||||
icon-left="rss"
|
||||
@click="subscribeToWebPush"
|
||||
v-else-if="canShowWebPush()"
|
||||
>{{ $t("WebPush") }}</b-button
|
||||
>
|
||||
<span v-else>{{ $t("You can't use webpush in this browser.") }}</span>
|
||||
<div class="setting-title">
|
||||
<h2>{{ $t("Participation notifications") }}</h2>
|
||||
</div>
|
||||
@@ -202,6 +215,14 @@ import { IUser } from "../../types/current-user.model";
|
||||
import RouteName from "../../router/name";
|
||||
import { IFeedToken } from "@/types/feedtoken.model";
|
||||
import { CREATE_FEED_TOKEN, DELETE_FEED_TOKEN } from "@/graphql/feed_tokens";
|
||||
import {
|
||||
subscribeUserToPush,
|
||||
unsubscribeUserToPush,
|
||||
} from "../../services/push-subscription";
|
||||
import {
|
||||
REGISTER_PUSH_MUTATION,
|
||||
UNREGISTER_PUSH_MUTATION,
|
||||
} from "@/graphql/webPush";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -235,6 +256,8 @@ export default class Notifications extends Vue {
|
||||
|
||||
showCopiedTooltip = { ics: false, atom: false };
|
||||
|
||||
subscribed = false;
|
||||
|
||||
mounted(): void {
|
||||
this.notificationPendingParticipationValues = {
|
||||
[INotificationPendingEnum.NONE]: this.$t("Do not receive any mail"),
|
||||
@@ -307,6 +330,55 @@ export default class Notifications extends Vue {
|
||||
this.feedTokens.push(newToken);
|
||||
}
|
||||
|
||||
async subscribeToWebPush(): Promise<void> {
|
||||
if (this.canShowWebPush()) {
|
||||
const subscription = await subscribeUserToPush();
|
||||
if (subscription) {
|
||||
const subscriptionJSON = subscription?.toJSON();
|
||||
console.log("subscription", subscriptionJSON);
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: REGISTER_PUSH_MUTATION,
|
||||
variables: {
|
||||
endpoint: subscriptionJSON.endpoint,
|
||||
auth: subscriptionJSON?.keys?.auth,
|
||||
p256dh: subscriptionJSON?.keys?.p256dh,
|
||||
},
|
||||
});
|
||||
this.subscribed = true;
|
||||
console.log(data);
|
||||
}
|
||||
} else {
|
||||
console.log("can't do webpush");
|
||||
}
|
||||
}
|
||||
|
||||
async unsubscribeToWebPush(): Promise<void> {
|
||||
const endpoint = await unsubscribeUserToPush();
|
||||
if (endpoint) {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: UNREGISTER_PUSH_MUTATION,
|
||||
variables: {
|
||||
endpoint,
|
||||
},
|
||||
});
|
||||
console.log(data);
|
||||
this.subscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
canShowWebPush(): boolean {
|
||||
return window.isSecureContext && !!navigator.serviceWorker;
|
||||
}
|
||||
|
||||
async created(): Promise<void> {
|
||||
this.subscribed = await this.isSubscribed();
|
||||
}
|
||||
|
||||
private async isSubscribed(): Promise<boolean> {
|
||||
const registration = await navigator.serviceWorker.getRegistration();
|
||||
return (await registration?.pushManager.getSubscription()) !== null;
|
||||
}
|
||||
|
||||
private async deleteFeedToken(token: string): Promise<void> {
|
||||
await this.$apollo.mutate({
|
||||
mutation: DELETE_FEED_TOKEN,
|
||||
|
||||
Reference in New Issue
Block a user