@jant/core 0.5.4 → 0.6.1

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 (90) hide show
  1. package/bin/commands/telegram/register-webhooks.js +93 -0
  2. package/dist/app-CMSW_AYG.js +6 -0
  3. package/dist/{app-BtNdUAqz.js → app-DYQdDMs8.js} +2249 -387
  4. package/dist/client/.vite/manifest.json +3 -3
  5. package/dist/client/_assets/client-BRTh1ii1.js +274 -0
  6. package/dist/client/_assets/client-CO4b-RKd.css +2 -0
  7. package/dist/client/_assets/{client-auth-DJ_5wx9N.js → client-auth-CSNcTJwP.js} +81 -81
  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/thread-context.ts +146 -2
  18. package/src/client/tiptap/__tests__/link-toolbar.test.ts +1 -1
  19. package/src/client/tiptap/bubble-menu.ts +1 -16
  20. package/src/client/tiptap/extensions.ts +2 -6
  21. package/src/client/tiptap/link-toolbar.ts +0 -21
  22. package/src/client/tiptap/toolbar-mode.ts +0 -43
  23. package/src/db/migrations/0022_old_gressill.sql +24 -0
  24. package/src/db/migrations/0023_broad_terror.sql +20 -0
  25. package/src/db/migrations/0024_red_the_twelve.sql +3 -0
  26. package/src/db/migrations/0025_exotic_wendell_rand.sql +1 -0
  27. package/src/db/migrations/meta/0022_snapshot.json +2267 -0
  28. package/src/db/migrations/meta/0023_snapshot.json +2396 -0
  29. package/src/db/migrations/meta/0024_snapshot.json +2417 -0
  30. package/src/db/migrations/meta/0025_snapshot.json +2424 -0
  31. package/src/db/migrations/meta/_journal.json +28 -0
  32. package/src/db/migrations/pg/0020_bizarre_smasher.sql +24 -0
  33. package/src/db/migrations/pg/0021_sharp_puppet_master.sql +20 -0
  34. package/src/db/migrations/pg/0022_blushing_blue_shield.sql +3 -0
  35. package/src/db/migrations/pg/0023_organic_zemo.sql +1 -0
  36. package/src/db/migrations/pg/meta/0020_snapshot.json +2904 -0
  37. package/src/db/migrations/pg/meta/0021_snapshot.json +3060 -0
  38. package/src/db/migrations/pg/meta/0022_snapshot.json +3078 -0
  39. package/src/db/migrations/pg/meta/0023_snapshot.json +3084 -0
  40. package/src/db/migrations/pg/meta/_journal.json +28 -0
  41. package/src/db/pg/schema.ts +82 -0
  42. package/src/db/schema.ts +90 -0
  43. package/src/i18n/coverage.generated.ts +2 -2
  44. package/src/i18n/locales/public/en.po +8 -0
  45. package/src/i18n/locales/public/zh-Hans.po +8 -0
  46. package/src/i18n/locales/public/zh-Hant.po +8 -0
  47. package/src/i18n/locales/settings/en.po +135 -0
  48. package/src/i18n/locales/settings/en.ts +1 -1
  49. package/src/i18n/locales/settings/zh-Hans.po +136 -1
  50. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  51. package/src/i18n/locales/settings/zh-Hant.po +136 -1
  52. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  53. package/src/lib/__tests__/image-dimensions.test.ts +314 -0
  54. package/src/lib/__tests__/telegram-entities.test.ts +180 -0
  55. package/src/lib/__tests__/telegram-pool-webhooks.test.ts +127 -0
  56. package/src/lib/env.ts +45 -0
  57. package/src/lib/ids.ts +3 -0
  58. package/src/lib/image-dimensions.ts +258 -0
  59. package/src/lib/telegram-entities.ts +240 -0
  60. package/src/lib/telegram-pool-webhooks.ts +86 -0
  61. package/src/lib/telegram-settings-status.tsx +109 -0
  62. package/src/lib/telegram.ts +363 -0
  63. package/src/node/runtime.ts +6 -0
  64. package/src/routes/api/__tests__/telegram.test.ts +612 -0
  65. package/src/routes/api/telegram.ts +782 -0
  66. package/src/routes/api/upload-multipart.ts +34 -12
  67. package/src/routes/api/upload.ts +23 -2
  68. package/src/routes/dash/settings.tsx +131 -1
  69. package/src/routes/pages/__tests__/post-page-title.test.ts +70 -0
  70. package/src/routes/pages/page.tsx +3 -2
  71. package/src/runtime/cloudflare.ts +20 -9
  72. package/src/runtime/node.ts +20 -9
  73. package/src/runtime/site.ts +2 -1
  74. package/src/services/__tests__/telegram.test.ts +148 -0
  75. package/src/services/index.ts +9 -0
  76. package/src/services/telegram.ts +613 -0
  77. package/src/services/upload-session.ts +39 -12
  78. package/src/styles/tokens.css +1 -0
  79. package/src/styles/ui.css +117 -38
  80. package/src/types/app-context.ts +6 -0
  81. package/src/types/bindings.ts +3 -0
  82. package/src/types/config.ts +40 -0
  83. package/src/ui/dash/settings/SettingsRootContent.tsx +48 -17
  84. package/src/ui/dash/settings/TelegramContent.tsx +549 -0
  85. package/src/ui/feed/ThreadPreview.tsx +90 -38
  86. package/src/ui/feed/__tests__/thread-preview.test.ts +66 -5
  87. package/src/ui/pages/PostPage.tsx +77 -15
  88. package/dist/app-DLINgGBd.js +0 -6
  89. package/dist/client/_assets/client-BErXNT6k.css +0 -2
  90. package/dist/client/_assets/client-CtAgWT8i.js +0 -274
