@jant/core 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/bin/commands/telegram/register-webhooks.js +93 -0
  2. package/dist/{app-C481ssbr.js → app-BIkkbVQk.js} +2252 -383
  3. package/dist/app-Bcr5_wZI.js +6 -0
  4. package/dist/client/.vite/manifest.json +3 -3
  5. package/dist/client/_assets/client-Bo7sKkAQ.js +274 -0
  6. package/dist/client/_assets/client-QHRvzZwk.css +2 -0
  7. package/dist/client/_assets/{client-auth-CfBiCAB7.js → client-auth-D1jDQgbH.js} +49 -49
  8. package/dist/{env-CgaH9Mut.js → env-C7e2Nlnt.js} +30 -1
  9. package/dist/{export-CR9Megtb.js → export-Bbn86HmS.js} +1 -1
  10. package/dist/{github-sync-DYZq9rQp.js → github-sync-CBQPRZ8H.js} +1 -1
  11. package/dist/{github-sync-8Vv06aCr.js → github-sync-dXsiZa_e.js} +2 -2
  12. package/dist/index.js +4 -4
  13. package/dist/node.js +61 -5
  14. package/package.json +2 -1
  15. package/src/__tests__/helpers/app.ts +15 -2
  16. package/src/app.tsx +3 -0
  17. package/src/client/components/jant-compose-editor.ts +72 -0
  18. package/src/client/thread-context.ts +146 -2
  19. package/src/client/tiptap/__tests__/link-toolbar.test.ts +1 -1
  20. package/src/client/tiptap/bubble-menu.ts +1 -16
  21. package/src/client/tiptap/extensions.ts +2 -6
  22. package/src/client/tiptap/link-toolbar.ts +0 -21
  23. package/src/client/tiptap/paste-media.ts +49 -33
  24. package/src/client/tiptap/toolbar-mode.ts +0 -43
  25. package/src/client/video-processor.ts +9 -0
  26. package/src/db/migrations/0022_old_gressill.sql +24 -0
  27. package/src/db/migrations/0023_broad_terror.sql +20 -0
  28. package/src/db/migrations/0024_red_the_twelve.sql +3 -0
  29. package/src/db/migrations/0025_exotic_wendell_rand.sql +1 -0
  30. package/src/db/migrations/meta/0022_snapshot.json +2267 -0
  31. package/src/db/migrations/meta/0023_snapshot.json +2396 -0
  32. package/src/db/migrations/meta/0024_snapshot.json +2417 -0
  33. package/src/db/migrations/meta/0025_snapshot.json +2424 -0
  34. package/src/db/migrations/meta/_journal.json +28 -0
  35. package/src/db/migrations/pg/0020_bizarre_smasher.sql +24 -0
  36. package/src/db/migrations/pg/0021_sharp_puppet_master.sql +20 -0
  37. package/src/db/migrations/pg/0022_blushing_blue_shield.sql +3 -0
  38. package/src/db/migrations/pg/0023_organic_zemo.sql +1 -0
  39. package/src/db/migrations/pg/meta/0020_snapshot.json +2904 -0
  40. package/src/db/migrations/pg/meta/0021_snapshot.json +3060 -0
  41. package/src/db/migrations/pg/meta/0022_snapshot.json +3078 -0
  42. package/src/db/migrations/pg/meta/0023_snapshot.json +3084 -0
  43. package/src/db/migrations/pg/meta/_journal.json +28 -0
  44. package/src/db/pg/schema.ts +82 -0
  45. package/src/db/schema.ts +90 -0
  46. package/src/i18n/coverage.generated.ts +2 -2
  47. package/src/i18n/locales/public/en.po +8 -0
  48. package/src/i18n/locales/public/zh-Hans.po +8 -0
  49. package/src/i18n/locales/public/zh-Hant.po +8 -0
  50. package/src/i18n/locales/settings/en.po +135 -0
  51. package/src/i18n/locales/settings/en.ts +1 -1
  52. package/src/i18n/locales/settings/zh-Hans.po +136 -1
  53. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  54. package/src/i18n/locales/settings/zh-Hant.po +136 -1
  55. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  56. package/src/lib/__tests__/image-dimensions.test.ts +314 -0
  57. package/src/lib/__tests__/mp4-track-flags.test.ts +117 -0
  58. package/src/lib/__tests__/telegram-entities.test.ts +180 -0
  59. package/src/lib/__tests__/telegram-pool-webhooks.test.ts +127 -0
  60. package/src/lib/env.ts +45 -0
  61. package/src/lib/ids.ts +3 -0
  62. package/src/lib/image-dimensions.ts +258 -0
  63. package/src/lib/mp4-track-flags.ts +71 -0
  64. package/src/lib/telegram-entities.ts +240 -0
  65. package/src/lib/telegram-pool-webhooks.ts +86 -0
  66. package/src/lib/telegram-settings-status.tsx +109 -0
  67. package/src/lib/telegram.ts +363 -0
  68. package/src/node/runtime.ts +6 -0
  69. package/src/routes/api/__tests__/telegram.test.ts +612 -0
  70. package/src/routes/api/telegram.ts +782 -0
  71. package/src/routes/api/upload-multipart.ts +34 -12
  72. package/src/routes/api/upload.ts +23 -2
  73. package/src/routes/dash/settings.tsx +131 -1
  74. package/src/routes/pages/__tests__/post-page-title.test.ts +70 -0
  75. package/src/routes/pages/page.tsx +3 -2
  76. package/src/runtime/cloudflare.ts +20 -9
  77. package/src/runtime/node.ts +20 -9
  78. package/src/runtime/site.ts +2 -1
  79. package/src/services/__tests__/telegram.test.ts +148 -0
  80. package/src/services/index.ts +9 -0
  81. package/src/services/telegram.ts +613 -0
  82. package/src/services/upload-session.ts +39 -12
  83. package/src/styles/tokens.css +1 -0
  84. package/src/styles/ui.css +134 -38
  85. package/src/types/app-context.ts +6 -0
  86. package/src/types/bindings.ts +3 -0
  87. package/src/types/config.ts +40 -0
  88. package/src/ui/dash/settings/SettingsRootContent.tsx +48 -17
  89. package/src/ui/dash/settings/TelegramContent.tsx +549 -0
  90. package/src/ui/feed/ThreadPreview.tsx +91 -38
  91. package/src/ui/feed/__tests__/thread-preview.test.ts +67 -5
  92. package/src/ui/pages/PostPage.tsx +78 -15
  93. package/dist/app-BgMwEN-M.js +0 -6
  94. package/dist/client/_assets/client-CJQYvkEx.js +0 -274
  95. package/dist/client/_assets/client-CQvi1Buw.css +0 -2
