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 { MessageMediaGrid } from '../components/tickets/MessageMediaGrid'; 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 } from '../types'; import { Card } from '@/components/data-display/Card'; import { Button } from '@/components/primitives/Button'; import { staggerContainer, staggerItem } from '@/components/motion/transitions'; import { ChatIcon, CloseIcon, ImageIcon, PlusIcon, SendIcon } from '@/components/icons'; import { usePlatform } from '@/platform'; import { linkifyText } from '../utils/linkify'; const log = logger.createLogger('Support'); // Media attachment state interface MediaAttachment { id: string; file: File; preview: string; uploading: boolean; fileId?: string; error?: string; } 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 (multi-upload, up to 10) const [createAttachments, setCreateAttachments] = useState([]); const [replyAttachments, setReplyAttachments] = useState([]); const createFileInputRef = useRef(null); const replyFileInputRef = useRef(null); const blobUrlsRef = useRef>(new Set()); useEffect(() => { const urls = blobUrlsRef; return () => { urls.current.forEach((u) => URL.revokeObjectURL(u)); }; }, []); const clearCreateAttachments = () => { createAttachments.forEach((a) => { if (a.preview) URL.revokeObjectURL(a.preview); }); setCreateAttachments([]); if (createFileInputRef.current) createFileInputRef.current.value = ''; }; const clearReplyAttachments = () => { replyAttachments.forEach((a) => { if (a.preview) URL.revokeObjectURL(a.preview); }); setReplyAttachments([]); if (replyFileInputRef.current) replyFileInputRef.current.value = ''; }; // 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 (multi-upload) const handleFileSelect = async ( file: File, setAttachments: React.Dispatch>, ) => { const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (!allowedTypes.includes(file.type)) return; if (file.size > 10 * 1024 * 1024) return; const preview = URL.createObjectURL(file); blobUrlsRef.current.add(preview); const id = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : `att_${Date.now()}_${Math.random().toString(36).slice(2)}`; const entry: MediaAttachment = { id, file, preview, uploading: true }; setAttachments((prev) => (prev.length >= 10 ? prev : [...prev, entry])); try { const result = await ticketsApi.uploadMedia(file, 'photo'); setAttachments((prev) => prev.map((a) => (a.id === id ? { ...a, uploading: false, fileId: result.file_id } : a)), ); } catch { setAttachments((prev) => prev.map((a) => a.id === id ? { ...a, uploading: false, error: t('support.uploadFailed') } : a, ), ); } }; const createMutation = useMutation({ mutationFn: async () => { const ready = createAttachments.filter((a) => a.fileId) as Array<{ fileId: string }>; const media = ready.length > 0 ? { media_type: 'photo', media_file_id: ready[0].fileId, media_items: ready.map((a) => ({ type: 'photo' as const, file_id: a.fileId })), } : undefined; return ticketsApi.createTicket(newTitle, newMessage, media); }, onSuccess: (ticket) => { queryClient.invalidateQueries({ queryKey: ['tickets'] }); setShowCreateForm(false); setNewTitle(''); setNewMessage(''); clearCreateAttachments(); setSelectedTicket(ticket); }, }); const replyMutation = useMutation({ mutationFn: async () => { const ready = replyAttachments.filter((a) => a.fileId) as Array<{ fileId: string }>; const media = ready.length > 0 ? { media_type: 'photo', media_file_id: ready[0].fileId, media_items: ready.map((a) => ({ type: 'photo' as const, file_id: a.fileId })), } : undefined; await ticketsApi.addMessage(selectedTicket!.id, replyMessage, media); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ticket', selectedTicket?.id] }); setReplyMessage(''); clearReplyAttachments(); }, }); 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}

); } // Attachments preview component const AttachmentsPreview = ({ items, onRemove, }: { items: MediaAttachment[]; onRemove: (idx: number) => void; }) => items.length === 0 ? null : (
{items.map((att, idx) => (
{att.preview ? ( Preview ) : (
{att.file.name.slice(-6)}
)} {att.uploading && (
)} {att.error && (
!
)}
))}
); return (

{t('support.title')}

{/* Contact support card for "both" mode — self-animated: mounts after the config query resolves, when the parent stagger orchestration has already finished and would leave it stuck at opacity 0 */} {supportConfig?.support_type === 'both' && supportConfig.support_username && (
{t('support.contactUs')}
{supportConfig.support_username}
)} {/* 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} />