discord-html-transcripts-fix 1.2.0 → 1.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.
@@ -47,8 +47,19 @@ const embed_1 = require("./embed");
47
47
  const reply_1 = __importDefault(require("./reply"));
48
48
  const systemMessage_1 = __importDefault(require("./systemMessage"));
49
49
 
50
+ const FLAG_CROSSPOSTED = 1 << 0;
51
+ const FLAG_IS_CROSSPOST = 1 << 1;
50
52
  const FLAG_SUPPRESS_EMBEDS = 1 << 2;
53
+ const FLAG_SUPPRESS_NOTIFICATIONS = 1 << 12;
51
54
  const FLAG_IS_VOICE_MESSAGE = 1 << 13;
55
+ const FLAG_HAS_SNAPSHOT = 1 << 14;
56
+ const ACTIVITY_TYPES = { 1: 'Join', 2: 'Spectate', 3: 'Listen', 5: 'Join Request' };
57
+
58
+ function abbrCount(n) {
59
+ if (n < 1000) return String(n);
60
+ if (n < 1000000) return (n / 1000).toFixed(n < 10000 ? 1 : 0).replace(/\.0$/, '') + 'K';
61
+ return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
62
+ }
52
63
 
53
64
  function t(context, key, fallback) {
54
65
  const dict = context?.i18n?.[context?.lang] || context?.i18n?.en || {};
@@ -60,7 +71,18 @@ async function DiscordMessage({ message, context }) {
60
71
  if (message.system)
61
72
  return (0, jsx_runtime_1.jsx)(systemMessage_1.default, { message: message, context: context });
62
73
 
63
- const isCrosspost = message.reference && message.reference.guildId !== message.guild?.id;
74
+ const isCrossGuildReply = message.reference && message.reference.guildId !== message.guild?.id;
75
+
76
+ // Data-attrs for client-side filtering (keyword/author/role/date/has-*).
77
+ const memberRoleIds = message.member?.roles?.cache
78
+ ? Array.from(message.member.roles.cache.keys()).join(',')
79
+ : '';
80
+ const authorName = (message.member?.nickname || message.author?.displayName || message.author?.username || '').toLowerCase();
81
+ const ts = message.createdAt instanceof Date ? message.createdAt.getTime() : 0;
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;
64
86
  const stickers = message.stickers && message.stickers.size > 0
65
87
  ? Array.from(message.stickers.values()).map((s) => ({
66
88
  id: s.id,
@@ -74,6 +96,13 @@ async function DiscordMessage({ message, context }) {
74
96
  const flags = (typeof message.flags?.bitfield === 'number') ? message.flags.bitfield : 0;
75
97
  const suppressEmbeds = !!(flags & FLAG_SUPPRESS_EMBEDS);
76
98
  const isVoice = !!(flags & FLAG_IS_VOICE_MESSAGE);
99
+ const isCrosspost = !!(flags & FLAG_IS_CROSSPOST);
100
+ const isCrossposted = !!(flags & FLAG_CROSSPOSTED);
101
+ const isSilent = !!(flags & FLAG_SUPPRESS_NOTIFICATIONS);
102
+ const activity = message.activity ? {
103
+ type: ACTIVITY_TYPES[message.activity.type] || 'Activity',
104
+ partyId: message.activity.partyId,
105
+ } : null;
77
106
 
78
107
  // Forwarded message snapshots (messageSnapshots) — render before the empty body
79
108
  const snapshots = Array.isArray(message.messageSnapshots) ? message.messageSnapshots : [];
@@ -82,19 +111,75 @@ async function DiscordMessage({ message, context }) {
82
111
 
83
112
  const editedAtIso = message.editedAt instanceof Date ? message.editedAt.toISOString() : null;
84
113
  const pinned = !!message.pinned;
114
+ const verifiedBot = !!message.author?.flags?.has?.(discord_js_1.UserFlags.VerifiedBot);
115
+
116
+ // Forum applied tags — only on the first message of a forum thread (the post itself)
117
+ let appliedTagPills = null;
118
+ if (message.channel?.isThread?.() && message.channel.parent?.availableTags && Array.isArray(message.channel.appliedTags) && message.channel.appliedTags.length > 0) {
119
+ const isFirstInThread = message.id === message.channel.id; // Forum post: thread id === starter message id
120
+ if (isFirstInThread) {
121
+ const available = message.channel.parent.availableTags;
122
+ appliedTagPills = (0, jsx_runtime_1.jsx)("div", {
123
+ className: "dht-applied-tags",
124
+ children: message.channel.appliedTags.map((tagId) => {
125
+ const tag = available.find((t) => t.id === tagId);
126
+ if (!tag) return null;
127
+ return (0, jsx_runtime_1.jsxs)("span", { className: "dht-tag-pill", children: [
128
+ tag.emoji?.name && (0, jsx_runtime_1.jsx)("span", { className: "dht-tag-emoji", children: tag.emoji.name }),
129
+ ' ',
130
+ tag.name
131
+ ] }, tagId);
132
+ })
133
+ });
134
+ }
135
+ }
136
+
137
+ // Build a comma-separated badge list for additional flag rendering
138
+ const slashCmdLine = renderSlashCommandLine(message, context);
139
+
140
+ // Resolve author profile data for Skyra slot attributes — without these the
141
+ // <discord-message> web component falls back to a generic display
142
+ const authorMember = message.member;
143
+ const authorDisplay = authorMember?.nickname ?? message.author?.displayName ?? message.author?.username;
144
+ const authorAvatar = authorMember?.displayAvatarURL?.({ size: 64 }) ?? message.author?.displayAvatarURL?.({ size: 64 });
145
+ const authorRoleHex = authorMember?.displayHexColor && authorMember.displayHexColor !== '#000000' ? authorMember.displayHexColor : undefined;
85
146
 
86
147
  return ((0, jsx_runtime_1.jsxs)("discord-message", {
87
148
  id: `m-${message.id}`,
88
149
  timestamp: message.createdAt,
89
150
  edited: message.editedAt !== null,
90
- server: isCrosspost ?? undefined,
151
+ server: (isCrossGuildReply || isCrosspost || isCrossposted) ? true : undefined,
91
152
  highlight: message.mentions.everyone || pinned,
92
153
  profile: message.author.id,
154
+ author: authorDisplay,
155
+ avatar: authorAvatar,
156
+ roleColor: authorRoleHex,
157
+ bot: message.author?.bot && !message.webhookId ? true : undefined,
158
+ verified: verifiedBot ? true : undefined,
159
+ className: pinned ? 'dht-msg dht-msg-pinned' : 'dht-msg',
160
+ "data-author-id": message.author.id,
161
+ "data-author-name": authorName,
162
+ "data-roles": memberRoleIds,
163
+ "data-timestamp": ts ? String(ts) : undefined,
164
+ "data-text": lowerText,
165
+ "data-pinned": pinned ? 'true' : undefined,
166
+ "data-has-image": hasImage ? 'true' : undefined,
167
+ "data-has-embed": hasEmbed ? 'true' : undefined,
168
+ "data-has-attachment": hasAttachment ? 'true' : undefined,
93
169
  children: [
94
- pinned && (0, jsx_runtime_1.jsx)("div", { className: "dht-pinned-indicator", title: "Pinned", children: "📌" }),
170
+ pinned && (0, jsx_runtime_1.jsx)("div", { className: "dht-pinned-indicator", title: "Pinned message", children: "📌" }),
171
+ appliedTagPills,
95
172
  (0, jsx_runtime_1.jsx)(reply_1.default, { message: message, context: context }),
96
173
  message.interaction && ((0, jsx_runtime_1.jsx)("discord-command", { slot: "reply", profile: message.interaction.user.id, command: '/' + message.interaction.commandName })),
97
- isApp && (0, jsx_runtime_1.jsx)("span", { className: "dht-app-badge", "data-i18n": "appBadge", children: t(context, 'appBadge', 'APP') }),
174
+ slashCmdLine,
175
+ 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: '✓' })] }),
176
+ isCrossposted && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-crosspost", title: "Published from announcement channel", children: '📣 Published' }),
177
+ isCrosspost && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-crosspost", title: "Follows another channel", children: '📡 Follow' }),
178
+ isSilent && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-silent", title: "Silent message (no notification)", children: '🔕 Silent' }),
179
+ activity && (0, jsx_runtime_1.jsxs)("div", { className: "dht-activity", children: [
180
+ (0, jsx_runtime_1.jsx)("span", { className: "dht-activity-icon", children: '🎮' }),
181
+ (0, jsx_runtime_1.jsxs)("span", { children: [activity.type, ' invite', activity.partyId ? ` · Party ${activity.partyId.slice(-6)}` : ''] })
182
+ ] }),
98
183
  editedAtIso && (0, jsx_runtime_1.jsx)("span", { className: "dht-edit-marker", title: editedAtIso, "data-edit-iso": editedAtIso, "data-i18n": "edited", "data-i18n-params": JSON.stringify({ time: editedAtIso }), children: '(' + t(context, 'edited', 'edited') + ')' }),
99
184
  Array.isArray(message.editHistory) && message.editHistory.length > 0 && renderEditHistory(message.editHistory, context),
100
185
  snapshots.length > 0 && renderSnapshots(snapshots, context),
@@ -109,13 +194,26 @@ async function DiscordMessage({ message, context }) {
109
194
  message.reactions.cache.size > 0 && ((0, jsx_runtime_1.jsx)("discord-reactions", {
110
195
  slot: "reactions",
111
196
  children: Array.from(message.reactions.cache.values()).map((reaction, id) => {
112
- const burst = reaction.burst_count > 0 || (reaction.count_details?.burst > 0);
197
+ const total = reaction.count ?? 0;
198
+ const burstCount = reaction.count_details?.burst || reaction.burst_count || 0;
199
+ const burst = burstCount > 0;
200
+ const burstColors = Array.isArray(reaction.burst_colors) && reaction.burst_colors.length
201
+ ? reaction.burst_colors.filter((c) => typeof c === 'string' && /^#[0-9a-fA-F]{3,8}$/.test(c))
202
+ : [];
203
+ const title = burst
204
+ ? `${total} reactions (${burstCount} super)`
205
+ : `${total} reaction${total !== 1 ? 's' : ''}`;
206
+ const style = burst && burstColors.length
207
+ ? { boxShadow: `inset 0 0 0 2px ${burstColors[0]}` }
208
+ : undefined;
113
209
  return ((0, jsx_runtime_1.jsx)("discord-reaction", {
114
210
  name: reaction.emoji?.name || ':unknown:',
115
211
  emoji: (0, utils_1.parseDiscordEmoji)(reaction.emoji) || undefined,
116
- count: reaction.count ?? 0,
212
+ count: abbrCount(total),
213
+ title,
117
214
  "data-burst": burst ? 'true' : undefined,
118
- className: burst ? 'dht-reaction--burst' : undefined
215
+ className: burst ? 'dht-reaction--burst' : undefined,
216
+ style,
119
217
  }, `${message.id}r${id}`));
120
218
  })
121
219
  })),
@@ -147,12 +245,25 @@ function renderThread(thread, context) {
147
245
  function renderStickers(stickers) {
148
246
  return (0, jsx_runtime_1.jsx)("div", {
149
247
  className: "dht-stickers",
150
- children: stickers.map((s) => (0, jsx_runtime_1.jsx)("img", {
151
- src: s.url,
152
- alt: s.name || 'sticker',
153
- title: s.name || '',
154
- className: "dht-sticker"
155
- }, s.id))
248
+ children: stickers.map((s) => {
249
+ // Discord sticker formats: 1=PNG, 2=APNG (animated png), 3=Lottie (JSON), 4=GIF
250
+ const format = s.format;
251
+ const baseUrl = `https://media.discordapp.net/stickers/${s.id}`;
252
+ if (format === 3) {
253
+ // Lottie — render as iframe to discord's lottie player CDN page, or fallback link
254
+ return (0, jsx_runtime_1.jsxs)("div", { className: "dht-sticker dht-sticker--lottie", title: s.name || '', children: [
255
+ (0, jsx_runtime_1.jsx)("div", { className: "dht-sticker-placeholder", children: '🎞️' }),
256
+ (0, jsx_runtime_1.jsxs)("span", { className: "dht-sticker-name", children: [s.name || 'sticker', ' (Lottie)'] })
257
+ ] }, s.id);
258
+ }
259
+ if (format === 4) {
260
+ return (0, jsx_runtime_1.jsx)("img", { src: s.url || `${baseUrl}.gif`, alt: s.name || 'sticker', title: s.name || '', className: "dht-sticker" }, s.id);
261
+ }
262
+ if (format === 2) {
263
+ return (0, jsx_runtime_1.jsx)("img", { src: s.url || `${baseUrl}.png`, alt: s.name || 'sticker', title: s.name || '', className: "dht-sticker dht-sticker--apng" }, s.id);
264
+ }
265
+ return (0, jsx_runtime_1.jsx)("img", { src: s.url || `${baseUrl}.png`, alt: s.name || 'sticker', title: s.name || '', className: "dht-sticker" }, s.id);
266
+ })
156
267
  });
157
268
  }
158
269
 
@@ -160,7 +271,8 @@ function renderPoll(poll, context) {
160
271
  let totalVotes = 0;
161
272
  const answers = poll.answers ? Array.from(poll.answers.values()) : [];
162
273
  for (const a of answers) totalVotes += a.voteCount || 0;
163
- const expires = poll.expiresAt instanceof Date ? poll.expiresAt.toLocaleString('de-DE') : '';
274
+ const locale = context?.lang === 'de' ? 'de-DE' : 'en-US';
275
+ const expires = poll.expiresAt instanceof Date ? poll.expiresAt.toLocaleString(locale) : '';
164
276
 
165
277
  return (0, jsx_runtime_1.jsxs)("div", {
166
278
  className: "dht-poll",
@@ -187,51 +299,132 @@ function renderPoll(poll, context) {
187
299
  }
188
300
 
189
301
  function renderVoiceIndicator(message, context) {
190
- const audio = message.attachments?.find?.((a) => typeof a.contentType === 'string' && a.contentType.startsWith('audio/'));
302
+ // FIX: discord.js Collection.find takes a predicate but on Map it conflicts; iterate values explicitly.
303
+ let audio = null;
304
+ if (message.attachments && typeof message.attachments.values === 'function') {
305
+ for (const a of message.attachments.values()) {
306
+ if (typeof a.contentType === 'string' && a.contentType.startsWith('audio/')) { audio = a; break; }
307
+ }
308
+ }
191
309
  const duration = audio?.duration_secs || audio?.durationSecs;
192
310
  const waveformB64 = audio?.waveform;
193
- let waveformSvg = null;
194
- if (typeof waveformB64 === 'string' && waveformB64.length > 0) {
311
+ let waveformRects = null;
312
+ let waveformWidth = 0;
313
+ if (typeof waveformB64 === 'string' && waveformB64.length > 0 && waveformB64.length < 16384) {
195
314
  try {
196
315
  const buf = Buffer.from(waveformB64, 'base64');
197
316
  const bars = Array.from(buf).slice(0, 100);
198
317
  const max = bars.length ? Math.max(1, ...bars) : 1;
199
- const width = bars.length * 3;
200
- const rects = bars.map((b, i) => {
318
+ waveformWidth = bars.length * 3;
319
+ waveformRects = bars.map((b, i) => {
201
320
  const h = Math.max(2, (b / max) * 20);
202
- return `<rect x="${i * 3}" y="${(22 - h) / 2}" width="2" height="${h}" rx="1" fill="#5865F2"/>`;
203
- }).join('');
204
- waveformSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} 22" width="${width}" height="22">${rects}</svg>`;
205
- } catch (_e) {}
321
+ return (0, jsx_runtime_1.jsx)("rect", { x: i * 3, y: (22 - h) / 2, width: 2, height: h, rx: 1, fill: "#5865F2" }, i);
322
+ });
323
+ } catch (_e) { waveformRects = null; }
206
324
  }
207
325
  return (0, jsx_runtime_1.jsxs)("div", { className: "dht-voice", children: [
208
326
  (0, jsx_runtime_1.jsx)("span", { className: "dht-voice-icon", children: '🎤' }),
209
327
  (0, jsx_runtime_1.jsx)("span", { className: "dht-voice-label", "data-i18n": "voiceMessage", children: t(context, 'voiceMessage', 'Voice message') }),
210
- waveformSvg && (0, jsx_runtime_1.jsx)("span", { className: "dht-voice-wave", dangerouslySetInnerHTML: { __html: waveformSvg } }),
328
+ waveformRects && (0, jsx_runtime_1.jsx)("svg", {
329
+ xmlns: "http://www.w3.org/2000/svg",
330
+ viewBox: `0 0 ${waveformWidth} 22`,
331
+ width: waveformWidth,
332
+ height: 22,
333
+ className: "dht-voice-wave",
334
+ children: waveformRects,
335
+ }),
211
336
  duration ? (0, jsx_runtime_1.jsxs)("span", { className: "dht-voice-dur", children: [Math.round(duration), 's'] }) : null,
212
337
  ] });
213
338
  }
214
339
 
215
- function renderSnapshots(snapshots, context) {
340
+ function toArray(maybeColl) {
341
+ if (!maybeColl) return [];
342
+ if (Array.isArray(maybeColl)) return maybeColl;
343
+ if (typeof maybeColl.values === 'function') return Array.from(maybeColl.values());
344
+ if (typeof maybeColl === 'object') return Object.values(maybeColl);
345
+ return [];
346
+ }
347
+
348
+ function renderSnapshots(snapshots, context, depth = 0) {
349
+ const arr = toArray(snapshots);
350
+ if (arr.length === 0 || depth >= 5) return null; // hard-cap at 5 levels
216
351
  return (0, jsx_runtime_1.jsx)("div", {
217
352
  className: "dht-forwarded-wrap",
218
- children: snapshots.map((snap, i) => {
353
+ children: arr.map((snap, i) => {
219
354
  const m = snap.message || snap;
220
355
  const author = m?.author?.displayName || m?.author?.username || 'Unknown';
221
356
  const text = m?.content || '';
222
- return (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded", children: [
357
+ const attachments = toArray(m?.attachments);
358
+ const nested = toArray(m?.messageSnapshots);
359
+ const embeds = toArray(m?.embeds);
360
+ const stickers = toArray(m?.stickers).map((s) => ({
361
+ id: s.id,
362
+ name: s.name,
363
+ url: s.url || `https://media.discordapp.net/stickers/${s.id}.png`,
364
+ format: s.format,
365
+ }));
366
+ return (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded", "data-depth": depth, children: [
223
367
  (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded-head", children: [
224
- (0, jsx_runtime_1.jsx)("span", { className: "dht-forwarded-icon", children: '↪️' }),
368
+ (0, jsx_runtime_1.jsx)("span", { className: "dht-forwarded-icon", children: '↪' }),
225
369
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "forwardedFrom", children: t(context, 'forwardedFrom', 'Forwarded from') }),
226
370
  (0, jsx_runtime_1.jsx)("strong", { children: ' ' + author }),
227
371
  ] }),
228
- text && (0, jsx_runtime_1.jsx)("div", { className: "dht-forwarded-body", children: (0, jsx_runtime_1.jsx)(content_1.default, { content: text, context: Object.assign({}, context, { type: content_1.RenderType.REPLY }) }) }),
229
- Array.isArray(m?.attachments) && m.attachments.length > 0 && (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded-attachments", children: [m.attachments.length, ' attachment(s)'] }),
372
+ text && (0, jsx_runtime_1.jsx)("div", { className: "dht-forwarded-body", children: (0, jsx_runtime_1.jsx)(content_1.default, { content: text, context: Object.assign({}, context, { type: content_1.RenderType.NORMAL }) }) }),
373
+ attachments.length > 0 && (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded-attachments", children: [
374
+ (0, jsx_runtime_1.jsxs)("strong", { children: ['📎 ', attachments.length, ' attachment', attachments.length !== 1 ? 's' : '', ':'] }),
375
+ (0, jsx_runtime_1.jsx)("ul", { children: attachments.map((a, ai) => {
376
+ const safeUrl = (0, utils_1.safeHref)(a.url);
377
+ return (0, jsx_runtime_1.jsxs)("li", { children: [
378
+ a.contentType?.startsWith?.('image/') && a.url
379
+ ? (0, jsx_runtime_1.jsx)("img", { src: safeUrl !== '#' ? safeUrl : '', alt: a.name || '', style: { maxWidth: '200px', borderRadius: '4px', display: 'block', margin: '4px 0' } })
380
+ : (0, jsx_runtime_1.jsxs)("a", { href: safeUrl, target: "_blank", rel: "noreferrer", children: [a.name || 'attachment', a.size ? ` (${Math.round(a.size / 1024)} KB)` : ''] })
381
+ ] }, ai);
382
+ }) })
383
+ ] }),
384
+ stickers.length > 0 && renderStickers(stickers),
385
+ embeds.length > 0 && (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded-embeds", children: ['📑 ', embeds.length, ' embed', embeds.length !== 1 ? 's' : ''] }),
386
+ nested.length > 0 && renderSnapshots(nested, context, depth + 1),
230
387
  ] }, i);
231
388
  })
232
389
  });
233
390
  }
234
391
 
392
+ function renderSlashCommandLine(message, context) {
393
+ const interaction = message?.interaction;
394
+ const metadata = message?.interactionMetadata;
395
+ const optsRaw = message?.interactionOptions || interaction?.options;
396
+ // Build command path: /cmd group? subcommand? plus key:value options
397
+ if (!optsRaw && !metadata) return null;
398
+
399
+ const opts = Array.isArray(optsRaw) ? optsRaw : [];
400
+ let cmdParts = [interaction?.commandName || '?'];
401
+ let valueOpts = opts;
402
+
403
+ // Detect subcommand groups: nested option structure { type:2/1 (group/subcommand), name, options }
404
+ for (let i = 0; i < 2; i++) {
405
+ if (valueOpts.length === 1 && valueOpts[0] && (valueOpts[0].type === 1 || valueOpts[0].type === 2)) {
406
+ cmdParts.push(valueOpts[0].name);
407
+ valueOpts = Array.isArray(valueOpts[0].options) ? valueOpts[0].options : [];
408
+ }
409
+ }
410
+ // Filter out autocomplete-only `focused` options
411
+ valueOpts = valueOpts.filter((o) => o && !o.focused);
412
+
413
+ if (!valueOpts.length && cmdParts.length === 1 && !metadata) return null;
414
+
415
+ const triggeredBy = metadata?.user || interaction?.user;
416
+
417
+ return (0, jsx_runtime_1.jsxs)("div", { className: "dht-slash-options", children: [
418
+ (0, jsx_runtime_1.jsx)("span", { className: "dht-slash-cmd-name", children: '/' + cmdParts.filter(Boolean).join(' ') }),
419
+ valueOpts.map((o, i) => (0, jsx_runtime_1.jsxs)("span", { className: "dht-slash-opt", children: [
420
+ (0, jsx_runtime_1.jsxs)("span", { className: "dht-slash-opt-name", children: [String(o.name), ':'] }),
421
+ ' ',
422
+ (0, jsx_runtime_1.jsx)("span", { className: "dht-slash-opt-val", children: String(o.value ?? '') })
423
+ ] }, i)),
424
+ 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 })] })
425
+ ] });
426
+ }
427
+
235
428
  function renderEditHistory(history, context) {
236
429
  // Optional feature — host bot may attach `message.editHistory: [{content, editedAt}, …]`
237
430
  return (0, jsx_runtime_1.jsxs)("details", { className: "dht-edit-history", children: [
@@ -33,7 +33,12 @@ async function MessageReply({ message, context }) {
33
33
  return (0, jsx_runtime_1.jsx)("discord-reply", { slot: "reply", children: "Message could not be loaded." });
34
34
 
35
35
  const isCrossPost = referencedMessage.reference && referencedMessage.reference.guildId !== message.guild?.id;
36
- const isCommand = referencedMessage.interaction !== null;
36
+ const isCommand = !!referencedMessage.interaction;
37
+ // Special-case: replying to a forwarded-only / Components-V2-only message has empty content
38
+ const hasContent = !!referencedMessage.content;
39
+ const hasSnapshot = Array.isArray(referencedMessage.messageSnapshots) && referencedMessage.messageSnapshots.length > 0;
40
+ const hasComponents = Array.isArray(referencedMessage.components) && referencedMessage.components.length > 0;
41
+ const hasSticker = referencedMessage.stickers?.size > 0;
37
42
  const author = referencedMessage.member?.nickname ?? referencedMessage.author.displayName ?? referencedMessage.author.username;
38
43
  const avatar = referencedMessage.author.avatarURL?.({ size: 32 }) ?? undefined;
39
44
  const roleColor = referencedMessage.member?.displayHexColor ?? undefined;
@@ -51,11 +56,17 @@ async function MessageReply({ message, context }) {
51
56
  op: op,
52
57
  server: isCrossPost ?? undefined,
53
58
  command: isCommand,
54
- children: referencedMessage.content
59
+ children: hasContent
55
60
  ? (0, jsx_runtime_1.jsx)("span", { "data-goto": referencedMessage.id, className: "reply-inline", children: (0, jsx_runtime_1.jsx)(content_1.default, { content: referencedMessage.content, context: Object.assign({}, context, { type: content_1.RenderType.REPLY }) }) })
56
61
  : isCommand
57
62
  ? (0, jsx_runtime_1.jsx)("em", { "data-goto": referencedMessage.id, children: "Click to see command." })
58
- : (0, jsx_runtime_1.jsx)("em", { "data-goto": referencedMessage.id, children: "Click to see attachment." })
63
+ : hasSnapshot
64
+ ? (0, jsx_runtime_1.jsx)("em", { "data-goto": referencedMessage.id, children: "Click to see forwarded message." })
65
+ : hasComponents
66
+ ? (0, jsx_runtime_1.jsx)("em", { "data-goto": referencedMessage.id, children: "Click to see message." })
67
+ : hasSticker
68
+ ? (0, jsx_runtime_1.jsx)("em", { "data-goto": referencedMessage.id, children: "Click to see sticker." })
69
+ : (0, jsx_runtime_1.jsx)("em", { "data-goto": referencedMessage.id, children: "Click to see attachment." })
59
70
  }));
60
71
  }
61
72
  //# sourceMappingURL=reply.js.map
@@ -23,13 +23,15 @@ async function SystemMessage({ message, context }) {
23
23
  case discord_js_1.MessageType.UserJoin:
24
24
  return ((0, jsx_runtime_1.jsx)("discord-system-message", { id: `m-${message.id}`, type: "join", children: (0, jsx_runtime_1.jsx)(JoinMessage, { member: message.member, fallbackUser: author, seed: message.id, context: ctx }) }, message.id));
25
25
 
26
- case discord_js_1.MessageType.ChannelPinnedMessage:
26
+ case discord_js_1.MessageType.ChannelPinnedMessage: {
27
+ const refId = message.reference?.messageId;
27
28
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "pin", children: [
28
29
  (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
29
30
  ' ',
30
31
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "pinned", children: t(ctx, 'pinned', 'pinned a message to this channel.') }),
31
- (0, jsx_runtime_1.jsx)("i", { "data-goto": message.reference?.messageId, children: " (jump)" }),
32
+ refId ? (0, jsx_runtime_1.jsx)("i", { "data-goto": refId, className: "dht-jump", role: "button", tabIndex: 0, children: " (jump)" }) : null,
32
33
  ] }, message.id));
34
+ }
33
35
 
34
36
  case discord_js_1.MessageType.GuildBoost:
35
37
  case discord_js_1.MessageType.GuildBoostTier1:
@@ -42,14 +44,19 @@ async function SystemMessage({ message, context }) {
42
44
  ] }, message.id));