@@ -321,6 +321,7 @@
321
321
  transparent
322
322
  );
323
323
  --site-thread-item-spacing: 32px;
324
+ --site-thread-context-max-height: 160px;
324
325
  --site-thread-dot-ring: color-mix(
325
326
  in srgb,
326
327
  var(--site-accent) 16%,
package/src/styles/ui.css CHANGED
@@ -2624,6 +2624,113 @@
2624
2624
  var(--site-thread-dot-ring);
2625
2625
  }
2626
2626
 
2627
+ /* Collapsible ancestor context wrapper.
2628
+ Server-rendered with `data-collapsed`. Crop and fade are always active
2629
+ while data-collapsed is set; JS only toggles the Show more/less button
2630
+ visibility based on real overflow. Negative left margin + matching
2631
+ padding extends the shell's box past the content edge so the dot
2632
+ markers (which live at left:-31.5px on desktop) survive
2633
+ `overflow: hidden`. `box-sizing: content-box` keeps the inner content
2634
+ width identical to before. */
2635
+ .thread-context-shell {
2636
+ position: relative;
2637
+ box-sizing: content-box;
2638
+ margin-left: -40px;
2639
+ padding-left: 40px;
2640
+ transition: max-height 0.32s cubic-bezier(0.4, 0, 0.2, 1);
2641
+ }
2642
+
2643
+ .thread-context-shell[data-collapsed] {
2644
+ max-height: var(--site-thread-context-max-height);
2645
+ overflow: hidden;
2646
+ /* Soft fade at the bottom edge so the cap doesn't make a hard cut
2647
+ through text when content barely overflows. Combined with the
2648
+ shell's opacity:0.5 this produces a smooth "context drifts off"
2649
+ look instead of a guillotine slice right above the toggle. */
2650
+ -webkit-mask-image: linear-gradient(
2651
+ to bottom,
2652
+ black 0%,
2653
+ black 60%,
2654
+ transparent 100%
2655
+ );
2656
+ mask-image: linear-gradient(
2657
+ to bottom,
2658
+ black 0%,
2659
+ black 60%,
2660
+ transparent 100%
2661
+ );
2662
+ }
2663
+
2664
+ /* Push ancestor context slightly into the background — enough to feel
2665
+ secondary to the focused post but not so much that text becomes gray
2666
+ and hard to read. Combined with the mask-image bottom fade this gives
2667
+ a "context drifts into the background" effect without dimming the
2668
+ whole shell to the point of illegibility. */
2669
+ .thread-context-shell {
2670
+ opacity: 0.6;
2671
+ transition: opacity 0.22s ease;
2672
+ }
2673
+
2674
+ .thread-context-shell:not([data-collapsed]) {
2675
+ opacity: 1;
2676
+ }
2677
+
2678
+ /* Tighter padding for context items inside the shell so a single-line
2679
+ root post doesn't leave 64px of empty padding pushing the toggle
2680
+ button far below the visible text. Items outside the shell (hero,
2681
+ descendants on detail pages) keep the default --site-thread-item-spacing
2682
+ so the focused post still gets generous breathing room. */
2683
+ .thread-context-shell .thread-item {
2684
+ padding-top: 16px;
2685
+ padding-bottom: 16px;
2686
+ }
2687
+
2688
+ .thread-context-toggle[hidden] {
2689
+ display: none;
2690
+ }
2691
+
2692
+ .thread-context-toggle {
2693
+ display: inline-flex;
2694
+ align-items: center;
2695
+ gap: 0.3rem;
2696
+ margin: 0 0 6px;
2697
+ padding: 0.3rem 0;
2698
+ border: 0;
2699
+ background: transparent;
2700
+ color: var(--site-text-secondary);
2701
+ font-size: var(--type-thread-context-meta);
2702
+ line-height: 1.2;
2703
+ cursor: pointer;
2704
+ transition: color 0.18s ease;
2705
+ }
2706
+
2707
+ .thread-context-toggle:hover {
2708
+ color: var(--site-text-primary);
2709
+ }
2710
+
2711
+ .thread-context-toggle:focus-visible {
2712
+ outline: 2px solid var(--site-accent);
2713
+ outline-offset: 4px;
2714
+ border-radius: 2px;
2715
+ }
2716
+
2717
+ .thread-context-toggle-chevron {
2718
+ width: 10px;
2719
+ height: 10px;
2720
+ opacity: 0.7;
2721
+ transition:
2722
+ transform 0.2s ease,
2723
+ opacity 0.18s ease;
2724
+ }
2725
+
2726
+ .thread-context-toggle:hover .thread-context-toggle-chevron {
2727
+ opacity: 1;
2728
+ }
2729
+
2730
+ .thread-context-toggle[aria-expanded="true"] .thread-context-toggle-chevron {
2731
+ transform: rotate(180deg);
2732
+ }
2733
+
2627
2734
  .thread-item-gap {
2628
2735
  padding-top: 0.15rem;
2629
2736
  padding-bottom: 0.35rem;
@@ -2701,6 +2808,16 @@
2701
2808
  scroll-margin-top: 80px;
2702
2809
  }
