@jant/core 0.6.15 → 0.6.16
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/bin/commands/import-site.js +7 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-C-oi_t8W.js → app-C7_4oab9.js} +480 -196
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-DxY5BFe8.js → client-Bq0vre8Y.js} +3 -3
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/{client-auth-CakPESv9.js → client-auth-F2rb9WfF.js} +1015 -977
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-CsFx6F_M.js → export-Dl5KCiEs.js} +93 -7
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-ppWXN3jb.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Cq1pzzOI.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +1 -1
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/client/__tests__/compose-bridge.test.ts +252 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/toast.test.ts +65 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2015 -318
- package/src/client/components/__tests__/jant-compose-editor.test.ts +53 -62
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +35 -89
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +47 -3
- package/src/client/components/jant-command-palette.ts +12 -6
- package/src/client/components/jant-compose-dialog.ts +1464 -680
- package/src/client/components/jant-compose-editor.ts +225 -93
- package/src/client/components/jant-compose-fullscreen.ts +3 -5
- package/src/client/components/jant-post-menu.ts +91 -21
- package/src/client/compose-bridge.ts +143 -122
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/post-refresh.ts +135 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/toast.ts +130 -38
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +18 -0
- package/src/db/schema.ts +20 -0
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/locales/public/en.po +89 -21
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +88 -20
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +88 -20
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/hugo-markdown.ts +5 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/post-display.ts +42 -9
- package/src/lib/timeline.ts +39 -9
- package/src/lib/tiptap-render.ts +4 -2
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +28 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/posts.ts +13 -9
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/posts.ts +7 -0
- package/src/routes/compose.tsx +6 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/pages/__tests__/archive-params.test.ts +292 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/archive.tsx +65 -17
- package/src/routes/pages/page.tsx +5 -2
- package/src/services/__tests__/collection.test.ts +32 -1
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +277 -0
- package/src/services/collection.ts +8 -10
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export.ts +3 -5
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +297 -100
- package/src/services/search.ts +9 -0
- package/src/styles/components.css +0 -14
- package/src/styles/site-media.css +4 -4
- package/src/styles/tokens.css +62 -1
- package/src/styles/ui.css +1363 -1283
- package/src/types/entities.ts +5 -0
- package/src/types/props.ts +12 -0
- package/src/types/views.ts +6 -0
- package/src/ui/compose/ComposeDialog.tsx +17 -17
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +63 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/PostFooter.tsx +35 -12
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/post-article-attributes.ts +6 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
|
@@ -400,7 +400,7 @@ describe("PostService - Timeline features", () => {
|
|
|
400
400
|
});
|
|
401
401
|
});
|
|
402
402
|
|
|
403
|
-
describe("
|
|
403
|
+
describe("getThreadTailIds", () => {
|
|
404
404
|
it("keeps independent root threads separate", async () => {
|
|
405
405
|
const root1 = await postService.create({
|
|
406
406
|
format: "note",
|
|
@@ -412,10 +412,7 @@ describe("PostService - Timeline features", () => {
|
|
|
412
412
|
quoteText: "quoted",
|
|
413
413
|
});
|
|
414
414
|
|
|
415
|
-
const result = await postService.
|
|
416
|
-
root1.id,
|
|
417
|
-
root2.id,
|
|
418
|
-
]);
|
|
415
|
+
const result = await postService.getThreadTailIds([root1.id, root2.id]);
|
|
419
416
|
|
|
420
417
|
expect(result.get(root1.id)).toBe(root1.id);
|
|
421
418
|
expect(result.get(root2.id)).toBe(root2.id);
|
|
@@ -436,14 +433,57 @@ describe("PostService - Timeline features", () => {
|
|
|
436
433
|
replyToId: root1.id,
|
|
437
434
|
});
|
|
438
435
|
|
|
439
|
-
const result = await postService.
|
|
440
|
-
root1.id,
|
|
441
|
-
root2.id,
|
|
442
|
-
]);
|
|
436
|
+
const result = await postService.getThreadTailIds([root1.id, root2.id]);
|
|
443
437
|
|
|
444
438
|
expect(result.get(root1.id)).toBe(reply1.id);
|
|
445
439
|
expect(result.get(root2.id)).toBe(root2.id);
|
|
446
440
|
});
|
|
441
|
+
|
|
442
|
+
it("skips a trailing draft by default", async () => {
|
|
443
|
+
const root = await postService.create({
|
|
444
|
+
format: "note",
|
|
445
|
+
bodyMarkdown: "root",
|
|
446
|
+
});
|
|
447
|
+
const reply = await postService.create({
|
|
448
|
+
format: "note",
|
|
449
|
+
bodyMarkdown: "published reply",
|
|
450
|
+
replyToId: root.id,
|
|
451
|
+
});
|
|
452
|
+
await postService.create({
|
|
453
|
+
format: "note",
|
|
454
|
+
bodyMarkdown: "unfinished",
|
|
455
|
+
replyToId: reply.id,
|
|
456
|
+
status: "draft",
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const result = await postService.getThreadTailIds([root.id]);
|
|
460
|
+
|
|
461
|
+
expect(result.get(root.id)).toBe(reply.id);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("returns a trailing draft when drafts are included", async () => {
|
|
465
|
+
const root = await postService.create({
|
|
466
|
+
format: "note",
|
|
467
|
+
bodyMarkdown: "root",
|
|
468
|
+
});
|
|
469
|
+
const reply = await postService.create({
|
|
470
|
+
format: "note",
|
|
471
|
+
bodyMarkdown: "published reply",
|
|
472
|
+
replyToId: root.id,
|
|
473
|
+
});
|
|
474
|
+
const draft = await postService.create({
|
|
475
|
+
format: "note",
|
|
476
|
+
bodyMarkdown: "unfinished",
|
|
477
|
+
replyToId: reply.id,
|
|
478
|
+
status: "draft",
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const result = await postService.getThreadTailIds([root.id], {
|
|
482
|
+
includeDrafts: true,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
expect(result.get(root.id)).toBe(draft.id);
|
|
486
|
+
});
|
|
447
487
|
});
|
|
448
488
|
|
|
449
489
|
describe("timeline assembly", () => {
|
|
@@ -586,7 +626,98 @@ describe("PostService - Timeline features", () => {
|
|
|
586
626
|
expect(rootIds).toEqual([newerRoot.id, olderRoot.id]);
|
|
587
627
|
});
|
|
588
628
|
|
|
589
|
-
|
|
629
|
+
// Quietness used to live only in control flow: the reply skipped the
|
|
630
|
+
// recalculation and nothing recorded that it had. Any later recalculation
|
|
631
|
+
// recomputed MAX(published_at) over the whole Thread, swallowed the quiet
|
|
632
|
+
// reply, and bumped the Thread anyway. It is a stored column now, so the
|
|
633
|
+
// decision survives whatever else happens to the Thread.
|
|
634
|
+
it("keeps a quiet reply quiet after an unrelated post is deleted", async () => {
|
|
635
|
+
const collection = await collectionService.create({
|
|
636
|
+
slug: "durable",
|
|
637
|
+
title: "Durable",
|
|
638
|
+
});
|
|
639
|
+
const olderRoot = await postService.create({
|
|
640
|
+
format: "note",
|
|
641
|
+
bodyMarkdown: "Older root",
|
|
642
|
+
publishedAt: 1000,
|
|
643
|
+
});
|
|
644
|
+
const newerRoot = await postService.create({
|
|
645
|
+
format: "note",
|
|
646
|
+
bodyMarkdown: "Newer root",
|
|
647
|
+
publishedAt: 2000,
|
|
648
|
+
});
|
|
649
|
+
await collectionService.addThread(collection.id, olderRoot.id);
|
|
650
|
+
await collectionService.addThread(collection.id, newerRoot.id);
|
|
651
|
+
|
|
652
|
+
const normalReply = await postService.create({
|
|
653
|
+
format: "note",
|
|
654
|
+
bodyMarkdown: "Normal reply",
|
|
655
|
+
replyToId: olderRoot.id,
|
|
656
|
+
publishedAt: 1500,
|
|
657
|
+
});
|
|
658
|
+
await postService.create({
|
|
659
|
+
format: "note",
|
|
660
|
+
bodyMarkdown: "Quiet reply",
|
|
661
|
+
replyToId: normalReply.id,
|
|
662
|
+
publishedAt: 3000,
|
|
663
|
+
quietReply: true,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Triggers a full recalculation of the Thread.
|
|
667
|
+
await postService.delete(normalReply.id);
|
|
668
|
+
|
|
669
|
+
const root = await postService.getById(olderRoot.id);
|
|
670
|
+
expect(root?.lastActivityAt).toBe(1000);
|
|
671
|
+
expect(root?.threadUpdatedAt).toBe(3000);
|
|
672
|
+
|
|
673
|
+
const rootIds = await postService.listCollectionThreadRootIds(
|
|
674
|
+
collection.id,
|
|
675
|
+
{ status: "published", sortOrder: "newest" },
|
|
676
|
+
);
|
|
677
|
+
expect(rootIds).toEqual([newerRoot.id, olderRoot.id]);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("records both activity timestamps for a quiet reply", async () => {
|
|
681
|
+
const root = await postService.create({
|
|
682
|
+
format: "note",
|
|
683
|
+
bodyMarkdown: "Root",
|
|
684
|
+
publishedAt: 1000,
|
|
685
|
+
});
|
|
686
|
+
await postService.create({
|
|
687
|
+
format: "note",
|
|
688
|
+
bodyMarkdown: "Quiet reply",
|
|
689
|
+
replyToId: root.id,
|
|
690
|
+
publishedAt: 3000,
|
|
691
|
+
quietReply: true,
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const updated = await postService.getById(root.id);
|
|
695
|
+
expect(updated?.lastActivityAt).toBe(1000);
|
|
696
|
+
expect(updated?.threadUpdatedAt).toBe(3000);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("moves both timestamps for a normal reply", async () => {
|
|
700
|
+
const root = await postService.create({
|
|
701
|
+
format: "note",
|
|
702
|
+
bodyMarkdown: "Root",
|
|
703
|
+
publishedAt: 1000,
|
|
704
|
+
});
|
|
705
|
+
await postService.create({
|
|
706
|
+
format: "note",
|
|
707
|
+
bodyMarkdown: "Normal reply",
|
|
708
|
+
replyToId: root.id,
|
|
709
|
+
publishedAt: 3000,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
const updated = await postService.getById(root.id);
|
|
713
|
+
expect(updated?.lastActivityAt).toBe(3000);
|
|
714
|
+
expect(updated?.threadUpdatedAt).toBe(3000);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Activity means the Thread gained a post. Editing an old post is not
|
|
718
|
+
// activity — otherwise fixing a typo would drag it back to the top of its
|
|
719
|
+
// collection, and "recently updated" would stop meaning anything.
|
|
720
|
+
it("does not reorder a Thread when its root is only edited", async () => {
|
|
590
721
|
const collection = await collectionService.create({
|
|
591
722
|
slug: "edited",
|
|
592
723
|
title: "Edited",
|
|
@@ -617,7 +748,7 @@ describe("PostService - Timeline features", () => {
|
|
|
617
748
|
},
|
|
618
749
|
);
|
|
619
750
|
|
|
620
|
-
expect(rootIds).toEqual([
|
|
751
|
+
expect(rootIds).toEqual([newerRoot.id, editedRoot.id]);
|
|
621
752
|
});
|
|
622
753
|
});
|
|
623
754
|
|
|
@@ -1470,6 +1470,9 @@ describe("PostService", () => {
|
|
|
1470
1470
|
expect(published?.status).toBe("published");
|
|
1471
1471
|
expect(published?.publishedAt).toBeTypeOf("number");
|
|
1472
1472
|
expect((published?.publishedAt ?? 0) >= post.updatedAt).toBe(true);
|
|
1473
|
+
expect(
|
|
1474
|
+
(await postService.list({ status: "draft" })).map((draft) => draft.id),
|
|
1475
|
+
).not.toContain(post.id);
|
|
1473
1476
|
});
|
|
1474
1477
|
|
|
1475
1478
|
it("clears publishedAt when converting a published post back to draft", async () => {
|
|
@@ -1856,6 +1859,92 @@ describe("PostService", () => {
|
|
|
1856
1859
|
});
|
|
1857
1860
|
});
|
|
1858
1861
|
|
|
1862
|
+
describe("createThreadWithAttachments", () => {
|
|
1863
|
+
// No attachments in these cases, so only the up-front id check is reached.
|
|
1864
|
+
const deps = {
|
|
1865
|
+
media: { validateIds: async () => [] },
|
|
1866
|
+
} as unknown as Parameters<
|
|
1867
|
+
typeof postService.createThreadWithAttachments
|
|
1868
|
+
>[1];
|
|
1869
|
+
|
|
1870
|
+
it("dates every reply from the root when the root is backdated", async () => {
|
|
1871
|
+
const backdated = 1710000000; // 2024-03-09
|
|
1872
|
+
|
|
1873
|
+
const created = await postService.createThreadWithAttachments(
|
|
1874
|
+
[
|
|
1875
|
+
{
|
|
1876
|
+
data: {
|
|
1877
|
+
format: "note",
|
|
1878
|
+
bodyMarkdown: "root",
|
|
1879
|
+
publishedAt: backdated,
|
|
1880
|
+
},
|
|
1881
|
+
attachments: [],
|
|
1882
|
+
},
|
|
1883
|
+
{ data: { format: "note", bodyMarkdown: "second" }, attachments: [] },
|
|
1884
|
+
{ data: { format: "note", bodyMarkdown: "third" }, attachments: [] },
|
|
1885
|
+
],
|
|
1886
|
+
deps,
|
|
1887
|
+
undefined,
|
|
1888
|
+
);
|
|
1889
|
+
|
|
1890
|
+
// Without inheritance the replies would land on today and the thread
|
|
1891
|
+
// would read as if it spanned two years.
|
|
1892
|
+
expect(created.map((p) => p.publishedAt)).toEqual([
|
|
1893
|
+
backdated,
|
|
1894
|
+
backdated,
|
|
1895
|
+
backdated,
|
|
1896
|
+
]);
|
|
1897
|
+
// Order still comes from createdAt/id, so the chain is intact.
|
|
1898
|
+
expect(created[1].replyToId).toBe(created[0].id);
|
|
1899
|
+
expect(created[2].replyToId).toBe(created[1].id);
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
it("keeps a reply's own date when one is given", async () => {
|
|
1903
|
+
const rootAt = 1710000000;
|
|
1904
|
+
const replyAt = 1720000000;
|
|
1905
|
+
|
|
1906
|
+
const created = await postService.createThreadWithAttachments(
|
|
1907
|
+
[
|
|
1908
|
+
{
|
|
1909
|
+
data: { format: "note", bodyMarkdown: "root", publishedAt: rootAt },
|
|
1910
|
+
attachments: [],
|
|
1911
|
+
},
|
|
1912
|
+
{
|
|
1913
|
+
data: {
|
|
1914
|
+
format: "note",
|
|
1915
|
+
bodyMarkdown: "second",
|
|
1916
|
+
publishedAt: replyAt,
|
|
1917
|
+
},
|
|
1918
|
+
attachments: [],
|
|
1919
|
+
},
|
|
1920
|
+
],
|
|
1921
|
+
deps,
|
|
1922
|
+
undefined,
|
|
1923
|
+
);
|
|
1924
|
+
|
|
1925
|
+
expect(created.map((p) => p.publishedAt)).toEqual([rootAt, replyAt]);
|
|
1926
|
+
});
|
|
1927
|
+
|
|
1928
|
+
it("leaves draft threads undated", async () => {
|
|
1929
|
+
const created = await postService.createThreadWithAttachments(
|
|
1930
|
+
[
|
|
1931
|
+
{
|
|
1932
|
+
data: { format: "note", bodyMarkdown: "root", status: "draft" },
|
|
1933
|
+
attachments: [],
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
data: { format: "note", bodyMarkdown: "second", status: "draft" },
|
|
1937
|
+
attachments: [],
|
|
1938
|
+
},
|
|
1939
|
+
],
|
|
1940
|
+
deps,
|
|
1941
|
+
undefined,
|
|
1942
|
+
);
|
|
1943
|
+
|
|
1944
|
+
expect(created.map((p) => p.publishedAt)).toEqual([null, null]);
|
|
1945
|
+
});
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1859
1948
|
describe("threads", () => {
|
|
1860
1949
|
it("sets threadId on reply to a root post", async () => {
|
|
1861
1950
|
const root = await postService.create({
|
|
@@ -1915,6 +2004,143 @@ describe("PostService", () => {
|
|
|
1915
2004
|
);
|
|
1916
2005
|
});
|
|
1917
2006
|
|
|
2007
|
+
it("refuses to feature an unpublished post", async () => {
|
|
2008
|
+
await expect(
|
|
2009
|
+
postService.create({
|
|
2010
|
+
format: "note",
|
|
2011
|
+
bodyMarkdown: "not ready",
|
|
2012
|
+
status: "draft",
|
|
2013
|
+
featured: true,
|
|
2014
|
+
}),
|
|
2015
|
+
).rejects.toThrow("Publish this post before featuring it.");
|
|
2016
|
+
|
|
2017
|
+
const draft = await postService.create({
|
|
2018
|
+
format: "note",
|
|
2019
|
+
bodyMarkdown: "not ready",
|
|
2020
|
+
status: "draft",
|
|
2021
|
+
});
|
|
2022
|
+
await expect(
|
|
2023
|
+
postService.update(draft.id, { featured: true }),
|
|
2024
|
+
).rejects.toThrow("Publish this post before featuring it.");
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
it("keeps a featured flag through a trip back to draft", async () => {
|
|
2028
|
+
const post = await postService.create({
|
|
2029
|
+
format: "note",
|
|
2030
|
+
bodyMarkdown: "published",
|
|
2031
|
+
featured: true,
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
const unpublished = await postService.update(post.id, {
|
|
2035
|
+
status: "draft",
|
|
2036
|
+
});
|
|
2037
|
+
|
|
2038
|
+
expect(unpublished?.status).toBe("draft");
|
|
2039
|
+
expect(unpublished?.featuredAt).not.toBeNull();
|
|
2040
|
+
});
|
|
2041
|
+
|
|
2042
|
+
it("publishes a reply whose parent is still a draft", async () => {
|
|
2043
|
+
const root = await postService.create({
|
|
2044
|
+
format: "note",
|
|
2045
|
+
bodyMarkdown: "root",
|
|
2046
|
+
});
|
|
2047
|
+
const draft = await postService.create({
|
|
2048
|
+
format: "note",
|
|
2049
|
+
bodyMarkdown: "unfinished",
|
|
2050
|
+
replyToId: root.id,
|
|
2051
|
+
status: "draft",
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
const next = await postService.create({
|
|
2055
|
+
format: "note",
|
|
2056
|
+
bodyMarkdown: "continues past the draft",
|
|
2057
|
+
replyToId: draft.id,
|
|
2058
|
+
});
|
|
2059
|
+
|
|
2060
|
+
// Readers never saw the draft, so publishing past it reads as
|
|
2061
|
+
// continuous. The draft stays parked until it is dealt with on its own.
|
|
2062
|
+
expect(next.status).toBe("published");
|
|
2063
|
+
expect(draft.status).toBe("draft");
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
it("publishes only the post asked for, leaving other drafts alone", async () => {
|
|
2067
|
+
const root = await postService.create({
|
|
2068
|
+
format: "note",
|
|
2069
|
+
bodyMarkdown: "root",
|
|
2070
|
+
});
|
|
2071
|
+
const first = await postService.create({
|
|
2072
|
+
format: "note",
|
|
2073
|
+
bodyMarkdown: "first draft",
|
|
2074
|
+
replyToId: root.id,
|
|
2075
|
+
status: "draft",
|
|
2076
|
+
});
|
|
2077
|
+
const second = await postService.create({
|
|
2078
|
+
format: "note",
|
|
2079
|
+
bodyMarkdown: "second draft",
|
|
2080
|
+
replyToId: first.id,
|
|
2081
|
+
status: "draft",
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
await postService.update(second.id, { status: "published" });
|
|
2085
|
+
|
|
2086
|
+
const statusById = new Map(
|
|
2087
|
+
(await postService.getThread(root.id)).map((p) => [p.id, p.status]),
|
|
2088
|
+
);
|
|
2089
|
+
expect(statusById.get(first.id)).toBe("draft");
|
|
2090
|
+
expect(statusById.get(second.id)).toBe("published");
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
it("keeps a draft between two published replies", async () => {
|
|
2094
|
+
const root = await postService.create({
|
|
2095
|
+
format: "note",
|
|
2096
|
+
bodyMarkdown: "root",
|
|
2097
|
+
});
|
|
2098
|
+
const middle = await postService.create({
|
|
2099
|
+
format: "note",
|
|
2100
|
+
bodyMarkdown: "middle",
|
|
2101
|
+
replyToId: root.id,
|
|
2102
|
+
});
|
|
2103
|
+
const last = await postService.create({
|
|
2104
|
+
format: "note",
|
|
2105
|
+
bodyMarkdown: "last",
|
|
2106
|
+
replyToId: middle.id,
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
await postService.update(middle.id, { status: "draft" });
|
|
2110
|
+
|
|
2111
|
+
const statusById = new Map(
|
|
2112
|
+
(await postService.getThread(root.id)).map((p) => [p.id, p.status]),
|
|
2113
|
+
);
|
|
2114
|
+
// Unpublishing one post touches only that post. The public thread simply
|
|
2115
|
+
// skips it; nothing else in the chain changes.
|
|
2116
|
+
expect(statusById.get(root.id)).toBe("published");
|
|
2117
|
+
expect(statusById.get(middle.id)).toBe("draft");
|
|
2118
|
+
expect(statusById.get(last.id)).toBe("published");
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
it("names the draft when an unpublished reply holds the thread tail", async () => {
|
|
2122
|
+
const root = await postService.create({
|
|
2123
|
+
format: "note",
|
|
2124
|
+
bodyMarkdown: "root",
|
|
2125
|
+
});
|
|
2126
|
+
await postService.create({
|
|
2127
|
+
format: "note",
|
|
2128
|
+
bodyMarkdown: "unfinished",
|
|
2129
|
+
replyToId: root.id,
|
|
2130
|
+
status: "draft",
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
await expect(
|
|
2134
|
+
postService.create({
|
|
2135
|
+
format: "note",
|
|
2136
|
+
bodyMarkdown: "reply",
|
|
2137
|
+
replyToId: root.id,
|
|
2138
|
+
}),
|
|
2139
|
+
).rejects.toThrow(
|
|
2140
|
+
"This thread ends with an unpublished draft. Finish that draft or discard it, then reply.",
|
|
2141
|
+
);
|
|
2142
|
+
});
|
|
2143
|
+
|
|
1918
2144
|
it("inherits status from root post", async () => {
|
|
1919
2145
|
const root = await postService.create({
|
|
1920
2146
|
format: "note",
|
|
@@ -2025,6 +2251,57 @@ describe("PostService", () => {
|
|
|
2025
2251
|
expect(thread[0]?.bodyText).toBe("root");
|
|
2026
2252
|
});
|
|
2027
2253
|
|
|
2254
|
+
it("getThreadPosition counts the chain from the root down to a post", async () => {
|
|
2255
|
+
const root = await postService.create({
|
|
2256
|
+
format: "note",
|
|
2257
|
+
bodyMarkdown: "root",
|
|
2258
|
+
});
|
|
2259
|
+
const reply1 = await postService.create({
|
|
2260
|
+
format: "note",
|
|
2261
|
+
bodyMarkdown: "reply1",
|
|
2262
|
+
replyToId: root.id,
|
|
2263
|
+
});
|
|
2264
|
+
const reply2 = await postService.create({
|
|
2265
|
+
format: "note",
|
|
2266
|
+
bodyMarkdown: "reply2",
|
|
2267
|
+
replyToId: reply1.id,
|
|
2268
|
+
});
|
|
2269
|
+
|
|
2270
|
+
expect(await postService.getThreadPosition(root.id)).toBe(1);
|
|
2271
|
+
expect(await postService.getThreadPosition(reply1.id)).toBe(2);
|
|
2272
|
+
expect(await postService.getThreadPosition(reply2.id)).toBe(3);
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
it("getThreadPosition walks the reply chain rather than counting members", async () => {
|
|
2276
|
+
// `create` only allows replying to the tail, so today a thread is a
|
|
2277
|
+
// straight chain and depth happens to equal its size. The walk is what
|
|
2278
|
+
// makes the two agree — counting `threadId` members would give the same
|
|
2279
|
+
// answer here and the wrong one the moment a draft chain is incomplete.
|
|
2280
|
+
const root = await postService.create({
|
|
2281
|
+
format: "note",
|
|
2282
|
+
bodyMarkdown: "root",
|
|
2283
|
+
});
|
|
2284
|
+
const tail = await postService.create({
|
|
2285
|
+
format: "note",
|
|
2286
|
+
bodyMarkdown: "tail",
|
|
2287
|
+
replyToId: root.id,
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
// A second post filed under the same thread without joining the chain:
|
|
2291
|
+
// it is not above the tail, so it must not push the tail's position.
|
|
2292
|
+
await postService.create({
|
|
2293
|
+
format: "note",
|
|
2294
|
+
bodyMarkdown: "unrelated",
|
|
2295
|
+
});
|
|
2296
|
+
|
|
2297
|
+
expect(await postService.getThreadPosition(tail.id)).toBe(2);
|
|
2298
|
+
expect(await postService.getThread(root.id)).toHaveLength(2);
|
|
2299
|
+
});
|
|
2300
|
+
|
|
2301
|
+
it("getThreadPosition returns 0 for a post that does not exist", async () => {
|
|
2302
|
+
expect(await postService.getThreadPosition("pst_missing")).toBe(0);
|
|
2303
|
+
});
|
|
2304
|
+
|
|
2028
2305
|
it("getThread excludes deleted posts", async () => {
|
|
2029
2306
|
const root = await postService.create({
|
|
2030
2307
|
format: "note",
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
supportsDrizzleTransaction,
|
|
16
16
|
} from "../db/index.js";
|
|
17
17
|
import type { DatabaseDialect } from "../db/dialect.js";
|
|
18
|
+
import { buildRootActivityExpr } from "../db/thread-activity.js";
|
|
18
19
|
import {
|
|
19
20
|
sqliteSchemaBundle,
|
|
20
21
|
type DatabaseSchema,
|
|
@@ -448,16 +449,13 @@ export function createCollectionService(
|
|
|
448
449
|
async function listDirectoryCollections(): Promise<
|
|
449
450
|
CollectionDirectoryCollection[]
|
|
450
451
|
> {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
${posts.updatedAt}
|
|
459
|
-
)
|
|
460
|
-
END`;
|
|
452
|
+
// `posts` is joined on thread_collections.thread_id, so each row is
|
|
453
|
+
// already the Thread root — the definition applies to it directly.
|
|
454
|
+
const threadActivityAt = buildRootActivityExpr({
|
|
455
|
+
lastActivityAt: posts.lastActivityAt,
|
|
456
|
+
publishedAt: posts.publishedAt,
|
|
457
|
+
updatedAt: posts.updatedAt,
|
|
458
|
+
});
|
|
461
459
|
const threadCount = sql<number>`
|
|
462
460
|
CAST(COUNT(
|
|
463
461
|
CASE
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
@keyframes lightbox-fade-in{0%{opacity:0}to{opacity:1}}@keyframes lightbox-scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.media-lightbox{background:0 0;border:none;outline:none;width:100%;max-width:100%;height:100%;max-height:100%;padding:0}.media-lightbox[open]{animation:.2s both lightbox-fade-in}.media-lightbox::backdrop{background-color:#000}.media-lightbox-content{outline:none;width:100%;height:100dvh;position:relative}.media-lightbox-stage{box-sizing:border-box;justify-content:center;align-items:center;width:100%;height:100%;padding:64px 96px;display:flex;overflow:hidden}.media-lightbox-stage-scroll{overscroll-behavior:contain;scrollbar-gutter:stable both-edges;-webkit-overflow-scrolling:touch;align-items:flex-start;overflow:hidden auto}.media-lightbox-img{object-fit:contain;border-radius:4px;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in;display:block}.media-lightbox-img-zoomable{cursor:zoom-in}.media-lightbox-img-scroll{width:min(100%,44rem);max-width:none;height:auto;max-height:none;margin:0 auto}.media-lightbox-img-zoomable.media-lightbox-img-scroll{cursor:zoom-out}.media-lightbox-close{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:40px;height:40px;transition:background-color .15s;display:flex;position:fixed;top:16px;left:16px}.media-lightbox-close:hover{background-color:#000000b3}.media-lightbox-nav{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:44px;height:44px;transition:background-color .15s;display:flex;position:fixed;top:50%;transform:translateY(-50%)}.media-lightbox-nav:hover{background-color:#000000b3}.media-lightbox-nav-prev{left:16px}.media-lightbox-nav-next{right:16px}.media-lightbox-counter{z-index:10;font-size:var(--type-xs);color:#ffffffb3;font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none;position:fixed;top:20px;left:50%;transform:translate(-50%)}.media-lightbox-short-frame{background-color:#000;border-radius:4px;max-width:100%;max-height:100%;display:block;position:relative;overflow:hidden}.media-lightbox-short-frame-contained{width:var(--media-lightbox-short-width);height:var(--media-lightbox-short-height)}.media-lightbox-short-viewport{width:100%;height:100%;min-height:0}.media-lightbox-video-short{object-fit:contain;cursor:pointer;width:100%;max-width:none;height:100%;max-height:none;display:block}.media-lightbox-short-controls{z-index:2;box-sizing:border-box;pointer-events:none;background:linear-gradient(#0000,#000000ad);align-items:center;gap:2px;height:44px;padding:0 6px;transition:opacity .18s,transform .18s;display:flex;position:absolute;bottom:0;left:0;right:0}.media-lightbox-short-progress{--media-progress:0%;z-index:1;appearance:none;pointer-events:auto;cursor:pointer;background:0 0;flex:auto;width:0;min-width:48px;height:44px;margin:0;padding:0;display:block;position:relative}.media-lightbox-short-progress::-webkit-slider-runnable-track{background:linear-gradient(to right, #ffffffeb 0, #ffffffeb var(--media-progress), #ffffff2e var(--media-progress), #ffffff2e 100%);border-radius:999px;height:2px;transition:height .15s,background .15s}.media-lightbox-short-progress::-webkit-slider-thumb{appearance:none;opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;margin-top:-4px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-progress::-moz-range-track{background:#ffffff2e;border:none;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-progress{background:#ffffffeb;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-thumb{opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-playback{z-index:2;color:#fff;pointer-events:auto;cursor:pointer;background-color:#0000;border:none;border-radius:999px;flex:0 0 44px;justify-content:center;align-items:center;width:44px;height:44px;padding:0;transition:background-color .15s,transform .15s;display:inline-flex;position:relative}.media-lightbox-short-playback svg{width:16px;height:16px;display:block}.media-lightbox-short-playback:hover{background-color:#ffffff24}.media-lightbox-short-playback:active{transform:scale(.92)}.media-lightbox-short-playback:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}.media-lightbox-short-progress:hover::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:focus-visible::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-track{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-progress{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-progress{height:4px}.media-lightbox-short-progress:hover::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:hover::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-mute{z-index:2;color:#fff;pointer-events:auto;cursor:pointer;background-color:#0000;border:none;border-radius:999px;flex:0 0 44px;justify-content:center;align-items:center;width:44px;height:44px;padding:0;transition:background-color .15s,transform .15s;display:inline-flex;position:relative}.media-lightbox-short-mute svg{flex-shrink:0;width:16px;height:16px;display:block}.media-lightbox-short-mute:hover{background-color:#ffffff24}.media-lightbox-short-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}@media (max-width:640px){.media-lightbox-stage{padding:48px 16px}.media-lightbox-img{border-radius:0}.media-lightbox-img-scroll{width:100%}.media-lightbox-close{width:36px;height:36px;top:12px;left:12px}.media-lightbox-nav{width:36px;height:36px}.media-lightbox-nav-prev{left:8px}.media-lightbox-nav-next{right:8px}.media-lightbox-short-frame{border-radius:0}}@media (hover:none),(pointer:coarse){.media-lightbox-short-frame-contained{height:calc(var(--media-lightbox-short-height) + 44px);grid-template-rows:minmax(0,1fr) 44px;display:grid}.media-lightbox-short-controls{background:#000;grid-row:2;position:relative}}@media (hover:hover) and (pointer:fine){.media-lightbox-short-controls{opacity:0;transform:translateY(4px)}.media-lightbox-short-controls>*{pointer-events:none}.media-lightbox-short-frame:hover .media-lightbox-short-controls,.media-lightbox-short-frame:focus-within .media-lightbox-short-controls{opacity:1;transform:translateY(0)}.media-lightbox-short-frame:hover .media-lightbox-short-controls>*,.media-lightbox-short-frame:focus-within .media-lightbox-short-controls>*{pointer-events:auto}}.media-visual-frame{border-radius:var(--media-radius,.5rem);background-color:var(--color-muted);display:block;overflow:hidden}.media-visual{background-position:50%;background-repeat:no-repeat;display:block}.media-video-wrap{position:relative}.media-video-link{cursor:pointer;display:block}.media-video-wrap video{object-fit:contain;background-color:var(--color-muted);width:100%;max-height:24rem}.media-video-wrap-short video{background-color:#000}.media-feed-video-mute{z-index:1;color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:999px;justify-content:center;align-items:center;width:28px;height:28px;transition:background-color .15s,transform .15s;display:inline-flex;position:absolute;bottom:16px;right:16px}.media-feed-video-mute svg{width:12px;height:12px}.media-feed-video-mute:hover{background-color:#0000009e;transform:scale(1.03)}.media-feed-video-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}.media-feed-video-icon{transition:opacity .15s,transform .15s;position:absolute}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-muted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-unmuted{opacity:1;transform:scale(1)}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-unmuted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-muted{opacity:0;transform:scale(.92)}.media-video-play-overlay{pointer-events:none;justify-content:center;align-items:center;transition:opacity .15s;display:flex;position:absolute;inset:0}.media-video-play-overlay svg{filter:drop-shadow(0 2px 6px #0006);opacity:.85;width:48px;height:48px}.media-gallery-card.media-audio-card{flex-direction:column;display:flex}.media-audio-card .media-audio-el{opacity:0;pointer-events:none;width:0;height:0;position:absolute}.media-audio-card .media-audio-artwork{background:linear-gradient(160deg,#8080801f 0%,#80808008 100%);flex:1;justify-content:center;align-items:center;width:100%;min-height:0;display:flex}.media-audio-card .media-audio-artwork svg{width:32px;height:32px;color:var(--site-text-secondary);opacity:.3}.media-audio-card .media-audio-waveform{cursor:pointer;touch-action:none;width:100%;height:24px;display:none}.media-audio-card.has-waveform .media-audio-waveform{display:block}.media-audio-card.has-waveform .media-audio-range{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.media-audio-card .media-audio-controls{flex-direction:column;flex-shrink:0;padding:0 0 6px;display:flex}.media-audio-card .media-audio-range{appearance:none;cursor:pointer;touch-action:none;background:0 0;width:100%;height:20px;margin:0;padding:0}.media-audio-card .media-audio-range::-webkit-slider-runnable-track{background:#80808026;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-track{background:#80808026;border:none;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-progress{background:var(--site-text-primary);border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-webkit-slider-thumb{-webkit-appearance:none;background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;margin-top:-3.5px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-webkit-slider-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-webkit-slider-thumb{opacity:1}.media-audio-card .media-audio-range::-moz-range-thumb{background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-moz-range-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-moz-range-thumb{opacity:1}.media-audio-card .media-audio-range:focus-visible{outline:2px solid var(--site-text-primary);outline-offset:2px;border-radius:2px}.media-audio-card .media-audio-row{align-items:center;gap:6px;min-width:0;padding:6px 8px 0;display:flex}.media-audio-card .media-audio-info{flex-direction:column;flex:1;gap:1px;min-width:0;display:flex}.media-audio-card .media-audio-title{font-size:var(--type-2xs);font-weight:var(--fw-medium,500);color:var(--site-text-primary);text-overflow:ellipsis;white-space:nowrap;line-height:1.3;overflow:hidden}.media-audio-card .media-audio-time{font-size:var(--type-2xs);color:var(--site-text-secondary);font-variant-numeric:tabular-nums;line-height:1}.media-audio-card .media-audio-play-btn{cursor:pointer;background:var(--site-text-primary);width:28px;height:28px;color:var(--background,#fff);border:none;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;transition:transform .12s;display:flex}.media-audio-card .media-audio-play-btn:hover{transform:scale(1.1)}.media-audio-card .media-audio-play-btn:active{transform:scale(.92)}.media-audio-card .media-audio-play-btn svg{width:14px;height:14px}.media-audio-card .media-audio-icon-play{margin-left:2px}.media-audio-card .media-audio-icon-pause,.media-audio-card.is-playing .media-audio-icon-play{display:none}.media-audio-card.is-playing .media-audio-icon-pause{display:block}.media-gallery-card{color:var(--site-text-primary);border-radius:var(--media-radius,.5rem);background-color:var(--site-nav-hover-bg);border:1px solid var(--site-divider);text-decoration:none;transition:background-color .15s;display:block;overflow:hidden}a.media-gallery-card:hover,button.media-gallery-card:hover{background-color:var(--site-divider)}button.media-gallery-card{cursor:pointer;font:inherit;text-align:inherit}.media-gallery-card-inner{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:8px;width:100%;height:100%;padding:16px 12px;display:flex}.media-gallery-card-icon{color:var(--site-text-secondary);opacity:.6}.media-gallery-card-summary{font-size:var(--type-xs);color:var(--site-text-secondary);-webkit-line-clamp:2;word-break:break-word;-webkit-box-orient:vertical;line-height:1.4;display:-webkit-box;overflow:hidden}.media-gallery-card-meta{font-size:var(--type-xs);color:var(--site-text-secondary)}.media-lightbox-video{background-color:#000;border-radius:4px;outline:none;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in}.media-lightbox-video:focus,.media-lightbox-video:focus-visible{outline:none}.media-lightbox-video-short{object-fit:contain;width:100%;max-width:none;height:100%;max-height:none;display:block}@media (max-width:640px){.media-lightbox-video{border-radius:0}}[data-post-media] img{background:0 0}.media-gallery-scroll-wrap{position:relative}.media-gallery-nav{z-index:2;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);color:#fff;cursor:pointer;opacity:0;pointer-events:none;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:34px;height:34px;padding:0;transition:opacity .15s,background-color .15s;display:flex;position:absolute;top:50%;transform:translateY(-50%)}.media-gallery-nav:hover{background-color:#000000b3}.media-gallery-nav svg{width:18px;height:18px;display:block}.media-gallery-nav-prev{left:8px}.media-gallery-nav-next{right:8px}@media (hover:hover) and (pointer:fine){.media-gallery-scroll-wrap.can-scroll-start:hover .media-gallery-nav-prev,.media-gallery-scroll-wrap.can-scroll-end:hover .media-gallery-nav-next{opacity:1;pointer-events:auto}}.media-gallery-scroll-wrap>[data-post-media]:focus-visible{outline:2px solid var(--site-text-primary);outline-offset:2px;border-radius:4px}
|
|
1
|
+
@keyframes lightbox-fade-in{0%{opacity:0}to{opacity:1}}@keyframes lightbox-scale-in{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.media-lightbox{background:0 0;border:none;outline:none;width:100%;max-width:100%;height:100%;max-height:100%;padding:0}.media-lightbox[open]{animation:.2s both lightbox-fade-in}.media-lightbox::backdrop{background-color:#000}.media-lightbox-content{outline:none;width:100%;height:100dvh;position:relative}.media-lightbox-stage{box-sizing:border-box;justify-content:center;align-items:center;width:100%;height:100%;padding:64px 96px;display:flex;overflow:hidden}.media-lightbox-stage-scroll{overscroll-behavior:contain;scrollbar-gutter:stable both-edges;-webkit-overflow-scrolling:touch;align-items:flex-start;overflow:hidden auto}.media-lightbox-img{object-fit:contain;border-radius:4px;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in;display:block}.media-lightbox-img-zoomable{cursor:zoom-in}.media-lightbox-img-scroll{width:min(100%,44rem);max-width:none;height:auto;max-height:none;margin:0 auto}.media-lightbox-img-zoomable.media-lightbox-img-scroll{cursor:zoom-out}.media-lightbox-close{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:40px;height:40px;transition:background-color .15s;display:flex;position:fixed;top:16px;left:16px}.media-lightbox-close:hover{background-color:#000000b3}.media-lightbox-nav{z-index:10;-webkit-backdrop-filter:blur(8px);color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:44px;height:44px;transition:background-color .15s;display:flex;position:fixed;top:50%;transform:translateY(-50%)}.media-lightbox-nav:hover{background-color:#000000b3}.media-lightbox-nav-prev{left:16px}.media-lightbox-nav-next{right:16px}.media-lightbox-counter{z-index:10;font-size:var(--type-xs);color:#ffffffb3;font-variant-numeric:tabular-nums;-webkit-user-select:none;user-select:none;position:fixed;top:20px;left:50%;transform:translate(-50%)}.media-lightbox-short-frame{background-color:#000;border-radius:4px;max-width:100%;max-height:100%;display:block;position:relative;overflow:hidden}.media-lightbox-short-frame-contained{width:var(--media-lightbox-short-width);height:var(--media-lightbox-short-height)}.media-lightbox-short-viewport{width:100%;height:100%;min-height:0}.media-lightbox-video-short{object-fit:contain;cursor:pointer;width:100%;max-width:none;height:100%;max-height:none;display:block}.media-lightbox-short-controls{z-index:2;box-sizing:border-box;pointer-events:none;background:linear-gradient(#0000,#000000ad);align-items:center;gap:2px;height:50px;padding:0 6px 6px;transition:opacity .18s,transform .18s;display:flex;position:absolute;bottom:0;left:0;right:0}.media-lightbox-short-progress{--media-progress:0%;z-index:1;appearance:none;pointer-events:auto;cursor:pointer;background:0 0;flex:auto;width:0;min-width:48px;height:44px;margin:0;padding:0;display:block;position:relative}.media-lightbox-short-progress::-webkit-slider-runnable-track{background:linear-gradient(to right, #ffffffeb 0, #ffffffeb var(--media-progress), #ffffff2e var(--media-progress), #ffffff2e 100%);border-radius:999px;height:2px;transition:height .15s,background .15s}.media-lightbox-short-progress::-webkit-slider-thumb{appearance:none;opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;margin-top:-4px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-progress::-moz-range-track{background:#ffffff2e;border:none;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-progress{background:#ffffffeb;border-radius:999px;height:2px}.media-lightbox-short-progress::-moz-range-thumb{opacity:.82;background-color:#fff;border:none;border-radius:999px;width:10px;height:10px;transition:opacity .15s,transform .15s;box-shadow:0 1px 4px #00000052}.media-lightbox-short-playback{z-index:2;color:#fff;pointer-events:auto;cursor:pointer;background-color:#0000;border:none;border-radius:999px;flex:0 0 44px;justify-content:center;align-items:center;width:44px;height:44px;padding:0;transition:background-color .15s,transform .15s;display:inline-flex;position:relative}.media-lightbox-short-playback svg{width:16px;height:16px;display:block}.media-lightbox-short-playback:hover{background-color:#ffffff24}.media-lightbox-short-playback:active{transform:scale(.92)}.media-lightbox-short-playback:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}.media-lightbox-short-progress:hover::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:focus-visible::-webkit-slider-runnable-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-track{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-track{height:4px}.media-lightbox-short-progress:hover::-moz-range-progress{height:4px}.media-lightbox-short-progress:focus-visible::-moz-range-progress{height:4px}.media-lightbox-short-progress:hover::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-webkit-slider-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:hover::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-progress:focus-visible::-moz-range-thumb{opacity:1;transform:scale(1.05)}.media-lightbox-short-mute{z-index:2;color:#fff;pointer-events:auto;cursor:pointer;background-color:#0000;border:none;border-radius:999px;flex:0 0 44px;justify-content:center;align-items:center;width:44px;height:44px;padding:0;transition:background-color .15s,transform .15s;display:inline-flex;position:relative}.media-lightbox-short-mute svg{flex-shrink:0;width:16px;height:16px;display:block}.media-lightbox-short-mute:hover{background-color:#ffffff24}.media-lightbox-short-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}@media (max-width:640px){.media-lightbox-stage{padding:48px 16px}.media-lightbox-img{border-radius:0}.media-lightbox-img-scroll{width:100%}.media-lightbox-close{width:36px;height:36px;top:12px;left:12px}.media-lightbox-nav{width:36px;height:36px}.media-lightbox-nav-prev{left:8px}.media-lightbox-nav-next{right:8px}.media-lightbox-short-frame{border-radius:0}}@media (hover:none),(pointer:coarse){.media-lightbox-short-frame-contained{height:calc(var(--media-lightbox-short-height) + 50px);grid-template-rows:minmax(0,1fr) 50px;display:grid}.media-lightbox-short-controls{background:#000;grid-row:2;position:relative}}@media (hover:hover) and (pointer:fine){.media-lightbox-short-controls{opacity:0;transform:translateY(4px)}.media-lightbox-short-controls>*{pointer-events:none}.media-lightbox-short-frame:hover .media-lightbox-short-controls,.media-lightbox-short-frame:focus-within .media-lightbox-short-controls{opacity:1;transform:translateY(0)}.media-lightbox-short-frame:hover .media-lightbox-short-controls>*,.media-lightbox-short-frame:focus-within .media-lightbox-short-controls>*{pointer-events:auto}}.media-visual-frame{border-radius:var(--media-radius,.5rem);background-color:var(--color-muted);display:block;overflow:hidden}.media-visual{background-position:50%;background-repeat:no-repeat;display:block}.media-video-wrap{position:relative}.media-video-link{cursor:pointer;display:block}.media-video-wrap video{object-fit:contain;background-color:var(--color-muted);width:100%;max-height:24rem}.media-video-wrap-short video{background-color:#000}.media-feed-video-mute{z-index:1;color:#fff;cursor:pointer;background-color:#00000080;border:none;border-radius:999px;justify-content:center;align-items:center;width:28px;height:28px;transition:background-color .15s,transform .15s;display:inline-flex;position:absolute;bottom:16px;right:16px}.media-feed-video-mute svg{width:12px;height:12px}.media-feed-video-mute:hover{background-color:#0000009e;transform:scale(1.03)}.media-feed-video-mute:focus-visible{outline:none;box-shadow:0 0 0 3px #fff3}.media-feed-video-icon{transition:opacity .15s,transform .15s;position:absolute}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-muted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-unmuted{opacity:1;transform:scale(1)}.media-feed-video-mute[data-muted=true] .media-feed-video-icon-unmuted,.media-feed-video-mute[data-muted=false] .media-feed-video-icon-muted{opacity:0;transform:scale(.92)}.media-video-play-overlay{pointer-events:none;justify-content:center;align-items:center;transition:opacity .15s;display:flex;position:absolute;inset:0}.media-video-play-overlay svg{filter:drop-shadow(0 2px 6px #0006);opacity:.85;width:48px;height:48px}.media-gallery-card.media-audio-card{flex-direction:column;display:flex}.media-audio-card .media-audio-el{opacity:0;pointer-events:none;width:0;height:0;position:absolute}.media-audio-card .media-audio-artwork{background:linear-gradient(160deg,#8080801f 0%,#80808008 100%);flex:1;justify-content:center;align-items:center;width:100%;min-height:0;display:flex}.media-audio-card .media-audio-artwork svg{width:32px;height:32px;color:var(--site-text-secondary);opacity:.3}.media-audio-card .media-audio-waveform{cursor:pointer;touch-action:none;width:100%;height:24px;display:none}.media-audio-card.has-waveform .media-audio-waveform{display:block}.media-audio-card.has-waveform .media-audio-range{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.media-audio-card .media-audio-controls{flex-direction:column;flex-shrink:0;padding:0 0 6px;display:flex}.media-audio-card .media-audio-range{appearance:none;cursor:pointer;touch-action:none;background:0 0;width:100%;height:20px;margin:0;padding:0}.media-audio-card .media-audio-range::-webkit-slider-runnable-track{background:#80808026;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-track{background:#80808026;border:none;border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-moz-range-progress{background:var(--site-text-primary);border-radius:1.5px;height:3px}.media-audio-card .media-audio-range::-webkit-slider-thumb{-webkit-appearance:none;background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;margin-top:-3.5px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-webkit-slider-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-webkit-slider-thumb{opacity:1}.media-audio-card .media-audio-range::-moz-range-thumb{background:var(--site-text-primary);opacity:0;border:none;border-radius:50%;width:10px;height:10px;transition:opacity .15s}.media-audio-card .media-audio-range:hover::-moz-range-thumb{opacity:1}.media-audio-card.is-playing .media-audio-range::-moz-range-thumb{opacity:1}.media-audio-card .media-audio-range:focus-visible{outline:2px solid var(--site-text-primary);outline-offset:2px;border-radius:2px}.media-audio-card .media-audio-row{align-items:center;gap:6px;min-width:0;padding:6px 8px 0;display:flex}.media-audio-card .media-audio-info{flex-direction:column;flex:1;gap:1px;min-width:0;display:flex}.media-audio-card .media-audio-title{font-size:var(--type-2xs);font-weight:var(--fw-medium,500);color:var(--site-text-primary);text-overflow:ellipsis;white-space:nowrap;line-height:1.3;overflow:hidden}.media-audio-card .media-audio-time{font-size:var(--type-2xs);color:var(--site-text-secondary);font-variant-numeric:tabular-nums;line-height:1}.media-audio-card .media-audio-play-btn{cursor:pointer;background:var(--site-text-primary);width:28px;height:28px;color:var(--background,#fff);border:none;border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;transition:transform .12s;display:flex}.media-audio-card .media-audio-play-btn:hover{transform:scale(1.1)}.media-audio-card .media-audio-play-btn:active{transform:scale(.92)}.media-audio-card .media-audio-play-btn svg{width:14px;height:14px}.media-audio-card .media-audio-icon-play{margin-left:2px}.media-audio-card .media-audio-icon-pause,.media-audio-card.is-playing .media-audio-icon-play{display:none}.media-audio-card.is-playing .media-audio-icon-pause{display:block}.media-gallery-card{color:var(--site-text-primary);border-radius:var(--media-radius,.5rem);background-color:var(--site-nav-hover-bg);border:1px solid var(--site-divider);text-decoration:none;transition:background-color .15s;display:block;overflow:hidden}a.media-gallery-card:hover,button.media-gallery-card:hover{background-color:var(--site-divider)}button.media-gallery-card{cursor:pointer;font:inherit;text-align:inherit}.media-gallery-card-inner{text-align:center;flex-direction:column;justify-content:center;align-items:center;gap:8px;width:100%;height:100%;padding:16px 12px;display:flex}.media-gallery-card-icon{color:var(--site-text-secondary);opacity:.6}.media-gallery-card-summary{font-size:var(--type-xs);color:var(--site-text-secondary);-webkit-line-clamp:2;word-break:break-word;-webkit-box-orient:vertical;line-height:1.4;display:-webkit-box;overflow:hidden}.media-gallery-card-meta{font-size:var(--type-xs);color:var(--site-text-secondary)}.media-lightbox-video{background-color:#000;border-radius:4px;outline:none;max-width:100%;max-height:100%;animation:.28s cubic-bezier(.22,1,.36,1) both lightbox-scale-in}.media-lightbox-video:focus,.media-lightbox-video:focus-visible{outline:none}.media-lightbox-video-short{object-fit:contain;width:100%;max-width:none;height:100%;max-height:none;display:block}@media (max-width:640px){.media-lightbox-video{border-radius:0}}[data-post-media] img{background:0 0}.media-gallery-scroll-wrap{position:relative}.media-gallery-nav{z-index:2;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);color:#fff;cursor:pointer;opacity:0;pointer-events:none;background-color:#00000080;border:none;border-radius:50%;justify-content:center;align-items:center;width:34px;height:34px;padding:0;transition:opacity .15s,background-color .15s;display:flex;position:absolute;top:50%;transform:translateY(-50%)}.media-gallery-nav:hover{background-color:#000000b3}.media-gallery-nav svg{width:18px;height:18px;display:block}.media-gallery-nav-prev{left:8px}.media-gallery-nav-next{right:8px}@media (hover:hover) and (pointer:fine){.media-gallery-scroll-wrap.can-scroll-start:hover .media-gallery-nav-prev,.media-gallery-scroll-wrap.can-scroll-end:hover .media-gallery-nav-next{opacity:1;pointer-events:auto}}.media-gallery-scroll-wrap>[data-post-media]:focus-visible{outline:2px solid var(--site-text-primary);outline-offset:2px;border-radius:4px}
|
|
2
2
|
/*$vite$:1*/
|
package/src/services/export.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
getDefaultJantAppleTouchIconBytes,
|
|
28
28
|
getDefaultJantFaviconIcoBytes,
|
|
29
29
|
} from "../lib/jant-branding.js";
|
|
30
|
+
import { getRootActivityAt } from "../db/thread-activity.js";
|
|
30
31
|
import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
|
|
31
32
|
import { extractBodyText } from "../lib/summary.js";
|
|
32
33
|
import { getMediaUrl, getPublicUrlForProvider } from "../lib/image.js";
|
|
@@ -974,6 +975,7 @@ async function buildThreadBundle(
|
|
|
974
975
|
source_url: reply.format === "quote" && reply.url ? reply.url : undefined,
|
|
975
976
|
quote_text: reply.quoteText ?? undefined,
|
|
976
977
|
rating: reply.rating ?? undefined,
|
|
978
|
+
quiet_reply: reply.quietReply ? true : undefined,
|
|
977
979
|
featured_at:
|
|
978
980
|
reply.featuredAt !== null ? toISOString(reply.featuredAt) : undefined,
|
|
979
981
|
pinned_at:
|
|
@@ -1326,11 +1328,7 @@ function buildExportedCollectionMetrics(
|
|
|
1326
1328
|
continue;
|
|
1327
1329
|
}
|
|
1328
1330
|
|
|
1329
|
-
const activityAt =
|
|
1330
|
-
post.lastActivityAt ??
|
|
1331
|
-
post.publishedAt ??
|
|
1332
|
-
post.updatedAt ??
|
|
1333
|
-
post.createdAt;
|
|
1331
|
+
const activityAt = getRootActivityAt(post);
|
|
1334
1332
|
const threadCollections = collectionsByThread.get(post.id) ?? [];
|
|
1335
1333
|
|
|
1336
1334
|
for (const collection of threadCollections) {
|
package/src/services/path.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { createEntityId } from "../lib/ids.js";
|
|
|
16
16
|
import { now } from "../lib/time.js";
|
|
17
17
|
import { ConflictError } from "../lib/errors.js";
|
|
18
18
|
import { normalizePath } from "../lib/url.js";
|
|
19
|
-
import type { PathKind, PathRecord } from "../types.js";
|
|
19
|
+
import type { PathKind, PathRecord, Status } from "../types.js";
|
|
20
20
|
|
|
21
21
|
export interface ResolvedPath extends PathRecord {
|
|
22
22
|
targetType: "post" | "collection" | "redirect" | "archive";
|
|
@@ -55,6 +55,7 @@ export interface NavigableItem {
|
|
|
55
55
|
path: string;
|
|
56
56
|
type: "post" | "collection";
|
|
57
57
|
format?: string;
|
|
58
|
+
status?: Status;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
export function toCollectionPath(slug: string): string {
|
|
@@ -375,6 +376,7 @@ export function createPathService(
|
|
|
375
376
|
.select({
|
|
376
377
|
title: posts.title,
|
|
377
378
|
format: posts.format,
|
|
379
|
+
status: posts.status,
|
|
378
380
|
path: pathRegistry.path,
|
|
379
381
|
})
|
|
380
382
|
.from(pathRegistry)
|
|
@@ -422,6 +424,7 @@ export function createPathService(
|
|
|
422
424
|
path: row.path,
|
|
423
425
|
type: "post",
|
|
424
426
|
format: row.format,
|
|
427
|
+
status: row.status,
|
|
425
428
|
});
|
|
426
429
|
}
|
|
427
430
|
|