postmortem 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/layout/layout.js CHANGED
@@ -1,17 +1,29 @@
1
1
  (function () {
2
+ if (POSTMORTEM && POSTMORTEM.downloadedPreview) document.title = 'PostMortem';
3
+
2
4
  let previousInbox;
3
5
  let currentView;
4
6
  let reloadIdentityIframeTimeout;
5
7
  let identityUuid;
6
8
  let indexUuid;
7
- let inboxInitialized = false;
8
-
9
- var htmlIframeDocument = document.querySelector("#html-iframe").contentDocument;
10
- var indexIframe = document.querySelector("#index-iframe");
11
- var identityIframe = document.querySelector("#identity-iframe");
12
- var textIframeDocument = document.querySelector("#text-iframe").contentDocument;
13
- var sourceIframeDocument = document.querySelector("#source-iframe").contentDocument;
14
- 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 = [
15
27
  '<link rel="stylesheet"',
16
28
  ' href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.18.3/styles/agate.min.css"',
17
29
  ' integrity="sha512-mMMMPADD4HAIogAWZbv+WjZTC0itafUZNI0jQm48PMBTApXt11omF5jhS7U1kp3R2Pr6oGJ+JwQKiUkUwCQaUQ=="',
@@ -22,52 +34,60 @@
22
34
  '<script>hljs.initHighlightingOnLoad();</' + 'script>',
23
35
  ].join('\n');
24
36
 
37
+ const storage = window.localStorage;
38
+
25
39
  const initialize = () => {
26
- loadMail(initialData);
27
-
28
- reloadIdentityIframeTimeout = setTimeout(() => identityIframe.src += '', 3000);
29
-
30
- window.addEventListener('message', function (ev) {
31
- switch (ev.data.type) {
32
- case 'index':
33
- renderInbox(ev.data.uuid, ev.data.mails);
34
- break;
35
- case 'identity':
36
- compareIdentity(ev.data.uuid);
37
- break;
38
- };
39
- });
40
+ reloadIdentityIframeTimeout = setTimeout(() => getIdentityIframe().src += '', 1000);
40
41
 
41
- setInterval(function () { indexIframe.contentWindow.postMessage('HELO', '*'); }, 1000);
42
- setInterval(function () { identityIframe.contentWindow.postMessage('HELO', '*'); }, 1000);
42
+ setInterval(function () { getIdentityIframe().contentWindow.postMessage('HELO', '*'); }, 200);
43
43
 
44
- toolbar.html.onclick = function (ev) { setView('html', ev); };
45
- toolbar.text.onclick = function (ev) { setView('text', ev); };
46
- toolbar.source.onclick = function (ev) { setView('source', ev); };
47
- toolbar.headers.onclick = function () { setHeadersView(!headersView); };
48
- 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();
49
51
 
50
- if (hasHtml) {
52
+ if (POSTMORTEM.hasHtml) {
51
53
  setView('html');
52
54
  } else {
53
55
  setView('text');
54
56
  }
55
57
 
56
- setColumnView(false);
58
+ setColumnView(!POSTMORTEM.downloadedPreview);
57
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
+ }
58
81
 
59
82
  $('[data-toggle="tooltip"]').tooltip();
60
83
  }
61
84
 
85
+ const requestLoad = (id) => {
86
+ requestedId = id;
87
+ requestedPending = true;
88
+ };
62
89
 
63
- var twoColumnView;
64
- var headersView;
65
- var headers = document.querySelector('.headers');
66
- var inbox = document.querySelector('#inbox');
67
- var columnSwitch = document.querySelector('.column-switch');
68
- var headersViewSwitch = document.querySelector('.headers-view-switch');
69
-
70
- var setHeadersView = function (enableHeadersView) {
90
+ const setHeadersView = (enableHeadersView) => {
71
91
  headersView = enableHeadersView;
72
92
  if (enableHeadersView) {
73
93
  setOn(headersViewSwitch);
@@ -78,11 +98,11 @@
78
98
  }
79
99
  };
80
100
 
81
- var setColumnView = function (enableTwoColumnView) {
82
- if (!inboxInitialized) return;
101
+ const setColumnView = (enableTwoColumnView) => {
102
+ if (!inbox) return;
83
103
 
84
- var container = document.querySelector('.container');
85
- twoColumnView = enableTwoColumnView;
104
+ const container = document.querySelector('.container');
105
+ twoColumnView = POSTMORTEM.downloadedPreview ? false : enableTwoColumnView;
86
106
  if (twoColumnView) {
87
107
  setVisible(inbox, true);
88
108
  setOn(columnSwitch);
@@ -94,42 +114,51 @@
94
114
  }
95
115
  };
96
116
 
97
- var contexts = ['source', 'text', 'html'];
117
+ const contexts = ['source', 'text', 'html'];
98
118
 
99
- var views = {
119
+ const views = {
100
120
  source: document.querySelector('.source-view'),
101
121
  html: document.querySelector('.html-view'),
102
122
  text: document.querySelector('.text-view'),
103
123
  };
104
124
 
105
- var toolbar = {
125
+ const toolbar = {
106
126
  source: document.querySelector('.source-view-switch'),
107
127
  html: document.querySelector('.html-view-switch'),
108
128
  text: document.querySelector('.text-view-switch'),
109
129
  headers: document.querySelector('.headers-view-switch'),
130
+ download: document.querySelector('#download-link'),
110
131
  };
111
132
 
112
- var setOn = function(element) {
133
+ const setOn = function(element) {
113
134
  element.classList.add('text-primary');
114
135
  element.classList.remove('text-secondary');
115
136
  };
116
137
 
117
- var setOff = function(element) {
138
+ const setOff = function(element) {
118
139
  element.classList.add('text-secondary');
119
140
  element.classList.remove('text-primary');
120
141
  };
121
142
 
122
- var setDisabled = function(element) {
143
+ const setDisabled = function(element) {
123
144
  element.classList.add('disabled');
124
145
  element.classList.remove('text-secondary');
125
146
  };
126
147
 
127
- var setEnabled = function(element) {
148
+ const setEnabled = function(element) {
128
149
  element.classList.remove('disabled');
129
150
  element.classList.add('text-secondary');
130
151
  };
131
152
 
132
- 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) {
133
162
  if (visible) {
134
163
  element.classList.add('visible');
135
164
  } else {
@@ -137,9 +166,9 @@
137
166
  }
138
167
  };
139
168
 
140
- var setView = function(context, ev) {
169
+ const setView = function(context, ev) {
141
170
  if (ev && $(ev.target).hasClass('disabled')) return;
142
- var key;
171
+ let key;
143
172
  for (i = 0; i < contexts.length; i++) {
144
173
  key = contexts[i];
145
174
  if (key === context) {
@@ -194,43 +223,211 @@
194
223
  setEnabled(toolbar.source);
195
224
  }
196
225
 
197
- setDisabled(columnSwitch);
198
226
  setView(currentView);
199
227
  };
200
228
 
201
- const loadMail = (mail) => {
202
- htmlIframeDocument.open();
203
- htmlIframeDocument.write(mail.htmlBody);
204
- htmlIframeDocument.close();
229
+ const setContent = (selector, content) => {
230
+ const target = document.querySelector(selector).contentDocument;
205
231
 
206
- textIframeDocument.open();
207
- textIframeDocument.write(`<pre>${htmlEscape(mail.textBody)}</pre>`);
208
- textIframeDocument.close();
232
+ target.open();
233
+ target.write(content);
234
+ target.close();
235
+ };
209
236
 
210
- sourceIframeDocument.open();
211
- sourceIframeDocument.write(`<pre><code style="padding: 1rem;" class="language-html">${htmlEscape(mail.htmlBody)}</code></pre>`);
212
- sourceIframeDocument.write(sourceHighlightBundle);
213
- 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);
214
253
 
215
254
  loadHeaders(mail);
216
255
  loadToolbar(mail);
217
- 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;
218
310
  };
219
311
 
220
- const loadDownloadLink = () => {
221
- 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' });
222
377
  const uri = window.URL.createObjectURL(blob);
223
378
  $("#download-link").attr('href', uri);
379
+ $("#download-link").attr('download', mail.subject.replace(/[^0-9a-zA-Z_ -]/gi, '') + '.html');
380
+ };
381
+
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
+ };
224
406
  };
225
407
 
226
408
  const compareIdentity = (uuid) => {
227
409
  clearTimeout(reloadIdentityIframeTimeout);
228
- reloadIdentityIframeTimeout = setTimeout(() => identityIframe.src += '', 3000);
229
- if (identityUuid !== uuid) indexIframe.src += '';
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
+ }
230
416
  identityUuid = uuid;
231
417
  };
232
418
 
233
- const renderInbox = (uuid, mails) => {
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)
234
431
  if (uuid === indexUuid) {
235
432
  return;
236
433
  }
@@ -239,28 +436,36 @@
239
436
  const parsedTimestamp = new Date(mail.timestamp);
240
437
  const timestampSpan = `<span class="timestamp">${parsedTimestamp.toLocaleString()}</span>`;
241
438
  const classes = ['list-group-item', 'inbox-item'];
439
+
242
440
  if (window.location.hash === '#' + mail.id) classes.push('active');
441
+
442
+ if (isNewMail(mail)) classes.push('unread');
443
+
243
444
  mailsById[mail.id] = mail;
244
- return `<li data-email-id="${mail.id}" class="${classes.join(' ')}"><a title="${mail.subject}" href="javascript:void(0)">${mail.subject}</a>${timestampSpan}</li>`
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('');
245
458
  });
459
+ updateInboxInfo();
246
460
  if (arrayIdentical(html, previousInbox)) return;
247
461
  previousInbox = html;
248
462
  $('#inbox').html('<ul class="list-group">' + html.join('\n') + '</ul>');
249
463
  $('.inbox-item').click((ev) => {
250
464
  const $target = $(ev.currentTarget);
251
465
  const id = $target.data('email-id');
252
- $('.inbox-item').removeClass('active');
253
- $target.addClass('active');
254
- window.location.hash = id;
255
466
  setTimeout(() => loadMail(mailsById[id].content), 0);
256
467
  });
257
468
 
258
- if (!inboxInitialized) {
259
- setEnabled(columnSwitch);
260
- setColumnView(true);
261
- setVisible(inbox, true);
262
- }
263
- inboxInitialized = true;
264
469
  indexUuid = uuid;
265
470
  };
266
471