2703
2810
 
2811
+ /* When a child post has an ancestor shell above it (toggle button sits
2812
+ directly before the current post), leave enough room above the scroll
2813
+ landing so the user can see the Show more button + a peek of the
2814
+ faded shell on initial load. Otherwise everything above is hidden in
2815
+ the viewport and the user has to discover it by scrolling up. Sized
2816
+ for the ghost-style toggle (~28px) + a glimpse of the faded shell. */
2817
+ .thread-context-toggle + .thread-detail-item {
2818
+ scroll-margin-top: 120px;
2819
+ }
2820
+
2704
2821
  .thread-group-detail .thread-item-current::before {
2705
2822
  background-color: var(--site-accent);
2706
2823
  box-shadow:
@@ -9681,19 +9798,6 @@
9681
9798
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15);
9682
9799
  }
9683
9800
 
9684
- .tiptap-bubble-menu-docked {
9685
- left: 50% !important;
9686
- top: auto !important;
9687
- bottom: calc(
9688
- env(safe-area-inset-bottom) + var(--tiptap-docked-offset, 16px)
9689
- );
9690
- transform: translateX(-50%);
9691
- gap: 4px;
9692
- padding: 6px;
9693
- border-radius: 12px;
9694
- max-width: calc(100vw - 24px);
9695
- }
9696
-
9697
9801
  .tiptap-bubble-menu::after {
9698
9802
  content: "";
9699
9803
  position: absolute;
@@ -9705,10 +9809,6 @@
9705
9809
  border-top: 5px solid var(--color-foreground);
9706
9810
  }
9707
9811
 
