discord-html-transcripts-fix 1.1.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.
@@ -1,76 +1,264 @@
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
+ let firstTs = null, lastTs = null;
44
+ for (const m of messages) {
45
+ if (m.attachments && m.attachments.forEach) {
46
+ m.attachments.forEach((a) => {
47
+ if (typeof a.contentType === 'string' && a.contentType.startsWith('image/')) images++;
48
+ });
49
+ }
50
+ if (m.author && !m.author.bot) humans.add(m.author.id);
51
+ const ts = m.createdAt instanceof Date ? m.createdAt.getTime() : (snowflakeToDate(m.id)?.getTime() || null);
52
+ if (ts) {
53
+ if (firstTs === null || ts < firstTs) firstTs = ts;
54
+ if (lastTs === null || ts > lastTs) lastTs = ts;
55
+ }
56
+ }
57
+ return {
58
+ messageCount: messages.length,
59
+ participantCount: humans.size,
60
+ imageCount: images,
61
+ firstTs,
62
+ lastTs,
63
+ spanMs: firstTs && lastTs ? lastTs - firstTs : null,
64
+ };
65
+ }
66
+
67
+ function snowflakeToDate(id) {
68
+ try {
69
+ const DISCORD_EPOCH = 1420070400000n;
70
+ const ms = (BigInt(id) >> 22n) + DISCORD_EPOCH;
71
+ return new Date(Number(ms));
72
+ } catch (_e) { return null; }
73
+ }
74
+
75
+ function buildDateSeparators(messages, lang) {
76
+ const seps = new Array(messages.length).fill(null);
77
+ let lastYmd = null;
78
+ // FIX: use local date (matches what the formatter shows). UTC keys were mismatched with the
79
+ // local-time formatter output, causing the wrong day label near midnight UTC.
80
+ const fmt = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : 'en-US', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' });
81
+ for (let i = 0; i < messages.length; i++) {
82
+ const m = messages[i];
83
+ const d = m.createdAt instanceof Date ? m.createdAt : snowflakeToDate(m.id);
84
+ if (!d) continue;
85
+ const ymd = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
86
+ if (ymd !== lastYmd) {
87
+ seps[i] = fmt.format(d);
88
+ lastYmd = ymd;
89
+ }
90
+ }
91
+ return seps;
39
92
  }
40
- catch (_b) { } // ignore errors
93
+
41
94
  async function render(_a) {
42
95
  var _b;
43
96
  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: {} };
97
+
98
+ const lang = options.language && client_1.defaultDicts[options.language] ? options.language : 'en';
99
+ const i18n = options.i18n
100
+ ? Object.fromEntries(Object.keys(client_1.defaultDicts).map((k) => [k, Object.assign({}, client_1.defaultDicts[k], options.i18n[k] || {})]))
101
+ : client_1.defaultDicts;
102
+
103
+ // Single-pass collector (profiles + extended in one walk)
104
+ const allCtx = await (0, buildProfiles_1.buildAllContext)(messages, channel).catch((e) => {
105
+ console.warn('[discord-html-transcripts-fix] buildAllContext failed:', e?.message || e);
106
+ return { profiles: {}, users: {}, roles: {}, channels: {} };
48
107
  });
