import { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, Camera, Edit3, Check, Loader2, UserPlus, Trash2, Search, Crown, Users, ImageIcon, FileText, Link as LinkIcon, Play, Download, ExternalLink, Video } from 'lucide-react'; import Cropper from 'react-easy-crop'; import { ChatApi } from '../../infrastructure/chatApi'; import { UserApi } from '../../../users/infrastructure/userApi'; import { useAuthStore } from '../../../auth/application/authStore'; import { useChatStore } from '../../application/chatStore'; import { useLang } from '../../../../core/infrastructure/i18n'; import { Chat, UserPresence, Message } from '../../../../core/domain/types'; import Avatar from '../../../../core/presentation/components/ui/Avatar'; import ConfirmModal from '../../../../core/presentation/components/ui/ConfirmModal'; import ImageLightbox from '../../../../core/presentation/components/ui/ImageLightbox'; import { getMediaUrl } from '../../../../core/utils/utils'; import { getCroppedImg } from '../../../../core/infrastructure/imageCrop'; interface GroupSettingsProps { chat: Chat; onClose: () => void; onGoToMessage?: (messageId: string, sequenceId?: number) => void; } export default function GroupSettings({ chat, onClose, onGoToMessage }: GroupSettingsProps) { const { user } = useAuthStore(); const { updateChat } = useChatStore(); const { t, lang } = useLang(); const currentMember = chat.members.find((m) => m.user.id === user?.id); const [removeTargetId, setRemoveTargetId] = useState(null); const isAdmin = ['admin', 'owner', 'Admin', 'Owner'].includes(currentMember?.role || ''); const [isEditingName, setIsEditingName] = useState(false); const [isEditingDesc, setIsEditingDesc] = useState(false); const [groupName, setGroupName] = useState(chat.name || ''); const [groupDesc, setGroupDesc] = useState(chat.description || ''); const [isSaving, setIsSaving] = useState(false); const [avatarUploading, setAvatarUploading] = useState(false); const [showAddMember, setShowAddMember] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [activeTab, setActiveTab] = useState<'members' | 'gifs' | 'media' | 'files' | 'links'>('members'); const [tabLoading, setTabLoading] = useState(false); const [sharedMedia, setSharedMedia] = useState([]); const [sharedGifs, setSharedGifs] = useState([]); const [sharedFiles, setSharedFiles] = useState([]); const [sharedLinks, setSharedLinks] = useState>([]); const [loadedTabs, setLoadedTabs] = useState>(new Set()); const [lightboxIndex, setLightboxIndex] = useState(null); // Cropping states const [isCropping, setIsCropping] = useState(false); const [cropImage, setCropImage] = useState(null); const [cropFile, setCropFile] = useState(null); const [crop, setCrop] = useState({ x: 0, y: 0 }); const [zoom, setZoom] = useState(1); const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); const fileInputRef = useRef(null); const searchInputRef = useRef(null); // Keep local state in sync with chat prop useEffect(() => { setGroupName(chat.name || ''); setGroupDesc(chat.description || ''); }, [chat.name, chat.description]); // Search users to add useEffect(() => { if (!searchQuery.trim()) { setSearchResults([]); return; } const timer = setTimeout(async () => { try { setIsSearching(true); const results = await UserApi.searchUsers(searchQuery); // Filter out users already in the group const memberIds = new Set(chat.members.map((m) => m.user.id)); setSearchResults(results.filter((u) => !memberIds.has(u.id))); } catch (e) { console.error(e); } finally { setIsSearching(false); } }, 300); return () => clearTimeout(timer); }, [searchQuery, chat.members]); const handleSaveName = async () => { if (!groupName.trim()) return; try { setIsSaving(true); const updatedChat = await ChatApi.updateGroup(chat.id, { name: groupName.trim() }); updateChat(updatedChat); setIsEditingName(false); } catch (e) { console.error(e); } finally { setIsSaving(false); } }; const handleSaveDesc = async () => { try { setIsSaving(true); const updatedChat = await ChatApi.updateGroup(chat.id, { description: groupDesc.trim() }); updateChat(updatedChat); setIsEditingDesc(false); } catch (e) { console.error(e); } finally { setIsSaving(false); } }; const handleFileChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = () => { setCropImage(reader.result as string); setCropFile(file); setIsCropping(true); }; reader.readAsDataURL(file); } }; const handleCropSave = async () => { if (!cropImage || !croppedAreaPixels) return; setAvatarUploading(true); try { const croppedFile = await getCroppedImg(cropImage, croppedAreaPixels); if (!croppedFile) throw new Error("Could not crop image"); const updatedChat = await ChatApi.uploadGroupAvatar(chat.id, croppedFile); useChatStore.getState().updateChat({ ...chat, avatar: updatedChat.avatar }); setIsCropping(false); setCropImage(null); setCropFile(null); } catch (err) { console.error('Failed to crop group avatar:', err); alert(t('error')); } finally { setAvatarUploading(false); } }; const handleAvatarUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; try { setAvatarUploading(true); const updatedChat = await ChatApi.uploadGroupAvatar(chat.id, file); updateChat(updatedChat); } catch (e) { console.error(e); } finally { setAvatarUploading(false); e.target.value = ''; } }; const handleRemoveAvatar = async () => { try { setAvatarUploading(true); const updatedChat = await ChatApi.removeGroupAvatar(chat.id); updateChat(updatedChat); } catch (e) { console.error(e); } finally { setAvatarUploading(false); } }; const handleAddMember = async (userId: string) => { try { const updatedChat = await ChatApi.addGroupMembers(chat.id, [userId]); updateChat(updatedChat); setSearchQuery(''); setSearchResults([]); } catch (e) { console.error(e); } }; const handleRemoveMember = async (userId: string) => { setRemoveTargetId(userId); }; const confirmRemoveMember = async () => { if (!removeTargetId) return; try { const updatedChat = await ChatApi.removeGroupMember(chat.id, removeTargetId); updateChat(updatedChat); } catch (e) { console.error(e); } setRemoveTargetId(null); }; const initials = (chat.name || 'G') .split(' ') .map((w: string) => w[0]) .join('') .slice(0, 2) .toUpperCase(); const loadTabData = async (tab: 'gifs' | 'media' | 'files' | 'links') => { if (loadedTabs.has(tab)) return; setTabLoading(true); try { const data = await ChatApi.getSharedMedia(chat.id, tab); if (tab === 'media') setSharedMedia(data); else if (tab === 'gifs') setSharedGifs(data); else if (tab === 'files') setSharedFiles(data); else setSharedLinks(data); setLoadedTabs(prev => new Set(prev).add(tab)); } catch (e) { console.error('Failed to load shared', tab, e); } finally { setTabLoading(false); } }; useEffect(() => { loadTabData('gifs'); loadTabData('media'); loadTabData('files'); loadTabData('links'); }, [chat.id]); const API_URL = import.meta.env.VITE_API_URL || ''; const allMedia = sharedMedia.flatMap(msg => (msg.media || []).map(m => ({ ...m, url: getMediaUrl(m.url), thumbnail: m.thumbnail ? getMediaUrl(m.thumbnail) : undefined, messageId: msg.id, createdAt: msg.createdAt, sequenceId: msg.sequenceId }))); const allGifs = sharedGifs.flatMap(msg => (msg.media || []).map(m => ({ ...m, url: getMediaUrl(m.url), thumbnail: m.thumbnail ? getMediaUrl(m.thumbnail) : undefined, messageId: msg.id, createdAt: msg.createdAt, sequenceId: msg.sequenceId }))); const sortedMedia = [...allMedia].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); const sortedGifs = [...allGifs].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); const sortedFiles = [...sharedFiles].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); const sortedLinks = [...sharedLinks].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); const renderGrouped = ( sortedItems: T[], renderItem: (item: T, originalIndex: number) => React.ReactNode, gridClass?: string ) => { let currentGroup: { dateStr: string; items: {item: T, idx: number}[] } | null = null; const groups: { dateStr: string; items: {item: T, idx: number}[] }[] = []; sortedItems.forEach((item, idx) => { const date = new Date(item.createdAt); const dateStr = date.toLocaleDateString(lang === 'ru' ? 'ru-RU' : 'en-US', { day: 'numeric', month: 'long', year: 'numeric' }); if (currentGroup?.dateStr !== dateStr) { currentGroup = { dateStr, items: [] }; groups.push(currentGroup); } currentGroup.items.push({item, idx}); }); return (
{groups.map((g, i) => (
{g.dateStr}
{g.items.map(({item, idx}) => renderItem(item, idx))}
))}
); }; const tabsConfig = [ { key: 'members' as const, label: t('membersCount') || 'Участники', icon: Users, count: chat.members.length }, { key: 'gifs' as const, label: 'GIF', icon: Play, count: sortedGifs.length }, { key: 'media' as const, label: t('mediaTab') || 'Медиа', icon: ImageIcon, count: sortedMedia.length }, { key: 'files' as const, label: t('filesTab') || 'Файлы', icon: FileText, count: sortedFiles.flatMap(msg => msg.media || []).length }, { key: 'links' as const, label: t('linksTab') || 'Ссылки', icon: LinkIcon, count: sortedLinks.flatMap(msg => msg.links || []).length }, ]; const availableTabs = tabsConfig.filter(tab => tab.key === 'members' || !loadedTabs.has(tab.key) || tab.count > 0); useEffect(() => { if (loadedTabs.size === 4 && availableTabs.length > 0 && !availableTabs.find(t => t.key === activeTab)) { setActiveTab(availableTabs[0].key); } }, [loadedTabs, activeTab]); // availableTabs removed from dependencies to avoid infinite loops since its reference runs on every render return ( <> {/* Header */}

{t('groupSettings')}

{/* Avatar */}
{isAdmin && ( <> {chat.avatar && !avatarUploading && ( )} )}
{isEditingName ? (
setGroupName(e.target.value)} className="bg-surface-tertiary border border-accent/30 rounded-xl px-4 py-2 text-lg font-bold text-white text-center focus:outline-none focus:border-accent" autoFocus />
) : (
isAdmin && setIsEditingName(true)} >

{chat.name || t('group')}

{isAdmin && ( )}
)}

{chat.members.length} {t('members')}

{/* Description */}
{isEditingDesc ? (