@jant/core 0.6.14 → 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 +88 -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-C75Zc8dD.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
- 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-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
- 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-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +7 -7
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- 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__/mise-config.test.ts +22 -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 +316 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -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 +82 -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__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
- package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
- 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-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +55 -3
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +14 -7
- package/src/client/components/jant-compose-dialog.ts +1542 -694
- package/src/client/components/jant-compose-editor.ts +237 -99
- package/src/client/components/jant-compose-fullscreen.ts +27 -9
- 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 +177 -95
- 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 +147 -123
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -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/post-refresh.ts +135 -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__/link-input-rules.test.ts +121 -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/link-input-rules.ts +4 -4
- 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 +133 -39
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -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-activity.test.ts +113 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -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/0031_many_ego.sql +3 -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/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +35 -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/0029_rare_hairball.sql +3 -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/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +35 -0
- package/src/db/pg/schema.ts +46 -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 +48 -20
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +222 -51
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +216 -45
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +216 -45
- 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__/feed.test.ts +168 -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 +24 -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__/slug.test.ts +18 -0
- 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__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -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/feed.ts +113 -11
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +15 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +193 -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 +47 -8
- 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 +137 -131
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +359 -83
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +58 -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__/palette.test.ts +44 -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 +14 -10
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +246 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +48 -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 +8 -3
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +384 -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__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +176 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +90 -22
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +86 -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 +190 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +249 -39
- package/src/services/__tests__/post.test.ts +844 -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 +224 -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 +94 -85
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +1142 -342
- package/src/services/search.ts +17 -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 -14
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +76 -10
- package/src/styles/ui.css +2227 -1347
- 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 +14 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +20 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +22 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +109 -19
- 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/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- 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/ArchivePage.tsx +131 -24
- 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 +70 -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 +49 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +38 -13
- 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/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +52 -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
package/src/lib/timeline.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
} from "../types.js";
|
|
15
15
|
import type { AppVariables } from "../types/app-context.js";
|
|
16
16
|
import { buildMediaMap } from "./media-helpers.js";
|
|
17
|
-
import { createMediaContext, toPostView } from "./view.js";
|
|
17
|
+
import { createMediaContext, resolveDraftTailId, toPostView } from "./view.js";
|
|
18
18
|
|
|
19
19
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
20
20
|
|
|
@@ -28,7 +28,17 @@ export interface TimelineResult {
|
|
|
28
28
|
totalCount: number;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
interface CuratedThreadSource {
|
|
32
|
+
posts: Array<{ post: Post; position: number }>;
|
|
33
|
+
highlightedPostIds: ReadonlySet<string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CuratedThreadDisplayOptions {
|
|
37
|
+
collectionPinnedThreadIds?: ReadonlySet<string>;
|
|
38
|
+
/** Visually focus the newest Post when the Thread itself is the selection. */
|
|
39
|
+
highlightLatestPost?: boolean;
|
|
40
|
+
showContextRatings: boolean;
|
|
41
|
+
}
|
|
32
42
|
|
|
33
43
|
async function buildTimelineItems(
|
|
34
44
|
c: Context<Env>,
|
|
@@ -41,13 +51,32 @@ async function buildTimelineItems(
|
|
|
41
51
|
// Batch load media, collections, and latest-reply contexts in parallel
|
|
42
52
|
const postIds = posts.map((p) => p.id);
|
|
43
53
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
const threadIds = posts.map((p) => p.threadId);
|
|
55
|
+
const [
|
|
56
|
+
rawMediaMap,
|
|
57
|
+
collectionsMap,
|
|
58
|
+
threadContexts,
|
|
59
|
+
aliasesMap,
|
|
60
|
+
publishedTails,
|
|
61
|
+
draftInclusiveTails,
|
|
62
|
+
] = await Promise.all([
|
|
63
|
+
c.var.services.media.getByPostIds(postIds),
|
|
64
|
+
c.var.services.collections.getCollectionsByPostIds(postIds),
|
|
65
|
+
c.var.services.posts.getThreadTimelineContext(postIds),
|
|
66
|
+
c.var.services.paths.getPostAliases(postIds),
|
|
67
|
+
// The feed never renders drafts — it would turn a reading surface into a
|
|
68
|
+
// workspace. So the author's trailing draft stays hidden here, and the
|
|
69
|
+
// reply affordance carries its ID instead of silently offering a reply
|
|
70
|
+
// the server will refuse.
|
|
71
|
+
c.var.isAuthenticated
|
|
72
|
+
? c.var.services.posts.getThreadTailIds(threadIds)
|
|
73
|
+
: Promise.resolve(new Map<string, string>()),
|
|
74
|
+
c.var.isAuthenticated
|
|
75
|
+
? c.var.services.posts.getThreadTailIds(threadIds, {
|
|
76
|
+
includeDrafts: true,
|
|
77
|
+
})
|
|
78
|
+
: Promise.resolve(new Map<string, string>()),
|
|
79
|
+
]);
|
|
51
80
|
const mediaMap = buildMediaMap(
|
|
52
81
|
rawMediaMap,
|
|
53
82
|
mediaCtx.r2PublicUrl,
|
|
@@ -57,12 +86,13 @@ async function buildTimelineItems(
|
|
|
57
86
|
mediaCtx.sitePathPrefix,
|
|
58
87
|
);
|
|
59
88
|
|
|
60
|
-
// Batch load media for
|
|
89
|
+
// Batch load media for the bounded leading/trailing thread context.
|
|
61
90
|
const contextPostIds = new Set<string>();
|
|
62
91
|
for (const ctx of threadContexts.values()) {
|
|
63
92
|
contextPostIds.add(ctx.latestReply.id);
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
for (const reply of [...ctx.leadingReplies, ...ctx.trailingReplies]) {
|
|
94
|
+
contextPostIds.add(reply.id);
|
|
95
|
+
}
|
|
66
96
|
}
|
|
67
97
|
const [contextMediaMap, contextCollectionsMap, contextAliasesMap] =
|
|
68
98
|
contextPostIds.size > 0
|
|
@@ -102,25 +132,33 @@ async function buildTimelineItems(
|
|
|
102
132
|
firstAlias(post.id),
|
|
103
133
|
);
|
|
104
134
|
|
|
135
|
+
const draftTailId = resolveDraftTailId(
|
|
136
|
+
post.threadId,
|
|
137
|
+
publishedTails,
|
|
138
|
+
draftInclusiveTails,
|
|
139
|
+
);
|
|
140
|
+
|
|
105
141
|
const threadCtx = threadContexts.get(post.id);
|
|
106
142
|
|
|
107
143
|
if (threadCtx) {
|
|
108
144
|
// Thread root is not the last post — hide reply button on it
|
|
109
145
|
postView.isLastInThread = false;
|
|
110
146
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
147
|
+
const toContextPostView = (reply: Post) =>
|
|
148
|
+
toPostView(
|
|
149
|
+
{
|
|
150
|
+
...reply,
|
|
151
|
+
mediaAttachments: contextMediaMap.get(reply.id) ?? [],
|
|
152
|
+
},
|
|
153
|
+
mediaCtx,
|
|
154
|
+
contextCollectionsMap.get(reply.id),
|
|
155
|
+
false,
|
|
156
|
+
firstContextAlias(reply.id),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const leadingReplyViews = threadCtx.leadingReplies.map(toContextPostView);
|
|
160
|
+
const trailingReplyViews =
|
|
161
|
+
threadCtx.trailingReplies.map(toContextPostView);
|
|
124
162
|
|
|
125
163
|
const latestReplyView = toPostView(
|
|
126
164
|
{
|
|
@@ -129,35 +167,26 @@ async function buildTimelineItems(
|
|
|
129
167
|
},
|
|
130
168
|
mediaCtx,
|
|
131
169
|
contextCollectionsMap.get(threadCtx.latestReply.id),
|
|
132
|
-
true, // latestReply is the last post in the thread
|
|
170
|
+
true, // latestReply is the last published post in the thread
|
|
133
171
|
firstContextAlias(threadCtx.latestReply.id),
|
|
134
172
|
);
|
|
135
|
-
|
|
136
|
-
const penultimateReplyView = threadCtx.penultimateReply
|
|
137
|
-
? toPostView(
|
|
138
|
-
{
|
|
139
|
-
...threadCtx.penultimateReply,
|
|
140
|
-
mediaAttachments:
|
|
141
|
-
contextMediaMap.get(threadCtx.penultimateReply.id) ?? [],
|
|
142
|
-
},
|
|
143
|
-
mediaCtx,
|
|
144
|
-
contextCollectionsMap.get(threadCtx.penultimateReply.id),
|
|
145
|
-
false, // penultimateReply is not the last post
|
|
146
|
-
firstContextAlias(threadCtx.penultimateReply.id),
|
|
147
|
-
)
|
|
148
|
-
: undefined;
|
|
173
|
+
if (draftTailId) latestReplyView.draftTailId = draftTailId;
|
|
149
174
|
|
|
150
175
|
return {
|
|
151
176
|
post: postView,
|
|
152
177
|
threadPreview: {
|
|
153
|
-
|
|
154
|
-
|
|
178
|
+
leadingReplies: leadingReplyViews,
|
|
179
|
+
trailingReplies: trailingReplyViews,
|
|
155
180
|
latestReply: latestReplyView,
|
|
156
181
|
totalReplyCount: threadCtx.totalReplyCount,
|
|
157
182
|
},
|
|
158
183
|
};
|
|
159
184
|
}
|
|
160
185
|
|
|
186
|
+
// No published replies, so the root carries the reply affordance — and any
|
|
187
|
+
// trailing draft with it.
|
|
188
|
+
if (draftTailId) postView.draftTailId = draftTailId;
|
|
189
|
+
|
|
161
190
|
return { post: postView };
|
|
162
191
|
});
|
|
163
192
|
}
|
|
@@ -182,13 +211,12 @@ export async function assembleTimelineItems(
|
|
|
182
211
|
async function buildCuratedThreadItems(
|
|
183
212
|
c: Context<Env>,
|
|
184
213
|
rootIds: string[],
|
|
185
|
-
threadsByRootId: Map<string,
|
|
186
|
-
|
|
187
|
-
collectionPinnedPostIds?: Set<string>,
|
|
214
|
+
threadsByRootId: Map<string, CuratedThreadSource>,
|
|
215
|
+
display: CuratedThreadDisplayOptions,
|
|
188
216
|
): Promise<TimelineItemView[]> {
|
|
189
217
|
const orderedThreads = rootIds
|
|
190
|
-
.map((rootId) => threadsByRootId.get(rootId)
|
|
191
|
-
.filter((thread) => thread
|
|
218
|
+
.map((rootId) => threadsByRootId.get(rootId))
|
|
219
|
+
.filter((thread): thread is CuratedThreadSource => Boolean(thread));
|
|
192
220
|
|
|
193
221
|
if (orderedThreads.length === 0) {
|
|
194
222
|
return [];
|
|
@@ -196,7 +224,7 @@ async function buildCuratedThreadItems(
|
|
|
196
224
|
|
|
197
225
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
198
226
|
const postIds = orderedThreads.flatMap((thread) =>
|
|
199
|
-
thread.map((post) => post.id),
|
|
227
|
+
thread.posts.map(({ post }) => post.id),
|
|
200
228
|
);
|
|
201
229
|
const [rawMediaMap, collectionsMap, curatedAliasesMap] = await Promise.all([
|
|
202
230
|
c.var.services.media.getByPostIds(postIds),
|
|
@@ -213,14 +241,15 @@ async function buildCuratedThreadItems(
|
|
|
213
241
|
);
|
|
214
242
|
|
|
215
243
|
return orderedThreads.reduce<TimelineItemView[]>((items, thread) => {
|
|
216
|
-
const
|
|
217
|
-
if (!
|
|
244
|
+
const rootEntry = thread.posts[0];
|
|
245
|
+
if (!rootEntry || rootEntry.position !== 0) {
|
|
218
246
|
return items;
|
|
219
247
|
}
|
|
220
248
|
|
|
221
|
-
const lastPostId = thread[thread.length - 1]?.id;
|
|
222
|
-
const
|
|
223
|
-
|
|
249
|
+
const lastPostId = thread.posts[thread.posts.length - 1]?.post.id;
|
|
250
|
+
const renderedPosts = thread.posts.map(({ post, position }) => ({
|
|
251
|
+
position,
|
|
252
|
+
view: toPostView(
|
|
224
253
|
{
|
|
225
254
|
...post,
|
|
226
255
|
mediaAttachments: mediaMap.get(post.id) ?? [],
|
|
@@ -229,54 +258,34 @@ async function buildCuratedThreadItems(
|
|
|
229
258
|
collectionsMap.get(post.id),
|
|
230
259
|
post.id === lastPostId,
|
|
231
260
|
curatedAliasesMap.get(post.id)?.[0],
|
|
232
|
-
|
|
261
|
+
post.id === post.threadId &&
|
|
262
|
+
display.collectionPinnedThreadIds?.has(post.threadId),
|
|
233
263
|
),
|
|
234
|
-
);
|
|
235
|
-
const rootView =
|
|
236
|
-
const selectedIds = selectedPostIdsByThread.get(root.id);
|
|
237
|
-
|
|
238
|
-
if (!rootView || !selectedIds || selectedIds.size === 0) {
|
|
239
|
-
return items;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const selectedIndices = postViews.reduce<number[]>(
|
|
243
|
-
(indices, post, index) => {
|
|
244
|
-
if (selectedIds.has(post.id)) {
|
|
245
|
-
indices.push(index);
|
|
246
|
-
}
|
|
247
|
-
return indices;
|
|
248
|
-
},
|
|
249
|
-
[],
|
|
250
|
-
);
|
|
264
|
+
}));
|
|
265
|
+
const rootView = renderedPosts[0]?.view;
|
|
251
266
|
|
|
252
|
-
if (
|
|
267
|
+
if (!rootView) {
|
|
253
268
|
return items;
|
|
254
269
|
}
|
|
255
270
|
|
|
256
|
-
const
|
|
257
|
-
const visibleIndices = [
|
|
258
|
-
...new Set([0, postViews.length - 1, ...selectedIndices]),
|
|
259
|
-
].sort((left, right) => left - right);
|
|
260
|
-
const segments = visibleIndices.reduce<
|
|
271
|
+
const segments = renderedPosts.reduce<
|
|
261
272
|
NonNullable<TimelineItemView["curatedThread"]>["segments"]
|
|
262
|
-
>((items,
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const previousIndex =
|
|
269
|
-
segmentIndex === 0 ? undefined : visibleIndices[segmentIndex - 1];
|
|
273
|
+
>((items, renderedPost, segmentIndex) => {
|
|
274
|
+
const previousPosition =
|
|
275
|
+
segmentIndex === 0
|
|
276
|
+
? undefined
|
|
277
|
+
: renderedPosts[segmentIndex - 1]?.position;
|
|
270
278
|
|
|
271
279
|
items.push({
|
|
272
|
-
post:
|
|
280
|
+
post: renderedPost.view,
|
|
273
281
|
hiddenBeforeCount:
|
|
274
|
-
|
|
275
|
-
?
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
282
|
+
previousPosition === undefined
|
|
283
|
+
? renderedPost.position
|
|
284
|
+
: renderedPost.position - previousPosition - 1,
|
|
285
|
+
highlighted:
|
|
286
|
+
thread.highlightedPostIds.has(renderedPost.view.id) ||
|
|
287
|
+
(display.highlightLatestPost === true &&
|
|
288
|
+
renderedPost.view.id === lastPostId),
|
|
280
289
|
});
|
|
281
290
|
|
|
282
291
|
return items;
|
|
@@ -286,10 +295,10 @@ async function buildCuratedThreadItems(
|
|
|
286
295
|
return items;
|
|
287
296
|
}
|
|
288
297
|
|
|
289
|
-
const
|
|
298
|
+
const isStandaloneRoot =
|
|
290
299
|
segments.length === 1 && segments[0]?.post.id === rootView.id;
|
|
291
300
|
|
|
292
|
-
if (
|
|
301
|
+
if (isStandaloneRoot) {
|
|
293
302
|
items.push({ post: rootView });
|
|
294
303
|
return items;
|
|
295
304
|
}
|
|
@@ -298,6 +307,7 @@ async function buildCuratedThreadItems(
|
|
|
298
307
|
post: rootView,
|
|
299
308
|
curatedThread: {
|
|
300
309
|
rootPost: rootView,
|
|
310
|
+
showContextRatings: display.showContextRatings,
|
|
301
311
|
segments,
|
|
302
312
|
},
|
|
303
313
|
});
|
|
@@ -429,36 +439,31 @@ export async function assembleFeaturedTimeline(
|
|
|
429
439
|
return { items: [], currentPage: page, totalPages, totalCount };
|
|
430
440
|
}
|
|
431
441
|
|
|
432
|
-
const
|
|
433
|
-
await c.var.services.posts.
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
selectedPostIdsByThread.set(threadId, new Set(selectedIds));
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const items = await buildCuratedThreadItems(
|
|
447
|
-
c,
|
|
448
|
-
rootIds,
|
|
449
|
-
threadsByRootId,
|
|
450
|
-
selectedPostIdsByThread,
|
|
442
|
+
const featuredThreads =
|
|
443
|
+
await c.var.services.posts.getFeaturedThreadTimelineData(rootIds);
|
|
444
|
+
const threadsByRootId = new Map<string, CuratedThreadSource>(
|
|
445
|
+
[...featuredThreads].map(([threadId, thread]) => [
|
|
446
|
+
threadId,
|
|
447
|
+
{
|
|
448
|
+
posts: thread.posts,
|
|
449
|
+
highlightedPostIds: new Set(thread.featuredPostIds),
|
|
450
|
+
},
|
|
451
|
+
]),
|
|
451
452
|
);
|
|
452
453
|
|
|
454
|
+
const items = await buildCuratedThreadItems(c, rootIds, threadsByRootId, {
|
|
455
|
+
showContextRatings: false,
|
|
456
|
+
});
|
|
457
|
+
|
|
453
458
|
return { items, currentPage: page, totalPages, totalCount };
|
|
454
459
|
}
|
|
455
460
|
|
|
456
461
|
/**
|
|
457
462
|
* Assembles a paginated collection timeline grouped by thread root.
|
|
458
463
|
*
|
|
459
|
-
* Threads are ordered by collection
|
|
460
|
-
*
|
|
461
|
-
*
|
|
464
|
+
* Threads are ordered by collection activity/rating semantics. Every published
|
|
465
|
+
* post in each matching Thread is rendered; Collection membership never selects
|
|
466
|
+
* or hides individual posts within the Thread.
|
|
462
467
|
*
|
|
463
468
|
* @param c - Hono context (provides services + appConfig)
|
|
464
469
|
* @param options - Collection IDs, optional page number, auth state, and sort
|
|
@@ -504,27 +509,28 @@ export async function assembleCollectionTimeline(
|
|
|
504
509
|
return { items: [], currentPage: page, totalPages, totalCount };
|
|
505
510
|
}
|
|
506
511
|
|
|
507
|
-
const [threadsByRootId,
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
),
|
|
514
|
-
c.var.services.collections.getPinnedPostIds(options.collectionIds),
|
|
515
|
-
]);
|
|
516
|
-
const selectedPostIdsByThread: CuratedThreadSelectionMap = new Map(
|
|
517
|
-
[...collectedPostIdsByThread.entries()].map(([threadId, postIds]) => [
|
|
512
|
+
const [threadsByRootId, pinnedThreadIds] = await Promise.all([
|
|
513
|
+
c.var.services.posts.getPublishedThreads(rootIds),
|
|
514
|
+
c.var.services.collections.getPinnedThreadIds(options.collectionIds),
|
|
515
|
+
]);
|
|
516
|
+
const curatedThreadsByRootId = new Map<string, CuratedThreadSource>(
|
|
517
|
+
[...threadsByRootId].map(([threadId, thread]) => [
|
|
518
518
|
threadId,
|
|
519
|
-
|
|
519
|
+
{
|
|
520
|
+
posts: thread.map((post, position) => ({ post, position })),
|
|
521
|
+
highlightedPostIds: new Set<string>(),
|
|
522
|
+
},
|
|
520
523
|
]),
|
|
521
524
|
);
|
|
522
525
|
const items = await buildCuratedThreadItems(
|
|
523
526
|
c,
|
|
524
527
|
rootIds,
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
+
curatedThreadsByRootId,
|
|
529
|
+
{
|
|
530
|
+
collectionPinnedThreadIds: pinnedThreadIds,
|
|
531
|
+
highlightLatestPost: true,
|
|
532
|
+
showContextRatings: true,
|
|
533
|
+
},
|
|
528
534
|
);
|
|
529
535
|
|
|
530
536
|
return { items, currentPage: page, totalPages, totalCount };
|
package/src/lib/timezones.ts
CHANGED
|
@@ -336,21 +336,28 @@ export const TIMEZONES: TimezoneEntry[] = [
|
|
|
336
336
|
},
|
|
337
337
|
];
|
|
338
338
|
|
|
339
|
-
function
|
|
340
|
-
return TIMEZONES.find(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
339
|
+
function findLegacyTimezoneEntry(value: string): TimezoneEntry | undefined {
|
|
340
|
+
return TIMEZONES.find((tz) => tz.legacyValues?.includes(value));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function resolveRuntimeTimeZone(value: string): string | null {
|
|
344
|
+
try {
|
|
345
|
+
return new Intl.DateTimeFormat("en-US", { timeZone: value })
|
|
346
|
+
.resolvedOptions()
|
|
347
|
+
.timeZone.trim();
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
346
351
|
}
|
|
347
352
|
|
|
348
353
|
/**
|
|
349
|
-
* Normalizes a timezone value from settings, env, or browser detection
|
|
350
|
-
*
|
|
354
|
+
* Normalizes a timezone value from settings, env, or browser detection. Any
|
|
355
|
+
* IANA name or fixed-offset identifier supported by the runtime is preserved;
|
|
356
|
+
* historical display labels are mapped to their curated replacement. Unknown
|
|
357
|
+
* values fall back to UTC.
|
|
351
358
|
*
|
|
352
359
|
* @param value - Timezone value to normalize
|
|
353
|
-
* @returns
|
|
360
|
+
* @returns Runtime-normalized timezone identifier or UTC
|
|
354
361
|
*
|
|
355
362
|
* @example
|
|
356
363
|
* ```ts
|
|
@@ -365,12 +372,17 @@ export function normalizeTimeZone(value: string | null | undefined): string {
|
|
|
365
372
|
return "UTC";
|
|
366
373
|
}
|
|
367
374
|
|
|
368
|
-
|
|
375
|
+
const runtimeTimeZone = resolveRuntimeTimeZone(trimmed);
|
|
376
|
+
if (runtimeTimeZone) {
|
|
377
|
+
return runtimeTimeZone;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return findLegacyTimezoneEntry(trimmed)?.value ?? "UTC";
|
|
369
381
|
}
|
|
370
382
|
|
|
371
383
|
/**
|
|
372
|
-
* Returns whether the
|
|
373
|
-
*
|
|
384
|
+
* Returns whether the runtime recognizes a timezone value or the app can map
|
|
385
|
+
* it from a historical display label.
|
|
374
386
|
*
|
|
375
387
|
* @param value - Timezone value to validate
|
|
376
388
|
* @returns `true` when the value can be normalized safely
|
|
@@ -381,7 +393,35 @@ export function isSupportedTimeZone(value: string | null | undefined): boolean {
|
|
|
381
393
|
return false;
|
|
382
394
|
}
|
|
383
395
|
|
|
384
|
-
return
|
|
396
|
+
return (
|
|
397
|
+
resolveRuntimeTimeZone(trimmed) !== null ||
|
|
398
|
+
findLegacyTimezoneEntry(trimmed) !== undefined
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Returns the curated settings options plus a valid current value that is not
|
|
404
|
+
* already represented. This keeps the UI compact without losing an IANA zone
|
|
405
|
+
* or fixed offset detected outside the curated list.
|
|
406
|
+
*
|
|
407
|
+
* @param currentValue - Current stored timezone value
|
|
408
|
+
* @returns Timezone options that can represent the current setting
|
|
409
|
+
*/
|
|
410
|
+
export function getTimeZoneOptions(currentValue: string): TimezoneEntry[] {
|
|
411
|
+
const normalized = normalizeTimeZone(currentValue);
|
|
412
|
+
if (TIMEZONES.some((timezone) => timezone.value === normalized)) {
|
|
413
|
+
return TIMEZONES;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return [
|
|
417
|
+
...TIMEZONES,
|
|
418
|
+
{
|
|
419
|
+
value: normalized,
|
|
420
|
+
label: normalized,
|
|
421
|
+
offset: "",
|
|
422
|
+
iana: [normalized],
|
|
423
|
+
},
|
|
424
|
+
];
|
|
385
425
|
}
|
|
386
426
|
|
|
387
427
|
/**
|