discord-html-transcripts-fix 1.2.0 → 1.4.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,63 @@ 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);
85
139
 
86
140
  return ((0, jsx_runtime_1.jsxs)("discord-message", {
87
141
  id: `m-${message.id}`,
88
142
  timestamp: message.createdAt,
89
143
  edited: message.editedAt !== null,
90
- server: isCrosspost ?? undefined,
144
+ server: (isCrossGuildReply || isCrosspost || isCrossposted) ? true : undefined,
91
145
  highlight: message.mentions.everyone || pinned,
92
146
  profile: message.author.id,
147
+ className: pinned ? 'dht-msg dht-msg-pinned' : 'dht-msg',
148
+ "data-author-id": message.author.id,
149
+ "data-author-name": authorName,
150
+ "data-roles": memberRoleIds,
151
+ "data-timestamp": ts ? String(ts) : undefined,
152
+ "data-text": lowerText,
153
+ "data-pinned": pinned ? 'true' : undefined,
154
+ "data-has-image": hasImage ? 'true' : undefined,
155
+ "data-has-embed": hasEmbed ? 'true' : undefined,
156
+ "data-has-attachment": hasAttachment ? 'true' : undefined,
93
157
  children: [
94
- pinned && (0, jsx_runtime_1.jsx)("div", { className: "dht-pinned-indicator", title: "Pinned", children: "📌" }),
158
+ pinned && (0, jsx_runtime_1.jsx)("div", { className: "dht-pinned-indicator", title: "Pinned message", children: "📌" }),
159
+ appliedTagPills,
95
160
  (0, jsx_runtime_1.jsx)(reply_1.default, { message: message, context: context }),
96
161
  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') }),
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: '✓' })] }),
164
+ isCrossposted && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-crosspost", title: "Published from announcement channel", children: '📣 Published' }),
165
+ isCrosspost && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-crosspost", title: "Follows another channel", children: '📡 Follow' }),
166
+ isSilent && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-silent", title: "Silent message (no notification)", children: '🔕 Silent' }),
167
+ activity && (0, jsx_runtime_1.jsxs)("div", { className: "dht-activity", children: [
168
+ (0, jsx_runtime_1.jsx)("span", { className: "dht-activity-icon", children: '🎮' }),
169
+ (0, jsx_runtime_1.jsxs)("span", { children: [activity.type, ' invite', activity.partyId ? ` · Party ${activity.partyId.slice(-6)}` : ''] })
170
+ ] }),
98
171
  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
172
  Array.isArray(message.editHistory) && message.editHistory.length > 0 && renderEditHistory(message.editHistory, context),
100
173
  snapshots.length > 0 && renderSnapshots(snapshots, context),
@@ -109,13 +182,26 @@ async function DiscordMessage({ message, context }) {
109
182
  message.reactions.cache.size > 0 && ((0, jsx_runtime_1.jsx)("discord-reactions", {
110
183
  slot: "reactions",
111
184
  children: Array.from(message.reactions.cache.values()).map((reaction, id) => {
112
- const burst = reaction.burst_count > 0 || (reaction.count_details?.burst > 0);
185
+ const total = reaction.count ?? 0;
186
+ const burstCount = reaction.count_details?.burst || reaction.burst_count || 0;
187
+ const burst = burstCount > 0;
188
+ const burstColors = Array.isArray(reaction.burst_colors) && reaction.burst_colors.length
189
+ ? reaction.burst_colors.filter((c) => typeof c === 'string' && /^#[0-9a-fA-F]{3,8}$/.test(c))
190
+ : [];
191
+ const title = burst
192
+ ? `${total} reactions (${burstCount} super)`
193
+ : `${total} reaction${total !== 1 ? 's' : ''}`;
194
+ const style = burst && burstColors.length
195
+ ? { boxShadow: `inset 0 0 0 2px ${burstColors[0]}` }
196
+ : undefined;
113
197
  return ((0, jsx_runtime_1.jsx)("discord-reaction", {
114
198
  name: reaction.emoji?.name || ':unknown:',
115
199
  emoji: (0, utils_1.parseDiscordEmoji)(reaction.emoji) || undefined,
116
- count: reaction.count ?? 0,
200
+ count: abbrCount(total),
201
+ title,
117
202
  "data-burst": burst ? 'true' : undefined,
118
- className: burst ? 'dht-reaction--burst' : undefined
203
+ className: burst ? 'dht-reaction--burst' : undefined,
204
+ style,
119
205
  }, `${message.id}r${id}`));
120
206
  })
121
207
  })),
@@ -147,12 +233,25 @@ function renderThread(thread, context) {
147
233
  function renderStickers(stickers) {
148
234
  return (0, jsx_runtime_1.jsx)("div", {
149
235
  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))
236
+ children: stickers.map((s) => {
237
+ // Discord sticker formats: 1=PNG, 2=APNG (animated png), 3=Lottie (JSON), 4=GIF
238
+ const format = s.format;
239
+ const baseUrl = `https://media.discordapp.net/stickers/${s.id}`;
240
+ if (format === 3) {
241
+ // Lottie — render as iframe to discord's lottie player CDN page, or fallback link
242
+ return (0, jsx_runtime_1.jsxs)("div", { className: "dht-sticker dht-sticker--lottie", title: s.name || '', children: [
243
+ (0, jsx_runtime_1.jsx)("div", { className: "dht-sticker-placeholder", children: '🎞️' }),
244
+ (0, jsx_runtime_1.jsxs)("span", { className: "dht-sticker-name", children: [s.name || 'sticker', ' (Lottie)'] })
245
+ ] }, s.id);
246
+ }
247
+ if (format === 4) {
248
+ return (0, jsx_runtime_1.jsx)("img", { src: s.url || `${baseUrl}.gif`, alt: s.name || 'sticker', title: s.name || '', className: "dht-sticker" }, s.id);
249
+ }
250
+ if (format === 2) {
251
+ 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);
252
+ }
253
+ return (0, jsx_runtime_1.jsx)("img", { src: s.url || `${baseUrl}.png`, alt: s.name || 'sticker', title: s.name || '', className: "dht-sticker" }, s.id);
254
+ })
156
255
  });
157
256
  }
