feat(admin): предупреждение о разбивке длинных текстов системных страниц в боте

Кабинет, в отличие от админки бота (лимит 4000 символов), позволяет
сохранять юридические тексты любой длины — и до парного фикса в боте
(BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot#3049) текст длиннее
~4096 символов ронял показ правил и политики при /start
(MESSAGE_TOO_LONG).

Теперь под текстовым полем политики, оферты и правил постоянно
показывается счётчик символов, а для текстов длиннее одного сообщения —
предупреждение с оценкой, на сколько сообщений бот разобьёт текст.
Оценка зеркалит жадную упаковку абзацев из split_telegram_text бота
(порог 3500 символов на кусок).

Предупреждение не показывается там, где оно неуместно:
- «Рекуррентные платежи» — документ не отображается в боте вовсе;
- режим отображения «Только веб» — бот документ не показывает.

Переводы добавлены в ru/en (с плюральными формами); остальные локали
используют английский fallback через defaultValue.
This commit is contained in:
kewldan
2026-07-12 19:30:48 +03:00
parent 20565b8f59
commit 93cfca46dc
3 changed files with 68 additions and 0 deletions

View File

@@ -4196,6 +4196,9 @@
"language": "Content language",
"content": "Content",
"contentPlaceholder": "HTML page content...",
"charCount": "Characters: {{count}}",
"botSplitEstimate_one": "⚠️ The bot will show it split into ~{{count}} message",
"botSplitEstimate_other": "⚠️ The bot will show it split into ~{{count}} messages",
"save": "Save",
"saving": "Saving...",
"saveError": "Failed to save. Please try again.",

View File

@@ -4741,6 +4741,10 @@
"language": "Язык контента",
"content": "Текст",
"contentPlaceholder": "HTML-текст страницы...",
"charCount": "Символов: {{count}}",
"botSplitEstimate_one": "⚠️ В боте будет показано по частям: ~{{count}} сообщение",
"botSplitEstimate_few": "⚠️ В боте будет показано по частям: ~{{count}} сообщения",
"botSplitEstimate_many": "⚠️ В боте будет показано по частям: ~{{count}} сообщений",
"save": "Сохранить",
"saving": "Сохранение...",
"saveError": "Не удалось сохранить. Попробуйте ещё раз.",

View File

@@ -20,6 +20,62 @@ const DISPLAY_MODES: LegalDisplayMode[] = ['bot', 'web', 'both'];
type DocumentKind = 'privacy-policy' | 'public-offer' | 'recurrent-payments';
// Bot chunk/page size (split_telegram_text max_length): longer texts are
// delivered by the bot in several messages / paginated pages
const TELEGRAM_SPLIT_THRESHOLD = 3500;
// Mirrors the bot's split_telegram_text greedy paragraph packing to estimate
// how many messages/pages the bot will produce for this text
function estimateTelegramParts(text: string): number {
const normalized = text.replace(/\r\n/g, '\n').trim();
if (!normalized) return 0;
if (normalized.length <= TELEGRAM_SPLIT_THRESHOLD) return 1;
const paragraphs = normalized.split('\n\n').filter((p) => p.trim());
let parts = 0;
let current = '';
for (const paragraph of paragraphs) {
const candidate = current ? `${current}\n\n${paragraph}` : paragraph;
if (candidate.length <= TELEGRAM_SPLIT_THRESHOLD) {
current = candidate;
continue;
}
if (current) {
parts += 1;
current = '';
}
if (paragraph.length <= TELEGRAM_SPLIT_THRESHOLD) {
current = paragraph;
} else {
parts += Math.ceil(paragraph.length / TELEGRAM_SPLIT_THRESHOLD);
}
}
if (current) parts += 1;
return parts;
}
function ContentLengthMeta({ text, botVisible }: { text: string; botVisible: boolean }) {
const { t } = useTranslation();
const parts = botVisible ? estimateTelegramParts(text) : 0;
return (
<div className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
<span className="text-dark-500">
{t('admin.legalPages.charCount', {
count: text.length,
defaultValue: 'Characters: {{count}}',
})}
</span>
{parts > 1 && (
<span className="text-warning-400">
{t('admin.legalPages.botSplitEstimate', {
count: parts,
defaultValue: '⚠️ The bot will show it split into ~{{count}} messages',
})}
</span>
)}
</div>
);
}
const DOCUMENT_API: Record<
DocumentKind,
{
@@ -236,6 +292,10 @@ function DocumentEditor({
className="input min-h-[320px] w-full font-mono text-sm"
placeholder={t('admin.legalPages.contentPlaceholder')}
/>
<ContentLengthMeta
text={contents[activeLang] ?? ''}
botVisible={kind !== 'recurrent-payments' && displayMode !== 'web'}
/>
</div>
{saveError && <p className="text-sm text-error-400">{saveError}</p>}
<button
@@ -336,6 +396,7 @@ function RulesEditor({ onDirtyChange }: { onDirtyChange: (dirty: boolean) => voi
className="input min-h-[320px] w-full font-mono text-sm"
placeholder={t('admin.legalPages.contentPlaceholder')}
/>
<ContentLengthMeta text={contents[activeLang] ?? ''} botVisible={displayMode !== 'web'} />
</div>
{saveError && <p className="text-sm text-error-400">{saveError}</p>}
<button