<template> <router-view /> <!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind --> <NotificationGroup group="alert"> <div class="fixed top-[calc(env(safe-area-inset-top)+1rem)] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end" > <Notification v-slot="{ notifications, close }" enter="transform ease-out duration-300 transition" enter-from="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-4" enter-to="translate-y-0 opacity-100 sm:translate-x-0" leave="transition ease-in duration-500" leave-from="opacity-100" leave-to="opacity-0" move="transition duration-500" move-delay="delay-300" > <div v-for="notification in notifications" :key="notification.id" class="w-full" role="alert" > <div v-if="notification.type === 'toast'" class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md" > <div class="w-full px-4 py-3"> <span class="font-semibold">{{ notification.title }}</span> <p class="text-sm">{{ notification.text }}</p> </div> </div> <div v-if="notification.type === 'info'" class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-100 rounded-lg shadow-md" > <div class="flex items-center justify-center w-12 bg-slate-600 text-slate-100" > <font-awesome icon="circle-info" class="fa-fw fa-xl" ></font-awesome> </div> <div class="relative w-full pl-4 pr-8 py-2 text-slate-900"> <span class="font-semibold">{{ notification.title }}</span> <p class="text-sm">{{ truncateLongWords(notification.text) }}</p> <button class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600" @click="close(notification.id)" > <font-awesome icon="xmark" class="fa-fw"></font-awesome> </button> </div> </div> <div v-if="notification.type === 'success'" class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-emerald-100 rounded-lg shadow-md" > <div class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100" > <font-awesome icon="circle-info" class="fa-fw fa-xl" ></font-awesome> </div> <div class="relative w-full pl-4 pr-8 py-2 text-emerald-900"> <span class="font-semibold">{{ notification.title }}</span> <p class="text-sm">{{ truncateLongWords(notification.text) }}</p> <button class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600" @click="close(notification.id)" > <font-awesome icon="xmark" class="fa-fw"></font-awesome> </button> </div> </div> <div v-if="notification.type === 'warning'" class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-amber-100 rounded-lg shadow-md" > <div class="flex items-center justify-center w-12 bg-amber-600 text-amber-100" > <font-awesome icon="triangle-exclamation" class="fa-fw fa-xl" ></font-awesome> </div> <div class="relative w-full pl-4 pr-8 py-2 text-amber-900"> <span class="font-semibold">{{ notification.title }}</span> <p class="text-sm">{{ truncateLongWords(notification.text) }}</p> <button class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600" @click="close(notification.id)" > <font-awesome icon="xmark" class="fa-fw"></font-awesome> </button> </div> </div> <div v-if="notification.type === 'danger'" class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-rose-100 rounded-lg shadow-md" > <div class="flex items-center justify-center w-12 bg-rose-600 text-rose-100" > <font-awesome icon="triangle-exclamation" class="fa-fw fa-xl" ></font-awesome> </div> <div class="relative w-full pl-4 pr-8 py-2 text-rose-900"> <span class="font-semibold">{{ notification.title }}</span> <p class="text-sm">{{ truncateLongWords(notification.text) }}</p> <button class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600" @click="close(notification.id)" > <font-awesome icon="xmark" class="fa-fw"></font-awesome> </button> </div> </div> </div> </Notification> </div> </NotificationGroup> <!-- This "group" of "modal" is the prompt for an answer. Set "type" as follows: "confirm" for yes/no, and "notification" ones: "-permission", "-mute", "-off" --> <NotificationGroup group="modal"> <div class="fixed z-[100] top-[env(safe-area-inset-top)] inset-x-0 w-full"> <Notification v-slot="{ notifications, close }" enter="transform ease-out duration-300 transition" enter-from="translate-y-2 opacity-0 sm:translate-y-4" enter-to="translate-y-0 opacity-100 sm:translate-y-0" leave="transition ease-in duration-500" leave-from="opacity-100" leave-to="opacity-0" move="transition duration-500" move-delay="delay-300" > <!-- see NotificationIface in constants/app.ts --> <div v-for="notification in notifications" :key="notification.id" class="w-full" role="alert" > <!-- Type of "confirm" will post a message. With onYes function, show a "Yes" button to call that function. With onNo function, show a "No" button to call that function, and pass it state of "askAgain" field shown if you set promptToStopAsking. --> <div v-if="notification.type === 'confirm'" class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50" > <div class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg" > <div class="w-full px-6 py-6 text-slate-900 text-center"> <span class="font-semibold text-lg"> {{ notification.title }} </span> <p class="text-sm mb-2">{{ notification.text }}</p> <button v-if="notification.onYes" class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2" @click=" notification.onYes(); close(notification.id); " > Yes{{ notification.yesText ? ", " + notification.yesText : "" }} </button> <button v-if="notification.onNo" class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2" @click=" notification.onNo(stopAsking); close(notification.id); stopAsking = false; // reset value " > No{{ notification.noText ? ", " + notification.noText : "" }} </button> <label v-if="notification.promptToStopAsking && notification.onNo" for="toggleStopAsking" class="flex items-center justify-between cursor-pointer my-4" @click="stopAsking = !stopAsking" > <!-- label --> <span class="ml-2">... and do not ask again.</span> <!-- toggle --> <div class="relative ml-2"> <!-- input --> <input v-model="stopAsking" type="checkbox" name="stopAsking" class="sr-only" /> <!-- line --> <div class="block bg-slate-500 w-14 h-8 rounded-full"></div> <!-- dot --> <div class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition" ></div> </div> </label> <button class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md" @click=" notification.onCancel ? notification.onCancel(stopAsking) : null; close(notification.id); stopAsking = false; // reset value for next time they open this modal " > {{ notification.onYes ? "Cancel" : "Close" }} </button> </div> </div> </div> <div v-if="notification.type === 'notification-mute'" class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50" > <div class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg" > <div class="w-full px-6 py-6 text-slate-900 text-center"> <p class="text-lg mb-4">Mute app notifications:</p> <button class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2" > For 1 Day </button> <button class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2" > For 2 Days </button> <button class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2" > For 1 Week </button> <button class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2" > Until I turn it back on </button> <button class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md" @click="close(notification.id)" > Cancel </button> </div> </div> </div> <div v-if="notification.type === 'notification-off'" class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50" > <div class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg" > <div class="w-full px-6 py-6 text-slate-900 text-center"> <p class="text-lg mb-4"> Would you like to <b>turn off</b> this notification? </p> <button class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2" @click=" close(notification.id); turnOffNotifications(notification); " > Turn Off Notification </button> <button class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md" @click="close(notification.id)" > Leave it On </button> </div> </div> </div> </div> </Notification> </div> </NotificationGroup> </template> <script lang="ts"> import { Vue, Component } from "vue-facing-decorator"; import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index"; import { NotificationIface } from "./constants/app"; import { logger } from "./utils/logger"; interface Settings { notifyingNewActivityTime?: string; notifyingReminderTime?: string; } @Component export default class App extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; stopAsking = false; // created() { // logger.log( // "Component created: Reactivity set up.", // window.location.pathname, // ); // } // beforeCreate() { // logger.log("Component beforeCreate: Instance initialized."); // } // beforeMount() { // logger.log("Component beforeMount: Template is about to be rendered."); // } // mounted() { // logger.log("Component mounted: Template is now rendered."); // } // beforeUpdate() { // logger.log("Component beforeUpdate: DOM is about to be updated."); // } // updated() { // logger.log("Component updated: DOM has been updated."); // } // beforeUnmount() { // logger.log("Component beforeUnmount: Cleaning up before removal."); // } // unmounted() { // logger.log("Component unmounted: Component removed from the DOM."); // } truncateLongWords(sentence: string) { return sentence .split(" ") .map((word) => (word.length > 30 ? word.slice(0, 30) + "..." : word)) .join(" "); } async turnOffNotifications( notification: NotificationIface, ): Promise<boolean> { logger.log("Starting turnOffNotifications..."); let subscription: PushSubscriptionJSON | null = null; let allGoingOff = false; try { logger.log("Retrieving settings for the active account..."); const settings: Settings = await retrieveSettingsForActiveAccount(); logger.log("Retrieved settings:", settings); const notifyingNewActivity = !!settings?.notifyingNewActivityTime; const notifyingReminder = !!settings?.notifyingReminderTime; if (!notifyingNewActivity || !notifyingReminder) { allGoingOff = true; logger.log("Both notifications are being turned off."); } logger.log("Checking service worker readiness..."); await navigator.serviceWorker?.ready .then((registration) => { logger.log("Service worker is ready. Fetching subscription..."); return registration.pushManager.getSubscription(); }) .then(async (subscript: PushSubscription | null) => { if (subscript) { subscription = subscript.toJSON(); logger.log("PushSubscription retrieved:", subscription); if (allGoingOff) { logger.log("Unsubscribing from push notifications..."); await subscript.unsubscribe(); logger.log("Successfully unsubscribed."); } } else { logConsoleAndDb("Subscription object is not available."); logger.log("No subscription found."); } }) .catch((error) => { logConsoleAndDb( "Push provider server communication failed: " + JSON.stringify(error), true, ); logger.error("Error during subscription fetch:", error); }); if (!subscription) { logger.log("No subscription available. Notifying user..."); this.$notify( { group: "alert", type: "info", title: "Finished", text: "Notifications are off.", }, 5000, ); logger.log("Exiting as there is no subscription to process."); return true; } const serverSubscription = { ...subscription, }; if (!allGoingOff) { serverSubscription["notifyType"] = notification.title; logger.log( `Server subscription updated with notifyType: ${notification.title}`, ); } logger.log("Sending unsubscribe request to the server..."); const pushServerSuccess = await fetch("/web-push/unsubscribe", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(serverSubscription), }) .then(async (response) => { if (!response.ok) { const errorBody = await response.text(); logConsoleAndDb( `Push server failed: ${response.status} ${errorBody}`, true, ); logger.error("Push server error response:", errorBody); } logger.log(`Server response status: ${response.status}`); return response.ok; }) .catch((error) => { logConsoleAndDb( "Push server communication failed: " + JSON.stringify(error), true, ); logger.error("Error during server communication:", error); return false; }); const message = pushServerSuccess ? "Notification is off." : "Notification is still on. Try to turn it off again."; logger.log("Server response processed. Message:", message); this.$notify( { group: "alert", type: "info", title: "Finished", text: message, }, 5000, ); if (notification.callback) { logger.log("Executing notification callback..."); notification.callback(pushServerSuccess); } logger.log( "Completed turnOffNotifications with success:", pushServerSuccess, ); return pushServerSuccess; } catch (error) { logConsoleAndDb( "Error turning off notifications: " + JSON.stringify(error), true, ); logger.error("Critical error in turnOffNotifications:", error); this.$notify( { group: "alert", type: "error", title: "Error", text: "Failed to turn off notifications. Please try again.", }, 5000, ); return false; } } } </script> <style> #Content { padding-left: 1.5rem; padding-right: 1.5rem; padding-top: calc(env(safe-area-inset-top) + 1.5rem); padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem); } #QuickNav ~ #Content { padding-bottom: calc(env(safe-area-inset-bottom) + 6rem); } </style>