9708
- .tiptap-bubble-menu-docked::after {
9709
- display: none;
9710
- }
9711
-
9712
9812
  .tiptap-bubble-btn {
9713
9813
  display: flex;
9714
9814
  align-items: center;
@@ -9765,11 +9865,6 @@
9765
9865
  flex: 1;
9766
9866
  }
9767
9867
 
9768
- .tiptap-link-input-docked .tiptap-link-input-fields {
9769
- min-width: 0;
9770
- width: 100%;
9771
- }
9772
-
9773
9868
  .tiptap-link-input-text {
9774
9869
  border: none;
9775
9870
  outline: none;
@@ -9785,17 +9880,6 @@
9785
9880
  color: var(--color-muted-foreground);
9786
9881
  }
9787
9882
 
9788
- .tiptap-link-input-docked {
9789
- left: 50% !important;
9790
- top: auto !important;
9791
- bottom: calc(
9792
- env(safe-area-inset-bottom) + var(--tiptap-docked-offset, 16px)
9793
- );
9794
- transform: translateX(-50%);
9795
- width: min(calc(100vw - 24px), 420px);
9796
- max-width: calc(100vw - 24px);
9797
- }
9798
-
9799
9883
  .tiptap-link-input::before {
9800
9884
  content: "";
9801
9885
  position: absolute;
@@ -9818,11 +9902,6 @@
9818
9902
  border-top: 5px solid var(--color-background);
9819
9903
  }
9820
9904
 
