@jant/core 0.3.35 → 0.3.37
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/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -140
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +57 -31
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
package/src/lib/storage.ts
CHANGED
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Bindings } from "../types.js";
|
|
8
8
|
|
|
9
|
+
/** Tracks an in-progress multipart upload */
|
|
10
|
+
export interface MultipartUploadSession {
|
|
11
|
+
uploadId: string;
|
|
12
|
+
key: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Represents a successfully uploaded part */
|
|
16
|
+
export interface UploadedPart {
|
|
17
|
+
partNumber: number;
|
|
18
|
+
etag: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
/**
|
|
10
22
|
* Common interface for storage operations.
|
|
11
23
|
*
|
|
@@ -20,13 +32,68 @@ export interface StorageDriver {
|
|
|
20
32
|
opts?: { contentType?: string },
|
|
21
33
|
): Promise<void>;
|
|
22
34
|
|
|
23
|
-
/** Retrieve a file from storage. Returns null if not found. */
|
|
35
|
+
/** Retrieve a file (or byte range) from storage. Returns null if not found. */
|
|
24
36
|
get(
|
|
25
37
|
key: string,
|
|
26
|
-
|
|
38
|
+
opts?: { range?: { offset: number; length: number } },
|
|
39
|
+
): Promise<{
|
|
40
|
+
body: ReadableStream;
|
|
41
|
+
contentType?: string;
|
|
42
|
+
size?: number;
|
|
43
|
+
} | null>;
|
|
27
44
|
|
|
28
45
|
/** Delete a file from storage */
|
|
29
46
|
delete(key: string): Promise<void>;
|
|
47
|
+
|
|
48
|
+
/** Start a multipart upload (optional — R2 only) */
|
|
49
|
+
createMultipartUpload?(
|
|
50
|
+
key: string,
|
|
51
|
+
opts?: { contentType?: string },
|
|
52
|
+
): Promise<MultipartUploadSession>;
|
|
53
|
+
|
|
54
|
+
/** Upload a single part of a multipart upload */
|
|
55
|
+
uploadPart?(
|
|
56
|
+
key: string,
|
|
57
|
+
uploadId: string,
|
|
58
|
+
partNumber: number,
|
|
59
|
+
body: ReadableStream | ArrayBuffer | Uint8Array,
|
|
60
|
+
): Promise<UploadedPart>;
|
|
61
|
+
|
|
62
|
+
/** Finalize a multipart upload by combining all parts */
|
|
63
|
+
completeMultipartUpload?(
|
|
64
|
+
key: string,
|
|
65
|
+
uploadId: string,
|
|
66
|
+
parts: UploadedPart[],
|
|
67
|
+
): Promise<void>;
|
|
68
|
+
|
|
69
|
+
/** Cancel a multipart upload and discard uploaded parts */
|
|
70
|
+
abortMultipartUpload?(key: string, uploadId: string): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Type guard that checks whether a storage driver supports multipart uploads.
|
|
75
|
+
*
|
|
76
|
+
* @param driver - The storage driver to check
|
|
77
|
+
* @returns true if all multipart methods are present
|
|
78
|
+
*/
|
|
79
|
+
export function supportsMultipart(
|
|
80
|
+
driver: StorageDriver,
|
|
81
|
+
): driver is StorageDriver &
|
|
82
|
+
Required<
|
|
83
|
+
Pick<
|
|
84
|
+
StorageDriver,
|
|
85
|
+
| "createMultipartUpload"
|
|
86
|
+
| "uploadPart"
|
|
87
|
+
| "completeMultipartUpload"
|
|
88
|
+
| "abortMultipartUpload"
|
|
89
|
+
>
|
|
90
|
+
> {
|
|
91
|
+
return (
|
|
92
|
+
typeof driver.createMultipartUpload === "function" &&
|
|
93
|
+
typeof driver.uploadPart === "function" &&
|
|
94
|
+
typeof driver.completeMultipartUpload === "function" &&
|
|
95
|
+
typeof driver.abortMultipartUpload === "function"
|
|
96
|
+
);
|
|
30
97
|
}
|
|
31
98
|
|
|
32
99
|
/**
|
|
@@ -45,18 +112,47 @@ export function createR2Driver(r2: R2Bucket): StorageDriver {
|
|
|
45
112
|
});
|
|
46
113
|
},
|
|
47
114
|
|
|
48
|
-
async get(key) {
|
|
49
|
-
const object = await r2.get(
|
|
115
|
+
async get(key, opts) {
|
|
116
|
+
const object = await r2.get(
|
|
117
|
+
key,
|
|
118
|
+
opts?.range ? { range: opts.range } : undefined,
|
|
119
|
+
);
|
|
50
120
|
if (!object) return null;
|
|
51
121
|
return {
|
|
52
122
|
body: object.body,
|
|
53
123
|
contentType: object.httpMetadata?.contentType ?? undefined,
|
|
124
|
+
size: object.size,
|
|
54
125
|
};
|
|
55
126
|
},
|
|
56
127
|
|
|
57
128
|
async delete(key) {
|
|
58
129
|
await r2.delete(key);
|
|
59
130
|
},
|
|
131
|
+
|
|
132
|
+
async createMultipartUpload(key, opts) {
|
|
133
|
+
const upload = await r2.createMultipartUpload(key, {
|
|
134
|
+
httpMetadata: opts?.contentType
|
|
135
|
+
? { contentType: opts.contentType }
|
|
136
|
+
: undefined,
|
|
137
|
+
});
|
|
138
|
+
return { uploadId: upload.uploadId, key: upload.key };
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
async uploadPart(key, uploadId, partNumber, body) {
|
|
142
|
+
const upload = r2.resumeMultipartUpload(key, uploadId);
|
|
143
|
+
const part = await upload.uploadPart(partNumber, body);
|
|
144
|
+
return { partNumber: part.partNumber, etag: part.etag };
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
async completeMultipartUpload(key, uploadId, parts) {
|
|
148
|
+
const upload = r2.resumeMultipartUpload(key, uploadId);
|
|
149
|
+
await upload.complete(parts);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
async abortMultipartUpload(key, uploadId) {
|
|
153
|
+
const upload = r2.resumeMultipartUpload(key, uploadId);
|
|
154
|
+
await upload.abort();
|
|
155
|
+
},
|
|
60
156
|
};
|
|
61
157
|
}
|
|
62
158
|
|
|
@@ -84,7 +180,14 @@ interface PutObjectInput {
|
|
|
84
180
|
ContentType?: string;
|
|
85
181
|
}
|
|
86
182
|
|
|
87
|
-
/** Input for GetObject
|
|
183
|
+
/** Input for GetObject */
|
|
184
|
+
interface GetObjectInput {
|
|
185
|
+
Bucket: string;
|
|
186
|
+
Key: string;
|
|
187
|
+
Range?: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Input for DeleteObject */
|
|
88
191
|
interface ObjectKeyInput {
|
|
89
192
|
Bucket: string;
|
|
90
193
|
Key: string;
|
|
@@ -94,13 +197,14 @@ interface ObjectKeyInput {
|
|
|
94
197
|
interface S3GetObjectOutput {
|
|
95
198
|
Body?: { transformToWebStream(): ReadableStream };
|
|
96
199
|
ContentType?: string;
|
|
200
|
+
ContentLength?: number;
|
|
97
201
|
}
|
|
98
202
|
|
|
99
203
|
/** Lazy-loaded S3 client bundle */
|
|
100
204
|
interface S3ClientBundle {
|
|
101
205
|
send: (command: unknown) => Promise<unknown>;
|
|
102
206
|
PutObjectCommand: S3CommandCtor<PutObjectInput>;
|
|
103
|
-
GetObjectCommand: S3CommandCtor<
|
|
207
|
+
GetObjectCommand: S3CommandCtor<GetObjectInput>;
|
|
104
208
|
DeleteObjectCommand: S3CommandCtor<ObjectKeyInput>;
|
|
105
209
|
bucket: string;
|
|
106
210
|
}
|
|
@@ -178,18 +282,22 @@ export function createS3Driver(config: S3DriverConfig): StorageDriver {
|
|
|
178
282
|
await s3.send(command);
|
|
179
283
|
},
|
|
180
284
|
|
|
181
|
-
async get(key) {
|
|
285
|
+
async get(key, opts) {
|
|
182
286
|
const s3 = await getClient();
|
|
183
287
|
try {
|
|
184
288
|
const command = new s3.GetObjectCommand({
|
|
185
289
|
Bucket: s3.bucket,
|
|
186
290
|
Key: key,
|
|
291
|
+
Range: opts?.range
|
|
292
|
+
? `bytes=${opts.range.offset}-${opts.range.offset + opts.range.length - 1}`
|
|
293
|
+
: undefined,
|
|
187
294
|
});
|
|
188
295
|
const response = (await s3.send(command)) as S3GetObjectOutput;
|
|
189
296
|
if (!response.Body) return null;
|
|
190
297
|
return {
|
|
191
298
|
body: response.Body.transformToWebStream(),
|
|
192
299
|
contentType: response.ContentType ?? undefined,
|
|
300
|
+
size: response.ContentLength ?? undefined,
|
|
193
301
|
};
|
|
194
302
|
} catch (err: unknown) {
|
|
195
303
|
// NoSuchKey → return null instead of throwing
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summary Extraction from Tiptap JSON
|
|
3
|
+
*
|
|
4
|
+
* Extracts a plain-text summary from a Tiptap JSON document for use
|
|
5
|
+
* in feeds, meta descriptions, and article previews.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface TiptapNode {
|
|
9
|
+
type: string;
|
|
10
|
+
content?: TiptapNode[];
|
|
11
|
+
text?: string;
|
|
12
|
+
marks?: unknown[];
|
|
13
|
+
attrs?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Recursively extracts plain text from a Tiptap node, ignoring marks.
|
|
18
|
+
*/
|
|
19
|
+
function extractPlainText(node: TiptapNode): string {
|
|
20
|
+
if (node.type === "text") return node.text ?? "";
|
|
21
|
+
if (node.type === "hardBreak") return "\n";
|
|
22
|
+
if (!node.content) return "";
|
|
23
|
+
return node.content.map(extractPlainText).join("");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extracts a plain-text summary from a Tiptap JSON body string.
|
|
28
|
+
*
|
|
29
|
+
* Algorithm:
|
|
30
|
+
* 1. If a `moreBreak` node is found, collect all paragraph text before it
|
|
31
|
+
* 2. Otherwise, accumulate paragraph nodes until limits are reached
|
|
32
|
+
* 3. Skip headings, images, code blocks, blockquotes, lists, horizontal rules
|
|
33
|
+
*
|
|
34
|
+
* @param bodyJson - Tiptap JSON string
|
|
35
|
+
* @param maxParagraphs - Maximum number of paragraphs to include
|
|
36
|
+
* @param maxChars - Maximum total character count
|
|
37
|
+
* @returns Plain text summary, or null if no paragraphs found
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const summary = extractSummary(body, 5, 500);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Content-bearing TipTap node types whose text should be indexed for search.
|
|
46
|
+
* Block-level containers (bulletList, orderedList, table, etc.) are included
|
|
47
|
+
* because they recurse into child nodes that carry text.
|
|
48
|
+
*/
|
|
49
|
+
const SEARCHABLE_TYPES = new Set([
|
|
50
|
+
"doc",
|
|
51
|
+
"paragraph",
|
|
52
|
+
"heading",
|
|
53
|
+
"codeBlock",
|
|
54
|
+
"bulletList",
|
|
55
|
+
"orderedList",
|
|
56
|
+
"listItem",
|
|
57
|
+
"blockquote",
|
|
58
|
+
"table",
|
|
59
|
+
"tableRow",
|
|
60
|
+
"tableCell",
|
|
61
|
+
"tableHeader",
|
|
62
|
+
"text",
|
|
63
|
+
"hardBreak",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Recursively extracts all searchable plain text from a TipTap JSON body string.
|
|
68
|
+
*
|
|
69
|
+
* Used for FTS indexing — includes text from paragraphs, headings, code blocks,
|
|
70
|
+
* lists, blockquotes, and tables. Skips non-textual nodes (image, moreBreak,
|
|
71
|
+
* horizontalRule). Block-level nodes are joined with spaces for better trigram
|
|
72
|
+
* matching.
|
|
73
|
+
*
|
|
74
|
+
* @param bodyJson - TipTap JSON string (the `body` column)
|
|
75
|
+
* @returns Plain text for FTS indexing, or null if parsing fails or doc is empty
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const text = extractBodyText(body);
|
|
80
|
+
* // "Hello world Some code here"
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function extractBodyText(bodyJson: string): string | null {
|
|
84
|
+
let doc: TiptapNode;
|
|
85
|
+
try {
|
|
86
|
+
doc = JSON.parse(bodyJson) as TiptapNode;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (doc.type !== "doc" || !doc.content) return null;
|
|
92
|
+
|
|
93
|
+
function collectText(node: TiptapNode): string {
|
|
94
|
+
if (!SEARCHABLE_TYPES.has(node.type)) return "";
|
|
95
|
+
if (node.type === "text") return node.text ?? "";
|
|
96
|
+
if (node.type === "hardBreak") return " ";
|
|
97
|
+
if (!node.content) return "";
|
|
98
|
+
return node.content.map(collectText).join(" ");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parts: string[] = [];
|
|
102
|
+
for (const child of doc.content) {
|
|
103
|
+
const text = collectText(child).trim();
|
|
104
|
+
if (text) parts.push(text);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function extractSummary(
|
|
111
|
+
bodyJson: string,
|
|
112
|
+
maxParagraphs: number,
|
|
113
|
+
maxChars: number,
|
|
114
|
+
): string | null {
|
|
115
|
+
let doc: TiptapNode;
|
|
116
|
+
try {
|
|
117
|
+
doc = JSON.parse(bodyJson) as TiptapNode;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (doc.type !== "doc" || !doc.content) return null;
|
|
123
|
+
|
|
124
|
+
const nodes = doc.content;
|
|
125
|
+
|
|
126
|
+
// Check for moreBreak — collect paragraph text before it
|
|
127
|
+
const moreBreakIdx = nodes.findIndex((n) => n.type === "moreBreak");
|
|
128
|
+
if (moreBreakIdx !== -1) {
|
|
129
|
+
const paragraphs: string[] = [];
|
|
130
|
+
for (let i = 0; i < moreBreakIdx; i++) {
|
|
131
|
+
const node = nodes[i];
|
|
132
|
+
if (!node) continue;
|
|
133
|
+
if (node.type === "paragraph") {
|
|
134
|
+
const text = extractPlainText(node).trim();
|
|
135
|
+
if (text) paragraphs.push(text);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return paragraphs.length > 0 ? paragraphs.join("\n\n") : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// No moreBreak — accumulate paragraphs up to limits
|
|
142
|
+
const paragraphs: string[] = [];
|
|
143
|
+
let totalChars = 0;
|
|
144
|
+
|
|
145
|
+
for (const node of nodes) {
|
|
146
|
+
if (node.type !== "paragraph") continue;
|
|
147
|
+
|
|
148
|
+
const text = extractPlainText(node).trim();
|
|
149
|
+
if (!text) continue;
|
|
150
|
+
|
|
151
|
+
if (paragraphs.length >= maxParagraphs || totalChars >= maxChars) break;
|
|
152
|
+
|
|
153
|
+
paragraphs.push(text);
|
|
154
|
+
totalChars += text.length;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return paragraphs.length > 0 ? paragraphs.join("\n\n") : null;
|
|
158
|
+
}
|
package/src/lib/theme.ts
CHANGED
|
@@ -33,15 +33,16 @@ export function getAvailableThemes(): ColorTheme[] {
|
|
|
33
33
|
* @param cssVariables - Extra CSS variable overrides
|
|
34
34
|
* @returns CSS string to inject in `<head>`, or empty string if nothing to inject
|
|
35
35
|
*
|
|
36
|
-
* Uses `:root:root`
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
36
|
+
* Uses `:root:root` for light mode and `@media (prefers-color-scheme: dark)`
|
|
37
|
+
* with `:root:root` for dark mode, giving higher specificity than BaseCoat
|
|
38
|
+
* defaults (`:root`). This ensures theme overrides win regardless of source
|
|
39
|
+
* order — important because Vite dev mode injects CSS as `<style>` tags
|
|
40
|
+
* after the theme `<style>`.
|
|
40
41
|
*
|
|
41
42
|
* @example
|
|
42
43
|
* ```typescript
|
|
43
44
|
* const css = buildThemeStyle(blueTheme, { "--radius": "0.5rem" });
|
|
44
|
-
* // => ":root:root {
|
|
45
|
+
* // => ":root:root { ... }\n@media (prefers-color-scheme: dark) { :root:root { ... } }"
|
|
45
46
|
* ```
|
|
46
47
|
*/
|
|
47
48
|
export function buildThemeStyle(
|
|
@@ -74,10 +75,12 @@ export function buildThemeStyle(
|
|
|
74
75
|
|
|
75
76
|
if (hasDark) {
|
|
76
77
|
const declarations = Object.entries(darkVars)
|
|
77
|
-
.map(([k, v]) => `
|
|
78
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
78
79
|
.join("\n");
|
|
79
|
-
// :root
|
|
80
|
-
parts.push(
|
|
80
|
+
// :root:root inside @media has specificity (0,0,2) > preset fallback :root (0,0,1)
|
|
81
|
+
parts.push(
|
|
82
|
+
`@media (prefers-color-scheme: dark) {\n :root:root {\n${declarations}\n }\n}`,
|
|
83
|
+
);
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
return parts.join("\n");
|
package/src/lib/timeline.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { Context } from "hono";
|
|
|
9
9
|
import type { Bindings, TimelineItemView } from "../types.js";
|
|
10
10
|
import type { AppVariables } from "../types/app-context.js";
|
|
11
11
|
import { buildMediaMap } from "./media-helpers.js";
|
|
12
|
-
import { createMediaContext, toPostView
|
|
12
|
+
import { createMediaContext, toPostView } from "./view.js";
|
|
13
13
|
|
|
14
14
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
15
|
|
|
@@ -40,17 +40,21 @@ export interface TimelineResult {
|
|
|
40
40
|
*/
|
|
41
41
|
export async function assembleTimeline(
|
|
42
42
|
c: Context<Env>,
|
|
43
|
-
options?: { page?: number },
|
|
43
|
+
options?: { page?: number; isAuthenticated?: boolean },
|
|
44
44
|
): Promise<TimelineResult> {
|
|
45
45
|
const pageSize = c.var.appConfig.pageSize;
|
|
46
46
|
|
|
47
47
|
const page = Math.max(1, options?.page ?? 1);
|
|
48
48
|
const offset = (page - 1) * pageSize;
|
|
49
49
|
|
|
50
|
+
const excludePrivate = !(options?.isAuthenticated ?? false);
|
|
51
|
+
|
|
50
52
|
// Get total count for pagination
|
|
51
53
|
const totalCount = await c.var.services.posts.count({
|
|
52
54
|
status: "published",
|
|
53
55
|
excludeReplies: true,
|
|
56
|
+
excludeUnlisted: true,
|
|
57
|
+
excludePrivate,
|
|
54
58
|
});
|
|
55
59
|
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
56
60
|
|
|
@@ -58,6 +62,8 @@ export async function assembleTimeline(
|
|
|
58
62
|
const posts = await c.var.services.posts.list({
|
|
59
63
|
status: "published",
|
|
60
64
|
excludeReplies: true,
|
|
65
|
+
excludeUnlisted: true,
|
|
66
|
+
excludePrivate,
|
|
61
67
|
limit: pageSize,
|
|
62
68
|
offset,
|
|
63
69
|
});
|
|
@@ -66,7 +72,7 @@ export async function assembleTimeline(
|
|
|
66
72
|
return { items: [], currentPage: page, totalPages };
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
// Batch load media
|
|
75
|
+
// Batch load media
|
|
70
76
|
const postIds = posts.map((p) => p.id);
|
|
71
77
|
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
72
78
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
@@ -77,55 +83,91 @@ export async function assembleTimeline(
|
|
|
77
83
|
mediaCtx.s3PublicUrl,
|
|
78
84
|
);
|
|
79
85
|
|
|
86
|
+
// Batch load collections for main posts
|
|
87
|
+
const collectionsMap =
|
|
88
|
+
await c.var.services.collections.getCollectionsByPostIds(postIds);
|
|
89
|
+
|
|
80
90
|
// Get reply counts to identify thread roots
|
|
81
91
|
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
82
92
|
const threadRootIds = postIds.filter((id) => (replyCounts.get(id) ?? 0) > 0);
|
|
83
93
|
|
|
84
|
-
// Batch load thread
|
|
85
|
-
const
|
|
86
|
-
threadRootIds
|
|
87
|
-
3,
|
|
88
|
-
);
|
|
94
|
+
// Batch load thread timeline context (latest reply + parent)
|
|
95
|
+
const threadContexts =
|
|
96
|
+
await c.var.services.posts.getThreadTimelineContext(threadRootIds);
|
|
89
97
|
|
|
90
|
-
// Batch load media for
|
|
91
|
-
const
|
|
92
|
-
for (const
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
// Batch load media for context posts (latestReply + parentReply)
|
|
99
|
+
const contextPostIds: string[] = [];
|
|
100
|
+
for (const ctx of threadContexts.values()) {
|
|
101
|
+
contextPostIds.push(ctx.latestReply.id);
|
|
102
|
+
if (ctx.parentReply) {
|
|
103
|
+
contextPostIds.push(ctx.parentReply.id);
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
?
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
const [contextMediaMap, contextCollectionsMap] =
|
|
107
|
+
contextPostIds.length > 0
|
|
108
|
+
? await Promise.all([
|
|
109
|
+
c.var.services.media
|
|
110
|
+
.getByPostIds(contextPostIds)
|
|
111
|
+
.then((raw) =>
|
|
112
|
+
buildMediaMap(
|
|
113
|
+
raw,
|
|
114
|
+
mediaCtx.r2PublicUrl,
|
|
115
|
+
mediaCtx.imageTransformUrl,
|
|
116
|
+
mediaCtx.s3PublicUrl,
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
c.var.services.collections.getCollectionsByPostIds(contextPostIds),
|
|
120
|
+
])
|
|
121
|
+
: [new Map(), new Map()];
|
|
106
122
|
|
|
107
123
|
// Assemble timeline items with View Models
|
|
108
124
|
const items: TimelineItemView[] = posts.map((post) => {
|
|
109
125
|
const postView = toPostView(
|
|
110
|
-
{
|
|
126
|
+
{
|
|
127
|
+
...post,
|
|
128
|
+
mediaAttachments: mediaMap.get(post.id) ?? [],
|
|
129
|
+
},
|
|
111
130
|
mediaCtx,
|
|
131
|
+
collectionsMap.get(post.id),
|
|
112
132
|
);
|
|
113
133
|
|
|
114
|
-
const
|
|
115
|
-
|
|
134
|
+
const threadCtx = threadContexts.get(post.id);
|
|
135
|
+
|
|
136
|
+
if (threadCtx) {
|
|
137
|
+
// Thread root is not the last post — hide reply button on it
|
|
138
|
+
postView.isLastInThread = false;
|
|
139
|
+
|
|
140
|
+
const latestReplyView = toPostView(
|
|
141
|
+
{
|
|
142
|
+
...threadCtx.latestReply,
|
|
143
|
+
mediaAttachments: contextMediaMap.get(threadCtx.latestReply.id) ?? [],
|
|
144
|
+
},
|
|
145
|
+
mediaCtx,
|
|
146
|
+
contextCollectionsMap.get(threadCtx.latestReply.id),
|
|
147
|
+
undefined,
|
|
148
|
+
true, // latestReply is the last post in the thread
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const parentReplyView = threadCtx.parentReply
|
|
152
|
+
? toPostView(
|
|
153
|
+
{
|
|
154
|
+
...threadCtx.parentReply,
|
|
155
|
+
mediaAttachments:
|
|
156
|
+
contextMediaMap.get(threadCtx.parentReply.id) ?? [],
|
|
157
|
+
},
|
|
158
|
+
mediaCtx,
|
|
159
|
+
contextCollectionsMap.get(threadCtx.parentReply.id),
|
|
160
|
+
undefined,
|
|
161
|
+
false, // parentReply is not the last post
|
|
162
|
+
)
|
|
163
|
+
: undefined;
|
|
116
164
|
|
|
117
|
-
if (replyCount > 0 && previewReplies) {
|
|
118
165
|
return {
|
|
119
166
|
post: postView,
|
|
120
167
|
threadPreview: {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
mediaAttachments: previewMediaMap.get(r.id) ?? [],
|
|
125
|
-
})),
|
|
126
|
-
mediaCtx,
|
|
127
|
-
),
|
|
128
|
-
totalReplyCount: replyCount,
|
|
168
|
+
latestReply: latestReplyView,
|
|
169
|
+
parentReply: parentReplyView,
|
|
170
|
+
totalReplyCount: threadCtx.totalReplyCount,
|
|
129
171
|
},
|
|
130
172
|
};
|
|
131
173
|
}
|