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; label: string;
action_type: 'callback' | 'url'; action_type: 'callback' | 'url';
action_value: string; action_value: string;
/** Telegram custom emoji перед текстом кнопки (числовая строка custom_emoji_id) */
icon_custom_emoji_id?: string;
} }
export interface BroadcastMedia { export interface BroadcastMedia {

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,10 +4,10 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
adminBroadcastsApi, adminBroadcastsApi,
BroadcastFilter, type BroadcastFilter,
TariffFilter, type TariffFilter,
CombinedBroadcastCreateRequest, type CombinedBroadcastCreateRequest,
CustomBroadcastButton, type CustomBroadcastButton,
} from '../api/adminBroadcasts'; } from '../api/adminBroadcasts';
import { AdminBackButton } from '../components/admin'; import { AdminBackButton } from '../components/admin';
import { TelegramPreview, EmailPreview } from '../components/broadcasts/BroadcastPreview'; import { TelegramPreview, EmailPreview } from '../components/broadcasts/BroadcastPreview';
@@ -63,6 +63,7 @@ export default function AdminBroadcastCreate() {
const [newButtonLabel, setNewButtonLabel] = useState(''); const [newButtonLabel, setNewButtonLabel] = useState('');
const [newButtonActionType, setNewButtonActionType] = useState<'callback' | 'url'>('callback'); const [newButtonActionType, setNewButtonActionType] = useState<'callback' | 'url'>('callback');
const [newButtonActionValue, setNewButtonActionValue] = useState(''); const [newButtonActionValue, setNewButtonActionValue] = useState('');
const [newButtonEmojiId, setNewButtonEmojiId] = useState('');
const [mediaFile, setMediaFile] = useState<File | null>(null); const [mediaFile, setMediaFile] = useState<File | null>(null);
const [mediaType, setMediaType] = useState<'photo' | 'video' | 'document'>('photo'); const [mediaType, setMediaType] = useState<'photo' | 'video' | 'document'>('photo');
const [mediaPreview, setMediaPreview] = useState<string | null>(null); const [mediaPreview, setMediaPreview] = useState<string | null>(null);
@@ -305,6 +306,8 @@ export default function AdminBroadcastCreate() {
// Custom button validation // Custom button validation
const isNewButtonValid = useMemo(() => { const isNewButtonValid = useMemo(() => {
if (!newButtonLabel.trim() || !newButtonActionValue.trim()) return false; 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') { if (newButtonActionType === 'url') {
return /^https:\/\/|^tg:\/\//.test(newButtonActionValue.trim()); return /^https:\/\/|^tg:\/\//.test(newButtonActionValue.trim());
} }
@@ -312,22 +315,25 @@ export default function AdminBroadcastCreate() {
return new TextEncoder().encode(newButtonActionValue.trim()).length <= 64; return new TextEncoder().encode(newButtonActionValue.trim()).length <= 64;
} }
return true; return true;
}, [newButtonLabel, newButtonActionType, newButtonActionValue]); }, [newButtonLabel, newButtonActionType, newButtonActionValue, newButtonEmojiId]);
// Custom button handlers // Custom button handlers
const addCustomButton = () => { const addCustomButton = () => {
if (!isNewButtonValid) return; if (!isNewButtonValid) return;
const emojiId = newButtonEmojiId.trim();
setCustomButtons((prev) => [ setCustomButtons((prev) => [
...prev, ...prev,
{ {
label: newButtonLabel.trim(), label: newButtonLabel.trim(),
action_type: newButtonActionType, action_type: newButtonActionType,
action_value: newButtonActionValue.trim(), action_value: newButtonActionValue.trim(),
...(emojiId ? { icon_custom_emoji_id: emojiId } : {}),
}, },
]); ]);
setNewButtonLabel(''); setNewButtonLabel('');
setNewButtonActionValue(''); setNewButtonActionValue('');
setNewButtonActionType('callback'); setNewButtonActionType('callback');
setNewButtonEmojiId('');
setIsAddingCustomButton(false); setIsAddingCustomButton(false);
}; };
@@ -800,6 +806,15 @@ export default function AdminBroadcastCreate() {
maxLength={newButtonActionType === 'callback' ? 64 : 256} maxLength={newButtonActionType === 'callback' ? 64 : 256}
className="input" 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"> <div className="flex gap-2">
<button <button
type="button" type="button"
@@ -807,6 +822,7 @@ export default function AdminBroadcastCreate() {
setIsAddingCustomButton(false); setIsAddingCustomButton(false);
setNewButtonLabel(''); setNewButtonLabel('');
setNewButtonActionValue(''); setNewButtonActionValue('');
setNewButtonEmojiId('');
}} }}
className="btn-secondary flex-1" className="btn-secondary flex-1"
> >