43
45
 
44
46
  case discord_js_1.MessageType.ThreadStarterMessage:
45
- case discord_js_1.MessageType.ThreadCreated:
47
+ case discord_js_1.MessageType.ThreadCreated: {
48
+ const refId = message.reference?.messageId;
49
+ const label = message.content || 'New thread';
46
50
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "thread", children: [
47
51
  (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
48
52
  ' ',
49
53
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "threadStarted", children: t(ctx, 'threadStarted', 'started a thread:') }),
50
54
  ' ',
51
- (0, jsx_runtime_1.jsx)("i", { "data-goto": message.reference?.messageId, children: message.content || 'New thread' }),
55
+ refId
56
+ ? (0, jsx_runtime_1.jsx)("i", { "data-goto": refId, className: "dht-jump", role: "button", tabIndex: 0, children: label })
57
+ : (0, jsx_runtime_1.jsx)("i", { children: label }),
52
58
  ] }, message.id));
59
+ }
53
60
 
54
61
  case discord_js_1.MessageType.ChannelNameChange:
55
62
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "edit", children: [
@@ -86,12 +93,17 @@ async function SystemMessage({ message, context }) {
86
93
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "startedCall", children: t(ctx, 'startedCall', 'started a call.') }),
87
94
  ] }, message.id));
