1// NowTempMail Service Worker - Minimal PWA Support + Email Notifications 2const CACHE_NAME = 'nowtempmail-v3'; 3 4// Offline fallback copy. 5// 6// A service worker has no access to the i18n runtime, so this text cannot 7// come from the catalog and lives here instead, keyed by the locale prefix in 8// the request URL. It was previously a hardcoded English string with no lang 9// attribute, which would have stayed English on every localised page. 10// Adding a locale is one entry here plus the matching sw.offline.* keys. 11const OFFLINE_COPY = { 12 en: { 13 lang: 'en', 14 title: 'Offline', 15 body: 'Please check your internet connection.', 16 }, 17}; 18 19function offlineResponse(url) { 20 let locale = "en"; 21 try { 22 const first = new URL(url).pathname.split("/")[1]; 23 if (OFFLINE_COPY[first]) locale = first; 24 } catch (e) { 25 // Malformed URL: fall through to the default locale. 26 } 27 const copy = OFFLINE_COPY[locale]; 28 const html = 29 "<!DOCTYPE html><html lang=" + JSON.stringify(copy.lang) + ">" + 30 "<head><meta charset=utf-8><meta name=viewport " + 31 "content=\"width=device-width, initial-scale=1\"><title>" + copy.title + 32 "</title></head><body><h1>" + copy.title + "</h1><p>" + copy.body + 33 "</p></body></html>"; 34 return new Response(html, { 35 status: 503, 36 headers: { 'Content-Type': 'text/html; charset=utf-8' }, 37 }); 38} 39 40// Notification state (synced from main page via postMessage) 41let currentMailbox = null; 42let knownEmailIds = new Set(); 43 44const STATIC_ASSETS = [ 45 '/images/android-chrome-192x192.png', 46 '/images/android-chrome-512x512.png', 47 '/images/favicon-32x32.png', 48 '/images/favicon-16x16.png', 49 '/logo.png' 50]; 51 52// Install event - cache static assets 53self.addEventListener('install', (event) => { 54 event.waitUntil( 55 caches.open(CACHE_NAME) 56 .then((cache) => { 57 return cache.addAll(STATIC_ASSETS); 58 }) 59 .catch((error) => { 60 console.warn('Service Worker: Failed to cache assets during install', error); 61 }) 62 ); 63 64 // Skip waiting to activate immediately 65 self.skipWaiting(); 66}); 67 68// Activate event - clean up old caches 69self.addEventListener('activate', (event) => { 70 event.waitUntil( 71 caches.keys().then((cacheNames) => { 72 return Promise.all( 73 cacheNames.map((cacheName) => { 74 if (cacheName !== CACHE_NAME) { 75 return caches.delete(cacheName); 76 } 77 }) 78 ); 79 }) 80 ); 81 82 // Take control of all clients immediately 83 self.clients.claim(); 84}); 85 86// Fetch event - network first with cache fallback for static assets 87self.addEventListener('fetch', (event) => { 88 const { request } = event; 89 90 // Skip non-GET requests 91 if (request.method !== 'GET') { 92 return; 93 } 94 95 // Skip external requests 96 if (!request.url.startsWith(self.location.origin)) { 97 return; 98 } 99 100 // Navigation requests (HTML pages) - always network first to ensure latest JS/CSS 101 if (request.mode === 'navigate') { 102 event.respondWith( 103 fetch(request) 104 .catch(() => offlineResponse(request.url)) 105 ); 106 return; 107 } 108 109 // For static image assets, try cache first, then network 110 if (STATIC_ASSETS.some(asset => request.url.endsWith(asset))) { 111 event.respondWith( 112 caches.match(request) 113 .then((cachedResponse) => { 114 if (cachedResponse) { 115 return cachedResponse; 116 } 117 118 return fetch(request) 119 .then((networkResponse) => { 120 // Cache the network response for future use 121 if (networkResponse.status === 200) { 122 const responseClone = networkResponse.clone(); 123 caches.open(CACHE_NAME) 124 .then((cache) => { 125 cache.put(request, responseClone); 126 }); 127 } 128 return networkResponse; 129 }); 130 }) 131 .catch(() => { 132 // Static asset failed - return nothing 133 return undefined; 134 }) 135 ); 136 return; 137 } 138 139 // Everything else (dynamic API calls like /messages, /mailbox, /message/*, 140 // plus JS/CSS bundles) is intentionally NOT intercepted â it goes straight to 141 // the network. The SW added no caching value for these, only overhead and a 142 // confusing duplicate entry in devtools. No respondWith => default handling. 143 // (Notifications are unaffected: they run through the `message` handler below, 144 // driven by the main thread, not through this fetch path.) 145}); 146 147// Handle messages from the main page (session sync + new email notifications) 148self.addEventListener('message', (event) => { 149 const { data } = event; 150 151 if (data.type === 'SET_SESSION') { 152 currentMailbox = data.mailbox; 153 knownEmailIds.clear(); 154 } 155 156 if (data.type === 'SHOW_NOTIFICATION' && data.title) { 157 event.waitUntil( 158 self.registration.showNotification(data.title, data.options || {}) 159 ); 160 } 161}); 162 163// Handle background sync (optional - for future email notifications) 164self.addEventListener('sync', (event) => { 165 if (event.tag === 'background-sync') { 166 // Future: implement background email checking
167 console.log('Background sync triggered'); 168 } 169}); 170 171// Handle push notifications (optional - for future email notifications) 172self.addEventListener('push', (event) => { 173 if (event.data) { 174 const data = event.data.json(); 175 const options = { 176 body: data.body || 'You have a new temporary email', 177 icon: '/images/android-chrome-192x192.png', 178 badge: '/images/favicon-32x32.png', 179 tag: 'email-notification', 180 requireInteraction: false, 181 actions: [ 182 { 183 action: 'view', 184 title: 'View Email' 185 }, 186 { 187 action: 'dismiss', 188 title: 'Dismiss' 189 } 190 ] 191 }; 192 193 event.waitUntil( 194 self.registration.showNotification(data.title || 'New Email', options) 195 ); 196 } 197}); 198 199// Handle notification clicks 200self.addEventListener('notificationclick', (event) => { 201 event.notification.close(); 202 203 const targetUrl = event.notification.data?.url || '/'; 204 205 event.waitUntil( 206 clients.matchAll({ type: 'window', includeUncontrolled: true }).then(windowClients => { 207 // Focus existing tab if available 208 for (const client of windowClients) { 209 if (client.url.includes(self.location.origin)) { 210 return client.focus().then(c => c.navigate(targetUrl)); 211 } 212 } 213 // Otherwise open new window 214 return clients.openWindow(targetUrl); 215 }) 216 ); 217}); 218
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.