< template >
< router -view / >
<!-- https : //github.com/emmanuelsw/notiwind -->
< NotificationGroup group = "alert" >
< div
class = "fixed top-4 right-4 w-full 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"
>
< fa icon = "circle-info" class = "fa-fw fa-xl" > < / fa >
< / 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" > { { notification . text } } < / p >
< button
@ click = "close(notification.id)"
class = "absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
>
< fa icon = "xmark" class = "fa-fw" > < / fa >
< / 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"
>
< fa icon = "circle-info" class = "fa-fw fa-xl" > < / fa >
< / 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" > { { notification . text } } < / p >
< button
@ click = "close(notification.id)"
class = "absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
>
< fa icon = "xmark" class = "fa-fw" > < / fa >
< / 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"
>
< fa icon = "triangle-exclamation" class = "fa-fw fa-xl" > < / fa >
< / 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" > { { notification . text } } < / p >
< button
@ click = "close(notification.id)"
class = "absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
>
< fa icon = "xmark" class = "fa-fw" > < / fa >
< / 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"
>
< fa icon = "triangle-exclamation" class = "fa-fw fa-xl" > < / fa >
< / 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" > { { notification . text } } < / p >
< button
@ click = "close(notification.id)"
class = "absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
>
< fa icon = "xmark" class = "fa-fw" > < / fa >
< / button >
< / div >
< / div >
< / div >
< / Notification >
< / div >
< / NotificationGroup >
< NotificationGroup group = "modal" >
< div class = "fixed z-[100] top-0 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"
>
< div
v - for = "notification in notifications"
: key = "notification.id"
class = "w-full"
role = "alert"
>
< div
v - if = "notification.type === 'notification-permission'"
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 v-if ="serviceWorkerReady" class="text-lg mb-4" >
Would you like to < b > turn on < / b > notifications for this app ?
< / p >
< p v -else class = "text-lg mb-4" >
Waiting for system initialization , which may take up to 10
seconds ...
< fa icon = "spinner" spin / >
< / p >
< button
v - if = "serviceWorkerReady"
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 = "
close ( notification . id ) ;
turnOnNotifications ( ) ;
"
>
Turn on Notifications
< / button >
< button
@ click = "close(notification.id)"
class = "block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Maybe Later
< / 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 Hour
< / 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 8 Hours
< / 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 24 Hours
< / 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
@ click = "close(notification.id)"
class = "block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
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 > notifications for this app ?
< / p >
< button
@ click = "
close ( notification . id ) ;
turnOffNotifications ( ) ;
"
class = "block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
>
Turn Off Notifications
< / button >
< button
@ click = "close(notification.id)"
class = "block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Leave it On
< / button >
< / div >
< / div >
< / div >
< / div >
< / Notification >
< / div >
< / NotificationGroup >
< / template >
< style > < / style >
< script lang = "ts" >
import { Vue , Component } from "vue-facing-decorator" ;
import axios from "axios" ;
interface ServiceWorkerMessage {
type : string ;
data : string ;
}
interface ServiceWorkerResponse {
// Define the properties and their types
success : boolean ;
message ? : string ;
}
// Example interface for error
interface ErrorResponse {
message : string ;
// Other properties as needed
}
interface VapidResponse {
data : {
vapidKey : string ;
} ;
}
import { DEFAULT_PUSH_SERVER } from "@/constants/app" ;
import { db } from "@/db/index" ;
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings" ;
import { sendTestThroughPushServer } from "@/libs/util" ;
interface Notification {
group : string ;
type : string ;
title : string ;
text : string ;
}
@ Component
export default class App extends Vue {
$notify ! : ( notification : Notification , timeout ? : number ) => void ;
b64 = "" ;
serviceWorkerReady = false ;
async mounted ( ) {
try {
await db . open ( ) ;
const settings = await db . settings . get ( MASTER_SETTINGS_KEY ) ;
let pushUrl = DEFAULT_PUSH_SERVER ;
if ( settings ? . webPushServer ) {
pushUrl = settings . webPushServer ;
}
await axios
. get ( pushUrl + "/web-push/vapid" )
. then ( ( response : VapidResponse ) => {
this . b64 = response . data ? . vapidKey || "" ;
console . log ( "Got vapid key:" , this . b64 ) ;
navigator . serviceWorker . addEventListener ( "controllerchange" , ( ) => {
console . log ( "New service worker is now controlling the page" ) ;
} ) ;
} ) ;
if ( ! this . b64 ) {
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Error Setting Notifications" ,
text : "Could not set notifications." ,
} ,
- 1 ,
) ;
}
} catch ( error ) {
if ( window . location . host . startsWith ( "localhost" ) ) {
console . log ( "Ignoring the error getting VAPID for local development." ) ;
} else {
console . error ( "Got an error initializing notifications:" , error ) ;
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Error Setting Notifications" ,
text : "Got an error setting notifications." ,
} ,
- 1 ,
) ;
}
}
// there may be a long pause here on first initialization
navigator . serviceWorker . ready . then ( ( ) => {
this . serviceWorkerReady = true ;
} ) ;
}
private sendMessageToServiceWorker (
message : ServiceWorkerMessage ,
) : Promise < unknown > {
return new Promise ( ( resolve , reject ) => {
if ( navigator . serviceWorker . controller ) {
const messageChannel = new MessageChannel ( ) ;
messageChannel . port1 . onmessage = ( event : MessageEvent ) => {
if ( event . data . error ) {
reject ( event . data . error as ErrorResponse ) ;
} else {
resolve ( event . data as ServiceWorkerResponse ) ;
}
} ;
navigator . serviceWorker . controller . postMessage ( message , [
messageChannel . port2 ,
] ) ;
} else {
reject ( "Service worker controller not available" ) ;
}
} ) ;
}
private askPermission ( ) : Promise < NotificationPermission > {
console . log ( "Requesting permission for notifications:" , navigator ) ;
if ( ! ( "serviceWorker" in navigator && navigator . serviceWorker . controller ) ) {
return Promise . reject ( "Service worker not available." ) ;
}
const secret = localStorage . getItem ( "secret" ) ;
if ( ! secret ) {
return Promise . reject ( "No secret found." ) ;
}
return this . sendSecretToServiceWorker ( secret )
. then ( ( ) => this . checkNotificationSupport ( ) )
. then ( ( ) => this . requestNotificationPermission ( ) )
. catch ( ( error ) => Promise . reject ( error ) ) ;
}
private sendSecretToServiceWorker ( secret : string ) : Promise < void > {
const message : ServiceWorkerMessage = {
type : "SEND_LOCAL_DATA" ,
data : secret ,
} ;
return this . sendMessageToServiceWorker ( message ) . then ( ( response ) => {
console . log ( "Response from service worker:" , response ) ;
} ) ;
}
private checkNotificationSupport ( ) : Promise < void > {
if ( ! ( "Notification" in window ) ) {
alert ( "This browser does not support notifications." ) ;
return Promise . reject ( "This browser does not support notifications." ) ;
}
if ( Notification . permission === "granted" ) {
return Promise . resolve ( ) ;
}
return Promise . resolve ( ) ;
}
private requestNotificationPermission ( ) : Promise < NotificationPermission > {
return Notification . requestPermission ( ) . then ( ( permission ) => {
if ( permission !== "granted" ) {
alert (
"Allow this app permission to make notifications for personal reminders." +
" You can adjust them at any time in your settings." ,
) ;
throw new Error ( "We weren't granted permission." ) ;
}
return permission ;
} ) ;
}
public async turnOnNotifications ( ) {
return this . askPermission ( )
. then ( ( permission ) => {
console . log ( "Permission granted:" , permission ) ;
// Call the function and handle promises
this . subscribeToPush ( )
. then ( ( ) => {
console . log ( "Subscribed successfully." ) ;
return navigator . serviceWorker . ready ;
} )
. then ( ( registration ) => {
return registration . pushManager . getSubscription ( ) ;
} )
. then ( async ( subscription ) => {
if ( subscription ) {
await this . $notify (
{
group : "alert" ,
type : "info" ,
title : "Notification Setup Underway" ,
text : "Setting up notifications for interesting activity, which takes about 10 seconds. If you don't see a final confirmation, check the 'Troubleshoot' page." ,
} ,
- 1 ,
) ;
this . sendSubscriptionToServer ( subscription ) ;
return subscription ;
} else {
throw new Error ( "Subscription object is not available." ) ;
}
} )
. then ( async ( subscription ) => {
console . log (
"Subscription data sent to server and all finished successfully." ,
) ;
await sendTestThroughPushServer ( subscription , true ) ;
this . $notify (
{
group : "alert" ,
type : "success" ,
title : "Notifications Turned On" ,
text : "Notifications are on. You should see at least one on your device; if not, check the 'Troubleshoot' page." ,
} ,
- 1 ,
) ;
} )
. catch ( ( error ) => {
console . error (
"Subscription or server communication failed:" ,
error ,
) ;
alert (
"Subscription or server communication failed. Try again in a while." ,
) ;
} ) ;
} )
. catch ( ( error ) => {
console . error (
"An error occurred setting notification permissions:" ,
error ,
) ;
alert ( "Some error occurred setting notification permissions." ) ;
} ) ;
}
private 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 ;
}
private subscribeToPush ( ) : Promise < void > {
return new Promise < void > ( ( resolve , reject ) => {
if ( ! ( "serviceWorker" in navigator && "PushManager" in window ) ) {
const errorMsg = "Push messaging is not supported" ;
console . warn ( errorMsg ) ;
return reject ( new Error ( errorMsg ) ) ;
}
if ( Notification . permission !== "granted" ) {
const errorMsg = "Notification permission not granted" ;
console . warn ( errorMsg ) ;
return reject ( new Error ( errorMsg ) ) ;
}
const applicationServerKey = this . urlBase64ToUint8Array ( this . b64 ) ;
const options : PushSubscriptionOptions = {
userVisibleOnly : true ,
applicationServerKey : applicationServerKey ,
} ;
navigator . serviceWorker . ready
. then ( ( registration ) => {
return registration . pushManager . subscribe ( options ) ;
} )
. then ( ( subscription ) => {
console . log ( "Push subscription successful:" , subscription ) ;
resolve ( ) ;
} )
. catch ( ( error ) => {
console . error ( "Push subscription failed:" , error , options ) ;
// Inform the user about the issue
alert (
"We encountered an issue setting up push notifications. " +
"If you wish to revoke notification permissions, please do so in your browser settings." ,
) ;
reject ( error ) ;
} ) ;
} ) ;
}
private sendSubscriptionToServer (
subscription : PushSubscription ,
) : Promise < void > {
console . log ( "About to send subscription..." , subscription ) ;
return fetch ( "/web-push/subscribe" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
} ,
body : JSON . stringify ( subscription ) ,
} ) . then ( ( response ) => {
if ( ! response . ok ) {
throw new Error ( "Failed to send subscription to server" ) ;
}
console . log ( "Subscription sent to server successfully." ) ;
} ) ;
}
async turnOffNotifications ( ) {
let subscription ;
const pushProviderSuccess = await navigator . serviceWorker . ready
. then ( ( registration ) => {
return registration . pushManager . getSubscription ( ) ;
} )
. then ( ( subscript ) => {
subscription = subscript ;
if ( subscription ) {
return subscription . unsubscribe ( ) ;
} else {
console . log ( "Subscription object is not available." ) ;
return false ;
}
} )
. catch ( ( error ) => {
console . log ( "Push provider server communication failed:" , error ) ;
return false ;
} ) ;
const pushServerSuccess = await fetch ( "/web-push/unsubscribe" , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
} ,
body : JSON . stringify ( subscription ) ,
} )
. then ( ( response ) => {
return response . ok ;
} )
. catch ( ( error ) => {
console . log ( "Push server communication failed:" , error ) ;
return false ;
} ) ;
alert (
"Notifications are off. Push provider unsubscribe " +
( pushProviderSuccess ? "succeeded" : "failed" ) +
( pushProviderSuccess === pushServerSuccess ? " and" : " but" ) +
" push server unsubscribe " +
( pushServerSuccess ? "succeeded" : "failed" ) +
"." ,
) ;
}
}
< / script >