diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml new file mode 100644 index 0000000..a3e92c3 --- /dev/null +++ b/.github/workflows/dev.yml @@ -0,0 +1,36 @@ +# This is a basic workflow to help you get started with Actions + +name: Deploy on push + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the "main" branch + push: + branches: [ "dev" ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: self-hosted + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v4 + + - name: Get to work dir + run: cd $GITHUB_WORKSPACE + + - name: Install dependencies + run: npm i + + - name: Run build task + run: npm run build + + - name: Deploy to web + run: rsync -avz --delete dist/ ~/nix_frontend_dev diff --git a/public/notification-service.js b/public/notification-service.js new file mode 100644 index 0000000..dd25629 --- /dev/null +++ b/public/notification-service.js @@ -0,0 +1,98 @@ +/// + +/** @type {ServiceWorkerGlobalScope} */ +const service_worker = self; + +function urlB64ToUint8Array(base64String) { + 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); + const outputData = outputArray.map((output, index) => + rawData.charCodeAt(index), + ); + + return outputData; +} + +service_worker.addEventListener("activate", async () => { + console.log("Hello from service worker"); + try { + /** @type {PushSubscriptionOptions} */ + const options = { + applicationServerKey: + "BCOsRaxpJeR0KyIPIg1rHx3pUtWVsGDGOxH65dDkqyU5ycF-CjPJxuqiXF4M0LpUMG_rk_YxSZX34uHbrV5umJQ", + userVisibleOnly: true, + }; + console.log("Hello from service worker second time"); + + const subscription = + await service_worker.registration.pushManager.subscribe(options); + + let subscribe_json = subscription.toJSON(); + console.log("Subscription", subscribe_json); + + const body = JSON.stringify(subscribe_json); + + await fetch("https://team.dtutimes.com/api/v1/notification/subscribe", { + method: "POST", + body, + headers: { + "Content-Type": "application/json", + }, + }) + .then((res) => res.text()) + .then(console.log) + .catch(console.error); + } catch (err) { + console.log("Error", err); + } +}); + +service_worker.addEventListener("push", function (event) { + const data = event.data.json(); + if (data) { + pushNotification( + data.title, + { + icon: "https://dtutimes.com/favicon.ico", + body: data.body, + }, + service_worker.registration, + ); + } else { + console.log("No data!"); + } +}); + +/** + * @param title {string} + * @param options {NotificationOptions} + * @param swRegistration {ServiceWorkerRegistration} + */ +const pushNotification = (title, options, swRegistration) => { + console.log("Push Notification", title, options); + swRegistration.showNotification(title, options); +}; + +service_worker.addEventListener("notificationclick", (event) => { + event.notification.close(); + event.waitUntil( + service_worker.clients + .matchAll({ + type: "window", + }) + .then((clientList) => { + for (const client of clientList) { + if ( + client.url === "https://team.dtutimes.com/notification" && + "focus" in client + ) + return client.focus(); + } + if (service_worker.clients.openWindow) + return service_worker.clients.openWindow("/notification"); + }), + ); +}); diff --git a/src/pages/Notification/index.tsx b/src/pages/Notification/index.tsx index 2f6f1c7..55bd708 100644 --- a/src/pages/Notification/index.tsx +++ b/src/pages/Notification/index.tsx @@ -3,6 +3,11 @@ import { ErrorContext } from "@/contexts/error"; import API from "@/services/API"; import { INotification } from "@/types/notification"; import React, { useEffect, useState } from "react"; +import { + setup_notification, + disable_notification, + setup_present, +} from "./notification-engine"; interface NotificationCardProps { notif: INotification; @@ -56,8 +61,11 @@ export default function NotificationPage() { const { setError } = React.useContext(ErrorContext); const [notifications, setNotifs] = useState(null); + const [status, setStatus] = useState(null); useEffect(() => { + setup_present().then(setStatus); + const notifications = "/notification"; API.get(notifications) .then((response) => { @@ -69,7 +77,7 @@ export default function NotificationPage() { }); }, []); - if (notifications === null) + if (notifications === null || status === null) return (
@@ -79,6 +87,29 @@ export default function NotificationPage() { return (

Latest Updates

+
+ {status ? ( + + ) : ( + + )} +
{notifications.map((notif) => { return ; })} diff --git a/src/pages/Notification/notification-engine.ts b/src/pages/Notification/notification-engine.ts new file mode 100644 index 0000000..3b51da7 --- /dev/null +++ b/src/pages/Notification/notification-engine.ts @@ -0,0 +1,61 @@ +class Notification { + static notify = window.Notification; + + public static async requestPermission() { + if (!Notification.isSupported()) { + throw new Error("Notification API is not supported"); + } + const permission = await Notification.notify.requestPermission(); + console.log("Notification API permission", permission); + return permission; + } + + public static isSupported() { + return Notification.notify !== undefined; + } +} + +class BgService { + static bg = window.navigator; + + public static isSupported() { + return BgService.bg !== undefined; + } + + public static async registerServiceWorker(swPath: string) { + const registration = await BgService.bg.serviceWorker.register(swPath); + console.log("Service worker registered", registration); + return registration; + } + + public static async unregisterServiceWorker() { + const registrations = await BgService.getRegistrations(); + registrations.forEach((registration) => registration.unregister()); + return registrations; + } + + public static async getRegistrations() { + const registration = await BgService.bg.serviceWorker.getRegistrations(); + console.log("Service worker registered", registration); + return registration; + } +} + +export async function setup_notification() { + Notification.requestPermission(); + + const worker = await BgService.registerServiceWorker( + "https://team.dtutimes.com/notification-service.js", + ); + + console.log("Service worker registered", worker); +} + +export async function disable_notification() { + await BgService.unregisterServiceWorker(); +} + +export async function setup_present() { + const services = await BgService.getRegistrations(); + return services.length > 0; +} diff --git a/tsconfig.json b/tsconfig.json index f8e9eef..e03b018 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,7 +31,8 @@ } }, "include": [ - "src" + "src", + "public/notification-service.js" ], "references": [ {