@jant/core 0.4.0 → 0.4.1

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.
Files changed (35) hide show
  1. package/dist/{app-B9XQDSoB.js → app-DQgkp6yV.js} +46 -63
  2. package/dist/app-DYJLFZaM.js +6 -0
  3. package/dist/client/.vite/manifest.json +3 -3
  4. package/dist/client/_assets/client-BbJ0FhON.css +2 -0
  5. package/dist/client/_assets/client-DqsPJKiP.js +272 -0
  6. package/dist/client/_assets/{client-auth-DFDajqqT.js → client-auth-N6fiJcOg.js} +82 -82
  7. package/dist/{export-ZBlfKSKm.js → export-DwH3ga3Y.js} +2 -2
  8. package/dist/{github-sync-C593r22F.js → github-sync-D2FO19Re.js} +2 -2
  9. package/dist/{github-sync-bL1hnx3Q.js → github-sync-eHOTYZGO.js} +1 -1
  10. package/dist/index.js +3 -3
  11. package/dist/node.js +4 -4
  12. package/package.json +1 -1
  13. package/src/client/__tests__/compose-shortcuts.test.ts +1 -4
  14. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1 -1
  15. package/src/client/components/__tests__/jant-media-lightbox.test.ts +89 -0
  16. package/src/client/components/compose-types.ts +6 -1
  17. package/src/client/components/jant-compose-dialog.ts +2 -0
  18. package/src/client/components/jant-compose-editor.ts +2 -1
  19. package/src/client/components/jant-media-lightbox.ts +33 -10
  20. package/src/client/compose-bridge.ts +83 -25
  21. package/src/client/compose-launch.ts +0 -13
  22. package/src/client/thread-context.ts +1 -140
  23. package/src/client/upload-session.ts +77 -31
  24. package/src/i18n/locales/public/en.po +0 -4
  25. package/src/i18n/locales/public/zh-Hans.po +0 -4
  26. package/src/i18n/locales/public/zh-Hant.po +0 -4
  27. package/src/services/export-theme/assets/client-site.js +1 -1
  28. package/src/styles/tokens.css +0 -1
  29. package/src/styles/ui.css +0 -71
  30. package/src/ui/feed/ThreadPreview.tsx +34 -65
  31. package/src/ui/feed/__tests__/thread-preview.test.ts +64 -58
  32. package/src/ui/feed/thread-preview-state.ts +0 -48
  33. package/dist/app-CHW6VVQt.js +0 -6
  34. package/dist/client/_assets/client-BoUn7xBo.css +0 -2
  35. package/dist/client/_assets/client-dSfWfMe9.js +0 -272
