discord-html-transcripts-fix 1.1.0 → 1.2.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.
@@ -1,76 +1,227 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  var __rest = (this && this.__rest) || function (s, e) {
3
6
  var t = {};
4
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
- t[p] = s[p];
7
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
6
8
  if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
9
  for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
- t[p[i]] = s[p[i]];
10
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
10
11
  }
11
12
  return t;
12
13
  };
13
- var __importDefault = (this && this.__importDefault) || function (mod) {
14
- return (mod && mod.__esModule) ? mod : { "default": mod };
15
- };
16
14
  var _a;
17
15
  Object.defineProperty(exports, "__esModule", { value: true });
18
16
  exports.default = render;
17
+ exports.renderToStream = renderToStream;
19
18
  const jsx_runtime_1 = require("react/jsx-runtime");
20
19
  const static_1 = require("react-dom/static");
21
20
  const buildProfiles_1 = require("../utils/buildProfiles");
22
21
  const client_1 = require("../static/client");
23
22
  const fs_1 = require("fs");
24
23
  const path_1 = __importDefault(require("path"));
25
- //const ssr_1 = require("@lit-labs/ssr");
26
24
  const transcript_1 = __importDefault(require("./transcript"));
27
25
  const utils_1 = require("../utils/utils");
28
- //const render_result_1 = require("@lit-labs/ssr/lib/render-result");
29
26
  const styles_1 = require("./renderers/components/styles");
30
27
  const DiscordImage_1 = require("./renderers/components/DiscordImage");
31
28
  const DiscordHighlightedCode_1 = require("./renderers/components/DiscordHighlightedCode");
32
- // read the package.json file and get the @derockdev/discord-components-core version
33
- let discordComponentsVersion = '^4.0.2';
29
+
30
+ // Lock to an exact resolved version — semver ranges in src= aren't cacheable by CDNs.
31
+ let discordComponentsVersion = '4.0.2';
34
32
  try {
35
33
  const packagePath = path_1.default.join(__dirname, '..', '..', 'package.json');
36
34
  const packageJSON = JSON.parse((0, fs_1.readFileSync)(packagePath, 'utf8'));
37
- discordComponentsVersion = (_a = packageJSON.dependencies['@skyra/discord-components-core']) !== null && _a !== void 0 ? _a : discordComponentsVersion;
38
- // eslint-disable-next-line no-empty
35
+ const v = (_a = packageJSON.dependencies['@skyra/discord-components-core']) !== null && _a !== void 0 ? _a : discordComponentsVersion;
36
+ const cleaned = String(v).replace(/^[\^~]/, '');
37
+ if (/^\d+\.\d+\.\d+/.test(cleaned)) discordComponentsVersion = cleaned;
38
+ } catch (_b) { /* ignore */ }
39
+
40
+ function computeStats(messages) {
41
+ let images = 0;
42
+ let humans = new Set();
43
+ for (const m of messages) {
44
+ if (m.attachments && m.attachments.forEach) {
45
+ m.attachments.forEach((a) => {
46
+ if (typeof a.contentType === 'string' && a.contentType.startsWith('image/')) images++;
47
+ });
48
+ }
49
+ if (m.author && !m.author.bot) humans.add(m.author.id);
50
+ }
51
+ return { messageCount: messages.length, participantCount: humans.size, imageCount: images };
52
+ }
53
+
54
+ function snowflakeToDate(id) {
55
+ try {
56
+ const DISCORD_EPOCH = 1420070400000n;
57
+ const ms = (BigInt(id) >> 22n) + DISCORD_EPOCH;
58
+ return new Date(Number(ms));
59
+ } catch (_e) { return null; }
60
+ }
61
+
62
+ function buildDateSeparators(messages, lang) {
63
+ // Returns an array same length as messages, each entry: separator string or null
64
+ const seps = new Array(messages.length).fill(null);
65
+ let lastYmd = null;
66
+ const fmt = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : 'en-US', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' });
67
+ for (let i = 0; i < messages.length; i++) {
68
+ const m = messages[i];
69
+ const d = m.createdAt instanceof Date ? m.createdAt : snowflakeToDate(m.id);
70
+ if (!d) continue;
71
+ const ymd = `${d.getUTCFullYear()}-${d.getUTCMonth()}-${d.getUTCDate()}`;
72
+ if (ymd !== lastYmd) {
73
+ seps[i] = fmt.format(d);
74
+ lastYmd = ymd;
75
+ }
76
+ }
77
+ return seps;
39
78
  }
