discord-html-transcripts-fix 1.4.0 → 1.7.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.
@@ -134,7 +134,6 @@ async function render(_a) {
134
134
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.searchBarStyles } }),
135
135
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.lightboxStyles } }),
136
136
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.tocStyles } }),
137
- (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.i18nStyles } }),
138
137
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.dateSeparatorStyles } }),
139
138
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.markdownStyles } }),
140
139
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.filterPanelStyles } }),
@@ -155,16 +154,13 @@ async function render(_a) {
155
154
  ] }),
156
155
  (0, jsx_runtime_1.jsxs)("body", { style: { margin: 0, minHeight: '100vh' }, children: [
157
156
  (0, jsx_runtime_1.jsx)(transcript_1.default, Object.assign({ messages, channel, callbacks }, options, renderContext)),
158
- (0, jsx_runtime_1.jsx)(StatsFooter, { stats, lang, i18n }),
157
+ renderStatsFooter(stats, lang, i18n, options.statsFooter),
159
158
  (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.mentionPopup } }),
160
- (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.backToTop } }),
161
159
  (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.searchBar } }),
162
160
  (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.lightbox } }),
163
161
  (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.toc } }),
164
- (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.i18nSwitcher } }),
165
162
  (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.filterPanel } }),
166
163
  (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.codeCopyButton } }),
167
- (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.permalinkButton } }),
168
164
  options.hydrate && (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.revealSpoiler } }),
169
165
  ] })
170
166
  ] });
@@ -200,27 +196,63 @@ function formatSpan(ms, lang) {
200
196
  if (mins > 0) return `${mins}m`;
201
197
  return '<1m';
202
198
  }
