@jant/core 0.6.14 → 0.6.15
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 +81 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-BxCOR3Uc.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +5 -5
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +64 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +18 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
- package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +2 -1
- package/src/client/components/jant-compose-dialog.ts +85 -21
- package/src/client/components/jant-compose-editor.ts +13 -7
- package/src/client/components/jant-compose-fullscreen.ts +24 -4
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +86 -74
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +4 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +8 -6
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +28 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +28 -20
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +133 -30
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +128 -25
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +128 -25
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +8 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +10 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/markdown-manager.ts +188 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +7 -1
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +98 -122
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +355 -81
- package/src/lib/view.ts +30 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +2 -2
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +208 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +41 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +2 -1
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/preview.test.ts +174 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +27 -7
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +83 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +159 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +109 -30
- package/src/services/__tests__/post.test.ts +567 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +226 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +91 -80
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/post.ts +874 -271
- package/src/services/search.ts +8 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -0
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +14 -9
- package/src/styles/ui.css +829 -29
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +9 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +8 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +16 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +92 -2
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +33 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +3 -1
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +46 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -21,11 +21,14 @@ import {
|
|
|
21
21
|
resolveCollectionSortOrder,
|
|
22
22
|
supportsCollectionRatingSort,
|
|
23
23
|
} from "../../../lib/collection-sort.js";
|
|
24
|
+
import { requirePublicApiEnabled } from "../../../middleware/public-content-access.js";
|
|
24
25
|
|
|
25
26
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
26
27
|
|
|
27
28
|
export const publicPostsApiRoutes = new Hono<Env>();
|
|
28
29
|
|
|
30
|
+
publicPostsApiRoutes.use("*", requirePublicApiEnabled());
|
|
31
|
+
|
|
29
32
|
const ListPublicPostsQuerySchema = z.object({
|
|
30
33
|
format: FormatSchema.optional(),
|
|
31
34
|
collection: z.string().optional(),
|
|
@@ -97,7 +100,7 @@ function isPublicDetailVisible(post: Post | null): post is Post {
|
|
|
97
100
|
export function toPublicPost(
|
|
98
101
|
post: Post,
|
|
99
102
|
mediaList: Media[],
|
|
100
|
-
|
|
103
|
+
threadCollections: Collection[],
|
|
101
104
|
appConfig: AppVariables["appConfig"],
|
|
102
105
|
options?: { content?: "markdown" },
|
|
103
106
|
): PublicPostResponse {
|
|
@@ -159,7 +162,7 @@ export function toPublicPost(
|
|
|
159
162
|
sitePathPrefix,
|
|
160
163
|
),
|
|
161
164
|
),
|
|
162
|
-
collections:
|
|
165
|
+
collections: threadCollections.map((collection) => ({
|
|
163
166
|
id: collection.id,
|
|
164
167
|
slug: collection.slug,
|
|
165
168
|
title: collection.title,
|
|
@@ -226,16 +229,19 @@ publicPostsApiRoutes.get("/", async (c) => {
|
|
|
226
229
|
? "newest"
|
|
227
230
|
: primaryCollection.sortOrder;
|
|
228
231
|
|
|
229
|
-
const
|
|
230
|
-
|
|
232
|
+
const ratedThreadCount =
|
|
233
|
+
await c.var.services.posts.countCollectionThreadRootsUpToForCollections(
|
|
231
234
|
collectionIds,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
{
|
|
236
|
+
status: "published",
|
|
237
|
+
excludePrivate: true,
|
|
238
|
+
excludeLatestHidden: true,
|
|
239
|
+
rootFormat: format,
|
|
240
|
+
hasRating: true,
|
|
241
|
+
},
|
|
242
|
+
2,
|
|
243
|
+
);
|
|
244
|
+
const showRatingSort = supportsCollectionRatingSort(ratedThreadCount);
|
|
239
245
|
const defaultSort = resolveCollectionSortOrder(
|
|
240
246
|
undefined,
|
|
241
247
|
requestedDefaultSort,
|
|
@@ -244,17 +250,28 @@ publicPostsApiRoutes.get("/", async (c) => {
|
|
|
244
250
|
sortOrder = resolveCollectionSortOrder(sort, defaultSort, showRatingSort);
|
|
245
251
|
}
|
|
246
252
|
|
|
247
|
-
const posts =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
253
|
+
const posts = collectionIds
|
|
254
|
+
? await c.var.services.posts.listCollectionThreadRootsForCollections(
|
|
255
|
+
collectionIds,
|
|
256
|
+
{
|
|
257
|
+
status: "published",
|
|
258
|
+
excludePrivate: true,
|
|
259
|
+
excludeLatestHidden: true,
|
|
260
|
+
rootFormat: format,
|
|
261
|
+
sortOrder,
|
|
262
|
+
cursor: cursor ?? undefined,
|
|
263
|
+
limit,
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
: await c.var.services.posts.list({
|
|
267
|
+
format,
|
|
268
|
+
status: "published",
|
|
269
|
+
cursor: cursor ?? undefined,
|
|
270
|
+
limit,
|
|
271
|
+
excludePrivate: true,
|
|
272
|
+
excludeLatestHidden: true,
|
|
273
|
+
excludeReplies: true,
|
|
274
|
+
});
|
|
258
275
|
|
|
259
276
|
const postIds = posts.map((post) => post.id);
|
|
260
277
|
const [mediaMap, collectionsMap] = await Promise.all([
|
|
@@ -289,13 +306,13 @@ publicPostsApiRoutes.get("/:slug", async (c) => {
|
|
|
289
306
|
throw new NotFoundError("Post");
|
|
290
307
|
}
|
|
291
308
|
|
|
292
|
-
const [mediaList,
|
|
309
|
+
const [mediaList, threadCollections] = await Promise.all([
|
|
293
310
|
c.var.services.media.getByPostId(post.id),
|
|
294
311
|
c.var.services.collections.getCollectionsByPostId(post.id),
|
|
295
312
|
]);
|
|
296
313
|
|
|
297
314
|
return c.json(
|
|
298
|
-
toPublicPost(post, mediaList,
|
|
315
|
+
toPublicPost(post, mediaList, threadCollections, c.var.appConfig, {
|
|
299
316
|
content,
|
|
300
317
|
}),
|
|
301
318
|
);
|
package/src/routes/api/search.ts
CHANGED
|
@@ -8,11 +8,14 @@ import type { AppVariables } from "../../types/app-context.js";
|
|
|
8
8
|
import { ValidationError, ExternalServiceError } from "../../lib/errors.js";
|
|
9
9
|
import { toSearchApiResult } from "../../lib/api-search.js";
|
|
10
10
|
import { rateLimit } from "../../middleware/rate-limit.js";
|
|
11
|
+
import { requirePublicApiAccess } from "../../middleware/public-content-access.js";
|
|
11
12
|
|
|
12
13
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
13
14
|
|
|
14
15
|
export const searchApiRoutes = new Hono<Env>();
|
|
15
16
|
|
|
17
|
+
searchApiRoutes.use("*", requirePublicApiAccess());
|
|
18
|
+
|
|
16
19
|
// Per-IP rate limit. The request-time wrapper is needed because the
|
|
17
20
|
// per-minute cap is pulled from `appConfig` which is only available on
|
|
18
21
|
// `c.var`; constructing the middleware once at module load would capture
|
|
@@ -13,7 +13,10 @@ import { parseValidated } from "../../lib/schemas.js";
|
|
|
13
13
|
import { ValidationError } from "../../lib/errors.js";
|
|
14
14
|
import { syncHostedControlPlaneSiteAvatar } from "../../lib/hosted-control-plane-sync.js";
|
|
15
15
|
import {
|
|
16
|
+
buildConfigEditorFields,
|
|
16
17
|
buildEditableSettingsResponse,
|
|
18
|
+
demoLockedSettingKeys,
|
|
19
|
+
isResettableConfigEditorKey,
|
|
17
20
|
partitionEditableSettingUpdates,
|
|
18
21
|
partitionImportableSettingUpdates,
|
|
19
22
|
} from "../../lib/api-settings.js";
|
|
@@ -31,6 +34,7 @@ settingsApiRoutes.get("/", requireAuthApi(), async (c) => {
|
|
|
31
34
|
settings: buildEditableSettingsResponse(
|
|
32
35
|
allSettings,
|
|
33
36
|
c.var.appConfig.demoMode,
|
|
37
|
+
c.env,
|
|
34
38
|
),
|
|
35
39
|
});
|
|
36
40
|
});
|
|
@@ -61,6 +65,7 @@ settingsApiRoutes.put("/", requireAuthApi(), async (c) => {
|
|
|
61
65
|
settings: buildEditableSettingsResponse(
|
|
62
66
|
allSettings,
|
|
63
67
|
c.var.appConfig.demoMode,
|
|
68
|
+
c.env,
|
|
64
69
|
),
|
|
65
70
|
...(rejectedKeys.length > 0 && { rejectedKeys }),
|
|
66
71
|
});
|
|
@@ -216,3 +221,31 @@ settingsApiRoutes.delete("/avatar", requireAuthApi(), async (c) => {
|
|
|
216
221
|
}
|
|
217
222
|
return c.json({ success: true });
|
|
218
223
|
});
|
|
224
|
+
|
|
225
|
+
// Remove one runtime override so the environment/default fallback applies.
|
|
226
|
+
// Registered after the concrete avatar route so `DELETE /avatar` remains
|
|
227
|
+
// unambiguous.
|
|
228
|
+
settingsApiRoutes.delete("/:key", requireAuthApi(), async (c) => {
|
|
229
|
+
const key = c.req.param("key");
|
|
230
|
+
const demoMode = c.var.appConfig.demoMode;
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
!isResettableConfigEditorKey(key) ||
|
|
234
|
+
(demoMode && demoLockedSettingKeys.has(key))
|
|
235
|
+
) {
|
|
236
|
+
throw new ValidationError("This setting can't be reset.", {
|
|
237
|
+
rejectedKeys: [key],
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await c.var.services.settings.remove(key);
|
|
242
|
+
const allSettings = await c.var.services.settings.getAll();
|
|
243
|
+
const setting = buildConfigEditorFields(allSettings, c.env, demoMode).find(
|
|
244
|
+
(field) => field.key === key,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
return c.json({
|
|
248
|
+
settings: buildEditableSettingsResponse(allSettings, demoMode, c.env),
|
|
249
|
+
setting,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -38,14 +38,13 @@ describe("Setup bootstrap logic", () => {
|
|
|
38
38
|
};
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it("creates
|
|
41
|
+
it("creates the five-item default navigation without Latest", async () => {
|
|
42
42
|
await runSetupBootstrap(services);
|
|
43
43
|
|
|
44
44
|
const navItemsList = await services.db.select().from(navItems);
|
|
45
|
-
expect(navItemsList).toHaveLength(
|
|
45
|
+
expect(navItemsList).toHaveLength(5);
|
|
46
46
|
|
|
47
47
|
expect(navItemsList.map((item) => item.systemKey)).toEqual([
|
|
48
|
-
"latest",
|
|
49
48
|
"featured",
|
|
50
49
|
"collections",
|
|
51
50
|
"archive",
|
|
@@ -125,7 +124,11 @@ describe("Setup bootstrap logic", () => {
|
|
|
125
124
|
const navItemsList = await services.db.select().from(navItems);
|
|
126
125
|
const systemItems = navItemsList.filter((item) => item.type === "system");
|
|
127
126
|
|
|
128
|
-
expect(systemItems).toHaveLength(
|
|
127
|
+
expect(systemItems).toHaveLength(5);
|
|
128
|
+
expect(systemItems.map((item) => item.systemKey)).not.toContain("latest");
|
|
129
|
+
expect(
|
|
130
|
+
systemItems.find((item) => item.systemKey === "collections")?.label,
|
|
131
|
+
).toBe("Collections");
|
|
129
132
|
});
|
|
130
133
|
|
|
131
134
|
it("creates a site shell when setup runs after a factory reset", async () => {
|
package/src/routes/compose.tsx
CHANGED
|
@@ -226,8 +226,9 @@ composeRoutes.post("/thread", async (c) => {
|
|
|
226
226
|
: data.url || undefined,
|
|
227
227
|
quoteText: data.quoteText || undefined,
|
|
228
228
|
rating: data.rating || undefined,
|
|
229
|
-
collectionIds: index === 0 ? data.collectionIds :
|
|
229
|
+
collectionIds: index === 0 ? data.collectionIds : undefined,
|
|
230
230
|
replyToId: index === 0 ? data.replyToId : undefined,
|
|
231
|
+
quietReply: data.quietReply,
|
|
231
232
|
publishedAt: index === 0 ? data.publishedAt : undefined,
|
|
232
233
|
},
|
|
233
234
|
attachments: data.attachments,
|
|
@@ -18,7 +18,7 @@ import { renderPublicPage } from "../../lib/render.js";
|
|
|
18
18
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
19
19
|
import { buildPageTitle } from "../../lib/page-title.js";
|
|
20
20
|
import { AdminBreadcrumb } from "../../ui/shared/AdminBreadcrumb.js";
|
|
21
|
-
import {
|
|
21
|
+
import { getTimeZoneOptions } from "../../lib/timezones.js";
|
|
22
22
|
import { ValidationError } from "../../lib/errors.js";
|
|
23
23
|
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
24
24
|
import { getAvailableThemes } from "../../lib/theme.js";
|
|
@@ -39,6 +39,7 @@ import { FontThemeContent } from "../../ui/dash/appearance/FontThemeContent.js";
|
|
|
39
39
|
import { AdvancedContent } from "../../ui/dash/appearance/AdvancedContent.js";
|
|
40
40
|
import { CodeInjectionContent } from "../../ui/dash/appearance/CodeInjectionContent.js";
|
|
41
41
|
import { ApiTokensContent } from "../../ui/dash/settings/ApiTokensContent.js";
|
|
42
|
+
import { ConfigEditorContent } from "../../ui/dash/settings/ConfigEditorContent.js";
|
|
42
43
|
import { DeleteAccountContent } from "../../ui/dash/settings/DeleteAccountContent.js";
|
|
43
44
|
import {
|
|
44
45
|
GitHubSyncContent,
|
|
@@ -78,6 +79,7 @@ import {
|
|
|
78
79
|
triggerGitHubSyncInline,
|
|
79
80
|
} from "../../lib/github-sync-trigger.js";
|
|
80
81
|
import { buildSyncSiteConfig } from "../../lib/github-sync-site-config.js";
|
|
82
|
+
import { buildConfigEditorFields } from "../../lib/api-settings.js";
|
|
81
83
|
import {
|
|
82
84
|
readGitHubSyncStatus,
|
|
83
85
|
renderStatusCardHtml,
|
|
@@ -142,6 +144,7 @@ function breadcrumbLabel(
|
|
|
142
144
|
| "fontTheme"
|
|
143
145
|
| "customCss"
|
|
144
146
|
| "codeInjection"
|
|
147
|
+
| "config"
|
|
145
148
|
| "account"
|
|
146
149
|
| "sessions"
|
|
147
150
|
| "password"
|
|
@@ -187,6 +190,13 @@ function breadcrumbLabel(
|
|
|
187
190
|
comment: "@context: Breadcrumb label",
|
|
188
191
|
}),
|
|
189
192
|
);
|
|
193
|
+
case "config":
|
|
194
|
+
return i18n._(
|
|
195
|
+
msg({
|
|
196
|
+
message: "Config Editor",
|
|
197
|
+
comment: "@context: Breadcrumb label",
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
190
200
|
case "account":
|
|
191
201
|
return i18n._(
|
|
192
202
|
msg({
|
|
@@ -366,7 +376,7 @@ settingsRoutes.get("/general", async (c) => {
|
|
|
366
376
|
showJantBrandingOnHome={appConfig.showJantBrandingOnHome}
|
|
367
377
|
noindex={appConfig.noindex}
|
|
368
378
|
demoMode={appConfig.demoMode}
|
|
369
|
-
timezones={
|
|
379
|
+
timezones={getTimeZoneOptions(appConfig.timeZone)}
|
|
370
380
|
aboutPage={aboutPage}
|
|
371
381
|
aboutEditUrl={aboutEditPath(c)}
|
|
372
382
|
aboutCreateUrl={publicPath(c, "/settings/general/about-page")}
|
|
@@ -808,6 +818,7 @@ settingsRoutes.get("/navigation", async (c) => {
|
|
|
808
818
|
directoryData={directoryData}
|
|
809
819
|
suggestedLinks={suggestedLinks}
|
|
810
820
|
mainRssFeed={c.var.appConfig.mainRssFeed}
|
|
821
|
+
rssFeedsEnabled={c.var.appConfig.rssFeedsEnabled}
|
|
811
822
|
siteName={navData.siteName}
|
|
812
823
|
sitePathPrefix={c.var.appConfig.sitePathPrefix}
|
|
813
824
|
/>
|
|
@@ -1051,6 +1062,38 @@ settingsRoutes.post("/code-injection", async (c) => {
|
|
|
1051
1062
|
);
|
|
1052
1063
|
});
|
|
1053
1064
|
|
|
1065
|
+
// ===========================================================================
|
|
1066
|
+
// Config Editor — explicitly approved runtime settings only.
|
|
1067
|
+
// ===========================================================================
|
|
1068
|
+
|
|
1069
|
+
settingsRoutes.get("/config", async (c) => {
|
|
1070
|
+
const navData = await getNavigationData(c);
|
|
1071
|
+
const fields = buildConfigEditorFields(
|
|
1072
|
+
c.var.allSettings,
|
|
1073
|
+
c.env,
|
|
1074
|
+
c.var.appConfig.demoMode,
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
return renderPublicPage(c, {
|
|
1078
|
+
title: buildPageTitle("Config Editor", navData.siteName),
|
|
1079
|
+
navData,
|
|
1080
|
+
content: (
|
|
1081
|
+
<>
|
|
1082
|
+
<AdminBreadcrumb
|
|
1083
|
+
parent={breadcrumbLabel(c, "settings")}
|
|
1084
|
+
parentHref={publicPath(c, "/settings")}
|
|
1085
|
+
current={breadcrumbLabel(c, "config")}
|
|
1086
|
+
/>
|
|
1087
|
+
<ConfigEditorContent
|
|
1088
|
+
fields={fields}
|
|
1089
|
+
endpoint={publicPath(c, "/api/settings")}
|
|
1090
|
+
sitePathPrefix={c.var.appConfig.sitePathPrefix}
|
|
1091
|
+
/>
|
|
1092
|
+
</>
|
|
1093
|
+
),
|
|
1094
|
+
});
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1054
1097
|
// ===========================================================================
|
|
1055
1098
|
// Account sub-menu
|
|
1056
1099
|
// ===========================================================================
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it,
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { eq } from "drizzle-orm";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import type { Bindings } from "../../../types.js";
|
|
@@ -43,6 +43,7 @@ function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
|
|
|
43
43
|
const env = {
|
|
44
44
|
SITE_ORIGIN: `http://localhost:${DEFAULT_APP_PORT}`,
|
|
45
45
|
SITE_PATH_PREFIX: "",
|
|
46
|
+
RSS_PUBLISH_DELAY_SECONDS: 0,
|
|
46
47
|
...envOverrides,
|
|
47
48
|
} as Bindings;
|
|
48
49
|
c.env = env;
|
|
@@ -67,6 +68,10 @@ function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
|
|
|
67
68
|
return { app, services, db: db as unknown as Database };
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
vi.restoreAllMocks();
|
|
73
|
+
});
|
|
74
|
+
|
|
70
75
|
describe("Atom Feed Routes", () => {
|
|
71
76
|
describe("/feed — site main feed", () => {
|
|
72
77
|
it("defaults to featured posts", async () => {
|
|
@@ -170,9 +175,10 @@ describe("Atom Feed Routes", () => {
|
|
|
170
175
|
expect(res.headers.get("Content-Type")).toBe(
|
|
171
176
|
"application/atom+xml; charset=utf-8",
|
|
172
177
|
);
|
|
178
|
+
expect(res.headers.get("Cache-Control")).toBe("public, max-age=60");
|
|
173
179
|
});
|
|
174
180
|
|
|
175
|
-
it("orders
|
|
181
|
+
it("orders Featured Threads by selected Post publication time", async () => {
|
|
176
182
|
const { app, services, db } = createFeedTestApp();
|
|
177
183
|
|
|
178
184
|
const olderPublished = await services.posts.create({
|
|
@@ -203,11 +209,9 @@ describe("Atom Feed Routes", () => {
|
|
|
203
209
|
expect(res.status).toBe(200);
|
|
204
210
|
|
|
205
211
|
const xml = await res.text();
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
xml.indexOf("Newer published"),
|
|
212
|
+
expect(xml.indexOf("Newer published")).toBeLessThan(
|
|
213
|
+
xml.indexOf("Older published"),
|
|
209
214
|
);
|
|
210
|
-
// <published> should reflect original publishedAt, not featuredAt
|
|
211
215
|
expect(xml).toContain("<published>1970-01-01T00:16:40.000Z</published>");
|
|
212
216
|
expect(xml).toContain("<published>1970-01-01T00:33:20.000Z</published>");
|
|
213
217
|
});
|
|
@@ -352,6 +356,45 @@ describe("Atom Feed Routes", () => {
|
|
|
352
356
|
expect(xml).toContain("Featured Post");
|
|
353
357
|
expect(xml).not.toContain("Regular Post");
|
|
354
358
|
});
|
|
359
|
+
|
|
360
|
+
it("deduplicates by Thread and orders from a Featured Child publication", async () => {
|
|
361
|
+
const { app, services } = createFeedTestApp();
|
|
362
|
+
|
|
363
|
+
const olderRoot = await services.posts.create({
|
|
364
|
+
format: "note",
|
|
365
|
+
title: "Older Thread Root",
|
|
366
|
+
bodyMarkdown: "Older root body",
|
|
367
|
+
status: "published",
|
|
368
|
+
publishedAt: 1000,
|
|
369
|
+
});
|
|
370
|
+
await services.posts.create({
|
|
371
|
+
format: "note",
|
|
372
|
+
title: "Featured Child",
|
|
373
|
+
bodyMarkdown: "Selected child body",
|
|
374
|
+
status: "published",
|
|
375
|
+
replyToId: olderRoot.id,
|
|
376
|
+
featured: true,
|
|
377
|
+
publishedAt: 3000,
|
|
378
|
+
});
|
|
379
|
+
await services.posts.create({
|
|
380
|
+
format: "note",
|
|
381
|
+
title: "Newer Root",
|
|
382
|
+
bodyMarkdown: "Newer root body",
|
|
383
|
+
status: "published",
|
|
384
|
+
featured: true,
|
|
385
|
+
publishedAt: 2000,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const res = await app.request("/featured/feed");
|
|
389
|
+
expect(res.status).toBe(200);
|
|
390
|
+
|
|
391
|
+
const xml = await res.text();
|
|
392
|
+
expect(xml.match(/<entry>/g)).toHaveLength(2);
|
|
393
|
+
expect(xml).toContain("Selected child body");
|
|
394
|
+
expect(xml.indexOf("Older Thread Root")).toBeLessThan(
|
|
395
|
+
xml.indexOf("Newer Root"),
|
|
396
|
+
);
|
|
397
|
+
});
|
|
355
398
|
});
|
|
356
399
|
|
|
357
400
|
describe("legacy feed path redirects", () => {
|
|
@@ -432,6 +475,33 @@ describe("Atom Feed Routes", () => {
|
|
|
432
475
|
});
|
|
433
476
|
});
|
|
434
477
|
|
|
478
|
+
it("emits canonical v3 footnotes from a stale stored projection", async () => {
|
|
479
|
+
const { app, services, db } = createFeedTestApp();
|
|
480
|
+
const post = await services.posts.create({
|
|
481
|
+
format: "note",
|
|
482
|
+
title: "Footnote feed",
|
|
483
|
+
bodyMarkdown: "Feed body[^1]\n\n[^1]: Feed definition",
|
|
484
|
+
status: "published",
|
|
485
|
+
});
|
|
486
|
+
await db
|
|
487
|
+
.update(postTable)
|
|
488
|
+
.set({
|
|
489
|
+
bodyHtml: '<span class="sidenote">legacy</span>',
|
|
490
|
+
bodyHtmlVersion: 1,
|
|
491
|
+
})
|
|
492
|
+
.where(eq(postTable.id, post.id));
|
|
493
|
+
|
|
494
|
+
const response = await app.request("/latest/feed");
|
|
495
|
+
const xml = await response.text();
|
|
496
|
+
|
|
497
|
+
expect(response.status).toBe(200);
|
|
498
|
+
expect(xml).toContain('role="doc-noteref"');
|
|
499
|
+
expect(xml).toContain('role="doc-endnotes"');
|
|
500
|
+
expect(xml).toMatch(/id="fn-[a-z0-9]{13}-1"/);
|
|
501
|
+
expect(xml).not.toContain(`fn-${post.id}`);
|
|
502
|
+
expect(xml).not.toContain("legacy");
|
|
503
|
+
});
|
|
504
|
+
|
|
435
505
|
describe("RSS_FEED_LIMIT env var", () => {
|
|
436
506
|
it("defaults to 50 when RSS_FEED_LIMIT is not set", async () => {
|
|
437
507
|
const { app, services } = createFeedTestApp();
|
|
@@ -527,6 +597,85 @@ describe("Atom Feed Routes", () => {
|
|
|
527
597
|
});
|
|
528
598
|
});
|
|
529
599
|
|
|
600
|
+
describe("RSS_PUBLISH_DELAY_SECONDS env var", () => {
|
|
601
|
+
it("includes the exact 300-second boundary and excludes newer Posts", async () => {
|
|
602
|
+
const currentTime = 2_000_000;
|
|
603
|
+
vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
604
|
+
const { app, services } = createFeedTestApp({
|
|
605
|
+
RSS_PUBLISH_DELAY_SECONDS: 300,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
await services.posts.create({
|
|
609
|
+
format: "note",
|
|
610
|
+
title: "Boundary Post",
|
|
611
|
+
bodyMarkdown: "Eligible at exactly five minutes",
|
|
612
|
+
status: "published",
|
|
613
|
+
publishedAt: currentTime - 300,
|
|
614
|
+
});
|
|
615
|
+
await services.posts.create({
|
|
616
|
+
format: "note",
|
|
617
|
+
title: "Recent Post",
|
|
618
|
+
bodyMarkdown: "Still inside the edit window",
|
|
619
|
+
status: "published",
|
|
620
|
+
publishedAt: currentTime - 299,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const xml = await (await app.request("/latest/feed")).text();
|
|
624
|
+
|
|
625
|
+
expect(xml).toContain("Boundary Post");
|
|
626
|
+
expect(xml).not.toContain("Recent Post");
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it("applies the delay to Featured Posts", async () => {
|
|
630
|
+
const currentTime = 2_000_000;
|
|
631
|
+
vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
632
|
+
const { app, services } = createFeedTestApp({
|
|
633
|
+
RSS_PUBLISH_DELAY_SECONDS: 300,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
await services.posts.create({
|
|
637
|
+
format: "note",
|
|
638
|
+
title: "Eligible Featured Post",
|
|
639
|
+
bodyMarkdown: "Old enough",
|
|
640
|
+
status: "published",
|
|
641
|
+
featured: true,
|
|
642
|
+
publishedAt: currentTime - 300,
|
|
643
|
+
});
|
|
644
|
+
await services.posts.create({
|
|
645
|
+
format: "note",
|
|
646
|
+
title: "Recent Featured Post",
|
|
647
|
+
bodyMarkdown: "Too new",
|
|
648
|
+
status: "published",
|
|
649
|
+
featured: true,
|
|
650
|
+
publishedAt: currentTime,
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const xml = await (await app.request("/featured/feed")).text();
|
|
654
|
+
|
|
655
|
+
expect(xml).toContain("Eligible Featured Post");
|
|
656
|
+
expect(xml).not.toContain("Recent Featured Post");
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("lets zero disable the delay", async () => {
|
|
660
|
+
const currentTime = 2_000_000;
|
|
661
|
+
vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
662
|
+
const { app, services } = createFeedTestApp({
|
|
663
|
+
RSS_PUBLISH_DELAY_SECONDS: 0,
|
|
664
|
+
});
|
|
665
|
+
await services.posts.create({
|
|
666
|
+
format: "note",
|
|
667
|
+
title: "Immediate Post",
|
|
668
|
+
bodyMarkdown: "No edit window",
|
|
669
|
+
status: "published",
|
|
670
|
+
publishedAt: currentTime,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const xml = await (await app.request("/latest/feed")).text();
|
|
674
|
+
|
|
675
|
+
expect(xml).toContain("Immediate Post");
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
530
679
|
describe("thread content in feed", () => {
|
|
531
680
|
it("includes thread replies inline with hr separators", async () => {
|
|
532
681
|
const { app, services } = createFeedTestApp();
|
|
@@ -552,5 +701,60 @@ describe("Atom Feed Routes", () => {
|
|
|
552
701
|
expect(xml).toContain("Reply content");
|
|
553
702
|
expect(xml).toContain("<hr/>");
|
|
554
703
|
});
|
|
704
|
+
|
|
705
|
+
it("delays new replies inside an already eligible Thread", async () => {
|
|
706
|
+
const currentTime = 2_000_000;
|
|
707
|
+
const dateNow = vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
708
|
+
const { app, services } = createFeedTestApp({
|
|
709
|
+
RSS_PUBLISH_DELAY_SECONDS: 300,
|
|
710
|
+
});
|
|
711
|
+
const root = await services.posts.create({
|
|
712
|
+
format: "note",
|
|
713
|
+
title: "Eligible Thread Root",
|
|
714
|
+
bodyMarkdown: "Root content",
|
|
715
|
+
status: "published",
|
|
716
|
+
publishedAt: currentTime - 600,
|
|
717
|
+
});
|
|
718
|
+
await services.posts.create({
|
|
719
|
+
format: "note",
|
|
720
|
+
bodyMarkdown: "Recent reply content",
|
|
721
|
+
status: "published",
|
|
722
|
+
replyToId: root.id,
|
|
723
|
+
publishedAt: currentTime,
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const beforeDelay = await (await app.request("/latest/feed")).text();
|
|
727
|
+
expect(beforeDelay).toContain("Root content");
|
|
728
|
+
expect(beforeDelay).not.toContain("Recent reply content");
|
|
729
|
+
|
|
730
|
+
dateNow.mockReturnValue((currentTime + 300) * 1000);
|
|
731
|
+
const afterDelay = await (await app.request("/latest/feed")).text();
|
|
732
|
+
expect(afterDelay).toContain("Recent reply content");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it("reflects eligible content edits in the Atom entry updated timestamp", async () => {
|
|
736
|
+
const currentTime = 2_000_000;
|
|
737
|
+
vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
738
|
+
const { app, services } = createFeedTestApp({
|
|
739
|
+
RSS_PUBLISH_DELAY_SECONDS: 300,
|
|
740
|
+
});
|
|
741
|
+
const root = await services.posts.create({
|
|
742
|
+
format: "note",
|
|
743
|
+
title: "Edited Post",
|
|
744
|
+
bodyMarkdown: "Original copy",
|
|
745
|
+
status: "published",
|
|
746
|
+
publishedAt: currentTime - 300,
|
|
747
|
+
});
|
|
748
|
+
await services.posts.update(root.id, {
|
|
749
|
+
bodyMarkdown: "Corrected copy",
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
const xml = await (await app.request("/latest/feed")).text();
|
|
753
|
+
const entry = xml.match(/<entry>[\s\S]*?<\/entry>/)?.[0];
|
|
754
|
+
|
|
755
|
+
expect(entry).toContain("Corrected copy");
|
|
756
|
+
expect(entry).not.toContain("Original copy");
|
|
757
|
+
expect(entry).toContain("<updated>1970-01-24T03:33:20.000Z</updated>");
|
|
758
|
+
});
|
|
555
759
|
});
|
|
556
760
|
});
|