158
257
 
@@ -160,7 +259,8 @@ function renderPoll(poll, context) {
160
259
  let totalVotes = 0;
161
260
  const answers = poll.answers ? Array.from(poll.answers.values()) : [];
162
261
  for (const a of answers) totalVotes += a.voteCount || 0;
163
- const expires = poll.expiresAt instanceof Date ? poll.expiresAt.toLocaleString('de-DE') : '';
262
+ const locale = context?.lang === 'de' ? 'de-DE' : 'en-US';
263
+ const expires = poll.expiresAt instanceof Date ? poll.expiresAt.toLocaleString(locale) : '';
164
264
 
165
265
  return (0, jsx_runtime_1.jsxs)("div", {
166
266
  className: "dht-poll",
@@ -187,51 +287,132 @@ function renderPoll(poll, context) {
187
287
  }
188
288
 
189
289
  function renderVoiceIndicator(message, context) {
190
- const audio = message.attachments?.find?.((a) => typeof a.contentType === 'string' && a.contentType.startsWith('audio/'));
290
+ // FIX: discord.js Collection.find takes a predicate but on Map it conflicts; iterate values explicitly.
291
+ let audio = null;
292
+ if (message.attachments && typeof message.attachments.values === 'function') {
293
+ for (const a of message.attachments.values()) {
294
+ if (typeof a.contentType === 'string' && a.contentType.startsWith('audio/')) { audio = a; break; }
295
+ }
296
+ }
191
297
  const duration = audio?.duration_secs || audio?.durationSecs;
192
298
  const waveformB64 = audio?.waveform;
193
- let waveformSvg = null;
194
- if (typeof waveformB64 === 'string' && waveformB64.length > 0) {
299
+ let waveformRects = null;
300
+ let waveformWidth = 0;
301
+ if (typeof waveformB64 === 'string' && waveformB64.length > 0 && waveformB64.length < 16384) {
195
302
  try {
196
303
  const buf = Buffer.from(waveformB64, 'base64');
197
304
  const bars = Array.from(buf).slice(0, 100);
198
305
  const max = bars.length ? Math.max(1, ...bars) : 1;
199
- const width = bars.length * 3;
200
- const rects = bars.map((b, i) => {
306
+ waveformWidth = bars.length * 3;
307
+ waveformRects = bars.map((b, i) => {
201
308
  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) {}
309
+ return (0, jsx_runtime_1.jsx)("rect", { x: i * 3, y: (22 - h) / 2, width: 2, height: h, rx: 1, fill: "#5865F2" }, i);
310
+ });
311
+ } catch (_e) { waveformRects = null; }
206
312
  }
207
313
  return (0, jsx_runtime_1.jsxs)("div", { className: "dht-voice", children: [
208
314
  (0, jsx_runtime_1.jsx)("span", { className: "dht-voice-icon", children: '🎤' }),
209
315
  (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 } }),
316
+ waveformRects && (0, jsx_runtime_1.jsx)("svg", {
317
+ xmlns: "http://www.w3.org/2000/svg",
318
+ viewBox: `0 0 ${waveformWidth} 22`,
319
+ width: waveformWidth,
320
+ height: 22,
321
+ className: "dht-voice-wave",
322
+ children: waveformRects,
323
+ }),
211
324
  duration ? (0, jsx_runtime_1.jsxs)("span", { className: "dht-voice-dur", children: [Math.round(duration), 's'] }) : null,
212
325
  ] });
213
326
  }
214
327
 
215
- function renderSnapshots(snapshots, context) {
328
+ function toArray(maybeColl) {
329
+ if (!maybeColl) return [];
330
+ if (Array.isArray(maybeColl)) return maybeColl;
331
+ if (typeof maybeColl.values === 'function') return Array.from(maybeColl.values());
332
+ if (typeof maybeColl === 'object') return Object.values(maybeColl);
333
+ return [];
334
+ }
335
+
336
+ function renderSnapshots(snapshots, context, depth = 0) {
337
+ const arr = toArray(snapshots);
338
+ if (arr.length === 0 || depth >= 5) return null; // hard-cap at 5 levels
216
339
  return (0, jsx_runtime_1.jsx)("div", {
217
340
  className: "dht-forwarded-wrap",
218
- children: snapshots.map((snap, i) => {
341
+ children: arr.map((snap, i) => {
219
342
  const m = snap.message || snap;
220
343
  const author = m?.author?.displayName || m?.author?.username || 'Unknown';
221
344
  const text = m?.content || '';
222
- return (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded", children: [
345
+ const attachments = toArray(m?.attachments);
346
+ const nested = toArray(m?.messageSnapshots);
347
+ const embeds = toArray(m?.embeds);
348
+ const stickers = toArray(m?.stickers).map((s) => ({
349
+ id: s.id,
350
+ name: s.name,
351
+ url: s.url || `https://media.discordapp.net/stickers/${s.id}.png`,
352
+ format: s.format,
353
+ }));
354
+ return (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded", "data-depth": depth, children: [
223
355
  (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded-head", children: [
224
- (0, jsx_runtime_1.jsx)("span", { className: "dht-forwarded-icon", children: '↪️' }),
356
+ (0, jsx_runtime_1.jsx)("span", { className: "dht-forwarded-icon", children: '↪' }),
225
357
  (0, jsx_runtime_1.jsx)("span", { "data-i18n": "forwardedFrom", children: t(context, 'forwardedFrom', 'Forwarded from') }),
226
358
  (0, jsx_runtime_1.jsx)("strong", { children: ' ' + author }),
227
359
  ] }),
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)'] }),
360
+ 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 }) }) }),
361
+ attachments.length > 0 && (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded-attachments", children: [
362
+ (0, jsx_runtime_1.jsxs)("strong", { children: ['📎 ', attachments.length, ' attachment', attachments.length !== 1 ? 's' : '', ':'] }),
363
+ (0, jsx_runtime_1.jsx)("ul", { children: attachments.map((a, ai) => {
364
+ const safeUrl = (0, utils_1.safeHref)(a.url);
365
+ return (0, jsx_runtime_1.jsxs)("li", { children: [
366
+ a.contentType?.startsWith?.('image/') && a.url
367
+ ? (0, jsx_runtime_1.jsx)("img", { src: safeUrl !== '#' ? safeUrl : '', alt: a.name || '', style: { maxWidth: '200px', borderRadius: '4px', display: 'block', margin: '4px 0' } })
368
+ : (0, jsx_runtime_1.jsxs)("a", { href: safeUrl, target: "_blank", rel: "noreferrer", children: [a.name || 'attachment', a.size ? ` (${Math.round(a.size / 1024)} KB)` : ''] })
369
+ ] }, ai);
370
+ }) })
371
+ ] }),
372
+ stickers.length > 0 && renderStickers(stickers),
373
+ embeds.length > 0 && (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded-embeds", children: ['📑 ', embeds.length, ' embed', embeds.length !== 1 ? 's' : ''] }),
374
+ nested.length > 0 && renderSnapshots(nested, context, depth + 1),
230
375
  ] }, i);
