feat: multi-media attachments, linkify URLs, shared MessageMediaGrid

- Add media_items array support to ticket messages (types, API clients)
- Create shared MessageMediaGrid component with photo grid, fullscreen
  viewer, keyboard nav, video/document rendering
- Replace per-page AdminMessageMedia/MessageMedia with MessageMediaGrid
- Add linkifyText util (DOMPurify-sanitized) for auto-linking URLs
- Support multi-file upload (up to 10) in AdminTickets and Support pages
- Remove unused ticketsApi import from AdminUserDetail
This commit is contained in:
Fringg
2026-04-29 08:45:15 +03:00
parent 9b1e26d4ec
commit 6d3010b621
8 changed files with 666 additions and 553 deletions

View File

@@ -19,10 +19,11 @@ import {
import { adminApi, type AdminTicket, type AdminTicketDetail } from '../api/admin';
import { promocodesApi, type PromoGroup } from '../api/promocodes';
import { promoOffersApi } from '../api/promoOffers';
import { ticketsApi } from '../api/tickets';
import { AdminBackButton } from '../components/admin';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { usePermissionStore } from '../store/permissions';
import { MessageMediaGrid } from '../components/tickets/MessageMediaGrid';
import { linkifyText } from '../utils/linkify';
// ============ Helpers ============
@@ -3092,32 +3093,13 @@ export default function AdminUserDetail() {
{formatDate(msg.created_at)}
</span>
</div>
<p className="whitespace-pre-wrap text-sm text-dark-200">
{msg.message_text}
</p>
{msg.has_media && msg.media_file_id && (
<div className="mt-2">
{msg.media_type === 'photo' ? (
<img
src={ticketsApi.getMediaUrl(msg.media_file_id)}
alt={msg.media_caption || ''}
className="max-h-48 max-w-full rounded-lg"
/>
) : (
<a
href={ticketsApi.getMediaUrl(msg.media_file_id)}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 rounded-lg bg-dark-700 px-2 py-1 text-xs text-dark-200 hover:bg-dark-600"
>
{msg.media_caption || msg.media_type}
</a>
)}
{msg.media_caption && msg.media_type === 'photo' && (
<p className="mt-1 text-xs text-dark-400">{msg.media_caption}</p>
)}
</div>
{msg.message_text && (
<p
className="whitespace-pre-wrap text-sm text-dark-200 [&_a]:text-accent-400 [&_a]:underline"
dangerouslySetInnerHTML={{ __html: linkifyText(msg.message_text) }}
/>
)}
<MessageMediaGrid message={msg} />
</div>
))}
<div ref={messagesEndRef} />