postmortem 0.2.3 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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