postmortem 0.2.1 → 0.3.0

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,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();