import { useState, useRef } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { ticketsApi } from '../api/tickets'; import { infoApi } from '../api/info'; import { logger } from '../utils/logger'; import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils/rateLimit'; import type { TicketDetail, TicketMessage } from '../types'; const log = logger.createLogger('Support'); const PlusIcon = () => ( ); const SendIcon = () => ( ); const ImageIcon = () => ( ); const CloseIcon = () => ( ); // Media attachment state interface MediaAttachment { file: File; preview: string; uploading: boolean; fileId?: string; error?: string; } // Message media display component function MessageMedia({ message, t }: { message: TicketMessage; t: (key: string) => string }) { const [imageLoaded, setImageLoaded] = useState(false); const [imageError, setImageError] = useState(false); const [showFullImage, setShowFullImage] = useState(false); if (!message.has_media || !message.media_file_id) { return null; } const mediaUrl = ticketsApi.getMediaUrl(message.media_file_id); if (message.media_type === 'photo') { return ( <>
{!imageLoaded && !imageError && (
)} {imageError ? (
{t('support.imageLoadFailed')}
) : ( {message.media_caption setImageLoaded(true)} onError={() => setImageError(true)} onClick={() => setShowFullImage(true)} /> )} {message.media_caption && (

{message.media_caption}

)}
{/* Full image modal */} {showFullImage && (
setShowFullImage(false)} > {message.media_caption
)} ); } // For documents/videos - show download link return (
{message.media_caption || `Download ${message.media_type}`}
); } export default function Support() { log.debug('Component loaded'); const { t } = useTranslation(); const queryClient = useQueryClient(); const [selectedTicket, setSelectedTicket] = useState(null); const [showCreateForm, setShowCreateForm] = useState(false); const [newTitle, setNewTitle] = useState(''); const [newMessage, setNewMessage] = useState(''); const [replyMessage, setReplyMessage] = useState(''); const [rateLimitError, setRateLimitError] = useState(null); // Media attachment states const [createAttachment, setCreateAttachment] = useState(null); const [replyAttachment, setReplyAttachment] = useState(null); const createFileInputRef = useRef(null); const replyFileInputRef = useRef(null); // Get support configuration const { data: supportConfig, isLoading: configLoading } = useQuery({ queryKey: ['support-config'], queryFn: infoApi.getSupportConfig, }); const { data: tickets, isLoading } = useQuery({ queryKey: ['tickets'], queryFn: () => ticketsApi.getTickets({ per_page: 20 }), enabled: supportConfig?.tickets_enabled === true, }); const { data: ticketDetail, isLoading: detailLoading } = useQuery({ queryKey: ['ticket', selectedTicket?.id], queryFn: () => ticketsApi.getTicket(selectedTicket!.id), enabled: !!selectedTicket, }); // Handle file selection const handleFileSelect = async ( file: File, setAttachment: (a: MediaAttachment | null) => void, ) => { // Validate file type const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (!allowedTypes.includes(file.type)) { setAttachment({ file, preview: '', uploading: false, error: t('support.invalidFileType'), }); return; } // Validate file size (10MB) if (file.size > 10 * 1024 * 1024) { setAttachment({ file, preview: '', uploading: false, error: t('support.fileTooLarge'), }); return; } // Create preview const preview = URL.createObjectURL(file); setAttachment({ file, preview, uploading: true }); try { const result = await ticketsApi.uploadMedia(file, 'photo'); setAttachment({ file, preview, uploading: false, fileId: result.file_id, }); } catch { setAttachment({ file, preview, uploading: false, error: t('support.uploadFailed'), }); } }; const createMutation = useMutation({ mutationFn: async () => { const media = createAttachment?.fileId ? { media_type: 'photo', media_file_id: createAttachment.fileId, } : undefined; return ticketsApi.createTicket(newTitle, newMessage, media); }, onSuccess: (ticket) => { queryClient.invalidateQueries({ queryKey: ['tickets'] }); setShowCreateForm(false); setNewTitle(''); setNewMessage(''); setCreateAttachment(null); setSelectedTicket(ticket); }, }); const replyMutation = useMutation({ mutationFn: async () => { const media = replyAttachment?.fileId ? { media_type: 'photo', media_file_id: replyAttachment.fileId, } : undefined; return ticketsApi.addMessage(selectedTicket!.id, replyMessage, media); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ticket', selectedTicket?.id] }); setReplyMessage(''); setReplyAttachment(null); }, }); const getStatusBadge = (status: string) => { switch (status) { case 'open': return 'badge-info'; case 'answered': return 'badge-success'; case 'pending': return 'badge-warning'; case 'closed': return 'badge-neutral'; default: return 'badge-neutral'; } }; const getStatusLabel = (status: string) => { return t(`support.status.${status}`) || status; }; // Show loading while checking configuration if (configLoading) { return (
); } // If tickets are disabled, show redirect message if (supportConfig && !supportConfig.tickets_enabled) { log.debug('Tickets disabled, config:', supportConfig); const getSupportMessage = () => { log.debug('Getting support message for type:', supportConfig.support_type); if (supportConfig.support_type === 'profile') { const supportUsername = supportConfig.support_username || '@support'; log.debug('Opening profile:', supportUsername); return { title: t('support.ticketsDisabled'), message: t('support.contactSupport', { username: supportUsername }), buttonText: t('support.contactUs'), buttonAction: () => { log.debug('Button clicked, opening:', supportUsername); const webApp = window.Telegram?.WebApp; // Extract username without @ const username = supportUsername.startsWith('@') ? supportUsername.slice(1) : supportUsername; const webUrl = `https://t.me/${username}`; log.debug('Web URL:', webUrl, 'WebApp methods:', { openTelegramLink: !!webApp?.openTelegramLink, openLink: !!webApp?.openLink, }); // Try openTelegramLink first (for tg:// links) if (webApp?.openTelegramLink) { log.debug('Using openTelegramLink with web URL'); try { webApp.openTelegramLink(webUrl); return; } catch (e) { log.error('openTelegramLink failed:', e); } } // Fallback to openLink if (webApp?.openLink) { log.debug('Using openLink'); try { webApp.openLink(webUrl, { try_browser: true }); return; } catch (e) { log.error('openLink failed:', e); } } // Last resort - window.open log.debug('Using window.open'); window.open(webUrl, '_blank'); }, }; } if (supportConfig.support_type === 'url' && supportConfig.support_url) { return { title: t('support.ticketsDisabled'), message: t('support.useExternalLink'), buttonText: t('support.openSupport'), buttonAction: () => { const webApp = window.Telegram?.WebApp; if (webApp?.openLink) { webApp.openLink(supportConfig.support_url!, { try_browser: true }); } else { window.open(supportConfig.support_url!, '_blank'); } }, }; } // Fallback: contact support (should not normally happen if config is correct) const supportUsername = supportConfig.support_username || '@support'; log.debug('Fallback: Opening profile:', supportUsername); return { title: t('support.ticketsDisabled'), message: t('support.contactSupport', { username: supportUsername }), buttonText: t('support.contactUs'), buttonAction: () => { log.debug('Fallback button clicked, opening:', supportUsername); const webApp = window.Telegram?.WebApp; // Extract username without @ const username = supportUsername.startsWith('@') ? supportUsername.slice(1) : supportUsername; const webUrl = `https://t.me/${username}`; log.debug('Fallback opening URL:', webUrl); if (webApp?.openTelegramLink) { log.debug('Fallback using openTelegramLink'); webApp.openTelegramLink(webUrl); } else if (webApp?.openLink) { log.debug('Fallback using openLink'); webApp.openLink(webUrl, { try_browser: true }); } else { log.debug('Fallback using window.open'); window.open(webUrl, '_blank'); } }, }; }; const supportMessage = getSupportMessage(); return (

{supportMessage.title}

{supportMessage.message}

); } // Attachment preview component const AttachmentPreview = ({ attachment, onRemove, }: { attachment: MediaAttachment; onRemove: () => void; }) => (
{attachment.preview && ( Attachment preview )} {attachment.uploading && (
)} {attachment.error &&
{attachment.error}
}
); return (

{t('support.title')}

{/* Tickets List */}

{t('support.yourTickets')}

{isLoading ? (
) : tickets?.items && tickets.items.length > 0 ? (
{tickets.items.map((ticket) => ( ))}
) : (
{t('support.noTickets')}
)}
{/* Ticket Detail / Create Form */}
{showCreateForm ? (

{t('support.createTicket')}

{ e.preventDefault(); setRateLimitError(null); // Rate limit: max 3 tickets per 60 seconds if (!checkRateLimit(RATE_LIMIT_KEYS.TICKET_CREATE, 3, 60000)) { const resetTime = getRateLimitResetTime(RATE_LIMIT_KEYS.TICKET_CREATE); setRateLimitError(t('support.tooManyRequests', { seconds: resetTime })); return; } createMutation.mutate(); }} className="space-y-4" >
setNewTitle(e.target.value)} required minLength={3} maxLength={255} />