@jant/core 0.6.15 → 0.7.0
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/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +67 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +88 -1
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1142 -268
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1691 -12
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +88 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +93 -34
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +1021 -66
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +21 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-display.ts +42 -9
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +11 -2
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +507 -357
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +183 -19
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +139 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +330 -47
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +281 -24
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +401 -301
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +55 -23
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View language: which language's browsing surface a request is asking for.
|
|
3
|
+
*
|
|
4
|
+
* A multilingual site keeps its primary language at the root (`/`, `/archive`,
|
|
5
|
+
* `/feed`) and gives every other language a URL prefix (`/en`, `/zh-hant`).
|
|
6
|
+
* The prefix selects a *view* — it filters the timeline, feeds and collection
|
|
7
|
+
* pages to that language — and it does not change any post's own address.
|
|
8
|
+
*
|
|
9
|
+
* Everything a handler needs to stay inside its view goes through
|
|
10
|
+
* `toViewPath()`, so a page rendered at `/en` links to `/en?page=2` rather
|
|
11
|
+
* than throwing the reader back to the primary language on the second page.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Context } from "hono";
|
|
15
|
+
import type { Bindings } from "../types/bindings.js";
|
|
16
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
17
|
+
import type {
|
|
18
|
+
LanguageAlternate,
|
|
19
|
+
LanguageSwitcherOption,
|
|
20
|
+
} from "../types/views.js";
|
|
21
|
+
import { toLanguagePrefix } from "../i18n/locales.js";
|
|
22
|
+
import { getOrBuildEntry } from "../i18n/supported-locales.js";
|
|
23
|
+
import { isPerLanguageSurface } from "./per-language-surfaces.js";
|
|
24
|
+
import { toAbsoluteSiteUrl, toPublicPath } from "./url.js";
|
|
25
|
+
|
|
26
|
+
type ViewContext = Context<{ Bindings: Bindings; Variables: AppVariables }>;
|
|
27
|
+
|
|
28
|
+
export type { LanguageAlternate, LanguageSwitcherOption };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What the first path segment of a request means.
|
|
32
|
+
*
|
|
33
|
+
* - `pass` — not a language prefix; the request belongs to another route.
|
|
34
|
+
* - `redirect` — a language prefix that has no view right now (the primary
|
|
35
|
+
* language's own prefix, or any prefix left over from before multilingual
|
|
36
|
+
* was switched off). Old links and feed subscriptions keep working.
|
|
37
|
+
* - `view` — serve this language's view.
|
|
38
|
+
*/
|
|
39
|
+
export type LanguageViewDecision =
|
|
40
|
+
| { kind: "pass" }
|
|
41
|
+
| { kind: "redirect"; to: string }
|
|
42
|
+
| { kind: "view"; lang: string };
|
|
43
|
+
|
|
44
|
+
const PASS: LanguageViewDecision = { kind: "pass" };
|
|
45
|
+
|
|
46
|
+
/** First segment of an internal path, without its slashes. */
|
|
47
|
+
function firstSegment(path: string): string {
|
|
48
|
+
const rest = path.startsWith("/") ? path.slice(1) : path;
|
|
49
|
+
const slash = rest.indexOf("/");
|
|
50
|
+
return slash === -1 ? rest : rest.slice(0, slash);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The same path with its first segment removed, always rooted at `/`. */
|
|
54
|
+
function stripFirstSegment(path: string): string {
|
|
55
|
+
const rest = path.startsWith("/") ? path.slice(1) : path;
|
|
56
|
+
const slash = rest.indexOf("/");
|
|
57
|
+
return slash === -1 ? "/" : rest.slice(slash);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Query string of the current request, including `?`, or an empty string. */
|
|
61
|
+
function queryString(c: ViewContext): string {
|
|
62
|
+
const index = c.req.url.indexOf("?");
|
|
63
|
+
return index === -1 ? "" : c.req.url.slice(index);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Decide what the language prefix in the current URL means.
|
|
68
|
+
*
|
|
69
|
+
* A site that has never configured a second language gets `pass` for every
|
|
70
|
+
* request — its posts are free to use any first segment they like, including
|
|
71
|
+
* ones that happen to look like language tags (`/is`, `/it`, `/no`).
|
|
72
|
+
*
|
|
73
|
+
* @param c - Request context, after `withConfig()`
|
|
74
|
+
* @returns How the request should be handled
|
|
75
|
+
* @example
|
|
76
|
+
* // On a site serving zh-Hans (primary) and en:
|
|
77
|
+
* resolveLanguageView(c); // GET /en/archive → { kind: "view", lang: "en" }
|
|
78
|
+
* resolveLanguageView(c); // GET /zh-hans → { kind: "redirect", to: "/" }
|
|
79
|
+
* resolveLanguageView(c); // GET /hello → { kind: "pass" }
|
|
80
|
+
*/
|
|
81
|
+
export function resolveLanguageView(c: ViewContext): LanguageViewDecision {
|
|
82
|
+
const { siteLanguage, additionalLanguages, multilingualEnabled } =
|
|
83
|
+
c.var.appConfig;
|
|
84
|
+
|
|
85
|
+
const primaryPrefix = toLanguagePrefix(siteLanguage);
|
|
86
|
+
const configured = additionalLanguages.filter(
|
|
87
|
+
(tag) => toLanguagePrefix(tag) !== primaryPrefix,
|
|
88
|
+
);
|
|
89
|
+
if (configured.length === 0) return PASS;
|
|
90
|
+
|
|
91
|
+
const segment = firstSegment(c.req.path).toLowerCase();
|
|
92
|
+
if (!segment) return PASS;
|
|
93
|
+
|
|
94
|
+
const redirect = (): LanguageViewDecision => ({
|
|
95
|
+
kind: "redirect",
|
|
96
|
+
to: `${toPublicPath(
|
|
97
|
+
stripFirstSegment(c.req.path),
|
|
98
|
+
c.var.appConfig.sitePathPrefix,
|
|
99
|
+
)}${queryString(c)}`,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// The primary language is served from the root, so its own prefix is only
|
|
103
|
+
// ever an alias for it (§4.3).
|
|
104
|
+
if (segment === primaryPrefix) return redirect();
|
|
105
|
+
|
|
106
|
+
const match = configured.find((tag) => toLanguagePrefix(tag) === segment);
|
|
107
|
+
if (!match) return PASS;
|
|
108
|
+
|
|
109
|
+
return multilingualEnabled ? { kind: "view", lang: match } : redirect();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Every URL prefix a path on this site can arrive under.
|
|
114
|
+
*
|
|
115
|
+
* The site's own languages, the primary one included: its prefix has no view
|
|
116
|
+
* of its own but still redirects to the root, so `/zh-hans/hello` names the
|
|
117
|
+
* same page as `/hello`. Empty on a site with one language, where a first
|
|
118
|
+
* segment that looks like a language tag is just a post's slug.
|
|
119
|
+
*
|
|
120
|
+
* Use it to resolve an address the author pasted; requests themselves are
|
|
121
|
+
* decided by {@link resolveLanguageView}, which knows view from redirect.
|
|
122
|
+
*
|
|
123
|
+
* @param config - Site language settings
|
|
124
|
+
* @returns Prefixes without slashes, e.g. `["zh-hans", "en"]`
|
|
125
|
+
*/
|
|
126
|
+
export function languageUrlPrefixes(config: {
|
|
127
|
+
siteLanguage: string;
|
|
128
|
+
additionalLanguages: readonly string[];
|
|
129
|
+
}): string[] {
|
|
130
|
+
const primaryPrefix = toLanguagePrefix(config.siteLanguage);
|
|
131
|
+
const others = config.additionalLanguages
|
|
132
|
+
.map((tag) => toLanguagePrefix(tag))
|
|
133
|
+
.filter((prefix) => prefix !== primaryPrefix);
|
|
134
|
+
|
|
135
|
+
return others.length === 0 ? [] : [primaryPrefix, ...new Set(others)];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether the current URL carries a language prefix.
|
|
140
|
+
*
|
|
141
|
+
* Distinct from `getViewLang()`: the root URLs of a multilingual site show the
|
|
142
|
+
* primary language, so they *have* a view language while having no prefix.
|
|
143
|
+
*
|
|
144
|
+
* @param c - Request context
|
|
145
|
+
* @returns True when the request came in under `/en`-style URLs
|
|
146
|
+
*/
|
|
147
|
+
export function isPrefixedLanguageView(c: ViewContext): boolean {
|
|
148
|
+
return c.var.viewLang !== undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The language the current view shows, for filtering content.
|
|
153
|
+
*
|
|
154
|
+
* On a multilingual site the root is the primary language's view, so this
|
|
155
|
+
* returns the primary language there rather than "no filter" — otherwise `/`
|
|
156
|
+
* would show every language while `/en` showed one, and the primary language
|
|
157
|
+
* would have no view of its own.
|
|
158
|
+
*
|
|
159
|
+
* @param c - Request context
|
|
160
|
+
* @returns Canonical BCP 47 tag, or `null` when the site is single-language
|
|
161
|
+
*/
|
|
162
|
+
export function getViewLang(c: ViewContext): string | null {
|
|
163
|
+
if (c.var.viewLang) return c.var.viewLang;
|
|
164
|
+
|
|
165
|
+
const { siteLanguage, additionalLanguages, multilingualEnabled } =
|
|
166
|
+
c.var.appConfig;
|
|
167
|
+
if (!multilingualEnabled) return null;
|
|
168
|
+
|
|
169
|
+
const primaryPrefix = toLanguagePrefix(siteLanguage);
|
|
170
|
+
const hasSecondLanguage = additionalLanguages.some(
|
|
171
|
+
(tag) => toLanguagePrefix(tag) !== primaryPrefix,
|
|
172
|
+
);
|
|
173
|
+
return hasSecondLanguage ? siteLanguage : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* URL prefix of the current view, for building in-view links.
|
|
178
|
+
*
|
|
179
|
+
* @param c - Request context
|
|
180
|
+
* @returns `/en`-style prefix, or an empty string on the root view
|
|
181
|
+
*/
|
|
182
|
+
export function viewBasePath(c: ViewContext): string {
|
|
183
|
+
const lang = c.var.viewLang;
|
|
184
|
+
return lang ? `/${toLanguagePrefix(lang)}` : "";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* URL prefix of the view a given language is served under.
|
|
189
|
+
*
|
|
190
|
+
* A post page belongs to its own language's site: its chrome — logo, nav,
|
|
191
|
+
* search — links into that language's view, so a reader on a Japanese post
|
|
192
|
+
* stays among Japanese surfaces. Post pages render at language-neutral URLs,
|
|
193
|
+
* so this is derived from the post's language rather than the request path.
|
|
194
|
+
*
|
|
195
|
+
* @param c - Request context
|
|
196
|
+
* @param lang - Content language of the page, or null/undefined when unknown
|
|
197
|
+
* @returns `/ja`-style prefix for an active non-primary language, otherwise
|
|
198
|
+
* an empty string (the root view)
|
|
199
|
+
* @example
|
|
200
|
+
* languageScopeBasePath(c, "ja"); // "/ja" on a zh-Hans + ja site
|
|
201
|
+
* languageScopeBasePath(c, "zh-Hans"); // "" — the primary lives at the root
|
|
202
|
+
*/
|
|
203
|
+
export function languageScopeBasePath(
|
|
204
|
+
c: ViewContext,
|
|
205
|
+
lang: string | null | undefined,
|
|
206
|
+
): string {
|
|
207
|
+
if (!lang) return "";
|
|
208
|
+
const { siteLanguage } = c.var.appConfig;
|
|
209
|
+
const prefix = toLanguagePrefix(lang);
|
|
210
|
+
if (prefix === toLanguagePrefix(siteLanguage)) return "";
|
|
211
|
+
const active = getViewLanguages(c).some(
|
|
212
|
+
(tag) => toLanguagePrefix(tag) === prefix,
|
|
213
|
+
);
|
|
214
|
+
return active ? `/${prefix}` : "";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Build a public path that stays inside the current language view.
|
|
219
|
+
*
|
|
220
|
+
* Use this for every in-page link a handler generates — pagination, filter
|
|
221
|
+
* chips, feed autodiscovery, JSON-LD. Post permalinks are deliberately *not*
|
|
222
|
+
* view-scoped: a post has one address whatever view it was reached from.
|
|
223
|
+
*
|
|
224
|
+
* @param c - Request context
|
|
225
|
+
* @param path - Internal app path such as `/archive`
|
|
226
|
+
* @returns Public path, prefixed by the view language and the site path prefix
|
|
227
|
+
* @example
|
|
228
|
+
* toViewPath(c, "/"); // "/" at the root, "/en" under the English view
|
|
229
|
+
* toViewPath(c, "/archive"); // "/archive" or "/en/archive"
|
|
230
|
+
*/
|
|
231
|
+
export function toViewPath(c: ViewContext, path = "/"): string {
|
|
232
|
+
const base = viewBasePath(c);
|
|
233
|
+
const { sitePathPrefix } = c.var.appConfig;
|
|
234
|
+
if (!base) return toPublicPath(path, sitePathPrefix);
|
|
235
|
+
const suffix = path === "/" || path === "" ? "" : path;
|
|
236
|
+
return toPublicPath(`${base}${suffix}`, sitePathPrefix);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The request path with the language prefix removed.
|
|
241
|
+
*
|
|
242
|
+
* Handlers that resolve a path against the site's own content — the
|
|
243
|
+
* `path_registry` catch-all above all — work on this, so `/en/hello` and
|
|
244
|
+
* `/hello` resolve to the same row.
|
|
245
|
+
*
|
|
246
|
+
* @param c - Request context
|
|
247
|
+
* @returns Internal path rooted at `/`, without the view prefix
|
|
248
|
+
*/
|
|
249
|
+
export function viewRelativePath(c: ViewContext): string {
|
|
250
|
+
return c.var.viewLang ? stripFirstSegment(c.req.path) : c.req.path;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// The table of per-language surfaces lives in its own module so the client can
|
|
254
|
+
// import it too — a link built in the browser needs the same answer.
|
|
255
|
+
export { isPerLanguageSurface };
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Build a path inside an arbitrary language's view.
|
|
259
|
+
*
|
|
260
|
+
* Used by the language switcher, which links out of the current view.
|
|
261
|
+
*
|
|
262
|
+
* @param c - Request context
|
|
263
|
+
* @param lang - Target language tag, or `null` for the primary view
|
|
264
|
+
* @param path - Internal app path such as `/archive`
|
|
265
|
+
* @returns Public path in that language's view
|
|
266
|
+
*/
|
|
267
|
+
export function toLanguagePath(
|
|
268
|
+
c: ViewContext,
|
|
269
|
+
lang: string | null,
|
|
270
|
+
path = "/",
|
|
271
|
+
): string {
|
|
272
|
+
const { sitePathPrefix, siteLanguage } = c.var.appConfig;
|
|
273
|
+
const isPrimary =
|
|
274
|
+
!lang || toLanguagePrefix(lang) === toLanguagePrefix(siteLanguage);
|
|
275
|
+
if (isPrimary) return toPublicPath(path, sitePathPrefix);
|
|
276
|
+
const suffix = path === "/" || path === "" ? "" : path;
|
|
277
|
+
return toPublicPath(`/${toLanguagePrefix(lang)}${suffix}`, sitePathPrefix);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Every language the site serves a view for, primary first.
|
|
282
|
+
*
|
|
283
|
+
* @param c - Request context
|
|
284
|
+
* @returns Canonical tags, or an empty array when multilingual is off
|
|
285
|
+
*/
|
|
286
|
+
export function getViewLanguages(c: ViewContext): string[] {
|
|
287
|
+
const { siteLanguage, additionalLanguages, multilingualEnabled } =
|
|
288
|
+
c.var.appConfig;
|
|
289
|
+
if (!multilingualEnabled) return [];
|
|
290
|
+
const primaryPrefix = toLanguagePrefix(siteLanguage);
|
|
291
|
+
const rest = additionalLanguages.filter(
|
|
292
|
+
(tag) => toLanguagePrefix(tag) !== primaryPrefix,
|
|
293
|
+
);
|
|
294
|
+
return rest.length === 0 ? [] : [siteLanguage, ...rest];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The site's languages as the composer offers them.
|
|
299
|
+
*
|
|
300
|
+
* Empty on a single-language site, which is what makes the composer show no
|
|
301
|
+
* language UI at all — an author who never turned multilingual content on
|
|
302
|
+
* should never meet it.
|
|
303
|
+
*
|
|
304
|
+
* @param c - Request context
|
|
305
|
+
* @returns Tag and native label per language, primary first
|
|
306
|
+
*/
|
|
307
|
+
export function buildComposeLanguages(
|
|
308
|
+
c: ViewContext,
|
|
309
|
+
): Array<{ tag: string; label: string }> {
|
|
310
|
+
return getViewLanguages(c).map((tag) => ({
|
|
311
|
+
tag,
|
|
312
|
+
label: getOrBuildEntry(tag).native,
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Build the site's language switcher for the current page.
|
|
318
|
+
*
|
|
319
|
+
* The switcher means "take me to this language's site", not "translate this
|
|
320
|
+
* page": from a list surface it goes to that surface in the other language,
|
|
321
|
+
* from a post it goes to the translation when one exists, and from anywhere
|
|
322
|
+
* without a counterpart in that language — a post with no translation, a
|
|
323
|
+
* settings page — it goes to that language's home. It is never disabled and
|
|
324
|
+
* never warns: a reader who lands on a language's home page has still
|
|
325
|
+
* arrived somewhere, and it never links to a 404.
|
|
326
|
+
*
|
|
327
|
+
* @param c - Request context
|
|
328
|
+
* @param options - Per-language destinations, and where to go without one
|
|
329
|
+
* @returns Switcher entries in configured order, or an empty array when the
|
|
330
|
+
* site serves one language
|
|
331
|
+
* @example
|
|
332
|
+
* buildLanguageSwitcher(c); // list surfaces: same path, each language
|
|
333
|
+
* buildLanguageSwitcher(c, {
|
|
334
|
+
* hrefByLanguage: new Map([["en", "/my-post"]]),
|
|
335
|
+
* fallbackPath: "/",
|
|
336
|
+
* }); // a post: its translations, else that language's home
|
|
337
|
+
*/
|
|
338
|
+
export function buildLanguageSwitcher(
|
|
339
|
+
c: ViewContext,
|
|
340
|
+
options: {
|
|
341
|
+
hrefByLanguage?: ReadonlyMap<string, string>;
|
|
342
|
+
fallbackPath?: string;
|
|
343
|
+
/** Language to mark as current. Defaults to the view's own language. */
|
|
344
|
+
currentLang?: string;
|
|
345
|
+
} = {},
|
|
346
|
+
): LanguageSwitcherOption[] {
|
|
347
|
+
const languages = getViewLanguages(c);
|
|
348
|
+
if (languages.length === 0) return [];
|
|
349
|
+
|
|
350
|
+
const currentPath = viewRelativePath(c);
|
|
351
|
+
const fallbackPath =
|
|
352
|
+
options.fallbackPath ??
|
|
353
|
+
(isPerLanguageSurface(currentPath) ? currentPath : "/");
|
|
354
|
+
const currentPrefix = toLanguagePrefix(
|
|
355
|
+
options.currentLang || getViewLang(c) || c.var.appConfig.siteLanguage,
|
|
356
|
+
);
|
|
357
|
+
const primaryPrefix = toLanguagePrefix(c.var.appConfig.siteLanguage);
|
|
358
|
+
|
|
359
|
+
return languages.map((lang) => {
|
|
360
|
+
const override = options.hrefByLanguage?.get(lang);
|
|
361
|
+
return {
|
|
362
|
+
lang,
|
|
363
|
+
label: getOrBuildEntry(lang).native,
|
|
364
|
+
href: override
|
|
365
|
+
? toPublicPath(override, c.var.appConfig.sitePathPrefix)
|
|
366
|
+
: toLanguagePath(c, lang, fallbackPath),
|
|
367
|
+
isCurrent: toLanguagePrefix(lang) === currentPrefix,
|
|
368
|
+
isPrimary: toLanguagePrefix(lang) === primaryPrefix,
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* `hreflang` alternates for the surface the current request is rendering.
|
|
375
|
+
*
|
|
376
|
+
* List surfaces exist once per language at the same path, so every language's
|
|
377
|
+
* copy of the current path is an alternate of it.
|
|
378
|
+
*
|
|
379
|
+
* An `x-default` alternate pointing at the primary language closes the set,
|
|
380
|
+
* so a reader whose language the site does not publish lands somewhere
|
|
381
|
+
* deliberate. Surfaces where it has no defined meaning — Atom feeds — opt out
|
|
382
|
+
* with `xDefault: false`.
|
|
383
|
+
*
|
|
384
|
+
* Returns an empty array on a single-language site, and when the site has no
|
|
385
|
+
* absolute URL configured — an hreflang `href` must be absolute to be honoured.
|
|
386
|
+
*
|
|
387
|
+
* The request's own query string rides along by default, so a filtered surface
|
|
388
|
+
* points at the same filter in each language. A page that also emits a
|
|
389
|
+
* `rel="canonical"` must pass `query` instead: every member of an hreflang set
|
|
390
|
+
* has to be a canonical URL, so the self-referential alternate cannot carry
|
|
391
|
+
* params the canonical strips — the two tags would give one page two
|
|
392
|
+
* identities.
|
|
393
|
+
*
|
|
394
|
+
* @param c - Request context
|
|
395
|
+
* @param options - `query` overrides the request's own query string, leading
|
|
396
|
+
* `?` included; `xDefault: false` omits the trailing `x-default` entry
|
|
397
|
+
* @returns Alternates for this surface, primary first
|
|
398
|
+
* @example
|
|
399
|
+
* // Rendering /en/archive on a zh-Hans + en site:
|
|
400
|
+
* buildSurfaceAlternates(c);
|
|
401
|
+
* // [{hreflang: "zh-Hans", href: "https://…/archive"},
|
|
402
|
+
* // {hreflang: "en", href: "https://…/en/archive"},
|
|
403
|
+
* // {hreflang: "x-default", href: "https://…/archive"}]
|
|
404
|
+
* @example
|
|
405
|
+
* // On /archive?utm_source=nl, whose canonical is /archive:
|
|
406
|
+
* buildSurfaceAlternates(c, { query: "" });
|
|
407
|
+
*/
|
|
408
|
+
export function buildSurfaceAlternates(
|
|
409
|
+
c: ViewContext,
|
|
410
|
+
options?: { query?: string; xDefault?: boolean },
|
|
411
|
+
): LanguageAlternate[] {
|
|
412
|
+
const languages = getViewLanguages(c);
|
|
413
|
+
const { siteUrl, sitePathPrefix } = c.var.appConfig;
|
|
414
|
+
if (languages.length === 0 || !siteUrl) return [];
|
|
415
|
+
|
|
416
|
+
const path = viewRelativePath(c);
|
|
417
|
+
const query = options?.query ?? queryString(c);
|
|
418
|
+
const primaryPrefix = toLanguagePrefix(c.var.appConfig.siteLanguage);
|
|
419
|
+
|
|
420
|
+
const alternates = languages.map((lang) => {
|
|
421
|
+
const prefix = toLanguagePrefix(lang);
|
|
422
|
+
const internal =
|
|
423
|
+
prefix === primaryPrefix ? path : `/${prefix}${path === "/" ? "" : path}`;
|
|
424
|
+
return {
|
|
425
|
+
hreflang: lang,
|
|
426
|
+
href: `${toAbsoluteSiteUrl(internal, siteUrl, sitePathPrefix)}${query}`,
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// A reader whose language the site does not publish lands on the primary one.
|
|
431
|
+
const primary = alternates[0];
|
|
432
|
+
if (primary && options?.xDefault !== false) {
|
|
433
|
+
alternates.push({ hreflang: "x-default", href: primary.href });
|
|
434
|
+
}
|
|
435
|
+
return alternates;
|
|
436
|
+
}
|
package/src/lib/view.ts
CHANGED
|
@@ -32,10 +32,19 @@ import {
|
|
|
32
32
|
formatDate,
|
|
33
33
|
formatTime,
|
|
34
34
|
formatRelativeTime,
|
|
35
|
+
formatYearMonthLabel,
|
|
35
36
|
} from "./time.js";
|
|
36
37
|
import { getCollectionPagePath } from "./collection-paths.js";
|
|
37
38
|
import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
38
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
extractSummaryHtml,
|
|
41
|
+
extractBodyText,
|
|
42
|
+
ARTICLE_SUMMARY_MAX_BLOCKS,
|
|
43
|
+
ARTICLE_SUMMARY_MAX_CHARS,
|
|
44
|
+
NOTE_SUMMARY_MAX_BLOCKS,
|
|
45
|
+
NOTE_SUMMARY_MAX_CHARS,
|
|
46
|
+
NOTE_SUMMARY_MIN_HIDDEN_CHARS,
|
|
47
|
+
} from "./summary.js";
|
|
39
48
|
import { renderTiptapDocumentAroundBoundary } from "./tiptap-render.js";
|
|
40
49
|
import { highlightText } from "./search-snippet.js";
|
|
41
50
|
import { isFullUrl, toPublicPath, toSameSitePath } from "./url.js";
|
|
@@ -140,15 +149,6 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
|
|
|
140
149
|
// Post Conversions
|
|
141
150
|
// =============================================================================
|
|
142
151
|
|
|
143
|
-
/** Feed summary limits for titled, article-style posts (the excerpt is a teaser). */
|
|
144
|
-
const ARTICLE_SUMMARY_MAX_BLOCKS = 5;
|
|
145
|
-
const ARTICLE_SUMMARY_MAX_CHARS = 500;
|
|
146
|
-
/** Larger feed summary limits for untitled notes — the body itself is the content. */
|
|
147
|
-
const NOTE_SUMMARY_MAX_BLOCKS = 10;
|
|
148
|
-
const NOTE_SUMMARY_MAX_CHARS = 1500;
|
|
149
|
-
/** Don't truncate an untitled note just to hide a tail under this many chars. */
|
|
150
|
-
const NOTE_SUMMARY_MIN_HIDDEN_CHARS = 200;
|
|
151
|
-
|
|
152
152
|
/**
|
|
153
153
|
* Splice a zero-width marker into rendered body HTML at a summary boundary.
|
|
154
154
|
*
|
|
@@ -401,11 +401,42 @@ export function toPostView(
|
|
|
401
401
|
collections,
|
|
402
402
|
replyToId: post.replyToId ?? undefined,
|
|
403
403
|
threadRootId: post.replyToId ? post.threadId : undefined,
|
|
404
|
+
// Language is a Thread property, carried on the root. Repeating it on every
|
|
405
|
+
// reply would invite per-reply controls that the data model does not allow.
|
|
406
|
+
language: post.replyToId ? undefined : (post.language ?? undefined),
|
|
404
407
|
isLastInThread: isLastInThread ?? true,
|
|
405
408
|
body: post.body ?? undefined,
|
|
406
409
|
};
|
|
407
410
|
}
|
|
408
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Resolves the unpublished draft sitting at the end of a Thread, if any.
|
|
414
|
+
*
|
|
415
|
+
* Both maps come from the same ordering over the same rows, differing only in
|
|
416
|
+
* whether drafts are eligible — so when they disagree, the draft-inclusive
|
|
417
|
+
* answer is by definition unpublished. No status lookup needed.
|
|
418
|
+
*
|
|
419
|
+
* @param threadId - Thread root ID to look up
|
|
420
|
+
* @param publishedTails - Tails from `getThreadTailIds(ids)`
|
|
421
|
+
* @param draftInclusiveTails - Tails from `getThreadTailIds(ids, { includeDrafts: true })`
|
|
422
|
+
* @returns The trailing draft's Post ID, or undefined when the Thread ends
|
|
423
|
+
* on a published Post
|
|
424
|
+
* @example
|
|
425
|
+
* ```ts
|
|
426
|
+
* const draftTailId = resolveDraftTailId(post.threadId, tails, draftTails);
|
|
427
|
+
* if (draftTailId) view.draftTailId = draftTailId;
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
export function resolveDraftTailId(
|
|
431
|
+
threadId: string,
|
|
432
|
+
publishedTails: Map<string, string>,
|
|
433
|
+
draftInclusiveTails: Map<string, string>,
|
|
434
|
+
): string | undefined {
|
|
435
|
+
const withDrafts = draftInclusiveTails.get(threadId);
|
|
436
|
+
if (!withDrafts) return undefined;
|
|
437
|
+
return withDrafts === publishedTails.get(threadId) ? undefined : withDrafts;
|
|
438
|
+
}
|
|
439
|
+
|
|
409
440
|
/**
|
|
410
441
|
* Batch converts PostWithMedia[] to PostView[].
|
|
411
442
|
*
|
|
@@ -479,6 +510,31 @@ export function toPostViewsFromPosts(
|
|
|
479
510
|
* @param currentPath - Current URL path for active state
|
|
480
511
|
* @param isAuthenticated - Whether the user is logged in (affects system settings item)
|
|
481
512
|
*/
|
|
513
|
+
/**
|
|
514
|
+
* System nav destinations that exist once per language view.
|
|
515
|
+
*
|
|
516
|
+
* `settings` is absent on purpose: the dashboard is a single, language-neutral
|
|
517
|
+
* surface, so a reader browsing `/en` still signs in at `/signin`.
|
|
518
|
+
*/
|
|
519
|
+
const LANGUAGE_SCOPED_SYSTEM_KEYS = new Set<string>([
|
|
520
|
+
"latest",
|
|
521
|
+
"featured",
|
|
522
|
+
"collections",
|
|
523
|
+
"archive",
|
|
524
|
+
"rss",
|
|
525
|
+
]);
|
|
526
|
+
|
|
527
|
+
/** Whether a nav item points at a surface that a language view re-serves. */
|
|
528
|
+
function isLanguageScopedNavItem(item: NavItem): boolean {
|
|
529
|
+
if (item.type === "system") {
|
|
530
|
+
return LANGUAGE_SCOPED_SYSTEM_KEYS.has(item.systemKey ?? "");
|
|
531
|
+
}
|
|
532
|
+
// Collections keep their slug but filter by language; posts and free-form
|
|
533
|
+
// links have one address each. A smart collection is a collection in this
|
|
534
|
+
// respect too — `/en/quotes` is the English half of the same conditions.
|
|
535
|
+
return item.type === "collection" || item.type === "smart_collection";
|
|
536
|
+
}
|
|
537
|
+
|
|
482
538
|
export function toNavItemView(
|
|
483
539
|
item: NavItem,
|
|
484
540
|
currentPath: string,
|
|
@@ -486,6 +542,7 @@ export function toNavItemView(
|
|
|
486
542
|
sitePathPrefix = "",
|
|
487
543
|
collectionFreshness?: Map<string, number>,
|
|
488
544
|
siteOrigin = "",
|
|
545
|
+
basePath = sitePathPrefix,
|
|
489
546
|
): NavItemView {
|
|
490
547
|
let url = item.url;
|
|
491
548
|
let label = item.label;
|
|
@@ -518,14 +575,15 @@ export function toNavItemView(
|
|
|
518
575
|
// so strip it back to a path and skip external-link affordances.
|
|
519
576
|
const sameSitePath = toSameSitePath(url, siteOrigin);
|
|
520
577
|
const isExternal = sameSitePath === null && isFullUrl(url);
|
|
578
|
+
const linkPrefix = isLanguageScopedNavItem(item) ? basePath : sitePathPrefix;
|
|
521
579
|
const publicUrl = isExternal
|
|
522
580
|
? url
|
|
523
|
-
: toPublicPath(sameSitePath ?? url,
|
|
581
|
+
: toPublicPath(sameSitePath ?? url, linkPrefix);
|
|
524
582
|
|
|
525
583
|
let isActive = false;
|
|
526
584
|
if (!isExternal) {
|
|
527
|
-
if (publicUrl ===
|
|
528
|
-
isActive = currentPath === (
|
|
585
|
+
if (publicUrl === linkPrefix || publicUrl === "/") {
|
|
586
|
+
isActive = currentPath === (linkPrefix || "/");
|
|
529
587
|
} else {
|
|
530
588
|
isActive =
|
|
531
589
|
currentPath === publicUrl || currentPath.startsWith(`${publicUrl}/`);
|
|
@@ -543,9 +601,11 @@ export function toNavItemView(
|
|
|
543
601
|
type: item.type as NavItemType,
|
|
544
602
|
systemKey: item.systemKey,
|
|
545
603
|
collectionId: item.collectionId,
|
|
604
|
+
smartCollectionId: item.smartCollectionId,
|
|
546
605
|
postId: item.postId,
|
|
547
606
|
label,
|
|
548
607
|
url: publicUrl,
|
|
608
|
+
targetTitle: item.targetTitle,
|
|
549
609
|
placement: item.placement as NavItemPlacement,
|
|
550
610
|
isActive,
|
|
551
611
|
isExternal,
|
|
@@ -568,6 +628,7 @@ export function toNavItemViews(
|
|
|
568
628
|
sitePathPrefix = "",
|
|
569
629
|
collectionFreshness?: Map<string, number>,
|
|
570
630
|
siteOrigin = "",
|
|
631
|
+
basePath = sitePathPrefix,
|
|
571
632
|
): NavItemView[] {
|
|
572
633
|
return items.map((item) =>
|
|
573
634
|
toNavItemView(
|
|
@@ -577,6 +638,7 @@ export function toNavItemViews(
|
|
|
577
638
|
sitePathPrefix,
|
|
578
639
|
collectionFreshness,
|
|
579
640
|
siteOrigin,
|
|
641
|
+
basePath,
|
|
580
642
|
),
|
|
581
643
|
);
|
|
582
644
|
}
|
|
@@ -675,11 +737,8 @@ export function toArchiveGroups(
|
|
|
675
737
|
const [year, month] = yearMonth.split("-");
|
|
676
738
|
if (!year || !month) continue;
|
|
677
739
|
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
year: "numeric",
|
|
681
|
-
month: "long",
|
|
682
|
-
});
|
|
740
|
+
const label = formatYearMonthLabel(yearMonth);
|
|
741
|
+
if (!label) continue;
|
|
683
742
|
|
|
684
743
|
groups.push({
|
|
685
744
|
year,
|
|
@@ -709,11 +768,8 @@ export function toArchiveGroupsWithMedia(
|
|
|
709
768
|
const [year, month] = yearMonth.split("-");
|
|
710
769
|
if (!year || !month) continue;
|
|
711
770
|
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
year: "numeric",
|
|
715
|
-
month: "long",
|
|
716
|
-
});
|
|
771
|
+
const label = formatYearMonthLabel(yearMonth);
|
|
772
|
+
if (!label) continue;
|
|
717
773
|
|
|
718
774
|
groups.push({
|
|
719
775
|
year,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scoped viewer state for Hono JSX renders.
|
|
3
|
+
*
|
|
4
|
+
* Whether the visitor is the signed-in author decides what a Post card should
|
|
5
|
+
* even emit. The post menu trigger, the reply trigger, and three of the four
|
|
6
|
+
* status badges only ever become visible under `body[data-authenticated]` or
|
|
7
|
+
* an article state an anonymous reader cannot reach, so for a logged-out
|
|
8
|
+
* visitor they are markup that can never be seen — several hundred DOM nodes
|
|
9
|
+
* of invisible chrome on a full timeline, paid for in style and layout on
|
|
10
|
+
* every load.
|
|
11
|
+
*
|
|
12
|
+
* The flag is bound once per render, alongside the i18n instance and from the
|
|
13
|
+
* same source (`c.var.isAuthenticated`), rather than threaded through the six
|
|
14
|
+
* component layers between a page and `PostFooter`. It must stay that same
|
|
15
|
+
* flag: `BaseLayout` writes it into `data-authenticated`, and a gate that
|
|
16
|
+
* disagrees with the CSS reveal either hides the author's own controls or
|
|
17
|
+
* brings the dead markup back.
|
|
18
|
+
*
|
|
19
|
+
* Renders are synchronous and single-threaded per request, so a module-level
|
|
20
|
+
* binding is safe here — the same reasoning the i18n context runs on.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether the current render is for the signed-in author.
|
|
25
|
+
*
|
|
26
|
+
* Defaults to `false` so a render that never bound a viewer emits the reader's
|
|
27
|
+
* markup. That is the safe direction: the author's controls are hidden until
|
|
28
|
+
* something says otherwise, rather than leaking into an anonymous page.
|
|
29
|
+
*/
|
|
30
|
+
let currentIsAuthor = false;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Bind the viewer for the render that is about to run.
|
|
34
|
+
*
|
|
35
|
+
* Called by the render provider, not by components.
|
|
36
|
+
*
|
|
37
|
+
* @param isAuthor - Whether this request carries the author's session
|
|
38
|
+
* @returns Nothing
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* bindViewer(c.get("isAuthenticated") === true);
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function bindViewer(isAuthor: boolean): void {
|
|
46
|
+
currentIsAuthor = isAuthor;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read the current render's viewer.
|
|
51
|
+
*
|
|
52
|
+
* @returns `isAuthor` — whether the signed-in author is looking at this page
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* const { isAuthor } = useViewer();
|
|
57
|
+
* return isAuthor ? <PostMenuTriggerButton /> : null;
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function useViewer(): { isAuthor: boolean } {
|
|
61
|
+
return { isAuthor: currentIsAuthor };
|
|
62
|
+
}
|