88
95
 
89
- case discord_js_1.MessageType.ChannelFollowAdd:
96
+ case discord_js_1.MessageType.ChannelFollowAdd: {
97
+ const followText = t(ctx, 'addedFollow', 'added {target} to follow this channel.');
98
+ const parts = followText.split('{target}');
90
99
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "edit", children: [
91
100
  (0, jsx_runtime_1.jsx)(Highlight, { color, children: authorName }),
92
101
  ' ',
93
- t(ctx, 'addedFollow', 'added {target} to follow this channel.').replace('{target}', message.content || ''),
102
+ parts[0],
103
+ (0, jsx_runtime_1.jsxs)("span", { className: "dht-mention dht-mention--channel", children: ['#', message.content || 'channel'] }),
104
+ parts[1] || '',
94
105
  ] }, message.id));
106
+ }
95
107
 
96
108
  case discord_js_1.MessageType.RecipientRemove:
97
109
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "leave", children: [
@@ -101,16 +113,57 @@ async function SystemMessage({ message, context }) {
101
113
  ] }, message.id));
102
114
 
103
115
  case discord_js_1.MessageType.AutoModerationAction: {
104
- // Discord packs detail into the first embed: matched_keyword, blocked_content, channel_id, rule_name
116
+ // Discord packs detail into the first embed: rule_name, matched_keyword, action_type, channel_id
105
117
  const e = message.embeds?.[0];
106
- const ruleName = e?.fields?.find?.((f) => /rule/i.test(f.name))?.value || e?.title || '';
118
+ const fields = Array.isArray(e?.fields) ? e.fields : [];
119
+ const ruleName = fields.find((f) => /rule_name|rule name/i.test(f.name))?.value || e?.title || '';
120
+ const matchedKeyword = fields.find((f) => /matched_keyword|keyword/i.test(f.name))?.value || '';
121
+ const action = fields.find((f) => /action_type|action/i.test(f.name))?.value || '';
107
122
  const blocked = e?.description || '';
123
+ const channelId = fields.find((f) => /channel/i.test(f.name))?.value;
108
124
  return ((0, jsx_runtime_1.jsxs)("discord-system-message", { id: `m-${message.id}`, type: "boost", children: [
109
125
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "autoModBlocked", children: t(ctx, 'autoModBlocked', 'AutoMod blocked a message.') }),
110
- ruleName && (0, jsx_runtime_1.jsxs)("span", { className: "dht-automod-rule", children: [' · ', (0, utils_1.escapeHtml)(ruleName)] }),
126
+ ruleName && (0, jsx_runtime_1.jsxs)("span", { className: "dht-automod-rule", children: [' · Rule: ', (0, jsx_runtime_1.jsx)("strong", { children: ruleName })] }),
127
+ action && (0, jsx_runtime_1.jsxs)("span", { className: "dht-automod-rule", children: [' · Action: ', action] }),
128
+ matchedKeyword && (0, jsx_runtime_1.jsxs)("span", { className: "dht-automod-rule", children: [' · Matched: ', (0, jsx_runtime_1.jsx)("code", { children: matchedKeyword })] }),
111
129
  blocked && (0, jsx_runtime_1.jsx)("blockquote", { className: "dht-automod-content", children: blocked }),
130
+ channelId && (0, jsx_runtime_1.jsxs)("span", { className: "dht-automod-rule", children: [' · In <#', channelId, '>'] }),
112
131
  ] }, message.id));
