@jant/core 0.6.7 → 0.6.9

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 (131) hide show
  1. package/bin/commands/uploads/cleanup.js +2 -0
  2. package/dist/{app-L1UPUArB.js → app-C-jxWmAV.js} +12421 -12033
  3. package/dist/app-DqHzOwL5.js +6 -0
  4. package/dist/client/.vite/manifest.json +3 -3
  5. package/dist/client/_assets/client-CGf2m3qp.css +2 -0
  6. package/dist/client/_assets/{client-B0MvB2r0.js → client-DWy1LEEk.js} +2 -2
  7. package/dist/client/_assets/{client-auth-CwwuucF_.js → client-auth-Blg-a5Ep.js} +365 -345
  8. package/dist/{env-CoSe-1y4.js → env-OHRKGcMj.js} +1 -1
  9. package/dist/{export-DLukCOO3.js → export-C2DIB7mm.js} +34 -9
  10. package/dist/{github-api-UD4u_7fa.js → github-api-BgSiE71w.js} +1 -1
  11. package/dist/{github-app-DeX6Td1O.js → github-app-BbklkFmU.js} +1 -1
  12. package/dist/{github-sync-BeDecPen.js → github-sync-7XQ5ZM6z.js} +3 -3
  13. package/dist/{github-sync-BtHY2AST.js → github-sync-BEFCfLKK.js} +3 -3
  14. package/dist/index.js +5 -5
  15. package/dist/node.js +6 -6
  16. package/dist/{url-XF0GbKGO.js → url-BMYO-Zlt.js} +42 -2
  17. package/package.json +1 -1
  18. package/src/__tests__/bin/uploads-cleanup.test.ts +2 -0
  19. package/src/client/__tests__/compose-bridge.test.ts +105 -0
  20. package/src/client/__tests__/hydrate-partial.test.ts +27 -0
  21. package/src/client/__tests__/note-expand.test.ts +130 -0
  22. package/src/client/archive-nav.js +2 -1
  23. package/src/client/audio-player.ts +7 -3
  24. package/src/client/components/__tests__/compose-format-convert.test.ts +357 -0
  25. package/src/client/components/__tests__/jant-compose-dialog.test.ts +313 -0
  26. package/src/client/components/__tests__/jant-compose-editor.test.ts +2 -0
  27. package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +2 -0
  28. package/src/client/components/__tests__/jant-settings-avatar.test.ts +5 -2
  29. package/src/client/components/__tests__/jant-settings-general.test.ts +55 -8
  30. package/src/client/components/compose-format-convert.ts +255 -0
  31. package/src/client/components/compose-types.ts +2 -0
  32. package/src/client/components/jant-compose-dialog.ts +110 -44
  33. package/src/client/components/jant-compose-editor.ts +64 -11
  34. package/src/client/components/jant-settings-general.ts +56 -18
  35. package/src/client/components/settings-types.ts +11 -0
  36. package/src/client/compose-bridge.ts +17 -0
  37. package/src/client/feed-video-player.ts +1 -1
  38. package/src/client/hydrate-partial.ts +25 -0
  39. package/src/client/note-expand.ts +63 -0
  40. package/src/client/settings-bridge.ts +3 -0
  41. package/src/client/tiptap/__tests__/mark-exit.test.ts +99 -0
  42. package/src/client/tiptap/bubble-menu.ts +37 -4
  43. package/src/client.ts +1 -0
  44. package/src/db/migrations/0026_absent_rhodey.sql +14 -0
  45. package/src/db/migrations/meta/0026_snapshot.json +2511 -0
  46. package/src/db/migrations/meta/_journal.json +7 -0
  47. package/src/db/migrations/pg/0024_high_violations.sql +14 -0
  48. package/src/db/migrations/pg/meta/0024_snapshot.json +3204 -0
  49. package/src/db/migrations/pg/meta/_journal.json +7 -0
  50. package/src/db/pg/schema.ts +36 -0
  51. package/src/db/schema.ts +36 -0
  52. package/src/i18n/__tests__/middleware.test.ts +46 -0
  53. package/src/i18n/locales/public/en.po +41 -0
  54. package/src/i18n/locales/public/en.ts +1 -1
  55. package/src/i18n/locales/public/zh-Hans.po +41 -0
  56. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  57. package/src/i18n/locales/public/zh-Hant.po +41 -0
  58. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  59. package/src/i18n/locales/settings/en.po +37 -22
  60. package/src/i18n/locales/settings/en.ts +1 -1
  61. package/src/i18n/locales/settings/zh-Hans.po +37 -22
  62. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  63. package/src/i18n/locales/settings/zh-Hant.po +37 -22
  64. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  65. package/src/i18n/middleware.ts +17 -8
  66. package/src/i18n/supported-locales.ts +5 -4
  67. package/src/lib/__tests__/markdown-to-tiptap.test.ts +1 -1
  68. package/src/lib/__tests__/markdown.test.ts +1 -1
  69. package/src/lib/__tests__/summary.test.ts +87 -0
  70. package/src/lib/__tests__/timeline.test.ts +48 -1
  71. package/src/lib/__tests__/tiptap-render.test.ts +4 -4
  72. package/src/lib/__tests__/url.test.ts +44 -0
  73. package/src/lib/__tests__/view.test.ts +168 -1
  74. package/src/lib/ids.ts +1 -0
  75. package/src/lib/navigation.ts +1 -0
  76. package/src/lib/resolve-config.ts +3 -2
  77. package/src/lib/summary.ts +42 -3
  78. package/src/lib/tiptap-render.ts +6 -2
  79. package/src/lib/upload.ts +16 -2
  80. package/src/lib/url.ts +41 -0
  81. package/src/lib/view.ts +102 -40
  82. package/src/preset.css +7 -1
  83. package/src/routes/api/__tests__/settings.test.ts +1 -4
  84. package/src/routes/api/__tests__/upload.test.ts +2 -0
  85. package/src/routes/api/internal/__tests__/uploads.test.ts +86 -0
  86. package/src/routes/api/internal/sites.ts +44 -1
  87. package/src/routes/api/public/__tests__/archive.test.ts +66 -0
  88. package/src/routes/api/public/archive.ts +22 -6
  89. package/src/routes/api/settings.ts +2 -1
  90. package/src/routes/api/telegram.ts +2 -1
  91. package/src/routes/auth/__tests__/setup.test.ts +14 -0
  92. package/src/routes/dash/__tests__/settings-avatar.test.ts +35 -17
  93. package/src/routes/dash/custom-urls.tsx +1 -1
  94. package/src/routes/dash/settings.tsx +23 -7
  95. package/src/routes/pages/__tests__/archive-params.test.ts +135 -0
  96. package/src/routes/pages/archive.tsx +116 -20
  97. package/src/routes/pages/collections.tsx +1 -0
  98. package/src/services/__tests__/media.test.ts +274 -30
  99. package/src/services/__tests__/post.test.ts +81 -0
  100. package/src/services/__tests__/settings.test.ts +55 -0
  101. package/src/services/bootstrap.ts +7 -0
  102. package/src/services/export-theme/assets/client-site.js +1 -1
  103. package/src/services/export-theme/layouts/_default/baseof.html +2 -1
  104. package/src/services/export-theme/styles/main.css +49 -15
  105. package/src/services/media.ts +199 -42
  106. package/src/services/post.ts +22 -2
  107. package/src/services/search.ts +4 -4
  108. package/src/services/settings.ts +49 -15
  109. package/src/services/upload-session.ts +28 -0
  110. package/src/styles/tokens.css +7 -5
  111. package/src/styles/ui.css +163 -34
  112. package/src/types/bindings.ts +1 -0
  113. package/src/types/config.ts +14 -1
  114. package/src/types/props.ts +3 -0
  115. package/src/ui/compose/ComposeDialog.tsx +13 -0
  116. package/src/ui/dash/settings/AccountMenuContent.tsx +0 -39
  117. package/src/ui/dash/settings/GeneralContent.tsx +38 -4
  118. package/src/ui/dash/settings/SettingsDirectory.tsx +26 -1
  119. package/src/ui/dash/settings/SettingsRootContent.tsx +46 -1
  120. package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +55 -0
  121. package/src/ui/feed/NoteCard.tsx +54 -5
  122. package/src/ui/feed/__tests__/timeline-cards.test.ts +73 -0
  123. package/src/ui/layouts/BaseLayout.tsx +1 -0
  124. package/src/ui/layouts/__tests__/BaseLayout.test.tsx +13 -0
  125. package/src/ui/pages/ArchivePage.tsx +89 -6
  126. package/src/ui/pages/CollectionsPage.tsx +7 -1
  127. package/src/ui/pages/__tests__/ArchivePage.test.tsx +37 -0
  128. package/src/ui/shared/CollectionDirectory.tsx +13 -3
  129. package/src/ui/shared/CollectionsManager.tsx +3 -0
  130. package/dist/app-C1QgMNRY.js +0 -6
  131. package/dist/client/_assets/client-BMPMuwvV.css +0 -2
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createTestApp } from "../../../__tests__/helpers/app.js";
3
+ import { archiveRoutes } from "../archive.js";
4
+
5
+ /**
6
+ * Integration coverage for archive filter param handling.
7
+ *
8
+ * The page and the feed share parseArchiveParams. These tests pin down:
9
+ * - the new single-word params (title/replies/media=any|none),
10
+ * - the legacy hasTitle/hasReplies/hasMedia=1/0 fallback on the feed,
11
+ * which keeps old subscriptions and stored custom archive URLs working,
12
+ * - the 308 canonical redirect on the page route for legacy spellings.
13
+ */
14
+
15
+ async function fetchFeed(
16
+ app: { request: (path: string) => Promise<Response> },
17
+ query: string,
18
+ ): Promise<string> {
19
+ const res = await app.request(`/archive/feed${query}`);
20
+ expect(res.status).toBe(200);
21
+ return res.text();
22
+ }
23
+
24
+ function setupApp() {
25
+ const { app, services } = createTestApp({ authenticated: false });
26
+ app.route("/archive", archiveRoutes);
27
+ return { app, services };
28
+ }
29
+
30
+ describe("archive feed filter params", () => {
31
+ it("filters by title with the new param and the legacy fallback", async () => {
32
+ const { app, services } = setupApp();
33
+ await services.posts.create({
34
+ format: "note",
35
+ title: "Titled post",
36
+ bodyMarkdown: "body with heading",
37
+ });
38
+ await services.posts.create({
39
+ format: "note",
40
+ bodyMarkdown: "body without heading",
41
+ });
42
+
43
+ const fresh = await fetchFeed(app, "?title=none");
44
+ expect(fresh).toContain("body without heading");
45
+ expect(fresh).not.toContain("body with heading");
46
+
47
+ const legacy = await fetchFeed(app, "?hasTitle=0");
48
+ expect(legacy).toContain("body without heading");
49
+ expect(legacy).not.toContain("body with heading");
50
+ });
51
+
52
+ it("filters by replies with the new param and the legacy fallback", async () => {
53
+ const { app, services } = setupApp();
54
+ const root = await services.posts.create({
55
+ format: "note",
56
+ bodyMarkdown: "thread root body",
57
+ });
58
+ await services.posts.create({
59
+ format: "note",
60
+ bodyMarkdown: "reply body",
61
+ replyToId: root.id,
62
+ });
63
+ await services.posts.create({
64
+ format: "note",
65
+ bodyMarkdown: "standalone body",
66
+ });
67
+
68
+ const threads = await fetchFeed(app, "?replies=any");
69
+ expect(threads).toContain("thread root body");
70
+ expect(threads).not.toContain("standalone body");
71
+
72
+ const singles = await fetchFeed(app, "?replies=none");
73
+ expect(singles).toContain("standalone body");
74
+ expect(singles).not.toContain("thread root body");
75
+
76
+ const legacySingles = await fetchFeed(app, "?hasReplies=0");
77
+ expect(legacySingles).toContain("standalone body");
78
+ expect(legacySingles).not.toContain("thread root body");
79
+ });
80
+
81
+ it("treats media=none as the legacy hasMedia=0", async () => {
82
+ const { app, services } = setupApp();
83
+ await services.posts.create({
84
+ format: "note",
85
+ bodyMarkdown: "text only body",
86
+ });
87
+
88
+ const fresh = await fetchFeed(app, "?media=none");
89
+ expect(fresh).toContain("text only body");
90
+
91
+ const legacy = await fetchFeed(app, "?hasMedia=0");
92
+ expect(legacy).toContain("text only body");
93
+
94
+ const withMedia = await fetchFeed(app, "?media=any");
95
+ expect(withMedia).not.toContain("text only body");
96
+ });
97
+ });
98
+
99
+ describe("archive page legacy param redirect", () => {
100
+ it("redirects legacy boolean params to their single-word spelling", async () => {
101
+ const { app } = setupApp();
102
+
103
+ const res = await app.request("/archive?hasTitle=0");
104
+ expect(res.status).toBe(308);
105
+ expect(res.headers.get("location")).toBe("/archive?title=none");
106
+ });
107
+
108
+ it("preserves other params and rewrites only legacy ones", async () => {
109
+ const { app } = setupApp();
110
+
111
+ const res = await app.request(
112
+ "/archive?format=note&hasReplies=1&utm_source=newsletter",
113
+ );
114
+ expect(res.status).toBe(308);
115
+ expect(res.headers.get("location")).toBe(
116
+ "/archive?format=note&utm_source=newsletter&replies=any",
117
+ );
118
+ });
119
+
120
+ it("redirects visibility=latest_hidden to the hidden alias", async () => {
121
+ const { app } = setupApp();
122
+
123
+ const res = await app.request("/archive?visibility=latest_hidden");
124
+ expect(res.status).toBe(308);
125
+ expect(res.headers.get("location")).toBe("/archive?visibility=hidden");
126
+ });
127
+
128
+ it("drops a legacy param without overriding an explicit new one", async () => {
129
+ const { app } = setupApp();
130
+
131
+ const res = await app.request("/archive?title=any&hasTitle=0");
132
+ expect(res.status).toBe(308);
133
+ expect(res.headers.get("location")).toBe("/archive?title=any");
134
+ });
135
+ });
@@ -57,6 +57,7 @@ interface ParsedArchiveParams {
57
57
  mediaKinds?: MediaKind[];
58
58
  hasMedia?: boolean;
59
59
  hasTitle?: boolean;
60
+ hasReplies?: boolean;
60
61
  visibility?: ArchiveVisibility;
61
62
  visibilityAll: boolean;
62
63
  view?: ArchiveView;
@@ -87,25 +88,45 @@ function parseArchiveParams(
87
88
 
88
89
  const collectionSlug = q("collection") || undefined;
89
90
 
90
- const mediaParam = q("media") || undefined;
91
- const mediaKinds = mediaParam
92
- ? (mediaParam
93
- .split(",")
94
- .filter((m): m is MediaKind =>
95
- (MEDIA_KINDS as readonly string[]).includes(m),
96
- ) as MediaKind[])
97
- : undefined;
91
+ // Presence filters use single-word params with any/none values
92
+ // (media=any|none|<kinds>, title=any|none, replies=any|none). The legacy
93
+ // hasMedia/hasTitle/hasReplies=1/0 params are still accepted so old
94
+ // bookmarks, feed subscriptions, and stored custom archive URLs keep
95
+ // working; new URLs are always generated in the new style.
96
+ const parsePresence = (
97
+ param: string | undefined,
98
+ legacy: string | undefined,
99
+ ): boolean | undefined => {
100
+ if (param === "any") return true;
101
+ if (param === "none") return false;
102
+ if (legacy === "1") return true;
103
+ if (legacy === "0") return false;
104
+ return undefined;
105
+ };
98
106
 
99
- const hasMediaParam = q("hasMedia");
100
- const hasMedia =
101
- hasMediaParam === "1" ? true : hasMediaParam === "0" ? false : undefined;
107
+ const mediaParam = q("media") || undefined;
108
+ const mediaIsPresence = mediaParam === "any" || mediaParam === "none";
109
+ const mediaKinds =
110
+ mediaParam && !mediaIsPresence
111
+ ? (mediaParam
112
+ .split(",")
113
+ .filter((m): m is MediaKind =>
114
+ (MEDIA_KINDS as readonly string[]).includes(m),
115
+ ) as MediaKind[])
116
+ : undefined;
117
+ const hasMedia = parsePresence(
118
+ mediaIsPresence ? mediaParam : undefined,
119
+ q("hasMedia"),
120
+ );
102
121
 
103
- const hasTitleParam = q("hasTitle");
104
- const hasTitle =
105
- hasTitleParam === "1" ? true : hasTitleParam === "0" ? false : undefined;
122
+ const hasTitle = parsePresence(q("title"), q("hasTitle"));
123
+ const hasReplies = parsePresence(q("replies"), q("hasReplies"));
106
124
 
107
125
  const VALID_VISIBILITIES = ["public", "latest_hidden", "private", "featured"];
108
- const visibilityParam = q("visibility");
126
+ const rawVisibilityParam = q("visibility");
127
+ // "hidden" is the URL spelling of the internal latest_hidden value
128
+ const visibilityParam =
129
+ rawVisibilityParam === "hidden" ? "latest_hidden" : rawVisibilityParam;
109
130
  const visibilityAll = visibilityParam === "all";
110
131
  const visibility =
111
132
  visibilityParam && VALID_VISIBILITIES.includes(visibilityParam)
@@ -129,6 +150,7 @@ function parseArchiveParams(
129
150
  mediaKinds: mediaKinds && mediaKinds.length > 0 ? mediaKinds : undefined,
130
151
  hasMedia,
131
152
  hasTitle,
153
+ hasReplies,
132
154
  visibility,
133
155
  visibilityAll,
134
156
  view,
@@ -184,6 +206,7 @@ function buildArchivePostFilters(
184
206
  mediaKinds: params.mediaKinds,
185
207
  hasMedia: params.hasMedia,
186
208
  hasTitle: params.hasTitle,
209
+ hasReplies: params.hasReplies,
187
210
  };
188
211
  }
189
212
 
@@ -198,12 +221,14 @@ function buildArchiveFeedQuery(params: ParsedArchiveParams): string {
198
221
  if (params.collectionSlug) qs.set("collection", params.collectionSlug);
199
222
  if (params.mediaKinds && params.mediaKinds.length > 0) {
200
223
  qs.set("media", params.mediaKinds.join(","));
201
- }
202
- if (params.hasMedia !== undefined) {
203
- qs.set("hasMedia", params.hasMedia ? "1" : "0");
224
+ } else if (params.hasMedia !== undefined) {
225
+ qs.set("media", params.hasMedia ? "any" : "none");
204
226
  }
205
227
  if (params.hasTitle !== undefined) {
206
- qs.set("hasTitle", params.hasTitle ? "1" : "0");
228
+ qs.set("title", params.hasTitle ? "any" : "none");
229
+ }
230
+ if (params.hasReplies !== undefined) {
231
+ qs.set("replies", params.hasReplies ? "any" : "none");
207
232
  }
208
233
  const str = qs.toString();
209
234
  return str ? `?${str}` : "";
@@ -211,6 +236,49 @@ function buildArchiveFeedQuery(params: ParsedArchiveParams): string {
211
236
 
212
237
  export const archiveRoutes = new Hono<Env>();
213
238
 
239
+ /**
240
+ * Build a canonical redirect target when a request uses legacy archive
241
+ * param spellings (hasMedia/hasTitle/hasReplies=1/0, visibility=latest_hidden).
242
+ *
243
+ * Only legacy params are rewritten; everything else (including unknown
244
+ * params) is preserved. Returns null when the URL is already canonical.
245
+ * Applies to the /archive page only — feeds and the public API accept
246
+ * legacy spellings silently, and custom archive URLs (path_registry
247
+ * query overrides) never reach this path.
248
+ *
249
+ * @param c - Hono context
250
+ * @returns Canonical path + query to redirect to, or null
251
+ */
252
+ function legacyArchiveParamsRedirect(c: Context<Env>): string | null {
253
+ const url = new URL(c.req.url);
254
+ const params = url.searchParams;
255
+ let changed = false;
256
+
257
+ const rewrites = [
258
+ ["hasMedia", "media"],
259
+ ["hasTitle", "title"],
260
+ ["hasReplies", "replies"],
261
+ ] as const;
262
+ for (const [legacy, name] of rewrites) {
263
+ const value = params.get(legacy);
264
+ if (value === null) continue;
265
+ if (!params.has(name) && (value === "1" || value === "0")) {
266
+ params.set(name, value === "1" ? "any" : "none");
267
+ }
268
+ params.delete(legacy);
269
+ changed = true;
270
+ }
271
+
272
+ if (params.get("visibility") === "latest_hidden") {
273
+ params.set("visibility", "hidden");
274
+ changed = true;
275
+ }
276
+
277
+ if (!changed) return null;
278
+ const qs = params.toString();
279
+ return `${url.pathname}${qs ? `?${qs}` : ""}`;
280
+ }
281
+
214
282
  // =============================================================================
215
283
  // Archive page — shared rendering
216
284
  // =============================================================================
@@ -360,6 +428,7 @@ export async function renderArchivePage(
360
428
  mediaKinds: params.mediaKinds,
361
429
  hasMedia: params.hasMedia,
362
430
  hasTitle: params.hasTitle,
431
+ hasReplies: params.hasReplies,
363
432
  visibility: effectiveVisibility,
364
433
  view: params.view,
365
434
  };
@@ -397,7 +466,11 @@ export async function renderArchivePage(
397
466
  // Archive page route
398
467
  // =============================================================================
399
468
 
400
- archiveRoutes.get("/", (c) => renderArchivePage(c));
469
+ archiveRoutes.get("/", (c) => {
470
+ const canonical = legacyArchiveParamsRedirect(c);
471
+ if (canonical) return c.redirect(canonical, 308);
472
+ return renderArchivePage(c);
473
+ });
401
474
 
402
475
  // =============================================================================
403
476
  // Archive feed
@@ -492,6 +565,28 @@ function buildArchiveFeedTitle(
492
565
  );
493
566
  }
494
567
 
568
+ if (params.hasReplies === true) {
569
+ parts.push(
570
+ i18n._(
571
+ msg({
572
+ message: "threads",
573
+ comment:
574
+ "@context: Archive feed title segment for hasReplies=1 filter",
575
+ }),
576
+ ),
577
+ );
578
+ } else if (params.hasReplies === false) {
579
+ parts.push(
580
+ i18n._(
581
+ msg({
582
+ message: "single posts",
583
+ comment:
584
+ "@context: Archive feed title segment for hasReplies=0 filter",
585
+ }),
586
+ ),
587
+ );
588
+ }
589
+
495
590
  if (params.validYear) {
496
591
  parts.push(String(params.validYear));
497
592
  }
@@ -534,6 +629,7 @@ async function buildArchiveFeedData(
534
629
  mediaKinds: params.mediaKinds,
535
630
  hasMedia: params.hasMedia,
536
631
  hasTitle: params.hasTitle,
632
+ hasReplies: params.hasReplies,
537
633
  ...(params.validYear
538
634
  ? {
539
635
  publishedAfter: Date.UTC(params.validYear, 0, 1) / 1000,
@@ -71,6 +71,7 @@ collectionsPageRoutes.get("/", async (c) => {
71
71
  items={directoryData.items}
72
72
  isAuthenticated={navData.isAuthenticated ?? false}
73
73
  sitePathPrefix={navData.sitePathPrefix}
74
+ siteOrigin={c.var.appConfig.siteOrigin}
74
75
  />
75
76
  ),
76
77
  });