-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathindex.js
188 lines (161 loc) · 5.69 KB
/
index.js
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
const contentContainer = document.querySelector('.content');
const confirmContainer = document.querySelector('#confirm');
const routes = [
{ path: /^\//, view: newsList },
{ path: /^news\/(\d+)/, view: newsDetail }
];
let currentNewsList = [];
async function router() {
const url = currentRoute();
const route = routes.find(r => r.path.test(url));
if (!route) {
return render(notFound());
}
const match = route.path.exec(url);
render(loading());
render(await route.view(...match));
clearConfirmDialog();
}
function render(htmlString) {
contentContainer.innerHTML = htmlString;
}
function currentRoute() {
return location.hash.slice(1) || '/';
}
function confirmUpdate(msg) {
const container = document.createElement('div');
const messageSpan = document.createElement('span');
const yes = document.createElement('span');
const no = document.createElement('span');
yes.appendChild(document.createTextNode('Yes please!'));
no.appendChild(document.createTextNode('No, let me be'));
yes.className = 'clickable';
no.className = 'clickable';
messageSpan.appendChild(document.createTextNode(msg));
container.appendChild(messageSpan);
container.appendChild(yes);
container.appendChild(no);
const onClick = resolve => () => {
container.remove();
resolve();
}
return new Promise(reslove => {
yes.addEventListener('click', onClick(() => reslove(true)));
no.addEventListener('click', onClick(() => reslove(false)));
confirmContainer.appendChild(container);
});
}
function clearConfirmDialog() {
// This may cause memory leaks in older browsers due to the event listeners
confirmContainer.innerHTML = '';
}
function createNewsNode(news) {
return `
<section>
<img src="${news.image}">
<article>
<a href="#news/${news.id}"><h2>${news.title}</h2></a>
<p>${news.text}</p>
</article>
</section>
`;
}
function isSameNewsList(a, b) {
if (a.length !== b.length) {
return false;
}
return a.every((article, i) => article.id === b[i].id);
}
async function fetchNews() {
const response = await fetch('https://happy-news-nmnepmqeqo.now.sh');
const newsList = await response.json();
currentNewsList = newsList;
return newsList;
}
async function detailNews(id) {
const response = await fetch(`https://happy-news-nmnepmqeqo.now.sh/${id}`);
return await response.json();
}
async function newsList() {
const news = await fetchNews();
return news.map(createNewsNode).join('');
}
async function newsDetail(_, id) {
try {
const newsFetch = detailNews(id);
const timeout = new Promise(resolve => setTimeout(resolve, 3000, new Error('timeout')));
const news = await Promise.race([newsFetch, timeout]).then(value => {
if (value instanceof Error) {
throw value;
}
return value;
});
return createNewsNode(news);
} catch (error) {
if (await registerSync('news-article-' + id)) {
return 'Could not fetch the article, I will try to download the article in the background and notify you when the article is ready 🎉';
} else {
return 'Could not fetch the article :/';
}
}
}
async function registerSync(tag) {
const hasNotificationPermission = await requestNotificationPermission();
if (!hasNotificationPermission) {
return false;
}
const reg = await navigator.serviceWorker.ready;
await reg.sync.register(tag);
return true;
}
function requestNotificationPermission() {
return new Promise(resolve => {
Notification.requestPermission(permission => resolve(permission === 'granted'));
});
}
function notFound() {
return '<p>404</p>';
}
function loading() {
return '<p>Loading... 🚀</p>';
}
['hashchange', 'load'].forEach(e => window.addEventListener(e, router));
// SW
const apiCacheName = 'api-cache-v1';
const applicationServerKey = urlB64ToUint8Array('BLKDIREFdJjk63LMAhjpwoBWPASDs1zQdKt5ovo-RFbiL839I4DoqM-pyk0WkBNKAGwyTfAc-QMBqsPjkWZWKMI');
async function fromCache(request, cacheName) {
const cache = await caches.open(cacheName);
return cache.match(request);
}
async function messageHandler({ type, url }) {
if (type === 'refresh-news-list' && currentRoute() === '/') {
const cachedData = await fromCache(url, apiCacheName);
if (!cachedData || !cachedData.ok) {
return;
}
const newsList = await cachedData.json();
const isNewList = !isSameNewsList(currentNewsList, newsList);
if (isNewList && await confirmUpdate('Sorry to bother you but do you want the latest news?')) {
render(newsList.map(createNewsNode).join(''));
}
}
}
navigator.serviceWorker.addEventListener('message', event => messageHandler(JSON.parse(event.data)));
function urlB64ToUint8Array(base64String) {
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;
}
async function getPushSubscription() {
const registration = await window.navigator.serviceWorker.ready;
let subscription = await registration.pushManager.getSubscription();
if (!subscription) {
subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey });
}
console.dir(JSON.stringify(subscription));
}