@@ -85,9 +85,7 @@ export function createSettingsEditorExtensions(
85
85
  BubbleMenu.configure({
86
86
  toolbarMode: "compose",
87
87
  }),
88
- LinkToolbar.configure({
89
- toolbarMode: "compose",
90
- }),
88
+ LinkToolbar,
91
89
  ];
92
90
  }
93
91
 
@@ -118,9 +116,7 @@ export function createEditorExtensions(
118
116
  BubbleMenu.configure({
119
117
  toolbarMode: options.toolbarMode ?? "default",
120
118
  }),
121
- LinkToolbar.configure({
122
- toolbarMode: options.toolbarMode ?? "default",
123
- }),
119
+ LinkToolbar,
124
120
  ExitableMarks,
125
121
  InsertParagraphAround,
126
122
  TabIndent,
@@ -13,11 +13,6 @@ import { Extension } from "@tiptap/core";
13
13
  import { Plugin, PluginKey } from "@tiptap/pm/state";
14
14
  import type { EditorState } from "@tiptap/pm/state";
15
15
  import type { EditorView } from "@tiptap/pm/view";
16
- import {
17
- applyDockedToolbarOffset,
18
- isComposeDockedToolbar,
19
- type FormattingToolbarMode,
20
- } from "./toolbar-mode.js";
21
16
  import {
22
17
  getFixedFloatingContainerRect,
23
18
  getFloatingPosition,
@@ -96,15 +91,8 @@ function getLinkRange(state: EditorState): LinkRange | null {
96
91
  export const LinkToolbar = Extension.create({
97
92
  name: "linkToolbar",
98
93
 
99
- addOptions() {
100
- return {
101
- toolbarMode: "default" as FormattingToolbarMode,
102
- };
103
- },
104
-
105
94
  addProseMirrorPlugins() {
106
95
  const editor = this.editor;
107
- const toolbarMode = this.options.toolbarMode as FormattingToolbarMode;
108
96
 
109
97
  // DOM elements
110
98
  let inputEl: HTMLElement | null = null;
@@ -204,17 +192,8 @@ export const LinkToolbar = Extension.create({
204
192
  from: number,
205
193
  to: number,
206
194
  ) {
207
- const docked = isComposeDockedToolbar(toolbarMode);
208
- el.classList.toggle("tiptap-link-input-docked", docked);
209
195
  el.style.display = "flex";
210
196
 
211
- if (docked) {
212
- applyDockedToolbarOffset(el, view);
213
- el.style.removeProperty("left");
214
- el.style.removeProperty("top");
215
- return;
216
- }
217
-
218
197
  const dialog = view.dom.closest("dialog");
219
198
  const start = view.coordsAtPos(from);
220
199
  const end = view.coordsAtPos(to);
@@ -76,6 +76,47 @@ export const PasteMedia = Extension.create<PasteMediaOptions>({
76
76
  addProseMirrorPlugins() {
77
77
  const extension = this;
78
78
 
79
+ /**
80
+ * Routes dropped/pasted files into inline images or attachments using the
81
+ * same decision as the host (`shouldInsertInline`). Returns false when
82
+ * there is nothing this extension can handle, so the caller leaves the
83
+ * event to the editor's default behavior.
84
+ */
85
+ const routeFiles = (files: File[]): boolean => {
86
+ const inlineFiles = files.filter(
87
+ (file) => extension.options.shouldInsertInline?.(file) === true,
88
+ );
89
+ const attachmentFiles = files.filter(
90
+ (file) => !inlineFiles.includes(file),
91
+ );
92
+
93
+ if (
94
+ inlineFiles.length === 0 &&
95
+ (attachmentFiles.length === 0 ||
96
+ extension.options.onPasteFiles === undefined)
97
+ ) {
98
+ return false;
99
+ }
100
+
101
+ for (const file of inlineFiles) {
102
+ const uploadInlineImage = extension.options.uploadInlineImage;
103
+ if (uploadInlineImage) {
104
+ void uploadInlineImage(file);
105
+ continue;
106
+ }
107
+ void uploadAndInsertInlineImage(extension.editor, file);
108
+ }
109
+
110
+ if (
111
+ attachmentFiles.length > 0 &&
112
+ extension.options.onPasteFiles !== undefined
113
+ ) {
114
+ extension.options.onPasteFiles(attachmentFiles);
115
+ }
116
+
117
+ return true;
118
+ };
119
+
79
120
  return [
80
121
  new Plugin({
81
122
  key: pasteMediaPluginKey,
@@ -83,40 +124,15 @@ export const PasteMedia = Extension.create<PasteMediaOptions>({
83
124
  handlePaste(_view, event) {
84
125
  const files = getClipboardFiles(event.clipboardData);
85
126
  if (files.length === 0) return false;
86
-
87
- const inlineFiles = files.filter(
88
- (file) => extension.options.shouldInsertInline?.(file) === true,
89
- );
90
- const attachmentFiles = files.filter(
91
- (file) => !inlineFiles.includes(file),
92
- );
93
-
94
- if (
95
- inlineFiles.length === 0 &&
96
- (attachmentFiles.length === 0 ||
97
- extension.options.onPasteFiles === undefined)
98
- ) {
99
- return false;
100
- }
101
-
127
+ if (!routeFiles(files)) return false;
128
+ event.preventDefault();
129
+ return true;
130
+ },
131
+ handleDrop(_view, event) {
132
+ const files = getClipboardFiles(event.dataTransfer);
133
+ if (files.length === 0) return false;
134
+ if (!routeFiles(files)) return false;
102
135
  event.preventDefault();
103
-
104
- for (const file of inlineFiles) {
105
- const uploadInlineImage = extension.options.uploadInlineImage;
106
- if (uploadInlineImage) {
107
- void uploadInlineImage(file);
108
- continue;
109
- }
110
- void uploadAndInsertInlineImage(extension.editor, file);
111
- }
112
-
113
- if (
114
- attachmentFiles.length > 0 &&
115
- extension.options.onPasteFiles !== undefined
116
- ) {
117
- extension.options.onPasteFiles(attachmentFiles);
118
- }
119
-
120
136
  return true;
121
137
  },
122
138
  },
@@ -1,44 +1 @@
1
- import type { EditorView } from "@tiptap/pm/view";
2
-
3
1
  export type FormattingToolbarMode = "default" | "compose";
4
-
5
- // Dock the formatting toolbar to the bottom of the compose surface only on
6
- // touch-first devices. `(pointer: coarse)` targets the primary input, so real
7
- // mobile/tablets match but a narrow desktop window (e.g. browser used as a
8
- // sidebar) keeps the floating bubble menu near the selection.
9
- const MOBILE_FORMATTING_QUERY = "(pointer: coarse)";
10
-
11
- export function isComposeDockedToolbar(mode: FormattingToolbarMode): boolean {
12
- return (
13
- mode === "compose" &&
14
- (globalThis.matchMedia?.(MOBILE_FORMATTING_QUERY).matches ?? false)
15
- );
16
- }
17
-
18
- export function applyDockedToolbarOffset(
19
- el: HTMLElement,
20
- view: EditorView,
21
- ): void {
22
- const composeEditor = view.dom.closest("jant-compose-editor");
23
- const root =
24
- view.dom.closest(
25
- "jant-compose-dialog, .compose-fullscreen, .compose-attached-panel",
26
- ) ?? view.dom.closest("dialog");
27
-
28
- let offset = 16;
29
- const toolsRow =
30
- composeEditor?.querySelector<HTMLElement>(".compose-tools-row") ??
31
- root?.querySelector<HTMLElement>(".compose-tools-row");
32
- const actionRow = root?.querySelector<HTMLElement>(".compose-action-row");
33
- const attachmentDock = composeEditor?.querySelector<HTMLElement>(
34
- ".compose-attachments-dock",
35
- );
36
-
37
- if (toolsRow) offset += toolsRow.getBoundingClientRect().height;
38
- if (actionRow) offset += actionRow.getBoundingClientRect().height;
39
- if (attachmentDock) {
40
- offset += attachmentDock.getBoundingClientRect().height;
41
- }
42
-
43
- el.style.setProperty("--tiptap-docked-offset", `${offset}px`);
44
- }
@@ -7,6 +7,8 @@
7
7
  * - Strips spurious rotation metadata from the output (mediabunny may
8
8
  * bake rotation into pixels AND write a display matrix, causing the
9
9
  * browser to double-rotate)
10
+ * - Clears the alternate_group track flag (mediabunny sets it non-zero,
11
+ * which stops Safari's native video controls from auto-hiding)
10
12
  * - Extracts poster frame + blurhash during processing
11
13
  *
12
14
  * Requires WebCodecs API support — check `isSupported()` before use.
@@ -25,6 +27,7 @@ import {
25
27
  } from "mediabunny";
26
28
  import { encode } from "blurhash";
27
29
  import { normalizeDurationSeconds } from "../lib/video-playback.js";
30
+ import { zeroTrackAlternateGroups } from "../lib/mp4-track-flags.js";
28
31
 
29
32
  /** Maximum pixels for the long edge of the output video. */
30
33
  const MAX_LONG_EDGE = 1920;
@@ -222,6 +225,12 @@ async function processToFile(
222
225
  const buffer = target.buffer;
223
226
  if (!buffer) throw new Error("Video processing produced no output");
224
227
 
228
+ // Mediabunny tags each track with a non-zero alternate_group, which makes
229
+ // Safari treat tracks as mutually exclusive alternates and never auto-hide
230
+ // the native <video> control bar during playback. Zero it so the controls
231
+ // behave like any other MP4.
232
+ zeroTrackAlternateGroups(buffer);
233
+
225
234
  // Detect whether this browser double-rotates. Chrome's WebCodecs
226
235
  // bakes rotation into the pixel data AND mediabunny writes a display
227
236
  // matrix → the browser applies the matrix again (double-rotation).
@@ -0,0 +1,24 @@
1
+ CREATE TABLE `telegram_binding` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `site_id` text NOT NULL,
4
+ `bot_id` text NOT NULL,
5
+ `telegram_user_id` text NOT NULL,
6
+ `telegram_username` text,
7
+ `last_update_id` integer,
8
+ `bound_at` integer NOT NULL,
9
+ FOREIGN KEY (`site_id`) REFERENCES `site`(`id`) ON UPDATE no action ON DELETE cascade
10
+ );
11
+ --> statement-breakpoint
12
+ CREATE UNIQUE INDEX `uq_telegram_binding_site_id` ON `telegram_binding` (`site_id`);--> statement-breakpoint
13
+ CREATE UNIQUE INDEX `uq_telegram_binding_bot_user` ON `telegram_binding` (`bot_id`,`telegram_user_id`);--> statement-breakpoint
14
+ CREATE TABLE `telegram_pending_binding` (
15
+ `id` text PRIMARY KEY NOT NULL,
16
+ `site_id` text NOT NULL,
17
+ `code` text NOT NULL,
18
+ `created_at` integer NOT NULL,
19
+ `expires_at` integer NOT NULL,
20
+ FOREIGN KEY (`site_id`) REFERENCES `site`(`id`) ON UPDATE no action ON DELETE cascade
21
+ );
22
+ --> statement-breakpoint
23
+ CREATE UNIQUE INDEX `uq_telegram_pending_binding_site_id` ON `telegram_pending_binding` (`site_id`);--> statement-breakpoint
24
+ CREATE UNIQUE INDEX `uq_telegram_pending_binding_code` ON `telegram_pending_binding` (`code`);
@@ -0,0 +1,20 @@
1
+ CREATE TABLE `telegram_media_group_item` (
2
+ `id` text PRIMARY KEY NOT NULL,
3
+ `site_id` text NOT NULL,
4
+ `bot_id` text NOT NULL,
5
+ `telegram_user_id` text NOT NULL,
6
+ `media_group_id` text NOT NULL,
7
+ `chat_id` integer NOT NULL,
8
+ `message_id` integer NOT NULL,
9
+ `update_id` integer NOT NULL,
10
+ `file_id` text NOT NULL,
11
+ `media_kind` text NOT NULL,
12
+ `mime_type` text,
13
+ `original_name` text,
14
+ `caption_markdown` text,
15
+ `created_at` integer NOT NULL,
16
+ FOREIGN KEY (`site_id`) REFERENCES `site`(`id`) ON UPDATE no action ON DELETE cascade
17
+ );
18
+ --> statement-breakpoint
19
+ CREATE INDEX `idx_telegram_media_group_item_group` ON `telegram_media_group_item` (`bot_id`,`media_group_id`);--> statement-breakpoint
20
+ CREATE UNIQUE INDEX `uq_telegram_media_group_item_message` ON `telegram_media_group_item` (`bot_id`,`media_group_id`,`message_id`);
@@ -0,0 +1,3 @@
1
+ ALTER TABLE `telegram_media_group_item` ADD `width` integer;--> statement-breakpoint
2
+ ALTER TABLE `telegram_media_group_item` ADD `height` integer;--> statement-breakpoint
3
+ ALTER TABLE `telegram_media_group_item` ADD `duration_seconds` integer;
@@ -0,0 +1 @@
1
+ ALTER TABLE `telegram_media_group_item` ADD `poster_file_id` text;