g-comments 0.0.0-stage → 0.1.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.
package/dist/react.cjs ADDED
@@ -0,0 +1,2557 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var chunkX6RAR374_cjs = require('./chunk-X6RAR374.cjs');
5
+ var react = require('react');
6
+ var jsxRuntime = require('react/jsx-runtime');
7
+
8
+ var useIsomorphicLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
9
+ function toCommentsError(error) {
10
+ if (chunkX6RAR374_cjs.isCommentsError(error)) {
11
+ return error;
12
+ }
13
+ return new chunkX6RAR374_cjs.CommentsError("unknown", error instanceof Error ? error.message : String(error), {
14
+ cause: error
15
+ });
16
+ }
17
+ function noop() {
18
+ }
19
+ function rootNodeOf(element) {
20
+ const root = element.getRootNode();
21
+ return root instanceof Document || root instanceof ShadowRoot ? root : element.ownerDocument;
22
+ }
23
+ function isLocalPage() {
24
+ if (typeof window === "undefined") {
25
+ return false;
26
+ }
27
+ const host = window.location.hostname;
28
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host.endsWith(".localhost") || host.endsWith(".test");
29
+ }
30
+ function readDraft(key) {
31
+ try {
32
+ return globalThis.localStorage?.getItem(key) ?? "";
33
+ } catch {
34
+ return "";
35
+ }
36
+ }
37
+ function writeDraft(key, value) {
38
+ try {
39
+ if (value.trim() === "") {
40
+ globalThis.localStorage?.removeItem(key);
41
+ } else {
42
+ globalThis.localStorage?.setItem(key, value);
43
+ }
44
+ } catch {
45
+ }
46
+ }
47
+ var tickListeners = /* @__PURE__ */ new Set();
48
+ var tickTimer;
49
+ function onMinuteTick(listener) {
50
+ tickListeners.add(listener);
51
+ tickTimer ??= setInterval(() => {
52
+ for (const tick of [...tickListeners]) {
53
+ tick();
54
+ }
55
+ }, 6e4);
56
+ return () => {
57
+ tickListeners.delete(listener);
58
+ if (tickListeners.size === 0) {
59
+ clearInterval(tickTimer);
60
+ tickTimer = void 0;
61
+ }
62
+ };
63
+ }
64
+ var UNITS = [
65
+ ["year", 31536e3],
66
+ ["month", 2592e3],
67
+ ["week", 604800],
68
+ ["day", 86400],
69
+ ["hour", 3600],
70
+ ["minute", 60]
71
+ ];
72
+ function formatRelative(iso, now, locale) {
73
+ const seconds = Math.round((Date.parse(iso) - now) / 1e3);
74
+ const format = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
75
+ for (const [unit, size] of UNITS) {
76
+ if (Math.abs(seconds) >= size) {
77
+ return format.format(Math.round(seconds / size), unit);
78
+ }
79
+ }
80
+ return format.format(0, "second");
81
+ }
82
+ function formatAbsolute(iso, locale) {
83
+ return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(
84
+ new Date(iso)
85
+ );
86
+ }
87
+
88
+ // src/react/context.ts
89
+ var CommentsContext = react.createContext(null);
90
+ var defaultTranslator = chunkX6RAR374_cjs.createTranslator();
91
+ function useCommentsContext(forum) {
92
+ const value = useOptionalCommentsContext(forum);
93
+ if (!value) {
94
+ throw new Error(
95
+ `${chunkX6RAR374_cjs.PACKAGE_NAME}: render this inside <DiscourseComments forum="\u2026"> or pass a \`forum\` prop`
96
+ );
97
+ }
98
+ return value;
99
+ }
100
+ function useOptionalCommentsContext(forum) {
101
+ const context = react.useContext(CommentsContext);
102
+ const standalone = react.useMemo(() => {
103
+ if (forum === void 0 || context && chunkX6RAR374_cjs.normalizeForum(forum).base === context.client.forum) {
104
+ return null;
105
+ }
106
+ return {
107
+ client: chunkX6RAR374_cjs.clientRegistry.acquire({ forum }),
108
+ t: context?.t ?? defaultTranslator,
109
+ locale: context?.locale ?? chunkX6RAR374_cjs.resolveLocale(void 0),
110
+ messages: context?.messages,
111
+ theme: context?.theme ?? "auto",
112
+ appearance: context?.appearance,
113
+ debug: context?.debug ?? false,
114
+ emit: context?.emit ?? noop
115
+ };
116
+ }, [forum, context]);
117
+ return standalone ?? context;
118
+ }
119
+ function useClient() {
120
+ const context = react.useContext(CommentsContext);
121
+ if (!context) {
122
+ throw new Error(`${chunkX6RAR374_cjs.PACKAGE_NAME}: useClient() needs a <DiscourseComments> provider`);
123
+ }
124
+ return context.client;
125
+ }
126
+ var QUIET_AUTH_ERRORS = /* @__PURE__ */ new Set(["cancelled", "invalid_key"]);
127
+ function DiscourseComments(props) {
128
+ const { children, appearance, locale, messages, theme = "auto", debug = false, onEvent } = props;
129
+ const explicitClient = props.client;
130
+ const forum = props.forum;
131
+ const creation = props.forum === void 0 ? null : {
132
+ forum: props.forum,
133
+ fetch: props.fetch,
134
+ headers: props.headers,
135
+ storage: props.storage,
136
+ clientId: props.clientId,
137
+ locale,
138
+ debug
139
+ };
140
+ const client = react.useMemo(
141
+ () => explicitClient ?? chunkX6RAR374_cjs.clientRegistry.acquire(creation),
142
+ [explicitClient, forum]
143
+ );
144
+ const messagesKey = messages ? JSON.stringify(messages) : "";
145
+ const t = react.useMemo(() => chunkX6RAR374_cjs.createTranslator(locale, messages), [locale, messagesKey]);
146
+ const onEventRef = react.useRef(onEvent);
147
+ react.useEffect(() => {
148
+ onEventRef.current = onEvent;
149
+ });
150
+ const value = react.useMemo(
151
+ () => ({
152
+ client,
153
+ t,
154
+ locale: chunkX6RAR374_cjs.resolveLocale(locale),
155
+ messages,
156
+ theme,
157
+ appearance,
158
+ debug,
159
+ emit: (event) => onEventRef.current?.(event)
160
+ }),
161
+ [client, t, locale, messagesKey, theme, appearance, debug]
162
+ );
163
+ react.useEffect(() => {
164
+ let previous = client.auth.state();
165
+ return client.auth.onChange((state) => {
166
+ const wasSignedIn = previous.status === "signed-in";
167
+ if (state.status === "signed-in" && state.user && !wasSignedIn) {
168
+ onEventRef.current?.({ type: "auth.signedIn", user: state.user });
169
+ } else if (state.status === "anonymous" && wasSignedIn) {
170
+ onEventRef.current?.({
171
+ type: "auth.signedOut",
172
+ expired: state.error?.type === "invalid_key"
173
+ });
174
+ }
175
+ const failure = state.error;
176
+ if (failure && failure !== previous.error && !QUIET_AUTH_ERRORS.has(failure.type)) {
177
+ onEventRef.current?.({ type: "error", action: "signIn", error: failure });
178
+ }
179
+ previous = state;
180
+ });
181
+ }, [client]);
182
+ return /* @__PURE__ */ jsxRuntime.jsx(CommentsContext.Provider, { value, children });
183
+ }
184
+
185
+ // src/react/appearance.ts
186
+ function cx(...classes) {
187
+ return classes.filter(Boolean).join(" ");
188
+ }
189
+ function mergeAppearance(...layers) {
190
+ const variables = {};
191
+ const elements = {};
192
+ for (const layer of layers) {
193
+ if (!layer) {
194
+ continue;
195
+ }
196
+ Object.assign(variables, layer.variables);
197
+ for (const [name, value] of Object.entries(layer.elements ?? {})) {
198
+ elements[name] = cx(elements[name], value) || void 0;
199
+ }
200
+ }
201
+ return { variables, elements };
202
+ }
203
+ function variablesStyle(variables) {
204
+ const style = {};
205
+ for (const [name, value] of Object.entries(variables ?? {})) {
206
+ if (value !== void 0 && value !== "") {
207
+ style[`--gc-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`] = value;
208
+ }
209
+ }
210
+ return style;
211
+ }
212
+ function Icon({ children }) {
213
+ return /* @__PURE__ */ jsxRuntime.jsx(
214
+ "svg",
215
+ {
216
+ className: "gc-icon",
217
+ width: "16",
218
+ height: "16",
219
+ viewBox: "0 0 24 24",
220
+ fill: "none",
221
+ stroke: "currentColor",
222
+ strokeWidth: "2",
223
+ strokeLinecap: "round",
224
+ strokeLinejoin: "round",
225
+ "aria-hidden": "true",
226
+ focusable: "false",
227
+ children
228
+ }
229
+ );
230
+ }
231
+ function HeartIcon({ filled = false }) {
232
+ return /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx(
233
+ "path",
234
+ {
235
+ fill: filled ? "currentColor" : "none",
236
+ d: "M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1.1L12 21l7.8-7.5 1-1.1a5.5 5.5 0 0 0 0-7.8z"
237
+ }
238
+ ) });
239
+ }
240
+ function ReplyIcon() {
241
+ return /* @__PURE__ */ jsxRuntime.jsxs(Icon, { children: [
242
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 17 4 12l5-5" }),
243
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 18v-2a4 4 0 0 0-4-4H4" })
244
+ ] });
245
+ }
246
+ function MoreIcon() {
247
+ return /* @__PURE__ */ jsxRuntime.jsxs(Icon, { children: [
248
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "5", cy: "12", r: "1" }),
249
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "1" }),
250
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "19", cy: "12", r: "1" })
251
+ ] });
252
+ }
253
+ function PinIcon() {
254
+ return /* @__PURE__ */ jsxRuntime.jsxs(Icon, { children: [
255
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 17v5" }),
256
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 10.8V4h6v6.8l2 3.2H7z" })
257
+ ] });
258
+ }
259
+ function BoldIcon() {
260
+ return /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z" }) });
261
+ }
262
+ function ItalicIcon() {
263
+ return /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 4h-9M14 20H5M15 4 9 20" }) });
264
+ }
265
+ function LinkIcon() {
266
+ return /* @__PURE__ */ jsxRuntime.jsxs(Icon, { children: [
267
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7" }),
268
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7" })
269
+ ] });
270
+ }
271
+ function CodeIcon() {
272
+ return /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m16 18 6-6-6-6M8 6l-6 6 6 6" }) });
273
+ }
274
+ function QuoteIcon() {
275
+ return /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 21c3 0 7-1 7-8V5H3v7h4c0 3-1 5-4 6zM14 21c3 0 7-1 7-8V5h-7v7h4c0 3-1 5-4 6z" }) });
276
+ }
277
+ function ImageIcon() {
278
+ return /* @__PURE__ */ jsxRuntime.jsxs(Icon, { children: [
279
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
280
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "9", r: "2" }),
281
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21" })
282
+ ] });
283
+ }
284
+ function BackIcon() {
285
+ return /* @__PURE__ */ jsxRuntime.jsx(Icon, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m15 18-6-6 6-6" }) });
286
+ }
287
+ var loaded = /* @__PURE__ */ new WeakMap();
288
+ function initial(client) {
289
+ return { client, config: loaded.get(client) ?? null, error: null };
290
+ }
291
+ function useConfig(client) {
292
+ const [stored, setState] = react.useState(() => initial(client));
293
+ let state = stored;
294
+ if (stored.client !== client) {
295
+ state = initial(client);
296
+ setState(state);
297
+ }
298
+ const [attempt, setAttempt] = react.useState(0);
299
+ react.useEffect(() => {
300
+ let active = true;
301
+ const update = (next) => setState((current) => active && current.client === client ? next(current) : current);
302
+ const load = () => client.config().then(
303
+ (config) => {
304
+ loaded.set(client, config);
305
+ update(
306
+ (current) => current.config === config && !current.error ? current : { client, config, error: null }
307
+ );
308
+ },
309
+ (error) => {
310
+ update((current) => ({ ...current, error: toCommentsError(error) }));
311
+ }
312
+ );
313
+ void load();
314
+ const stop = client.auth.onChange(() => void load());
315
+ return () => {
316
+ active = false;
317
+ stop();
318
+ };
319
+ }, [client, attempt]);
320
+ const reload = react.useCallback(() => setAttempt((count) => count + 1), []);
321
+ return { config: state.config, error: state.error, reload };
322
+ }
323
+
324
+ // src/react/hooks/use-commenter.ts
325
+ function useCommenter(options = {}) {
326
+ const { client } = useCommentsContext(options.forum);
327
+ const { config } = useConfig(client);
328
+ return useCommenterFor(client, config);
329
+ }
330
+ function useCommenterFor(client, config) {
331
+ const { auth } = client;
332
+ const subscribe = react.useCallback((onChange) => auth.onChange(onChange), [auth]);
333
+ const getState = react.useCallback(() => auth.state(), [auth]);
334
+ const state = react.useSyncExternalStore(subscribe, getState, getState);
335
+ const actions = react.useMemo(
336
+ () => ({
337
+ signIn: (signInOptions) => auth.signIn(signInOptions),
338
+ cancelSignIn: () => auth.cancelSignIn(),
339
+ signOut: () => auth.signOut()
340
+ }),
341
+ [auth]
342
+ );
343
+ const providers = config?.auth_providers ?? auth.providers;
344
+ return react.useMemo(
345
+ () => ({
346
+ user: state.user,
347
+ status: state.status,
348
+ providers,
349
+ degraded: state.degraded,
350
+ error: state.error,
351
+ ...actions
352
+ }),
353
+ [state, providers, actions]
354
+ );
355
+ }
356
+ function useThread(options = {}) {
357
+ const { client } = useCommentsContext(options.forum);
358
+ return useThreadFor(client, options);
359
+ }
360
+ function useThreadFor(client, { identifier, url, title, sort }, onOpenError) {
361
+ const [attempt, setAttempt] = react.useState(0);
362
+ const [opened, setOpened] = react.useState(
363
+ { thread: null, error: null }
364
+ );
365
+ const pageKey = url === void 0 ? chunkX6RAR374_cjs.currentUrlWithoutHash() ?? "" : "";
366
+ const latest = react.useRef({ title, sort, onOpenError });
367
+ latest.current = { title, sort, onOpenError };
368
+ const requestedSort = react.useRef(sort);
369
+ react.useEffect(() => {
370
+ let active = true;
371
+ let handle = null;
372
+ setOpened((current) => current.thread || current.error ? { thread: null, error: null } : current);
373
+ queueMicrotask(() => {
374
+ if (!active) {
375
+ return;
376
+ }
377
+ const { title: openTitle, sort: openSort } = latest.current;
378
+ requestedSort.current = openSort;
379
+ client.threads.open({ identifier, url, title: openTitle, sort: openSort }).then(
380
+ (thread2) => {
381
+ if (!active) {
382
+ thread2.dispose();
383
+ return;
384
+ }
385
+ handle = thread2;
386
+ setOpened({ thread: thread2, error: null });
387
+ },
388
+ (error) => {
389
+ if (active) {
390
+ const failure = toCommentsError(error);
391
+ setOpened({ thread: null, error: failure });
392
+ latest.current.onOpenError?.(failure);
393
+ }
394
+ }
395
+ );
396
+ });
397
+ return () => {
398
+ active = false;
399
+ handle?.dispose();
400
+ };
401
+ }, [client, identifier, url, pageKey, attempt]);
402
+ const { thread } = opened;
403
+ react.useEffect(() => {
404
+ if (thread && sort !== void 0 && sort !== requestedSort.current) {
405
+ requestedSort.current = sort;
406
+ void thread.setSort(sort).catch(() => {
407
+ });
408
+ }
409
+ }, [thread, sort]);
410
+ const subscribe = react.useCallback(
411
+ (onChange) => thread ? thread.subscribe(onChange) : () => {
412
+ },
413
+ [thread]
414
+ );
415
+ const getSnapshot = react.useCallback(() => thread?.snapshot() ?? null, [thread]);
416
+ const snapshot = react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
417
+ const retry = react.useCallback(() => {
418
+ if (thread) {
419
+ void thread.load().catch(() => {
420
+ });
421
+ } else {
422
+ setAttempt((count) => count + 1);
423
+ }
424
+ }, [thread]);
425
+ return { thread, snapshot, error: opened.error, retry };
426
+ }
427
+ var RootContext = react.createContext(null);
428
+ function useRootContext(component = "This component") {
429
+ const value = react.useContext(RootContext);
430
+ if (!value) {
431
+ throw new Error(`${chunkX6RAR374_cjs.PACKAGE_NAME}: ${component} must be rendered inside <Comments.Root>`);
432
+ }
433
+ return value;
434
+ }
435
+ function Root(props) {
436
+ const context = useOptionalCommentsContext(props.forum);
437
+ if (!context) {
438
+ return /* @__PURE__ */ jsxRuntime.jsx("section", { className: cx("gc-root", "gc-root--unconfigured", props.className), "data-gc-unconfigured": "", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-notice", children: `${chunkX6RAR374_cjs.PACKAGE_NAME}: render <Comments> inside <DiscourseComments forum="\u2026"> or pass a forum prop.` }) });
439
+ }
440
+ return /* @__PURE__ */ jsxRuntime.jsx(ConfiguredRoot, { ...props });
441
+ }
442
+ function ConfiguredRoot(props) {
443
+ const {
444
+ forum,
445
+ identifier,
446
+ url,
447
+ title,
448
+ sort,
449
+ maxDepth = 2,
450
+ signIn: signInMode = "inline",
451
+ onSignInRequired,
452
+ readOnly = false,
453
+ highlight,
454
+ guidelinesUrl,
455
+ onPosted,
456
+ appearance,
457
+ unstyled = false,
458
+ theme: themeProp,
459
+ locale: localeProp,
460
+ className,
461
+ children
462
+ } = props;
463
+ const context = useCommentsContext(forum);
464
+ const { client, emit } = context;
465
+ const rootRef = react.useRef(null);
466
+ const t = react.useMemo(
467
+ () => localeProp ? chunkX6RAR374_cjs.createTranslator(localeProp, context.messages) : context.t,
468
+ [localeProp, context.messages, context.t]
469
+ );
470
+ const locale = localeProp ? chunkX6RAR374_cjs.resolveLocale(localeProp) : context.locale;
471
+ const theme = themeProp ?? context.theme;
472
+ const { variables, elements } = react.useMemo(
473
+ () => mergeAppearance(context.appearance, appearance),
474
+ [context.appearance, appearance]
475
+ );
476
+ const threadIdRef = react.useRef(void 0);
477
+ const reportError = react.useCallback(
478
+ (action, error) => emit({ type: "error", action, error: toCommentsError(error), threadId: threadIdRef.current }),
479
+ [emit]
480
+ );
481
+ const { thread, snapshot, error: openError, retry: retryThread } = useThreadFor(
482
+ client,
483
+ { identifier, url, title, sort },
484
+ (error) => reportError("open", error)
485
+ );
486
+ threadIdRef.current = thread?.id;
487
+ const { config, error: configError, reload: reloadConfig } = useConfig(client);
488
+ const commenter = useCommenterFor(client, config);
489
+ const retry = react.useCallback(() => {
490
+ retryThread();
491
+ if (configError) {
492
+ reloadConfig();
493
+ }
494
+ }, [retryThread, configError, reloadConfig]);
495
+ const loadError = snapshot?.status === "error" ? snapshot.error : null;
496
+ react.useEffect(() => {
497
+ if (loadError) {
498
+ reportError("load", loadError);
499
+ }
500
+ }, [loadError, reportError]);
501
+ const userId = commenter.user?.id ?? null;
502
+ const [authEpoch, setAuthEpoch] = react.useState(
503
+ { userId, thread: null }
504
+ );
505
+ if (authEpoch.userId !== userId) {
506
+ setAuthEpoch({ userId, thread: snapshot?.thread ?? null });
507
+ }
508
+ const permissionsFresh = !!snapshot && snapshot.thread !== authEpoch.thread;
509
+ const signedIn = commenter.status === "signed-in";
510
+ const closed = snapshot?.thread.closed ?? false;
511
+ const writable = !readOnly && !commenter.degraded;
512
+ const open = !!snapshot && snapshot.status !== "error" && writable && !closed;
513
+ const commentingDenied = open && signedIn && permissionsFresh && !snapshot.thread.can_comment;
514
+ const canCompose = open && !commentingDenied && (signedIn || signInMode === "on-submit");
515
+ const canReply = open && !commentingDenied;
516
+ const [focusView, setFocusView] = react.useState(null);
517
+ const viewRequest = react.useRef(0);
518
+ const [highlightId, setHighlightId] = react.useState(null);
519
+ const focusRequest = react.useRef(null);
520
+ const [, setFocusTick] = react.useState(0);
521
+ const focusComment = react.useCallback((commentId, options) => {
522
+ focusRequest.current = { id: commentId, scroll: options?.scroll ?? false };
523
+ setFocusTick((tick) => tick + 1);
524
+ }, []);
525
+ react.useEffect(() => {
526
+ const request = focusRequest.current;
527
+ const element = request && rootRef.current?.querySelector(`#gc-${request.id}`);
528
+ if (!request || !element) {
529
+ return;
530
+ }
531
+ focusRequest.current = null;
532
+ if (request.scroll) {
533
+ element.scrollIntoView?.({ block: "center" });
534
+ }
535
+ element.focus({ preventScroll: request.scroll });
536
+ });
537
+ react.useEffect(() => {
538
+ viewRequest.current++;
539
+ setFocusView(null);
540
+ }, [thread]);
541
+ const continueThread = react.useCallback(
542
+ async (commentId) => {
543
+ if (!thread) {
544
+ return;
545
+ }
546
+ const request = ++viewRequest.current;
547
+ try {
548
+ const context2 = await thread.context(commentId);
549
+ if (request !== viewRequest.current) {
550
+ return;
551
+ }
552
+ setFocusView({
553
+ id: commentId,
554
+ ancestors: context2.ancestors,
555
+ truncated: context2.ancestorsTruncated
556
+ });
557
+ focusComment(commentId);
558
+ } catch (error) {
559
+ if (request === viewRequest.current) {
560
+ reportError("context", error);
561
+ }
562
+ }
563
+ },
564
+ [thread, focusComment, reportError]
565
+ );
566
+ const closeFocusView = react.useCallback(() => {
567
+ viewRequest.current++;
568
+ if (focusView) {
569
+ focusComment(focusView.id);
570
+ }
571
+ setFocusView(null);
572
+ }, [focusView, focusComment]);
573
+ const hashId = useHashCommentId();
574
+ const requestedHighlight = highlight ?? hashId;
575
+ const handledHighlight = react.useRef(null);
576
+ const ready = snapshot?.status === "ready";
577
+ react.useEffect(() => {
578
+ const handled = handledHighlight.current;
579
+ if (!thread || !ready || requestedHighlight === null || handled?.thread === thread && handled.id === requestedHighlight) {
580
+ return;
581
+ }
582
+ handledHighlight.current = { thread, id: requestedHighlight };
583
+ const id = requestedHighlight;
584
+ const request = ++viewRequest.current;
585
+ void (async () => {
586
+ let location = chunkX6RAR374_cjs.findComment(thread.snapshot().comments, id);
587
+ if (!location) {
588
+ try {
589
+ await thread.context(id);
590
+ } catch (error) {
591
+ if (request === viewRequest.current) {
592
+ reportError("context", error);
593
+ }
594
+ return;
595
+ }
596
+ location = chunkX6RAR374_cjs.findComment(thread.snapshot().comments, id);
597
+ }
598
+ if (!location || request !== viewRequest.current) {
599
+ return;
600
+ }
601
+ if (location.depth > maxDepth) {
602
+ const cut = location.depth - maxDepth;
603
+ const tree = thread.snapshot().comments;
604
+ const ancestors = location.path.slice(0, cut).map((ancestorId) => chunkX6RAR374_cjs.findComment(tree, ancestorId)?.node).filter((node) => node !== void 0);
605
+ setFocusView({ id: location.path[cut], ancestors, truncated: false });
606
+ } else {
607
+ setFocusView(null);
608
+ }
609
+ setHighlightId(id);
610
+ focusComment(id, { scroll: true });
611
+ })();
612
+ }, [thread, ready, requestedHighlight, maxDepth, focusComment, reportError]);
613
+ const onSignInRequiredRef = react.useRef(onSignInRequired);
614
+ onSignInRequiredRef.current = onSignInRequired;
615
+ const canRequestSignIn = signInMode !== "manual" || onSignInRequired !== void 0;
616
+ const ensureSignedIn = react.useCallback(() => {
617
+ if (client.auth.status === "signed-in") {
618
+ return Promise.resolve(true);
619
+ }
620
+ if (signInMode === "manual") {
621
+ onSignInRequiredRef.current?.();
622
+ return Promise.resolve(false);
623
+ }
624
+ return client.auth.signIn().then(
625
+ (user) => user !== null,
626
+ () => false
627
+ );
628
+ }, [client, signInMode]);
629
+ const threadKey = snapshot?.thread.key;
630
+ const draftKey = react.useCallback(
631
+ (scope) => threadKey === void 0 ? null : `${chunkX6RAR374_cjs.STORAGE_PREFIX}:${client.forum}:draft:${threadKey}:${scope}`,
632
+ [client.forum, threadKey]
633
+ );
634
+ const value = {
635
+ client,
636
+ t,
637
+ locale,
638
+ thread,
639
+ snapshot,
640
+ openError,
641
+ retry,
642
+ config,
643
+ configError,
644
+ commenter,
645
+ maxDepth,
646
+ signInMode,
647
+ readOnly,
648
+ guidelinesUrl: guidelinesUrl ?? config?.guidelines_url ?? null,
649
+ debug: context.debug,
650
+ elements,
651
+ writable,
652
+ canCompose,
653
+ commentingDenied,
654
+ canReply,
655
+ canRequestSignIn,
656
+ focusView,
657
+ continueThread,
658
+ closeFocusView,
659
+ highlightId,
660
+ focusComment,
661
+ ensureSignedIn,
662
+ reportError,
663
+ emit,
664
+ onPosted,
665
+ draftKey
666
+ };
667
+ return /* @__PURE__ */ jsxRuntime.jsx(RootContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx(
668
+ "section",
669
+ {
670
+ ref: rootRef,
671
+ className: cx("gc-root", !unstyled && "gc-root--styled", elements.root, className),
672
+ "data-gc-theme": theme === "inherit" ? void 0 : theme,
673
+ style: variablesStyle(variables),
674
+ "aria-label": t("comments"),
675
+ "aria-busy": !snapshot && !openError ? true : void 0,
676
+ children
677
+ }
678
+ ) });
679
+ }
680
+ function useHashCommentId() {
681
+ const [id, setId] = react.useState(null);
682
+ react.useEffect(() => {
683
+ const read = () => {
684
+ const match = /^#gc-(\d+)$/.exec(window.location.hash);
685
+ setId(match ? Number(match[1]) : null);
686
+ };
687
+ read();
688
+ window.addEventListener("hashchange", read);
689
+ return () => window.removeEventListener("hashchange", read);
690
+ }, []);
691
+ return id;
692
+ }
693
+
694
+ // src/react/composer/markdown.ts
695
+ function applyFormat(value, start, end, format) {
696
+ const selected = value.slice(start, end);
697
+ switch (format) {
698
+ case "bold":
699
+ return wrap(value, start, end, "**", "**");
700
+ case "italic":
701
+ return wrap(value, start, end, "*", "*");
702
+ case "code":
703
+ return selected.includes("\n") ? fence(value, start, end) : wrap(value, start, end, "`", "`");
704
+ case "link": {
705
+ const url = "https://";
706
+ const urlStart = start + selected.length + 3;
707
+ return {
708
+ value: `${value.slice(0, start)}[${selected}](${url})${value.slice(end)}`,
709
+ selectionStart: urlStart,
710
+ selectionEnd: urlStart + url.length
711
+ };
712
+ }
713
+ case "quote":
714
+ return quote(value, start, end);
715
+ }
716
+ }
717
+ function insertText(value, start, end, text) {
718
+ const caret = start + text.length;
719
+ return {
720
+ value: value.slice(0, start) + text + value.slice(end),
721
+ selectionStart: caret,
722
+ selectionEnd: caret
723
+ };
724
+ }
725
+ function imageMarkdown(upload) {
726
+ const name = upload.original_filename.replace(/\.[^.]+$/, "").replace(/[[\]|]/g, "");
727
+ const size = upload.width && upload.height ? `|${upload.width}x${upload.height}` : "";
728
+ return `![${name}${size}](${upload.short_url})`;
729
+ }
730
+ function mentionQuery(value, caret) {
731
+ const match = /(?:^|[\s([{])@([\w.-]{1,60})$/.exec(value.slice(0, caret));
732
+ if (!match) {
733
+ return null;
734
+ }
735
+ const query = match[1];
736
+ return { start: caret - query.length - 1, query };
737
+ }
738
+ function wrap(value, start, end, before, after) {
739
+ return {
740
+ value: value.slice(0, start) + before + value.slice(start, end) + after + value.slice(end),
741
+ selectionStart: start + before.length,
742
+ selectionEnd: end + before.length
743
+ };
744
+ }
745
+ function fence(value, start, end) {
746
+ const before = start > 0 && value[start - 1] !== "\n" ? "\n" : "";
747
+ const open = `${before}\`\`\`
748
+ `;
749
+ const close = "\n```\n";
750
+ return {
751
+ value: value.slice(0, start) + open + value.slice(start, end) + close + value.slice(end),
752
+ selectionStart: start + open.length,
753
+ selectionEnd: end + open.length
754
+ };
755
+ }
756
+ function quote(value, start, end) {
757
+ const lineStart = value.lastIndexOf("\n", start - 1) + 1;
758
+ const lines = value.slice(lineStart, end).split("\n");
759
+ const quoted = lines.map((line) => `> ${line}`).join("\n");
760
+ return {
761
+ value: value.slice(0, lineStart) + quoted + value.slice(end),
762
+ selectionStart: start + 2,
763
+ selectionEnd: end + 2 * lines.length
764
+ };
765
+ }
766
+ var SEARCH_DELAY_MS = 150;
767
+ function useMentions(options) {
768
+ const { client, threadId, enabled, value, caret, onPick } = options;
769
+ const query = enabled ? mentionQuery(value, caret) : null;
770
+ const [results, setResults] = react.useState({
771
+ query: "",
772
+ users: []
773
+ });
774
+ const [active, setActive] = react.useState(0);
775
+ const [dismissed, setDismissed] = react.useState(null);
776
+ const queryKey = query ? `${query.start}:${query.query}` : null;
777
+ react.useEffect(() => {
778
+ if (!query) {
779
+ return;
780
+ }
781
+ let current = true;
782
+ const timer = setTimeout(() => {
783
+ client.mentions(query.query, threadId).then(
784
+ (users) => {
785
+ if (current) {
786
+ setResults({ query: query.query, users });
787
+ setActive(0);
788
+ }
789
+ },
790
+ () => {
791
+ }
792
+ );
793
+ }, SEARCH_DELAY_MS);
794
+ return () => {
795
+ current = false;
796
+ clearTimeout(timer);
797
+ };
798
+ }, [client, threadId, query?.query]);
799
+ const open = query !== null && results.query === query.query && results.users.length > 0 && dismissed !== queryKey;
800
+ const pick = react.useCallback(
801
+ (index) => {
802
+ const user = results.users[index];
803
+ if (query && user) {
804
+ onPick(query, user.username);
805
+ }
806
+ },
807
+ [results.users, query, onPick]
808
+ );
809
+ const onKeyDown = (event) => {
810
+ if (!open) {
811
+ return false;
812
+ }
813
+ const count = results.users.length;
814
+ switch (event.key) {
815
+ case "ArrowDown":
816
+ setActive((index) => (index + 1) % count);
817
+ break;
818
+ case "ArrowUp":
819
+ setActive((index) => (index - 1 + count) % count);
820
+ break;
821
+ case "Enter":
822
+ case "Tab":
823
+ if (event.metaKey || event.ctrlKey) {
824
+ return false;
825
+ }
826
+ pick(active);
827
+ break;
828
+ case "Escape":
829
+ setDismissed(queryKey);
830
+ break;
831
+ default:
832
+ return false;
833
+ }
834
+ event.preventDefault();
835
+ event.stopPropagation();
836
+ return true;
837
+ };
838
+ return { open, users: results.users, active, setActive, onKeyDown, pick };
839
+ }
840
+ function Composer(props) {
841
+ const root = useRootContext("Comments.Composer");
842
+ if (!root.thread || !root.snapshot || !composerAllowed(root, props.editing)) {
843
+ return null;
844
+ }
845
+ return /* @__PURE__ */ jsxRuntime.jsx(ComposerForm, { ...props, thread: root.thread });
846
+ }
847
+ function composerAllowed(root, editing) {
848
+ if (editing) {
849
+ return root.writable && root.commenter.status === "signed-in" && editing.can.edit;
850
+ }
851
+ return root.canCompose;
852
+ }
853
+ var FORMATS = [
854
+ { format: "bold", label: "bold", icon: /* @__PURE__ */ jsxRuntime.jsx(BoldIcon, {}) },
855
+ { format: "italic", label: "italic", icon: /* @__PURE__ */ jsxRuntime.jsx(ItalicIcon, {}) },
856
+ { format: "link", label: "insertLink", icon: /* @__PURE__ */ jsxRuntime.jsx(LinkIcon, {}) },
857
+ { format: "code", label: "code", icon: /* @__PURE__ */ jsxRuntime.jsx(CodeIcon, {}) },
858
+ { format: "quote", label: "quote", icon: /* @__PURE__ */ jsxRuntime.jsx(QuoteIcon, {}) }
859
+ ];
860
+ function ComposerForm({
861
+ replyTo,
862
+ editing,
863
+ placeholder,
864
+ autoFocus,
865
+ onDone,
866
+ className,
867
+ thread
868
+ }) {
869
+ const root = useRootContext();
870
+ const { t, client, config, commenter, elements } = root;
871
+ const id = react.useId();
872
+ const textareaRef = react.useRef(null);
873
+ const fileRef = react.useRef(null);
874
+ const pendingSelection = react.useRef(null);
875
+ const [timer] = react.useState(chunkX6RAR374_cjs.createComposerTimer);
876
+ const draftKey = editing ? null : root.draftKey(replyTo ? `reply:${replyTo.id}` : "root");
877
+ const [value, setValueState] = react.useState(() => draftKey ? readDraft(draftKey) : "");
878
+ const valueRef = react.useRef(value);
879
+ valueRef.current = value;
880
+ const [caret, setCaret] = react.useState(0);
881
+ const [raw, setRaw] = react.useState(editing ? "loading" : "loaded");
882
+ const [rawAttempt, setRawAttempt] = react.useState(0);
883
+ const [tab, setTab] = react.useState("write");
884
+ const [preview, setPreview] = react.useState(
885
+ { html: null, loading: false, failed: false }
886
+ );
887
+ const previewRequest = react.useRef(0);
888
+ const [submitting, setSubmitting] = react.useState(false);
889
+ const [error, setError] = react.useState(null);
890
+ const [uploading, setUploading] = react.useState(0);
891
+ const [waitSeconds, startCountdown] = useCountdown();
892
+ const submission = react.useRef(0);
893
+ const allowedRef = react.useRef(true);
894
+ allowedRef.current = composerAllowed(root, editing);
895
+ const setValue = react.useCallback(
896
+ (next, selection) => {
897
+ valueRef.current = next;
898
+ setValueState(next);
899
+ if (draftKey) {
900
+ writeDraft(draftKey, next);
901
+ }
902
+ if (selection) {
903
+ pendingSelection.current = selection;
904
+ }
905
+ },
906
+ [draftKey]
907
+ );
908
+ const applyEdit = react.useCallback(
909
+ (edit) => setValue(edit.value, [edit.selectionStart, edit.selectionEnd]),
910
+ [setValue]
911
+ );
912
+ useIsomorphicLayoutEffect(() => {
913
+ const selection = pendingSelection.current;
914
+ const textarea = textareaRef.current;
915
+ if (selection && textarea) {
916
+ pendingSelection.current = null;
917
+ textarea.focus();
918
+ textarea.setSelectionRange(selection[0], selection[1]);
919
+ setCaret(selection[1]);
920
+ }
921
+ });
922
+ react.useEffect(() => {
923
+ const textarea = textareaRef.current;
924
+ if (autoFocus && !editing && textarea) {
925
+ const end = valueRef.current.length;
926
+ textarea.focus();
927
+ textarea.setSelectionRange(end, end);
928
+ setCaret(end);
929
+ }
930
+ }, [autoFocus, editing]);
931
+ react.useEffect(
932
+ () => () => {
933
+ submission.current++;
934
+ },
935
+ [thread]
936
+ );
937
+ const editingId = editing?.id;
938
+ react.useEffect(() => {
939
+ if (editingId === void 0) {
940
+ return;
941
+ }
942
+ let active = true;
943
+ setRaw("loading");
944
+ setError(null);
945
+ thread.raw(editingId).then(
946
+ (source) => {
947
+ if (active) {
948
+ setValue(source, [source.length, source.length]);
949
+ setRaw("loaded");
950
+ }
951
+ },
952
+ (failure) => {
953
+ if (active) {
954
+ setError(toCommentsError(failure));
955
+ setRaw("failed");
956
+ }
957
+ }
958
+ );
959
+ return () => {
960
+ active = false;
961
+ };
962
+ }, [thread, editingId, setValue, rawAttempt]);
963
+ react.useEffect(() => {
964
+ if (waitSeconds === 0) {
965
+ setError((current) => current?.type === "rate_limit" ? null : current);
966
+ }
967
+ }, [waitSeconds]);
968
+ const signedIn = commenter.status === "signed-in";
969
+ const signingIn = commenter.status === "signing-in";
970
+ const uploadsEnabled = signedIn && config?.features.uploads === true;
971
+ const min = Math.max(config?.limits.min_post_length ?? 1, 1);
972
+ const max = config?.limits.max_post_length ?? Number.POSITIVE_INFINITY;
973
+ const length = value.trim().length;
974
+ const tooShort = length > 0 && length < min;
975
+ const tooLong = length > max;
976
+ const loadingRaw = raw === "loading";
977
+ const editable = raw === "loaded" && !submitting;
978
+ const busy = submitting || raw !== "loaded" || uploading > 0 || waitSeconds > 0 || signingIn;
979
+ const canSubmit = !busy && length >= min && !tooLong;
980
+ const onPickMention = react.useCallback(
981
+ (query, username) => applyEdit(insertText(valueRef.current, query.start, caret, `@${username} `)),
982
+ [applyEdit, caret]
983
+ );
984
+ const mentions = useMentions({
985
+ client,
986
+ threadId: thread.id,
987
+ enabled: signedIn && config?.features.mentions !== false && tab === "write" && editable,
988
+ value,
989
+ caret,
990
+ onPick: onPickMention
991
+ });
992
+ async function send() {
993
+ if (!allowedRef.current) {
994
+ return;
995
+ }
996
+ const body = valueRef.current;
997
+ setSubmitting(true);
998
+ setError(null);
999
+ try {
1000
+ if (editing) {
1001
+ await thread.edit(editing.id, body);
1002
+ onDone?.();
1003
+ return;
1004
+ }
1005
+ const result = await thread.post({ body, replyTo: replyTo?.id, composer: timer });
1006
+ if (valueRef.current === body) {
1007
+ setValue("");
1008
+ }
1009
+ setTab("write");
1010
+ if (result.type === "created") {
1011
+ root.emit({ type: "comment.posted", threadId: thread.id, comment: result.comment });
1012
+ if (replyTo) {
1013
+ root.focusComment(result.comment.id);
1014
+ }
1015
+ } else {
1016
+ root.emit({ type: "comment.pending", threadId: thread.id, pending: result.pending });
1017
+ }
1018
+ root.onPosted?.(result);
1019
+ onDone?.();
1020
+ } catch (failure) {
1021
+ const commentsError = toCommentsError(failure);
1022
+ if (commentsError.type === "rate_limit" && commentsError.waitSeconds) {
1023
+ startCountdown(commentsError.waitSeconds);
1024
+ }
1025
+ setError(commentsError);
1026
+ root.reportError(editing ? "edit" : "post", commentsError);
1027
+ } finally {
1028
+ setSubmitting(false);
1029
+ }
1030
+ }
1031
+ function submit() {
1032
+ if (!canSubmit) {
1033
+ return;
1034
+ }
1035
+ if (signedIn) {
1036
+ void send();
1037
+ return;
1038
+ }
1039
+ if (root.signInMode === "on-submit") {
1040
+ const token = ++submission.current;
1041
+ setError(null);
1042
+ client.auth.signIn().then(
1043
+ (user) => {
1044
+ if (user && token === submission.current) {
1045
+ void send();
1046
+ }
1047
+ },
1048
+ (failure) => {
1049
+ const signInError = toCommentsError(failure);
1050
+ if (token === submission.current && signInError.type !== "cancelled") {
1051
+ setError(signInError);
1052
+ }
1053
+ }
1054
+ );
1055
+ }
1056
+ }
1057
+ function cancel() {
1058
+ submission.current++;
1059
+ onDone?.();
1060
+ }
1061
+ function format(kind) {
1062
+ const textarea = textareaRef.current;
1063
+ const start = textarea?.selectionStart ?? valueRef.current.length;
1064
+ const end = textarea?.selectionEnd ?? start;
1065
+ applyEdit(applyFormat(valueRef.current, start, end, kind));
1066
+ }
1067
+ async function upload(files, pasted = false) {
1068
+ for (const file of files.filter((candidate) => candidate.type.startsWith("image/"))) {
1069
+ setUploading((count) => count + 1);
1070
+ try {
1071
+ const uploaded = await client.attachments.upload(thread.id, file, { pasted });
1072
+ const textarea = textareaRef.current;
1073
+ const current = valueRef.current;
1074
+ const start = textarea?.selectionStart ?? current.length;
1075
+ const end = textarea?.selectionEnd ?? start;
1076
+ const before = start > 0 && current[start - 1] !== "\n" ? "\n" : "";
1077
+ applyEdit(insertText(current, start, end, `${before}${imageMarkdown(uploaded)}
1078
+ `));
1079
+ } catch (failure) {
1080
+ setError(toCommentsError(failure));
1081
+ root.reportError("upload", failure);
1082
+ } finally {
1083
+ setUploading((count) => count - 1);
1084
+ }
1085
+ }
1086
+ }
1087
+ function showPreview() {
1088
+ setTab("preview");
1089
+ const body = valueRef.current;
1090
+ const request = ++previewRequest.current;
1091
+ if (!body.trim()) {
1092
+ setPreview({ html: null, loading: false, failed: false });
1093
+ return;
1094
+ }
1095
+ setPreview({ html: null, loading: true, failed: false });
1096
+ client.preview(body).then(
1097
+ (html) => {
1098
+ if (request === previewRequest.current) {
1099
+ setPreview({ html, loading: false, failed: false });
1100
+ }
1101
+ },
1102
+ (failure) => {
1103
+ if (request === previewRequest.current) {
1104
+ setPreview({ html: null, loading: false, failed: true });
1105
+ root.reportError("preview", failure);
1106
+ }
1107
+ }
1108
+ );
1109
+ }
1110
+ function onChange(event) {
1111
+ timer.typing();
1112
+ setValue(event.target.value);
1113
+ setCaret(event.target.selectionStart);
1114
+ }
1115
+ function onKeyDown(event) {
1116
+ if (mentions.onKeyDown(event)) {
1117
+ return;
1118
+ }
1119
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
1120
+ event.preventDefault();
1121
+ submit();
1122
+ } else if (event.key === "Escape" && (replyTo || editing)) {
1123
+ event.preventDefault();
1124
+ cancel();
1125
+ }
1126
+ }
1127
+ function onPaste(event) {
1128
+ const files = [...event.clipboardData.files];
1129
+ if (uploadsEnabled && editable && files.some((file) => file.type.startsWith("image/"))) {
1130
+ event.preventDefault();
1131
+ void upload(files, true);
1132
+ }
1133
+ }
1134
+ function onDragOver(event) {
1135
+ if (uploadsEnabled && editable && event.dataTransfer.types.includes("Files")) {
1136
+ event.preventDefault();
1137
+ }
1138
+ }
1139
+ function onDrop(event) {
1140
+ const files = [...event.dataTransfer.files];
1141
+ if (uploadsEnabled && editable && files.length > 0) {
1142
+ event.preventDefault();
1143
+ void upload(files);
1144
+ }
1145
+ }
1146
+ function onTabKeyDown(event) {
1147
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
1148
+ event.preventDefault();
1149
+ if (tab === "write") {
1150
+ showPreview();
1151
+ } else {
1152
+ setTab("write");
1153
+ }
1154
+ rootNodeOf(event.currentTarget).getElementById(`${id}-${tab === "write" ? "preview" : "write"}-tab`)?.focus();
1155
+ }
1156
+ }
1157
+ const label = editing ? t("editing") : replyTo ? t("replyingTo", { username: replyTo.author?.username ?? "" }) : t("composerPlaceholder");
1158
+ const hintId = `${id}-hint`;
1159
+ const errorId = `${id}-error`;
1160
+ const listId = `${id}-mentions`;
1161
+ const messages = error ? errorMessages(error, waitSeconds, t) : [];
1162
+ const submitLabel = editing ? t("save") : replyTo ? t("reply") : t("post");
1163
+ const defaultPlaceholder = editing ? void 0 : replyTo ? t("replyPlaceholder") : t("composerPlaceholder");
1164
+ const hint = tooLong ? t("tooLong", { max }) : tooShort ? t("tooShort", { min }) : null;
1165
+ const canPreview = signedIn;
1166
+ const isMain = !replyTo && !editing;
1167
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1168
+ "div",
1169
+ {
1170
+ className: cx(
1171
+ "gc-composer",
1172
+ replyTo && "gc-composer--replying",
1173
+ editing && "gc-composer--editing",
1174
+ elements.composer,
1175
+ className
1176
+ ),
1177
+ children: [
1178
+ isMain && root.guidelinesUrl && /* @__PURE__ */ jsxRuntime.jsx(
1179
+ "a",
1180
+ {
1181
+ className: "gc-composer__guidelines",
1182
+ href: root.guidelinesUrl,
1183
+ target: "_blank",
1184
+ rel: "noopener noreferrer",
1185
+ children: t("guidelines")
1186
+ }
1187
+ ),
1188
+ !isMain && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-composer__context", children: label }),
1189
+ canPreview && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "gc-composer__tabs", role: "tablist", children: [
1190
+ /* @__PURE__ */ jsxRuntime.jsx(
1191
+ "button",
1192
+ {
1193
+ type: "button",
1194
+ role: "tab",
1195
+ id: `${id}-write-tab`,
1196
+ className: "gc-composer__tab",
1197
+ "aria-selected": tab === "write",
1198
+ "aria-controls": `${id}-write`,
1199
+ tabIndex: tab === "write" ? 0 : -1,
1200
+ onClick: () => setTab("write"),
1201
+ onKeyDown: onTabKeyDown,
1202
+ children: t("write")
1203
+ }
1204
+ ),
1205
+ /* @__PURE__ */ jsxRuntime.jsx(
1206
+ "button",
1207
+ {
1208
+ type: "button",
1209
+ role: "tab",
1210
+ id: `${id}-preview-tab`,
1211
+ className: "gc-composer__tab",
1212
+ "aria-selected": tab === "preview",
1213
+ "aria-controls": `${id}-preview`,
1214
+ tabIndex: tab === "preview" ? 0 : -1,
1215
+ onClick: showPreview,
1216
+ onKeyDown: onTabKeyDown,
1217
+ children: t("preview")
1218
+ }
1219
+ )
1220
+ ] }),
1221
+ /* @__PURE__ */ jsxRuntime.jsxs(
1222
+ "div",
1223
+ {
1224
+ className: "gc-composer__panel",
1225
+ id: `${id}-write`,
1226
+ role: canPreview ? "tabpanel" : void 0,
1227
+ "aria-labelledby": canPreview ? `${id}-write-tab` : void 0,
1228
+ hidden: tab !== "write",
1229
+ children: [
1230
+ signedIn && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("gc-toolbar", elements.toolbar), role: "group", "aria-label": t("formatting"), children: [
1231
+ FORMATS.map(({ format: kind, label: key, icon }) => /* @__PURE__ */ jsxRuntime.jsx(
1232
+ "button",
1233
+ {
1234
+ type: "button",
1235
+ className: "gc-toolbar__button",
1236
+ "aria-label": t(key),
1237
+ title: t(key),
1238
+ disabled: !editable,
1239
+ onClick: () => format(kind),
1240
+ children: icon
1241
+ },
1242
+ kind
1243
+ )),
1244
+ uploadsEnabled && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1245
+ /* @__PURE__ */ jsxRuntime.jsx(
1246
+ "button",
1247
+ {
1248
+ type: "button",
1249
+ className: "gc-toolbar__button",
1250
+ "aria-label": t("upload"),
1251
+ title: t("upload"),
1252
+ disabled: !editable,
1253
+ onClick: () => fileRef.current?.click(),
1254
+ children: /* @__PURE__ */ jsxRuntime.jsx(ImageIcon, {})
1255
+ }
1256
+ ),
1257
+ /* @__PURE__ */ jsxRuntime.jsx(
1258
+ "input",
1259
+ {
1260
+ ref: fileRef,
1261
+ type: "file",
1262
+ accept: "image/*",
1263
+ multiple: true,
1264
+ hidden: true,
1265
+ onChange: (event) => {
1266
+ const files = [...event.target.files ?? []];
1267
+ event.target.value = "";
1268
+ void upload(files);
1269
+ }
1270
+ }
1271
+ )
1272
+ ] })
1273
+ ] }),
1274
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "gc-composer__field", children: [
1275
+ /* @__PURE__ */ jsxRuntime.jsx(
1276
+ "textarea",
1277
+ {
1278
+ ref: textareaRef,
1279
+ className: cx("gc-composer__input", elements.textarea),
1280
+ value,
1281
+ placeholder: placeholder ?? defaultPlaceholder,
1282
+ "aria-label": label,
1283
+ "aria-describedby": cx(hint && hintId, messages.length > 0 && errorId) || void 0,
1284
+ "aria-invalid": tooLong || void 0,
1285
+ "aria-autocomplete": mentions.open ? "list" : void 0,
1286
+ "aria-controls": mentions.open ? listId : void 0,
1287
+ "aria-activedescendant": mentions.open ? `${listId}-${mentions.active}` : void 0,
1288
+ rows: isMain ? 3 : 2,
1289
+ disabled: raw !== "loaded",
1290
+ readOnly: submitting,
1291
+ onChange,
1292
+ onSelect: (event) => setCaret(event.currentTarget.selectionStart),
1293
+ onFocus: () => timer.start(),
1294
+ onKeyDown,
1295
+ onPaste,
1296
+ onDragOver,
1297
+ onDrop
1298
+ }
1299
+ ),
1300
+ mentions.open && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "gc-mentions", role: "listbox", id: listId, "aria-label": t("mentionSuggestions"), children: mentions.users.map((user, index) => /* @__PURE__ */ jsxRuntime.jsxs(
1301
+ "li",
1302
+ {
1303
+ id: `${listId}-${index}`,
1304
+ role: "option",
1305
+ "aria-selected": index === mentions.active,
1306
+ className: cx(
1307
+ "gc-mentions__option",
1308
+ index === mentions.active && "gc-mentions__option--active"
1309
+ ),
1310
+ onMouseDown: (event) => {
1311
+ event.preventDefault();
1312
+ mentions.pick(index);
1313
+ },
1314
+ onMouseEnter: () => mentions.setActive(index),
1315
+ children: [
1316
+ /* @__PURE__ */ jsxRuntime.jsx("img", { className: "gc-avatar gc-avatar--small", src: user.avatar_url, alt: "" }),
1317
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "gc-mentions__username", children: [
1318
+ "@",
1319
+ user.username
1320
+ ] }),
1321
+ user.name && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "gc-mentions__name", children: user.name })
1322
+ ]
1323
+ },
1324
+ user.id
1325
+ )) })
1326
+ ] })
1327
+ ]
1328
+ }
1329
+ ),
1330
+ canPreview && /* @__PURE__ */ jsxRuntime.jsx(
1331
+ "div",
1332
+ {
1333
+ className: "gc-composer__panel",
1334
+ id: `${id}-preview`,
1335
+ role: "tabpanel",
1336
+ "aria-labelledby": `${id}-preview-tab`,
1337
+ "aria-busy": preview.loading || void 0,
1338
+ hidden: tab !== "preview",
1339
+ children: preview.loading ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-composer__status", children: t("loading") }) : preview.failed ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-composer__status", children: t("genericError") }) : preview.html ? /* @__PURE__ */ jsxRuntime.jsx(
1340
+ "div",
1341
+ {
1342
+ className: "gc-composer__preview gc-prose",
1343
+ dangerouslySetInnerHTML: { __html: preview.html }
1344
+ }
1345
+ ) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-composer__status", children: t("previewEmpty") })
1346
+ }
1347
+ ),
1348
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "gc-composer__footer", children: [
1349
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "gc-composer__messages", "aria-live": "polite", children: [
1350
+ loadingRaw && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-composer__status", children: t("loading") }),
1351
+ uploading > 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-composer__status", children: t("uploading") }),
1352
+ signingIn && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-composer__status", children: t("signingIn") }),
1353
+ hint && /* @__PURE__ */ jsxRuntime.jsx("p", { id: hintId, className: "gc-composer__hint", children: hint }),
1354
+ messages.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { id: errorId, className: "gc-composer__error", role: "alert", children: messages.map((message) => /* @__PURE__ */ jsxRuntime.jsx("p", { children: message }, message)) })
1355
+ ] }),
1356
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "gc-composer__buttons", children: [
1357
+ signingIn && /* @__PURE__ */ jsxRuntime.jsx(
1358
+ "button",
1359
+ {
1360
+ type: "button",
1361
+ className: cx("gc-button", elements.button),
1362
+ onClick: () => client.auth.cancelSignIn(),
1363
+ children: t("signInCancel")
1364
+ }
1365
+ ),
1366
+ raw === "failed" && /* @__PURE__ */ jsxRuntime.jsx(
1367
+ "button",
1368
+ {
1369
+ type: "button",
1370
+ className: cx("gc-button", elements.button),
1371
+ onClick: () => setRawAttempt((count) => count + 1),
1372
+ children: t("retry")
1373
+ }
1374
+ ),
1375
+ !isMain && !signingIn && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: cx("gc-button", elements.button), onClick: cancel, children: t("cancel") }),
1376
+ /* @__PURE__ */ jsxRuntime.jsx(
1377
+ "button",
1378
+ {
1379
+ type: "button",
1380
+ className: cx("gc-button", "gc-button--primary", elements.button, elements.primaryButton),
1381
+ disabled: !canSubmit,
1382
+ "aria-busy": submitting || void 0,
1383
+ onClick: submit,
1384
+ children: submitting && !editing ? t("posting") : submitLabel
1385
+ }
1386
+ )
1387
+ ] })
1388
+ ] })
1389
+ ]
1390
+ }
1391
+ );
1392
+ }
1393
+ function errorMessages(error, waitSeconds, t) {
1394
+ switch (error.type) {
1395
+ case "rate_limit":
1396
+ return waitSeconds > 0 ? [t("rateLimited", { count: waitSeconds })] : error.errors.slice(0, 1);
1397
+ case "invalid_key":
1398
+ case "not_logged_in":
1399
+ return [t("sessionExpired")];
1400
+ case "unreachable":
1401
+ case "unknown":
1402
+ case "cancelled":
1403
+ return [t("genericError")];
1404
+ default:
1405
+ return error.errors.length > 0 ? error.errors : [t("genericError")];
1406
+ }
1407
+ }
1408
+ function useCountdown() {
1409
+ const [until, setUntil] = react.useState(0);
1410
+ const [now, setNow] = react.useState(() => Date.now());
1411
+ react.useEffect(() => {
1412
+ if (until <= Date.now()) {
1413
+ return;
1414
+ }
1415
+ const timer = setInterval(() => {
1416
+ const at = Date.now();
1417
+ setNow(at);
1418
+ if (at >= until) {
1419
+ clearInterval(timer);
1420
+ }
1421
+ }, 250);
1422
+ return () => clearInterval(timer);
1423
+ }, [until]);
1424
+ const start = react.useCallback((seconds) => {
1425
+ const at = Date.now();
1426
+ setNow(at);
1427
+ setUntil(at + seconds * 1e3);
1428
+ }, []);
1429
+ return [Math.max(0, Math.ceil((until - now) / 1e3)), start];
1430
+ }
1431
+ var ItemContext = react.createContext(null);
1432
+ function useItemContext(component) {
1433
+ const value = react.useContext(ItemContext);
1434
+ if (!value) {
1435
+ throw new Error(`${chunkX6RAR374_cjs.PACKAGE_NAME}: ${component} must be rendered inside <Comments.Item>`);
1436
+ }
1437
+ return value;
1438
+ }
1439
+ function Avatar({ className }) {
1440
+ const { comment } = useItemContext("Comments.Avatar");
1441
+ const { elements } = useRootContext("Comments.Avatar");
1442
+ return /* @__PURE__ */ jsxRuntime.jsx(UserAvatar, { user: comment.author, className: cx(elements.avatar, className) });
1443
+ }
1444
+ function UserAvatar({
1445
+ user,
1446
+ className
1447
+ }) {
1448
+ if (user?.avatar_url) {
1449
+ return /* @__PURE__ */ jsxRuntime.jsx(
1450
+ "img",
1451
+ {
1452
+ className: cx("gc-avatar", className),
1453
+ src: user.avatar_url,
1454
+ alt: "",
1455
+ loading: "lazy",
1456
+ decoding: "async"
1457
+ }
1458
+ );
1459
+ }
1460
+ const initial2 = (user?.name || user?.username || "").charAt(0).toUpperCase();
1461
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx("gc-avatar", "gc-avatar--placeholder", className), "aria-hidden": "true", children: initial2 });
1462
+ }
1463
+ function Meta({ className }) {
1464
+ const { comment } = useItemContext("Comments.Meta");
1465
+ const { t, locale, client, elements } = useRootContext("Comments.Meta");
1466
+ const author = comment.author;
1467
+ const name = author ? author.name || author.username : null;
1468
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("gc-comment__meta", elements.meta, className), children: [
1469
+ name && (author?.username ? /* @__PURE__ */ jsxRuntime.jsx(
1470
+ "a",
1471
+ {
1472
+ className: "gc-comment__author",
1473
+ href: `${client.forum}/u/${encodeURIComponent(author.username)}`,
1474
+ target: "_blank",
1475
+ rel: "noopener noreferrer",
1476
+ children: name
1477
+ }
1478
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "gc-comment__author", children: name })),
1479
+ author?.staff && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "gc-badge gc-badge--staff", children: t("staff") }),
1480
+ comment.pinned && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "gc-badge gc-badge--featured", children: [
1481
+ /* @__PURE__ */ jsxRuntime.jsx(PinIcon, {}),
1482
+ t("pinned")
1483
+ ] }),
1484
+ /* @__PURE__ */ jsxRuntime.jsx("a", { className: "gc-comment__time", href: `#gc-${comment.id}`, children: /* @__PURE__ */ jsxRuntime.jsx(RelativeTime, { iso: comment.created_at, locale }) }),
1485
+ comment.edited && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "gc-comment__edited", title: formatAbsolute(comment.updated_at, locale), children: t("edited") })
1486
+ ] });
1487
+ }
1488
+ function RelativeTime({ iso, locale }) {
1489
+ const [now, setNow] = react.useState(Date.now);
1490
+ react.useEffect(() => onMinuteTick(() => setNow(Date.now())), []);
1491
+ return /* @__PURE__ */ jsxRuntime.jsx("time", { dateTime: iso, title: formatAbsolute(iso, locale), children: formatRelative(iso, now, locale) });
1492
+ }
1493
+ function Body({ className }) {
1494
+ const { comment, mode, setMode } = useItemContext("Comments.Body");
1495
+ const { t, elements } = useRootContext("Comments.Body");
1496
+ const classes = cx("gc-comment__body", elements.body, className);
1497
+ if (mode === "edit") {
1498
+ return /* @__PURE__ */ jsxRuntime.jsx(Composer, { editing: comment, onDone: () => setMode("view") });
1499
+ }
1500
+ if (comment.deleted) {
1501
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx(classes, "gc-comment__body--removed"), children: t("deleted") });
1502
+ }
1503
+ if (comment.hidden && !comment.html) {
1504
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx(classes, "gc-comment__body--removed"), children: t("hidden") });
1505
+ }
1506
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1507
+ comment.hidden && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-comment__hidden-note", children: t("hidden") }),
1508
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cx(classes, "gc-prose"), dangerouslySetInnerHTML: { __html: comment.html } })
1509
+ ] });
1510
+ }
1511
+ function Actions({ className }) {
1512
+ const root = useRootContext("Comments.Actions");
1513
+ const { comment, depth, setMode, setNotice } = useItemContext("Comments.Actions");
1514
+ const { t, thread, commenter, elements, writable } = root;
1515
+ if (!thread || comment.deleted) {
1516
+ return null;
1517
+ }
1518
+ const signedIn = commenter.status === "signed-in";
1519
+ const { likes, can } = comment;
1520
+ const likeDisabled = !writable || (signedIn ? !likes.can_like && !likes.liked : !root.canRequestSignIn);
1521
+ const buttonClass = cx("gc-button", "gc-button--ghost", "gc-button--small", elements.button);
1522
+ const fail = (action, error) => {
1523
+ setNotice({ text: actionError(error, t), error: true });
1524
+ root.reportError(action, error);
1525
+ };
1526
+ const toggleLike = () => {
1527
+ setNotice(null);
1528
+ const liked = !likes.liked;
1529
+ const ready = signedIn ? Promise.resolve(true) : root.ensureSignedIn();
1530
+ void ready.then(async (ok) => {
1531
+ if (!ok) {
1532
+ return;
1533
+ }
1534
+ try {
1535
+ const updated = liked ? await thread.like(comment.id) : await thread.unlike(comment.id);
1536
+ root.emit({ type: "comment.liked", threadId: thread.id, comment: updated, liked });
1537
+ } catch (error) {
1538
+ fail("like", error);
1539
+ }
1540
+ });
1541
+ };
1542
+ const reply = (event) => {
1543
+ const trigger = event.currentTarget;
1544
+ setNotice(null);
1545
+ if (signedIn || root.signInMode === "on-submit") {
1546
+ setMode("reply", trigger);
1547
+ return;
1548
+ }
1549
+ void root.ensureSignedIn().then((ok) => ok && setMode("reply", trigger));
1550
+ };
1551
+ const items = [
1552
+ {
1553
+ key: "copy",
1554
+ label: t("copyLink"),
1555
+ onSelect: () => {
1556
+ const href = `${chunkX6RAR374_cjs.currentUrlWithoutHash() ?? ""}#gc-${comment.id}`;
1557
+ const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
1558
+ if (!clipboard) {
1559
+ window.location.hash = `gc-${comment.id}`;
1560
+ return;
1561
+ }
1562
+ clipboard.writeText(href).then(
1563
+ () => setNotice({ text: t("linkCopied"), error: false }),
1564
+ () => {
1565
+ window.location.hash = `gc-${comment.id}`;
1566
+ }
1567
+ );
1568
+ }
1569
+ }
1570
+ ];
1571
+ if (writable && can.edit) {
1572
+ items.push({ key: "edit", label: t("edit"), onSelect: (trigger) => setMode("edit", trigger) });
1573
+ }
1574
+ if (writable && can.pin && depth === 0) {
1575
+ items.push({
1576
+ key: "pin",
1577
+ label: comment.pinned ? t("unpin") : t("pin"),
1578
+ onSelect: () => {
1579
+ const action = comment.pinned ? thread.unpin(comment.id) : thread.pin(comment.id);
1580
+ action.catch((error) => fail("pin", error));
1581
+ }
1582
+ });
1583
+ }
1584
+ if (writable && can.flag) {
1585
+ items.push({ key: "report", label: t("report"), onSelect: (trigger) => setMode("report", trigger) });
1586
+ }
1587
+ if (writable && can.delete) {
1588
+ items.push({
1589
+ key: "delete",
1590
+ label: t("delete"),
1591
+ danger: true,
1592
+ onSelect: (trigger) => setMode("delete", trigger)
1593
+ });
1594
+ }
1595
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("gc-actions", elements.actions, className), children: [
1596
+ /* @__PURE__ */ jsxRuntime.jsxs(
1597
+ "button",
1598
+ {
1599
+ type: "button",
1600
+ className: cx(buttonClass, "gc-actions__like", likes.liked && "gc-actions__like--active"),
1601
+ "aria-pressed": likes.liked,
1602
+ "aria-label": `${t(likes.liked ? "unlike" : "like")}, ${t("likeCount", { count: likes.count })}`,
1603
+ disabled: likeDisabled,
1604
+ onClick: toggleLike,
1605
+ children: [
1606
+ /* @__PURE__ */ jsxRuntime.jsx(HeartIcon, { filled: likes.liked }),
1607
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "gc-actions__count", children: likes.count })
1608
+ ]
1609
+ }
1610
+ ),
1611
+ root.canReply && (signedIn || root.canRequestSignIn) && /* @__PURE__ */ jsxRuntime.jsxs("button", { type: "button", className: buttonClass, onClick: reply, children: [
1612
+ /* @__PURE__ */ jsxRuntime.jsx(ReplyIcon, {}),
1613
+ t("reply")
1614
+ ] }),
1615
+ /* @__PURE__ */ jsxRuntime.jsx(ActionMenu, { items, label: t("moreActions"), buttonClass })
1616
+ ] });
1617
+ }
1618
+ function ActionMenu({
1619
+ items,
1620
+ label,
1621
+ buttonClass
1622
+ }) {
1623
+ const { elements } = useRootContext();
1624
+ const [open, setOpen] = react.useState(false);
1625
+ const id = react.useId();
1626
+ const buttonRef = react.useRef(null);
1627
+ const menuRef = react.useRef(null);
1628
+ react.useEffect(() => {
1629
+ if (!open) {
1630
+ return;
1631
+ }
1632
+ menuRef.current?.querySelector('[role="menuitem"]')?.focus();
1633
+ const onPointerDown = (event) => {
1634
+ const path = event.composedPath();
1635
+ const inside = (node) => node !== null && path.includes(node);
1636
+ if (!inside(menuRef.current) && !inside(buttonRef.current)) {
1637
+ setOpen(false);
1638
+ }
1639
+ };
1640
+ document.addEventListener("pointerdown", onPointerDown);
1641
+ return () => document.removeEventListener("pointerdown", onPointerDown);
1642
+ }, [open]);
1643
+ const close = (refocus) => {
1644
+ setOpen(false);
1645
+ {
1646
+ buttonRef.current?.focus();
1647
+ }
1648
+ };
1649
+ const onMenuKeyDown = (event) => {
1650
+ const options = [...menuRef.current?.querySelectorAll('[role="menuitem"]') ?? []];
1651
+ const index = options.indexOf(rootNodeOf(event.currentTarget).activeElement);
1652
+ const move = (next) => options[(next + options.length) % options.length]?.focus();
1653
+ switch (event.key) {
1654
+ case "ArrowDown":
1655
+ move(index + 1);
1656
+ break;
1657
+ case "ArrowUp":
1658
+ move(index - 1);
1659
+ break;
1660
+ case "Home":
1661
+ move(0);
1662
+ break;
1663
+ case "End":
1664
+ move(options.length - 1);
1665
+ break;
1666
+ case "Escape":
1667
+ close();
1668
+ break;
1669
+ case "Tab":
1670
+ setOpen(false);
1671
+ return;
1672
+ default:
1673
+ return;
1674
+ }
1675
+ event.preventDefault();
1676
+ event.stopPropagation();
1677
+ };
1678
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "gc-menu", children: [
1679
+ /* @__PURE__ */ jsxRuntime.jsx(
1680
+ "button",
1681
+ {
1682
+ ref: buttonRef,
1683
+ type: "button",
1684
+ className: cx(buttonClass, "gc-button--icon"),
1685
+ "aria-label": label,
1686
+ "aria-haspopup": "menu",
1687
+ "aria-expanded": open,
1688
+ "aria-controls": open ? id : void 0,
1689
+ onClick: () => setOpen((current) => !current),
1690
+ children: /* @__PURE__ */ jsxRuntime.jsx(MoreIcon, {})
1691
+ }
1692
+ ),
1693
+ open && /* @__PURE__ */ jsxRuntime.jsx(
1694
+ "ul",
1695
+ {
1696
+ ref: menuRef,
1697
+ id,
1698
+ role: "menu",
1699
+ "aria-label": label,
1700
+ className: cx("gc-menu__list", elements.menu),
1701
+ onKeyDown: onMenuKeyDown,
1702
+ children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx("li", { role: "none", children: /* @__PURE__ */ jsxRuntime.jsx(
1703
+ "button",
1704
+ {
1705
+ type: "button",
1706
+ role: "menuitem",
1707
+ tabIndex: -1,
1708
+ className: cx("gc-menu__item", item.danger && "gc-menu__item--danger"),
1709
+ onClick: () => {
1710
+ close();
1711
+ item.onSelect(buttonRef.current);
1712
+ },
1713
+ children: item.label
1714
+ }
1715
+ ) }, item.key))
1716
+ }
1717
+ )
1718
+ ] });
1719
+ }
1720
+ var FLAG_TYPES = [
1721
+ "inappropriate",
1722
+ "spam",
1723
+ "illegal",
1724
+ "off_topic",
1725
+ "notify_moderators"
1726
+ ];
1727
+ var MESSAGE_REQUIRED = /* @__PURE__ */ new Set(["illegal", "notify_moderators"]);
1728
+ function ReportForm({ onClose }) {
1729
+ const root = useRootContext("Comments.Item");
1730
+ const { comment, setNotice } = useItemContext("Comments.Item");
1731
+ const { t, thread, elements } = root;
1732
+ const [type, setType] = react.useState("inappropriate");
1733
+ const [message, setMessage] = react.useState("");
1734
+ const [submitting, setSubmitting] = react.useState(false);
1735
+ const [error, setError] = react.useState(null);
1736
+ const id = react.useId();
1737
+ const formRef = react.useRef(null);
1738
+ const needsMessage = MESSAGE_REQUIRED.has(type);
1739
+ const messageLabel = needsMessage ? t("reportMessageRequired") : t("reportMessagePlaceholder");
1740
+ react.useEffect(() => {
1741
+ formRef.current?.querySelector("input:checked")?.focus();
1742
+ }, []);
1743
+ const submit = async (event) => {
1744
+ event.preventDefault();
1745
+ if (!thread || !root.writable || !comment.can.flag || needsMessage && !message.trim()) {
1746
+ return;
1747
+ }
1748
+ setSubmitting(true);
1749
+ setError(null);
1750
+ try {
1751
+ await thread.flag(comment.id, { type, message: message.trim() || void 0 });
1752
+ setNotice({ text: t("reportSent"), error: false });
1753
+ onClose();
1754
+ } catch (failure) {
1755
+ setError(actionError(failure, t));
1756
+ root.reportError("flag", failure);
1757
+ setSubmitting(false);
1758
+ }
1759
+ };
1760
+ return /* @__PURE__ */ jsxRuntime.jsx(
1761
+ "div",
1762
+ {
1763
+ role: "dialog",
1764
+ "aria-modal": "false",
1765
+ "aria-labelledby": `${id}-title`,
1766
+ className: cx("gc-report", elements.dialog),
1767
+ onKeyDown: (event) => {
1768
+ if (event.key === "Escape") {
1769
+ event.stopPropagation();
1770
+ onClose();
1771
+ }
1772
+ },
1773
+ children: /* @__PURE__ */ jsxRuntime.jsxs("form", { ref: formRef, onSubmit: submit, children: [
1774
+ /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { className: "gc-report__options", children: [
1775
+ /* @__PURE__ */ jsxRuntime.jsx("legend", { id: `${id}-title`, className: "gc-report__title", children: t("reportTitle") }),
1776
+ FLAG_TYPES.map((flag) => /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "gc-report__option", children: [
1777
+ /* @__PURE__ */ jsxRuntime.jsx(
1778
+ "input",
1779
+ {
1780
+ type: "radio",
1781
+ name: `${id}-type`,
1782
+ value: flag,
1783
+ checked: type === flag,
1784
+ onChange: () => setType(flag)
1785
+ }
1786
+ ),
1787
+ t(`report.${flag}`)
1788
+ ] }, flag))
1789
+ ] }),
1790
+ /* @__PURE__ */ jsxRuntime.jsx(
1791
+ "textarea",
1792
+ {
1793
+ className: "gc-report__message",
1794
+ value: message,
1795
+ placeholder: messageLabel,
1796
+ "aria-label": messageLabel,
1797
+ required: needsMessage,
1798
+ rows: 2,
1799
+ onChange: (event) => setMessage(event.target.value)
1800
+ }
1801
+ ),
1802
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-report__error", role: "alert", children: error }),
1803
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "gc-report__buttons", children: [
1804
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: cx("gc-button", elements.button), onClick: onClose, children: t("cancel") }),
1805
+ /* @__PURE__ */ jsxRuntime.jsx(
1806
+ "button",
1807
+ {
1808
+ type: "submit",
1809
+ className: cx("gc-button", "gc-button--primary", elements.button, elements.primaryButton),
1810
+ disabled: submitting || needsMessage && !message.trim(),
1811
+ children: t("report")
1812
+ }
1813
+ )
1814
+ ] })
1815
+ ] })
1816
+ }
1817
+ );
1818
+ }
1819
+ function DeleteConfirm({ onClose }) {
1820
+ const root = useRootContext("Comments.Item");
1821
+ const { comment, setNotice } = useItemContext("Comments.Item");
1822
+ const { t, thread, elements } = root;
1823
+ const [deleting, setDeleting] = react.useState(false);
1824
+ const id = react.useId();
1825
+ const cancelRef = react.useRef(null);
1826
+ react.useEffect(() => cancelRef.current?.focus(), []);
1827
+ const confirm = async () => {
1828
+ if (!thread || !root.writable || !comment.can.delete) {
1829
+ return;
1830
+ }
1831
+ setDeleting(true);
1832
+ try {
1833
+ await thread.remove(comment.id);
1834
+ } catch (failure) {
1835
+ setNotice({ text: actionError(failure, t), error: true });
1836
+ root.reportError("delete", failure);
1837
+ }
1838
+ onClose();
1839
+ };
1840
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1841
+ "div",
1842
+ {
1843
+ role: "alertdialog",
1844
+ "aria-modal": "false",
1845
+ "aria-labelledby": id,
1846
+ className: cx("gc-confirm", elements.dialog),
1847
+ onKeyDown: (event) => {
1848
+ if (event.key === "Escape") {
1849
+ event.stopPropagation();
1850
+ onClose();
1851
+ }
1852
+ },
1853
+ children: [
1854
+ /* @__PURE__ */ jsxRuntime.jsx("p", { id, className: "gc-confirm__text", children: t("deleteConfirm") }),
1855
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "gc-confirm__buttons", children: [
1856
+ /* @__PURE__ */ jsxRuntime.jsx("button", { ref: cancelRef, type: "button", className: cx("gc-button", elements.button), onClick: onClose, children: t("cancel") }),
1857
+ /* @__PURE__ */ jsxRuntime.jsx(
1858
+ "button",
1859
+ {
1860
+ type: "button",
1861
+ className: cx("gc-button", "gc-button--danger", elements.button),
1862
+ disabled: deleting,
1863
+ onClick: () => void confirm(),
1864
+ children: t("delete")
1865
+ }
1866
+ )
1867
+ ] })
1868
+ ]
1869
+ }
1870
+ );
1871
+ }
1872
+ function actionError(error, t) {
1873
+ const failure = toCommentsError(error);
1874
+ if (failure.type === "rate_limit" && failure.waitSeconds) {
1875
+ return t("rateLimited", { count: failure.waitSeconds });
1876
+ }
1877
+ if (failure.type === "invalid_key" || failure.type === "not_logged_in") {
1878
+ return t("sessionExpired");
1879
+ }
1880
+ if (failure.type === "unreachable" || failure.type === "unknown" || failure.errors.length === 0) {
1881
+ return t("genericError");
1882
+ }
1883
+ return failure.errors[0];
1884
+ }
1885
+ function DevPanel({ className }) {
1886
+ const { client, openError, snapshot, configError, debug, commenter } = useRootContext("Comments.DevPanel");
1887
+ if (!debug && !isLocalPage()) {
1888
+ return null;
1889
+ }
1890
+ const error = openError ?? (snapshot?.status === "error" ? snapshot.error : null) ?? configError ?? null;
1891
+ const origin = typeof window === "undefined" ? "" : window.location.origin;
1892
+ if (!error) {
1893
+ if (!debug || !snapshot) {
1894
+ return null;
1895
+ }
1896
+ return /* @__PURE__ */ jsxRuntime.jsxs("details", { className: cx("gc-devpanel", "gc-devpanel--ok", className), children: [
1897
+ /* @__PURE__ */ jsxRuntime.jsxs("summary", { className: "gc-devpanel__title", children: [
1898
+ chunkX6RAR374_cjs.PACKAGE_NAME,
1899
+ " diagnostics"
1900
+ ] }),
1901
+ /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "gc-devpanel__details", children: [
1902
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Forum" }),
1903
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: client.forum }),
1904
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Thread" }),
1905
+ /* @__PURE__ */ jsxRuntime.jsxs("dd", { children: [
1906
+ snapshot.thread.id,
1907
+ " ",
1908
+ snapshot.virtual ? "(not created yet)" : ""
1909
+ ] }),
1910
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Key" }),
1911
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: snapshot.thread.key }),
1912
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Updates" }),
1913
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: snapshot.connection }),
1914
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Reader" }),
1915
+ /* @__PURE__ */ jsxRuntime.jsxs("dd", { children: [
1916
+ commenter.status,
1917
+ commenter.degraded ? " (degraded)" : ""
1918
+ ] })
1919
+ ] })
1920
+ ] });
1921
+ }
1922
+ const { summary, causes } = diagnose(error, client.forum, origin);
1923
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1924
+ "aside",
1925
+ {
1926
+ className: cx("gc-devpanel", className),
1927
+ "aria-label": `${chunkX6RAR374_cjs.PACKAGE_NAME} setup diagnostics`,
1928
+ children: [
1929
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "gc-devpanel__title", children: [
1930
+ chunkX6RAR374_cjs.PACKAGE_NAME,
1931
+ " could not load comments"
1932
+ ] }),
1933
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-devpanel__summary", children: summary }),
1934
+ causes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "gc-devpanel__causes", children: causes.map((cause) => /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
1935
+ cause.text,
1936
+ cause.link && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1937
+ " ",
1938
+ /* @__PURE__ */ jsxRuntime.jsx("a", { href: cause.link.href, target: "_blank", rel: "noopener noreferrer", children: cause.link.label })
1939
+ ] }),
1940
+ cause.code && /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "gc-devpanel__code", children: /* @__PURE__ */ jsxRuntime.jsx("code", { children: cause.code }) })
1941
+ ] }, cause.text)) }),
1942
+ /* @__PURE__ */ jsxRuntime.jsxs("dl", { className: "gc-devpanel__details", children: [
1943
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Forum" }),
1944
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: client.forum }),
1945
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Page origin" }),
1946
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { children: origin }),
1947
+ /* @__PURE__ */ jsxRuntime.jsx("dt", { children: "Error" }),
1948
+ /* @__PURE__ */ jsxRuntime.jsxs("dd", { children: [
1949
+ /* @__PURE__ */ jsxRuntime.jsx("code", { children: error.type }),
1950
+ " (status ",
1951
+ error.status,
1952
+ "): ",
1953
+ error.message
1954
+ ] })
1955
+ ] }),
1956
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-devpanel__note", children: "Shown on localhost or with debug. Readers see a short \u201CComments are unavailable\u201D notice." })
1957
+ ]
1958
+ }
1959
+ );
1960
+ }
1961
+ function diagnose(error, forum, origin) {
1962
+ const host = origin ? new URL(origin).host : "this site's host";
1963
+ const embedding = {
1964
+ href: `${forum}/admin/customize/embedding`,
1965
+ label: "Open Admin \u2192 Customize \u2192 Embedding"
1966
+ };
1967
+ const setting = (name) => ({
1968
+ href: `${forum}/admin/site_settings/category/all_results?filter=${name}`,
1969
+ label: `Open ${name}`
1970
+ });
1971
+ const originNotAllowed = {
1972
+ text: `This page's origin (${origin || "unknown"}) is not allowed: add ${host} as an embedding host, or add the origin to discourse_comments_allowed_origins.`,
1973
+ link: embedding
1974
+ };
1975
+ const pluginDisabled = {
1976
+ text: "The discourse-comments plugin is not installed or is disabled (discourse_comments_enabled).",
1977
+ link: setting("discourse_comments_enabled")
1978
+ };
1979
+ switch (error.type) {
1980
+ case "unreachable":
1981
+ return {
1982
+ summary: `The browser got no usable response from ${forum}; CORS hides the reason, so check these in order.`,
1983
+ causes: [
1984
+ originNotAllowed,
1985
+ {
1986
+ text: "The forum is behind HTTP basic auth, which rejects CORS preflight (OPTIONS) requests. Let OPTIONS through without credentials and pass the credentials with the provider's headers prop, e.g. in nginx:",
1987
+ code: [
1988
+ "location / {",
1989
+ " limit_except OPTIONS {",
1990
+ ' auth_basic "Restricted";',
1991
+ " auth_basic_user_file /etc/nginx/.htpasswd;",
1992
+ " }",
1993
+ "}"
1994
+ ].join("\n")
1995
+ },
1996
+ pluginDisabled,
1997
+ {
1998
+ text: "The forum URL is wrong or the forum is offline.",
1999
+ link: { href: forum, label: "Open the forum" }
2000
+ }
2001
+ ]
2002
+ };
2003
+ case "not_found":
2004
+ return { summary: "The comments API answered 404.", causes: [pluginDisabled] };
2005
+ case "page_not_allowed":
2006
+ return {
2007
+ summary: `${forum} does not accept comments for this page.`,
2008
+ causes: [
2009
+ {
2010
+ text: `Add ${host} as an embedding host (check its allowed paths), or add this origin to discourse_comments_allowed_origins.`,
2011
+ link: embedding
2012
+ }
2013
+ ]
2014
+ };
2015
+ case "no_category":
2016
+ return {
2017
+ summary: "The forum has no category for new comment threads.",
2018
+ causes: [
2019
+ { text: `Pick a category for ${host} under Embedding.`, link: embedding },
2020
+ {
2021
+ text: "Or set discourse_comments_default_category (and discourse_comments_default_author).",
2022
+ link: setting("discourse_comments_default_category")
2023
+ }
2024
+ ]
2025
+ };
2026
+ case "invalid_url":
2027
+ return {
2028
+ summary: "The forum rejected the page URL.",
2029
+ causes: [
2030
+ {
2031
+ text: 'Pass an absolute http(s) url prop, or add a <link rel="canonical"> to the page.'
2032
+ }
2033
+ ]
2034
+ };
2035
+ case "thread_conflict":
2036
+ return {
2037
+ summary: "The identifier and the URL already belong to different threads.",
2038
+ causes: [
2039
+ { text: "Pass the identifier this URL was first used with, or only one of the two." }
2040
+ ]
2041
+ };
2042
+ case "invalid_access":
2043
+ return {
2044
+ summary: "The forum refused access to this thread.",
2045
+ causes: [
2046
+ {
2047
+ text: "The thread's category may not be visible to this reader; check the category's security settings."
2048
+ }
2049
+ ]
2050
+ };
2051
+ default:
2052
+ return { summary: error.message, causes: [] };
2053
+ }
2054
+ }
2055
+ function Header({ children, className }) {
2056
+ const { elements } = useRootContext("Comments.Header");
2057
+ return /* @__PURE__ */ jsxRuntime.jsx("header", { className: cx("gc-header", elements.header, className), children: children ?? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2058
+ /* @__PURE__ */ jsxRuntime.jsx(Count, {}),
2059
+ /* @__PURE__ */ jsxRuntime.jsx(Sort, {})
2060
+ ] }) });
2061
+ }
2062
+ function Count({ as: Tag = "h2", render, className }) {
2063
+ const { t, snapshot, elements } = useRootContext("Comments.Count");
2064
+ const count = snapshot?.thread.count;
2065
+ return /* @__PURE__ */ jsxRuntime.jsx(Tag, { className: cx("gc-header__count", elements.count, className), children: count === void 0 ? t("comments") : render ? render(count) : t("commentCount", { count }) });
2066
+ }
2067
+ function Sort({ className }) {
2068
+ const { t, thread, snapshot, elements, reportError } = useRootContext("Comments.Sort");
2069
+ const id = react.useId();
2070
+ if (!thread || !snapshot || snapshot.thread.sorts.length < 2) {
2071
+ return null;
2072
+ }
2073
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("gc-sort", elements.sort, className), children: [
2074
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "gc-sort__label", htmlFor: id, children: t("sortBy") }),
2075
+ /* @__PURE__ */ jsxRuntime.jsx(
2076
+ "select",
2077
+ {
2078
+ id,
2079
+ className: "gc-sort__select",
2080
+ value: snapshot.sort,
2081
+ disabled: snapshot.status === "loading",
2082
+ onChange: (event) => void thread.setSort(event.target.value).catch(
2083
+ (error) => reportError("load", error)
2084
+ ),
2085
+ children: snapshot.thread.sorts.map((sort) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: sort, children: t(`sort.${sort}`) }, sort))
2086
+ }
2087
+ )
2088
+ ] });
2089
+ }
2090
+ var defaultRender = (comment, depth) => /* @__PURE__ */ jsxRuntime.jsx(Item, { comment, depth });
2091
+ var ListContext = react.createContext(defaultRender);
2092
+ function List({ children, className }) {
2093
+ const root = useRootContext("Comments.List");
2094
+ const render = children ?? defaultRender;
2095
+ const { snapshot, openError, t, elements, focusView } = root;
2096
+ if (openError || snapshot?.status === "error") {
2097
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx("gc-notice", "gc-notice--error", elements.notice), role: "alert", children: [
2098
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-notice__text", children: t("unavailable") }),
2099
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: cx("gc-button", elements.button), onClick: root.retry, children: t("retry") })
2100
+ ] });
2101
+ }
2102
+ if (!snapshot || snapshot.status === "loading" && snapshot.comments.length === 0) {
2103
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-list__status", role: "status", children: t("loading") });
2104
+ }
2105
+ const listClass = cx("gc-list", elements.list, className);
2106
+ if (focusView) {
2107
+ const tree = snapshot.comments;
2108
+ const target = chunkX6RAR374_cjs.findComment(tree, focusView.id)?.node;
2109
+ return /* @__PURE__ */ jsxRuntime.jsx(ListContext.Provider, { value: render, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "gc-focus", children: [
2110
+ /* @__PURE__ */ jsxRuntime.jsxs(
2111
+ "button",
2112
+ {
2113
+ type: "button",
2114
+ className: cx("gc-button", "gc-button--link", "gc-focus__back", elements.button),
2115
+ onClick: root.closeFocusView,
2116
+ children: [
2117
+ /* @__PURE__ */ jsxRuntime.jsx(BackIcon, {}),
2118
+ t("backToAll")
2119
+ ]
2120
+ }
2121
+ ),
2122
+ focusView.truncated && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-focus__truncated", children: t("ancestorsTruncated") }),
2123
+ focusView.ancestors.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "gc-context", children: focusView.ancestors.map((ancestor) => /* @__PURE__ */ jsxRuntime.jsx(
2124
+ Item,
2125
+ {
2126
+ comment: chunkX6RAR374_cjs.findComment(tree, ancestor.id)?.node ?? ancestor,
2127
+ showReplies: false
2128
+ },
2129
+ ancestor.id
2130
+ )) }),
2131
+ target && /* @__PURE__ */ jsxRuntime.jsx("ol", { className: listClass, children: render(target, 0) })
2132
+ ] }) });
2133
+ }
2134
+ if (snapshot.comments.length === 0) {
2135
+ return null;
2136
+ }
2137
+ return /* @__PURE__ */ jsxRuntime.jsx(ListContext.Provider, { value: render, children: /* @__PURE__ */ jsxRuntime.jsx("ol", { className: listClass, children: snapshot.comments.map((comment) => /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: render(comment, 0) }, comment.id)) }) });
2138
+ }
2139
+ function Item({ comment, depth = 0, children, showReplies = true, className }) {
2140
+ const root = useRootContext("Comments.Item");
2141
+ const { t, elements } = root;
2142
+ const [mode, setModeState] = react.useState("view");
2143
+ const [notice, setNotice] = react.useState(null);
2144
+ const articleRef = react.useRef(null);
2145
+ const triggerRef = react.useRef(null);
2146
+ const restoreFocus = react.useRef(false);
2147
+ const setMode = react.useCallback((next, trigger) => {
2148
+ if (next === "view") {
2149
+ restoreFocus.current = true;
2150
+ } else {
2151
+ triggerRef.current = trigger ?? null;
2152
+ }
2153
+ setModeState(next);
2154
+ }, []);
2155
+ react.useEffect(() => {
2156
+ if (mode !== "view" || !restoreFocus.current) {
2157
+ return;
2158
+ }
2159
+ restoreFocus.current = false;
2160
+ const trigger = triggerRef.current;
2161
+ (trigger?.isConnected ? trigger : articleRef.current)?.focus();
2162
+ }, [mode]);
2163
+ const signedIn = root.commenter.status === "signed-in";
2164
+ const writable = root.writable && signedIn;
2165
+ const allowed = {
2166
+ view: true,
2167
+ reply: root.canCompose,
2168
+ edit: writable && comment.can.edit,
2169
+ report: writable && comment.can.flag,
2170
+ delete: writable && comment.can.delete
2171
+ };
2172
+ const modeAllowed = allowed[mode];
2173
+ const activeMode = modeAllowed ? mode : "view";
2174
+ react.useEffect(() => {
2175
+ if (!modeAllowed) {
2176
+ setMode("view");
2177
+ }
2178
+ }, [modeAllowed, setMode]);
2179
+ react.useEffect(() => {
2180
+ if (!notice || notice.error) {
2181
+ return;
2182
+ }
2183
+ const timer = setTimeout(() => setNotice(null), 3e3);
2184
+ return () => clearTimeout(timer);
2185
+ }, [notice]);
2186
+ const value = react.useMemo(
2187
+ () => ({ comment, depth, mode: activeMode, setMode, notice, setNotice }),
2188
+ [comment, depth, activeMode, setMode, notice]
2189
+ );
2190
+ const name = comment.author ? comment.author.name || comment.author.username : null;
2191
+ const label = comment.deleted || !name ? t("deletedComment") : t("commentBy", { name });
2192
+ const close = () => setMode("view");
2193
+ return /* @__PURE__ */ jsxRuntime.jsx(ItemContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsxs(
2194
+ "li",
2195
+ {
2196
+ className: cx(
2197
+ "gc-comment",
2198
+ depth > 0 && "gc-comment--reply",
2199
+ comment.pinned && "gc-comment--pinned",
2200
+ comment.deleted && "gc-comment--deleted",
2201
+ root.highlightId === comment.id && "gc-comment--highlighted",
2202
+ elements.comment,
2203
+ className
2204
+ ),
2205
+ "data-depth": depth,
2206
+ children: [
2207
+ /* @__PURE__ */ jsxRuntime.jsxs(
2208
+ "article",
2209
+ {
2210
+ ref: articleRef,
2211
+ id: `gc-${comment.id}`,
2212
+ className: "gc-comment__card",
2213
+ tabIndex: -1,
2214
+ "aria-label": label,
2215
+ children: [
2216
+ children ?? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2217
+ /* @__PURE__ */ jsxRuntime.jsx(Avatar, {}),
2218
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "gc-comment__main", children: [
2219
+ /* @__PURE__ */ jsxRuntime.jsx(Meta, {}),
2220
+ /* @__PURE__ */ jsxRuntime.jsx(Body, {}),
2221
+ /* @__PURE__ */ jsxRuntime.jsx(Actions, {})
2222
+ ] })
2223
+ ] }),
2224
+ activeMode === "report" && /* @__PURE__ */ jsxRuntime.jsx(ReportForm, { onClose: close }),
2225
+ activeMode === "delete" && /* @__PURE__ */ jsxRuntime.jsx(DeleteConfirm, { onClose: close }),
2226
+ notice && /* @__PURE__ */ jsxRuntime.jsx(
2227
+ "p",
2228
+ {
2229
+ className: cx("gc-comment__notice", notice.error && "gc-comment__notice--error"),
2230
+ role: notice.error ? "alert" : "status",
2231
+ children: notice.text
2232
+ }
2233
+ )
2234
+ ]
2235
+ }
2236
+ ),
2237
+ activeMode === "reply" && /* @__PURE__ */ jsxRuntime.jsx(Composer, { replyTo: comment, autoFocus: true, onDone: close }),
2238
+ showReplies && /* @__PURE__ */ jsxRuntime.jsx(Replies, {})
2239
+ ]
2240
+ }
2241
+ ) });
2242
+ }
2243
+ function Replies() {
2244
+ const root = useRootContext();
2245
+ const { comment, depth } = useItemContext("Comments.Item");
2246
+ const render = react.useContext(ListContext);
2247
+ const { t, thread, snapshot, maxDepth, elements } = root;
2248
+ if (!thread || !snapshot) {
2249
+ return null;
2250
+ }
2251
+ const children = comment.children;
2252
+ const linkButton = cx("gc-button", "gc-button--link", elements.button);
2253
+ if (depth >= maxDepth) {
2254
+ if (comment.direct_reply_count === 0 && children.length === 0) {
2255
+ return null;
2256
+ }
2257
+ return /* @__PURE__ */ jsxRuntime.jsx(
2258
+ "button",
2259
+ {
2260
+ type: "button",
2261
+ className: cx(linkButton, "gc-comment__continue"),
2262
+ onClick: () => void root.continueThread(comment.id),
2263
+ children: t("continueThread")
2264
+ }
2265
+ );
2266
+ }
2267
+ const branch = snapshot.branches[comment.id];
2268
+ const remaining = comment.direct_reply_count - children.length;
2269
+ const hasMore = (branch?.hasMore ?? true) && remaining > 0;
2270
+ const name = comment.author?.name || comment.author?.username || "";
2271
+ const loadReplies = async () => {
2272
+ const before = new Set(children.map((child) => child.id));
2273
+ try {
2274
+ await thread.loadReplies(comment.id);
2275
+ } catch (error) {
2276
+ root.reportError("loadReplies", error);
2277
+ return;
2278
+ }
2279
+ const loaded2 = chunkX6RAR374_cjs.findComment(thread.snapshot().comments, comment.id)?.node.children ?? [];
2280
+ const first = loaded2.find((child) => !before.has(child.id));
2281
+ if (first) {
2282
+ root.focusComment(first.id);
2283
+ }
2284
+ };
2285
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2286
+ children.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ol", { className: "gc-replies", "aria-label": t("repliesTo", { name }), children: children.map((child) => /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: render(child, depth + 1) }, child.id)) }),
2287
+ hasMore && /* @__PURE__ */ jsxRuntime.jsx(
2288
+ "button",
2289
+ {
2290
+ type: "button",
2291
+ className: cx(linkButton, "gc-comment__more"),
2292
+ disabled: branch?.loading,
2293
+ "aria-busy": branch?.loading || void 0,
2294
+ onClick: () => void loadReplies(),
2295
+ children: children.length === 0 ? t("loadReplies", { count: remaining }) : t("loadMoreReplies", { count: remaining })
2296
+ }
2297
+ )
2298
+ ] });
2299
+ }
2300
+ function SignIn({ children, className }) {
2301
+ const root = useRootContext("Comments.SignIn");
2302
+ const { t, commenter, elements, client, config, snapshot, signInMode } = root;
2303
+ const titleId = react.useId();
2304
+ if (children) {
2305
+ return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: children(commenter) });
2306
+ }
2307
+ if (signInMode === "manual") {
2308
+ return null;
2309
+ }
2310
+ const button = cx("gc-button", elements.button);
2311
+ const classes = cx("gc-signin", elements.signIn, className);
2312
+ if (commenter.status === "signed-in" && commenter.user) {
2313
+ const { user } = commenter;
2314
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx(classes, "gc-signin--signed-in"), children: [
2315
+ /* @__PURE__ */ jsxRuntime.jsx(UserAvatar, { user, className: "gc-avatar--small" }),
2316
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "gc-signin__user", children: t("signedInAs", { username: `@${user.username}` }) }),
2317
+ /* @__PURE__ */ jsxRuntime.jsx(
2318
+ "button",
2319
+ {
2320
+ type: "button",
2321
+ className: cx(button, "gc-button--link"),
2322
+ onClick: () => void commenter.signOut(),
2323
+ children: t("signOut")
2324
+ }
2325
+ )
2326
+ ] });
2327
+ }
2328
+ const closed = snapshot?.thread.closed ?? false;
2329
+ if (signInMode !== "inline" || !root.writable || closed || !snapshot) {
2330
+ return null;
2331
+ }
2332
+ const forumName = config?.forum.name ?? new URL(client.forum).host;
2333
+ const signingIn = commenter.status === "signing-in";
2334
+ const failure = commenter.error && commenter.error.type !== "cancelled" ? commenter.error.type === "invalid_key" ? t("sessionExpired") : commenter.error.errors[0] ?? t("genericError") : null;
2335
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2336
+ "div",
2337
+ {
2338
+ className: cx(classes, signingIn && "gc-signin--signing-in"),
2339
+ role: "group",
2340
+ "aria-labelledby": titleId,
2341
+ children: [
2342
+ /* @__PURE__ */ jsxRuntime.jsx("p", { id: titleId, className: "gc-signin__title", children: t("joinConversation") }),
2343
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { "aria-live": "polite", children: [
2344
+ signingIn ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-signin__body", children: t("signingIn") }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-signin__body", children: t("joinConversationBody", { forum: forumName }) }),
2345
+ failure && !signingIn && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "gc-signin__error", role: "alert", children: failure })
2346
+ ] }),
2347
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "gc-signin__actions", children: signingIn ? /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: button, onClick: () => commenter.cancelSignIn(), children: t("signInCancel") }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2348
+ /* @__PURE__ */ jsxRuntime.jsx(
2349
+ "button",
2350
+ {
2351
+ type: "button",
2352
+ className: cx(button, "gc-button--primary", elements.primaryButton),
2353
+ onClick: () => void commenter.signIn().catch(() => {
2354
+ }),
2355
+ children: t("signIn")
2356
+ }
2357
+ ),
2358
+ commenter.providers.map((provider) => /* @__PURE__ */ jsxRuntime.jsx(
2359
+ "button",
2360
+ {
2361
+ type: "button",
2362
+ className: button,
2363
+ onClick: () => void commenter.signIn({ provider: provider.name }).catch(() => {
2364
+ }),
2365
+ children: t("signInWith", { provider: provider.title })
2366
+ },
2367
+ provider.name
2368
+ ))
2369
+ ] }) })
2370
+ ]
2371
+ }
2372
+ );
2373
+ }
2374
+ function Notice({ className }) {
2375
+ const { t, snapshot, readOnly, commenter, commentingDenied, elements } = useRootContext("Comments.Notice");
2376
+ if (!snapshot || snapshot.status === "error") {
2377
+ return null;
2378
+ }
2379
+ let text = null;
2380
+ let modifier = "";
2381
+ if (readOnly || commenter.degraded) {
2382
+ text = t("readOnly");
2383
+ modifier = "gc-notice--read-only";
2384
+ } else if (snapshot.thread.closed) {
2385
+ text = t("closed");
2386
+ modifier = "gc-notice--closed";
2387
+ } else if (commentingDenied) {
2388
+ text = t("cannotComment");
2389
+ modifier = "gc-notice--denied";
2390
+ }
2391
+ if (!text) {
2392
+ return null;
2393
+ }
2394
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx("gc-notice", modifier, elements.notice, className), role: "status", children: text });
2395
+ }
2396
+ function NewComments({ className }) {
2397
+ const root = useRootContext("Comments.NewComments");
2398
+ const { t, thread, snapshot, elements, focusView } = root;
2399
+ const count = focusView ? 0 : snapshot?.pendingNew ?? 0;
2400
+ const show = () => {
2401
+ if (!thread) {
2402
+ return;
2403
+ }
2404
+ const before = new Set(thread.snapshot().comments.map((comment) => comment.id));
2405
+ thread.showPending();
2406
+ const first = thread.snapshot().comments.find((comment) => !before.has(comment.id));
2407
+ if (first) {
2408
+ root.focusComment(first.id);
2409
+ }
2410
+ };
2411
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "gc-pill-region", "aria-live": "polite", children: count > 0 && /* @__PURE__ */ jsxRuntime.jsx(
2412
+ "button",
2413
+ {
2414
+ type: "button",
2415
+ className: cx("gc-button", "gc-pill", elements.button, elements.pill, className),
2416
+ onClick: show,
2417
+ children: t("newComments", { count })
2418
+ }
2419
+ ) });
2420
+ }
2421
+ function AwaitingApproval({ className }) {
2422
+ const { t, snapshot, elements } = useRootContext("Comments.AwaitingApproval");
2423
+ const pending = snapshot?.awaitingApproval ?? [];
2424
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "gc-pending-region", "aria-live": "polite", children: pending.map((entry, index) => /* @__PURE__ */ jsxRuntime.jsx(
2425
+ "p",
2426
+ {
2427
+ className: cx("gc-notice", "gc-notice--pending", elements.notice, className),
2428
+ children: entry.message || t("pending")
2429
+ },
2430
+ entry.reviewable_id ?? `pending-${index}`
2431
+ )) });
2432
+ }
2433
+ function LoadMore({ className }) {
2434
+ const root = useRootContext("Comments.LoadMore");
2435
+ const { t, thread, snapshot, elements, focusView } = root;
2436
+ if (!thread || !snapshot?.hasMore || focusView || snapshot.status !== "ready") {
2437
+ return null;
2438
+ }
2439
+ const loadMore = async () => {
2440
+ const before = new Set(thread.snapshot().comments.map((comment) => comment.id));
2441
+ try {
2442
+ await thread.loadMore();
2443
+ } catch (error) {
2444
+ root.reportError("loadMore", error);
2445
+ return;
2446
+ }
2447
+ const first = thread.snapshot().comments.find((comment) => !before.has(comment.id));
2448
+ if (first && !thread.snapshot().hasMore) {
2449
+ root.focusComment(first.id);
2450
+ }
2451
+ };
2452
+ return /* @__PURE__ */ jsxRuntime.jsx(
2453
+ "button",
2454
+ {
2455
+ type: "button",
2456
+ className: cx("gc-button", "gc-load-more", elements.button, elements.loadMore, className),
2457
+ disabled: snapshot.loadingMore,
2458
+ "aria-busy": snapshot.loadingMore || void 0,
2459
+ onClick: () => void loadMore(),
2460
+ children: snapshot.loadingMore ? t("loading") : t("loadMore")
2461
+ }
2462
+ );
2463
+ }
2464
+ function CommentsSection(props) {
2465
+ return /* @__PURE__ */ jsxRuntime.jsxs(Root, { ...props, children: [
2466
+ /* @__PURE__ */ jsxRuntime.jsx(Header, {}),
2467
+ /* @__PURE__ */ jsxRuntime.jsx(DevPanel, {}),
2468
+ /* @__PURE__ */ jsxRuntime.jsx(Notice, {}),
2469
+ /* @__PURE__ */ jsxRuntime.jsx(SignIn, {}),
2470
+ /* @__PURE__ */ jsxRuntime.jsx(Composer, {}),
2471
+ /* @__PURE__ */ jsxRuntime.jsx(AwaitingApproval, {}),
2472
+ /* @__PURE__ */ jsxRuntime.jsx(NewComments, {}),
2473
+ /* @__PURE__ */ jsxRuntime.jsx(List, {}),
2474
+ /* @__PURE__ */ jsxRuntime.jsx(LoadMore, {})
2475
+ ] });
2476
+ }
2477
+ var Comments = Object.assign(CommentsSection, {
2478
+ Root,
2479
+ Header,
2480
+ Count,
2481
+ Sort,
2482
+ SignIn,
2483
+ Composer,
2484
+ List,
2485
+ Item,
2486
+ Avatar,
2487
+ Meta,
2488
+ Body,
2489
+ Actions,
2490
+ LoadMore,
2491
+ NewComments,
2492
+ AwaitingApproval,
2493
+ Notice,
2494
+ DevPanel
2495
+ });
2496
+ function useCommentCount(ref, options = {}) {
2497
+ const client = useOptionalCommentsContext(options.forum)?.client;
2498
+ const key = ref ? safeEncode(ref) : null;
2499
+ const latestRef = react.useRef(ref);
2500
+ latestRef.current = ref;
2501
+ const [state, setState] = react.useState({
2502
+ key: null,
2503
+ count: null,
2504
+ error: null
2505
+ });
2506
+ react.useEffect(() => {
2507
+ const current = latestRef.current;
2508
+ if (key === null || !current || !client) {
2509
+ return;
2510
+ }
2511
+ let active = true;
2512
+ client.count(current).then(
2513
+ (count) => active && setState({ key, count, error: null }),
2514
+ (error) => active && setState({ key, count: null, error: toCommentsError(error) })
2515
+ );
2516
+ return () => {
2517
+ active = false;
2518
+ };
2519
+ }, [client, key]);
2520
+ return state.key === key ? { count: state.count, error: state.error } : { count: null, error: null };
2521
+ }
2522
+ function safeEncode(ref) {
2523
+ try {
2524
+ return chunkX6RAR374_cjs.encodeRef(ref);
2525
+ } catch {
2526
+ return null;
2527
+ }
2528
+ }
2529
+ function CommentCount({
2530
+ identifier,
2531
+ host,
2532
+ url,
2533
+ topicId,
2534
+ forum,
2535
+ render,
2536
+ className
2537
+ }) {
2538
+ const context = useOptionalCommentsContext(forum);
2539
+ const pageUrl = url ?? chunkX6RAR374_cjs.defaultPageUrl();
2540
+ const ref = topicId !== void 0 ? { topicId } : identifier !== void 0 ? { identifier, host } : pageUrl ? { url: pageUrl } : null;
2541
+ const { count } = useCommentCount(ref, { forum });
2542
+ if (!context) {
2543
+ return null;
2544
+ }
2545
+ const { t } = context;
2546
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx("gc-count", className), "aria-busy": count === null || void 0, children: count === null ? null : render ? render(count) : t("commentCount", { count }) });
2547
+ }
2548
+
2549
+ exports.CommentCount = CommentCount;
2550
+ exports.Comments = Comments;
2551
+ exports.DiscourseComments = DiscourseComments;
2552
+ exports.useClient = useClient;
2553
+ exports.useCommentCount = useCommentCount;
2554
+ exports.useCommenter = useCommenter;
2555
+ exports.useThread = useThread;
2556
+ //# sourceMappingURL=react.cjs.map
2557
+ //# sourceMappingURL=react.cjs.map