import { useState, useRef, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { motion } from 'framer-motion'; import { ticketsApi } from '../api/tickets'; import { infoApi } from '../api/info'; import { useAuthStore } from '../store/auth'; import { logger } from '../utils/logger'; import { checkRateLimit, getRateLimitResetTime, RATE_LIMIT_KEYS } from '../utils/rateLimit'; import type { TicketDetail, TicketMessage } from '../types'; import { Card } from '@/components/data-display/Card'; import { Button } from '@/components/primitives/Button'; import { staggerContainer, staggerItem } from '@/components/motion/transitions'; import { usePlatform } from '@/platform'; 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 isAdmin = useAuthStore((state) => state.isAdmin); const queryClient = useQueryClient(); const { openTelegramLink, openLink } = usePlatform(); 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); const createPreviewRef = useRef(null); const replyPreviewRef = useRef(null); // Revoke blob URLs on unmount to prevent memory leaks useEffect(() => { const createRef = createPreviewRef; const replyRef = replyPreviewRef; return () => { if (createRef.current) URL.revokeObjectURL(createRef.current); if (replyRef.current) URL.revokeObjectURL(replyRef.current); }; }, []); const clearCreateAttachment = () => { if (createPreviewRef.current) { URL.revokeObjectURL(createPreviewRef.current); createPreviewRef.current = null; } clearCreateAttachment(); }; const clearReplyAttachment = () => { if (replyPreviewRef.current) { URL.revokeObjectURL(replyPreviewRef.current); replyPreviewRef.current = null; } clearReplyAttachment(); }; // 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; } // Revoke old blob URL before creating new one const previewRef = setAttachment === setCreateAttachment ? createPreviewRef : replyPreviewRef; if (previewRef.current) URL.revokeObjectURL(previewRef.current); const preview = URL.createObjectURL(file); previewRef.current = preview; 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(''); clearCreateAttachment(); 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(''); clearReplyAttachment(); }, }); 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: isAdmin ? t('support.ticketsDisabled') : t('support.title'), message: t('support.contactSupport', { username: supportUsername }), buttonText: t('support.contactUs'), buttonAction: () => { log.debug('Button clicked, opening:', supportUsername); // Extract username without @ const username = supportUsername.startsWith('@') ? supportUsername.slice(1) : supportUsername; const webUrl = `https://t.me/${username}`; log.debug('Web URL:', webUrl); // Use platform's openTelegramLink openTelegramLink(webUrl); }, }; } if (supportConfig.support_type === 'url' && supportConfig.support_url) { return { title: isAdmin ? t('support.ticketsDisabled') : t('support.title'), message: t('support.useExternalLink'), buttonText: t('support.openSupport'), buttonAction: () => { openLink(supportConfig.support_url!, { tryInstantView: false }); }, }; } // 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: isAdmin ? t('support.ticketsDisabled') : t('support.title'), message: t('support.contactSupport', { username: supportUsername }), buttonText: t('support.contactUs'), buttonAction: () => { log.debug('Fallback button clicked, opening:', supportUsername); // Extract username without @ const username = supportUsername.startsWith('@') ? supportUsername.slice(1) : supportUsername; const webUrl = `https://t.me/${username}`; log.debug('Fallback opening URL:', webUrl); // Use platform's openTelegramLink openTelegramLink(webUrl); }, }; }; 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} />