Merge PR #484: поле icon_custom_emoji_id у кастомных кнопок рассылки

feat(broadcasts): поле icon_custom_emoji_id у кастомных кнопок рассылки
This commit is contained in:
Egor
2026-07-13 15:13:22 +03:00
committed by GitHub
6 changed files with 27 additions and 5 deletions

View File

@@ -52,6 +52,8 @@ export interface CustomBroadcastButton {
label: string;
action_type: 'callback' | 'url';
action_value: string;
/** Telegram custom emoji перед текстом кнопки (числовая строка custom_emoji_id) */
icon_custom_emoji_id?: string;
}
export interface BroadcastMedia {

View File

@@ -1947,6 +1947,7 @@
"customButtonTypeUrl": "URL",
"customButtonCallbackPlaceholder": "callback_data (e.g. back_to_menu)",
"customButtonUrlPlaceholder": "https://example.com",
"customButtonEmojiIdPlaceholder": "Custom emoji ID (optional)",
"preview": "Preview",
"previewEmpty": "— empty —",
"emailSubjectLabel": "Subject",

View File

@@ -1580,6 +1580,7 @@
"customButtonTypeUrl": "لینک",
"customButtonCallbackPlaceholder": "callback_data (مثلاً back_to_menu)",
"customButtonUrlPlaceholder": "https://example.com",
"customButtonEmojiIdPlaceholder": "شناسه ایموجی سفارشی (اختیاری)",
"category": "دسته‌بندی",
"categoryNews": "اخبار",
"categoryPromo": "تبلیغاتی",

View File

@@ -1974,6 +1974,7 @@
"customButtonTypeUrl": "Ссылка",
"customButtonCallbackPlaceholder": "callback_data (например, back_to_menu)",
"customButtonUrlPlaceholder": "https://example.com",
"customButtonEmojiIdPlaceholder": "Custom emoji ID (необязательно)",
"preview": "Предпросмотр",
"previewEmpty": "— пусто —",
"emailSubjectLabel": "Тема",

View File

@@ -1681,6 +1681,7 @@
"customButtonTypeUrl": "链接",
"customButtonCallbackPlaceholder": "callback_data例如 back_to_menu",
"customButtonUrlPlaceholder": "https://example.com",
"customButtonEmojiIdPlaceholder": "自定义 Emoji ID可选",
"category": "类别",
"categoryNews": "新闻",
"categoryPromo": "促销",

View File

@@ -4,10 +4,10 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
adminBroadcastsApi,
BroadcastFilter,
TariffFilter,
CombinedBroadcastCreateRequest,
CustomBroadcastButton,
type BroadcastFilter,
type TariffFilter,
type CombinedBroadcastCreateRequest,
type CustomBroadcastButton,
} from '../api/adminBroadcasts';
import { AdminBackButton } from '../components/admin';
import { TelegramPreview, EmailPreview } from '../components/broadcasts/BroadcastPreview';
@@ -63,6 +63,7 @@ export default function AdminBroadcastCreate() {
const [newButtonLabel, setNewButtonLabel] = useState('');
const [newButtonActionType, setNewButtonActionType] = useState<'callback' | 'url'>('callback');
const [newButtonActionValue, setNewButtonActionValue] = useState('');
const [newButtonEmojiId, setNewButtonEmojiId] = useState('');
const [mediaFile, setMediaFile] = useState<File | null>(null);
const [mediaType, setMediaType] = useState<'photo' | 'video' | 'document'>('photo');
const [mediaPreview, setMediaPreview] = useState<string | null>(null);
@@ -305,6 +306,8 @@ export default function AdminBroadcastCreate() {
// Custom button validation
const isNewButtonValid = useMemo(() => {
if (!newButtonLabel.trim() || !newButtonActionValue.trim()) return false;
// custom_emoji_id — необязательное поле, но если задано — числовая строка (Bot API)
if (newButtonEmojiId.trim() && !/^\d{1,64}$/.test(newButtonEmojiId.trim())) return false;
if (newButtonActionType === 'url') {
return /^https:\/\/|^tg:\/\//.test(newButtonActionValue.trim());
}
@@ -312,22 +315,25 @@ export default function AdminBroadcastCreate() {
return new TextEncoder().encode(newButtonActionValue.trim()).length <= 64;
}
return true;
}, [newButtonLabel, newButtonActionType, newButtonActionValue]);
}, [newButtonLabel, newButtonActionType, newButtonActionValue, newButtonEmojiId]);
// Custom button handlers
const addCustomButton = () => {
if (!isNewButtonValid) return;
const emojiId = newButtonEmojiId.trim();
setCustomButtons((prev) => [
...prev,
{
label: newButtonLabel.trim(),
action_type: newButtonActionType,
action_value: newButtonActionValue.trim(),
...(emojiId ? { icon_custom_emoji_id: emojiId } : {}),
},
]);
setNewButtonLabel('');
setNewButtonActionValue('');
setNewButtonActionType('callback');
setNewButtonEmojiId('');
setIsAddingCustomButton(false);
};
@@ -800,6 +806,15 @@ export default function AdminBroadcastCreate() {
maxLength={newButtonActionType === 'callback' ? 64 : 256}
className="input"
/>
<input
type="text"
inputMode="numeric"
value={newButtonEmojiId}
onChange={(e) => setNewButtonEmojiId(e.target.value)}
placeholder={t('admin.broadcasts.customButtonEmojiIdPlaceholder')}
maxLength={64}
className="input"
/>
<div className="flex gap-2">
<button
type="button"
@@ -807,6 +822,7 @@ export default function AdminBroadcastCreate() {
setIsAddingCustomButton(false);
setNewButtonLabel('');
setNewButtonActionValue('');
setNewButtonEmojiId('');
}}
className="btn-secondary flex-1"
>