discord-transcript-ui 1.0.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.
@@ -0,0 +1,788 @@
1
+ /*!
2
+ * discord-transcript-ui v1.0.0 — enhancement layer
3
+ * MIT licensed. No dependencies. Light DOM only.
4
+ *
5
+ * This file is optional: the stylesheet + static HTML already render a readable
6
+ * transcript. The script only adds things CSS cannot express — avatars and author
7
+ * headers from the profile map, formatted timestamps, spoiler/attachment
8
+ * interaction, embed chrome built from attributes, and lazy media.
9
+ *
10
+ * Config (define it any time before DOMContentLoaded):
11
+ * window.$discordMessage = {
12
+ * profiles: { skyra: { author: 'Skyra', avatar: 'https://…', roleColor: '#ff0000', bot: true } },
13
+ * avatars: { default: 'blue', blue: 'https://cdn.discordapp.com/embed/avatars/0.png' }
14
+ * };
15
+ * window.discordTranscript = { groupWindow: 7, locale: 'en-US', observe: true };
16
+ */
17
+
18
+ (function () {
19
+ 'use strict';
20
+
21
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
22
+ if (window.DiscordTranscript && window.DiscordTranscript.version) return;
23
+
24
+ var VERSION = '1.0.0';
25
+ var GROUP_WINDOW_MS = 7 * 60 * 1000;
26
+
27
+ var DEFAULT_AVATARS = {
28
+ blue: 'https://cdn.discordapp.com/embed/avatars/0.png',
29
+ gray: 'https://cdn.discordapp.com/embed/avatars/1.png',
30
+ green: 'https://cdn.discordapp.com/embed/avatars/2.png',
31
+ orange: 'https://cdn.discordapp.com/embed/avatars/3.png',
32
+ red: 'https://cdn.discordapp.com/embed/avatars/4.png',
33
+ pink: 'https://cdn.discordapp.com/embed/avatars/5.png'
34
+ };
35
+
36
+ /* Initial colours for authors with no avatar, so archived transcripts never
37
+ depend on a remote image to render a distinguishable gutter. */
38
+ var INITIAL_COLORS = [
39
+ '#5865f2',
40
+ '#3ba55c',
41
+ '#faa81a',
42
+ '#ed4245',
43
+ '#eb459e',
44
+ '#00a8fc',
45
+ '#9b59b6',
46
+ '#1abc9c'
47
+ ];
48
+
49
+ var relativeTimers = [];
50
+ var relativeElements = [];
51
+
52
+ /* ---------------------------------------------------------------- config */
53
+
54
+ function config() {
55
+ return (window.$discordMessage = window.$discordMessage || {});
56
+ }
57
+
58
+ function options() {
59
+ return (window.discordTranscript = window.discordTranscript || {});
60
+ }
61
+
62
+ function avatarMap() {
63
+ var merged = {};
64
+ var key;
65
+ for (key in DEFAULT_AVATARS) merged[key] = DEFAULT_AVATARS[key];
66
+ var custom = config().avatars || {};
67
+ for (key in custom) if (custom[key]) merged[key] = custom[key];
68
+ if (!merged.default || DEFAULT_AVATARS[merged.default]) {
69
+ merged.default = merged[custom.default] || DEFAULT_AVATARS.blue;
70
+ }
71
+ return merged;
72
+ }
73
+
74
+ function resolveAvatar(value) {
75
+ var avatars = avatarMap();
76
+ if (value === undefined || value === null || value === '') return avatars.default;
77
+ if (avatars[value]) return avatars[value];
78
+ return value;
79
+ }
80
+
81
+ function profileFor(element) {
82
+ var key = element.getAttribute('profile');
83
+ var profiles = config().profiles || {};
84
+ return (key && profiles[key]) || {};
85
+ }
86
+
87
+ function attrOr(element, attribute, fallback) {
88
+ var value = element.getAttribute(attribute);
89
+ return value === null || value === '' ? fallback : value;
90
+ }
91
+
92
+ /** Merge a profile entry with per-element attributes (attributes win). */
93
+ function identity(element) {
94
+ var profile = profileFor(element);
95
+ var author = attrOr(element, 'author', profile.author || 'User');
96
+ var avatar = element.getAttribute('avatar') || profile.avatar;
97
+ return {
98
+ author: author,
99
+ avatarUrl: resolveAvatar(avatar),
100
+ hasAvatar: Boolean(avatar),
101
+ roleColor: element.getAttribute('role-color') || profile.roleColor || null,
102
+ bot: has(element, 'bot') || Boolean(profile.bot),
103
+ verified: has(element, 'verified') || Boolean(profile.verified),
104
+ server: has(element, 'server') || Boolean(profile.server),
105
+ official: has(element, 'official-app') || Boolean(profile.officialApp),
106
+ op: has(element, 'op') || Boolean(profile.op)
107
+ };
108
+ }
109
+
110
+ function has(element, attribute) {
111
+ return element.hasAttribute(attribute);
112
+ }
113
+
114
+ function initialsColor(name) {
115
+ var sum = 0;
116
+ for (var i = 0; i < name.length; i++) sum = (sum + name.charCodeAt(i)) % 997;
117
+ return INITIAL_COLORS[sum % INITIAL_COLORS.length];
118
+ }
119
+
120
+ /* ------------------------------------------------------------ timestamps */
121
+
122
+ function parseStamp(value) {
123
+ if (value === null || value === undefined || value === '') return null;
124
+ if (/^\d+$/.test(value)) return new Date(Number(value));
125
+ var date = new Date(value);
126
+ return isNaN(date.getTime()) ? null : date;
127
+ }
128
+
129
+ function timeZone() {
130
+ return options().timeZone || undefined;
131
+ }
132
+
133
+ function locale() {
134
+ return options().locale || 'en-US';
135
+ }
136
+
137
+ function intl(formatOptions, date) {
138
+ return new Intl.DateTimeFormat(locale(), Object.assign({ timeZone: timeZone() }, formatOptions)).format(date);
139
+ }
140
+
141
+ function hour12(use24) {
142
+ return !(use24 || options().hour24);
143
+ }
144
+
145
+ function clockTime(date, use24) {
146
+ return intl({ hour: 'numeric', minute: '2-digit', hour12: hour12(use24) }, date).replace(/\u202f/g, ' ');
147
+ }
148
+
149
+ /** Discord message-header stamp: relative days for today/yesterday. */
150
+ function headerStamp(date, use24) {
151
+ var now = new Date();
152
+ var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
153
+ var day = new Date(date.getFullYear(), date.getMonth(), date.getDate());
154
+ var diffDays = Math.round((today - day) / 86400000);
155
+ if (diffDays === 0) return 'Today at ' + clockTime(date, use24);
156
+ if (diffDays === 1) return 'Yesterday at ' + clockTime(date, use24);
157
+ return intl({ day: '2-digit', month: '2-digit', year: 'numeric' }, date);
158
+ }
159
+
160
+ /** Discord's <t:…> format flags: t T d D f F R. */
161
+ function formatStamp(date, flag, use24) {
162
+ switch (flag) {
163
+ case 't':
164
+ return clockTime(date, use24);
165
+ case 'T':
166
+ return intl(
167
+ { hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: hour12(use24) },
168
+ date
169
+ ).replace(/\u202f/g, ' ');
170
+ case 'd':
171
+ return intl({ day: '2-digit', month: '2-digit', year: 'numeric' }, date);
172
+ case 'D':
173
+ return intl({ day: 'numeric', month: 'long', year: 'numeric' }, date);
174
+ case 'F':
175
+ return (
176
+ intl({ weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }, date) +
177
+ ' at ' +
178
+ clockTime(date, use24)
179
+ );
180
+ case 'R':
181
+ return relativeStamp(date);
182
+ case 'f':
183
+ default:
184
+ return (
185
+ intl({ day: 'numeric', month: 'long', year: 'numeric' }, date) +
186
+ ' at ' +
187
+ clockTime(date, use24)
188
+ );
189
+ }
190
+ }
191
+
192
+ function relativeStamp(date) {
193
+ var delta = Math.round((date.getTime() - Date.now()) / 1000);
194
+ var future = delta > 0;
195
+ var units = [
196
+ ['year', 31536000],
197
+ ['month', 2592000],
198
+ ['week', 604800],
199
+ ['day', 86400],
200
+ ['hour', 3600],
201
+ ['minute', 60],
202
+ ['second', 1]
203
+ ];
204
+ for (var i = 0; i < units.length; i++) {
205
+ var size = units[i][1];
206
+ if (Math.abs(delta) >= size || size === 1) {
207
+ var amount = Math.abs(Math.round(delta / size));
208
+ var label = units[i][0] + (amount === 1 ? '' : 's');
209
+ return future ? 'in ' + amount + ' ' + label : amount + ' ' + label + ' ago';
210
+ }
211
+ }
212
+ return 'now';
213
+ }
214
+
215
+ /* ------------------------------------------------------------- factories */
216
+
217
+ function make(tag, className, text) {
218
+ var node = document.createElement(tag);
219
+ if (className) node.className = className;
220
+ if (text !== undefined) node.textContent = text;
221
+ return node;
222
+ }
223
+
224
+ function avatarNode(identity, className) {
225
+ var wrap = make('span', className || 'dt-avatar');
226
+ if (identity.hasAvatar) {
227
+ var img = make('img');
228
+ img.src = identity.avatarUrl;
229
+ img.alt = '';
230
+ img.decoding = 'async';
231
+ wrap.appendChild(img);
232
+ } else {
233
+ wrap.classList.add('dt-avatar--initials');
234
+ wrap.style.backgroundColor = initialsColor(identity.author);
235
+ wrap.textContent = (identity.author || '?').charAt(0);
236
+ }
237
+ return wrap;
238
+ }
239
+
240
+ function badgeRow(identity) {
241
+ var row = make('span', 'dt-badges');
242
+ if (identity.bot) {
243
+ if (identity.verified) row.appendChild(make('span', 'dt-badge dt-badge--verified'));
244
+ else row.appendChild(make('span', 'dt-badge', 'BOT'));
245
+ } else if (identity.server) {
246
+ row.appendChild(make('span', 'dt-badge dt-badge--server', 'SERVER'));
247
+ } else if (identity.official) {
248
+ row.appendChild(make('span', 'dt-badge dt-badge--official', 'OFFICIAL'));
249
+ }
250
+ if (identity.op) row.appendChild(make('span', 'dt-badge', 'OP'));
251
+ return row.children.length ? row : null;
252
+ }
253
+
254
+ /* --------------------------------------------------------- message upgrade */
255
+
256
+ function upgradeMessage(element) {
257
+ if (has(element, 'data-dt-ready')) return;
258
+
259
+ var identity = identityOf(element);
260
+ var stamp = parseStamp(element.getAttribute('timestamp'));
261
+ var body = make('div', 'dt-body');
262
+
263
+ /* Optional parser-supplied header escapes the generated one. */
264
+ var suppliedHeader = element.querySelector(':scope > discord-author-info, :scope > discord-message-header');
265
+ if (suppliedHeader) suppliedHeader.remove();
266
+
267
+ while (element.firstChild) body.appendChild(element.firstChild);
268
+
269
+ var wrap = make('div', 'dt-msg');
270
+ wrap.appendChild(avatarNode(identity, 'dt-avatar'));
271
+
272
+ var content = make('div', 'dt-content');
273
+ var header = make('div', 'dt-header');
274
+ var author = make('span', 'dt-author', identity.author);
275
+ if (identity.roleColor) author.style.color = identity.roleColor;
276
+ header.appendChild(author);
277
+
278
+ var badges = badgeRow(identity);
279
+ if (badges) header.appendChild(badges);
280
+
281
+ var use24 = has(element, 'twenty-four');
282
+ if (stamp) {
283
+ var time = make('span', 'dt-timestamp', headerStamp(stamp, use24));
284
+ time.title = formatStamp(stamp, 'F', use24);
285
+ header.appendChild(time);
286
+ element.setAttribute('data-dt-short-time', clockTime(stamp, use24));
287
+ }
288
+
289
+ content.appendChild(header);
290
+ content.appendChild(body);
291
+ wrap.appendChild(content);
292
+ element.appendChild(wrap);
293
+
294
+ if (has(element, 'edited')) body.appendChild(make('span', 'dt-edited', '(edited)'));
295
+ if (isJumbo(body)) body.classList.add('dt-jumbo');
296
+
297
+ element.setAttribute('data-dt-ready', '');
298
+ }
299
+
300
+ function identityOf(element) {
301
+ return identity(element);
302
+ }
303
+
304
+ function isJumbo(body) {
305
+ var text = (body.textContent || '').replace(/\s+/g, '');
306
+ if (text.length > 0) return false;
307
+ var emoji = body.querySelectorAll('discord-custom-emoji, img.emoji');
308
+ return emoji.length > 0 && emoji.length <= 6;
309
+ }
310
+
311
+ /** Continuation grouping: same author, at most a `groupWindow` minute gap. */
312
+ function applyGrouping(list) {
313
+ if (options().grouping === false) return;
314
+ var windowMs = (options().groupWindow || 7) * 60 * 1000;
315
+ var previous = null;
316
+ var previousStamp = null;
317
+
318
+ for (var i = 0; i < list.length; i++) {
319
+ var message = list[i];
320
+ message.removeAttribute('data-dt-continuation');
321
+ message.removeAttribute('data-dt-group-start');
322
+
323
+ if (message.hasAttribute('message-body-only')) {
324
+ message.setAttribute('data-dt-continuation', '');
325
+ previous = null;
326
+ previousStamp = null;
327
+ continue;
328
+ }
329
+
330
+ var identityKey =
331
+ message.getAttribute('profile') || message.getAttribute('author') || message.getAttribute('avatar');
332
+ var stamp = parseStamp(message.getAttribute('timestamp'));
333
+ var sameAuthor = previous !== null && identityKey !== null && identityKey === previous;
334
+ var closeInTime = previousStamp === null || stamp === null || Math.abs(stamp - previousStamp) <= windowMs;
335
+
336
+ if (sameAuthor && closeInTime) {
337
+ message.setAttribute('data-dt-continuation', '');
338
+ } else if (previous !== null) {
339
+ message.setAttribute('data-dt-group-start', '');
340
+ }
341
+
342
+ previous = identityKey;
343
+ previousStamp = stamp;
344
+ }
345
+ }
346
+
347
+ /* ----------------------------------------------------------- sub-upgrades */
348
+
349
+ function upgradeReply(element) {
350
+ if (has(element, 'data-dt-ready') || has(element, 'deleted')) return;
351
+ var identity = identityOf(element);
352
+ var leading = [];
353
+
354
+ if (!element.querySelector(':scope > .dt-reply-avatar')) {
355
+ if (identity.hasAvatar) leading.push(avatarNode(identity, 'dt-reply-avatar'));
356
+ }
357
+
358
+ var author = make('span', 'dt-reply-author', (has(element, 'mentions') ? '@' : '') + identity.author);
359
+ if (identity.roleColor) author.style.color = identity.roleColor;
360
+ leading.push(author);
361
+
362
+ var mentions = make('span', 'dt-sr-only', 'replying to ');
363
+ element.insertBefore(mentions, element.firstChild);
364
+ for (var i = leading.length - 1; i >= 0; i--) element.insertBefore(leading[i], mentions.nextSibling);
365
+
366
+ element.setAttribute('data-dt-ready', '');
367
+ }
368
+
369
+ function upgradeTime(element) {
370
+ if (has(element, 'data-dt-ready')) return;
371
+ var raw = element.getAttribute('timestamp');
372
+ if (!raw) return;
373
+ var date = parseStamp(raw === 'now' ? String(Date.now()) : raw);
374
+ if (!date) return;
375
+ var use24 = has(element, 'twenty-four');
376
+ var flag = element.getAttribute('format') || 'f';
377
+ element.setAttribute('data-dt-stamp', date.toISOString());
378
+ element.textContent = formatStamp(date, flag, use24);
379
+ element.setAttribute('datetime', date.toISOString());
380
+ element.title = formatStamp(date, 'F', use24);
381
+ element.setAttribute('data-dt-ready', '');
382
+ if (flag === 'R' && relativeElements.indexOf(element) === -1) relativeElements.push(element);
383
+ }
384
+
385
+ function stampOf(element) {
386
+ return parseStamp(element.getAttribute('data-dt-stamp') || element.getAttribute('timestamp'));
387
+ }
388
+
389
+ function refreshRelative() {
390
+ for (var i = 0; i < relativeElements.length; i++) {
391
+ var date = stampOf(relativeElements[i]);
392
+ if (date) relativeElements[i].textContent = relativeStamp(date);
393
+ }
394
+ }
395
+
396
+ function upgradeSpoilers(root) {
397
+ var spoilers = root.querySelectorAll('discord-spoiler:not([data-dt-ready])');
398
+ for (var i = 0; i < spoilers.length; i++) {
399
+ var spoiler = spoilers[i];
400
+ spoiler.setAttribute('data-dt-ready', '');
401
+ spoiler.setAttribute('role', 'button');
402
+ if (!spoiler.hasAttribute('tabindex')) spoiler.setAttribute('tabindex', '0');
403
+ spoiler.setAttribute('aria-expanded', has(spoiler, 'activated') ? 'true' : 'false');
404
+ }
405
+
406
+ var media = root.querySelectorAll(
407
+ 'discord-image-attachment[spoiler], discord-video-attachment[spoiler]'
408
+ );
409
+ for (var j = 0; j < media.length; j++) {
410
+ if (media[j].querySelector(':scope > .dt-spoiler-overlay')) continue;
411
+ media[j].appendChild(make('button', 'dt-spoiler-overlay', 'Spoiler'));
412
+ }
413
+ }
414
+
415
+ function toggleSpoiler(element) {
416
+ var open = !has(element, 'data-dt-revealed');
417
+ if (open) element.setAttribute('data-dt-revealed', '');
418
+ else element.removeAttribute('data-dt-revealed');
419
+ if (element.hasAttribute('aria-expanded')) element.setAttribute('aria-expanded', open ? 'true' : 'false');
420
+ }
421
+
422
+ function upgradeReactions(element) {
423
+ if (has(element, 'data-dt-ready')) return;
424
+ var emoji = element.getAttribute('emoji');
425
+ var isUrl = emoji && /^(https?:|\/|\.\/)/.test(emoji);
426
+ if (isUrl) {
427
+ var img = make('img', 'dt-reaction-emoji');
428
+ img.src = emoji;
429
+ img.alt = element.getAttribute('name') || '';
430
+ img.loading = 'lazy';
431
+ img.decoding = 'async';
432
+ element.appendChild(img);
433
+ } else if (emoji) {
434
+ element.appendChild(make('span', 'dt-reaction-emoji', emoji));
435
+ }
436
+ var count = parseInt(element.getAttribute('count') || '1', 10);
437
+ if (count > 1) element.appendChild(make('span', 'dt-reaction-count', String(count)));
438
+ element.setAttribute('data-dt-ready', '');
439
+ }
440
+
441
+ function reactionCount(element) {
442
+ var node = element.querySelector(':scope > .dt-reaction-count');
443
+ return node ? parseInt(node.textContent, 10) || 1 : 1;
444
+ }
445
+
446
+ function setReactionCount(element, value) {
447
+ var node = element.querySelector(':scope > .dt-reaction-count');
448
+ if (value <= 1) {
449
+ if (node) node.remove();
450
+ return;
451
+ }
452
+ if (!node) element.appendChild(make('span', 'dt-reaction-count', String(value)));
453
+ else node.textContent = String(value);
454
+ }
455
+
456
+ function upgradeEmbed(element) {
457
+ if (has(element, 'data-dt-ready')) return;
458
+ var color = element.getAttribute('color');
459
+ if (color) element.style.setProperty('--dt-embed-color', color);
460
+
461
+ var content = element.querySelector(':scope > .dt-embed-content');
462
+ if (!content) {
463
+ content = make('div', 'dt-embed-content');
464
+ while (element.firstChild) content.appendChild(element.firstChild);
465
+ element.appendChild(content);
466
+ }
467
+
468
+ var provider = element.getAttribute('provider');
469
+ if (provider && !content.querySelector(':scope > .dt-embed-provider')) {
470
+ content.insertBefore(make('span', 'dt-embed-provider', provider), content.firstChild);
471
+ }
472
+
473
+ var authorName = element.getAttribute('author-name');
474
+ if (authorName && !content.querySelector(':scope > .dt-embed-author')) {
475
+ var authorRow = make('div', 'dt-embed-author');
476
+ var authorImage = element.getAttribute('author-image');
477
+ if (authorImage) {
478
+ var icon = make('img');
479
+ icon.src = authorImage;
480
+ icon.alt = '';
481
+ icon.loading = 'lazy';
482
+ icon.decoding = 'async';
483
+ authorRow.appendChild(icon);
484
+ }
485
+ var authorUrl = element.getAttribute('author-url');
486
+ if (authorUrl) {
487
+ var link = make('a', null, authorName);
488
+ link.href = authorUrl;
489
+ link.target = '_blank';
490
+ link.rel = 'noopener noreferrer';
491
+ authorRow.appendChild(link);
492
+ } else {
493
+ authorRow.appendChild(make('span', null, authorName));
494
+ }
495
+ content.insertBefore(authorRow, content.firstChild);
496
+ }
497
+
498
+ var title = element.getAttribute('embed-title') || element.getAttribute('title');
499
+ if (title && !content.querySelector(':scope > .dt-embed-title')) {
500
+ var url = element.getAttribute('url');
501
+ var titleNode = url ? make('a', 'dt-embed-title dt-embed-title--link', title) : make('span', 'dt-embed-title', title);
502
+ if (url) {
503
+ titleNode.href = url;
504
+ titleNode.target = '_blank';
505
+ titleNode.rel = 'noopener noreferrer';
506
+ }
507
+ var afterAuthor = content.querySelector(':scope > .dt-embed-author, :scope > .dt-embed-provider');
508
+ if (afterAuthor) afterAuthor.insertAdjacentElement('afterend', titleNode);
509
+ else content.insertBefore(titleNode, content.firstChild);
510
+ }
511
+
512
+ var thumbnail = element.getAttribute('thumbnail');
513
+ if (thumbnail && !element.querySelector(':scope > .dt-embed-thumbnail')) {
514
+ var thumb = make('div', 'dt-embed-thumbnail');
515
+ var thumbImg = make('img');
516
+ thumbImg.src = thumbnail;
517
+ thumbImg.alt = '';
518
+ thumbImg.loading = 'lazy';
519
+ thumbImg.decoding = 'async';
520
+ thumb.appendChild(thumbImg);
521
+ element.appendChild(thumb);
522
+ }
523
+
524
+ var video = element.getAttribute('video');
525
+ var image = element.getAttribute('image');
526
+ if ((video || image) && !element.querySelector(':scope > .dt-embed-image')) {
527
+ var media = make('div', 'dt-embed-image');
528
+ if (video) {
529
+ var videoNode = make('video');
530
+ videoNode.controls = true;
531
+ videoNode.preload = 'metadata';
532
+ videoNode.src = video;
533
+ media.appendChild(videoNode);
534
+ } else {
535
+ var img = make('img');
536
+ img.src = image;
537
+ img.alt = '';
538
+ img.loading = 'lazy';
539
+ img.decoding = 'async';
540
+ media.appendChild(img);
541
+ }
542
+ var footer = content.querySelector(':scope > discord-embed-footer');
543
+ content.insertBefore(media, footer || null);
544
+ }
545
+
546
+ element.setAttribute('data-dt-ready', '');
547
+ }
548
+
549
+ function upgradeImage(element) {
550
+ if (has(element, 'data-dt-ready')) return;
551
+ var url = element.getAttribute('url');
552
+ if (url && !element.querySelector('img')) {
553
+ var img = make('img');
554
+ img.src = url;
555
+ img.alt = element.getAttribute('alt') || 'discord image attachment';
556
+ img.loading = 'lazy';
557
+ img.decoding = 'async';
558
+ var width = element.getAttribute('width');
559
+ var height = element.getAttribute('height');
560
+ if (width) img.style.maxWidth = width + 'px';
561
+ if (height) img.style.maxHeight = height + 'px';
562
+ element.appendChild(img);
563
+ }
564
+ var nested = element.querySelector('img');
565
+ if (nested && !nested.getAttribute('loading')) nested.loading = 'lazy';
566
+ element.setAttribute('data-dt-ready', '');
567
+ }
568
+
569
+ function upgradeMedia(element, tag) {
570
+ if (has(element, 'data-dt-ready')) return;
571
+ var href = element.getAttribute('href') || element.getAttribute('src');
572
+ if (href && !element.querySelector(tag)) {
573
+ var node = make(tag);
574
+ node.controls = true;
575
+ node.preload = 'metadata';
576
+ node.src = href;
577
+ if (tag === 'video') {
578
+ node.playsInline = true;
579
+ var poster = element.getAttribute('poster');
580
+ if (poster) node.poster = poster;
581
+ }
582
+ element.appendChild(node);
583
+ }
584
+ element.setAttribute('data-dt-ready', '');
585
+ }
586
+
587
+ function upgradeFile(element) {
588
+ if (has(element, 'data-dt-ready')) return;
589
+ var href = element.getAttribute('href');
590
+ if (href && !element.firstChild) {
591
+ var link = make('a');
592
+ link.href = href;
593
+ link.target = element.getAttribute('target') || '_blank';
594
+ link.rel = 'noopener noreferrer';
595
+ var name = element.getAttribute('name') || href.split('/').pop() || 'file';
596
+ link.appendChild(make('span', 'dt-file-icon', element.getAttribute('type') || 'FILE'));
597
+ var meta = make('span', 'dt-file-meta');
598
+ meta.appendChild(make('span', 'dt-file-name', name));
599
+ meta.appendChild(
600
+ make(
601
+ 'span',
602
+ 'dt-file-size',
603
+ [element.getAttribute('bytes'), element.getAttribute('bytes-unit')].filter(Boolean).join(' ')
604
+ )
605
+ );
606
+ link.appendChild(meta);
607
+ element.appendChild(link);
608
+ }
609
+ element.setAttribute('data-dt-ready', '');
610
+ }
611
+
612
+ function upgradeCode(element) {
613
+ if (has(element, 'data-dt-ready') || element.querySelector(':scope > .dt-copy')) return;
614
+ var button = make('button', 'dt-copy', 'Copy');
615
+ button.type = 'button';
616
+ element.appendChild(button);
617
+ element.setAttribute('data-dt-ready', '');
618
+ }
619
+
620
+ function lazyImages(root) {
621
+ var images = root.querySelectorAll(
622
+ 'discord-attachments img:not([loading]), discord-embed img:not([loading]), discord-embed video, discord-thread img'
623
+ );
624
+ for (var i = 0; i < images.length; i++) images[i].setAttribute('loading', 'lazy');
625
+ }
626
+
627
+ /* ------------------------------------------------------------- traversal */
628
+
629
+ function upgrade(root) {
630
+ if (!root || !root.querySelectorAll) return;
631
+
632
+ var messages = root.querySelectorAll('discord-message:not([data-dt-ready])');
633
+ for (var i = 0; i < messages.length; i++) upgradeMessage(messages[i]);
634
+
635
+ var lists = root.querySelectorAll('discord-messages');
636
+ for (var j = 0; j < lists.length; j++) {
637
+ var grouped = lists[j].querySelectorAll(':scope > discord-message');
638
+ if (grouped.length) applyGrouping(grouped);
639
+ }
640
+
641
+ var replies = root.querySelectorAll('discord-reply:not([data-dt-ready])');
642
+ for (var k = 0; k < replies.length; k++) upgradeReply(replies[k]);
643
+
644
+ var times = root.querySelectorAll('discord-time:not([data-dt-ready])');
645
+ for (var m = 0; m < times.length; m++) upgradeTime(times[m]);
646
+
647
+ var embeds = root.querySelectorAll('discord-embed:not([data-dt-ready])');
648
+ for (var n = 0; n < embeds.length; n++) upgradeEmbed(embeds[n]);
649
+
650
+ var files = root.querySelectorAll('discord-file-attachment:not([data-dt-ready])');
651
+ for (var o = 0; o < files.length; o++) upgradeFile(files[o]);
652
+
653
+ var images = root.querySelectorAll('discord-image-attachment:not([data-dt-ready])');
654
+ for (var p = 0; p < images.length; p++) upgradeImage(images[p]);
655
+
656
+ var videos = root.querySelectorAll('discord-video-attachment:not([data-dt-ready])');
657
+ for (var q = 0; q < videos.length; q++) upgradeMedia(videos[q], 'video');
658
+
659
+ var audios = root.querySelectorAll('discord-audio-attachment:not([data-dt-ready])');
660
+ for (var r = 0; r < audios.length; r++) upgradeMedia(audios[r], 'audio');
661
+
662
+ var reactions = root.querySelectorAll('discord-reaction:not([data-dt-ready])');
663
+ for (var s = 0; s < reactions.length; s++) upgradeReactions(reactions[s]);
664
+
665
+ var codes = root.querySelectorAll('discord-pre:not([data-dt-ready])');
666
+ for (var t = 0; t < codes.length; t++) upgradeCode(codes[t]);
667
+
668
+ upgradeSpoilers(root);
669
+ lazyImages(root);
670
+ applyThemeFallbacks(root);
671
+ }
672
+
673
+ /** `light-theme` on the container mirrors to data-theme for the CSS tokens. */
674
+ function applyThemeFallbacks(root) {
675
+ var lists = root.querySelectorAll('discord-messages');
676
+ for (var i = 0; i < lists.length; i++) {
677
+ if (lists[i].hasAttribute('light-theme') && !lists[i].hasAttribute('data-theme')) {
678
+ lists[i].setAttribute('data-theme', 'light');
679
+ }
680
+ }
681
+ }
682
+
683
+ /* ---------------------------------------------------------------- events */
684
+
685
+ function onClick(event) {
686
+ var target = event.target;
687
+ if (!target || !target.closest) return;
688
+
689
+ var spoiler = target.closest('discord-spoiler');
690
+ if (spoiler) {
691
+ toggleSpoiler(spoiler);
692
+ return;
693
+ }
694
+
695
+ var overlay = target.closest('.dt-spoiler-overlay');
696
+ if (overlay && overlay.parentElement) {
697
+ overlay.parentElement.setAttribute('data-dt-revealed', '');
698
+ return;
699
+ }
700
+
701
+ var reaction = target.closest('discord-reaction');
702
+ if (reaction && reaction.hasAttribute('data-dt-ready')) {
703
+ var count = reactionCount(reaction);
704
+ if (has(reaction, 'reacted')) {
705
+ reaction.removeAttribute('reacted');
706
+ setReactionCount(reaction, Math.max(1, count - 1));
707
+ } else {
708
+ reaction.setAttribute('reacted', '');
709
+ setReactionCount(reaction, count + 1);
710
+ }
711
+ return;
712
+ }
713
+
714
+ var copy = target.closest('.dt-copy');
715
+ if (copy && copy.parentElement && navigator.clipboard) {
716
+ var pre = copy.parentElement;
717
+ var text = pre.querySelector('discord-code, pre, code');
718
+ navigator.clipboard.writeText((text || pre).textContent).then(function () {
719
+ copy.textContent = 'Copied';
720
+ setTimeout(function () {
721
+ copy.textContent = 'Copy';
722
+ }, 1200);
723
+ });
724
+ }
725
+ }
726
+
727
+ function onKeydown(event) {
728
+ if (event.key !== 'Enter' && event.key !== ' ') return;
729
+ var target = event.target;
730
+ if (!target || !target.closest) return;
731
+ var spoiler = target.closest('discord-spoiler');
732
+ if (spoiler) {
733
+ event.preventDefault();
734
+ toggleSpoiler(spoiler);
735
+ }
736
+ }
737
+
738
+ /* ------------------------------------------------------------------ boot */
739
+
740
+ var pending = null;
741
+
742
+ function schedule(root) {
743
+ if (pending) return;
744
+ pending = window.requestAnimationFrame(function () {
745
+ pending = null;
746
+ upgrade(root || document);
747
+ });
748
+ }
749
+
750
+ function boot() {
751
+ upgrade(document);
752
+ document.addEventListener('click', onClick, false);
753
+ document.addEventListener('keydown', onKeydown, false);
754
+
755
+ if (window.setInterval) {
756
+ relativeTimers.push(
757
+ window.setInterval(function () {
758
+ if (relativeElements.length) refreshRelative();
759
+ }, 60000)
760
+ );
761
+ }
762
+
763
+ if (options().observe !== false && window.MutationObserver) {
764
+ var observer = new MutationObserver(function (records) {
765
+ for (var i = 0; i < records.length; i++) {
766
+ if (records[i].addedNodes.length) {
767
+ schedule(document);
768
+ return;
769
+ }
770
+ }
771
+ });
772
+ observer.observe(document.documentElement, { childList: true, subtree: true });
773
+ }
774
+ }
775
+
776
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, false);
777
+ else boot();
778
+
779
+ window.DiscordTranscript = {
780
+ version: VERSION,
781
+ upgrade: function (root) {
782
+ upgrade(root || document);
783
+ },
784
+ refresh: refreshRelative,
785
+ formatStamp: formatStamp,
786
+ parseStamp: parseStamp
787
+ };
788
+ })();