108
+
109
+ const stats = computeStats(messages);
110
+ const dateSeps = buildDateSeparators(messages, lang);
111
+
49
112
  const globalData = {
50
- profiles,
51
- users: extended.users,
52
- roles: extended.roles,
53
- channels: extended.channels,
113
+ profiles: allCtx.profiles,
114
+ users: allCtx.users,
115
+ roles: allCtx.roles,
116
+ channels: allCtx.channels,
117
+ lang,
118
+ i18n,
119
+ stats,
54
120
  };
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;
121
+
122
+ const renderContext = Object.assign({ lang, i18n, dateSeps, stats }, callbacks ? { callbacks } : {});
123
+
124
+ const docTree = (0, jsx_runtime_1.jsxs)("html", { lang, children: [
125
+ (0, jsx_runtime_1.jsxs)("head", { children: [
126
+ (0, jsx_runtime_1.jsx)("meta", { charSet: "utf-8" }),
127
+ (0, jsx_runtime_1.jsx)("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
128
+ (0, jsx_runtime_1.jsx)("link", { rel: "preconnect", href: "https://cdn.jsdelivr.net/" }),
129
+ (0, jsx_runtime_1.jsx)("link", { rel: "preconnect", href: "https://cdn.discordapp.com" }),
130
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.ggSansFont } }),
131
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: styles_1.globalStyles } }),
132
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.mentionPopupStyles } }),
133
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.transcriptUtils } }),
134
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.searchBarStyles } }),
135
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.lightboxStyles } }),
136
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.tocStyles } }),
137
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.i18nStyles } }),
138
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.dateSeparatorStyles } }),
139
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.markdownStyles } }),
140
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.filterPanelStyles } }),
141
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: DiscordImage_1.DiscordAttachmentStyles } }),
142
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: DiscordHighlightedCode_1.DiscordHighlightStyles } }),
143
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: extraStyles } }),
144
+ (0, jsx_runtime_1.jsx)("link", { rel: "icon", type: "image/png", href: options.favicon === 'guild'
145
+ ? channel.isDMBased()
146
+ ? undefined
147
+ : ((_b = channel.guild.iconURL({ size: 16, extension: 'png' })) !== null && _b !== void 0 ? _b : undefined)
148
+ : options.favicon }),
149
+ (0, jsx_runtime_1.jsx)("title", { children: channel.isDMBased() ? 'Direct Messages' : channel.name }),
150
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.scrollToMessage } }),
151
+ // Non-blocking inline JSON: type="application/json" so the parser doesn't pause
152
+ (0, jsx_runtime_1.jsx)("script", { id: "dht-data", type: "application/json", dangerouslySetInnerHTML: { __html: (0, utils_1.safeJsonForScript)(globalData) } }),
153
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: 'try{globalThis.$discordMessage=JSON.parse(document.getElementById("dht-data").textContent);}catch(e){globalThis.$discordMessage={};}' } }),
154
+ !options.hydrate && (0, jsx_runtime_1.jsx)("script", { type: "module", src: `https://cdn.jsdelivr.net/npm/@skyra/discord-components-core@${discordComponentsVersion}/+esm`, crossOrigin: "anonymous" }),
155
+ ] }),
156
+ (0, jsx_runtime_1.jsxs)("body", { style: { margin: 0, minHeight: '100vh' }, children: [
157
+ (0, jsx_runtime_1.jsx)(transcript_1.default, Object.assign({ messages, channel, callbacks }, options, renderContext)),
158
+ (0, jsx_runtime_1.jsx)(StatsFooter, { stats, lang, i18n }),
159
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.mentionPopup } }),
160
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.backToTop } }),
161
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.searchBar } }),
162
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.lightbox } }),
163
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.toc } }),
164
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.i18nSwitcher } }),
165
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.filterPanel } }),
166
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.codeCopyButton } }),
167
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.permalinkButton } }),
168
+ options.hydrate && (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.revealSpoiler } }),
169
+ ] })
170
+ ] });
171
+
172
+ const { prelude } = await (0, static_1.prerenderToNodeStream)(docTree);
173
+
174
+ if (options.hydrate) {
175
+ const markup = await (0, utils_1.streamToString)(prelude);
176
+ const { render: renderLit } = await import('@lit-labs/ssr');
177
+ const result = renderLit(markup);
178
+ const { collectResult } = await import('@lit-labs/ssr/lib/render-result.js');
179
+ return await collectResult(result);
180
+ }
181
+
182
+ if (options.returnType === 'stream' || options.stream) {
183
+ // Return the underlying Node stream — caller is responsible for piping.
184
+ return prelude;
185
+ }
186
+ return await (0, utils_1.streamToString)(prelude);
187
+ }
188
+
189
+ async function renderToStream(args) {
190
+ return render(Object.assign({}, args, { returnType: 'stream' }));
191
+ }
192
+
193
+ function formatSpan(ms, lang) {
194
+ if (!ms || ms < 0) return '';
195
+ const days = Math.floor(ms / 86400000);
196
+ const hours = Math.floor((ms % 86400000) / 3600000);
197
+ const mins = Math.floor((ms % 3600000) / 60000);
198
+ if (days > 0) return `${days}d ${hours}h`;
199
+ if (hours > 0) return `${hours}h ${mins}m`;
200
+ if (mins > 0) return `${mins}m`;
201
+ return '<1m';
202
+ }
203
+ function StatsFooter({ stats, lang, i18n }) {
204
+ const dict = (i18n && i18n[lang]) || (i18n && i18n.en) || {};
205
+ const locale = lang === 'de' ? 'de-DE' : 'en-US';
206
+ const fmtDate = (ms) => new Date(ms).toLocaleString(locale, { dateStyle: 'medium', timeStyle: 'short' });
207
+ return (0, jsx_runtime_1.jsxs)("footer", { className: "dht-stats", children: [
208
+ (0, jsx_runtime_1.jsx)("span", { children: (dict.statsMessages || '{n} messages').replace('{n}', stats.messageCount) }),
209
+ ' · ',
210
+ (0, jsx_runtime_1.jsx)("span", { children: (dict.statsParticipants || '{n} participants').replace('{n}', stats.participantCount) }),
211
+ ' · ',
212
+ (0, jsx_runtime_1.jsx)("span", { children: (dict.statsImages || '{n} images').replace('{n}', stats.imageCount) }),
213
+ stats.firstTs && stats.lastTs && (0, jsx_runtime_1.jsxs)("span", { children: [' · ', fmtDate(stats.firstTs), ' → ', fmtDate(stats.lastTs)] }),
214
+ stats.spanMs && (0, jsx_runtime_1.jsxs)("span", { children: [' · ', formatSpan(stats.spanMs)] }),
215
+ ] });
75
216
  }
