@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.
- package/bin/commands/uploads/cleanup.js +2 -0
- package/dist/{app-L1UPUArB.js → app-C-jxWmAV.js} +12421 -12033
- package/dist/app-DqHzOwL5.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-CGf2m3qp.css +2 -0
- package/dist/client/_assets/{client-B0MvB2r0.js → client-DWy1LEEk.js} +2 -2
- package/dist/client/_assets/{client-auth-CwwuucF_.js → client-auth-Blg-a5Ep.js} +365 -345
- package/dist/{env-CoSe-1y4.js → env-OHRKGcMj.js} +1 -1
- package/dist/{export-DLukCOO3.js → export-C2DIB7mm.js} +34 -9
- package/dist/{github-api-UD4u_7fa.js → github-api-BgSiE71w.js} +1 -1
- package/dist/{github-app-DeX6Td1O.js → github-app-BbklkFmU.js} +1 -1
- package/dist/{github-sync-BeDecPen.js → github-sync-7XQ5ZM6z.js} +3 -3
- package/dist/{github-sync-BtHY2AST.js → github-sync-BEFCfLKK.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-XF0GbKGO.js → url-BMYO-Zlt.js} +42 -2
- package/package.json +1 -1
- package/src/__tests__/bin/uploads-cleanup.test.ts +2 -0
- package/src/client/__tests__/compose-bridge.test.ts +105 -0
- package/src/client/__tests__/hydrate-partial.test.ts +27 -0
- package/src/client/__tests__/note-expand.test.ts +130 -0
- package/src/client/archive-nav.js +2 -1
- package/src/client/audio-player.ts +7 -3
- package/src/client/components/__tests__/compose-format-convert.test.ts +357 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +313 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +2 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +2 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +5 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +55 -8
- package/src/client/components/compose-format-convert.ts +255 -0
- package/src/client/components/compose-types.ts +2 -0
- package/src/client/components/jant-compose-dialog.ts +110 -44
- package/src/client/components/jant-compose-editor.ts +64 -11
- package/src/client/components/jant-settings-general.ts +56 -18
- package/src/client/components/settings-types.ts +11 -0
- package/src/client/compose-bridge.ts +17 -0
- package/src/client/feed-video-player.ts +1 -1
- package/src/client/hydrate-partial.ts +25 -0
- package/src/client/note-expand.ts +63 -0
- package/src/client/settings-bridge.ts +3 -0
- package/src/client/tiptap/__tests__/mark-exit.test.ts +99 -0
- package/src/client/tiptap/bubble-menu.ts +37 -4
- package/src/client.ts +1 -0
- package/src/db/migrations/0026_absent_rhodey.sql +14 -0
- package/src/db/migrations/meta/0026_snapshot.json +2511 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0024_high_violations.sql +14 -0
- package/src/db/migrations/pg/meta/0024_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +36 -0
- package/src/db/schema.ts +36 -0
- package/src/i18n/__tests__/middleware.test.ts +46 -0
- package/src/i18n/locales/public/en.po +41 -0
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +41 -0
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +41 -0
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +37 -22
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +37 -22
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +37 -22
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +17 -8
- package/src/i18n/supported-locales.ts +5 -4
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +1 -1
- package/src/lib/__tests__/markdown.test.ts +1 -1
- package/src/lib/__tests__/summary.test.ts +87 -0
- package/src/lib/__tests__/timeline.test.ts +48 -1
- package/src/lib/__tests__/tiptap-render.test.ts +4 -4
- package/src/lib/__tests__/url.test.ts +44 -0
- package/src/lib/__tests__/view.test.ts +168 -1
- package/src/lib/ids.ts +1 -0
- package/src/lib/navigation.ts +1 -0
- package/src/lib/resolve-config.ts +3 -2
- package/src/lib/summary.ts +42 -3
- package/src/lib/tiptap-render.ts +6 -2
- package/src/lib/upload.ts +16 -2
- package/src/lib/url.ts +41 -0
- package/src/lib/view.ts +102 -40
- package/src/preset.css +7 -1
- package/src/routes/api/__tests__/settings.test.ts +1 -4
- package/src/routes/api/__tests__/upload.test.ts +2 -0
- package/src/routes/api/internal/__tests__/uploads.test.ts +86 -0
- package/src/routes/api/internal/sites.ts +44 -1
- package/src/routes/api/public/__tests__/archive.test.ts +66 -0
- package/src/routes/api/public/archive.ts +22 -6
- package/src/routes/api/settings.ts +2 -1
- package/src/routes/api/telegram.ts +2 -1
- package/src/routes/auth/__tests__/setup.test.ts +14 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +35 -17
- package/src/routes/dash/custom-urls.tsx +1 -1
- package/src/routes/dash/settings.tsx +23 -7
- package/src/routes/pages/__tests__/archive-params.test.ts +135 -0
- package/src/routes/pages/archive.tsx +116 -20
- package/src/routes/pages/collections.tsx +1 -0
- package/src/services/__tests__/media.test.ts +274 -30
- package/src/services/__tests__/post.test.ts +81 -0
- package/src/services/__tests__/settings.test.ts +55 -0
- package/src/services/bootstrap.ts +7 -0
- package/src/services/export-theme/assets/client-site.js +1 -1
- package/src/services/export-theme/layouts/_default/baseof.html +2 -1
- package/src/services/export-theme/styles/main.css +49 -15
- package/src/services/media.ts +199 -42
- package/src/services/post.ts +22 -2
- package/src/services/search.ts +4 -4
- package/src/services/settings.ts +49 -15
- package/src/services/upload-session.ts +28 -0
- package/src/styles/tokens.css +7 -5
- package/src/styles/ui.css +163 -34
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +14 -1
- package/src/types/props.ts +3 -0
- package/src/ui/compose/ComposeDialog.tsx +13 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +0 -39
- package/src/ui/dash/settings/GeneralContent.tsx +38 -4
- package/src/ui/dash/settings/SettingsDirectory.tsx +26 -1
- package/src/ui/dash/settings/SettingsRootContent.tsx +46 -1
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +55 -0
- package/src/ui/feed/NoteCard.tsx +54 -5
- package/src/ui/feed/__tests__/timeline-cards.test.ts +73 -0
- package/src/ui/layouts/BaseLayout.tsx +1 -0
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +13 -0
- package/src/ui/pages/ArchivePage.tsx +89 -6
- package/src/ui/pages/CollectionsPage.tsx +7 -1
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +37 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -3
- package/src/ui/shared/CollectionsManager.tsx +3 -0
- package/dist/app-C1QgMNRY.js +0 -6
- 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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
100
|
-
const
|
|
101
|
-
|
|
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
|
|
104
|
-
const
|
|
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
|
|
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
|
-
|
|
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("
|
|
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) =>
|
|
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,
|