40
- catch (_b) { } // ignore errors
79
+
41
80
  async function render(_a) {
42
81
  var _b;
43
82
  var { messages, channel, callbacks } = _a, options = __rest(_a, ["messages", "channel", "callbacks"]);
44
- const profiles = await (0, buildProfiles_1.buildProfiles)(messages);
45
- const extended = await (0, buildProfiles_1.buildExtendedContext)(messages, channel).catch((e) => {
46
- console.warn('[discord-html-transcripts-fix] buildExtendedContext failed:', e && e.message ? e.message : e);
47
- return { users: {}, roles: {}, channels: {} };
83
+
84
+ const lang = options.language && client_1.defaultDicts[options.language] ? options.language : 'en';
85
+ const i18n = options.i18n
86
+ ? Object.fromEntries(Object.keys(client_1.defaultDicts).map((k) => [k, Object.assign({}, client_1.defaultDicts[k], options.i18n[k] || {})]))
87
+ : client_1.defaultDicts;
88
+
89
+ // Single-pass collector (profiles + extended in one walk)
90
+ const allCtx = await (0, buildProfiles_1.buildAllContext)(messages, channel).catch((e) => {
91
+ console.warn('[discord-html-transcripts-fix] buildAllContext failed:', e?.message || e);
92
+ return { profiles: {}, users: {}, roles: {}, channels: {} };
48
93
  });
94
+
95
+ const stats = computeStats(messages);
96
+ const dateSeps = buildDateSeparators(messages, lang);
97
+
49
98
  const globalData = {
50
- profiles,
51
- users: extended.users,
52
- roles: extended.roles,
53
- channels: extended.channels,
99
+ profiles: allCtx.profiles,
100
+ users: allCtx.users,
101
+ roles: allCtx.roles,
102
+ channels: allCtx.channels,
103
+ lang,
104
+ i18n,
105
+ stats,
54
106
  };
55
- const { prelude } = await (0, static_1.prerenderToNodeStream)((0, jsx_runtime_1.jsxs)("html", { children: [(0, jsx_runtime_1.jsxs)("head", { children: [(0, jsx_runtime_1.jsx)("meta", { charSet: "utf-8" }), (0, jsx_runtime_1.jsx)("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), (0, jsx_runtime_1.jsx)("link", { rel: "preconnect", href: "https://cdn.jsdelivr.net/" }), (0, jsx_runtime_1.jsx)("link", { rel: "preconnect", href: "https://cdn.discordapp.com" }), (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.ggSansFont } }), (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: styles_1.globalStyles } }), (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.mentionPopupStyles } }), (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.transcriptUtils } }), (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: DiscordImage_1.DiscordAttachmentStyles } }), (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: DiscordHighlightedCode_1.DiscordHighlightStyles } }), (0, jsx_runtime_1.jsx)("link", { rel: "icon", type: "image/png", href: options.favicon === 'guild'
56
- ? channel.isDMBased()
57
- ? undefined
58
- : ((_b = channel.guild.iconURL({ size: 16, extension: 'png' })) !== null && _b !== void 0 ? _b : undefined)
59
- : options.favicon }), (0, jsx_runtime_1.jsx)("title", { children: channel.isDMBased() ? 'Direct Messages' : channel.name }), (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: {
60
- __html: client_1.scrollToMessage,
61
- } }), !options.hydrate && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: {
62
- __html: `globalThis.$discordMessage=${JSON.stringify(globalData)}`,
63
- } }), (0, jsx_runtime_1.jsx)("script", { type: "module", src: `https://cdn.jsdelivr.net/npm/@skyra/discord-components-core@${discordComponentsVersion}/+esm` })] }))] }), (0, jsx_runtime_1.jsx)("body", { style: {
64
- margin: 0,
65
- minHeight: '100vh',
66
- }, children: (0, jsx_runtime_1.jsx)(transcript_1.default, Object.assign({ messages: messages, channel: channel, callbacks: callbacks }, options)) }), (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.mentionPopup } }), (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.backToTop } }), options.hydrate && (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.revealSpoiler } })] }));
67
- const markup = await (0, utils_1.streamToString)(prelude);
68
- if (options.hydrate) {
69
- const { render: renderLit } = await import('@lit-labs/ssr');
70
- const result = renderLit(markup);
71
- const { collectResult } = await import('@lit-labs/ssr/lib/render-result.js');
72
- return await collectResult(result);
73
- }
74
- return markup;
107
+
108
+ const renderContext = Object.assign({ lang, i18n, dateSeps, stats }, callbacks ? { callbacks } : {});
109
+
110
+ const docTree = (0, jsx_runtime_1.jsxs)("html", { lang, children: [
111
+ (0, jsx_runtime_1.jsxs)("head", { children: [
112
+ (0, jsx_runtime_1.jsx)("meta", { charSet: "utf-8" }),
113
+ (0, jsx_runtime_1.jsx)("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
114
+ (0, jsx_runtime_1.jsx)("link", { rel: "preconnect", href: "https://cdn.jsdelivr.net/" }),
115
+ (0, jsx_runtime_1.jsx)("link", { rel: "preconnect", href: "https://cdn.discordapp.com" }),
116
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.ggSansFont } }),
117
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: styles_1.globalStyles } }),
118
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.mentionPopupStyles } }),
119
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.transcriptUtils } }),
120
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.searchBarStyles } }),
121
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.lightboxStyles } }),
122
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.tocStyles } }),
123
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.i18nStyles } }),
124
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.dateSeparatorStyles } }),
125
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: DiscordImage_1.DiscordAttachmentStyles } }),
126
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: DiscordHighlightedCode_1.DiscordHighlightStyles } }),
127
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: extraStyles } }),
128
+ (0, jsx_runtime_1.jsx)("link", { rel: "icon", type: "image/png", href: options.favicon === 'guild'
129
+ ? channel.isDMBased()
130
+ ? undefined
131
+ : ((_b = channel.guild.iconURL({ size: 16, extension: 'png' })) !== null && _b !== void 0 ? _b : undefined)
132
+ : options.favicon }),
133
+ (0, jsx_runtime_1.jsx)("title", { children: channel.isDMBased() ? 'Direct Messages' : channel.name }),
134
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.scrollToMessage } }),
135
+ // Non-blocking inline JSON: type="application/json" so the parser doesn't pause
136
+ (0, jsx_runtime_1.jsx)("script", { id: "dht-data", type: "application/json", dangerouslySetInnerHTML: { __html: (0, utils_1.safeJsonForScript)(globalData) } }),
137
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: 'try{globalThis.$discordMessage=JSON.parse(document.getElementById("dht-data").textContent);}catch(e){globalThis.$discordMessage={};}' } }),
138
+ !options.hydrate && (0, jsx_runtime_1.jsx)("script", { type: "module", src: `https://cdn.jsdelivr.net/npm/@skyra/discord-components-core@${discordComponentsVersion}/+esm`, crossOrigin: "anonymous" }),
139
+ ] }),
140
+ (0, jsx_runtime_1.jsxs)("body", { style: { margin: 0, minHeight: '100vh' }, children: [
141
+ (0, jsx_runtime_1.jsx)(transcript_1.default, Object.assign({ messages, channel, callbacks }, options, renderContext)),
142
+ (0, jsx_runtime_1.jsx)(StatsFooter, { stats, lang, i18n }),
143
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.mentionPopup } }),
144
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.backToTop } }),
145
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.searchBar } }),
146
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.lightbox } }),
147
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.toc } }),
148
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.i18nSwitcher } }),
149
+ options.hydrate && (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.revealSpoiler } }),
150
+ ] })
151
+ ] });
152
+
153
+ const { prelude } = await (0, static_1.prerenderToNodeStream)(docTree);
154
+
155
+ if (options.hydrate) {
156
+ const markup = await (0, utils_1.streamToString)(prelude);
157
+ const { render: renderLit } = await import('@lit-labs/ssr');
158
+ const result = renderLit(markup);
159
+ const { collectResult } = await import('@lit-labs/ssr/lib/render-result.js');
160
+ return await collectResult(result);
161
+ }
162
+
163
+ if (options.returnType === 'stream' || options.stream) {
164
+ // Return the underlying Node stream — caller is responsible for piping.
165
+ return prelude;
166
+ }
167
+ return await (0, utils_1.streamToString)(prelude);
168
+ }
169
+
170
+ async function renderToStream(args) {
171
+ return render(Object.assign({}, args, { returnType: 'stream' }));
172
+ }
173
+
174
+ function StatsFooter({ stats, lang, i18n }) {
175
+ const dict = (i18n && i18n[lang]) || (i18n && i18n.en) || {};
176
+ return (0, jsx_runtime_1.jsxs)("footer", { className: "dht-stats", children: [
177
+ (0, jsx_runtime_1.jsx)("span", { "data-i18n": "statsMessages", "data-i18n-params": JSON.stringify({ n: stats.messageCount }), children: (dict.statsMessages || '{n} messages').replace('{n}', stats.messageCount) }),
178
+ ' · ',
179
+ (0, jsx_runtime_1.jsx)("span", { "data-i18n": "statsParticipants", "data-i18n-params": JSON.stringify({ n: stats.participantCount }), children: (dict.statsParticipants || '{n} participants').replace('{n}', stats.participantCount) }),
180
+ ' · ',
181
+ (0, jsx_runtime_1.jsx)("span", { "data-i18n": "statsImages", "data-i18n-params": JSON.stringify({ n: stats.imageCount }), children: (dict.statsImages || '{n} images').replace('{n}', stats.imageCount) }),
182
+ ] });
75
183
  }
