@jant/core 0.3.45 → 0.3.47
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/db/execute-file.js +12 -4
- package/bin/commands/db/rehearse.js +2 -2
- package/bin/commands/export.js +12 -4
- package/bin/commands/import-site.js +99 -305
- package/bin/commands/migrate.js +36 -69
- package/bin/commands/reset-password.js +10 -4
- package/bin/commands/site/export.js +59 -248
- package/bin/commands/site/snapshot/export.js +58 -45
- package/bin/commands/site/snapshot/import.js +104 -52
- package/bin/lib/node-env.js +100 -0
- package/bin/lib/runtime-target.js +64 -0
- package/bin/lib/site-snapshot.js +185 -54
- package/bin/lib/sql-export.js +19 -2
- package/dist/{app-C-L7wL6o.js → app-3REcR-3U.js} +332 -190
- package/dist/app-B67XOEyo.js +6 -0
- package/dist/client/.vite/manifest.json +2 -2
- package/dist/client/_assets/{client-auth-Dcon89Av.js → client-auth-Ce5WEAVS.js} +236 -183
- package/dist/client/_assets/client-s71Js1Cu.css +2 -0
- package/dist/{github-sync-CQ1x271f.js → export-ZBlfKSKm.js} +12 -439
- package/dist/github-sync-C593r22F.js +4 -0
- package/dist/github-sync-bL1hnx3Q.js +428 -0
- package/dist/index.js +3 -2
- package/dist/node.js +5 -4
- package/package.json +3 -2
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/__tests__/import-site-command.test.ts +18 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +70 -0
- package/src/client/components/jant-compose-dialog.ts +7 -6
- package/src/client/components/jant-compose-editor.ts +6 -5
- package/src/client/components/jant-settings-general.ts +164 -22
- package/src/client/components/settings-types.ts +4 -6
- package/src/client/random-uuid.ts +23 -0
- package/src/client-auth.ts +1 -1
- package/src/db/__tests__/demo-canonical-snapshot.test.ts +1 -1
- package/src/db/__tests__/migration-rehearsal.test.ts +2 -5
- package/src/db/backfills/0004_register_apple_touch_media_rows.sql +65 -0
- package/src/db/migrations/0021_thankful_phalanx.sql +16 -0
- package/src/db/migrations/meta/0021_snapshot.json +2121 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0019_gray_natasha_romanoff.sql +20 -0
- package/src/db/migrations/pg/meta/0019_snapshot.json +2718 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +21 -26
- package/src/db/rehearsal-fixtures/demo-current.json +1 -1
- package/src/db/schema.ts +16 -20
- package/src/i18n/__tests__/middleware.test.ts +43 -1
- package/src/i18n/coverage.generated.ts +17 -0
- package/src/i18n/i18n.ts +18 -2
- package/src/i18n/index.ts +3 -0
- package/src/i18n/locales/settings/en.po +16 -11
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +17 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +16 -11
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +84 -2
- package/src/i18n/middleware.ts +25 -16
- package/src/i18n/supported-locales.ts +153 -0
- package/src/lib/__tests__/csp-builder.test.ts +19 -2
- package/src/lib/__tests__/feed.test.ts +242 -1
- package/src/lib/__tests__/post-meta.test.ts +0 -1
- package/src/lib/__tests__/view.test.ts +0 -1
- package/src/lib/csp-builder.ts +28 -10
- package/src/lib/feed.ts +153 -3
- package/src/middleware/__tests__/secure-headers.test.ts +89 -0
- package/src/middleware/auth.ts +1 -1
- package/src/middleware/secure-headers.ts +47 -1
- package/src/node/__tests__/cli-runtime-target.test.ts +110 -2
- package/src/node/__tests__/cli-site-snapshot.test.ts +308 -13
- package/src/node/__tests__/cli-site-token-env.test.ts +2 -7
- package/src/node/__tests__/cli-snapshot-meta.test.ts +85 -0
- package/src/node/__tests__/cli-sql-export.test.ts +49 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +8 -2
- package/src/routes/api/__tests__/settings.test.ts +3 -2
- package/src/routes/api/github-sync.tsx +1 -1
- package/src/routes/api/settings.ts +4 -1
- package/src/routes/auth/signin.tsx +6 -0
- package/src/routes/pages/archive.tsx +4 -2
- package/src/services/__tests__/post.test.ts +19 -19
- package/src/services/__tests__/search.test.ts +0 -1
- package/src/services/__tests__/settings.test.ts +22 -3
- package/src/services/bootstrap.ts +7 -3
- package/src/services/collection.ts +3 -3
- package/src/services/export.ts +0 -3
- package/src/services/navigation.ts +0 -2
- package/src/services/path.ts +1 -38
- package/src/services/post.ts +32 -66
- package/src/services/search.ts +0 -6
- package/src/services/settings.ts +47 -6
- package/src/services/site-admin.ts +6 -1
- package/src/styles/ui.css +12 -23
- package/src/types/entities.ts +0 -1
- package/src/ui/color-themes.ts +1 -1
- package/src/ui/dash/settings/GeneralContent.tsx +17 -19
- package/src/ui/dash/settings/SettingsRootContent.tsx +17 -28
- package/src/ui/feed/NoteCard.tsx +1 -11
- package/src/ui/feed/__tests__/timeline-cards.test.ts +1 -1
- package/src/ui/pages/HomePage.tsx +1 -4
- package/src/ui/pages/PostPage.tsx +2 -0
- package/bin/commands/collections.js +0 -268
- package/bin/commands/media.js +0 -302
- package/bin/commands/posts.js +0 -262
- package/bin/commands/search.js +0 -53
- package/bin/commands/settings.js +0 -93
- package/bin/lib/http-api.js +0 -223
- package/bin/lib/media-upload.js +0 -206
- package/dist/app-Hvqe7Ks_.js +0 -5
- package/dist/client/_assets/client-DDs6NzB3.css +0 -2
- package/src/__tests__/bin/content-cli.test.ts +0 -179
- package/src/__tests__/bin/media-cli.test.ts +0 -192
- /package/dist/{github-api-BkRWnqMx.js → github-api-Bh0PH3zr.js} +0 -0
- /package/dist/{github-app-WeadXMb8.js → github-app-D0GvNnqp.js} +0 -0
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
adoptPendingInlineImageUploads,
|
|
48
48
|
} from "../tiptap/inline-image-upload.js";
|
|
49
49
|
import { isSafeAbsoluteUrl } from "../../lib/url.js";
|
|
50
|
+
import { randomUUID } from "../random-uuid.js";
|
|
50
51
|
|
|
51
52
|
interface ComposeFilePickerCloseDetail {
|
|
52
53
|
cancelled: boolean;
|
|
@@ -220,7 +221,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
220
221
|
private _lastEditorSelection: ComposeEditorSelection | null = null;
|
|
221
222
|
private _emojiPickerEl: HTMLElement | null = null;
|
|
222
223
|
private _emojiContainer: HTMLElement | null = null;
|
|
223
|
-
private readonly _urlStatusId = `compose-url-status-${
|
|
224
|
+
private readonly _urlStatusId = `compose-url-status-${randomUUID()}`;
|
|
224
225
|
private _onDocClickBound = this._onDocumentClick.bind(this);
|
|
225
226
|
private _scrollBufferApplied = false;
|
|
226
227
|
private _filePickerCleanup: (() => void) | null = null;
|
|
@@ -880,7 +881,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
880
881
|
// Convert media attachments to ComposeAttachment[] with status "done"
|
|
881
882
|
if (data.media?.length) {
|
|
882
883
|
const attachments = data.media.map((m) => ({
|
|
883
|
-
clientId:
|
|
884
|
+
clientId: randomUUID(),
|
|
884
885
|
file: new File([], m.originalName ?? "existing", { type: m.mimeType }),
|
|
885
886
|
previewUrl: m.previewUrl,
|
|
886
887
|
posterUrl: null,
|
|
@@ -906,7 +907,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
906
907
|
// Invalid JSON — leave as null
|
|
907
908
|
}
|
|
908
909
|
return {
|
|
909
|
-
clientId: t.clientId ??
|
|
910
|
+
clientId: t.clientId ?? randomUUID(),
|
|
910
911
|
bodyJson: parsed,
|
|
911
912
|
bodyHtml: t.bodyHtml ?? "",
|
|
912
913
|
summary: t.summary,
|
|
@@ -979,7 +980,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
979
980
|
|
|
980
981
|
private _openAttachedText() {
|
|
981
982
|
const item: AttachedTextItem = {
|
|
982
|
-
clientId:
|
|
983
|
+
clientId: randomUUID(),
|
|
983
984
|
bodyJson: null,
|
|
984
985
|
bodyHtml: "",
|
|
985
986
|
summary: "",
|
|
@@ -1242,7 +1243,7 @@ export class JantComposeEditor extends LitElement {
|
|
|
1242
1243
|
continue;
|
|
1243
1244
|
}
|
|
1244
1245
|
|
|
1245
|
-
const clientId =
|
|
1246
|
+
const clientId = randomUUID();
|
|
1246
1247
|
const previewUrl = URL.createObjectURL(file);
|
|
1247
1248
|
newAttachments.push({
|
|
1248
1249
|
clientId,
|
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
import { LitElement, html, nothing } from "lit";
|
|
11
11
|
import type { Editor } from "@tiptap/core";
|
|
12
12
|
import { MAX_SITE_NAME_LENGTH } from "../../types.js";
|
|
13
|
+
import {
|
|
14
|
+
getSupportedLocaleEntries,
|
|
15
|
+
getOrBuildEntry,
|
|
16
|
+
type LocaleEntry,
|
|
17
|
+
} from "../../i18n/supported-locales.js";
|
|
13
18
|
import type {
|
|
14
19
|
SettingsInitialData,
|
|
15
20
|
SettingsLabels,
|
|
16
21
|
SettingsTimezone,
|
|
17
22
|
SettingsCjkFont,
|
|
18
|
-
SettingsLanguage,
|
|
19
23
|
} from "./settings-types.js";
|
|
20
24
|
import { showToast } from "../toast.js";
|
|
21
25
|
import {
|
|
@@ -28,7 +32,6 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
28
32
|
labels: { type: Object },
|
|
29
33
|
timezones: { type: Array },
|
|
30
34
|
cjkFonts: { type: Array, attribute: "cjk-fonts" },
|
|
31
|
-
languages: { type: Array },
|
|
32
35
|
siteNameFallback: { type: String, attribute: "sitename-fallback" },
|
|
33
36
|
siteDescriptionFallback: {
|
|
34
37
|
type: String,
|
|
@@ -49,6 +52,8 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
49
52
|
|
|
50
53
|
// Language, CJK & time group
|
|
51
54
|
_siteLanguage: { state: true },
|
|
55
|
+
_localeOpen: { state: true },
|
|
56
|
+
_localeQuery: { state: true },
|
|
52
57
|
_cjkSerifFont: { state: true },
|
|
53
58
|
_timeZone: { state: true },
|
|
54
59
|
_origLocale: { state: true },
|
|
@@ -75,7 +80,6 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
75
80
|
declare labels: SettingsLabels;
|
|
76
81
|
declare timezones: SettingsTimezone[];
|
|
77
82
|
declare cjkFonts: SettingsCjkFont[];
|
|
78
|
-
declare languages: SettingsLanguage[];
|
|
79
83
|
declare siteNameFallback: string;
|
|
80
84
|
declare siteDescriptionFallback: string;
|
|
81
85
|
declare demoMode: boolean;
|
|
@@ -97,6 +101,10 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
97
101
|
|
|
98
102
|
// Language, CJK & time
|
|
99
103
|
declare _siteLanguage: string;
|
|
104
|
+
/** Whether the locale combobox dropdown is currently open. */
|
|
105
|
+
declare _localeOpen: boolean;
|
|
106
|
+
/** Search query inside the locale combobox. */
|
|
107
|
+
declare _localeQuery: string;
|
|
100
108
|
declare _cjkSerifFont: string;
|
|
101
109
|
declare _timeZone: string;
|
|
102
110
|
declare _origLocale: {
|
|
@@ -137,7 +145,6 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
137
145
|
this.labels = {} as SettingsLabels;
|
|
138
146
|
this.timezones = [];
|
|
139
147
|
this.cjkFonts = [];
|
|
140
|
-
this.languages = [];
|
|
141
148
|
this.siteNameFallback = "";
|
|
142
149
|
this.siteDescriptionFallback = "";
|
|
143
150
|
this.demoMode = false;
|
|
@@ -157,6 +164,8 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
157
164
|
this._siteLoading = false;
|
|
158
165
|
|
|
159
166
|
this._siteLanguage = "en";
|
|
167
|
+
this._localeOpen = false;
|
|
168
|
+
this._localeQuery = "";
|
|
160
169
|
this._cjkSerifFont = "off";
|
|
161
170
|
this._timeZone = "UTC";
|
|
162
171
|
this._origLocale = {
|
|
@@ -181,8 +190,16 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
181
190
|
this._searchLoading = false;
|
|
182
191
|
}
|
|
183
192
|
|
|
193
|
+
connectedCallback() {
|
|
194
|
+
super.connectedCallback();
|
|
195
|
+
document.addEventListener("click", this._onLocalePickerDocumentClick);
|
|
196
|
+
document.addEventListener("keydown", this._onLocalePickerKeydown);
|
|
197
|
+
}
|
|
198
|
+
|
|
184
199
|
disconnectedCallback() {
|
|
185
200
|
super.disconnectedCallback();
|
|
201
|
+
document.removeEventListener("click", this._onLocalePickerDocumentClick);
|
|
202
|
+
document.removeEventListener("keydown", this._onLocalePickerKeydown);
|
|
186
203
|
this._descEditor?.destroy();
|
|
187
204
|
this._descEditor = null;
|
|
188
205
|
this._footerEditor?.destroy();
|
|
@@ -387,6 +404,146 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
387
404
|
);
|
|
388
405
|
}
|
|
389
406
|
|
|
407
|
+
// ── Locale combobox ────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
private _filteredLocaleEntries(): LocaleEntry[] {
|
|
410
|
+
const all = getSupportedLocaleEntries();
|
|
411
|
+
const query = this._localeQuery.trim().toLowerCase();
|
|
412
|
+
if (!query) return all;
|
|
413
|
+
return all.filter(
|
|
414
|
+
(e) =>
|
|
415
|
+
e.tag.toLowerCase().includes(query) ||
|
|
416
|
+
e.native.toLowerCase().includes(query) ||
|
|
417
|
+
e.english.toLowerCase().includes(query),
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private _toggleLocalePicker = () => {
|
|
422
|
+
this._localeOpen = !this._localeOpen;
|
|
423
|
+
if (!this._localeOpen) {
|
|
424
|
+
this._localeQuery = "";
|
|
425
|
+
} else {
|
|
426
|
+
// Focus the search input on next paint.
|
|
427
|
+
this.updateComplete.then(() => {
|
|
428
|
+
const input = this.querySelector<HTMLInputElement>(
|
|
429
|
+
"[data-locale-search]",
|
|
430
|
+
);
|
|
431
|
+
input?.focus();
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
private _selectLocale(tag: string) {
|
|
437
|
+
this._siteLanguage = tag;
|
|
438
|
+
this._localeOpen = false;
|
|
439
|
+
this._localeQuery = "";
|
|
440
|
+
this._syncLocaleDirty();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private _onLocalePickerDocumentClick = (e: Event) => {
|
|
444
|
+
if (!this._localeOpen) return;
|
|
445
|
+
const target = e.target as Node | null;
|
|
446
|
+
const picker = this.querySelector("[data-locale-picker]");
|
|
447
|
+
if (picker && target && !picker.contains(target)) {
|
|
448
|
+
this._localeOpen = false;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
private _onLocalePickerKeydown = (e: KeyboardEvent) => {
|
|
453
|
+
if (e.key === "Escape" && this._localeOpen) {
|
|
454
|
+
this._localeOpen = false;
|
|
455
|
+
this._localeQuery = "";
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
private _renderLanguagePicker() {
|
|
460
|
+
const current = getOrBuildEntry(this._siteLanguage || "en");
|
|
461
|
+
const filtered = this._filteredLocaleEntries();
|
|
462
|
+
const searchPlaceholder =
|
|
463
|
+
this.labels.siteLanguageSearchPlaceholder || "Search…";
|
|
464
|
+
const noMatches = this.labels.siteLanguageNoMatches || "No matches.";
|
|
465
|
+
|
|
466
|
+
return html`
|
|
467
|
+
<div class="relative" data-locale-picker>
|
|
468
|
+
<button
|
|
469
|
+
type="button"
|
|
470
|
+
class="input flex w-full items-center justify-between text-left"
|
|
471
|
+
aria-expanded=${this._localeOpen ? "true" : "false"}
|
|
472
|
+
aria-haspopup="listbox"
|
|
473
|
+
aria-labelledby="site-language-label"
|
|
474
|
+
@click=${this._toggleLocalePicker}
|
|
475
|
+
>
|
|
476
|
+
<span class="truncate">
|
|
477
|
+
${current.native}
|
|
478
|
+
<span class="ml-2 text-xs text-muted-foreground">
|
|
479
|
+
${current.tag} · ${Math.round(current.coverage * 100)}% translated
|
|
480
|
+
</span>
|
|
481
|
+
</span>
|
|
482
|
+
<span class="ml-2 text-muted-foreground" aria-hidden="true">▾</span>
|
|
483
|
+
</button>
|
|
484
|
+
|
|
485
|
+
${this._localeOpen
|
|
486
|
+
? html`
|
|
487
|
+
<div
|
|
488
|
+
class="absolute left-0 right-0 top-full z-10 mt-1 max-h-72 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md"
|
|
489
|
+
>
|
|
490
|
+
<div class="border-b p-2">
|
|
491
|
+
<input
|
|
492
|
+
type="text"
|
|
493
|
+
class="input w-full"
|
|
494
|
+
data-locale-search
|
|
495
|
+
placeholder=${searchPlaceholder}
|
|
496
|
+
autocomplete="off"
|
|
497
|
+
spellcheck="false"
|
|
498
|
+
.value=${this._localeQuery}
|
|
499
|
+
@input=${(e: Event) => {
|
|
500
|
+
this._localeQuery = (e.target as HTMLInputElement).value;
|
|
501
|
+
}}
|
|
502
|
+
/>
|
|
503
|
+
</div>
|
|
504
|
+
<div role="listbox" class="max-h-56 overflow-auto py-1">
|
|
505
|
+
${filtered.length === 0
|
|
506
|
+
? html`
|
|
507
|
+
<div class="px-3 py-2 text-sm text-muted-foreground">
|
|
508
|
+
${noMatches}
|
|
509
|
+
</div>
|
|
510
|
+
`
|
|
511
|
+
: filtered.map(
|
|
512
|
+
(entry) => html`
|
|
513
|
+
<button
|
|
514
|
+
type="button"
|
|
515
|
+
role="option"
|
|
516
|
+
aria-selected=${entry.tag === this._siteLanguage
|
|
517
|
+
? "true"
|
|
518
|
+
: "false"}
|
|
519
|
+
class=${[
|
|
520
|
+
"flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm hover:bg-accent",
|
|
521
|
+
entry.tag === this._siteLanguage
|
|
522
|
+
? "bg-accent/60"
|
|
523
|
+
: "",
|
|
524
|
+
].join(" ")}
|
|
525
|
+
@click=${() => this._selectLocale(entry.tag)}
|
|
526
|
+
>
|
|
527
|
+
<span class="flex flex-col">
|
|
528
|
+
<span>${entry.native}</span>
|
|
529
|
+
<span class="text-xs text-muted-foreground">
|
|
530
|
+
${entry.tag} · ${entry.english}
|
|
531
|
+
</span>
|
|
532
|
+
</span>
|
|
533
|
+
<span class="text-xs text-muted-foreground">
|
|
534
|
+
${Math.round(entry.coverage * 100)}% translated
|
|
535
|
+
</span>
|
|
536
|
+
</button>
|
|
537
|
+
`,
|
|
538
|
+
)}
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
`
|
|
542
|
+
: nothing}
|
|
543
|
+
</div>
|
|
544
|
+
`;
|
|
545
|
+
}
|
|
546
|
+
|
|
390
547
|
// ── Feed group helpers ────────────────────────────────────────────
|
|
391
548
|
|
|
392
549
|
private _syncFeedDirty() {
|
|
@@ -652,25 +809,10 @@ export class JantSettingsGeneral extends LitElement {
|
|
|
652
809
|
>
|
|
653
810
|
${this._renderSectionTitle(this.labels.languageAndTime)}
|
|
654
811
|
<div class="field">
|
|
655
|
-
<label class="label"
|
|
656
|
-
|
|
657
|
-
class="select"
|
|
658
|
-
@change=${(e: Event) => {
|
|
659
|
-
this._siteLanguage = (e.target as HTMLSelectElement).value;
|
|
660
|
-
this._syncLocaleDirty();
|
|
661
|
-
}}
|
|
812
|
+
<label id="site-language-label" class="label"
|
|
813
|
+
>${this.labels.siteLanguage}</label
|
|
662
814
|
>
|
|
663
|
-
|
|
664
|
-
(lang) => html`
|
|
665
|
-
<option
|
|
666
|
-
value=${lang.value}
|
|
667
|
-
?selected=${this._siteLanguage === lang.value}
|
|
668
|
-
>
|
|
669
|
-
${lang.label}
|
|
670
|
-
</option>
|
|
671
|
-
`,
|
|
672
|
-
)}
|
|
673
|
-
</select>
|
|
815
|
+
${this._renderLanguagePicker()}
|
|
674
816
|
<p class="text-sm text-muted-foreground mt-1">
|
|
675
817
|
${this.labels.siteLanguageHelp}
|
|
676
818
|
</p>
|
|
@@ -43,6 +43,10 @@ export interface SettingsLabels {
|
|
|
43
43
|
markdownSupported: string;
|
|
44
44
|
siteLanguage: string;
|
|
45
45
|
siteLanguageHelp: string;
|
|
46
|
+
/** Placeholder shown inside the locale combobox search field. */
|
|
47
|
+
siteLanguageSearchPlaceholder: string;
|
|
48
|
+
/** Empty-state message when the search filters out every option. */
|
|
49
|
+
siteLanguageNoMatches: string;
|
|
46
50
|
cjkFont: string;
|
|
47
51
|
cjkFontHelp: string;
|
|
48
52
|
timeZone: string;
|
|
@@ -71,12 +75,6 @@ export interface SettingsCjkFont {
|
|
|
71
75
|
label: string;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
/** Site language option for the select dropdown */
|
|
75
|
-
export interface SettingsLanguage {
|
|
76
|
-
value: string;
|
|
77
|
-
label: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
78
|
export interface SettingsInitialData {
|
|
81
79
|
siteName: string;
|
|
82
80
|
siteDescription: string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC4122 v4 UUID generator with fallback for insecure contexts.
|
|
3
|
+
*
|
|
4
|
+
* `crypto.randomUUID()` is only available in secure contexts (HTTPS or
|
|
5
|
+
* localhost). Self-hosted Jant deployments are often accessed over plain
|
|
6
|
+
* HTTP on a LAN address, where the native API is `undefined`. This helper
|
|
7
|
+
* uses the native API when available and falls back to a `Math.random`-based
|
|
8
|
+
* v4 string otherwise. The fallback is not cryptographically strong, but
|
|
9
|
+
* client-side IDs here only need to be unique within a single page session.
|
|
10
|
+
*/
|
|
11
|
+
export const randomUUID = (): string => {
|
|
12
|
+
if (
|
|
13
|
+
typeof crypto !== "undefined" &&
|
|
14
|
+
typeof crypto.randomUUID === "function"
|
|
15
|
+
) {
|
|
16
|
+
return crypto.randomUUID();
|
|
17
|
+
}
|
|
18
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
19
|
+
const r = (Math.random() * 16) | 0;
|
|
20
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
21
|
+
return v.toString(16);
|
|
22
|
+
});
|
|
23
|
+
};
|
package/src/client-auth.ts
CHANGED
|
@@ -62,10 +62,7 @@ describe("migration rehearsal", () => {
|
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
expect(
|
|
65
|
-
queryLocalCount(
|
|
66
|
-
persistDir,
|
|
67
|
-
"SELECT COUNT(*) AS count FROM post WHERE deleted_at IS NULL",
|
|
68
|
-
),
|
|
65
|
+
queryLocalCount(persistDir, "SELECT COUNT(*) AS count FROM post"),
|
|
69
66
|
).toBeGreaterThan(0);
|
|
70
67
|
expect(
|
|
71
68
|
queryLocalCount(persistDir, "SELECT COUNT(*) AS count FROM post_fts"),
|
|
@@ -73,5 +70,5 @@ describe("migration rehearsal", () => {
|
|
|
73
70
|
} finally {
|
|
74
71
|
rmSync(persistDir, { recursive: true, force: true });
|
|
75
72
|
}
|
|
76
|
-
},
|
|
73
|
+
}, 600_000);
|
|
77
74
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- Register apple-touch icons in the media table.
|
|
2
|
+
--
|
|
3
|
+
-- Pre-JAN-6, apple-touch icons were uploaded to storage but only referenced
|
|
4
|
+
-- via the SITE_FAVICON_APPLE_TOUCH setting; they had no corresponding media
|
|
5
|
+
-- row. The snapshot logic compensated with a hardcoded list of "storage
|
|
6
|
+
-- setting keys", and the dashboard's media manager couldn't see the file.
|
|
7
|
+
--
|
|
8
|
+
-- JAN-6 makes apple-touch a first-class media entry so snapshot/admin
|
|
9
|
+
-- behavior is uniform with avatar. This backfill creates the missing media
|
|
10
|
+
-- rows for sites that already had apple-touch icons uploaded.
|
|
11
|
+
--
|
|
12
|
+
-- The size column is a placeholder (1). We have no way to read the real byte
|
|
13
|
+
-- length from pure SQL, and the field is only used for storage budgeting
|
|
14
|
+
-- that doesn't apply to favicon assets. Re-uploading the apple-touch icon
|
|
15
|
+
-- through the dashboard refreshes size to the accurate value.
|
|
16
|
+
--
|
|
17
|
+
-- The provider is borrowed from an existing media row in the same site so
|
|
18
|
+
-- the (provider, storage_key) unique index stays consistent with how that
|
|
19
|
+
-- site's other media is tracked.
|
|
20
|
+
INSERT INTO "media" (
|
|
21
|
+
"id",
|
|
22
|
+
"site_id",
|
|
23
|
+
"filename",
|
|
24
|
+
"original_name",
|
|
25
|
+
"mime_type",
|
|
26
|
+
"size",
|
|
27
|
+
"storage_key",
|
|
28
|
+
"provider",
|
|
29
|
+
"position",
|
|
30
|
+
"media_kind",
|
|
31
|
+
"created_at",
|
|
32
|
+
"updated_at"
|
|
33
|
+
)
|
|
34
|
+
SELECT
|
|
35
|
+
'med_apt_' || substr(s."site_id", 5),
|
|
36
|
+
s."site_id",
|
|
37
|
+
'apple-touch-icon.png',
|
|
38
|
+
'apple-touch-icon.png',
|
|
39
|
+
'image/png',
|
|
40
|
+
1,
|
|
41
|
+
s."value",
|
|
42
|
+
COALESCE(
|
|
43
|
+
(
|
|
44
|
+
SELECT m."provider"
|
|
45
|
+
FROM "media" m
|
|
46
|
+
WHERE m."site_id" = s."site_id"
|
|
47
|
+
ORDER BY m."created_at"
|
|
48
|
+
LIMIT 1
|
|
49
|
+
),
|
|
50
|
+
'r2'
|
|
51
|
+
),
|
|
52
|
+
'a0',
|
|
53
|
+
'image',
|
|
54
|
+
s."updated_at",
|
|
55
|
+
s."updated_at"
|
|
56
|
+
FROM "site_setting" s
|
|
57
|
+
WHERE s."key" = 'SITE_FAVICON_APPLE_TOUCH'
|
|
58
|
+
AND s."value" IS NOT NULL
|
|
59
|
+
AND trim(s."value") <> ''
|
|
60
|
+
AND NOT EXISTS (
|
|
61
|
+
SELECT 1
|
|
62
|
+
FROM "media" m
|
|
63
|
+
WHERE m."site_id" = s."site_id"
|
|
64
|
+
AND m."storage_key" = s."value"
|
|
65
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
DELETE FROM `post` WHERE `deleted_at` IS NOT NULL;--> statement-breakpoint
|
|
2
|
+
DROP INDEX `idx_post_site_thread_live_created`;--> statement-breakpoint
|
|
3
|
+
DROP INDEX `idx_post_site_status_deleted_published`;--> statement-breakpoint
|
|
4
|
+
DROP INDEX `idx_post_site_status_deleted_activity`;--> statement-breakpoint
|
|
5
|
+
DROP INDEX `idx_post_site_root_live_published_activity`;--> statement-breakpoint
|
|
6
|
+
DROP INDEX `idx_post_site_root_live_draft_updated`;--> statement-breakpoint
|
|
7
|
+
DROP INDEX `idx_post_site_reply_live_thread_created`;--> statement-breakpoint
|
|
8
|
+
DROP INDEX `idx_post_site_featured_live_featured_at`;--> statement-breakpoint
|
|
9
|
+
CREATE INDEX `idx_post_site_thread_created` ON `post` (`site_id`,`thread_id`,`created_at`,`id`);--> statement-breakpoint
|
|
10
|
+
CREATE INDEX `idx_post_site_status_published` ON `post` (`site_id`,`status`,`published_at`);--> statement-breakpoint
|
|
11
|
+
CREATE INDEX `idx_post_site_status_activity` ON `post` (`site_id`,`status`,`last_activity_at`);--> statement-breakpoint
|
|
12
|
+
CREATE INDEX `idx_post_site_root_published_activity` ON `post` (`site_id`,`last_activity_at`,`id`) WHERE "post"."reply_to_id" IS NULL AND "post"."status" = 'published';--> statement-breakpoint
|
|
13
|
+
CREATE INDEX `idx_post_site_root_draft_updated` ON `post` (`site_id`,`updated_at`,`id`) WHERE "post"."reply_to_id" IS NULL AND "post"."status" = 'draft';--> statement-breakpoint
|
|
14
|
+
CREATE INDEX `idx_post_site_reply_thread_created` ON `post` (`site_id`,`thread_id`,`created_at`,`id`) WHERE "post"."reply_to_id" IS NOT NULL AND "post"."status" = 'published';--> statement-breakpoint
|
|
15
|
+
CREATE INDEX `idx_post_site_featured_featured_at` ON `post` (`site_id`,`featured_at`,`thread_id`,`id`) WHERE "post"."status" = 'published' AND "post"."featured_at" IS NOT NULL;--> statement-breakpoint
|
|
16
|
+
ALTER TABLE `post` DROP COLUMN `deleted_at`;
|