231
376
  })
232
377
  });
233
378
  }
234
379
 
380
+ function renderSlashCommandLine(message, context) {
381
+ const interaction = message?.interaction;
382
+ const metadata = message?.interactionMetadata;
383
+ const optsRaw = message?.interactionOptions || interaction?.options;
384
+ // Build command path: /cmd group? subcommand? plus key:value options
385
+ if (!optsRaw && !metadata) return null;
386
+
387
+ const opts = Array.isArray(optsRaw) ? optsRaw : [];
388
+ let cmdParts = [interaction?.commandName || '?'];
389
+ let valueOpts = opts;
390
+
391
+ // Detect subcommand groups: nested option structure { type:2/1 (group/subcommand), name, options }
392
+ for (let i = 0; i < 2; i++) {
393
+ if (valueOpts.length === 1 && valueOpts[0] && (valueOpts[0].type === 1 || valueOpts[0].type === 2)) {
394
+ cmdParts.push(valueOpts[0].name);
395
+ valueOpts = Array.isArray(valueOpts[0].options) ? valueOpts[0].options : [];
396
+ }
397
+ }
398
+ // Filter out autocomplete-only `focused` options
399
+ valueOpts = valueOpts.filter((o) => o && !o.focused);
400
+
401
+ if (!valueOpts.length && cmdParts.length === 1 && !metadata) return null;
402
+
403
+ const triggeredBy = metadata?.user || interaction?.user;
404
+
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
+ ] });
414
+ }
415
+
235
416
  function renderEditHistory(history, context) {
236
417
  // Optional feature — host bot may attach `message.editHistory: [{content, editedAt}, …]`
237
418
  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)({