@jant/core 0.6.6 → 0.6.8
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/uploads/cleanup.js +1 -0
- package/dist/{app-BJkOcMbZ.js → app-9P4rVCe2.js} +396 -117
- package/dist/app-DaxS_Cz-.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-C6peCkkD.css +2 -0
- package/dist/client/_assets/{client-mBvc8KAT.js → client-CXnEhyyv.js} +2 -2
- package/dist/client/_assets/{client-auth-BlfwVtHz.js → client-auth-CSItbyU8.js} +360 -358
- package/dist/{env-CoSe-1y4.js → env-OHRKGcMj.js} +1 -1
- package/dist/{export-DLukCOO3.js → export-Be082J0n.js} +33 -8
- package/dist/{github-api-UD4u_7fa.js → github-api-BgSiE71w.js} +1 -1
- package/dist/{github-app-DeX6Td1O.js → github-app-BbklkFmU.js} +1 -1
- package/dist/{github-sync-BtHY2AST.js → github-sync-D1Cw8mOY.js} +3 -3
- package/dist/{github-sync-BeDecPen.js → github-sync-_kPWM4m9.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +6 -6
- package/dist/{url-XF0GbKGO.js → url-BMYO-Zlt.js} +42 -2
- package/package.json +1 -1
- package/src/__tests__/bin/uploads-cleanup.test.ts +2 -0
- package/src/client/__tests__/compose-bridge.test.ts +105 -0
- package/src/client/__tests__/hydrate-partial.test.ts +27 -0
- package/src/client/__tests__/json.test.ts +94 -0
- package/src/client/__tests__/note-expand.test.ts +130 -0
- package/src/client/archive-nav.js +2 -1
- package/src/client/audio-player.ts +7 -3
- package/src/client/components/__tests__/compose-format-convert.test.ts +357 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +357 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +2 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +2 -0
- package/src/client/components/compose-format-convert.ts +255 -0
- package/src/client/components/compose-types.ts +2 -0
- package/src/client/components/jant-collection-directory.ts +1 -0
- package/src/client/components/jant-collection-form.ts +1 -0
- package/src/client/components/jant-command-palette.ts +4 -0
- package/src/client/components/jant-compose-dialog.ts +106 -44
- package/src/client/components/jant-compose-editor.ts +65 -11
- package/src/client/components/jant-compose-fullscreen.ts +3 -0
- package/src/client/components/jant-nav-manager.ts +4 -0
- package/src/client/components/jant-post-menu.ts +3 -0
- package/src/client/components/jant-repo-picker.ts +3 -0
- package/src/client/components/jant-settings-general.ts +3 -0
- package/src/client/compose-bridge.ts +17 -0
- package/src/client/feed-video-player.ts +1 -1
- package/src/client/hydrate-partial.ts +25 -0
- package/src/client/json.ts +56 -2
- package/src/client/multipart-upload.ts +17 -7
- package/src/client/note-expand.ts +63 -0
- package/src/client/upload-session.ts +17 -9
- package/src/client.ts +1 -0
- package/src/i18n/locales/public/en.po +41 -0
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +41 -0
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +41 -0
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +12 -12
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +12 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +12 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +1 -1
- package/src/lib/__tests__/markdown.test.ts +1 -1
- package/src/lib/__tests__/summary.test.ts +87 -0
- package/src/lib/__tests__/timeline.test.ts +48 -1
- package/src/lib/__tests__/tiptap-render.test.ts +4 -4
- package/src/lib/__tests__/url.test.ts +44 -0
- package/src/lib/__tests__/view.test.ts +168 -1
- package/src/lib/navigation.ts +1 -0
- package/src/lib/resolve-config.ts +2 -2
- package/src/lib/summary.ts +42 -3
- package/src/lib/tiptap-render.ts +6 -2
- package/src/lib/upload.ts +2 -2
- package/src/lib/url.ts +41 -0
- package/src/lib/view.ts +102 -40
- package/src/preset.css +7 -1
- package/src/routes/api/internal/__tests__/uploads.test.ts +68 -0
- package/src/routes/api/internal/sites.ts +77 -1
- package/src/routes/api/public/__tests__/archive.test.ts +66 -0
- package/src/routes/api/public/archive.ts +22 -6
- package/src/routes/api/telegram.ts +2 -1
- package/src/routes/dash/custom-urls.tsx +1 -1
- package/src/routes/dash/settings.tsx +8 -5
- package/src/routes/pages/__tests__/archive-params.test.ts +135 -0
- package/src/routes/pages/archive.tsx +116 -20
- package/src/routes/pages/collections.tsx +1 -0
- package/src/services/__tests__/media.test.ts +83 -0
- package/src/services/__tests__/post.test.ts +81 -0
- package/src/services/export-theme/assets/client-site.js +1 -1
- package/src/services/export-theme/styles/main.css +49 -15
- package/src/services/media.ts +31 -1
- package/src/services/post.ts +22 -2
- package/src/services/search.ts +4 -4
- package/src/services/site-admin.ts +121 -0
- package/src/services/upload-session.ts +18 -0
- package/src/styles/tokens.css +1 -1
- package/src/styles/ui.css +163 -34
- package/src/types/config.ts +1 -1
- package/src/types/props.ts +3 -0
- package/src/ui/compose/ComposeDialog.tsx +13 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +0 -39
- package/src/ui/dash/settings/SettingsDirectory.tsx +26 -1
- package/src/ui/dash/settings/SettingsRootContent.tsx +46 -1
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +55 -0
- package/src/ui/feed/NoteCard.tsx +54 -5
- package/src/ui/feed/__tests__/timeline-cards.test.ts +73 -0
- package/src/ui/pages/ArchivePage.tsx +89 -6
- package/src/ui/pages/CollectionsPage.tsx +7 -1
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +37 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -3
- package/src/ui/shared/CollectionsManager.tsx +3 -0
- package/dist/app-CL2PC1Fl.js +0 -6
- package/dist/client/_assets/client-BMPMuwvV.css +0 -2
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
getSelectedFirstOrder,
|
|
37
37
|
} from "../collection-picker-order.js";
|
|
38
38
|
import type { JantComposeEditor } from "./jant-compose-editor.js";
|
|
39
|
+
import { convertComposeFormat } from "./compose-format-convert.js";
|
|
39
40
|
import { getMediaCategory } from "../../lib/upload.js";
|
|
40
41
|
import { getSlugValidationIssue } from "../../lib/slug-format.js";
|
|
41
42
|
import { createTiptapEditor } from "../tiptap/create-editor.js";
|
|
@@ -666,7 +667,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
666
667
|
super();
|
|
667
668
|
this.collections = [];
|
|
668
669
|
this.labels = {} as ComposeLabels;
|
|
669
|
-
this.uploadMaxFileSize =
|
|
670
|
+
this.uploadMaxFileSize = 1024;
|
|
670
671
|
this.pageMode = false;
|
|
671
672
|
this.closeHref = "/";
|
|
672
673
|
this.autoRestoreDraft = false;
|
|
@@ -743,13 +744,14 @@ export class JantComposeDialog extends LitElement {
|
|
|
743
744
|
this._scheduleCollectionPickerAutofocus();
|
|
744
745
|
}
|
|
745
746
|
if (
|
|
746
|
-
changed.has("_format") ||
|
|
747
747
|
changed.has("_collectionIds") ||
|
|
748
748
|
changed.has("_slug") ||
|
|
749
749
|
changed.has("_publishedAtInput") ||
|
|
750
750
|
changed.has("_visibility")
|
|
751
751
|
) {
|
|
752
|
-
// Schedule draft auto-save (new-post and edit modes, not draft-load)
|
|
752
|
+
// Schedule draft auto-save (new-post and edit modes, not draft-load).
|
|
753
|
+
// `_format` is intentionally excluded: a bare format switch is exploratory
|
|
754
|
+
// and shouldn't persist a draft on its own (see `_switchFormat`).
|
|
753
755
|
if (!this._draftSourceId) {
|
|
754
756
|
this._scheduleDraftSave();
|
|
755
757
|
}
|
|
@@ -2269,6 +2271,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
2269
2271
|
|
|
2270
2272
|
private _handleDialogCancel = (e: Event) => {
|
|
2271
2273
|
e.preventDefault();
|
|
2274
|
+
// Defensive: some browsers dispatch <dialog> `cancel` for Escape even
|
|
2275
|
+
// when the IME consumed it. Mirror the guard from _handleKeydown.
|
|
2276
|
+
const ke = e as Partial<globalThis.KeyboardEvent>;
|
|
2277
|
+
if (ke.isComposing || ke.keyCode === 229) return;
|
|
2272
2278
|
if (this._shouldIgnoreEscapeClose()) return;
|
|
2273
2279
|
if (this._dismissEscapeOverlay()) return;
|
|
2274
2280
|
this.requestClose();
|
|
@@ -2411,6 +2417,10 @@ export class JantComposeDialog extends LitElement {
|
|
|
2411
2417
|
|
|
2412
2418
|
private _handleKeydown = (e: Event) => {
|
|
2413
2419
|
const ke = e as globalThis.KeyboardEvent;
|
|
2420
|
+
// Let IME consume keys during composition (e.g. CJK candidate selection).
|
|
2421
|
+
// Without this, pressing Escape to dismiss the IME popup would trigger the
|
|
2422
|
+
// "Save to drafts?" prompt. See GitHub issue #120.
|
|
2423
|
+
if (ke.isComposing || ke.keyCode === 229) return;
|
|
2414
2424
|
if (ke.key !== "Escape") {
|
|
2415
2425
|
this._clearFilePickerEscapeState();
|
|
2416
2426
|
}
|
|
@@ -2448,7 +2458,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
2448
2458
|
"jant-compose-editor",
|
|
2449
2459
|
)[this._focusedThreadIndex];
|
|
2450
2460
|
editor?.dispatchEvent(
|
|
2451
|
-
new CustomEvent("jant:
|
|
2461
|
+
new CustomEvent("jant:format-change", {
|
|
2452
2462
|
detail: { format: target },
|
|
2453
2463
|
bubbles: true,
|
|
2454
2464
|
}),
|
|
@@ -3044,6 +3054,14 @@ export class JantComposeDialog extends LitElement {
|
|
|
3044
3054
|
const editor = this._editor;
|
|
3045
3055
|
if (!editor) return;
|
|
3046
3056
|
|
|
3057
|
+
// Only persist genuine unsaved changes. Without this, merely opening a post
|
|
3058
|
+
// for edit (or restoring a draft) would write a local draft of the
|
|
3059
|
+
// unchanged content, since loading fires content-change events.
|
|
3060
|
+
if (!this._hasUnsavedChanges()) {
|
|
3061
|
+
globalThis.localStorage.removeItem(this._currentDraftStorageKey());
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3047
3065
|
const data = editor.getData();
|
|
3048
3066
|
const hasContent =
|
|
3049
3067
|
!!data.body ||
|
|
@@ -3055,7 +3073,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
3055
3073
|
data.attachedTexts.length > 0;
|
|
3056
3074
|
|
|
3057
3075
|
if (!hasContent) {
|
|
3058
|
-
globalThis.localStorage.removeItem(
|
|
3076
|
+
globalThis.localStorage.removeItem(this._currentDraftStorageKey());
|
|
3059
3077
|
return;
|
|
3060
3078
|
}
|
|
3061
3079
|
|
|
@@ -3090,7 +3108,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
3090
3108
|
|
|
3091
3109
|
try {
|
|
3092
3110
|
globalThis.localStorage.setItem(
|
|
3093
|
-
|
|
3111
|
+
this._currentDraftStorageKey(),
|
|
3094
3112
|
JSON.stringify(draft),
|
|
3095
3113
|
);
|
|
3096
3114
|
} catch {
|
|
@@ -3541,10 +3559,43 @@ export class JantComposeDialog extends LitElement {
|
|
|
3541
3559
|
|
|
3542
3560
|
private static readonly _FORMATS: ComposeFormat[] = ["note", "link", "quote"];
|
|
3543
3561
|
|
|
3562
|
+
/**
|
|
3563
|
+
* Whether a format switch should convert fields (fold/extract). Only when
|
|
3564
|
+
* editing an existing post or a server draft — for a brand-new post, switching
|
|
3565
|
+
* just hides/shows fields and nothing is persisted yet, so conversion would
|
|
3566
|
+
* pollute the body for no benefit.
|
|
3567
|
+
*/
|
|
3568
|
+
private _shouldConvertOnFormatSwitch(): boolean {
|
|
3569
|
+
return !!(this._editPostId || this._draftSourceId);
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3544
3572
|
private _switchFormat(target: ComposeFormat) {
|
|
3545
3573
|
if (this._format === target) return;
|
|
3546
|
-
|
|
3574
|
+
const editor = this._editor;
|
|
3575
|
+
if (editor && this._shouldConvertOnFormatSwitch()) {
|
|
3576
|
+
// Fold fields the target can't hold into the body before the format
|
|
3577
|
+
// change recreates the editor from `_bodyJson`. Synchronous, so the old
|
|
3578
|
+
// Tiptap instance can't fire onUpdate and clobber what we just wrote.
|
|
3579
|
+
// `applyConvertedFields` suppresses the one content-change event the
|
|
3580
|
+
// conversion emits, so the switch itself never schedules a draft save.
|
|
3581
|
+
editor.applyConvertedFields(
|
|
3582
|
+
convertComposeFormat(
|
|
3583
|
+
this._format,
|
|
3584
|
+
target,
|
|
3585
|
+
editor.getConvertibleFields(),
|
|
3586
|
+
),
|
|
3587
|
+
);
|
|
3588
|
+
}
|
|
3589
|
+
// A bare format switch shouldn't persist a local draft, so drop any save
|
|
3590
|
+
// already pending from loading the post.
|
|
3591
|
+
this._cancelDraftSaveTimer();
|
|
3547
3592
|
this._format = target;
|
|
3593
|
+
// Sync the editor's format this tick. Lit commits the `.format` binding
|
|
3594
|
+
// only after this render returns, so `_canPublish()` (which reads
|
|
3595
|
+
// `editor.getData()`, keyed on the editor's format) would otherwise compute
|
|
3596
|
+
// against the stale format for one render and leave the submit button
|
|
3597
|
+
// wrongly disabled.
|
|
3598
|
+
if (editor) editor.format = target;
|
|
3548
3599
|
this._showPublishPanel = false;
|
|
3549
3600
|
if (this._shouldAutofocusFormatInput()) {
|
|
3550
3601
|
globalThis.requestAnimationFrame(() => this._editor?.focusInput());
|
|
@@ -3563,6 +3614,16 @@ export class JantComposeDialog extends LitElement {
|
|
|
3563
3614
|
const draftButtonLabel = this._hasContent()
|
|
3564
3615
|
? this.labels.saveAsDraft
|
|
3565
3616
|
: this.labels.drafts;
|
|
3617
|
+
// Format selector sits inline (above each post) whenever more than one post
|
|
3618
|
+
// is on screen — a reply (parent shown above) or a multi-post thread. The
|
|
3619
|
+
// header then shows a plain title instead of the format selector.
|
|
3620
|
+
const isReply = !!(this._replyToId && this._replyToData);
|
|
3621
|
+
const showTitle = isReply || this._threadItems.length > 0;
|
|
3622
|
+
const headerTitle = this._editPostId
|
|
3623
|
+
? this.labels.editTitle
|
|
3624
|
+
: isReply
|
|
3625
|
+
? this.labels.replyTitle
|
|
3626
|
+
: this.labels.newThread;
|
|
3566
3627
|
|
|
3567
3628
|
return html`
|
|
3568
3629
|
<header class="compose-dialog-header">
|
|
@@ -3575,39 +3636,33 @@ export class JantComposeDialog extends LitElement {
|
|
|
3575
3636
|
</button>
|
|
3576
3637
|
|
|
3577
3638
|
<div class="compose-dialog-header-center">
|
|
3578
|
-
${
|
|
3579
|
-
? html`<span class="compose-dialog-title"
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
"
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
${formatLabels[f]}
|
|
3606
|
-
</button>
|
|
3607
|
-
`,
|
|
3608
|
-
)}
|
|
3609
|
-
</div>
|
|
3610
|
-
`}
|
|
3639
|
+
${showTitle
|
|
3640
|
+
? html`<span class="compose-dialog-title">${headerTitle}</span>`
|
|
3641
|
+
: html`
|
|
3642
|
+
<div class="compose-segmented">
|
|
3643
|
+
<div
|
|
3644
|
+
class=${classMap({
|
|
3645
|
+
"compose-format-pill": true,
|
|
3646
|
+
"compose-format-pill-link": this._format === "link",
|
|
3647
|
+
"compose-format-pill-quote": this._format === "quote",
|
|
3648
|
+
})}
|
|
3649
|
+
></div>
|
|
3650
|
+
${formats.map(
|
|
3651
|
+
(f) => html`
|
|
3652
|
+
<button
|
|
3653
|
+
type="button"
|
|
3654
|
+
class=${classMap({
|
|
3655
|
+
"compose-segmented-item": true,
|
|
3656
|
+
"compose-segmented-item-active": this._format === f,
|
|
3657
|
+
})}
|
|
3658
|
+
@click=${() => this._switchFormat(f)}
|
|
3659
|
+
>
|
|
3660
|
+
${formatLabels[f]}
|
|
3661
|
+
</button>
|
|
3662
|
+
`,
|
|
3663
|
+
)}
|
|
3664
|
+
</div>
|
|
3665
|
+
`}
|
|
3611
3666
|
</div>
|
|
3612
3667
|
|
|
3613
3668
|
<div class="compose-dialog-header-actions">
|
|
@@ -5289,9 +5344,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
5289
5344
|
@focusin=${() => {
|
|
5290
5345
|
this._focusedThreadIndex = index;
|
|
5291
5346
|
}}
|
|
5292
|
-
@jant:
|
|
5293
|
-
e: CustomEvent<{ format: ComposeFormat }>,
|
|
5294
|
-
) => {
|
|
5347
|
+
@jant:format-change=${(e: CustomEvent<{ format: ComposeFormat }>) => {
|
|
5295
5348
|
e.stopPropagation();
|
|
5296
5349
|
this._threadItems = this._threadItems.map((it, i) =>
|
|
5297
5350
|
i === index ? { ...it, format: e.detail.format } : it,
|
|
@@ -5415,6 +5468,7 @@ export class JantComposeDialog extends LitElement {
|
|
|
5415
5468
|
.format=${this._format}
|
|
5416
5469
|
.labels=${this.labels}
|
|
5417
5470
|
.uploadMaxFileSize=${this.uploadMaxFileSize}
|
|
5471
|
+
.inlineFormat=${isReply}
|
|
5418
5472
|
.slashCommandDiscovered=${this.slashCommandDiscovered}
|
|
5419
5473
|
></jant-compose-editor>`;
|
|
5420
5474
|
|
|
@@ -5438,7 +5492,15 @@ export class JantComposeDialog extends LitElement {
|
|
|
5438
5492
|
? html`
|
|
5439
5493
|
<div class="compose-thread-layout">
|
|
5440
5494
|
${this._renderReplyContext()}
|
|
5441
|
-
<div
|
|
5495
|
+
<div
|
|
5496
|
+
class="compose-editor-row"
|
|
5497
|
+
@jant:format-change=${(
|
|
5498
|
+
e: CustomEvent<{ format: ComposeFormat }>,
|
|
5499
|
+
) => {
|
|
5500
|
+
e.stopPropagation();
|
|
5501
|
+
this._switchFormat(e.detail.format);
|
|
5502
|
+
}}
|
|
5503
|
+
>
|
|
5442
5504
|
<div class="compose-thread-dot"></div>
|
|
5443
5505
|
${editor}
|
|
5444
5506
|
</div>
|
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
ComposeEditorSelection,
|
|
31
31
|
ComposeFullscreenOpenDetail,
|
|
32
32
|
} from "./compose-types.js";
|
|
33
|
+
import type { ComposeConvertFields } from "./compose-format-convert.js";
|
|
33
34
|
import {
|
|
34
35
|
UPLOAD_ACCEPT,
|
|
35
36
|
getMediaCategory,
|
|
@@ -178,6 +179,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
178
179
|
uploadMaxFileSize: { type: Number },
|
|
179
180
|
threadItem: { type: Boolean, attribute: "thread-item" },
|
|
180
181
|
removable: { type: Boolean },
|
|
182
|
+
inlineFormat: { type: Boolean, attribute: "inline-format" },
|
|
181
183
|
slashCommandDiscovered: { type: Boolean },
|
|
182
184
|
_title: { state: true },
|
|
183
185
|
_bodyJson: { state: true },
|
|
@@ -203,6 +205,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
203
205
|
declare uploadMaxFileSize: number;
|
|
204
206
|
declare threadItem: boolean;
|
|
205
207
|
declare removable: boolean;
|
|
208
|
+
declare inlineFormat: boolean;
|
|
206
209
|
declare slashCommandDiscovered: boolean;
|
|
207
210
|
declare _title: string;
|
|
208
211
|
declare _bodyJson: JSONContent | null;
|
|
@@ -234,6 +237,14 @@ export class JantComposeEditor extends LitElement {
|
|
|
234
237
|
private _scrollBufferApplied = false;
|
|
235
238
|
private _filePickerCleanup: (() => void) | null = null;
|
|
236
239
|
private _suppressAttachedTextOpenUntil = 0;
|
|
240
|
+
/**
|
|
241
|
+
* Set by {@link applyConvertedFields} so the format-conversion content writes
|
|
242
|
+
* don't emit a content-changed event (which would schedule a draft autosave).
|
|
243
|
+
* A bare format switch shouldn't persist a local draft — see `_switchFormat`.
|
|
244
|
+
* Always consumed: a switch also changes `format`, so `updated()` is guaranteed
|
|
245
|
+
* to run this cycle.
|
|
246
|
+
*/
|
|
247
|
+
private _suppressContentChangedOnce = false;
|
|
237
248
|
#inlineImageUploadGeneration = 0;
|
|
238
249
|
#inlineImageUploadPromises = new Set<Promise<void>>();
|
|
239
250
|
#sortable: { destroy(): void } | null = null;
|
|
@@ -247,9 +258,10 @@ export class JantComposeEditor extends LitElement {
|
|
|
247
258
|
super();
|
|
248
259
|
this.format = "note";
|
|
249
260
|
this.labels = {} as ComposeLabels;
|
|
250
|
-
this.uploadMaxFileSize =
|
|
261
|
+
this.uploadMaxFileSize = 1024;
|
|
251
262
|
this.threadItem = false;
|
|
252
263
|
this.removable = false;
|
|
264
|
+
this.inlineFormat = false;
|
|
253
265
|
this.slashCommandDiscovered = false;
|
|
254
266
|
this._title = "";
|
|
255
267
|
this._bodyJson = null;
|
|
@@ -903,13 +915,19 @@ export class JantComposeEditor extends LitElement {
|
|
|
903
915
|
}
|
|
904
916
|
}
|
|
905
917
|
|
|
906
|
-
// Notify parent dialog of content changes for draft auto-save
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
918
|
+
// Notify parent dialog of content changes for draft auto-save. A format
|
|
919
|
+
// conversion writes content fields too, but it's not a user edit, so skip
|
|
920
|
+
// the notification once when asked.
|
|
921
|
+
if (this._suppressContentChangedOnce) {
|
|
922
|
+
this._suppressContentChangedOnce = false;
|
|
923
|
+
} else {
|
|
924
|
+
for (const key of changed.keys()) {
|
|
925
|
+
if (JantComposeEditor._CONTENT_PROPS.has(key as string)) {
|
|
926
|
+
this.dispatchEvent(
|
|
927
|
+
new Event("jant:compose-content-changed", { bubbles: true }),
|
|
928
|
+
);
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
913
931
|
}
|
|
914
932
|
}
|
|
915
933
|
}
|
|
@@ -924,6 +942,39 @@ export class JantComposeEditor extends LitElement {
|
|
|
924
942
|
};
|
|
925
943
|
}
|
|
926
944
|
|
|
945
|
+
/**
|
|
946
|
+
* Raw field values for format conversion. Unlike {@link getData}, this returns
|
|
947
|
+
* every field regardless of the current format (so a hidden quote/url survives
|
|
948
|
+
* a switch) plus the freshest, normalized body.
|
|
949
|
+
*/
|
|
950
|
+
getConvertibleFields(): ComposeConvertFields {
|
|
951
|
+
return {
|
|
952
|
+
title: this._title,
|
|
953
|
+
url: this._url,
|
|
954
|
+
quoteText: this._quoteText,
|
|
955
|
+
quoteAuthor: this._quoteAuthor,
|
|
956
|
+
showTitle: this._showTitle,
|
|
957
|
+
bodyJson: this._normalizeDocJson(
|
|
958
|
+
this._editor?.getJSON() ?? this._bodyJson,
|
|
959
|
+
),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Write back fields produced by `convertComposeFormat`. The body is applied via
|
|
965
|
+
* `_bodyJson` only — the imminent format change recreates the editor from it, so
|
|
966
|
+
* calling `setContent` here would be redundant.
|
|
967
|
+
*/
|
|
968
|
+
applyConvertedFields(fields: ComposeConvertFields): void {
|
|
969
|
+
this._suppressContentChangedOnce = true;
|
|
970
|
+
this._title = fields.title;
|
|
971
|
+
this._url = fields.url;
|
|
972
|
+
this._quoteText = fields.quoteText;
|
|
973
|
+
this._quoteAuthor = fields.quoteAuthor;
|
|
974
|
+
this._showTitle = fields.showTitle;
|
|
975
|
+
this._bodyJson = fields.bodyJson;
|
|
976
|
+
}
|
|
977
|
+
|
|
927
978
|
/** Pre-fill all fields for edit mode or draft restore */
|
|
928
979
|
populate(data: {
|
|
929
980
|
format: string;
|
|
@@ -1736,6 +1787,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
1736
1787
|
@input=${(e: Event) => this._onInput("_title", e)}
|
|
1737
1788
|
@focus=${(e: Event) => this._onFieldFocus(e)}
|
|
1738
1789
|
@keydown=${(e: globalThis.KeyboardEvent) => {
|
|
1790
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
1739
1791
|
if (e.key === "Enter") {
|
|
1740
1792
|
e.preventDefault();
|
|
1741
1793
|
this._editor?.commands.focus("start");
|
|
@@ -2432,7 +2484,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
2432
2484
|
`;
|
|
2433
2485
|
}
|
|
2434
2486
|
|
|
2435
|
-
private
|
|
2487
|
+
private _renderFormatHeader() {
|
|
2436
2488
|
const formatLabels: Record<ComposeFormat, string> = {
|
|
2437
2489
|
note: this.labels.note,
|
|
2438
2490
|
link: this.labels.link,
|
|
@@ -2462,7 +2514,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
2462
2514
|
@click=${() => {
|
|
2463
2515
|
if (this.format !== f) {
|
|
2464
2516
|
this.dispatchEvent(
|
|
2465
|
-
new CustomEvent("jant:
|
|
2517
|
+
new CustomEvent("jant:format-change", {
|
|
2466
2518
|
bubbles: true,
|
|
2467
2519
|
detail: { format: f },
|
|
2468
2520
|
}),
|
|
@@ -2517,7 +2569,9 @@ export class JantComposeEditor extends LitElement {
|
|
|
2517
2569
|
|
|
2518
2570
|
render() {
|
|
2519
2571
|
return html`
|
|
2520
|
-
${this.threadItem
|
|
2572
|
+
${this.threadItem || this.inlineFormat
|
|
2573
|
+
? this._renderFormatHeader()
|
|
2574
|
+
: nothing}
|
|
2521
2575
|
<section class="compose-body">
|
|
2522
2576
|
${this.format === "note"
|
|
2523
2577
|
? this._renderNoteFields()
|
|
@@ -188,6 +188,8 @@ export class JantComposeFullscreen extends LitElement {
|
|
|
188
188
|
|
|
189
189
|
private _onDocumentKeydown = (e: globalThis.KeyboardEvent) => {
|
|
190
190
|
if (!this._open || e.key !== "Escape") return;
|
|
191
|
+
// Let IME consume Escape during composition (e.g. CJK candidate dismiss).
|
|
192
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
191
193
|
if (this._hasActiveEscapeOverlay()) return;
|
|
192
194
|
|
|
193
195
|
e.preventDefault();
|
|
@@ -273,6 +275,7 @@ export class JantComposeFullscreen extends LitElement {
|
|
|
273
275
|
this._title = (e.target as HTMLInputElement).value;
|
|
274
276
|
}}
|
|
275
277
|
@keydown=${(e: globalThis.KeyboardEvent) => {
|
|
278
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
276
279
|
if (e.key === "Enter") {
|
|
277
280
|
e.preventDefault();
|
|
278
281
|
this._editor?.commands.focus("start");
|
|
@@ -103,6 +103,10 @@ export class JantNavManager extends LitElement {
|
|
|
103
103
|
if (!("key" in event) || event.key !== "Escape" || !this._showPreviewMore) {
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
|
+
// Defensive: nav editor has many text inputs; let IME swallow Escape
|
|
107
|
+
// when the user is dismissing a CJK candidate popup.
|
|
108
|
+
const ke = event as globalThis.KeyboardEvent;
|
|
109
|
+
if (ke.isComposing || ke.keyCode === 229) return;
|
|
106
110
|
|
|
107
111
|
event.preventDefault();
|
|
108
112
|
this.#closePreviewMore();
|
|
@@ -169,6 +169,9 @@ export class JantPostMenu extends LitElement {
|
|
|
169
169
|
|
|
170
170
|
#handleKeydown = (e: Event) => {
|
|
171
171
|
const ke = e as globalThis.KeyboardEvent;
|
|
172
|
+
// Let IME consume Escape during composition (e.g. dismissing the CJK
|
|
173
|
+
// candidate popup in the collection search input).
|
|
174
|
+
if (ke.isComposing || ke.keyCode === 229) return;
|
|
172
175
|
if (ke.key === "Escape") {
|
|
173
176
|
if (this._addCollectionPanelOpen) {
|
|
174
177
|
this.#closeAddCollectionPanel();
|
|
@@ -418,6 +418,9 @@ export class JantRepoPicker extends LitElement {
|
|
|
418
418
|
|
|
419
419
|
#handleEscape = (e: KeyboardEvent) => {
|
|
420
420
|
if (e.key !== "Escape") return;
|
|
421
|
+
// Let IME consume Escape during composition (e.g. dismissing the CJK
|
|
422
|
+
// candidate popup in the owner/repo search inputs).
|
|
423
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
421
424
|
if (this._ownerOpen || this._repoOpen) {
|
|
422
425
|
this._ownerOpen = false;
|
|
423
426
|
this._repoOpen = false;
|
|
@@ -450,6 +450,7 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
450
450
|
};
|
|
451
451
|
|
|
452
452
|
private _onLocalePickerKeydown = (e: KeyboardEvent) => {
|
|
453
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
453
454
|
if (e.key === "Escape" && this._localeOpen) {
|
|
454
455
|
this._localeOpen = false;
|
|
455
456
|
this._localeQuery = "";
|
|
@@ -614,6 +615,8 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
614
615
|
dirty: boolean,
|
|
615
616
|
loading: boolean,
|
|
616
617
|
) {
|
|
618
|
+
// Pressing Enter to commit an IME candidate must not also submit the form.
|
|
619
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
617
620
|
if (
|
|
618
621
|
e.key === "Enter" &&
|
|
619
622
|
!loading &&
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
queueToastForNextPage,
|
|
27
27
|
} from "./toast.js";
|
|
28
28
|
import { openReplyForArticle } from "./compose-launch.js";
|
|
29
|
+
import { hydratePartial } from "./hydrate-partial.js";
|
|
29
30
|
import { getJsonString, readJsonObject } from "./json.js";
|
|
30
31
|
import { uploadViaSession } from "./upload-session.js";
|
|
31
32
|
import { publicPath } from "./runtime-paths.js";
|
|
@@ -75,6 +76,10 @@ async function refreshTimelineThreadView(
|
|
|
75
76
|
if (!html) return false;
|
|
76
77
|
|
|
77
78
|
content.innerHTML = html;
|
|
79
|
+
// Swapped-in markup carries interactions whose per-element setup only runs
|
|
80
|
+
// on DOMContentLoaded (thread "Show more" toggle, feed video autoplay, audio
|
|
81
|
+
// waveform); re-initialize them or they stay inert until a full reload.
|
|
82
|
+
hydratePartial(content);
|
|
78
83
|
return true;
|
|
79
84
|
} catch {
|
|
80
85
|
return false;
|
|
@@ -97,6 +102,7 @@ async function refreshPostCardView(postId: string): Promise<boolean> {
|
|
|
97
102
|
);
|
|
98
103
|
if (!content) return false;
|
|
99
104
|
content.innerHTML = html;
|
|
105
|
+
hydratePartial(content);
|
|
100
106
|
return true;
|
|
101
107
|
}
|
|
102
108
|
|
|
@@ -106,6 +112,11 @@ async function refreshPostCardView(postId: string): Promise<boolean> {
|
|
|
106
112
|
if (!article) return false;
|
|
107
113
|
|
|
108
114
|
article.outerHTML = html;
|
|
115
|
+
// outerHTML detaches `article`; re-query the replacement to hydrate it.
|
|
116
|
+
const nextArticle = document.querySelector<HTMLElement>(
|
|
117
|
+
`article[data-post-id="${postId}"]`,
|
|
118
|
+
);
|
|
119
|
+
if (nextArticle) hydratePartial(nextArticle);
|
|
109
120
|
return true;
|
|
110
121
|
} catch {
|
|
111
122
|
return false;
|
|
@@ -125,6 +136,12 @@ async function refreshPostPageView(postId: string): Promise<boolean> {
|
|
|
125
136
|
if (!html) return false;
|
|
126
137
|
|
|
127
138
|
container.outerHTML = html;
|
|
139
|
+
// outerHTML detaches `container`; re-query the replacement to hydrate it
|
|
140
|
+
// (see refreshTimelineThreadView).
|
|
141
|
+
const next = document.querySelector<HTMLElement>(
|
|
142
|
+
`[data-post-view][data-post-view-id="${postId}"]`,
|
|
143
|
+
);
|
|
144
|
+
if (next) hydratePartial(next);
|
|
128
145
|
return true;
|
|
129
146
|
} catch {
|
|
130
147
|
return false;
|
|
@@ -337,7 +337,7 @@ function handleMuteToggle(event: Event): void {
|
|
|
337
337
|
}
|
|
338
338
|
|
|
339
339
|
export function initFeedVideoPlayer(
|
|
340
|
-
root: globalThis.
|
|
340
|
+
root: globalThis.Document | globalThis.Element = document,
|
|
341
341
|
): void {
|
|
342
342
|
const videos = root.querySelectorAll<HTMLVideoElement>(
|
|
343
343
|
"[data-feed-short-video]",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-initialize interactive behaviors inside a server-rendered fragment that
|
|
3
|
+
* was swapped into the DOM after page load — e.g. compose-bridge replacing a
|
|
4
|
+
* timeline item or post view after a reply or edit.
|
|
5
|
+
*
|
|
6
|
+
* Most interactions survive a swap on their own: note-expand and the audio
|
|
7
|
+
* transport use document-level event delegation, and media-scroll-hint runs its
|
|
8
|
+
* own MutationObserver. The ones gathered here need per-element setup
|
|
9
|
+
* (IntersectionObserver / ResizeObserver / canvas drawing) that otherwise only
|
|
10
|
+
* runs once on DOMContentLoaded, so a freshly swapped fragment would stay inert
|
|
11
|
+
* until a full reload. Each initializer is idempotent, so calling this on a root
|
|
12
|
+
* that already contains initialized nodes is safe.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { setupThreadContexts } from "./thread-context.js";
|
|
16
|
+
import { initFeedVideoPlayer } from "./feed-video-player.js";
|
|
17
|
+
import { initPrecomputedWaveforms } from "./audio-player.js";
|
|
18
|
+
|
|
19
|
+
export function hydratePartial(
|
|
20
|
+
root: globalThis.Document | globalThis.Element,
|
|
21
|
+
): void {
|
|
22
|
+
setupThreadContexts(root);
|
|
23
|
+
initFeedVideoPlayer(root);
|
|
24
|
+
initPrecomputedWaveforms(root);
|
|
25
|
+
}
|
package/src/client/json.ts
CHANGED
|
@@ -32,6 +32,60 @@ export function getJsonNumber(value: unknown, key: string): number | undefined {
|
|
|
32
32
|
export async function readJsonObject(
|
|
33
33
|
response: Response,
|
|
34
34
|
): Promise<Record<string, unknown>> {
|
|
35
|
-
const
|
|
36
|
-
|
|
35
|
+
const text = await response.text();
|
|
36
|
+
if (!text.trim()) return {};
|
|
37
|
+
try {
|
|
38
|
+
const data = JSON.parse(text);
|
|
39
|
+
return isJsonObject(data) ? data : {};
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Expected JSON (HTTP ${response.status}) but got: ${truncate(text.trim(), 200)}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Read a server error message from a failed Response.
|
|
49
|
+
* Prefers JSON `{ error }`, falls back to the raw text body so server-side
|
|
50
|
+
* failures (e.g. plain-text 404 from an edge/proxy) reach the user instead of
|
|
51
|
+
* being masked by a cryptic JSON parse error.
|
|
52
|
+
*/
|
|
53
|
+
export async function readErrorMessage(
|
|
54
|
+
response: Response,
|
|
55
|
+
fallback: string,
|
|
56
|
+
): Promise<string> {
|
|
57
|
+
let text: string;
|
|
58
|
+
try {
|
|
59
|
+
text = await response.text();
|
|
60
|
+
} catch {
|
|
61
|
+
return fallback;
|
|
62
|
+
}
|
|
63
|
+
return extractErrorMessage(text, fallback);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Same as readErrorMessage but for a body already read as text (e.g. XHR). */
|
|
67
|
+
export function readErrorMessageFromText(
|
|
68
|
+
text: string,
|
|
69
|
+
fallback: string,
|
|
70
|
+
): string {
|
|
71
|
+
return extractErrorMessage(text, fallback);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractErrorMessage(text: string, fallback: string): string {
|
|
75
|
+
const trimmed = text.trim();
|
|
76
|
+
if (!trimmed) return fallback;
|
|
77
|
+
try {
|
|
78
|
+
const data = JSON.parse(trimmed);
|
|
79
|
+
if (isJsonObject(data)) {
|
|
80
|
+
const msg = getJsonString(data, "error");
|
|
81
|
+
if (msg) return msg;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Not JSON — fall through and surface the raw text below.
|
|
85
|
+
}
|
|
86
|
+
return truncate(trimmed, 200);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function truncate(value: string, max: number): string {
|
|
90
|
+
return value.length > max ? `${value.slice(0, max)}…` : value;
|
|
37
91
|
}
|