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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +4 -46
- data/.ruby-version +1 -1
- data/Gemfile +0 -2
- data/Makefile +4 -0
- data/README.md +20 -12
- data/layout/default.html.erb +53 -13
- data/layout/dependencies.css +0 -4
- data/layout/dependencies.js +3 -1
- data/layout/favicon.b64 +1 -0
- data/layout/layout.css +156 -16
- data/layout/layout.js +303 -78
- data/layout/postmortem_identity.html.erb +20 -0
- data/layout/{index.html.erb → postmortem_index.html.erb} +3 -6
- data/lib/postmortem.rb +5 -2
- data/lib/postmortem/adapters.rb +1 -1
- data/lib/postmortem/adapters/action_mailer.rb +6 -41
- data/lib/postmortem/adapters/base.rb +35 -2
- data/lib/postmortem/adapters/mail.rb +3 -3
- data/lib/postmortem/adapters/pony.rb +3 -6
- data/lib/postmortem/delivery.rb +11 -9
- data/lib/postmortem/identity.rb +20 -0
- data/lib/postmortem/index.rb +12 -4
- data/lib/postmortem/layout.rb +8 -0
- data/lib/postmortem/plugins/action_mailer.rb +7 -0
- data/lib/postmortem/version.rb +1 -1
- data/postmortem.gemspec +6 -3
- metadata +52 -8
- data/doc/screenshot.png +0 -0
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
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
23
|
-
loadMail(initialData);
|
37
|
+
const storage = window.localStorage;
|
24
38
|
|
25
|
-
|
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 () {
|
42
|
+
setInterval(function () { getIdentityIframe().contentWindow.postMessage('HELO', '*'); }, 200);
|
35
43
|
|
36
|
-
toolbar.html.onclick
|
37
|
-
toolbar.text.onclick
|
38
|
-
toolbar.source.onclick
|
39
|
-
toolbar.headers.onclick
|
40
|
-
columnSwitch.onclick
|
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(
|
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
|
-
|
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
|
-
|
74
|
-
if (!
|
101
|
+
const setColumnView = (enableTwoColumnView) => {
|
102
|
+
if (!inbox) return;
|
75
103
|
|
76
|
-
|
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
|
-
|
117
|
+
const contexts = ['source', 'text', 'html'];
|
90
118
|
|
91
|
-
|
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
|
-
|
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
|
-
|
133
|
+
const setOn = function(element) {
|
105
134
|
element.classList.add('text-primary');
|
106
135
|
element.classList.remove('text-secondary');
|
107
136
|
};
|
108
137
|
|
109
|
-
|
138
|
+
const setOff = function(element) {
|
110
139
|
element.classList.add('text-secondary');
|
111
140
|
element.classList.remove('text-primary');
|
112
141
|
};
|
113
142
|
|
114
|
-
|
143
|
+
const setDisabled = function(element) {
|
115
144
|
element.classList.add('disabled');
|
116
145
|
element.classList.remove('text-secondary');
|
117
146
|
};
|
118
147
|
|
119
|
-
|
148
|
+
const setEnabled = function(element) {
|
120
149
|
element.classList.remove('disabled');
|
121
150
|
element.classList.add('text-secondary');
|
122
151
|
};
|
123
152
|
|
124
|
-
|
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
|
-
|
169
|
+
const setView = function(context, ev) {
|
133
170
|
if (ev && $(ev.target).hasClass('disabled')) return;
|
134
|
-
|
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
|
194
|
-
|
195
|
-
htmlIframeDocument.write(mail.htmlBody);
|
196
|
-
htmlIframeDocument.close();
|
229
|
+
const setContent = (selector, content) => {
|
230
|
+
const target = document.querySelector(selector).contentDocument;
|
197
231
|
|
198
|
-
|
199
|
-
|
200
|
-
|
232
|
+
target.open();
|
233
|
+
target.write(content);
|
234
|
+
target.close();
|
235
|
+
};
|
201
236
|
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
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
|
213
|
-
|
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
|
219
|
-
const
|
220
|
-
|
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 = `— ${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
|
-
|
225
|
-
|
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
|
233
|
-
|
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
|
-
|
240
|
-
setEnabled(columnSwitch);
|
241
|
-
setColumnView(true);
|
242
|
-
setVisible(inbox, true);
|
243
|
-
}
|
244
|
-
inboxInitialized = true;
|
469
|
+
indexUuid = uuid;
|
245
470
|
};
|
246
471
|
|
247
472
|
initialize();
|