postmortem 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/layout/layout.js CHANGED
@@ -1,14 +1,29 @@
1
1
  (function () {
2
+ if (POSTMORTEM && POSTMORTEM.downloadedPreview) document.title = 'PostMortem';
3
+
2
4
  let previousInbox;
3
5
  let currentView;
6
+ let reloadIdentityIframeTimeout;
7
+ let identityUuid;
4
8
  let indexUuid;
5
- let inboxInitialized = false;
6
-
7
- var htmlIframeDocument = document.querySelector("#html-iframe").contentDocument;
8
- var indexIframe = document.querySelector("#index-iframe");
9
- var textIframeDocument = document.querySelector("#text-iframe").contentDocument;
10
- var sourceIframeDocument = document.querySelector("#source-iframe").contentDocument;
11
- var sourceHighlightBundle = [
9
+ let twoColumnView;
10
+ let headersView;
11
+ let indexIframeTimeout;
12
+ let requestedId;
13
+ let requestedPending = false;
14
+
15
+ const inboxContent = [];
16
+ const headers = document.querySelector('.headers');
17
+ const inbox = document.querySelector('#inbox-container');
18
+ const inboxInfo = document.querySelector('#inbox-info');
19
+ const columnSwitch = document.querySelector('.column-switch');
20
+ const headersViewSwitch = document.querySelector('.headers-view-switch');
21
+ const readAllButton = document.querySelector('.read-all-button');
22
+ const showHideReadButton = document.querySelector('.show-hide-read-button');
23
+ const showHideReadIcon = document.querySelector('.show-hide-read-icon');
24
+ const getIndexIframe = () => document.querySelector("#index-iframe");
25
+ const getIdentityIframe = () => document.querySelector("#identity-iframe");
26
+ const sourceHighlightBundle = [
12
27
  '<link rel="stylesheet"',
13
28
  ' href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/styles/agate.min.css"',
14
29
  ' integrity="sha512-mMMMPADD4HAIogAWZbv+WjZTC0itafUZNI0jQm48PMBTApXt11omF5jhS7U1kp3R2Pr6oGJ+JwQKiUkUwCQaUQ=="',
@@ -19,47 +34,60 @@
19
34
  '<script>hljs.initHighlightingOnLoad();</' + 'script>',
20
35
  ].join('\n');
21
36
 
22
- const initialize = () => {
23
- loadMail(initialData);
37
+ const storage = window.localStorage;
24
38
 
25
- let reloadIframeTimeout = setTimeout(() => indexIframe.src += '', 3000);
26
-
27
- window.addEventListener('message', function (ev) {
28
- clearTimeout(reloadIframeTimeout);
29
- reloadIframeTimeout = setTimeout(() => indexIframe.src += '', 3000);
30
- if (indexUuid !== ev.data.uuid) renderInbox(ev.data.mails);
31
- indexUuid = ev.data.uuid;
32
- });
39
+ const initialize = () => {
40
+ reloadIdentityIframeTimeout = setTimeout(() => getIdentityIframe().src += '', 1000);
33
41
 
34
- setInterval(function () { indexIframe.contentWindow.postMessage('HELO', '*'); }, 1000);
42
+ setInterval(function () { getIdentityIframe().contentWindow.postMessage('HELO', '*'); }, 200);
35
43
 
36
- toolbar.html.onclick = function (ev) { setView('html', ev); };
37
- toolbar.text.onclick = function (ev) { setView('text', ev); };
38
- toolbar.source.onclick = function (ev) { setView('source', ev); };
39
- toolbar.headers.onclick = function () { setHeadersView(!headersView); };
40
- columnSwitch.onclick = function () { setColumnView(!twoColumnView); };
44
+ toolbar.html.onclick = (ev) => setView('html', ev);
45
+ toolbar.text.onclick = (ev) => setView('text', ev);
46
+ toolbar.source.onclick = (ev) => setView('source', ev);
47
+ toolbar.headers.onclick = () => setHeadersView(!headersView);
48
+ columnSwitch.onclick = () => setColumnView(!twoColumnView);
49
+ readAllButton.onclick = () => markAllAsRead();
50
+ showHideReadButton.onclick = () => toggleHideReadMessages();
41
51
 
42
- if (hasHtml) {
52
+ if (POSTMORTEM.hasHtml) {
43
53
  setView('html');
44
54
  } else {
45
55
  setView('text');
46
56
  }
47
57
 
48
- setColumnView(false);
58
+ setColumnView(!POSTMORTEM.downloadedPreview);
49
59
  setHeadersView(true);
60
+ setEnabled(columnSwitch);
61
+ setOn(columnSwitch);
62
+ setHidden(inbox, POSTMORTEM.downloadedPreview);
63
+
64
+ if (POSTMORTEM.downloadedPreview) {
65
+ setHidden(toolbar.download, true);
66
+ setHidden(columnSwitch, true);
67
+ loadMail(POSTMORTEM.initialData);
68
+ } else {
69
+ requestLoad(window.location.hash.replace('#', ''));
70
+ window.addEventListener('message', function (ev) {
71
+ switch (ev.data.type) {
72
+ case 'index':
73
+ loadInbox(ev.data.uuid, ev.data.mails);
74
+ break;
75
+ case 'identity':
76
+ compareIdentity(ev.data.uuid);
77
+ break;
78
+ };
79
+ });
80
+ }
50
81
 
51
82
  $('[data-toggle="tooltip"]').tooltip();
52
83
  }
53
84
 
85
+ const requestLoad = (id) => {
86
+ requestedId = id;
87
+ requestedPending = true;
88
+ };
54
89
 
55
- var twoColumnView;
56
- var headersView;
57
- var headers = document.querySelector('.headers');
58
- var inbox = document.querySelector('#inbox');
59
- var columnSwitch = document.querySelector('.column-switch');
60
- var headersViewSwitch = document.querySelector('.headers-view-switch');
61
-
62
- var setHeadersView = function (enableHeadersView) {
90
+ const setHeadersView = (enableHeadersView) => {
63
91
  headersView = enableHeadersView;
64
92
  if (enableHeadersView) {
65
93
  setOn(headersViewSwitch);
@@ -70,11 +98,11 @@
70
98
  }
71
99
  };
72
100
 
73
- var setColumnView = function (enableTwoColumnView) {
74
- if (!inboxInitialized) return;
101
+ const setColumnView = (enableTwoColumnView) => {
102
+ if (!inbox) return;
75
103
 
76
- var container = document.querySelector('.container');
77
- twoColumnView = enableTwoColumnView;
104
+ const container = document.querySelector('.container');
105
+ twoColumnView = POSTMORTEM.downloadedPreview ? false : enableTwoColumnView;
78
106
  if (twoColumnView) {
79
107
  setVisible(inbox, true);
80
108
  setOn(columnSwitch);
@@ -86,42 +114,51 @@
86
114
  }
87
115
  };
88
116
 
89
- var contexts = ['source', 'text', 'html'];
117
+ const contexts = ['source', 'text', 'html'];
90
118
 
91
- var views = {
119
+ const views = {
92
120
  source: document.querySelector('.source-view'),
93
121
  html: document.querySelector('.html-view'),
94
122
  text: document.querySelector('.text-view'),
95
123
  };
96
124
 
97
- var toolbar = {
125
+ const toolbar = {
98
126
  source: document.querySelector('.source-view-switch'),
99
127
  html: document.querySelector('.html-view-switch'),
100
128
  text: document.querySelector('.text-view-switch'),
101
129
  headers: document.querySelector('.headers-view-switch'),
130
+ download: document.querySelector('#download-link'),
102
131
  };
103
132
 
104
- var setOn = function(element) {
133
+ const setOn = function(element) {
105
134
  element.classList.add('text-primary');
106
135
  element.classList.remove('text-secondary');
107
136
  };
108
137
 
109
- var setOff = function(element) {
138
+ const setOff = function(element) {
110
139
  element.classList.add('text-secondary');
111
140
  element.classList.remove('text-primary');
112
141
  };
113
142
 
114
- var setDisabled = function(element) {
143
+ const setDisabled = function(element) {
115
144
  element.classList.add('disabled');
116
145
  element.classList.remove('text-secondary');
117
146
  };
118
147
 
119
- var setEnabled = function(element) {
148
+ const setEnabled = function(element) {
120
149
  element.classList.remove('disabled');
121
150
  element.classList.add('text-secondary');
122
151
  };
123
152
 
124
- var setVisible = function(element, visible) {
153
+ const setHidden = function(element, hidden) {
154
+ if (hidden) {
155
+ element.classList.add('hidden');
156
+ } else {
157
+ element.classList.remove('hidden');
158
+ }
159
+ };
160
+
161
+ const setVisible = function(element, visible) {
125
162
  if (visible) {
126
163
  element.classList.add('visible');
127
164
  } else {
@@ -129,9 +166,9 @@
129
166
  }
130
167
  };
131
168
 
132
- var setView = function(context, ev) {
169
+ const setView = function(context, ev) {
133
170
  if (ev && $(ev.target).hasClass('disabled')) return;
134
- var key;
171
+ let key;
135
172
  for (i = 0; i < contexts.length; i++) {
136
173
  key = contexts[i];
137
174
  if (key === context) {
@@ -186,62 +223,250 @@
186
223
  setEnabled(toolbar.source);
187
224
  }
188
225
 
189
- setDisabled(columnSwitch);
190
226
  setView(currentView);
191
227
  };
192
228
 
193
- const loadMail = (mail) => {
194
- htmlIframeDocument.open();
195
- htmlIframeDocument.write(mail.htmlBody);
196
- htmlIframeDocument.close();
229
+ const setContent = (selector, content) => {
230
+ const target = document.querySelector(selector).contentDocument;
197
231
 
198
- textIframeDocument.open();
199
- textIframeDocument.write(`<pre>${htmlEscape(mail.textBody)}</pre>`);
200
- textIframeDocument.close();
232
+ target.open();
233
+ target.write(content);
234
+ target.close();
235
+ };
201
236
 
202
- sourceIframeDocument.open();
203
- sourceIframeDocument.write(`<pre><code style="padding: 1rem;" class="language-html">${htmlEscape(mail.htmlBody)}</code></pre>`);
204
- sourceIframeDocument.write(sourceHighlightBundle);
205
- sourceIframeDocument.close();
237
+ const loadMail = (mail) => {
238
+ const initializeScript = document.querySelector("#initialize-script");
239
+ const initObject = {
240
+ initialData: mail,
241
+ hasHtml: !!mail.htmlBody,
242
+ hasText: !!mail.textBody,
243
+ downloadedPreview: true,
244
+ };
245
+
246
+ initializeScript.text = [
247
+ `const POSTMORTEM = ${JSON.stringify(initObject)};`
248
+ ].join('\n\n');
249
+
250
+ setContent('#html-iframe', mail.htmlBody);
251
+ setContent('#text-iframe', `<pre>${htmlEscape(mail.textBody)}</pre>`);
252
+ setContent('#source-iframe', `<pre><code style="padding: 1rem;" class="language-html">${htmlEscape(mail.htmlBody)}</code></pre>` + sourceHighlightBundle);
206
253
 
207
254
  loadHeaders(mail);
208
255
  loadToolbar(mail);
209
- loadDownloadLink();
256
+ loadDownloadLink(mail);
257
+ loadUploadLink(mail);
258
+
259
+ highlightMail(mail);
260
+ markAsRead(mail);
261
+ updateInboxInfo();
262
+ };
263
+
264
+ const markAsRead = (mail) => {
265
+ storage.setItem(mail.id, 'read');
266
+ $(`li[data-email-id="${mail.id}"]`).removeClass('unread');
267
+ $(readAllButton).blur();
268
+ };
269
+
270
+ const showReadMessages = (show) => {
271
+ if (show) {
272
+ inbox.classList.remove('hide-read');
273
+ } else {
274
+ inbox.classList.add('hide-read');
275
+ }
276
+ };
277
+
278
+ const toggleHideReadMessages = () => {
279
+ const $target = $(showHideReadButton);
280
+ const $icon = $(showHideReadIcon);
281
+
282
+ if ($target.data('state') === 'hide') {
283
+ $target.data('state', 'show');
284
+ $target.attr('data-original-title', 'Hide read messages');
285
+ $target.attr('title', 'Hide read messages');
286
+ $icon.removeClass('fa-eye-slash');
287
+ $icon.addClass('fa fa-eye text-primary');
288
+ showReadMessages(true);
289
+ } else {
290
+ $target.data('state', 'hide');
291
+ $target.attr('data-original-title', 'Show read messages');
292
+ $target.attr('title', 'Show read messages');
293
+ $icon.removeClass('fa-eye text-primary');
294
+ $icon.addClass('fa fa-eye-slash');
295
+ showReadMessages(false);
296
+ }
297
+
298
+ $target.blur();
299
+ };
300
+
301
+ const markAllAsRead = () => {
302
+ inboxContent.forEach(mail => markAsRead(mail));
303
+ updateInboxInfo();
304
+ };
305
+
306
+ const isNewMail = (mail) => {
307
+ if (!storage.getItem(mail.id)) return true;
308
+
309
+ return false;
210
310
  };
211
311
 
212
- const loadDownloadLink = () => {
213
- const blob = new Blob([document.documentElement.innerHTML], { type: 'application/octet-stream' });
312
+ const highlightMail = (mail) => {
313
+ window.location.hash = mail.id;
314
+ const $target = $(`li[data-email-id="${mail.id}"]`);
315
+ $('.inbox-item').removeClass('active');
316
+ $target.addClass('active');
317
+ };
318
+
319
+ const alertUploadFailure = (response, content) => {
320
+ alert(`Upload failed. Got: ${response.status} ${response.statusText}: ${content}`);
321
+ };
322
+
323
+ const copyToClipboard = (text) => {
324
+ const textArea = document.createElement("textarea");
325
+ textArea.style.position = 'fixed';
326
+ textArea.style.top = 0;
327
+ textArea.style.left = 0;
328
+ textArea.style.width = '2em';
329
+ textArea.style.height = '2em';
330
+ textArea.style.padding = 0;
331
+ textArea.style.border = 'none';
332
+ textArea.style.outline = 'none';
333
+ textArea.style.boxShadow = 'none';
334
+ textArea.style.background = 'transparent';
335
+ textArea.value = text;
336
+ document.body.appendChild(textArea);
337
+ textArea.focus();
338
+ textArea.select();
339
+
340
+ let success;
341
+
342
+ try {
343
+ success = document.execCommand('copy');
344
+ } catch (err) {
345
+ console.log('Clipboard copy error');
346
+ }
347
+
348
+ document.body.removeChild(textArea);
349
+ return success;
350
+ };
351
+
352
+ const alertUploadSuccess = (data) => {
353
+ const popup = document.querySelector("#upload-popup");
354
+ const uploadedEmailLink = document.querySelector("#uploaded-email-link");
355
+ const copyUploadedEmailLink = document.querySelector("#copy-uploaded-email-link");
356
+ const url = `https://postmortem.delivery/${data.uri}`;
357
+
358
+ uploadedEmailLink.textContent = `postmortem.delivery/${data.uri}`;
359
+ uploadedEmailLink.href = url;
360
+ copyUploadedEmailLink.onclick = (ev) => {
361
+ ev.stopPropagation();
362
+ ev.preventDefault();
363
+ const success = copyToClipboard(url);
364
+ console.log(success);
365
+ };
366
+
367
+ popup.classList.remove("hidden");
368
+ popup.classList.add("fade-in");
369
+ };
370
+
371
+ const loadDownloadLink = (mail) => {
372
+ const html = document.documentElement.innerHTML;
373
+ const start = html.indexOf('<!--INBOX-START-->');
374
+ const end = html.indexOf('<!--INBOX-END-->') + '<!--INBOX-END-->'.length;
375
+ const modifiedHtml = [html.substring(0, start), html.substring(end + 1, html.length)].join('');
376
+ const blob = new Blob([modifiedHtml], { type: 'application/octet-stream' });
214
377
  const uri = window.URL.createObjectURL(blob);
215
378
  $("#download-link").attr('href', uri);
379
+ $("#download-link").attr('download', mail.subject.replace(/[^0-9a-zA-Z_ -]/gi, '') + '.html');
216
380
  };
217
381
 
218
- const renderInbox = function (mails) {
219
- const html = mails.map((mail, invertedIndex) => {
220
- const index = (mails.length - 1) - invertedIndex;
382
+ const loadUploadLink = (email) => {
383
+ const link = document.querySelector("#upload-link");
384
+
385
+ link.onclick = async (ev) => {
386
+ ev.stopPropagation();
387
+ ev.preventDefault();
388
+
389
+ const response = await fetch(POSTMORTEM.uploadUrl, {
390
+ method: 'POST',
391
+ cache: 'no-cache',
392
+ headers: { 'Content-Type': 'application/json' },
393
+ body: JSON.stringify({ email })
394
+ });
395
+
396
+ if (response.ok) {
397
+ const data = await response.json();
398
+ alertUploadSuccess(data);
399
+ } else {
400
+ const content = await response.text();
401
+ alertUploadFailure(response, content);
402
+ }
403
+
404
+ return false;
405
+ };
406
+ };
407
+
408
+ const compareIdentity = (uuid) => {
409
+ clearTimeout(reloadIdentityIframeTimeout);
410
+ reloadIdentityIframeTimeout = setTimeout(() => getIdentityIframe().src += '', 1000);
411
+ if (identityUuid !== uuid) {
412
+ getIndexIframe().src += '';
413
+ clearTimeout(indexIframeTimeout);
414
+ indexIframeTimeout = setInterval(function () { getIndexIframe().contentWindow.postMessage('HELO', '*'); }, 200);
415
+ }
416
+ identityUuid = uuid;
417
+ };
418
+
419
+ const updateInboxInfo = () => {
420
+ if (!inboxContent.length) return;
421
+
422
+ const unreadCount = inboxContent.filter((mail) => isNewMail(mail)).length;
423
+ document.title = `PostMortem ${unreadCount}/${inboxContent.length} (unread/total)`;
424
+ inboxInfo.textContent = `${inboxContent.length} emails (${unreadCount} unread)`;
425
+ inboxInfo.innerHTML = `&mdash; ${inboxInfo.innerHTML}`;
426
+ };
427
+
428
+ const loadInbox = (uuid, mails) => {
429
+ clearTimeout(indexIframeTimeout);
430
+ inboxContent.splice(0, Infinity, ...mails)
431
+ if (uuid === indexUuid) {
432
+ return;
433
+ }
434
+ const mailsById = {};
435
+ const html = mails.map((mail, index) => {
221
436
  const parsedTimestamp = new Date(mail.timestamp);
222
437
  const timestampSpan = `<span class="timestamp">${parsedTimestamp.toLocaleString()}</span>`;
223
438
  const classes = ['list-group-item', 'inbox-item'];
224
- if (window.location.hash === '#' + index) classes.push('active');
225
- return `<li data-email-index="${index}" class="${classes.join(' ')}"><a title="${mail.subject}" href="javascript:void(0)">${mail.subject}</a>${timestampSpan}</li>`
439
+
440
+ if (window.location.hash === '#' + mail.id) classes.push('active');
441
+
442
+ if (isNewMail(mail)) classes.push('unread');
443
+
444
+ mailsById[mail.id] = mail;
445
+
446
+ if (requestedPending && mail.id === requestedId) {
447
+ requestedPending = false;
448
+ requestedId = null;
449
+ setTimeout(() => loadMail(mail.content), 0);
450
+ }
451
+
452
+ return [`<li data-email-id="${mail.id}" class="${classes.join(' ')}">`,
453
+ `<a title="${mail.subject}" href="javascript:void(0)">`,
454
+ `<i class="fa fa-envelope-open read-icon"></i>`,
455
+ `<i class="fa fa-envelope unread-icon"></i>${mail.subject}`,
456
+ `</a>`,
457
+ `${timestampSpan}</li>`].join('');
226
458
  });
459
+ updateInboxInfo();
227
460
  if (arrayIdentical(html, previousInbox)) return;
228
461
  previousInbox = html;
229
462
  $('#inbox').html('<ul class="list-group">' + html.join('\n') + '</ul>');
230
463
  $('.inbox-item').click((ev) => {
231
464
  const $target = $(ev.currentTarget);
232
- const index = $target.data('email-index');
233
- $('.inbox-item').removeClass('active');
234
- $target.addClass('active');
235
- window.location.hash = index;
236
- setTimeout(() => loadMail(mails[index].content), 0);
465
+ const id = $target.data('email-id');
466
+ setTimeout(() => loadMail(mailsById[id].content), 0);
237
467
  });
238
468
 
239
- if (!inboxInitialized) {
240
- setEnabled(columnSwitch);
241
- setColumnView(true);
242
- setVisible(inbox, true);
243
- }
244
- inboxInitialized = true;
469
+ indexUuid = uuid;
245
470
  };
246
471
 
247
472
  initialize();