203
- function StatsFooter({ stats, lang, i18n }) {
204
- const dict = (i18n && i18n[lang]) || (i18n && i18n.en) || {};
199
+ // Renders the bottom stats footer. Caller-controlled via options.statsFooter:
200
+ // false → no footer
201
+ // { enabled: false } → no footer
202
+ // { template: '{messages} ...' } → render with custom template
203
+ // Placeholders: {messages} {participants}
204
+ // {images} {from} {to} {span}
205
+ // Default: en template "X messages · Y participants · Z images · from → to · span"
206
+ function renderStatsFooter(stats, lang, i18n, opts) {
207
+ if (opts === false) return null;
208
+ const config = (opts && typeof opts === 'object') ? opts : {};
209
+ if (config.enabled === false) return null;
210
+
205
211
  const locale = lang === 'de' ? 'de-DE' : 'en-US';
206
- const fmtDate = (ms) => new Date(ms).toLocaleString(locale, { dateStyle: 'medium', timeStyle: 'short' });
207
- return (0, jsx_runtime_1.jsxs)("footer", { className: "dht-stats", children: [
208
- (0, jsx_runtime_1.jsx)("span", { children: (dict.statsMessages || '{n} messages').replace('{n}', stats.messageCount) }),
209
- ' · ',
210
- (0, jsx_runtime_1.jsx)("span", { children: (dict.statsParticipants || '{n} participants').replace('{n}', stats.participantCount) }),
211
- ' · ',
212
- (0, jsx_runtime_1.jsx)("span", { children: (dict.statsImages || '{n} images').replace('{n}', stats.imageCount) }),
213
- stats.firstTs && stats.lastTs && (0, jsx_runtime_1.jsxs)("span", { children: [' · ', fmtDate(stats.firstTs), ' → ', fmtDate(stats.lastTs)] }),
214
- stats.spanMs && (0, jsx_runtime_1.jsxs)("span", { children: [' · ', formatSpan(stats.spanMs)] }),
215
- ] });
212
+ const fmtDate = (ms) => ms ? new Date(ms).toLocaleString(locale, { dateStyle: 'medium', timeStyle: 'short' }) : '';
213
+ const span = stats.spanMs ? formatSpan(stats.spanMs) : '';
214
+
215
+ const values = {
216
+ messages: String(stats.messageCount),
217
+ participants: String(stats.participantCount),
218
+ images: String(stats.imageCount),
219
+ from: fmtDate(stats.firstTs),
220
+ to: fmtDate(stats.lastTs),
221
+ span,
222
+ };
223
+
224
+ if (typeof config.template === 'string' && config.template.length > 0) {
225
+ const rendered = config.template.replace(/\{(messages|participants|images|from|to|span)\}/g, (_, k) => values[k] || '');
226
+ return (0, jsx_runtime_1.jsx)("footer", { className: "dht-stats", children: rendered });
227
+ }
228
+
229
+ const dict = (i18n && i18n[lang]) || (i18n && i18n.en) || {};
230
+ const parts = [
231
+ (dict.statsMessages || '{n} messages').replace('{n}', values.messages),
232
+ (dict.statsParticipants || '{n} participants').replace('{n}', values.participants),
233
+ (dict.statsImages || '{n} images').replace('{n}', values.images),
234
+ ];
235
+ if (values.from && values.to) parts.push(values.from + ' → ' + values.to);
236
+ if (values.span) parts.push(values.span);
237
+ return (0, jsx_runtime_1.jsx)("footer", { className: "dht-stats", children: parts.join(' · ') });
216
238
  }
217
239
 
218
240
  const extraStyles = `
219
241
  /* Ensure the host establishes a positioning context for the absolute pin indicator,
220
242
  and clamp it inside the message box on narrow viewports. */
243
+ /* Action-row buttons — Discord-like horizontal spacing */
244
+ discord-action-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:8px 0}
245
+ .dht-v2-container discord-action-row,.dht-section-content discord-action-row{margin:6px 0}
246
+ /* V2 container — Discord-like spacing inside block flow */
247
+ .dht-v2-container .dht-heading,.dht-section-content .dht-heading{margin:8px 0 4px}
248
+ .dht-v2-container .dht-heading:first-child,.dht-section-content .dht-heading:first-child{margin-top:0}
249
+ .dht-v2-container .dht-v2-separator,.dht-section-content .dht-v2-separator{margin:8px 0}
250
+ .dht-v2-container strong,.dht-section-content strong{display:inline;font-weight:700;color:#fff}
251
+ .dht-v2-container .dht-mention,.dht-section-content .dht-mention{display:inline-block;vertical-align:baseline}
252
+ /* System-message author — clickable like a user mention */
253
+ .dht-sys-author{cursor:pointer;font-style:normal;font-weight:500}
254
+ .dht-sys-author:hover{text-decoration:underline}
221
255
  .dht-msg-pinned{position:relative}
222
- .dht-pinned-indicator{position:absolute;top:4px;left:4px;font-size:12px;opacity:.75;pointer-events:none}
223
- @media (max-width:640px){.dht-pinned-indicator{position:static;display:inline-block;margin-right:4px}}
224
256
  .dht-app-badge,.dht-popup-badge{background:#5865F2;color:#fff;font-size:10px;padding:1px 6px;border-radius:4px;margin-left:6px;vertical-align:middle;font-weight:600}
225
257
  .dht-edit-marker{color:#949ba4;font-size:11px;margin-left:4px;cursor:help}
226
258
  .dht-suppressed{color:#6e727a;font-size:11px;font-style:italic}
@@ -19,12 +19,17 @@ function DiscordContainer({ children, accentColor, spoiler }) {
19
19
  // string can't smuggle CSS into the inline borderLeft.
20
20
  const raw = colorToHex(accentColor);
21
21
  const hex = raw ? (0, utils_1.safeColor)(raw, null) : null;
22
+ // FIX: previous layout was display:flex;flex-direction:column;gap:8px which
23
+ // forced every direct child — including inline <strong>, <br>, mention
24
+ // <span> and text nodes — to become its own vertical flex item. That broke
25
+ // sentences across multiple lines and made role mentions fill the entire
26
+ // row. Use normal block flow instead so inline content lays out the way
27
+ // Discord renders it.
22
28
  const style = {
23
- display: 'flex',
29
+ display: 'block',
24
30
  width: '100%',
25
31
  maxWidth: '500px',
26
32
  minWidth: 0,
27
- flexDirection: 'column',
28
33
  backgroundColor: '#3f4248',
29
34
  padding: '16px',
30
35
  borderLeft: hex ? `4px solid ${hex}` : '1px solid #4f5359',
@@ -34,10 +39,13 @@ function DiscordContainer({ children, accentColor, spoiler }) {
34
39
  marginTop: '2px',
35
40
  marginBottom: '2px',
36
41
  borderRadius: '10px',
37
- gap: '8px',
38
42
  boxSizing: 'border-box',
43
+ lineHeight: 1.4,
44
+ color: '#dbdee1',
45
+ wordBreak: 'break-word',
46
+ overflowWrap: 'anywhere',
39
47
  };
40
- const node = (0, jsx_runtime_1.jsx)("div", { style, children });
48
+ const node = (0, jsx_runtime_1.jsx)("div", { className: "dht-v2-container", style, children });
41
49
  if (spoiler) {
42
50
  return (0, jsx_runtime_1.jsx)("span", { className: "discord-spoiler", children: node });
43
51
  }
@@ -3,12 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
4
  function SectionContent({ children }) {
5
5
  return ((0, jsx_runtime_1.jsx)("div", {
6
+ className: "dht-section-content",
6
7
  style: {
7
- display: 'flex',
8
- flexDirection: 'column',
8
+ // Block flow so inline content (mentions, strong, br, text) flows
9
+ // naturally inside the section instead of becoming flex rows.
10
+ display: 'block',
9
11
  flex: 1,
10
- minWidth: 0, // allow text to shrink/wrap inside flex parent
12
+ minWidth: 0,
11
13
  overflowWrap: 'anywhere',
14
+ wordBreak: 'break-word',
15
+ lineHeight: 1.4,
12
16
  },
13
17
  children
14
18
  }));
@@ -80,9 +80,11 @@ async function DiscordMessage({ message, context }) {
80
80
  const authorName = (message.member?.nickname || message.author?.displayName || message.author?.username || '').toLowerCase();
81
81
  const ts = message.createdAt instanceof Date ? message.createdAt.getTime() : 0;
82
82
  const lowerText = (message.content || '').toLowerCase().slice(0, 4096);
83
- const hasImage = message.attachments && Array.from(message.attachments.values()).some(a => typeof a.contentType === 'string' && a.contentType.startsWith('image/'));
84
- const hasEmbed = message.embeds && message.embeds.length > 0;
85
- const hasAttachment = message.attachments && message.attachments.size > 0;
83
+ const mediaFlags = detectMediaFlags(message);
84
+ const hasImage = mediaFlags.hasImage;
85
+ const hasEmbed = mediaFlags.hasEmbed;
86
+ const hasAttachment = mediaFlags.hasAttachment;
87
+ const hasComponentV2 = mediaFlags.hasComponentV2;
86
88
  const stickers = message.stickers && message.stickers.size > 0
87
89
  ? Array.from(message.stickers.values()).map((s) => ({
88
90
  id: s.id,
@@ -107,11 +109,8 @@ async function DiscordMessage({ message, context }) {
107
109
  // Forwarded message snapshots (messageSnapshots) — render before the empty body
108
110
  const snapshots = Array.isArray(message.messageSnapshots) ? message.messageSnapshots : [];
109
111
 
110
- const isApp = !!(message.author?.bot && message.applicationId);
111
-
112
112
  const editedAtIso = message.editedAt instanceof Date ? message.editedAt.toISOString() : null;
113
113
  const pinned = !!message.pinned;
114
- const verifiedBot = !!message.author?.flags?.has?.(discord_js_1.UserFlags.VerifiedBot);
115
114
 
116
115
  // Forum applied tags — only on the first message of a forum thread (the post itself)
117
116
  let appliedTagPills = null;
@@ -134,8 +133,10 @@ async function DiscordMessage({ message, context }) {
134
133
  }
135
134
  }
136
135
 
137
- // Build a comma-separated badge list for additional flag rendering
138
- const slashCmdLine = renderSlashCommandLine(message, context);
136
+ // Slash command interaction data is attached to <discord-command> as
137
+ // data attributes so the popup can show parameters Discord-style. We
138
+ // no longer render a separate "/cmd opt:val by user" line.
139
+ const slashData = buildSlashCommandData(message);
139
140
 
140
141
  return ((0, jsx_runtime_1.jsxs)("discord-message", {
141
142
  id: `m-${message.id}`,
@@ -154,13 +155,20 @@ async function DiscordMessage({ message, context }) {
154
155
  "data-has-image": hasImage ? 'true' : undefined,
155
156
  "data-has-embed": hasEmbed ? 'true' : undefined,
156
157
  "data-has-attachment": hasAttachment ? 'true' : undefined,
158
+ "data-has-component-v2": hasComponentV2 ? 'true' : undefined,
157
159
  children: [
158
- pinned && (0, jsx_runtime_1.jsx)("div", { className: "dht-pinned-indicator", title: "Pinned message", children: "📌" }),
159
160
  appliedTagPills,
160
161
  (0, jsx_runtime_1.jsx)(reply_1.default, { message: message, context: context }),
161
- message.interaction && ((0, jsx_runtime_1.jsx)("discord-command", { slot: "reply", profile: message.interaction.user.id, command: '/' + message.interaction.commandName })),
162
- slashCmdLine,
163
- isApp && (0, jsx_runtime_1.jsxs)("span", { className: "dht-app-badge", "data-i18n": "appBadge", children: [t(context, 'appBadge', 'APP'), verifiedBot && (0, jsx_runtime_1.jsx)("span", { className: "dht-verified-check", title: "Verified Bot", children: '✓' })] }),
162
+ message.interaction && ((0, jsx_runtime_1.jsx)("discord-command", {
163
+ slot: "reply",
164
+ profile: message.interaction.user.id,
165
+ command: (slashData && slashData.cmd) || ('/' + message.interaction.commandName),
166
+ className: "dht-slash-clickable",
167
+ "data-slash-cmd": slashData ? slashData.cmd : ('/' + message.interaction.commandName),
168
+ "data-slash-by": slashData ? slashData.by : '',
169
+ "data-slash-by-id": slashData ? slashData.byId : message.interaction.user.id,
170
+ "data-slash-options": JSON.stringify((slashData && slashData.opts) || []),
171
+ })),
164
172
  isCrossposted && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-crosspost", title: "Published from announcement channel", children: '📣 Published' }),
165
173
  isCrosspost && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-crosspost", title: "Follows another channel", children: '📡 Follow' }),
166
174
  isSilent && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-silent", title: "Silent message (no notification)", children: '🔕 Silent' }),
@@ -377,40 +385,108 @@ function renderSnapshots(snapshots, context, depth = 0) {
377
385
  });
378
386
  }
379
387
 
380
- function renderSlashCommandLine(message, context) {
388
+ const COMPONENT_TYPE_ACTION_ROW = 1;
389
+ const COMPONENT_TYPE_SECTION = 9;
390
+ const COMPONENT_TYPE_THUMBNAIL = 11;
391
+ const COMPONENT_TYPE_MEDIA_GALLERY = 12;
392
+ const COMPONENT_TYPE_FILE = 13;
393
+ const COMPONENT_TYPE_CONTAINER = 17;
394
+ const FLAG_IS_COMPONENTS_V2 = 1 << 15;
395
+
396
+ // Walks message.embeds and message.components to determine whether the message
397
+ // carries media or V2 containers — used to drive the filter checkboxes
398
+ // (data-has-image, data-has-attachment, data-has-embed, data-has-component-v2).
399
+ function detectMediaFlags(message) {
400
+ const out = { hasImage: false, hasAttachment: false, hasEmbed: false, hasComponentV2: false };
401
+
402
+ // Direct attachments
403
+ if (message.attachments && typeof message.attachments.values === 'function') {
404
+ for (const a of message.attachments.values()) {
405
+ out.hasAttachment = true;
406
+ const ct = typeof a.contentType === 'string' ? a.contentType : '';
407
+ if (ct.startsWith('image/') || ct.startsWith('video/')) out.hasImage = true;
408
+ }
409
+ }
410
+
411
+ // Classic embeds
412
+ if (Array.isArray(message.embeds)) {
413
+ for (const e of message.embeds) {
414
+ if (!e) continue;
415
+ out.hasEmbed = true;
416
+ if (e.image || e.thumbnail) out.hasImage = true;
417
+ if (e.video && (e.video.url || e.video.proxyURL || e.video.proxy_url)) out.hasImage = true;
418
+ }
419
+ }
420
+
421
+ // Components V2 — message-level flag and recursive walk
422
+ const flags = (typeof message.flags?.bitfield === 'number') ? message.flags.bitfield : 0;
423
+ if (flags & FLAG_IS_COMPONENTS_V2) out.hasComponentV2 = true;
424
+
425
+ const walk = (comp) => {
426
+ if (!comp || typeof comp.type !== 'number') return;
427
+ switch (comp.type) {
428
+ case COMPONENT_TYPE_CONTAINER:
429
+ out.hasComponentV2 = true;
430
+ if (Array.isArray(comp.components)) for (const c of comp.components) walk(c);
431
+ break;
432
+ case COMPONENT_TYPE_SECTION:
433
+ if (Array.isArray(comp.components)) for (const c of comp.components) walk(c);
434
+ if (comp.accessory) walk(comp.accessory);
435
+ break;
436
+ case COMPONENT_TYPE_ACTION_ROW:
437
+ if (Array.isArray(comp.components)) for (const c of comp.components) walk(c);
438
+ break;
439
+ case COMPONENT_TYPE_MEDIA_GALLERY:
440
+ if (Array.isArray(comp.items) && comp.items.length > 0) out.hasImage = true;
441
+ break;
442
+ case COMPONENT_TYPE_THUMBNAIL:
443
+ if (comp.media && (comp.media.url || comp.media.proxy_url || comp.media.proxyURL)) out.hasImage = true;
444
+ break;
445
+ case COMPONENT_TYPE_FILE:
446
+ out.hasAttachment = true;
447
+ if (comp.file && typeof comp.file.url === 'string' && /\.(png|jpe?g|gif|webp|bmp|svg|mp4|webm|mov)(\?|$)/i.test(comp.file.url)) out.hasImage = true;
448
+ break;
449
+ default:
450
+ if (Array.isArray(comp.components)) for (const c of comp.components) walk(c);
451
+ break;
452
+ }
453
+ };
454
+ if (Array.isArray(message.components)) for (const c of message.components) walk(c);
455
+
456
+ return out;
457
+ }
458
+
459
+ function buildSlashCommandData(message) {
381
460
  const interaction = message?.interaction;
382
461
  const metadata = message?.interactionMetadata;
383
462
  const optsRaw = message?.interactionOptions || interaction?.options;
384
- // Build command path: /cmd group? subcommand? plus key:value options
385
- if (!optsRaw && !metadata) return null;
463
+ if (!interaction && !metadata) return null;
386
464
 
387
- const opts = Array.isArray(optsRaw) ? optsRaw : [];
388
- let cmdParts = [interaction?.commandName || '?'];
389
- let valueOpts = opts;
465
+ let cmdParts = [interaction?.commandName || metadata?.name || '?'];
466
+ let valueOpts = Array.isArray(optsRaw) ? optsRaw : [];
390
467
 
391
- // Detect subcommand groups: nested option structure { type:2/1 (group/subcommand), name, options }
468
+ // Walk subcommand-group → subcommand nesting.
392
469
  for (let i = 0; i < 2; i++) {
393
470
  if (valueOpts.length === 1 && valueOpts[0] && (valueOpts[0].type === 1 || valueOpts[0].type === 2)) {
394
471
  cmdParts.push(valueOpts[0].name);
395
472
  valueOpts = Array.isArray(valueOpts[0].options) ? valueOpts[0].options : [];
396
473
  }
397
474
  }
398
- // Filter out autocomplete-only `focused` options
399
475
  valueOpts = valueOpts.filter((o) => o && !o.focused);
400
476
 
401
- if (!valueOpts.length && cmdParts.length === 1 && !metadata) return null;
402
-
403
- const triggeredBy = metadata?.user || interaction?.user;
477
+ const triggeredBy = metadata?.user || interaction?.user || null;
478
+ const opts = valueOpts.map((o) => ({
479
+ name: String(o.name ?? ''),
480
+ value: o.value == null ? '' : String(o.value),
481
+ type: typeof o.type === 'number' ? o.type : null,
482
+ }));
404
483
 
405
- return (0, jsx_runtime_1.jsxs)("div", { className: "dht-slash-options", children: [
406
- (0, jsx_runtime_1.jsx)("span", { className: "dht-slash-cmd-name", children: '/' + cmdParts.filter(Boolean).join(' ') }),
407
- valueOpts.map((o, i) => (0, jsx_runtime_1.jsxs)("span", { className: "dht-slash-opt", children: [
408
- (0, jsx_runtime_1.jsxs)("span", { className: "dht-slash-opt-name", children: [String(o.name), ':'] }),
409
- ' ',
410
- (0, jsx_runtime_1.jsx)("span", { className: "dht-slash-opt-val", children: String(o.value ?? '') })
411
- ] }, i)),
412
- triggeredBy && (0, jsx_runtime_1.jsxs)("span", { className: "dht-slash-opt", children: ['by ', (0, jsx_runtime_1.jsx)("strong", { children: triggeredBy.displayName || triggeredBy.username || triggeredBy.id })] })
413
- ] });
484
+ return {
485
+ cmd: '/' + cmdParts.filter(Boolean).join(' '),
486
+ opts,
487
+ by: triggeredBy ? (triggeredBy.displayName || triggeredBy.username || triggeredBy.id || '') : '',
488
+ byId: triggeredBy ? (triggeredBy.id || '') : '',
489
+ };
414
490
  }
415
491
 
416
492
  function renderEditHistory(history, context) {
@@ -16,6 +16,7 @@ async function SystemMessage({ message, context }) {
16
16
  const author = message.author;
17
17
  const authorName = author?.displayName ?? author?.username ?? 'Unknown';
18
18
  const color = message.member?.roles?.color?.hexColor;
19
+ const authorId = author?.id;
19
20
  const ctx = context || {};
20
21
 
21
22
  switch (message.type) {
@@ -25,12 +26,31 @@ async function SystemMessage({ message, context }) {
25
26
 
26
27
  case discord_js_1.MessageType.ChannelPinnedMessage: {
27
28
  const refId = message.reference?.messageId;
28
- return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "pin", children: [
29
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
30
- ' ',
31
- (0, jsx_runtime_1.jsx)("span", { "data-i18n": "pinned", children: t(ctx, 'pinned', 'pinned a message to this channel.') }),
32
- refId ? (0, jsx_runtime_1.jsx)("i", { "data-goto": refId, className: "dht-jump", role: "button", tabIndex: 0, children: " (jump)" }) : null,
33
- ] }, message.id));
29
+ // Discord renders this as: "{user} pinned {a message} to this channel."
30
+ // where "a message" is the clickable jump link. The i18n key `pinned`
31
+ // accepts a `{message}` placeholder; if absent (legacy dict), we fall
32
+ // back to flat text + a trailing " (jump)".
33
+ const template = t(ctx, 'pinned', 'pinned {message} to this channel.');
34
+ const linkLabel = t(ctx, 'pinnedLink', 'a message');
35
+ const linkEl = refId
36
+ ? (0, jsx_runtime_1.jsx)("i", { "data-goto": refId, className: "dht-jump", role: "button", tabIndex: 0, children: linkLabel })
37
+ : (0, jsx_runtime_1.jsx)("strong", { children: linkLabel });
38
+ const parts = template.includes('{message}') ? template.split('{message}') : null;
39
+ return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "pin", children: parts
40
+ ? [
41
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
42
+ ' ',
43
+ parts[0],
44
+ linkEl,
45
+ parts[1],
46
+ ]
47
+ : [
48
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
49
+ ' ',
50
+ (0, jsx_runtime_1.jsx)("span", { "data-i18n": "pinned", children: template }),
51
+ refId ? (0, jsx_runtime_1.jsx)("i", { "data-goto": refId, className: "dht-jump", role: "button", tabIndex: 0, children: " (jump)" }) : null,
52
+ ]
53
+ }, message.id));
34
54
  }
35
55
 
36
56
  case discord_js_1.MessageType.GuildBoost:
@@ -38,7 +58,7 @@ async function SystemMessage({ message, context }) {
38
58
  case discord_js_1.MessageType.GuildBoostTier2:
39
59
  case discord_js_1.MessageType.GuildBoostTier3:
40
60
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "boost", children: [
41
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
61
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
42
62
  ' ',
43
63
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "boosted", children: t(ctx, 'boosted', 'boosted the server!') }),
44
64
  ] }, message.id));
@@ -48,7 +68,7 @@ async function SystemMessage({ message, context }) {
48
68
  const refId = message.reference?.messageId;
49
69
  const label = message.content || 'New thread';
50
70
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "thread", children: [
51
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
71
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
52
72
  ' ',
53
73
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "threadStarted", children: t(ctx, 'threadStarted', 'started a thread:') }),
54
74
  ' ',
@@ -60,7 +80,7 @@ async function SystemMessage({ message, context }) {
60
80
 
61
81
  case discord_js_1.MessageType.ChannelNameChange:
62
82
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "edit", children: [
63
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
83
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
64
84
  ' ',
65
85
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "changedChannelName", children: t(ctx, 'changedChannelName', 'changed the channel name:') }),
66
86
  ' ',
@@ -69,7 +89,7 @@ async function SystemMessage({ message, context }) {
69
89
 
70
90
  case discord_js_1.MessageType.ChannelIconChange:
71
91
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "edit", children: [
72
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
92
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
73
93
  ' ',
74
94
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "changedChannelIcon", children: t(ctx, 'changedChannelIcon', 'changed the channel icon.') }),
75
95
  ] }, message.id));
@@ -78,7 +98,7 @@ async function SystemMessage({ message, context }) {
78
98
  case discord_js_1.MessageType.ContextMenuCommand: {
79
99
  const cmd = message.interaction?.commandName ? '/' + message.interaction.commandName : 'a command';
80
100
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "call", children: [
81
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
101
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
82
102
  ' ',
83
103
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "usedCommand", children: t(ctx, 'usedCommand', 'used') }),
84
104
  ' ',
@@ -88,7 +108,7 @@ async function SystemMessage({ message, context }) {
88
108
 
89
109
  case discord_js_1.MessageType.Call:
90
110
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "call", children: [
91
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
111
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
92
112
  ' ',
93
113
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "startedCall", children: t(ctx, 'startedCall', 'started a call.') }),
94
114
  ] }, message.id));
@@ -97,7 +117,7 @@ async function SystemMessage({ message, context }) {
97
117
  const followText = t(ctx, 'addedFollow', 'added {target} to follow this channel.');
98
118
  const parts = followText.split('{target}');
99
119
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "edit", children: [
100
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
120
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
101
121
  ' ',
102
122
  parts[0],
103
123
  (0, jsx_runtime_1.jsxs)("span", { className: "dht-mention dht-mention--channel", children: ['#', message.content || 'channel'] }),
@@ -107,7 +127,7 @@ async function SystemMessage({ message, context }) {
107
127
 
108
128
  case discord_js_1.MessageType.RecipientRemove:
109
129
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "leave", children: [
110
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
130
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
111
131
  ' ',
112
132
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "left", children: t(ctx, 'left', 'left.') }),
113
133
  ] }, message.id));
@@ -132,34 +152,34 @@ async function SystemMessage({ message, context }) {
132
152
  }
133
153
  case discord_js_1.MessageType.StageStart:
134
154
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "call", children: [
135
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
155
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
136
156
  ' started a stage: ',
137
157
  (0, jsx_runtime_1.jsx)("strong", { children: message.content || '' })
138
158
  ] }, message.id));
139
159
  case discord_js_1.MessageType.StageEnd:
140
160
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "leave", children: [
141
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
161
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
142
162
  ' ended the stage.'
143
163
  ] }, message.id));
144
164
  case discord_js_1.MessageType.StageSpeaker:
145
165
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "call", children: [
146
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
166
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
147
167
  ' is now a speaker.'
148
168
  ] }, message.id));
149
169
  case discord_js_1.MessageType.StageRaiseHand:
150
170
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "call", children: [
151
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
171
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
152
172
  ' requested to speak.'
153
173
  ] }, message.id));
154
174
  case discord_js_1.MessageType.StageTopic:
155
175
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "edit", children: [
156
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
176
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
157
177
  ' changed the stage topic: ',
158
178
  (0, jsx_runtime_1.jsx)("strong", { children: message.content || '' })
159
179
  ] }, message.id));
160
180
  case discord_js_1.MessageType.GuildApplicationPremiumSubscription:
161
181
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "boost", children: [
162
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
182
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
163
183
  ' subscribed ',
164
184
  (0, jsx_runtime_1.jsx)("strong", { children: message.applicationId ? `app ${message.applicationId}` : 'an app' }),
165
185
  ' to this server.'
@@ -168,7 +188,7 @@ async function SystemMessage({ message, context }) {
168
188
  case discord_js_1.MessageType.RoleSubscriptionPurchase: {
169
189
  const roleName = message.roleSubscriptionData?.tierName || message.content || 'a role';
170
190
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "boost", children: [
171
- (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
191
+ (0, jsx_runtime_1.jsx)(Highlight, { color, userId: authorId, children: authorName }),
172
192
  ' ',
173
193
  t(ctx, 'roleSubPurchased', 'subscribed to {role}!').replace('{role}', roleName),
174
194
  ] }, message.id));
@@ -193,9 +213,16 @@ async function SystemMessage({ message, context }) {
193
213
  }
194
214
  }
195
215
 
196
- function Highlight({ children, color }) {
216
+ function Highlight({ children, color, userId }) {
197
217
  const safe = typeof color === 'string' && /^#[0-9a-fA-F]{3,8}$/.test(color) ? color : 'white';
198
- return (0, jsx_runtime_1.jsx)("i", { style: { color: safe }, children: children });
218
+ const props = { style: { color: safe } };
219
+ // Wire up the user popup the same way mentions and message authors do.
220
+ if (typeof userId === 'string' && userId) {
221
+ props.className = 'dht-sys-author';
222
+ props['data-mention-type'] = 'user';
223
+ props['data-mention-id'] = userId;
224
+ }
225
+ return (0, jsx_runtime_1.jsx)("i", Object.assign({}, props, { children }));
199
226
  }
200
227
 
201
228
  const allJoinMessages = [
@@ -252,11 +279,12 @@ function pickJoinMessage(seed) {
252
279
 
253
280
  function JoinMessage({ member, fallbackUser, seed, context }) {
254
281
  const template = pickJoinMessage(seed);
282
+ const uid = member?.id || fallbackUser?.id;
255
283
  return template
256
284
  .split('{user}')
257
285
  .flatMap((item, i) => [
258
286
  item,
259
- (0, jsx_runtime_1.jsx)(Highlight, { color: member?.roles?.color?.hexColor, children: member?.nickname ?? fallbackUser.displayName ?? fallbackUser.username }, i),
287
+ (0, jsx_runtime_1.jsx)(Highlight, { color: member?.roles?.color?.hexColor, userId: uid, children: member?.nickname ?? fallbackUser.displayName ?? fallbackUser.username }, i),
260
288
  ])
261
289
  .slice(0, -1);
262
290
  }
@@ -48,15 +48,35 @@ async function DiscordMessages(_a) {
48
48
  dateSeps[idx] && (0, jsx_runtime_1.jsx)("div", { className: "dht-date-sep", children: dateSeps[idx] }, `sep-${idx}`),
49
49
  (0, jsx_runtime_1.jsx)(message_1.default, { message: message, context: Object.assign({ messages, channel, callbacks }, options) }, message.id)
50
50
  ]),
51
- (0, jsx_runtime_1.jsxs)("div", { style: { textAlign: 'center', width: '100%' }, children: [
52
- options.footerText
53
- ? options.footerText.replaceAll('{number}', messages.length.toString()).replaceAll('{s}', messages.length > 1 ? 's' : '')
54
- : `Exported ${messages.length} message${messages.length > 1 ? 's' : ''}.`,
55
- ' ',
56
- options.poweredBy
57
- ? (0, jsx_runtime_1.jsxs)("span", { style: { textAlign: 'center' }, children: ["Powered by ", (0, jsx_runtime_1.jsx)("a", { href: "https://github.com/ItzDerock/discord-html-transcripts", style: { color: 'lightblue' }, children: "discord-html-transcripts" }), "."] })
58
- : null
59
- ] })
51
+ renderLegacyFooter(messages, options),
60
52
  ] }));
61
53
  }
54
+
55
+ // Legacy "Exported X message(s). Powered by …" line. The new stats footer
56
+ // (see renderStatsFooter in generator/index.js) already shows the message
57
+ // count, so this line is suppressed by default and only renders when:
58
+ // - options.statsFooter === false (caller opted out of the new footer), or
59
+ // - options.footerText is explicitly set to a string, or
60
+ // - options.poweredBy === true (caller wants the credit link).
61
+ function renderLegacyFooter(messages, options) {
62
+ const statsDisabled = options.statsFooter === false ||
63
+ (options.statsFooter && options.statsFooter.enabled === false);
64
+ const customText = (typeof options.footerText === 'string' && options.footerText.length > 0)
65
+ ? options.footerText.replaceAll('{number}', messages.length.toString()).replaceAll('{s}', messages.length > 1 ? 's' : '')
66
+ : null;
67
+ const wantsPowered = !!options.poweredBy;
68
+ // Default render only if stats footer is off AND no custom text was set.
69
+ const defaultText = statsDisabled && !customText
70
+ ? `Exported ${messages.length} message${messages.length > 1 ? 's' : ''}.`
71
+ : null;
72
+ const shown = customText || defaultText;
73
+ if (!shown && !wantsPowered) return null;
74
+ return (0, jsx_runtime_1.jsxs)("div", { style: { textAlign: 'center', width: '100%' }, children: [
75
+ shown,
76
+ shown && wantsPowered ? ' ' : null,
77
+ wantsPowered
78
+ ? (0, jsx_runtime_1.jsxs)("span", { style: { textAlign: 'center' }, children: ["Powered by ", (0, jsx_runtime_1.jsx)("a", { href: "https://github.com/ItzDerock/discord-html-transcripts", style: { color: 'lightblue' }, children: "discord-html-transcripts" }), "."] })
79
+ : null
80
+ ] });
81
+ }
62
82
  //# sourceMappingURL=transcript.js.map