76
- //# sourceMappingURL=index.js.map
217
+
218
+ const extraStyles = `
219
+ /* Ensure the host establishes a positioning context for the absolute pin indicator,
220
+ and clamp it inside the message box on narrow viewports. */
221
+ .dht-msg-pinned{position:relative}
222
+ .dht-pinned-indicator{position:absolute;top:4px;left:4px;font-size:12px;opacity:.75;pointer-events:none}
223
+ @media (max-width:640px){.dht-pinned-indicator{position:static;display:inline-block;margin-right:4px}}
224
+ .dht-app-badge,.dht-popup-badge{background:#5865F2;color:#fff;font-size:10px;padding:1px 6px;border-radius:4px;margin-left:6px;vertical-align:middle;font-weight:600}
225
+ .dht-edit-marker{color:#949ba4;font-size:11px;margin-left:4px;cursor:help}
226
+ .dht-suppressed{color:#6e727a;font-size:11px;font-style:italic}
227
+ .dht-stickers{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
228
+ .dht-sticker{width:160px;height:160px;border-radius:8px;object-fit:contain;background:rgba(255,255,255,.02)}
229
+ .dht-poll{border:1px solid #2b2d31;background:#1e1f22;border-radius:8px;padding:12px 14px;margin-top:6px;max-width:480px}
230
+ .dht-poll-q{font-weight:700;margin-bottom:8px;font-size:15px}
231
+ .dht-poll-answers{display:flex;flex-direction:column;gap:6px}
232
+ .dht-poll-a{background:#2b2d31;border-radius:6px;padding:6px 10px;position:relative;overflow:hidden}
233
+ .dht-poll-bar{position:absolute;top:0;left:0;height:100%;background:rgba(88,101,242,.25);z-index:0}
234
+ .dht-poll-line{position:relative;z-index:1;display:flex;justify-content:space-between;gap:8px}
235
+ .dht-poll-pct{color:#b5bac1;font-size:12px;white-space:nowrap}
236
+ .dht-poll-foot{margin-top:8px;font-size:11px;color:#949ba4}
237
+ .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}
238
+ .dht-voice-wave svg{display:block}
239
+ .dht-voice-dur{color:#b5bac1;font-size:11px}
240
+ .dht-forwarded-wrap{margin:4px 0}
241
+ .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}
242
+ .dht-forwarded-head{font-size:12px;color:#949ba4;margin-bottom:2px}
243
+ .dht-forwarded-icon{margin-right:4px}
244
+ .dht-forwarded-body{color:#dbdee1}
245
+ .dht-forwarded-attachments{margin-top:4px;font-size:11px;color:#949ba4}
246
+ .dht-edit-history{font-size:12px;color:#b5bac1;margin-top:4px}
247
+ .dht-edit-history summary{cursor:pointer;color:#949ba4}
248
+ .dht-edit-history li{margin-top:4px}
249
+ .dht-edit-history time{display:block;color:#6e727a;font-size:10px;margin-bottom:2px}
250
+ .dht-thread-badge{display:inline-block;background:#3f4248;color:#dbdee1;font-size:10px;padding:1px 6px;border-radius:4px;margin-left:6px}
251
+ .dht-thread-badge--locked{background:#ED4245}
252
+ .dht-thread-badge--archived{background:#6c757d}
253
+ .dht-gifv{max-width:400px;max-height:300px;border-radius:6px}
254
+ .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}
255
+ .dht-mention--cmd{background:rgba(88,101,242,.22)!important}
256
+ .dht-embed-provider{color:#949ba4;font-size:11px;margin-bottom:2px}
257
+ .dht-embed-video{margin-top:6px;font-size:13px}
258
+ .dht-embed-video a{color:#8ab4ff;text-decoration:none}
259
+ .dht-embed-video a:hover{text-decoration:underline}
260
+ .dht-automod-rule{font-size:11px;color:#949ba4}
261
+ .dht-automod-content{margin:4px 0 0 12px;color:#dbdee1;border-left:3px solid #f23f42;padding-left:8px;font-size:13px}
262
+ .dht-reaction--burst{box-shadow:inset 0 0 0 1px gold}
263
+ `;
264
+ //# 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,47 @@
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
+ const utils_1 = require("../../../utils/utils");
5
+
6
+ function colorToHex(color) {
7
+ if (color == null) return null;
8
+ if (typeof color === 'string') {
9
+ return color.startsWith('#') ? color : '#' + color.replace(/^0x/i, '');
10
+ }
11
+ if (typeof color === 'number') {
12
+ return '#' + color.toString(16).padStart(6, '0');
13
+ }
14
+ return null;
15
+ }
16
+
17
+ function DiscordContainer({ children, accentColor, spoiler }) {
18
+ // SECURITY: route accentColor through safeColor so an attacker-controlled
19
+ // string can't smuggle CSS into the inline borderLeft.
20
+ const raw = colorToHex(accentColor);
21
+ const hex = raw ? (0, utils_1.safeColor)(raw, null) : null;
22
+ const style = {
23
+ display: 'flex',
24
+ width: '100%',
25
+ maxWidth: '500px',
26
+ minWidth: 0,
27
+ flexDirection: 'column',
28
+ backgroundColor: '#3f4248',
29
+ padding: '16px',
30
+ borderLeft: hex ? `4px solid ${hex}` : '1px solid #4f5359',
31
+ borderTop: '1px solid #4f5359',
32
+ borderRight: '1px solid #4f5359',
33
+ borderBottom: '1px solid #4f5359',
34
+ marginTop: '2px',
35
+ marginBottom: '2px',
36
+ borderRadius: '10px',
37
+ gap: '8px',
38
+ boxSizing: 'border-box',
39
+ };
40
+ const node = (0, jsx_runtime_1.jsx)("div", { style, children });
41
+ if (spoiler) {
42
+ return (0, jsx_runtime_1.jsx)("span", { className: "discord-spoiler", children: node });
43
+ }
44
+ return node;
17
45
  }
