@jant/core 0.6.10 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/import-site.js +45 -20
- package/dist/app-B5P1b4TF.js +6 -0
- package/dist/{app-CGHkOdme.js → app-D-zAhayc.js} +847 -244
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-BUxR-E8O.css +2 -0
- package/dist/client/_assets/{client-DYrWuaIk.js → client-S1cdvulk.js} +1 -1
- package/dist/client/_assets/{client-auth-B5Re0uCd.js → client-auth-YW92ZH7u.js} +158 -97
- package/dist/{export-DY1v5Iqu.js → export-Ckwh4BiE.js} +96 -96
- package/dist/{github-sync-LefaslGJ.js → github-sync-BMmK3JZ0.js} +2 -2
- package/dist/{github-sync-2_T7nbOv.js → github-sync-Dl9wQMar.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/__tests__/import-site-command.test.ts +75 -0
- package/src/__tests__/vite-dev-plugins.test.ts +34 -0
- package/src/client/__tests__/compose-launch.test.ts +50 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +74 -18
- package/src/client/components/__tests__/jant-command-palette.test.ts +63 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +33 -0
- package/src/client/components/__tests__/jant-compose-editor-rehost-notice.test.ts +81 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +77 -1
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +71 -0
- package/src/client/components/compose-types.ts +4 -0
- package/src/client/components/jant-command-palette.ts +67 -2
- package/src/client/components/jant-compose-dialog.ts +8 -2
- package/src/client/components/jant-compose-editor.ts +111 -0
- package/src/client/components/jant-nav-manager.ts +142 -2
- package/src/client/components/jant-settings-general.ts +56 -0
- package/src/client/components/nav-manager-types.ts +14 -0
- package/src/client/components/settings-types.ts +30 -0
- package/src/client/compose-bridge.ts +25 -8
- package/src/client/compose-launch.ts +30 -0
- package/src/client/compose-shortcuts.ts +46 -3
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +143 -0
- package/src/client/tiptap/__tests__/paste-rehost-e2e.test.ts +65 -0
- package/src/client/tiptap/__tests__/rehost-images.test.ts +192 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +4 -0
- package/src/client/tiptap/inline-image-upload.ts +174 -50
- package/src/client/tiptap/rehost-images.ts +108 -0
- package/src/i18n/__tests__/fallback.test.ts +24 -0
- package/src/i18n/locales/public/en.po +10 -0
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +10 -0
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +10 -0
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +54 -0
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +64 -10
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +56 -2
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +96 -10
- package/src/lib/__tests__/upload-sideload.test.ts +78 -0
- package/src/lib/__tests__/url-fetch.test.ts +181 -0
- package/src/lib/feed.ts +63 -3
- package/src/lib/post-display.ts +14 -8
- package/src/lib/upload.ts +111 -0
- package/src/lib/url-fetch.ts +263 -0
- package/src/preset.css +60 -18
- package/src/routes/api/__tests__/uploads.test.ts +63 -1
- package/src/routes/api/uploads.ts +52 -0
- package/src/routes/dash/settings.tsx +41 -2
- package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
- package/src/routes/pages/page.tsx +44 -12
- package/src/services/__tests__/about-page.test.ts +116 -0
- package/src/services/__tests__/media.test.ts +168 -1
- package/src/services/__tests__/navigation.test.ts +190 -1
- package/src/services/__tests__/post-timeline.test.ts +119 -2
- package/src/services/about-page.ts +143 -0
- package/src/services/export-theme/layouts/_default/rss.xml +2 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
- package/src/services/export-theme/styles/main.css +46 -14
- package/src/services/index.ts +27 -17
- package/src/services/media.ts +111 -0
- package/src/services/navigation.ts +155 -1
- package/src/services/post.ts +24 -7
- package/src/styles/components.css +20 -0
- package/src/styles/tokens.css +26 -8
- package/src/styles/ui.css +149 -7
- package/src/types/views.ts +13 -0
- package/src/ui/compose/ComposeDialog.tsx +16 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +57 -0
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
- package/src/ui/dash/settings/GeneralContent.tsx +44 -0
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
- package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
- package/src/ui/layouts/BaseLayout.tsx +12 -0
- package/dist/app-D24n0DoH.js +0 -6
- package/dist/client/_assets/client-xWDl78yi.css +0 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "./url-BMYO-Zlt.js";
|
|
2
|
-
import "./export-
|
|
3
|
-
import { i as classifyRepoForSync, o as createGitHubSyncService } from "./github-sync-
|
|
2
|
+
import "./export-Ckwh4BiE.js";
|
|
3
|
+
import { i as classifyRepoForSync, o as createGitHubSyncService } from "./github-sync-Dl9wQMar.js";
|
|
4
4
|
export { classifyRepoForSync, createGitHubSyncService };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as parseMarkdownDocument, r as parseFrontMatter, t as createExportService } from "./export-
|
|
1
|
+
import { c as parseMarkdownDocument, r as parseFrontMatter, t as createExportService } from "./export-Ckwh4BiE.js";
|
|
2
2
|
import { r as getInstallationToken } from "./github-app-BbklkFmU.js";
|
|
3
3
|
import { r as parseRepoSlug, t as createGitHubClient } from "./github-api-BgSiE71w.js";
|
|
4
4
|
//#region src/lib/markdown-to-tiptap.ts
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { y as url_exports } from "./url-BMYO-Zlt.js";
|
|
2
|
-
import { A as MAX_MEDIA_ATTACHMENTS, C as toMediaView, D as toPostViews, E as toPostView, F as STATUSES, I as TEXT_ATTACHMENT_CONTENT_FORMATS, M as MEDIA_KINDS, N as NAV_ITEM_TYPES, O as toSearchResultView, P as SORT_ORDERS, S as toArchiveGroupsWithMedia, T as toNavItemViews, b as createMediaContext, j as MAX_PINNED_POSTS, k as FORMATS, m as defaultFeedRenderer, t as createApp, w as toNavItemView, x as toArchiveGroups } from "./app-
|
|
3
|
-
import {
|
|
2
|
+
import { A as MAX_MEDIA_ATTACHMENTS, C as toMediaView, D as toPostViews, E as toPostView, F as STATUSES, I as TEXT_ATTACHMENT_CONTENT_FORMATS, M as MEDIA_KINDS, N as NAV_ITEM_TYPES, O as toSearchResultView, P as SORT_ORDERS, S as toArchiveGroupsWithMedia, T as toNavItemViews, b as createMediaContext, j as MAX_PINNED_POSTS, k as FORMATS, m as defaultFeedRenderer, t as createApp, w as toNavItemView, x as toArchiveGroups } from "./app-D-zAhayc.js";
|
|
3
|
+
import { S as time_exports, a as markdown_exports } from "./export-Ckwh4BiE.js";
|
|
4
4
|
import "./env-OHRKGcMj.js";
|
|
5
|
-
import "./github-sync-
|
|
5
|
+
import "./github-sync-Dl9wQMar.js";
|
|
6
6
|
export { FORMATS, MAX_MEDIA_ATTACHMENTS, MAX_PINNED_POSTS, MEDIA_KINDS, NAV_ITEM_TYPES, SORT_ORDERS, STATUSES, TEXT_ATTACHMENT_CONTENT_FORMATS, createApp, createMediaContext, defaultFeedRenderer, markdown_exports as markdown, time_exports as time, toArchiveGroups, toArchiveGroupsWithMedia, toMediaView, toNavItemView, toNavItemViews, toPostView, toPostViews, toSearchResultView, url_exports as url };
|
package/dist/node.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "./url-BMYO-Zlt.js";
|
|
2
|
-
import { B as isAssetPath, L as buildThemeStyle, R as BUILTIN_COLOR_THEMES, _ as sqliteSchemaBundle, a as resolveDatabaseDialect, c as getWebhookUrl, d as BUILTIN_FONT_THEMES, f as getCjkSerifCssVariables, g as pgSchemaBundle, h as createStorageDriver, i as createSiteService, l as setMyCommands, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as getFontThemeCssVariables, r as createNodeRequestRuntime, s as resolveConfig, t as createApp, u as setWebhook, v as createNodeDatabase, y as schema_exports, z as getPublicAssetBasePath } from "./app-
|
|
3
|
-
import { t as createExportService } from "./export-
|
|
2
|
+
import { B as isAssetPath, L as buildThemeStyle, R as BUILTIN_COLOR_THEMES, _ as sqliteSchemaBundle, a as resolveDatabaseDialect, c as getWebhookUrl, d as BUILTIN_FONT_THEMES, f as getCjkSerifCssVariables, g as pgSchemaBundle, h as createStorageDriver, i as createSiteService, l as setMyCommands, n as createNodeCliRuntime, o as getHostBasedStartupConfigurationIssues, p as getFontThemeCssVariables, r as createNodeRequestRuntime, s as resolveConfig, t as createApp, u as setWebhook, v as createNodeDatabase, y as schema_exports, z as getPublicAssetBasePath } from "./app-D-zAhayc.js";
|
|
3
|
+
import { t as createExportService } from "./export-Ckwh4BiE.js";
|
|
4
4
|
import { C as shouldTrustProxy, S as getTelegramWebhookSecret, b as getSiteResolutionMode, d as getHostedControlPlaneBaseUrl, i as getConfiguredSingleSitePathPrefix, l as getEnvString, r as getConfiguredSingleSiteOrigin, x as getTelegramBotPool, y as getPort } from "./env-OHRKGcMj.js";
|
|
5
|
-
import "./github-sync-
|
|
5
|
+
import "./github-sync-Dl9wQMar.js";
|
|
6
6
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
7
7
|
import { serve } from "@hono/node-server";
|
|
8
8
|
import Database from "better-sqlite3";
|
|
@@ -529,7 +529,7 @@ async function createNodeRequestHandler(options) {
|
|
|
529
529
|
async function start(env = process.env, app) {
|
|
530
530
|
const handler = await createNodeRequestHandler({
|
|
531
531
|
env,
|
|
532
|
-
app: async () => app ?? (await import("./app-
|
|
532
|
+
app: async () => app ?? (await import("./app-B5P1b4TF.js")).createApp()
|
|
533
533
|
});
|
|
534
534
|
const hostname = resolveHost(env);
|
|
535
535
|
const port = resolvePort(env);
|
package/package.json
CHANGED
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
uploadMediaList,
|
|
25
25
|
normalizeTextAttachmentSpec,
|
|
26
26
|
isAbsoluteImportUrl,
|
|
27
|
+
shouldImportReplyQuietly,
|
|
27
28
|
} = __test__;
|
|
28
29
|
|
|
29
30
|
async function writeFileTree(
|
|
@@ -368,6 +369,35 @@ describe("Hugo import CLI helpers", () => {
|
|
|
368
369
|
});
|
|
369
370
|
});
|
|
370
371
|
|
|
372
|
+
it("buildPostPayloadFromBundle can mark imported replies as quiet", () => {
|
|
373
|
+
const bundle = {
|
|
374
|
+
slug: "reply-a",
|
|
375
|
+
frontMatter: {
|
|
376
|
+
id: "pst_reply",
|
|
377
|
+
title: "Reply A",
|
|
378
|
+
date: "2026-04-01T01:00:00Z",
|
|
379
|
+
slug: "reply-a",
|
|
380
|
+
type: "post",
|
|
381
|
+
format: "note",
|
|
382
|
+
status: "published",
|
|
383
|
+
visibility: "public",
|
|
384
|
+
},
|
|
385
|
+
body: "Reply body",
|
|
386
|
+
};
|
|
387
|
+
const data = buildPostPayloadFromBundle(bundle, {
|
|
388
|
+
bodyMarkdown: "Reply body",
|
|
389
|
+
attachments: [],
|
|
390
|
+
memberships: { entries: [], ids: [] },
|
|
391
|
+
replyToId: "pst_root",
|
|
392
|
+
quietReply: true,
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
expect(data).toMatchObject({
|
|
396
|
+
replyToId: "pst_root",
|
|
397
|
+
quietReply: true,
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
371
401
|
it("buildPostPayloadFromBundle preserves every canonical visibility value", () => {
|
|
372
402
|
const base = {
|
|
373
403
|
slug: "hello",
|
|
@@ -406,6 +436,51 @@ describe("Hugo import CLI helpers", () => {
|
|
|
406
436
|
expect(fallback.visibility).toBeUndefined();
|
|
407
437
|
});
|
|
408
438
|
|
|
439
|
+
it("shouldImportReplyQuietly preserves exported thread activity cutoffs", () => {
|
|
440
|
+
const rootFrontMatter = {
|
|
441
|
+
date: "2026-04-01T00:00:00Z",
|
|
442
|
+
last_activity_at: "2026-04-01T01:00:00Z",
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
expect(
|
|
446
|
+
shouldImportReplyQuietly(rootFrontMatter, {
|
|
447
|
+
date: "2026-04-01T00:30:00Z",
|
|
448
|
+
status: "published",
|
|
449
|
+
}),
|
|
450
|
+
).toBe(false);
|
|
451
|
+
expect(
|
|
452
|
+
shouldImportReplyQuietly(rootFrontMatter, {
|
|
453
|
+
date: "2026-04-01T01:00:00Z",
|
|
454
|
+
status: "published",
|
|
455
|
+
}),
|
|
456
|
+
).toBe(false);
|
|
457
|
+
expect(
|
|
458
|
+
shouldImportReplyQuietly(rootFrontMatter, {
|
|
459
|
+
date: "2026-04-01T02:00:00Z",
|
|
460
|
+
status: "published",
|
|
461
|
+
}),
|
|
462
|
+
).toBe(true);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("shouldImportReplyQuietly falls back to root date for all-quiet threads", () => {
|
|
466
|
+
const rootFrontMatter = {
|
|
467
|
+
date: "2026-04-01T00:00:00Z",
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
expect(
|
|
471
|
+
shouldImportReplyQuietly(rootFrontMatter, {
|
|
472
|
+
date: "2026-04-01T01:00:00Z",
|
|
473
|
+
status: "published",
|
|
474
|
+
}),
|
|
475
|
+
).toBe(true);
|
|
476
|
+
expect(
|
|
477
|
+
shouldImportReplyQuietly(rootFrontMatter, {
|
|
478
|
+
date: "2026-04-01T01:00:00Z",
|
|
479
|
+
status: "draft",
|
|
480
|
+
}),
|
|
481
|
+
).toBe(false);
|
|
482
|
+
});
|
|
483
|
+
|
|
409
484
|
it("getRootAliasPathsForImport prefers root_aliases and strips reply slugs", () => {
|
|
410
485
|
// Reply slug paths are normalized (no trailing slash) to match the
|
|
411
486
|
// return format of normalizeImportAliasPath.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { shouldRunLinguiBuildForFile } from "../../vite.dev-plugins.js";
|
|
3
|
+
|
|
4
|
+
describe("vite dev plugins", () => {
|
|
5
|
+
it("does not run Lingui build for generated i18n output", () => {
|
|
6
|
+
expect(
|
|
7
|
+
shouldRunLinguiBuildForFile(
|
|
8
|
+
"/repo/packages/core/src/i18n/coverage.generated.ts",
|
|
9
|
+
),
|
|
10
|
+
).toBe(false);
|
|
11
|
+
expect(
|
|
12
|
+
shouldRunLinguiBuildForFile(
|
|
13
|
+
"\\repo\\packages\\core\\src\\i18n\\coverage.generated.ts",
|
|
14
|
+
),
|
|
15
|
+
).toBe(false);
|
|
16
|
+
expect(
|
|
17
|
+
shouldRunLinguiBuildForFile(
|
|
18
|
+
"/repo/packages/core/src/i18n/locales/settings/en.ts",
|
|
19
|
+
),
|
|
20
|
+
).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("runs Lingui build for source TypeScript files", () => {
|
|
24
|
+
expect(
|
|
25
|
+
shouldRunLinguiBuildForFile("/repo/packages/core/src/ui/pages/Home.tsx"),
|
|
26
|
+
).toBe(true);
|
|
27
|
+
expect(
|
|
28
|
+
shouldRunLinguiBuildForFile("/repo/packages/core/src/routes/posts.ts"),
|
|
29
|
+
).toBe(true);
|
|
30
|
+
expect(
|
|
31
|
+
shouldRunLinguiBuildForFile("/repo/packages/core/src/styles/ui.css"),
|
|
32
|
+
).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
3
|
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getReplyRefreshTarget,
|
|
6
|
+
getReplyTargetArticle,
|
|
7
|
+
} from "../compose-launch.js";
|
|
5
8
|
|
|
6
9
|
function getArticle(): HTMLElement {
|
|
7
10
|
const article = document.querySelector<HTMLElement>("article[data-post]");
|
|
@@ -9,6 +12,14 @@ function getArticle(): HTMLElement {
|
|
|
9
12
|
return article;
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
function getArticleByPostId(postId: string): HTMLElement {
|
|
16
|
+
const article = document.querySelector<HTMLElement>(
|
|
17
|
+
`article[data-post-id="${postId}"]`,
|
|
18
|
+
);
|
|
19
|
+
if (!article) throw new Error(`expected article for ${postId}`);
|
|
20
|
+
return article;
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
describe("getReplyRefreshTarget", () => {
|
|
13
24
|
afterEach(() => {
|
|
14
25
|
document.body.innerHTML = "";
|
|
@@ -118,3 +129,41 @@ describe("getReplyRefreshTarget", () => {
|
|
|
118
129
|
expect(getReplyRefreshTarget(getArticle())).toBeNull();
|
|
119
130
|
});
|
|
120
131
|
});
|
|
132
|
+
|
|
133
|
+
describe("getReplyTargetArticle", () => {
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
document.body.innerHTML = "";
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("targets the latest thread detail post when the current page is the root", () => {
|
|
139
|
+
document.body.innerHTML = `
|
|
140
|
+
<div data-post-view data-post-view-id="pst_root">
|
|
141
|
+
<div class="thread-group thread-group-detail" data-page="post">
|
|
142
|
+
<div class="thread-item thread-detail-item" data-post-current>
|
|
143
|
+
<article data-post data-post-id="pst_root" data-thread-root-id="pst_root"></article>
|
|
144
|
+
</div>
|
|
145
|
+
<div class="thread-item thread-detail-item">
|
|
146
|
+
<article data-post data-post-id="pst_reply_1" data-thread-root-id="pst_root"></article>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="thread-item thread-detail-item">
|
|
149
|
+
<article data-post data-post-id="pst_reply_2" data-thread-root-id="pst_root"></article>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
expect(getReplyTargetArticle(document)?.dataset.postId).toBe("pst_reply_2");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("keeps using the current article outside thread detail pages", () => {
|
|
159
|
+
document.body.innerHTML = `
|
|
160
|
+
<div data-post-view data-post-view-id="pst_single">
|
|
161
|
+
<article data-post data-post-id="pst_single"></article>
|
|
162
|
+
</div>
|
|
163
|
+
`;
|
|
164
|
+
|
|
165
|
+
expect(getReplyTargetArticle(document)).toBe(
|
|
166
|
+
getArticleByPostId("pst_single"),
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { __testOnly as composeDiscoveryTestOnly } from "../compose-discovery.js";
|
|
5
|
-
import "../compose-shortcuts.js";
|
|
5
|
+
import { __testOnly as composeShortcutsTestOnly } from "../compose-shortcuts.js";
|
|
6
6
|
|
|
7
7
|
type ComposeHarness = HTMLElement & {
|
|
8
8
|
openNew: (options?: unknown) => Promise<void>;
|
|
@@ -44,6 +44,7 @@ function renderThreadDetailPage() {
|
|
|
44
44
|
threadGroup.className = "thread-group thread-group-detail";
|
|
45
45
|
|
|
46
46
|
const currentItem = document.createElement("div");
|
|
47
|
+
currentItem.className = "thread-item thread-detail-item";
|
|
47
48
|
currentItem.dataset.postCurrent = "";
|
|
48
49
|
|
|
49
50
|
const currentArticle = document.createElement("article");
|
|
@@ -58,6 +59,7 @@ function renderThreadDetailPage() {
|
|
|
58
59
|
`;
|
|
59
60
|
|
|
60
61
|
const hoveredItem = document.createElement("div");
|
|
62
|
+
hoveredItem.className = "thread-item thread-detail-item";
|
|
61
63
|
|
|
62
64
|
const hoveredArticle = document.createElement("article");
|
|
63
65
|
hoveredArticle.dataset.post = "";
|
|
@@ -70,14 +72,30 @@ function renderThreadDetailPage() {
|
|
|
70
72
|
<div data-post-body>Hovered body</div>
|
|
71
73
|
`;
|
|
72
74
|
|
|
75
|
+
const latestItem = document.createElement("div");
|
|
76
|
+
latestItem.className = "thread-item thread-detail-item";
|
|
77
|
+
|
|
78
|
+
const latestArticle = document.createElement("article");
|
|
79
|
+
latestArticle.dataset.post = "";
|
|
80
|
+
latestArticle.dataset.postId = "post-latest";
|
|
81
|
+
latestArticle.dataset.threadRootId = "thread-root";
|
|
82
|
+
latestArticle.dataset.format = "note";
|
|
83
|
+
latestArticle.innerHTML = `
|
|
84
|
+
<div data-post-meta>meta</div>
|
|
85
|
+
<time class="dt-published">Mar 21</time>
|
|
86
|
+
<div data-post-body>Latest body</div>
|
|
87
|
+
`;
|
|
88
|
+
|
|
73
89
|
currentItem.appendChild(currentArticle);
|
|
74
90
|
hoveredItem.appendChild(hoveredArticle);
|
|
91
|
+
latestItem.appendChild(latestArticle);
|
|
75
92
|
threadGroup.appendChild(currentItem);
|
|
76
93
|
threadGroup.appendChild(hoveredItem);
|
|
94
|
+
threadGroup.appendChild(latestItem);
|
|
77
95
|
postView.appendChild(threadGroup);
|
|
78
96
|
document.body.appendChild(postView);
|
|
79
97
|
|
|
80
|
-
return { currentArticle, hoveredArticle };
|
|
98
|
+
return { currentArticle, hoveredArticle, latestArticle };
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
describe("compose shortcuts", () => {
|
|
@@ -130,7 +148,7 @@ describe("compose shortcuts", () => {
|
|
|
130
148
|
expect(composeEl.openNew).not.toHaveBeenCalled();
|
|
131
149
|
});
|
|
132
150
|
|
|
133
|
-
it("opens a reply composer for the
|
|
151
|
+
it("opens a reply composer for the latest thread post on detail pages with r", () => {
|
|
134
152
|
const composeEl = createComposeHarness();
|
|
135
153
|
|
|
136
154
|
const postView = document.createElement("div");
|
|
@@ -138,22 +156,45 @@ describe("compose shortcuts", () => {
|
|
|
138
156
|
postView.dataset.postViewId = "post-current";
|
|
139
157
|
|
|
140
158
|
const current = document.createElement("div");
|
|
159
|
+
current.className = "thread-item thread-detail-item";
|
|
141
160
|
current.dataset.postCurrent = "";
|
|
142
161
|
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
const currentArticle = document.createElement("article");
|
|
163
|
+
currentArticle.dataset.post = "";
|
|
164
|
+
currentArticle.dataset.postId = "post-current";
|
|
165
|
+
currentArticle.dataset.threadRootId = "thread-root";
|
|
166
|
+
currentArticle.dataset.format = "quote";
|
|
167
|
+
currentArticle.innerHTML = `
|
|
149
168
|
<div data-post-meta>meta</div>
|
|
150
169
|
<div class="post-status-badges">badges</div>
|
|
151
170
|
<time class="dt-published">Mar 19</time>
|
|
152
|
-
<div data-post-body>
|
|
171
|
+
<div data-post-body>Current body</div>
|
|
172
|
+
`;
|
|
173
|
+
|
|
174
|
+
const latest = document.createElement("div");
|
|
175
|
+
latest.className = "thread-item thread-detail-item";
|
|
176
|
+
|
|
177
|
+
const latestArticle = document.createElement("article");
|
|
178
|
+
latestArticle.dataset.post = "";
|
|
179
|
+
latestArticle.dataset.postId = "post-latest";
|
|
180
|
+
latestArticle.dataset.threadRootId = "thread-root";
|
|
181
|
+
latestArticle.dataset.format = "quote";
|
|
182
|
+
latestArticle.innerHTML = `
|
|
183
|
+
<div data-post-meta>meta</div>
|
|
184
|
+
<div class="post-status-badges">badges</div>
|
|
185
|
+
<time class="dt-published">Mar 21</time>
|
|
186
|
+
<div data-post-body>Latest reply body</div>
|
|
153
187
|
`;
|
|
154
188
|
|
|
155
|
-
|
|
156
|
-
|
|
189
|
+
const threadGroup = document.createElement("div");
|
|
190
|
+
threadGroup.dataset.page = "post";
|
|
191
|
+
threadGroup.className = "thread-group thread-group-detail";
|
|
192
|
+
|
|
193
|
+
current.appendChild(currentArticle);
|
|
194
|
+
latest.appendChild(latestArticle);
|
|
195
|
+
threadGroup.appendChild(current);
|
|
196
|
+
threadGroup.appendChild(latest);
|
|
197
|
+
postView.appendChild(threadGroup);
|
|
157
198
|
document.body.appendChild(postView);
|
|
158
199
|
|
|
159
200
|
const event = dispatchShortcut(document, "r");
|
|
@@ -165,13 +206,13 @@ describe("compose shortcuts", () => {
|
|
|
165
206
|
composeEl.openReply,
|
|
166
207
|
).mock.calls[0] ?? [null, null, null, null];
|
|
167
208
|
|
|
168
|
-
expect(postId).toBe("post-
|
|
209
|
+
expect(postId).toBe("post-latest");
|
|
169
210
|
expect(threadRootId).toBe("thread-root");
|
|
170
211
|
expect(refreshTarget).toEqual({ kind: "post-view", id: "post-current" });
|
|
171
212
|
expect(vi.mocked(composeEl.openReply).mock.calls[0]?.[4]).toBeUndefined();
|
|
172
|
-
expect(replyData).toMatchObject({ dateText: "Mar
|
|
213
|
+
expect(replyData).toMatchObject({ dateText: "Mar 21" });
|
|
173
214
|
expect((replyData as { contentHtml: string }).contentHtml).toContain(
|
|
174
|
-
"
|
|
215
|
+
"Latest reply body",
|
|
175
216
|
);
|
|
176
217
|
expect((replyData as { contentHtml: string }).contentHtml).not.toContain(
|
|
177
218
|
"meta",
|
|
@@ -181,7 +222,7 @@ describe("compose shortcuts", () => {
|
|
|
181
222
|
);
|
|
182
223
|
});
|
|
183
224
|
|
|
184
|
-
it("
|
|
225
|
+
it("targets the latest thread post for reply shortcuts even when an older post is hovered", () => {
|
|
185
226
|
const composeEl = createComposeHarness();
|
|
186
227
|
const { hoveredArticle } = renderThreadDetailPage();
|
|
187
228
|
const originalQuerySelector = document.querySelector.bind(document);
|
|
@@ -199,8 +240,8 @@ describe("compose shortcuts", () => {
|
|
|
199
240
|
|
|
200
241
|
expect(event.defaultPrevented).toBe(true);
|
|
201
242
|
expect(composeEl.openReply).toHaveBeenCalledWith(
|
|
202
|
-
"post-
|
|
203
|
-
expect.objectContaining({ dateText: "Mar
|
|
243
|
+
"post-latest",
|
|
244
|
+
expect.objectContaining({ dateText: "Mar 21" }),
|
|
204
245
|
"thread-root",
|
|
205
246
|
{ kind: "post-view", id: "post-current" },
|
|
206
247
|
);
|
|
@@ -228,6 +269,21 @@ describe("compose shortcuts", () => {
|
|
|
228
269
|
expect(composeEl.openEdit).toHaveBeenCalledWith("post-hovered");
|
|
229
270
|
});
|
|
230
271
|
|
|
272
|
+
it("opens the current post editor from the edit query parameter", async () => {
|
|
273
|
+
const composeEl = createComposeHarness();
|
|
274
|
+
composeEl.openEdit = vi.fn(async () => {});
|
|
275
|
+
document.body.setAttribute("data-authenticated", "true");
|
|
276
|
+
renderThreadDetailPage();
|
|
277
|
+
globalThis.history.pushState({}, "", "/about?edit=1");
|
|
278
|
+
|
|
279
|
+
composeShortcutsTestOnly.openEditFromQueryParam();
|
|
280
|
+
await Promise.resolve();
|
|
281
|
+
|
|
282
|
+
expect(composeEl.openEdit).toHaveBeenCalledWith("post-current");
|
|
283
|
+
expect(window.location.pathname).toBe("/about");
|
|
284
|
+
expect(window.location.search).toBe("");
|
|
285
|
+
});
|
|
286
|
+
|
|
231
287
|
it("keeps using the hovered post for reply shortcuts in the timeline", () => {
|
|
232
288
|
const composeEl = createComposeHarness();
|
|
233
289
|
const article = document.createElement("article");
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
getPathCommandSearchQuery,
|
|
6
|
+
getPathCommandTarget,
|
|
7
|
+
} from "../jant-command-palette.js";
|
|
8
|
+
import type { JantCommandPalette } from "../jant-command-palette.js";
|
|
9
|
+
|
|
10
|
+
function mockPaletteApi() {
|
|
11
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
12
|
+
new Response(JSON.stringify({ items: [] }), {
|
|
13
|
+
headers: { "Content-Type": "application/json" },
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function renderPalette(query: string) {
|
|
19
|
+
mockPaletteApi();
|
|
20
|
+
const el = document.createElement(
|
|
21
|
+
"jant-command-palette",
|
|
22
|
+
) as JantCommandPalette;
|
|
23
|
+
el._open = true;
|
|
24
|
+
el._query = query;
|
|
25
|
+
document.body.appendChild(el);
|
|
26
|
+
await Promise.resolve();
|
|
27
|
+
await el.updateComplete;
|
|
28
|
+
return el;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resultTitles(root: globalThis.Element): string[] {
|
|
32
|
+
return Array.from(
|
|
33
|
+
root.querySelectorAll<HTMLElement>(".command-palette-result-title"),
|
|
34
|
+
).map((item) => item.textContent?.trim() ?? "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("JantCommandPalette slash path mode", () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
document.body.innerHTML = "";
|
|
41
|
+
globalThis.localStorage.clear();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("shows go-to-path first and search second for slash queries", async () => {
|
|
45
|
+
const el = await renderPalette("/draft-123");
|
|
46
|
+
|
|
47
|
+
expect(resultTitles(el)).toEqual([
|
|
48
|
+
"Go to /draft-123",
|
|
49
|
+
'Search for "draft-123"',
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("normalizes repeated leading slashes into a local path", () => {
|
|
54
|
+
expect(getPathCommandTarget("//example.com")).toBe("/example.com");
|
|
55
|
+
expect(getPathCommandSearchQuery("//example.com")).toBe("example.com");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("accepts fullwidth slash input", async () => {
|
|
59
|
+
const el = await renderPalette("/notes");
|
|
60
|
+
|
|
61
|
+
expect(resultTitles(el)).toEqual(["Go to /notes", 'Search for "notes"']);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -644,6 +644,9 @@ describe("JantComposeDialog", () => {
|
|
|
644
644
|
).toBeNull();
|
|
645
645
|
// ...it sits inline above the reply editor instead.
|
|
646
646
|
expect(el.querySelector(".compose-thread-post-header")).not.toBeNull();
|
|
647
|
+
expect(
|
|
648
|
+
el.querySelector(".compose-thread-layout.compose-reply-compose-layout"),
|
|
649
|
+
).not.toBeNull();
|
|
647
650
|
});
|
|
648
651
|
|
|
649
652
|
it("shows an Edit title with the format selector above the post when editing a reply", async () => {
|
|
@@ -3065,6 +3068,36 @@ describe("JantComposeDialog", () => {
|
|
|
3065
3068
|
expect(css).toMatch(
|
|
3066
3069
|
/\.compose-editor-row\s+\.compose-attachments-dock\s*\{[\s\S]*max-height:\s*min\(240px,\s*34dvh\);[\s\S]*overflow-y:\s*auto;/,
|
|
3067
3070
|
);
|
|
3071
|
+
expect(css).toMatch(
|
|
3072
|
+
/\.compose-reply-compose-layout\s+\.compose-editor-row\s+\.compose-attachments-dock\s*\{[\s\S]*--compose-reply-attachment-width:\s*96px;[\s\S]*--compose-reply-attachment-height:\s*72px;[\s\S]*max-height:\s*min\(116px,\s*18dvh\);/,
|
|
3073
|
+
);
|
|
3074
|
+
expect(css).toMatch(
|
|
3075
|
+
/\.compose-reply-compose-layout[\s\S]*\.compose-attachment:only-child[\s\S]*\.compose-attachment-img,[\s\S]*\.compose-reply-compose-layout[\s\S]*\.compose-attachment:not\(:only-child\)[\s\S]*\.compose-attachment-img\s*\{[\s\S]*height:\s*var\(--compose-reply-attachment-height\);[\s\S]*object-fit:\s*cover;/,
|
|
3076
|
+
);
|
|
3077
|
+
expect(css).toMatch(
|
|
3078
|
+
/\.compose-reply-compose-layout\s+\.compose-thread-post-header\s*\+\s*\.compose-body\s*\{[\s\S]*padding-top:\s*12px;/,
|
|
3079
|
+
);
|
|
3080
|
+
expect(css).toMatch(
|
|
3081
|
+
/\.compose-dialog,[\s\S]*\.compose-page-shell\s*>\s*jant-compose-dialog\s*\{[\s\S]*--compose-quote-input-size:\s*var\(--type-content-subtitle\);[\s\S]*--compose-quote-input-leading:\s*1\.32;/,
|
|
3082
|
+
);
|
|
3083
|
+
expect(css).toMatch(
|
|
3084
|
+
/\.compose-quote-text\s*\{[\s\S]*font-size:\s*var\(--compose-quote-input-size\);[\s\S]*line-height:\s*var\(--compose-quote-input-leading\);/,
|
|
3085
|
+
);
|
|
3086
|
+
expect(css).toMatch(
|
|
3087
|
+
/\.compose-reply-compose-layout\s*\{[\s\S]*--compose-title-input-size:\s*var\(--type-content-subtitle\);[\s\S]*--compose-quote-input-size:\s*calc\(var\(--type-content-body\) \* 1\.06\);[\s\S]*--compose-quote-input-leading:\s*1\.42;[\s\S]*--compose-inline-input-size:\s*var\(--type-base\);/,
|
|
3088
|
+
);
|
|
3089
|
+
expect(css).toMatch(
|
|
3090
|
+
/\.compose-reply-compose-layout\s+\.compose-quote-wrap\s*\{[\s\S]*margin-top:\s*0\.4rem;[\s\S]*padding:\s*24px 18px 18px;[\s\S]*border-radius:\s*0\.95rem;/,
|
|
3091
|
+
);
|
|
3092
|
+
expect(css).toMatch(
|
|
3093
|
+
/\.compose-reply-compose-layout\s+\.compose-link-url-wrap\s*\{[\s\S]*margin-top:\s*0\.4rem;/,
|
|
3094
|
+
);
|
|
3095
|
+
expect(css).toMatch(
|
|
3096
|
+
/\.compose-reply-compose-layout\s+\.compose-quote-text\s*\{[\s\S]*min-height:\s*6\.5rem;[\s\S]*padding-top:\s*0\.35rem;/,
|
|
3097
|
+
);
|
|
3098
|
+
expect(css).toMatch(
|
|
3099
|
+
/\.compose-reply-compose-layout\s+\.compose-tiptap-body\s+\.tiptap\s*\{[\s\S]*font-size:\s*var\(--type-content-body\);/,
|
|
3100
|
+
);
|
|
3068
3101
|
});
|
|
3069
3102
|
|
|
3070
3103
|
it("remove button clears attachment", async () => {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { Editor } from "@tiptap/core";
|
|
5
|
+
import type { ComposeLabels } from "../compose-types.js";
|
|
6
|
+
import "../jant-compose-editor.js";
|
|
7
|
+
import type { JantComposeEditor } from "../jant-compose-editor.js";
|
|
8
|
+
|
|
9
|
+
const labels = {
|
|
10
|
+
bodyPlaceholder: "What's on your mind...",
|
|
11
|
+
imageNotRehosted: "An image couldn't be saved — its original link was kept.",
|
|
12
|
+
imagesNotRehosted:
|
|
13
|
+
"{count} images couldn't be saved — their original links were kept.",
|
|
14
|
+
} as unknown as ComposeLabels;
|
|
15
|
+
|
|
16
|
+
function editorOf(el: JantComposeEditor): Editor {
|
|
17
|
+
const editor = (el as unknown as { _editor?: Editor | null })._editor;
|
|
18
|
+
if (!editor) throw new Error("expected compose editor instance");
|
|
19
|
+
return editor;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
|
+
|
|
24
|
+
function dispatchPaste(editor: Editor, html: string, text = "") {
|
|
25
|
+
const event = new Event("paste", {
|
|
26
|
+
bubbles: true,
|
|
27
|
+
cancelable: true,
|
|
28
|
+
}) as Event & { clipboardData: unknown };
|
|
29
|
+
event.clipboardData = {
|
|
30
|
+
getData: (type: string) =>
|
|
31
|
+
type === "text/html" ? html : type === "text/plain" ? text : "",
|
|
32
|
+
files: [],
|
|
33
|
+
items: [],
|
|
34
|
+
types: ["text/html", "text/plain"],
|
|
35
|
+
};
|
|
36
|
+
editor.commands.focus();
|
|
37
|
+
editor.view.dom.dispatchEvent(event);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
const container = document.createElement("div");
|
|
42
|
+
container.id = "toast-container";
|
|
43
|
+
document.body.appendChild(container);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
document.body.innerHTML = "";
|
|
48
|
+
vi.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("compose editor: rehost failure notice", () => {
|
|
52
|
+
it("shows an error toast when a pasted remote image can't be rehosted", async () => {
|
|
53
|
+
// Server rejects the sideload (e.g. host hotlink protection).
|
|
54
|
+
vi.stubGlobal(
|
|
55
|
+
"fetch",
|
|
56
|
+
vi.fn(async () => new Response("blocked", { status: 403 })),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const el = document.createElement(
|
|
60
|
+
"jant-compose-editor",
|
|
61
|
+
) as JantComposeEditor;
|
|
62
|
+
el.format = "note";
|
|
63
|
+
el.labels = labels;
|
|
64
|
+
document.body.appendChild(el);
|
|
65
|
+
await el.updateComplete;
|
|
66
|
+
|
|
67
|
+
dispatchPaste(
|
|
68
|
+
editorOf(el),
|
|
69
|
+
'<p><img src="https://ext.example/blocked.png"></p>',
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Let the rehost fire, the sideload fail, then the debounce window elapse.
|
|
73
|
+
await wait(50);
|
|
74
|
+
await wait(900);
|
|
75
|
+
|
|
76
|
+
const toast = document.querySelector("#toast-container .toast");
|
|
77
|
+
expect(toast).not.toBeNull();
|
|
78
|
+
expect(toast?.className).toContain("toast-error");
|
|
79
|
+
expect(toast?.textContent ?? "").toContain("couldn't be saved");
|
|
80
|
+
});
|
|
81
|
+
});
|