76
- //# sourceMappingURL=index.js.map
184
+
185
+ const extraStyles = `
186
+ .dht-pinned-indicator{position:absolute;top:6px;right:8px;font-size:12px;opacity:.6}
187
+ .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}
188
+ .dht-edit-marker{color:#949ba4;font-size:11px;margin-left:4px;cursor:help}
189
+ .dht-suppressed{color:#6e727a;font-size:11px;font-style:italic}
190
+ .dht-stickers{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
191
+ .dht-sticker{width:160px;height:160px;border-radius:8px;object-fit:contain;background:rgba(255,255,255,.02)}
192
+ .dht-poll{border:1px solid #2b2d31;background:#1e1f22;border-radius:8px;padding:12px 14px;margin-top:6px;max-width:480px}
193
+ .dht-poll-q{font-weight:700;margin-bottom:8px;font-size:15px}
194
+ .dht-poll-answers{display:flex;flex-direction:column;gap:6px}
195
+ .dht-poll-a{background:#2b2d31;border-radius:6px;padding:6px 10px;position:relative;overflow:hidden}
196
+ .dht-poll-bar{position:absolute;top:0;left:0;height:100%;background:rgba(88,101,242,.25);z-index:0}
197
+ .dht-poll-line{position:relative;z-index:1;display:flex;justify-content:space-between;gap:8px}
198
+ .dht-poll-pct{color:#b5bac1;font-size:12px;white-space:nowrap}
199
+ .dht-poll-foot{margin-top:8px;font-size:11px;color:#949ba4}
200
+ .dht-voice{display:inline-flex;align-items:center;gap:8px;background:#2b2d31;padding:6px 12px;border-radius:8px;color:#dbdee1;font-size:13px;margin:4px 0}
201
+ .dht-voice-wave svg{display:block}
202
+ .dht-voice-dur{color:#b5bac1;font-size:11px}
203
+ .dht-forwarded-wrap{margin:4px 0}
204
+ .dht-forwarded{border-left:3px solid #5865F2;background:rgba(88,101,242,.06);padding:6px 10px;border-radius:0 6px 6px 0;margin-bottom:4px}
205
+ .dht-forwarded-head{font-size:12px;color:#949ba4;margin-bottom:2px}
206
+ .dht-forwarded-icon{margin-right:4px}
207
+ .dht-forwarded-body{color:#dbdee1}
208
+ .dht-forwarded-attachments{margin-top:4px;font-size:11px;color:#949ba4}
209
+ .dht-edit-history{font-size:12px;color:#b5bac1;margin-top:4px}
210
+ .dht-edit-history summary{cursor:pointer;color:#949ba4}
211
+ .dht-edit-history li{margin-top:4px}
212
+ .dht-edit-history time{display:block;color:#6e727a;font-size:10px;margin-bottom:2px}
213
+ .dht-thread-badge{display:inline-block;background:#3f4248;color:#dbdee1;font-size:10px;padding:1px 6px;border-radius:4px;margin-left:6px}
214
+ .dht-thread-badge--locked{background:#dc3545}
215
+ .dht-thread-badge--archived{background:#6c757d}
216
+ .dht-gifv{max-width:400px;max-height:300px;border-radius:6px}
217
+ .dht-stats{margin:24px auto;padding:12px;text-align:center;color:#b5bac1;font-size:13px;font-family:"gg sans",sans-serif;border-top:1px solid #2b2d31;max-width:800px}
218
+ .dht-mention--cmd{background:rgba(88,101,242,.22)!important}
219
+ .dht-embed-provider{color:#949ba4;font-size:11px;margin-bottom:2px}
220
+ .dht-embed-video{margin-top:6px;font-size:13px}
221
+ .dht-embed-video a{color:#8ab4ff;text-decoration:none}
222
+ .dht-embed-video a:hover{text-decoration:underline}
223
+ .dht-automod-rule{font-size:11px;color:#949ba4}
224
+ .dht-automod-content{margin:4px 0 0 12px;color:#dbdee1;border-left:3px solid #f23f42;padding-left:8px;font-size:13px}
225
+ .dht-reaction--burst{box-shadow:inset 0 0 0 1px gold}
226
+ `;
227
+ //# sourceMappingURL=index.js.map
@@ -6,62 +6,53 @@ const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const types_1 = require("../../types");
7
7
  const utils_1 = require("../../utils/utils");