18
46
  exports.default = DiscordContainer;
19
- //# sourceMappingURL=Container.js.map
47
+ //# 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 url = media.media.url;
14
+ const isVideo = typeof url === 'string' && /\.(mp4|webm|mov)(\?|$)/i.test(url);
15
+ const img = isVideo
16
+ ? (0, jsx_runtime_1.jsx)("video", { src: url, autoPlay: true, loop: true, muted: true, playsInline: true, style: { width: '100%', height: '100%', objectFit: 'cover' } })
17
+ : (0, jsx_runtime_1.jsx)("img", { src: url, alt: media.description || 'Media content', loading: 'lazy', style: { width: '100%', height: '100%', objectFit: 'cover' } });
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
@@ -2,13 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
4
  const discord_js_1 = require("discord.js");
5
+
6
+ /**
7
+ * V2 Separator component. `spacing` is Small (1) or Large (2). `divider` toggles the visual line.
8
+ * In Discord's UI: Small ≈ 8px, Large ≈ 16px vertical padding. Divider draws a 1px line.
9
+ */
5
10
  function DiscordSeparator({ divider, spacing }) {
6
- return ((0, jsx_runtime_1.jsx)("div", { style: {
11
+ const isLarge = spacing === discord_js_1.SeparatorSpacingSize?.Large || spacing === 2;
12
+ const pad = isLarge ? 16 : 8;
13
+ const showLine = divider !== false; // default true in Discord — only false when explicitly set
14
+ return (0, jsx_runtime_1.jsx)("div", {
15
+ className: "dht-v2-separator",
16
+ style: {
7
17
  width: '100%',
8
- height: divider ? '1px' : '0px',
9
- backgroundColor: '#4f5359',
10
- margin: spacing === discord_js_1.SeparatorSpacingSize.Large ? '8px 0' : '0',
11
- } }));
18
+ padding: `${pad}px 0`,
19
+ display: 'flex',
20
+ alignItems: 'center',
21
+ },
22
+ children: showLine ? (0, jsx_runtime_1.jsx)("div", { style: { width: '100%', height: '1px', backgroundColor: 'rgba(79,84,89,0.65)' } }) : null
23
+ });
12
24
  }