@@ -14,7 +14,12 @@ export interface ComposeAttachment {
14
14
  clientId: string;
15
15
  file: File;
16
16
  previewUrl: string;
17
- /** Blob URL for a video poster frame (used as `<video poster>` on Safari) */
17
+ /**
18
+ * Poster URL for video attachments, used as `<video poster>` so Safari shows
19
+ * a preview frame (Chrome renders the first frame natively, Safari does not).
20
+ * For new uploads this is a blob URL produced by `URL.createObjectURL`; for
21
+ * edit mode it's the server-side poster URL.
22
+ */
18
23
  posterUrl: string | null;
19
24
  status: "pending" | "processing" | "uploading" | "done" | "error";
20
25
  progress: number | null;
@@ -66,6 +66,7 @@ interface ApiMediaAttachment {
66
66
  type: "media";
67
67
  id: string;
68
68
  previewUrl: string;
69
+ posterUrl?: string | null;
69
70
  alt?: string;
70
71
  mimeType: string;
71
72
  url?: string;
@@ -429,6 +430,7 @@ async function resolveApiAttachments(allAttachments: ApiAttachment[]) {
429
430
  const media = mediaItems.map((m) => ({
430
431
  id: m.id,
431
432
  previewUrl: m.previewUrl,
433
+ posterUrl: m.posterUrl ?? null,
432
434
  alt: m.alt,
433
435
  mimeType: m.mimeType,
434
436
  originalName: m.originalName,
@@ -837,6 +837,7 @@ export class JantComposeEditor extends LitElement {
837
837
  media?: Array<{
838
838
  id: string;
839
839
  previewUrl: string;
840
+ posterUrl?: string | null;
840
841
  alt?: string;
841
842
  mimeType: string;
842
843
  originalName?: string;
@@ -884,7 +885,7 @@ export class JantComposeEditor extends LitElement {
884
885
  clientId: randomUUID(),
885
886
  file: new File([], m.originalName ?? "existing", { type: m.mimeType }),
886
887
  previewUrl: m.previewUrl,
887
- posterUrl: null,
888
+ posterUrl: m.posterUrl ?? null,
888
889
  status: "done" as const,
889
890
  progress: null,
890
891
  mediaId: m.id,
@@ -285,18 +285,41 @@ export class JantMediaLightbox extends LitElement {
285
285
  if (ke.key === "Escape") {
286
286
  e.preventDefault();
287
287
  this.close();
288
- } else if (
289
- target?.classList.contains("media-lightbox-short-progress") &&
290
- (ke.key === "ArrowLeft" || ke.key === "ArrowRight")
291
- ) {
292
288
  return;
293
- } else if (ke.key === "ArrowLeft") {
294
- e.preventDefault();
295
- this.#prev();
296
- } else if (ke.key === "ArrowRight") {
297
- e.preventDefault();
298
- this.#next();
299
289
  }
290
+ if (ke.key !== "ArrowLeft" && ke.key !== "ArrowRight") return;
291
+
292
+ // Let the progress slider's native arrow-key seeking through.
293
+ if (target?.classList.contains("media-lightbox-short-progress")) return;
294
+
295
+ // On videos, arrow keys scrub the playhead. Item switching happens via
296
+ // the on-screen prev/next buttons — matches YouTube/native player conventions.
297
+ const currentImage = this._images[this._currentIndex];
298
+ if (currentImage?.mimeType?.startsWith("video/")) {
299
+ const video = this.querySelector<HTMLVideoElement>(
300
+ ".media-lightbox-video",
301
+ );
302
+ if (video) {
303
+ e.preventDefault();
304
+ const step = 5;
305
+ const delta = ke.key === "ArrowLeft" ? -step : step;
306
+ const duration =
307
+ Number.isFinite(video.duration) && video.duration > 0
308
+ ? video.duration
309
+ : null;
310
+ const nextTime =
311
+ duration != null
312
+ ? Math.max(0, Math.min(video.currentTime + delta, duration))
313
+ : Math.max(0, video.currentTime + delta);
314
+ video.currentTime = nextTime;
315
+ this._videoCurrentTime = nextTime;
316
+ }
317
+ return;
318
+ }
319
+
320
+ e.preventDefault();
321
+ if (ke.key === "ArrowLeft") this.#prev();
322
+ else this.#next();
300
323
  };
301
324
 
302
325
  #handleDialogClick = (e: Event) => {
@@ -21,6 +21,7 @@ import {
21
21
  import {
22
22
  showToast,
23
23
  showPersistentToast,
24
+ updateToast,
24
25
  replaceWithAutoClose,
25
26
  queueToastForNextPage,
26
27
  } from "./toast.js";
@@ -28,7 +29,6 @@ import { openReplyForArticle } from "./compose-launch.js";
28
29
  import { getJsonString, readJsonObject } from "./json.js";
29
30
  import { uploadViaSession } from "./upload-session.js";
30
31
  import { publicPath } from "./runtime-paths.js";
31
- import { setupThreadContexts } from "./thread-context.js";
32
32
  import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
33
33
  import { getMediaCategory } from "../lib/upload.js";
34
34
  import { resolveInlineImageUrls } from "./tiptap/inline-image-upload.js";
@@ -75,7 +75,6 @@ async function refreshTimelineThreadView(
75
75
  if (!html) return false;
76
76
 
77
77
  content.innerHTML = html;
78
- setupThreadContexts(content);
79
78
  return true;
80
79
  } catch {
81
80
  return false;
@@ -98,7 +97,6 @@ async function refreshPostCardView(postId: string): Promise<boolean> {
98
97
  );
99
98
  if (!content) return false;
100
99
  content.innerHTML = html;
101
- setupThreadContexts(content);
102
100
  return true;
103
101
  }
104
102
 
@@ -108,12 +106,6 @@ async function refreshPostCardView(postId: string): Promise<boolean> {
108
106
  if (!article) return false;
109
107
 
110
108
  article.outerHTML = html;
111
- const refreshed = document.querySelector<HTMLElement>(
112
- `article[data-post-id="${postId}"]`,
113
- );
114
- if (refreshed) {
115
- setupThreadContexts(refreshed);
116
- }
117
109
  return true;
118
110
  } catch {
119
111
  return false;
@@ -133,12 +125,6 @@ async function refreshPostPageView(postId: string): Promise<boolean> {
133
125
  if (!html) return false;
134
126
 
135
127
  container.outerHTML = html;
136
- const refreshed = document.querySelector<HTMLElement>(
137
- `[data-post-view][data-post-view-id="${postId}"]`,
138
- );
139
- if (refreshed) {
140
- setupThreadContexts(refreshed);
141
- }
142
128
  return true;
143
129
  } catch {
144
130
  return false;
@@ -173,6 +159,46 @@ const uploadPromises = new Map<string, Promise<string | null>>();
173
159
  /** Track attachments removed while their upload is still in flight */
174
160
  const removedClientIds = new Set<string>();
175
161
 
162
+ /**
163
+ * Track upload-phase progress (0..1) per clientId for live toast aggregation.
164
+ * Populated only during the actual byte-upload phase — processing/transcoding
165
+ * progress is intentionally excluded so the percentage doesn't reset mid-flight.
166
+ */
167
+ const uploadProgress = new Map<string, number>();
168
+
169
+ interface ActiveUploadToast {
170
+ clientIds: string[];
171
+ baseMsg: string;
172
+ }
173
+
174
+ let activeUploadToast: ActiveUploadToast | null = null;
175
+
176
+ function refreshUploadToast() {
177
+ if (!activeUploadToast) return;
178
+ const { clientIds, baseMsg } = activeUploadToast;
179
+ if (clientIds.length === 0) return;
180
+
181
+ let sum = 0;
182
+ let done = 0;
183
+ for (const id of clientIds) {
184
+ const p = uploadProgress.get(id) ?? 0;
185
+ sum += Math.min(1, Math.max(0, p));
186
+ if (p >= 1) done += 1;
187
+ }
188
+ const total = clientIds.length;
189
+ const pct = Math.floor((sum / total) * 100);
190
+ // Show "currently on item N of M" rather than "N completed of M": while any
191
+ // work is in flight we report the next-in-progress index (capped at total),
192
+ // so 2 files with neither done shows "1/2" instead of the confusing "0/2".
193
+ const current = Math.min(done + 1, total);
194
+
195
+ const message =
196
+ total === 1
197
+ ? `${baseMsg} ${pct}%`
198
+ : `${baseMsg} ${pct}% ${current}/${total}`;
199
+ updateToast("compose-deferred", message);
200
+ }
201
+
176
202
  /**
177
203
  * Track completed upload mediaIds by clientId.
178
204
  *
@@ -265,6 +291,13 @@ async function uploadFile(
265
291
  clientId: string,
266
292
  editor: JantComposeEditor | null,
267
293
  ): Promise<string | null> {
294
+ // Capture cheap metadata up-front so we can release `file` (the original
295
+ // potentially-huge blob) as soon as transcoding finishes. On iOS Safari
296
+ // holding a 300MB+ source blob alongside the transcoded output, upload
297
+ // chunks, and decoder buffers can push the tab past the per-process
298
+ // memory cap and get it silently reloaded mid-publish.
299
+ const fileType = file.type;
300
+ const fileName = file.name;
268
301
  try {
269
302
  let toUpload: File;
270
303
  let width: number | undefined;
@@ -274,7 +307,7 @@ async function uploadFile(
274
307
  let waveform: string | undefined;
275
308
  let poster: Blob | undefined;
276
309
 
277
- if (file.type.startsWith("video/")) {
310
+ if (fileType.startsWith("video/")) {
278
311
  // Video: transcode with mediabunny (requires WebCodecs)
279
312
  if (!VideoProcessor.isSupported()) {
280
313
  editor?.updateAttachmentStatus(
@@ -298,6 +331,8 @@ async function uploadFile(
298
331
  editor?.updateAttachmentProgress(clientId, progress);
299
332
  });
300
333
  toUpload = result.file;
334
+ // Drop the original blob ref now that we have the transcoded output.
335
+ file = null as unknown as File;
301
336
  width = result.width;
302
337
  height = result.height;
303
338
  durationSeconds = result.durationSeconds;
@@ -306,7 +341,7 @@ async function uploadFile(
306
341
  if (poster) {
307
342
  editor?.updateAttachmentPoster(clientId, poster);
308
343
  }
309
- } else if (file.type.startsWith("audio/")) {
344
+ } else if (fileType.startsWith("audio/")) {
310
345
  // Audio: transcode to AAC (.m4a) (requires WebCodecs)
311
346
  if (!AudioProcessor.isSupported()) {
312
347
  editor?.updateAttachmentStatus(
@@ -330,23 +365,24 @@ async function uploadFile(
330
365
  editor?.updateAttachmentProgress(clientId, progress);
331
366
  });
332
367
  toUpload = result.file;
368
+ file = null as unknown as File;
333
369
  } else if (
334
- file.type.startsWith("image/") ||
335
- /\.heic$/i.test(file.name) ||
336
- /\.heif$/i.test(file.name)
370
+ fileType.startsWith("image/") ||
371
+ /\.heic$/i.test(fileName) ||
372
+ /\.heif$/i.test(fileName)
337
373
  ) {
338
374
  // Image: convert HEIC/HEIF if needed, then resize + convert to WebP
339
375
  let imageFile = file;
340
376
  try {
341
377
  const { isHeic, heicTo } = await import("heic-to");
342
- if (await isHeic(file)) {
378
+ if (await isHeic(imageFile)) {
343
379
  editor?.updateAttachmentStatus(clientId, "processing", null, null);
344
380
  const blob = await heicTo({
345
- blob: file,
381
+ blob: imageFile,
346
382
  type: "image/jpeg",
347
383
  quality: 0.92,
348
384
  });
349
- imageFile = new File([blob], file.name.replace(/\.heic$/i, ".jpg"), {
385
+ imageFile = new File([blob], fileName.replace(/\.heic$/i, ".jpg"), {
350
386
  type: "image/jpeg",
351
387
  });
352
388
  editor?.updateAttachmentPreview(clientId, imageFile);
@@ -355,6 +391,8 @@ async function uploadFile(
355
391
  toUpload = result.file;
356
392
  width = result.width;
357
393
  height = result.height;
394
+ file = null as unknown as File;
395
+ imageFile = null as unknown as File;
358
396
  } catch {
359
397
  editor?.removeAttachment(clientId);
360
398
  showToast("Image format not supported.", "error");
@@ -369,7 +407,7 @@ async function uploadFile(
369
407
 
370
408
  // Extract metadata for non-video files (video metadata comes from VideoProcessor)
371
409
  // Audio waveform is already extracted above (before AudioProcessor runs).
372
- if (!file.type.startsWith("video/")) {
410
+ if (!fileType.startsWith("video/")) {
373
411
  const meta = await extractMediaMetadata(toUpload);
374
412
  width ??= meta.width;
375
413
  height ??= meta.height;
@@ -387,7 +425,7 @@ async function uploadFile(
387
425
  // markdown via the compose API and materialized by `createTextAttachment`.
388
426
  let summary: string | undefined;
389
427
  let chars: number | undefined;
390
- const category = getMediaCategory(file.type);
428
+ const category = getMediaCategory(fileType);
391
429
  if (category === "text") {
392
430
  try {
393
431
  const textContent = await toUpload.text();
@@ -400,6 +438,9 @@ async function uploadFile(
400
438
  }
401
439
  }
402
440
 
441
+ uploadProgress.set(clientId, 0);
442
+ refreshUploadToast();
443
+
403
444
  const result = await uploadViaSession(
404
445
  toUpload,
405
446
  {
@@ -414,13 +455,19 @@ async function uploadFile(
414
455
  },
415
456
  (progress) => {
416
457
  editor?.updateAttachmentProgress(clientId, progress);
458
+ uploadProgress.set(clientId, progress);
459
+ refreshUploadToast();
417
460
  },
418
461
  );
419
462
 
463
+ uploadProgress.set(clientId, 1);
464
+ refreshUploadToast();
420
465
  editor?.updateAttachmentStatus(clientId, "done", result.id, null);
421
466
  completedMediaIds.set(clientId, result.id);
422
467
  return result.id;
423
468
  } catch (error) {
469
+ uploadProgress.delete(clientId);
470
+ refreshUploadToast();
424
471
  const message = error instanceof Error ? error.message : "Upload failed";
425
472
  editor?.updateAttachmentStatus(clientId, "error", null, message);
426
473
  // Error is shown on the attachment thumbnail; only toast when there's no editor context.
@@ -625,10 +672,21 @@ document.addEventListener("jant:compose-submit-deferred", async (e: Event) => {
625
672
  // Show persistent toast only when uploads are still in flight
626
673
  if (hasPending) {
627
674
  showPersistentToast("compose-deferred", uploadingMsg);
675
+ if (detail.pendingAttachments.length > 0) {
676
+ activeUploadToast = {
677
+ clientIds: detail.pendingAttachments.map((a) => a.clientId),
678
+ baseMsg: uploadingMsg,
679
+ };
680
+ refreshUploadToast();
681
+ }
628
682
  }
629
683
 
630
684
  /** Show result toast — replaces persistent toast if one exists, otherwise shows a new one */
631
685
  const toastMsg = (msg: string, type: "success" | "error" = "success") => {
686
+ if (activeUploadToast) {
687
+ for (const id of activeUploadToast.clientIds) uploadProgress.delete(id);
688
+ activeUploadToast = null;
689
+ }
632
690
  if (hasPending) {
633
691
  replaceWithAutoClose("compose-deferred", msg, type);
634
692
  } else {
@@ -1,5 +1,4 @@
1
1
  import type { JantComposeDialog } from "./components/jant-compose-dialog.js";
2
- import type { ComposeFormat } from "./components/compose-types.js";
3
2
 
4
3
  interface ReplyToData {
5
4
  contentHtml: string;
@@ -100,16 +99,6 @@ function getReplyData(article: HTMLElement): ReplyToData {
100
99
  };
101
100
  }
102
101
 
103
- function getArticleComposeFormat(
104
- article: HTMLElement,
105
- ): ComposeFormat | undefined {
106
- const format = article.dataset.format;
107
- if (format === "note" || format === "link" || format === "quote") {
108
- return format;
109
- }
110
- return undefined;
111
- }
112
-
113
102
  export async function openNewCompose(
114
103
  options?: ComposeOpenOptions,
115
104
  ): Promise<void> {
@@ -124,12 +113,10 @@ export async function openReplyForArticle(article: HTMLElement): Promise<void> {
124
113
  if (!dialog) return;
125
114
 
126
115
  const threadRootId = article.dataset.threadRootId ?? postId;
127
- const initialFormat = getArticleComposeFormat(article);
128
116
  await dialog.openReply(
129
117
  postId,
130
118
  getReplyData(article),
131
119
  threadRootId,
132
120
  getReplyRefreshTarget(article) ?? undefined,
133
- initialFormat ? { initialFormat } : undefined,
134
121
  );
135
122
  }
@@ -1,131 +1,9 @@
1
1
  /**
2
2
  * Thread Context Interactions
3
3
  *
4
- * 1. Expand/collapse faded ancestor context via toggle button
5
- * 2. Auto-scroll to current post on thread detail pages
4
+ * Auto-scroll to current post on thread detail pages.
6
5
  */
7
6
 
8
- function parsePixelValue(value: string, fallback: number): number {
9
- const parsed = Number.parseFloat(value);
10
- return Number.isFinite(parsed) ? parsed : fallback;
11
- }
12
-
13
- function getCollapsedMaxHeight(container: HTMLElement): number {
14
- const wasExpanded = container.classList.contains("expanded");
15
- if (wasExpanded) {
16
- container.classList.remove("expanded");
17
- }
18
-
19
- const value = getComputedStyle(container).maxHeight;
20
- const maxHeight = parsePixelValue(value, 188);
21
-
22
- if (wasExpanded) {
23
- container.classList.add("expanded");
24
- }
25
-
26
- return maxHeight;
27
- }
28
-
29
- function getPendingImages(container: HTMLElement): HTMLImageElement[] {
30
- return Array.from(container.querySelectorAll("img")).filter(
31
- (image) => !image.complete,
32
- );
33
- }
34
-
35
- function waitForContentToSettle(
36
- container: HTMLElement,
37
- callback: () => void,
38
- ): void {
39
- const pendingImages = getPendingImages(container);
40
- if (pendingImages.length === 0) {
41
- requestAnimationFrame(() => {
42
- requestAnimationFrame(callback);
43
- });
44
- return;
45
- }
46
-
47
- let remaining = pendingImages.length;
48
- const handleDone = (): void => {
49
- remaining -= 1;
50
- if (remaining === 0) {
51
- callback();
52
- }
53
- };
54
-
55
- pendingImages.forEach((image) => {
56
- image.addEventListener("load", handleDone, { once: true });
57
- image.addEventListener("error", handleDone, { once: true });
58
- });
59
- }
60
-
61
- function updateThreadContextState(
62
- container: HTMLElement,
63
- toggle: HTMLElement,
64
- allowExpand: boolean,
65
- ): void {
66
- const collapsedMaxHeight = getCollapsedMaxHeight(container);
67
- const isExpanded = container.classList.contains("expanded");
68
- const overflows = container.scrollHeight > collapsedMaxHeight + 1;
69
- const showMoreLabel = toggle.dataset.labelMore ?? "Show more";
70
- const showLessLabel = toggle.dataset.labelLess ?? "Show less";
71
-
72
- if (!overflows) {
73
- if (allowExpand) {
74
- container.classList.remove(
75
- "thread-context-collapsed",
76
- "thread-context-faded",
77
- "expanded",
78
- );
79
- } else {
80
- container.classList.add("thread-context-collapsed");
81
- container.classList.remove("thread-context-faded", "expanded");
82
- }
83
- toggle.classList.add("hidden");
84
- toggle.textContent = showMoreLabel;
85
- toggle.setAttribute("aria-expanded", "false");
86
- return;
87
- }
88
-
89
- container.classList.add("thread-context-collapsed");
90
- container.classList.add("thread-context-faded");
91
- toggle.classList.remove("hidden");
92
- toggle.textContent = isExpanded ? showLessLabel : showMoreLabel;
93
- toggle.setAttribute("aria-expanded", isExpanded ? "true" : "false");
94
- }
95
-
96
- function setupThreadContext(group: HTMLElement): void {
97
- const container = group.querySelector<HTMLElement>("[data-thread-context]");
98
- const toggle = group.querySelector<HTMLElement>(
99
- "[data-thread-context-toggle]",
100
- );
101
- if (!container || !toggle) return;
102
-
103
- let allowExpand = false;
104
- updateThreadContextState(container, toggle, allowExpand);
105
-
106
- waitForContentToSettle(container, () => {
107
- allowExpand = true;
108
- updateThreadContextState(container, toggle, allowExpand);
109
- });
110
-
111
- if ("ResizeObserver" in globalThis) {
112
- const observer = new globalThis.ResizeObserver(() => {
113
- updateThreadContextState(container, toggle, allowExpand);
114
- });
115
- observer.observe(container);
116
- }
117
- }
118
-
119
- export function setupThreadContexts(
120
- root: globalThis.Document | globalThis.Element = document,
121
- ): void {
122
- root.querySelectorAll(".thread-group").forEach((group) => {
123
- if (group instanceof HTMLElement) {
124
- setupThreadContext(group);
125
- }
126
- });
127
- }
128
-
129
7
  function isFirstThreadDetailItem(current: HTMLElement): boolean {
130
8
  const group = current.closest<HTMLElement>(".thread-group-detail");
131
9
  if (!group) return false;
@@ -161,25 +39,8 @@ function scrollCurrentDetailPostIntoView(
161
39
  });
162
40
  }
163
41
 
164
- // Expand/collapse: event delegation on toggle buttons
165
- document.addEventListener("click", (e) => {
166
- const toggle = (e.target as HTMLElement).closest<HTMLElement>(
167
- "[data-thread-context-toggle]",
168
- );
169
- if (!toggle) return;
170
-
171
- const container = toggle
172
- .closest(".thread-group")
173
- ?.querySelector<HTMLElement>("[data-thread-context]");
174
- if (!container) return;
175
-
176
- container.classList.toggle("expanded");
177
- updateThreadContextState(container, toggle, true);
178
- });
179
-
180
42
  // Auto-scroll to current post on detail pages
181
43
  document.addEventListener("DOMContentLoaded", () => {
182
- setupThreadContexts(document);
183
44
  scrollCurrentDetailPostIntoView(document);
184
45
  });
185
46