< template >
< router -view / >
<!-- Messages in the upper - right - 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 >
<!--
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-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"
>
<!-- 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"
@ click = "
notification . onYes ( ) ;
close ( notification . id ) ;
"
class = "block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
Yes
{ { notification . yesText ? ", " + notification . yesText : "" } }
< / button >
< button
v - if = "notification.onNo"
@ click = "
notification . onNo ( stopAsking ) ;
close ( notification . id ) ;
stopAsking = false ; // reset value
"
class = "block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
>
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
type = "checkbox"
v - model = "stopAsking"
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
@ click = "
notification . onCancel
? notification . onCancel ( stopAsking )
: null ;
close ( notification . id ) ;
stopAsking = false ; // reset value
"
class = "block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
{ { notification . onYes ? "Cancel" : "Close" } }
< / button >
< / div >
< / div >
< / div >
< 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 be notified of new activity once a day ?
< / 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 >
< div v-if ="serviceWorkerReady" >
< span class = "flex flex-row justify-center" >
< span class = "mt-2" > Yes , tell me at : < / span >
< input
type = "number"
class = "rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20"
v - model = "hourInput"
/ >
< span
class = "rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20"
@ click = "hourAm = !hourAm"
>
< span v-if ="hourAm" > AM < fa icon = "chevron-down" / > < / span >
< span v-else > PM < fa icon = "chevron-up" / > < / span >
< / span >
< / span >
< button
class = "block w-full text-center text-md font-bold uppercase bg-blue-600 text-white mt-2 px-2 py-2 rounded-md"
@ click = "
( ) => {
if ( checkHour ( ) ) {
close ( notification . id ) ;
turnOnNotifications ( ) ;
}
}
"
>
Turn on Daily Message
< / button >
< / div >
< button
@ click = "close(notification.id)"
class = "block w-full text-center text-md font-bold uppercase bg-slate-600 text-white mt-4 px-2 py-2 rounded-md"
>
No , Not Now
< / 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 axios from "axios" ;
import { Vue , Component } from "vue-facing-decorator" ;
import * as libsUtil from "@/libs/util" ;
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 ;
} ;
}
interface PushSubscriptionWithTime extends PushSubscriptionJSON {
notifyTime : { utcHour : number } ;
}
import { DEFAULT_PUSH_SERVER , NotificationIface } from "@/constants/app" ;
import { db } from "@/db/index" ;
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings" ;
import { sendTestThroughPushServer } from "@/libs/util" ;
@ Component
export default class App extends Vue {
$notify ! : ( notification : NotificationIface , timeout ? : number ) => void ;
stopAsking = false ;
b64 = "" ;
hourAm = true ;
hourInput = "8" ;
serviceWorkerReady = true ;
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 ;
}
if ( pushUrl . startsWith ( "http://localhost" ) ) {
console . log ( "Not checking for VAPID in this local environment." ) ;
} else {
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 ;
} ) ;
}
// this allows us to show an error without closing the dialog
checkHour ( ) {
if ( ! libsUtil . isNumeric ( this . hourInput ) ) {
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Not a Number" ,
text : "The time must be an hour number." ,
} ,
5000 ,
) ;
return false ;
}
const hourNum = libsUtil . numberOrZero ( this . hourInput ) ;
if ( ! Number . isInteger ( hourNum ) ) {
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Not a Whole Number" ,
text : "The time must be a whole hour number." ,
} ,
5000 ,
) ;
return false ;
}
if ( hourNum < 1 || 12 < hourNum ) {
this . $notify (
{
group : "alert" ,
type : "danger" ,
title : "Not a Whole Number" ,
text : "The time must be an hour between 1 and 12." ,
} ,
5000 ,
) ;
return false ;
}
return true ;
}
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 ,
) ;
// we already checked that this is a valid hour number
const rawHourNum = libsUtil . numberOrZero ( this . hourInput ) ;
const adjHourNum = rawHourNum + ( this . hourAm ? 0 : 12 ) ;
const hourNum = adjHourNum % 24 ;
const utcHour =
hourNum + Math . round ( new Date ( ) . getTimezoneOffset ( ) / 60 ) ;
const finalUtcHour = ( utcHour + ( utcHour < 0 ? 24 : 0 ) ) % 24 ;
const subscriptionWithTime : PushSubscriptionWithTime = {
notifyTime : { utcHour : finalUtcHour } ,
... subscription . toJSON ( ) ,
} ;
await this . sendSubscriptionToServer ( subscriptionWithTime ) ;
return subscriptionWithTime ;
} else {
throw new Error ( "Subscription object is not available." ) ;
}
} )
. then ( async ( subscription : PushSubscriptionWithTime ) => {
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 : PushSubscriptionWithTime ,
) : 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 . error ( "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 . error ( "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 >