113
132
  }
133
+ case discord_js_1.MessageType.StageStart:
134
+ 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 }),
136
+ ' started a stage: ',
137
+ (0, jsx_runtime_1.jsx)("strong", { children: message.content || '' })
138
+ ] }, message.id));
139
+ case discord_js_1.MessageType.StageEnd:
140
+ 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 }),
142
+ ' ended the stage.'
143
+ ] }, message.id));
144
+ case discord_js_1.MessageType.StageSpeaker:
145
+ 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 }),
147
+ ' is now a speaker.'
148
+ ] }, message.id));
149
+ case discord_js_1.MessageType.StageRaiseHand:
150
+ 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 }),
152
+ ' requested to speak.'
153
+ ] }, message.id));
154
+ case discord_js_1.MessageType.StageTopic:
155
+ 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 }),
157
+ ' changed the stage topic: ',
158
+ (0, jsx_runtime_1.jsx)("strong", { children: message.content || '' })
159
+ ] }, message.id));
160
+ case discord_js_1.MessageType.GuildApplicationPremiumSubscription:
161
+ 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 }),
163
+ ' subscribed ',
164
+ (0, jsx_runtime_1.jsx)("strong", { children: message.applicationId ? `app ${message.applicationId}` : 'an app' }),
165
+ ' to this server.'
166
+ ] }, message.id));
114
167
 