13
25
  exports.default = DiscordSeparator;
14
- //# sourceMappingURL=Spacing.js.map
26
+ //# sourceMappingURL=Spacing.js.map
@@ -8,12 +8,22 @@ const components_1 = require("../../components");
8
8
  const SectionContent_1 = __importDefault(require("./SectionContent"));
9
9
  const SectionAccessory_1 = __importDefault(require("./SectionAccessory"));
10
10
  function DiscordSection({ children, accessory, id }) {
11
- return ((0, jsx_runtime_1.jsxs)("div", { style: {
11
+ return ((0, jsx_runtime_1.jsxs)("div", {
12
+ style: {
12
13
  display: 'flex',
13
14
  flexDirection: 'row',
14
15
  width: '100%',
15
16
  maxWidth: '500px',
16
- }, children: [(0, jsx_runtime_1.jsx)(SectionContent_1.default, { children: children }), (0, jsx_runtime_1.jsx)(SectionAccessory_1.default, { children: accessory && (0, jsx_runtime_1.jsx)(components_1.Component, { component: accessory, id: id }) })] }));
17
+ minWidth: 0,
18
+ gap: '12px',
19
+ alignItems: 'flex-start',
20
+ boxSizing: 'border-box',
21
+ },
22
+ children: [
23
+ (0, jsx_runtime_1.jsx)(SectionContent_1.default, { children }),
24
+ accessory ? (0, jsx_runtime_1.jsx)(SectionAccessory_1.default, { children: (0, jsx_runtime_1.jsx)(components_1.Component, { component: accessory, id }) }) : null,
25
+ ]
26
+ }));
17
27
  }
18
28
  exports.default = DiscordSection;
19
- //# sourceMappingURL=Section.js.map
29
+ //# sourceMappingURL=Section.js.map
@@ -2,15 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
4
  function SectionAccessory({ children }) {
5
- if (!children)
6
- return null;
7
- return ((0, jsx_runtime_1.jsx)("div", { style: {
5
+ if (!children) return null;
6
+ return ((0, jsx_runtime_1.jsx)("div", {
7
+ style: {
8
8
  display: 'flex',
9
- width: '100%',
10
- maxWidth: '500px',
9
+ flex: '0 0 auto',
11
10
  justifyContent: 'flex-end',
12
11
  alignItems: 'center',
13
- }, children: children }));
12
+ },
13
+ children
14
+ }));
14
15
  }
15
16
  exports.default = SectionAccessory;
16
- //# sourceMappingURL=SectionAccessory.js.map
17
+ //# sourceMappingURL=SectionAccessory.js.map
@@ -2,11 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
4
  function SectionContent({ children }) {
5
- return ((0, jsx_runtime_1.jsx)("div", { style: {
5
+ return ((0, jsx_runtime_1.jsx)("div", {
6
+ style: {
6
7
  display: 'flex',
7
8
  flexDirection: 'column',
8
- width: '100%',
9
- }, children: children }));
9
+ flex: 1,
10
+ minWidth: 0, // allow text to shrink/wrap inside flex parent
11
+ overflowWrap: 'anywhere',
12
+ },
13
+ children
14
+ }));
10
15
  }
11
16
  exports.default = SectionContent;
12
- //# sourceMappingURL=SectionContent.js.map
17
+ //# sourceMappingURL=SectionContent.js.map