@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.
- package/bin/commands/telegram/register-webhooks.js +93 -0
- package/dist/{app-C481ssbr.js → app-BIkkbVQk.js} +2252 -383
- package/dist/app-Bcr5_wZI.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-Bo7sKkAQ.js +274 -0
- package/dist/client/_assets/client-QHRvzZwk.css +2 -0
- package/dist/client/_assets/{client-auth-CfBiCAB7.js → client-auth-D1jDQgbH.js} +49 -49
- package/dist/{env-CgaH9Mut.js → env-C7e2Nlnt.js} +30 -1
- package/dist/{export-CR9Megtb.js → export-Bbn86HmS.js} +1 -1
- package/dist/{github-sync-DYZq9rQp.js → github-sync-CBQPRZ8H.js} +1 -1
- package/dist/{github-sync-8Vv06aCr.js → github-sync-dXsiZa_e.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/node.js +61 -5
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +15 -2
- package/src/app.tsx +3 -0
- package/src/client/components/jant-compose-editor.ts +72 -0
- package/src/client/thread-context.ts +146 -2
- package/src/client/tiptap/__tests__/link-toolbar.test.ts +1 -1
- package/src/client/tiptap/bubble-menu.ts +1 -16
- package/src/client/tiptap/extensions.ts +2 -6
- package/src/client/tiptap/link-toolbar.ts +0 -21
- package/src/client/tiptap/paste-media.ts +49 -33
- package/src/client/tiptap/toolbar-mode.ts +0 -43
- package/src/client/video-processor.ts +9 -0
- package/src/db/migrations/0022_old_gressill.sql +24 -0
- package/src/db/migrations/0023_broad_terror.sql +20 -0
- package/src/db/migrations/0024_red_the_twelve.sql +3 -0
- package/src/db/migrations/0025_exotic_wendell_rand.sql +1 -0
- package/src/db/migrations/meta/0022_snapshot.json +2267 -0
- package/src/db/migrations/meta/0023_snapshot.json +2396 -0
- package/src/db/migrations/meta/0024_snapshot.json +2417 -0
- package/src/db/migrations/meta/0025_snapshot.json +2424 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0020_bizarre_smasher.sql +24 -0
- package/src/db/migrations/pg/0021_sharp_puppet_master.sql +20 -0
- package/src/db/migrations/pg/0022_blushing_blue_shield.sql +3 -0
- package/src/db/migrations/pg/0023_organic_zemo.sql +1 -0
- package/src/db/migrations/pg/meta/0020_snapshot.json +2904 -0
- package/src/db/migrations/pg/meta/0021_snapshot.json +3060 -0
- package/src/db/migrations/pg/meta/0022_snapshot.json +3078 -0
- package/src/db/migrations/pg/meta/0023_snapshot.json +3084 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +82 -0
- package/src/db/schema.ts +90 -0
- package/src/i18n/coverage.generated.ts +2 -2
- package/src/i18n/locales/public/en.po +8 -0
- package/src/i18n/locales/public/zh-Hans.po +8 -0
- package/src/i18n/locales/public/zh-Hant.po +8 -0
- package/src/i18n/locales/settings/en.po +135 -0
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +136 -1
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +136 -1
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/image-dimensions.test.ts +314 -0
- package/src/lib/__tests__/mp4-track-flags.test.ts +117 -0
- package/src/lib/__tests__/telegram-entities.test.ts +180 -0
- package/src/lib/__tests__/telegram-pool-webhooks.test.ts +127 -0
- package/src/lib/env.ts +45 -0
- package/src/lib/ids.ts +3 -0
- package/src/lib/image-dimensions.ts +258 -0
- package/src/lib/mp4-track-flags.ts +71 -0
- package/src/lib/telegram-entities.ts +240 -0
- package/src/lib/telegram-pool-webhooks.ts +86 -0
- package/src/lib/telegram-settings-status.tsx +109 -0
- package/src/lib/telegram.ts +363 -0
- package/src/node/runtime.ts +6 -0
- package/src/routes/api/__tests__/telegram.test.ts +612 -0
- package/src/routes/api/telegram.ts +782 -0
- package/src/routes/api/upload-multipart.ts +34 -12
- package/src/routes/api/upload.ts +23 -2
- package/src/routes/dash/settings.tsx +131 -1
- package/src/routes/pages/__tests__/post-page-title.test.ts +70 -0
- package/src/routes/pages/page.tsx +3 -2
- package/src/runtime/cloudflare.ts +20 -9
- package/src/runtime/node.ts +20 -9
- package/src/runtime/site.ts +2 -1
- package/src/services/__tests__/telegram.test.ts +148 -0
- package/src/services/index.ts +9 -0
- package/src/services/telegram.ts +613 -0
- package/src/services/upload-session.ts +39 -12
- package/src/styles/tokens.css +1 -0
- package/src/styles/ui.css +134 -38
- package/src/types/app-context.ts +6 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +40 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +48 -17
- package/src/ui/dash/settings/TelegramContent.tsx +549 -0
- package/src/ui/feed/ThreadPreview.tsx +91 -38
- package/src/ui/feed/__tests__/thread-preview.test.ts +67 -5
- package/src/ui/pages/PostPage.tsx +78 -15
- package/dist/app-BgMwEN-M.js +0 -6
- package/dist/client/_assets/client-CJQYvkEx.js +0 -274
- package/dist/client/_assets/client-CQvi1Buw.css +0 -2
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram settings page
|
|
3
|
+
*
|
|
4
|
+
* States:
|
|
5
|
+
* 1. Bring-your-own bot, no token saved — token input form
|
|
6
|
+
* 2. Not connected, a bot is available — deep link + QR + binding code
|
|
7
|
+
* 3. Connected — connected account, posting hint, disconnect
|
|
8
|
+
*
|
|
9
|
+
* In env-managed-pool deployments the token field is never shown: the bot
|
|
10
|
+
* pool is platform-owned, so users only ever see the binding code flow.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { msg } from "@lingui/core/macro";
|
|
14
|
+
import { buildConfirmActionExpression } from "../../../lib/confirm.js";
|
|
15
|
+
import { escapeHtml } from "../../../lib/html.js";
|
|
16
|
+
import { toPublicPath } from "../../../lib/url.js";
|
|
17
|
+
import { useLingui } from "../../../i18n/context.js";
|
|
18
|
+
|
|
19
|
+
export interface TelegramSettingsView {
|
|
20
|
+
/** True when `TELEGRAM_BOT_TOKENS` is set — the bot pool is platform-owned. */
|
|
21
|
+
managed: boolean;
|
|
22
|
+
/** Active binding for this site, or null. */
|
|
23
|
+
binding: {
|
|
24
|
+
telegramUsername: string | null;
|
|
25
|
+
boundAt: number;
|
|
26
|
+
} | null;
|
|
27
|
+
/** Bring-your-own only: a bot token has been saved for this site. */
|
|
28
|
+
userBotConfigured: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Connect affordances, present when nothing is connected yet and a bot is
|
|
31
|
+
* available to connect through.
|
|
32
|
+
*/
|
|
33
|
+
connect: {
|
|
34
|
+
code: string;
|
|
35
|
+
deepLink: string;
|
|
36
|
+
/** QR-encoded `deepLink`, rendered SVG markup. */
|
|
37
|
+
qrSvg: string;
|
|
38
|
+
botUsername: string;
|
|
39
|
+
} | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function Spinner({
|
|
43
|
+
signal,
|
|
44
|
+
size = "size-4",
|
|
45
|
+
}: {
|
|
46
|
+
signal: string;
|
|
47
|
+
size?: string;
|
|
48
|
+
}) {
|
|
49
|
+
return (
|
|
50
|
+
<svg
|
|
51
|
+
data-show={`$${signal}`}
|
|
52
|
+
style="display:none"
|
|
53
|
+
class={`animate-spin ${size}`}
|
|
54
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
55
|
+
viewBox="0 0 24 24"
|
|
56
|
+
fill="none"
|
|
57
|
+
stroke="currentColor"
|
|
58
|
+
stroke-width="2"
|
|
59
|
+
stroke-linecap="round"
|
|
60
|
+
stroke-linejoin="round"
|
|
61
|
+
role="status"
|
|
62
|
+
>
|
|
63
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
64
|
+
</svg>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const STATUS_DOT = `<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="5" cy="5" r="5" fill="currentColor"/></svg>`;
|
|
69
|
+
|
|
70
|
+
export function TelegramContent({
|
|
71
|
+
view,
|
|
72
|
+
sitePathPrefix = "",
|
|
73
|
+
streamUrl,
|
|
74
|
+
}: {
|
|
75
|
+
view: TelegramSettingsView;
|
|
76
|
+
sitePathPrefix?: string;
|
|
77
|
+
/**
|
|
78
|
+
* When set and the page is in its "waiting for the user to bind" state,
|
|
79
|
+
* mounts a Datastar SSE subscription so the panel auto-swaps to the
|
|
80
|
+
* connected view the moment a binding lands. The patched-in connected
|
|
81
|
+
* view has no `data-init`, so the stream closes naturally.
|
|
82
|
+
*/
|
|
83
|
+
streamUrl?: string;
|
|
84
|
+
}) {
|
|
85
|
+
const settingsBase = toPublicPath("/settings/telegram", sitePathPrefix);
|
|
86
|
+
|
|
87
|
+
let inner;
|
|
88
|
+
if (view.binding) {
|
|
89
|
+
inner = <TelegramConnected view={view} settingsBase={settingsBase} />;
|
|
90
|
+
} else if (!view.managed && !view.userBotConfigured) {
|
|
91
|
+
inner = <TelegramSetupForm settingsBase={settingsBase} />;
|
|
92
|
+
} else {
|
|
93
|
+
inner = <TelegramConnect view={view} settingsBase={settingsBase} />;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Only the "ready to connect" state subscribes — that's the only time
|
|
97
|
+
// we're actively waiting for an external event (the user messaging the
|
|
98
|
+
// bot). Connected and setup-form states have nothing to poll for.
|
|
99
|
+
const subscribe = !view.binding && view.connect && streamUrl;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
id="telegram-status"
|
|
104
|
+
data-init={subscribe ? `@get('${streamUrl}')` : undefined}
|
|
105
|
+
>
|
|
106
|
+
{inner}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function TelegramSetupForm({ settingsBase }: { settingsBase: string }) {
|
|
112
|
+
const { i18n } = useLingui();
|
|
113
|
+
return (
|
|
114
|
+
<div class="flex flex-col gap-6 max-w-form">
|
|
115
|
+
<div>
|
|
116
|
+
<h2 class="text-lg font-medium mb-1">
|
|
117
|
+
{i18n._(
|
|
118
|
+
msg({
|
|
119
|
+
message: "Telegram",
|
|
120
|
+
comment: "@context: Settings section heading for Telegram setup",
|
|
121
|
+
}),
|
|
122
|
+
)}
|
|
123
|
+
</h2>
|
|
124
|
+
<p class="text-sm text-muted-foreground">
|
|
125
|
+
{i18n._(
|
|
126
|
+
msg({
|
|
127
|
+
message:
|
|
128
|
+
"Connect a Telegram bot, then anything you message it gets published as a note.",
|
|
129
|
+
comment:
|
|
130
|
+
"@context: Intro text on the Telegram settings page when no bot is set up",
|
|
131
|
+
}),
|
|
132
|
+
)}
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<form
|
|
137
|
+
class="flex flex-col gap-4"
|
|
138
|
+
data-on:submit__prevent={`@post('${settingsBase}/connect')`}
|
|
139
|
+
data-indicator="_connecting"
|
|
140
|
+
>
|
|
141
|
+
<div class="field">
|
|
142
|
+
<label class="label" for="telegram-token">
|
|
143
|
+
{i18n._(
|
|
144
|
+
msg({
|
|
145
|
+
message: "Bot token",
|
|
146
|
+
comment: "@context: Label for the Telegram bot token input",
|
|
147
|
+
}),
|
|
148
|
+
)}
|
|
149
|
+
</label>
|
|
150
|
+
<input
|
|
151
|
+
id="telegram-token"
|
|
152
|
+
data-bind="token"
|
|
153
|
+
type="password"
|
|
154
|
+
class="input"
|
|
155
|
+
placeholder="123456789:ABC..."
|
|
156
|
+
required
|
|
157
|
+
autocomplete="off"
|
|
158
|
+
/>
|
|
159
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
160
|
+
{i18n._(
|
|
161
|
+
msg({
|
|
162
|
+
message:
|
|
163
|
+
"Create a bot by messaging @BotFather on Telegram, then paste the token it gives you.",
|
|
164
|
+
comment:
|
|
165
|
+
"@context: Help text explaining where to get a Telegram bot token",
|
|
166
|
+
}),
|
|
167
|
+
)}
|
|
168
|
+
</p>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="flex mt-2">
|
|
171
|
+
<button type="submit" class="btn" data-attr:disabled="$_connecting">
|
|
172
|
+
<Spinner signal="_connecting" />
|
|
173
|
+
{i18n._(
|
|
174
|
+
msg({
|
|
175
|
+
message: "Save bot token",
|
|
176
|
+
comment: "@context: Button to save the Telegram bot token",
|
|
177
|
+
}),
|
|
178
|
+
)}
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
</form>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function TelegramConnect({
|
|
187
|
+
view,
|
|
188
|
+
settingsBase,
|
|
189
|
+
}: {
|
|
190
|
+
view: TelegramSettingsView;
|
|
191
|
+
settingsBase: string;
|
|
192
|
+
}) {
|
|
193
|
+
const { i18n } = useLingui();
|
|
194
|
+
const connect = view.connect;
|
|
195
|
+
|
|
196
|
+
if (!connect) {
|
|
197
|
+
return (
|
|
198
|
+
<div class="flex flex-col gap-4 max-w-form">
|
|
199
|
+
<div class="alert">
|
|
200
|
+
<span>
|
|
201
|
+
{i18n._(
|
|
202
|
+
msg({
|
|
203
|
+
message:
|
|
204
|
+
"Telegram is set up, but the bot couldn't be reached. Check the bot token and try again.",
|
|
205
|
+
comment:
|
|
206
|
+
"@context: Error shown on the Telegram settings page when the bot can't be reached",
|
|
207
|
+
}),
|
|
208
|
+
)}
|
|
209
|
+
</span>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div class="flex flex-col gap-6 max-w-form">
|
|
217
|
+
<div>
|
|
218
|
+
<h2 class="text-lg font-medium mb-1">
|
|
219
|
+
{i18n._(
|
|
220
|
+
msg({
|
|
221
|
+
message: "Connect Telegram",
|
|
222
|
+
comment:
|
|
223
|
+
"@context: Heading on the Telegram settings page when ready to connect",
|
|
224
|
+
}),
|
|
225
|
+
)}
|
|
226
|
+
</h2>
|
|
227
|
+
<p class="text-sm text-muted-foreground">
|
|
228
|
+
{i18n._(
|
|
229
|
+
msg({
|
|
230
|
+
message:
|
|
231
|
+
"Open the bot and send the binding code, then anything you message it becomes a note.",
|
|
232
|
+
comment:
|
|
233
|
+
"@context: Instructions on the Telegram settings page for connecting an account",
|
|
234
|
+
}),
|
|
235
|
+
)}
|
|
236
|
+
</p>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div
|
|
240
|
+
class="flex flex-col items-center gap-4 py-2"
|
|
241
|
+
data-signals="{_codeCopied: false, _manualOpen: false}"
|
|
242
|
+
>
|
|
243
|
+
{/*
|
|
244
|
+
* Primary affordances. The button is the fastest path on any
|
|
245
|
+
* device that has a Telegram client installed — one click and
|
|
246
|
+
* Telegram resolves `?start=CODE` into a bound chat. The QR is
|
|
247
|
+
* only meaningful on desktop (you can't scan your own phone
|
|
248
|
+
* screen), where it covers the "Jant open on this machine but
|
|
249
|
+
* Telegram only on my phone" case. Both routes deliver the
|
|
250
|
+
* same deep link URL; only the carrier differs.
|
|
251
|
+
*/}
|
|
252
|
+
<div
|
|
253
|
+
class="hidden sm:block bg-white p-3 rounded-lg shadow-sm"
|
|
254
|
+
style="width:180px;height:180px"
|
|
255
|
+
aria-label={i18n._(
|
|
256
|
+
msg({
|
|
257
|
+
message: "QR code linking to the Telegram bot",
|
|
258
|
+
comment:
|
|
259
|
+
"@context: Accessible label for the Telegram connect QR code",
|
|
260
|
+
}),
|
|
261
|
+
)}
|
|
262
|
+
dangerouslySetInnerHTML={{ __html: connect.qrSvg }}
|
|
263
|
+
/>
|
|
264
|
+
<span
|
|
265
|
+
class="hidden sm:block text-xs text-muted-foreground"
|
|
266
|
+
aria-hidden="true"
|
|
267
|
+
>
|
|
268
|
+
{i18n._(
|
|
269
|
+
msg({
|
|
270
|
+
message: "or",
|
|
271
|
+
comment:
|
|
272
|
+
"@context: Separator between the QR code and the deep-link button on the Telegram connect page",
|
|
273
|
+
}),
|
|
274
|
+
)}
|
|
275
|
+
</span>
|
|
276
|
+
<a
|
|
277
|
+
href={connect.deepLink}
|
|
278
|
+
target="_blank"
|
|
279
|
+
rel="noopener noreferrer"
|
|
280
|
+
class="btn"
|
|
281
|
+
>
|
|
282
|
+
{i18n._(
|
|
283
|
+
msg({
|
|
284
|
+
message: "Open Telegram to connect",
|
|
285
|
+
comment:
|
|
286
|
+
"@context: Button that opens the Telegram bot via a deep link",
|
|
287
|
+
}),
|
|
288
|
+
)}
|
|
289
|
+
</a>
|
|
290
|
+
|
|
291
|
+
<div class="flex items-center gap-3 text-xs text-muted-foreground">
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
|
+
class="cursor-pointer hover:text-foreground underline-offset-4 hover:underline inline-flex items-center gap-1"
|
|
295
|
+
aria-controls="telegram-manual-fallback"
|
|
296
|
+
data-attr:aria-expanded="$_manualOpen"
|
|
297
|
+
data-on:click="$_manualOpen = !$_manualOpen"
|
|
298
|
+
>
|
|
299
|
+
{i18n._(
|
|
300
|
+
msg({
|
|
301
|
+
message: "Connect manually",
|
|
302
|
+
comment:
|
|
303
|
+
"@context: Toggle that reveals manual Telegram binding instructions when the deep link can't be used",
|
|
304
|
+
}),
|
|
305
|
+
)}
|
|
306
|
+
<span data-text="$_manualOpen ? '↑' : '↓'">↓</span>
|
|
307
|
+
</button>
|
|
308
|
+
<span aria-hidden="true">·</span>
|
|
309
|
+
<button
|
|
310
|
+
type="button"
|
|
311
|
+
class="cursor-pointer hover:text-foreground underline underline-offset-4 decoration-dotted inline-flex items-center gap-1.5"
|
|
312
|
+
data-on:click__prevent={`@post('${settingsBase}/regenerate-code')`}
|
|
313
|
+
data-indicator="_regenerating"
|
|
314
|
+
data-attr:disabled="$_regenerating"
|
|
315
|
+
>
|
|
316
|
+
<Spinner signal="_regenerating" size="size-3" />
|
|
317
|
+
{i18n._(
|
|
318
|
+
msg({
|
|
319
|
+
message: "Get a new code",
|
|
320
|
+
comment:
|
|
321
|
+
"@context: Button to regenerate the Telegram binding code",
|
|
322
|
+
}),
|
|
323
|
+
)}
|
|
324
|
+
</button>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div
|
|
328
|
+
id="telegram-manual-fallback"
|
|
329
|
+
data-show="$_manualOpen"
|
|
330
|
+
style="display:none"
|
|
331
|
+
class="flex flex-col items-center gap-3 w-full pt-1"
|
|
332
|
+
>
|
|
333
|
+
<p
|
|
334
|
+
class="text-sm text-muted-foreground text-center"
|
|
335
|
+
dangerouslySetInnerHTML={{
|
|
336
|
+
__html: i18n._(
|
|
337
|
+
msg({
|
|
338
|
+
message: "Open {linkOpen}@{botUsername}{linkClose} and send:",
|
|
339
|
+
comment:
|
|
340
|
+
"@context: Caption inside the manual Telegram binding fallback. {linkOpen}/{linkClose} wrap the bot username as a t.me link — keep them around the @username token in translations.",
|
|
341
|
+
}),
|
|
342
|
+
{
|
|
343
|
+
botUsername: escapeHtml(connect.botUsername),
|
|
344
|
+
linkOpen: `<a href="${escapeHtml(`https://t.me/${connect.botUsername}`)}" target="_blank" rel="noopener noreferrer" class="underline underline-offset-2 hover:text-foreground">`,
|
|
345
|
+
linkClose: "</a>",
|
|
346
|
+
},
|
|
347
|
+
),
|
|
348
|
+
}}
|
|
349
|
+
/>
|
|
350
|
+
<div class="flex items-center gap-2">
|
|
351
|
+
<code class="text-sm bg-muted px-3 py-1.5 rounded font-mono select-all">
|
|
352
|
+
/start {connect.code}
|
|
353
|
+
</code>
|
|
354
|
+
<button
|
|
355
|
+
type="button"
|
|
356
|
+
class="btn-sm-outline shrink-0"
|
|
357
|
+
aria-label={i18n._(
|
|
358
|
+
msg({
|
|
359
|
+
message: "Copy",
|
|
360
|
+
comment: "@context: Button to copy the Telegram binding code",
|
|
361
|
+
}),
|
|
362
|
+
)}
|
|
363
|
+
data-on:click={`navigator.clipboard.writeText('/start ${connect.code}'); $_codeCopied = true`}
|
|
364
|
+
data-text={`$_codeCopied ? '${i18n._(
|
|
365
|
+
msg({
|
|
366
|
+
message: "Copied",
|
|
367
|
+
comment: "@context: Feedback after copying to clipboard",
|
|
368
|
+
}),
|
|
369
|
+
)}' : '${i18n._(
|
|
370
|
+
msg({
|
|
371
|
+
message: "Copy",
|
|
372
|
+
comment: "@context: Button to copy the Telegram binding code",
|
|
373
|
+
}),
|
|
374
|
+
)}'`}
|
|
375
|
+
>
|
|
376
|
+
{i18n._(
|
|
377
|
+
msg({
|
|
378
|
+
message: "Copy",
|
|
379
|
+
comment: "@context: Button to copy the Telegram binding code",
|
|
380
|
+
}),
|
|
381
|
+
)}
|
|
382
|
+
</button>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
{!view.managed ? (
|
|
388
|
+
<div class="flex justify-center pt-2">
|
|
389
|
+
<RemoveBotLink settingsBase={settingsBase} />
|
|
390
|
+
</div>
|
|
391
|
+
) : null}
|
|
392
|
+
</div>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function TelegramConnected({
|
|
397
|
+
view,
|
|
398
|
+
settingsBase,
|
|
399
|
+
}: {
|
|
400
|
+
view: TelegramSettingsView;
|
|
401
|
+
settingsBase: string;
|
|
402
|
+
}) {
|
|
403
|
+
const { i18n } = useLingui();
|
|
404
|
+
const account = view.binding?.telegramUsername
|
|
405
|
+
? `@${view.binding.telegramUsername}`
|
|
406
|
+
: i18n._(
|
|
407
|
+
msg({
|
|
408
|
+
message: "your Telegram account",
|
|
409
|
+
comment:
|
|
410
|
+
"@context: Fallback name when a connected Telegram account has no username",
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
const disconnectLabel = i18n._(
|
|
414
|
+
msg({
|
|
415
|
+
message: "Disconnect",
|
|
416
|
+
comment: "@context: Button to disconnect Telegram",
|
|
417
|
+
}),
|
|
418
|
+
);
|
|
419
|
+
const cancelLabel = i18n._(
|
|
420
|
+
msg({
|
|
421
|
+
message: "Cancel",
|
|
422
|
+
comment: "@context: Button label to dismiss a dialog or action",
|
|
423
|
+
}),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<div class="flex flex-col gap-8 max-w-form">
|
|
428
|
+
<div>
|
|
429
|
+
<h2 class="text-lg font-medium mb-1">
|
|
430
|
+
{i18n._(
|
|
431
|
+
msg({
|
|
432
|
+
message: "Telegram",
|
|
433
|
+
comment: "@context: Settings section heading for Telegram setup",
|
|
434
|
+
}),
|
|
435
|
+
)}
|
|
436
|
+
</h2>
|
|
437
|
+
<div class="flex items-center gap-2 text-sm">
|
|
438
|
+
<span
|
|
439
|
+
class="text-green-600 dark:text-green-500"
|
|
440
|
+
dangerouslySetInnerHTML={{ __html: STATUS_DOT }}
|
|
441
|
+
/>
|
|
442
|
+
<span>
|
|
443
|
+
{i18n._(
|
|
444
|
+
msg({
|
|
445
|
+
message: "Connected as {account}",
|
|
446
|
+
comment:
|
|
447
|
+
"@context: Status label when Telegram is connected, with the account name",
|
|
448
|
+
}),
|
|
449
|
+
{ account },
|
|
450
|
+
)}
|
|
451
|
+
</span>
|
|
452
|
+
</div>
|
|
453
|
+
<p class="text-sm text-muted-foreground mt-2">
|
|
454
|
+
{i18n._(
|
|
455
|
+
msg({
|
|
456
|
+
message: "Message the bot any text and it's published as a note.",
|
|
457
|
+
comment: "@context: Hint shown when Telegram is connected",
|
|
458
|
+
}),
|
|
459
|
+
)}
|
|
460
|
+
</p>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<section class="flex flex-col gap-3 border-t pt-8">
|
|
464
|
+
<p class="text-sm text-muted-foreground">
|
|
465
|
+
{i18n._(
|
|
466
|
+
msg({
|
|
467
|
+
message:
|
|
468
|
+
"Stop accepting posts from Telegram. Your existing notes stay published.",
|
|
469
|
+
comment:
|
|
470
|
+
"@context: Description for the Telegram disconnect action",
|
|
471
|
+
}),
|
|
472
|
+
)}
|
|
473
|
+
</p>
|
|
474
|
+
<div class="flex flex-wrap items-center gap-4 mt-1">
|
|
475
|
+
<button
|
|
476
|
+
type="button"
|
|
477
|
+
class="cursor-pointer text-sm text-destructive/80 hover:text-destructive hover:underline underline-offset-4 transition-colors inline-flex items-center gap-1.5 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
478
|
+
data-indicator="_disconnecting"
|
|
479
|
+
data-attr:disabled="$_disconnecting"
|
|
480
|
+
data-on:click__prevent={buildConfirmActionExpression(
|
|
481
|
+
`@post('${settingsBase}/disconnect')`,
|
|
482
|
+
{
|
|
483
|
+
message: i18n._(
|
|
484
|
+
msg({
|
|
485
|
+
message:
|
|
486
|
+
"Disconnect Telegram? You can reconnect any time with a new binding code.",
|
|
487
|
+
comment:
|
|
488
|
+
"@context: Confirmation message when disconnecting Telegram",
|
|
489
|
+
}),
|
|
490
|
+
),
|
|
491
|
+
confirmLabel: disconnectLabel,
|
|
492
|
+
cancelLabel,
|
|
493
|
+
tone: "danger",
|
|
494
|
+
},
|
|
495
|
+
)}
|
|
496
|
+
>
|
|
497
|
+
<Spinner signal="_disconnecting" />
|
|
498
|
+
{disconnectLabel}
|
|
499
|
+
</button>
|
|
500
|
+
{!view.managed ? <RemoveBotLink settingsBase={settingsBase} /> : null}
|
|
501
|
+
</div>
|
|
502
|
+
</section>
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function RemoveBotLink({ settingsBase }: { settingsBase: string }) {
|
|
508
|
+
const { i18n } = useLingui();
|
|
509
|
+
const removeLabel = i18n._(
|
|
510
|
+
msg({
|
|
511
|
+
message: "Remove bot",
|
|
512
|
+
comment: "@context: Button to remove the saved Telegram bot token",
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
515
|
+
const cancelLabel = i18n._(
|
|
516
|
+
msg({
|
|
517
|
+
message: "Cancel",
|
|
518
|
+
comment: "@context: Button label to dismiss a dialog or action",
|
|
519
|
+
}),
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
<button
|
|
524
|
+
type="button"
|
|
525
|
+
class="cursor-pointer text-xs text-muted-foreground hover:text-destructive underline underline-offset-4 decoration-dotted inline-flex items-center gap-1.5"
|
|
526
|
+
data-indicator="_removing"
|
|
527
|
+
data-attr:disabled="$_removing"
|
|
528
|
+
data-on:click__prevent={buildConfirmActionExpression(
|
|
529
|
+
`@post('${settingsBase}/remove-bot')`,
|
|
530
|
+
{
|
|
531
|
+
message: i18n._(
|
|
532
|
+
msg({
|
|
533
|
+
message:
|
|
534
|
+
"Remove the saved bot token? Its webhook is deleted and any connected account is disconnected.",
|
|
535
|
+
comment:
|
|
536
|
+
"@context: Confirmation message when removing the Telegram bot token",
|
|
537
|
+
}),
|
|
538
|
+
),
|
|
539
|
+
confirmLabel: removeLabel,
|
|
540
|
+
cancelLabel,
|
|
541
|
+
tone: "danger",
|
|
542
|
+
},
|
|
543
|
+
)}
|
|
544
|
+
>
|
|
545
|
+
<Spinner signal="_removing" size="size-3" />
|
|
546
|
+
{removeLabel}
|
|
547
|
+
</button>
|
|
548
|
+
);
|
|
549
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thread Preview
|
|
3
3
|
*
|
|
4
|
-
* Shows latest reply as the hero post with ancestor context above.
|
|
4
|
+
* Shows latest reply as the hero post with collapsible faded ancestor context above.
|
|
5
5
|
* Thread line connects all posts via `.thread-group` / `.thread-item`.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -52,6 +52,19 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
52
52
|
count: hiddenCount,
|
|
53
53
|
},
|
|
54
54
|
);
|
|
55
|
+
const showMoreLabel = i18n._(
|
|
56
|
+
msg({
|
|
57
|
+
message: "Show more",
|
|
58
|
+
comment: "@context: Expand faded thread ancestor context in the feed",
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
const showLessLabel = i18n._(
|
|
62
|
+
msg({
|
|
63
|
+
message: "Show less",
|
|
64
|
+
comment:
|
|
65
|
+
"@context: Collapse expanded thread ancestor context in the feed",
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
55
68
|
const renderedSecondReply =
|
|
56
69
|
secondReply && secondReply.id !== latestReply.id ? secondReply : undefined;
|
|
57
70
|
const renderedPenultimateReply =
|
|
@@ -62,47 +75,87 @@ export const ThreadPreview: FC<ThreadPreviewProps> = ({
|
|
|
62
75
|
: undefined;
|
|
63
76
|
const gapHref = renderedSecondReply?.permalink ?? latestReply.permalink;
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
/>
|
|
74
|
-
</div>
|
|
78
|
+
// Always render the collapsible shell for thread previews. Structural
|
|
79
|
+
// signals (how many ancestor posts exist) aren't a reliable proxy for
|
|
80
|
+
// visual height — a single long root article will push the hero far
|
|
81
|
+
// off-screen just as much as several short replies would. The server
|
|
82
|
+
// assumes overflow (the common case) and renders the cap + fade + toggle
|
|
83
|
+
// immediately; client-side measurement removes the affordance when the
|
|
84
|
+
// content actually fits inside the cap, so users never see a no-op
|
|
85
|
+
// "Show more" button.
|
|
75
86
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
const rootItem = (
|
|
88
|
+
<div class="thread-item thread-item-context">
|
|
89
|
+
<TimelineItemFromPost
|
|
90
|
+
post={rootPost}
|
|
91
|
+
mode="feed"
|
|
92
|
+
display={ROOT_CONTEXT_DISPLAY}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const secondReplyItem = renderedSecondReply ? (
|
|
98
|
+
<div class="thread-item thread-item-context">
|
|
99
|
+
<TimelineItemFromPost
|
|
100
|
+
post={renderedSecondReply}
|
|
101
|
+
mode="feed"
|
|
102
|
+
display={CONTEXT_DISPLAY}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
) : null;
|
|
106
|
+
|
|
107
|
+
const gapItem =
|
|
108
|
+
hiddenCount > 0 ? (
|
|
109
|
+
<div class="thread-item thread-item-gap">
|
|
110
|
+
<a href={gapHref} class="thread-gap-link">
|
|
111
|
+
{hiddenPostsLabel}
|
|
112
|
+
</a>
|
|
113
|
+
</div>
|
|
114
|
+
) : null;
|
|
86
115
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
const penultimateItem = renderedPenultimateReply ? (
|
|
117
|
+
<div class="thread-item thread-item-context">
|
|
118
|
+
<TimelineItemFromPost
|
|
119
|
+
post={renderedPenultimateReply}
|
|
120
|
+
mode="feed"
|
|
121
|
+
display={CONTEXT_DISPLAY}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
) : null;
|
|
95
125
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
return (
|
|
127
|
+
<div class="thread-group thread-group-preview">
|
|
128
|
+
<div class="thread-context-shell" data-thread-context data-collapsed="">
|
|
129
|
+
{rootItem}
|
|
130
|
+
{secondReplyItem}
|
|
131
|
+
{gapItem}
|
|
132
|
+
{penultimateItem}
|
|
133
|
+
<div class="thread-context-fade" aria-hidden="true" />
|
|
134
|
+
</div>
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
class="thread-context-toggle"
|
|
138
|
+
data-thread-context-toggle
|
|
139
|
+
data-label-more={showMoreLabel}
|
|
140
|
+
data-label-less={showLessLabel}
|
|
141
|
+
aria-expanded="false"
|
|
142
|
+
>
|
|
143
|
+
<span class="thread-context-toggle-label">{showMoreLabel}</span>
|
|
144
|
+
<svg
|
|
145
|
+
class="thread-context-toggle-chevron"
|
|
146
|
+
viewBox="0 0 16 16"
|
|
147
|
+
aria-hidden="true"
|
|
148
|
+
>
|
|
149
|
+
<path
|
|
150
|
+
d="M4 6l4 4 4-4"
|
|
151
|
+
fill="none"
|
|
152
|
+
stroke="currentColor"
|
|
153
|
+
stroke-width="1.5"
|
|
154
|
+
stroke-linecap="round"
|
|
155
|
+
stroke-linejoin="round"
|
|
103
156
|
/>
|
|
104
|
-
</
|
|
105
|
-
|
|
157
|
+
</svg>
|
|
158
|
+
</button>
|
|
106
159
|
|
|
107
160
|
{/* Latest reply (full card, hero) */}
|
|
108
161
|
<div class="thread-item thread-item-hero">
|