9821
- .tiptap-link-input-docked::before,
9822
- .tiptap-link-input-docked::after {
9823
- display: none;
9824
- }
9825
-
9826
9905
  .tiptap-link-input-field {
9827
9906
  border: none;
9828
9907
  outline: none;
@@ -24,6 +24,12 @@ export type AppSession = Awaited<ReturnType<Auth["api"]["getSession"]>>;
24
24
 
25
25
  export interface AppVariables {
26
26
  services: Services;
27
+ /**
28
+ * Builds a `Services` object scoped to an arbitrary site. Used by
29
+ * host-agnostic handlers (e.g. the Telegram webhook) that resolve the
30
+ * target site from request data rather than the hostname.
31
+ */
32
+ servicesForSite: (siteId: string) => Services;
27
33
  hostedHandoff: HostedHandoffService;
28
34
  auth: Auth;
29
35
  currentSite: Site;
@@ -53,6 +53,9 @@ export interface Bindings {
53
53
  HOSTED_CONTROL_PLANE_PROVIDER_NAME?: EnvBindingValue;
54
54
  HOSTED_CONTROL_PLANE_SSO_SECRET?: EnvBindingValue;
55
55
  HOSTED_CONTROL_PLANE_INTERNAL_TOKEN?: EnvBindingValue;
56
+ // Telegram bot integration
57
+ TELEGRAM_BOT_TOKENS?: EnvBindingValue;
58
+ TELEGRAM_WEBHOOK_SECRET?: EnvBindingValue;
56
59
  // Timeline
57
60
  PAGE_SIZE?: EnvBindingValue;
58
61
  SEARCH_PAGE_SIZE?: EnvBindingValue;
@@ -396,6 +396,46 @@ export const CONFIG_FIELDS = {
396
396
  envOnly: true,
397
397
  envKeys: ["GITHUB_APP_WEBHOOK_SECRET"],
398
398
  },
399
+
400
+ // Telegram bot pool (env-only). When TELEGRAM_BOT_TOKENS is set the bot
401
+ // tokens are platform-managed (hosted, or a single-site operator who opts
402
+ // in) and the settings UI hides the token field. Comma-separated list of
403
+ // `<bot_id>:<secret>` tokens; TELEGRAM_WEBHOOK_SECRET is the shared
404
+ // `secret_token` used when registering every pool bot's webhook.
405
+ TELEGRAM_BOT_TOKENS: {
406
+ defaultValue: "",
407
+ envOnly: true,
408
+ envKeys: ["TELEGRAM_BOT_TOKENS"],
409
+ },
410
+ TELEGRAM_WEBHOOK_SECRET: {
411
+ defaultValue: "",
412
+ envOnly: true,
413
+ envKeys: ["TELEGRAM_WEBHOOK_SECRET"],
414
+ },
415
+
416
+ // Telegram bring-your-own bot (DB-only, single-site, managed via the
417
+ // Telegram settings page when TELEGRAM_BOT_TOKENS is not set).
418
+ TELEGRAM_BOT_TOKEN: {
419
+ defaultValue: "",
420
+ envOnly: false,
421
+ internal: true,
422
+ },
423
+ TELEGRAM_BOT_ID: {
424
+ defaultValue: "",
425
+ envOnly: false,
426
+ internal: true,
427
+ },
428
+ TELEGRAM_BOT_USERNAME: {
429
+ defaultValue: "",
430
+ envOnly: false,
431
+ internal: true,
432
+ },
433
+ /** Per-site `secret_token` for a bring-your-own bot's webhook. */
434
+ TELEGRAM_BOT_WEBHOOK_SECRET: {
435
+ defaultValue: "",
436
+ envOnly: false,
437
+ internal: true,
438
+ },
399
439
  } as const satisfies Record<string, ConfigField>;
400
440
 
401
441
  export type ConfigKey = keyof typeof CONFIG_FIELDS;
@@ -24,6 +24,7 @@ const ICONS = {
24
24
  key: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4"/><path d="m21 2-9.6 9.6"/><circle cx="7.5" cy="15.5" r="5.5"/></svg>`,
25
25
  shield: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg>`,
26
26
  gitBranch: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>`,
27
+ send: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/></svg>`,
27
28
  };
28
29
 
29
30
  export function SettingsRootContent({
@@ -114,23 +115,6 @@ export function SettingsRootContent({
114
115
  }),
115
116
  )}
116
117
  />
117
- <SettingsDirectoryLink
118
- href={toPublicPath("/settings/github-sync", sitePathPrefix)}
119
- icon={ICONS.gitBranch}
120
- tone="subtle"
121
- name={i18n._(
122
- msg({
123
- message: "GitHub Sync",
124
- comment: "@context: Settings item — GitHub sync settings",
125
- }),
126
- )}
127
- description={i18n._(
128
- msg({
129
- message: "Back up and sync content with a GitHub repository",
130
- comment: "@context: Settings item description for GitHub sync",
131
- }),
132
- )}
133
- />
134
118
  </SettingsDirectorySection>
135
119
 
136
120
  <SettingsDirectorySection
@@ -225,6 +209,53 @@ export function SettingsRootContent({
225
209
  />
226
210
  </SettingsDirectorySection>
227
211
 
212
+ <SettingsDirectorySection
213
+ title={i18n._(
214
+ msg({
215
+ message: "Integrations",
216
+ comment:
217
+ "@context: Settings group label for third-party integrations",
218
+ }),
219
+ )}
220
+ >
221
+ <SettingsDirectoryLink
222
+ href={toPublicPath("/settings/github-sync", sitePathPrefix)}
223
+ icon={ICONS.gitBranch}
224
+ tone="subtle"
225
+ name={i18n._(
226
+ msg({
227
+ message: "GitHub Sync",
228
+ comment: "@context: Settings item — GitHub sync settings",
229
+ }),
230
+ )}
231
+ description={i18n._(
232
+ msg({
233
+ message: "Back up and sync content with a GitHub repository",
234
+ comment: "@context: Settings item description for GitHub sync",
235
+ }),
236
+ )}
237
+ />
238
+ <SettingsDirectoryLink
239
+ href={toPublicPath("/settings/telegram", sitePathPrefix)}
240
+ icon={ICONS.send}
241
+ tone="subtle"
242
+ name={i18n._(
243
+ msg({
244
+ message: "Telegram",
245
+ comment:
246
+ "@context: Settings item — Telegram integration settings",
247
+ }),
248
+ )}
249
+ description={i18n._(
250
+ msg({
251
+ message: "Post notes by messaging a Telegram bot",
252
+ comment:
253
+ "@context: Settings item description for Telegram integration",
254
+ }),
255
+ )}
256
+ />
257
+ </SettingsDirectorySection>
258
+
228
259
  <SettingsDirectorySection
229
260
  title={i18n._(
230
261
  msg({