8
8
  const DiscordImage_1 = require("./components/DiscordImage");
9
- /**
10
- * Renders all attachments for a message
11
- * @param message
12
- * @param context
13
- * @returns
14
- */
9
+
15
10
  async function Attachments(props) {
16
- if (props.message.attachments.size === 0)
17
- return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {});
11
+ if (props.message.attachments.size === 0) return (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, {});
18
12
  return props.message.attachments.map((attachment, id) => ((0, jsx_runtime_1.jsx)(Attachment, { attachment: attachment, message: props.message, context: props.context }, id)));
19
13
  }
20
- /**
21
- * Renders one Discord Attachment
22
- * @param props - the attachment and rendering context
23
- */
24
- async function Attachment({ attachment, context, message, }) {
25
- let url = attachment.url;
14
+
15
+ async function Attachment({ attachment, context, message }) {
16
+ let url = (0, utils_1.safeHref)(attachment.url) === '#' ? '#' : attachment.url;
26
17
  const attachmentType = getAttachmentType(attachment);
27
18
  const [bytes, bytesUnit] = (0, utils_1.formatBytes)(attachment.size);
28
- // if the attachment is an image, download it to a data url
19
+ const altText = attachment.description || attachment.name;
20
+
21
+ // GIFV detection: animated content served as video but spec-typed as GIF
22
+ const isGifv = attachment.contentType === 'video/mp4' && /\.(gifv?|webm)$/i.test(attachment.name || '');
23
+
29
24
  switch (attachmentType) {
30
25
  case types_1.AttachmentTypes.Image: {
31
26
  const downloaded = await context.callbacks.resolveImageSrc(attachment.toJSON(), message.toJSON());
32
27
  if (downloaded !== null) {
33
- url = downloaded !== null && downloaded !== void 0 ? downloaded : url;
28
+ url = downloaded ?? url;
34
29
  }
35
- return (0, jsx_runtime_1.jsx)(DiscordImage_1.DiscordImageAttachment, { url: url, alt: attachment.name }, attachment.id);
30
+ // GIF / animated content — Discord auto-plays. Render as <video> when needed.
31
+ if (attachment.contentType === 'image/gif' || isGifv) {
32
+ return (0, jsx_runtime_1.jsx)("video", { src: url, autoPlay: true, loop: true, muted: true, playsInline: true, className: "dht-gifv", "aria-label": altText }, attachment.id);
33
+ }
34
+ return (0, jsx_runtime_1.jsx)(DiscordImage_1.DiscordImageAttachment, { url: url, alt: altText, title: attachment.description || undefined }, attachment.id);
36
35
  }
37
36
  case types_1.AttachmentTypes.Video: {
38
37
  return (0, jsx_runtime_1.jsx)("discord-video-attachment", { slot: "attachments", href: url }, attachment.id);
39
38
  }
40
39
  case types_1.AttachmentTypes.Audio: {
41
- return ((0, jsx_runtime_1.jsx)("discord-audio-attachment", { slot: "attachments", href: url, bytes: bytes, bytesUnit: bytesUnit, name: attachment.name }, attachment.id));
40
+ // Voice messages have waveform + duration_secs — message.js renders the indicator
41
+ return (0, jsx_runtime_1.jsx)("discord-audio-attachment", { slot: "attachments", href: url, bytes: bytes, bytesUnit: bytesUnit, name: attachment.name, title: attachment.description || undefined }, attachment.id);
42
42
  }
43
43
  case types_1.AttachmentTypes.File: {
44
- return ((0, jsx_runtime_1.jsx)("discord-file-attachment", { slot: "attachments", href: url, bytes: bytes, "bytes-unit": bytesUnit, name: attachment.name }, attachment.id));
44
+ return (0, jsx_runtime_1.jsx)("discord-file-attachment", { slot: "attachments", href: url, bytes: bytes, "bytes-unit": bytesUnit, name: attachment.name, title: attachment.description || undefined }, attachment.id);
45
45
  }
46
46
  }
47
47
  }
48
- /**
49
- * Parses the attachment content type.
50
- * @param attachment Discord.js attachment object
51
- * @returns
52
- */
48
+
53
49
  function getAttachmentType(attachment) {
54
- var _a, _b;
55
- switch ((_b = (_a = attachment.contentType) === null || _a === void 0 ? void 0 : _a.split('/')) === null || _b === void 0 ? void 0 : _b[0]) {
56
- case 'audio':
57
- return types_1.AttachmentTypes.Audio;
58
- case 'image':
59
- return types_1.AttachmentTypes.Image;
60
- case 'video':
61
- return types_1.AttachmentTypes.Video;
62
- case undefined:
63
- default:
64
- return types_1.AttachmentTypes.File;
50
+ const top = attachment.contentType?.split('/')?.[0];
51
+ switch (top) {
52
+ case 'audio': return types_1.AttachmentTypes.Audio;
53
+ case 'image': return types_1.AttachmentTypes.Image;
54
+ case 'video': return types_1.AttachmentTypes.Video;
55
+ default: return types_1.AttachmentTypes.File;
65
56
  }
66
57
  }
67
- //# sourceMappingURL=attachment.js.map
58
+ //# sourceMappingURL=attachment.js.map
@@ -1,19 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
- function DiscordContainer({ children }) {
5
- return ((0, jsx_runtime_1.jsx)("div", { style: {
6
- display: 'flex',
7
- width: '500px',
8
- flexDirection: 'column',
9
- backgroundColor: '#3f4248',
10
- padding: '16px',
11
- border: '1px solid #4f5359',
12
- marginTop: '2px',
13
- marginBottom: '2px',
14
- borderRadius: '10px',
15
- gap: '8px',
16
- }, children: children }));
4
+
5
+ function colorToHex(color) {
6
+ if (color == null) return null;
7
+ if (typeof color === 'string') {
8
+ return color.startsWith('#') ? color : '#' + color.replace(/^0x/i, '');
9
+ }
10
+ if (typeof color === 'number') {
11
+ return '#' + color.toString(16).padStart(6, '0');
12
+ }
13
+ return null;
14
+ }
15
+
16
+ function DiscordContainer({ children, accentColor, spoiler }) {
17
+ const hex = colorToHex(accentColor);
18
+ const style = {
19
+ display: 'flex',
20
+ width: '500px',
21
+ flexDirection: 'column',
22
+ backgroundColor: '#3f4248',
23
+ padding: '16px',
24
+ // Use accent color as a left border (Discord's V2 container style)
25
+ borderLeft: hex ? `4px solid ${hex}` : '1px solid #4f5359',
26
+ borderTop: '1px solid #4f5359',
27
+ borderRight: '1px solid #4f5359',
28
+ borderBottom: '1px solid #4f5359',
29
+ marginTop: '2px',
30
+ marginBottom: '2px',
31
+ borderRadius: '10px',
32
+ gap: '8px',
33
+ };
34
+ const node = (0, jsx_runtime_1.jsx)("div", { style: style, children: children });
35
+ if (spoiler) {
36
+ return (0, jsx_runtime_1.jsx)("span", { className: "discord-spoiler", children: node });
37
+ }
38
+ return node;
17
39
  }
18
40
  exports.default = DiscordContainer;
19
- //# sourceMappingURL=Container.js.map
41
+ //# sourceMappingURL=Container.js.map
@@ -9,24 +9,30 @@ function DiscordMediaGallery({ component }) {
9
9
  const count = component.items.length;
10
10
  const imagesToShow = component.items.slice(0, 10);
11
11
  const hasMore = component.items.length > 10;
12
- return ((0, jsx_runtime_1.jsx)("div", { style: (0, utils_1.getGalleryLayout)(count), children: imagesToShow.map((media, idx) => ((0, jsx_runtime_1.jsxs)("div", { style: (0, utils_1.getImageStyle)(idx, count), children: [(0, jsx_runtime_1.jsx)("img", { src: media.media.url, alt: media.description || 'Media content', style: {
13
- width: '100%',
14
- height: '100%',
15
- objectFit: 'cover',
16
- } }), hasMore && idx === imagesToShow.length - 1 && ((0, jsx_runtime_1.jsxs)("div", { style: {
17
- position: 'absolute',
18
- top: 0,
19
- left: 0,
20
- width: '100%',
21
- height: '100%',
22
- display: 'flex',
23
- alignItems: 'center',
24
- justifyContent: 'center',
25
- backgroundColor: 'rgba(0, 0, 0, 0.7)',
26
- color: 'white',
27
- fontSize: '20px',
28
- fontWeight: 'bold',
29
- }, children: ["+", component.items.length - 10] }))] }, idx))) }));
12
+ return ((0, jsx_runtime_1.jsx)("div", { style: (0, utils_1.getGalleryLayout)(count), children: imagesToShow.map((media, idx) => {
13
+ const img = (0, jsx_runtime_1.jsx)("img", { src: media.media.url, alt: media.description || 'Media content', style: {
14
+ width: '100%',
15
+ height: '100%',
16
+ objectFit: 'cover',
17
+ } });
18
+ const wrapped = media.spoiler
19
+ ? (0, jsx_runtime_1.jsx)("span", { className: "discord-spoiler", style: { display: 'block', width: '100%', height: '100%' }, children: img })
20
+ : img;
21
+ return ((0, jsx_runtime_1.jsxs)("div", { style: (0, utils_1.getImageStyle)(idx, count), children: [wrapped, hasMore && idx === imagesToShow.length - 1 && ((0, jsx_runtime_1.jsxs)("div", { style: {
22
+ position: 'absolute',
23
+ top: 0,
24
+ left: 0,
25
+ width: '100%',
26
+ height: '100%',
27
+ display: 'flex',
28
+ alignItems: 'center',
29
+ justifyContent: 'center',
30
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
31
+ color: 'white',
32
+ fontSize: '20px',
33
+ fontWeight: 'bold',
34
+ }, children: ["+", component.items.length - 10] }))] }, idx));
35
+ }) }));
30
36
  }
31
37
  exports.default = DiscordMediaGallery;
32
38
  //# sourceMappingURL=Media%20Gallery.js.map
@@ -22,8 +22,13 @@ function ComponentRow({ component, id, context, }) {
22
22
  switch (component.type) {
23
23
  case discord_js_1.ComponentType.ActionRow:
24
24
  return ((0, jsx_runtime_1.jsx)("discord-action-row", { children: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: component.components.map((nestedComponent, id) => ((0, jsx_runtime_1.jsx)(Component, { component: nestedComponent, id: id }, id))) }) }, id));
25
- case discord_js_1.ComponentType.Container:
26
- return ((0, jsx_runtime_1.jsx)(Container_1.default, { children: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: component.components.map((nestedComponent, id) => ((0, jsx_runtime_1.jsx)(ComponentRow, { component: nestedComponent, id: id, context: context }, id))) }) }, id));
25
+ case discord_js_1.ComponentType.Container: {
26
+ const accentColor = component.accent_color !== undefined
27
+ ? component.accent_color
28
+ : (component.accentColor !== undefined ? component.accentColor : null);
29
+ const spoiler = !!component.spoiler;
30
+ return ((0, jsx_runtime_1.jsx)(Container_1.default, { accentColor: accentColor, spoiler: spoiler, children: (0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: component.components.map((nestedComponent, id) => ((0, jsx_runtime_1.jsx)(ComponentRow, { component: nestedComponent, id: id, context: context }, id))) }) }, id));
31
+ }
27
32
  case discord_js_1.ComponentType.File: {
28
33
  const attachmentComponent = (0, jsx_runtime_1.jsx)("discord-file-attachment", { href: component.file.url });
29
34
  if (component.spoiler) {