115
168
  case discord_js_1.MessageType.RoleSubscriptionPurchase: {
116
169
  const roleName = message.roleSubscriptionData?.tierName || message.content || 'a role';
package/dist/index.js CHANGED
@@ -44,11 +44,33 @@ async function generateFromMessages(messages, channel, options = {}) {
44
44
  }
45
45
  }
46
46
 
47
+ // Pre-build resolution maps from messages so mentions resolve to a name even
48
+ // when the user/role/channel isn't in client cache and can't be fetched.
49
+ const userIndex = new Map();
50
+ const roleIndex = new Map();
51
+ const channelIndex = new Map();
52
+ for (const m of transformedMessages) {
53
+ if (m.author) userIndex.set(m.author.id, m.author);
54
+ if (m.member?.user) userIndex.set(m.member.user.id, m.member.user);
55
+ if (m.interaction?.user) userIndex.set(m.interaction.user.id, m.interaction.user);
56
+ if (m.mentions) {
57
+ if (m.mentions.users) for (const [id, u] of m.mentions.users) userIndex.set(id, u);
58
+ if (m.mentions.roles) for (const [id, r] of m.mentions.roles) roleIndex.set(id, r);
59
+ if (m.mentions.channels) for (const [id, c] of m.mentions.channels) channelIndex.set(id, c);
60
+ }
61
+ }
62
+ if (!channel.isDMBased() && channel.guild) {
63
+ // pre-warm every role we can see (cheap — cache only)
64
+ for (const [id, r] of channel.guild.roles.cache) roleIndex.set(id, r);
65
+ }
66
+
47
67
  const callbacks = Object.assign({
48
68
  resolveImageSrc,
49
- resolveChannel: async (id) => channel.client.channels.fetch(id).catch(() => null),
50
- resolveUser: async (id) => channel.client.users.fetch(id).catch(() => null),
51
- resolveRole: channel.isDMBased() ? () => null : async (id) => channel.guild?.roles.fetch(id).catch(() => null),
69
+ resolveChannel: async (id) => channelIndex.get(id) || channel.client.channels.fetch(id).catch(() => null),
70
+ resolveUser: async (id) => userIndex.get(id) || channel.client.users.fetch(id).catch(() => null),
71
+ resolveRole: channel.isDMBased()
72
+ ? () => null
73
+ : async (id) => roleIndex.get(id) || channel.guild?.roles.fetch(id).catch(() => null),
52
74
  }, options.callbacks ?? {});
53
75
 
54
76
  const rendered = await (0, generator_1.default)({