-
Notifications
You must be signed in to change notification settings - Fork 239
Expand file tree
/
Copy pathservice-worker.js
More file actions
255 lines (231 loc) · 7.64 KB
/
Copy pathservice-worker.js
File metadata and controls
255 lines (231 loc) · 7.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
// Must be located at the root.
importScripts('https://cdn.jsdelivr.net/npm/firebase@9.14.0/firebase-app-compat.js');
importScripts('https://cdn.jsdelivr.net/npm/firebase@9.14.0/firebase-messaging-compat.js');
importScripts('firebase-init.js');
importScripts('version.js');
// Channel to notify the webapp. There is no BroadcastChannel in Safari.
const webAppChannel = (typeof BroadcastChannel == 'function') ?
new BroadcastChannel('tinode-sw') : null;
// Basic internationalization.
const i18n = {
'ar': {
'new_message': "رسالة جديدة",
'new_chat': "محادثة جديدة",
},
'de': {
'new_message': "Neue Nachricht",
'new_chat': "Neuer Chat",
},
'en': {
'new_message': "New message",
'new_chat': "New chat",
},
'fr': {
'new_message': "Nouveau message",
'new_chat': "Nouvelle conversation",
},
'id': {
'new_message': "Pesan baru",
'new_chat': "Chat baru",
},
'es': {
'new_message': "Nuevo mensaje",
'new_chat': "Nueva conversación",
},
'it': {
'new_message': "Nuovo Messaggio",
'new_chat': "Nuova Chat",
},
'ko': {
'new_message': "새로운 메시지",
'new_chat': "새로운 채팅",
},
'pt': {
'new_message': "Nova Mensagem",
'new_chat': "Nova Conversa",
},
'ro': {
'new_message': "Mesaj nou",
'new_chat': "Chat nou",
},
'ru': {
'new_message': "Новое сообщение",
'new_chat': "Новый чат",
},
'th': {
'new_message': "ข้อความใหม่",
'new_chat': "แชทใหม่",
},
'uk': {
'new_message': "Нове повідомлення",
'new_chat': "Новий чат",
},
'vi': {
'new_message': "Tin nhắn mới",
'new_chat': "Cuộc trò chuyện mới",
},
'zh': {
'new_message': "新讯息",
'new_chat': "新聊天",
},
'zh-TW': {
'new_message': "新訊息",
'new_chat': "新聊天",
}
};
self.i18nMessage = function (id) {
if (!id) {
return null;
}
// Choose translations: given something like 'de-CH', try 'de-CH' then 'de' then 'en'.
const lang = i18n[self.locale] || i18n[self.baseLocale] || i18n['en'];
// Try finding string by id in the specified language, if missing try English, otherwise use the id itself
// as the last resort.
return lang[id] || i18n['en'][id] || id;
}
firebase.initializeApp(FIREBASE_INIT);
const fbMessaging = firebase.messaging();
// This method shows the push notifications while the window is in background.
fbMessaging.onBackgroundMessage(payload => {
// Notify webapp that a message was received.
if (webAppChannel) {
webAppChannel.postMessage(payload.data);
}
if (payload.data.silent == 'true') {
// No need to show anything.
return;
}
const titles = { 'msg': 'new_message', 'sub': 'new_chat' };
const pushType = payload.data.what || 'msg';
const title = payload.data.title || self.i18nMessage(titles[pushType]);
if (title) {
const options = {
body: payload.data.content || '', // TODO: content for 'sub' should be topic's or user's title.
icon: '/img/logo96.png', // TODO: use topic's or user's avatar (would have to fetch for 'sub', read from db for 'msg').
badge: '/img/badge96.png',
tag: payload.data.topic || undefined,
data: {
topic: payload.data.topic
}
};
return self.registration.showNotification(title, options);
}
});
// Update service worker immediately for both the current client
// and all other active clients.
self.addEventListener('install', _ => {
self.skipWaiting();
});
self.addEventListener('activate', event => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(keys.filter(key => key != PACKAGE_VERSION).map(key => caches.delete(key)));
await self.clients.claim();
})());
});
// This code handles a click on notification: takes
// the user to the browser tab with the chat or opens a new tab.
self.addEventListener('notificationclick', event => {
const data = event.notification.data;
event.notification.close();
if (!data) {
console.info("Missing 'data' in notification", event.notification);
return;
}
const urlHash = '#/' + data.topic;
event.waitUntil(self.clients.matchAll({
type: 'window',
includeUncontrolled: true
})
.then(windowClients => {
let anyClient = null;
for (let i = 0; i < windowClients.length; i++) {
const url = new URL(windowClients[i].url);
if (url.hash.includes(data.topic)) {
// Found the Tinode tab with the right topic open.
return windowClients[i].focus();
} else {
// This will be the least recently used tab.
anyClient = windowClients[i];
}
}
// Found tab with Tinode on a different topic,
// navigate to the right topic.
if (anyClient) {
const url = new URL(anyClient.url);
url.hash = urlHash;
return anyClient.focus().then(thisClient => {
return thisClient.navigate(url);
});
}
// Did not find a Tinode browser tab. Open one.
const url = new URL(self.location.origin);
url.hash = urlHash;
return clients.openWindow(url);
}));
});
// This is needed for 'Add to Home Screen'.
self.addEventListener('fetch', event => {
if (event.request.method != 'GET') {
return;
}
// Bypass caching for the service worker file itself to prevent update loops.
if (event.request.url.includes('service-worker.js')) {
return;
}
event.respondWith((async () => {
const cache = await caches.open(PACKAGE_VERSION);
const reqUrl = new URL(event.request.url);
// Cache handling strategy: for navigation requests (index.html, index-dev.html)
// use network-first strategy, for all other requests use cache-first strategy.
if (event.request.mode == 'navigate') {
// Network-First Strategy
return fetchAndCache(event.request, cache, reqUrl)
.catch(() => getFromCache(event.request, cache, reqUrl))
.catch(() => new Response('Offline text fallback', { status: 503 }));
} else {
// Cache-First Strategy
return getFromCache(event.request, cache, reqUrl)
.catch(() => fetchAndCache(event.request, cache, reqUrl))
.catch(() => new Response('', { status: 404 }));
}
})());
});
/**
* Read from cache. Throw an error if item is missing (cache miss).
*/
async function getFromCache(request, cache, reqUrl) {
// Using ignoreSearch=true to read cached attachments despite different auth signatures.
const match = await cache.match(request, { ignoreSearch: (self.location.origin == reqUrl.origin) });
if (!match) {
throw new Error('Cache miss'); // Rejects the promise to trigger the .catch() block
}
return match;
}
/**
* Fetch from network and silently save copy to a specific cache bucket.
*/
async function fetchAndCache(request, cache, reqUrl) {
const response = await fetch(request);
if (!response || response.status != 200 || response.type != 'basic') {
return response;
}
if (reqUrl && (reqUrl.protocol == 'http:' || reqUrl.protocol == 'https:')) {
await cache.put(request, response.clone());
}
return response;
}
// This code gets the human language from the webapp.
self.addEventListener('message', event => {
const data = (typeof event.data == 'string') ?
JSON.parse(event.data) : event.data;
if (data.type == 'clear-caches') {
event.waitUntil(
caches.keys().then(keys => Promise.all(keys.map(key => caches.delete(key))))
);
} else if (data.type == 'config') {
// The locale is used for selecting strings in an appropriate language.
self.locale = data.locale || '';
self.baseLocale = self.locale.toLowerCase().split(/[-_]/)[0];
}
});