import { useState, useEffect, useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { X, MessageSquare, Phone, MoreHorizontal, UserMinus, UserPlus, UserCheck, Calendar, Info, BellOff, Image as ImageIcon, Film, FileText, Link as LinkIcon, Search, ExternalLink, Download } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { useLang } from '../../../../core/infrastructure/i18n'; import { useAuthStore } from '../../../auth/application/authStore'; import { useFriendStore } from '../../../friends/application/friendStore'; import { ChatApi } from '../../../chats/infrastructure/chatApi'; import { httpClient } from '../../../../core/infrastructure/httpClient'; import { Loader2 } from 'lucide-react'; import ImageLightbox from '../../../../core/presentation/components/ui/ImageLightbox'; import type { FriendWithId, FriendRequest } from '../../../../core/domain/types'; import Avatar from '../../../../core/presentation/components/ui/Avatar'; import ConfirmModal from '../../../../core/presentation/components/ui/ConfirmModal'; interface UserProfileProps { userId: string; onClose: () => void; onMessage?: (userId: string) => void; isSelf?: boolean; chatId?: string; onGoToMessage?: (msgId: string, sequenceId?: number) => void; } type TabType = 'media' | 'gif' | 'files' | 'links'; export default function UserProfile({ userId, onClose, onMessage, isSelf: isSelfProp, chatId, onGoToMessage }: UserProfileProps) { const { t, lang } = useLang(); const { user: currentUser } = useAuthStore(); const { friends, friendRequests, sendRequest, removeFriend, loadFriends } = useFriendStore(); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState('media'); const [tabData, setTabData] = useState([]); const [counts, setCounts] = useState>({}); const [countsLoading, setCountsLoading] = useState(true); const [contentLoading, setContentLoading] = useState(false); // Lightbox state const [lightbox, setLightbox] = useState<{ open: boolean; index: number }>({ open: false, index: 0 }); const targetId = useMemo(() => userId?.toLowerCase(), [userId]); const isMe = useMemo(() => isSelfProp || currentUser?.id?.toLowerCase() === targetId, [isSelfProp, currentUser?.id, targetId]); const fetchUser = useCallback(async () => { try { const res = await httpClient.request(`/profiles/${userId}`); setUser(res); } catch (e) { console.error(e); } finally { setLoading(false); } }, [userId]); const loadSummary = useCallback(async () => { if (!chatId) { setCountsLoading(false); return; } try { const types: TabType[] = ['media', 'gif', 'files', 'links']; const data = await Promise.all( types.map(t => ChatApi.getSharedMedia(chatId, t === 'gif' ? 'gifs' : t)) ); const newCounts: Record = {}; types.forEach((t, i) => { newCounts[t] = data[i].length; }); setCounts(newCounts); const available = types.filter(t => newCounts[t] > 0); if (available.length > 0 && !available.includes(activeTab)) { setActiveTab(available[0]); } } catch (e) { console.error(e); } finally { setCountsLoading(false); } }, [chatId, activeTab]); const loadCurrentTabContent = useCallback(async () => { if (!chatId) return; setContentLoading(true); try { const data = await ChatApi.getSharedMedia(chatId, activeTab === 'gif' ? 'gifs' : activeTab); setTabData(data); } catch (e) { console.error(e); } finally { setContentLoading(false); } }, [chatId, activeTab]); useEffect(() => { fetchUser(); loadSummary(); loadFriends(); }, [fetchUser, loadSummary, loadFriends]); useEffect(() => { loadCurrentTabContent(); }, [loadCurrentTabContent]); const friend = useMemo(() => friends.find((f: FriendWithId) => f.id?.toLowerCase() === targetId), [friends, targetId]); const outgoingReq = useMemo(() => friendRequests.find((r: FriendRequest) => r.user?.id?.toLowerCase() === targetId && r.isOutgoing), [friendRequests, targetId]); const incomingReq = useMemo(() => friendRequests.find((r: FriendRequest) => r.user?.id?.toLowerCase() === targetId && !r.isOutgoing), [friendRequests, targetId]); const formatRegistrationDate = (dateStr: string) => { if (!dateStr) return ''; const date = new Date(dateStr); if (lang === 'ru') { const monthsGenitive = [ 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря' ]; return `${date.getDate()} ${monthsGenitive[date.getMonth()]} ${date.getFullYear()} г.`; } return date.toLocaleDateString('en-US', { day: 'numeric', month: 'long', year: 'numeric' }); }; const resolveUrl = (url?: string) => { if (!url) return ''; if (url.startsWith('http')) return url; const baseUrl = (import.meta.env.VITE_API_URL || '').replace(/\/api$/, ''); return `${baseUrl}${url.startsWith('/') ? '' : '/'}${url}`; }; const formatSize = (bytes?: number) => { if (!bytes) return ''; const units = ['B', 'KB', 'MB', 'GB']; let i = 0; while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++; } return `${bytes.toFixed(1)} ${units[i]}`; }; const handleFriendAction = async () => { if (friend) { await removeFriend(friend.friendshipId); } else if (!outgoingReq && !incomingReq) { await sendRequest(userId); } }; const availableTabs = useMemo(() => { const all: { id: TabType, icon: any, label: string }[] = [ { id: 'media', icon: ImageIcon, label: t('mediaTab') }, { id: 'gif', icon: Film, label: 'GIF' }, { id: 'files', icon: FileText, label: t('filesTab') }, { id: 'links', icon: LinkIcon, label: t('linksTab') } ]; return isMe ? all : all.filter(t => counts[t.id] > 0); }, [counts, lang, isMe, t]); // Gallery items for Lightbox const galleryItems = useMemo(() => { return tabData .flatMap(item => { if (!item.media || item.media.length === 0) return []; return item.media.map((m: any) => ({ id: m.id, url: resolveUrl(m.url), type: (m.type?.toLowerCase() === 'video' || m.url.toLowerCase().endsWith('.mp4')) ? 'video' : 'image', originalItem: item })); }) .filter(i => { if (activeTab === 'media') { const isGif = i.type === 'video' && (i.url.toLowerCase().endsWith('.mp4') || i.url.toLowerCase().indexOf('klipy') !== -1); const isStandardImageGif = i.type === 'image' && i.url.toLowerCase().endsWith('.gif'); return !(isGif || isStandardImageGif); } return true; }); }, [tabData, activeTab]); const openInLightbox = (idx: number) => { setLightbox({ open: true, index: idx }); }; return createPortal( e.stopPropagation()} className="relative w-full lg:max-w-[850px] h-full lg:h-[85vh] bg-[#0a0a0a] lg:rounded-[3rem] border-none lg:border lg:border-white/10 shadow-2xl overflow-hidden flex flex-col backdrop-blur-3xl" >
{t('userProfileUpper')}
{loading ? (
) : ( <> {/* Profile Card */}

{user.displayName}

@{user.username || 'user_' + userId.slice(0, 5)}
{t('joined')} {formatRegistrationDate(user.createdAt)}
{!isMe && (
{friend ? ( ) : outgoingReq ? ( ) : incomingReq ? ( ) : ( )}
)}
{/* About Section */}
{t('aboutMe')}

{user.bio || t('noBio')}

{/* Tabs Nav */} {availableTabs.length > 0 && (
{availableTabs.map((t) => ( ))}
)} {/* Tab Content */}
{contentLoading ? ( ) : tabData.length === 0 ? ( {t('empty')} ) : ( {(activeTab === 'media' || activeTab === 'gif') ? ( tabData.flatMap((item) => (item.media || []).map((m: any) => { const mType = m.type?.toLowerCase() || 'image'; const mFilename = m.filename?.toLowerCase() || ''; const isGif = mType === 'gif' || (mType === 'image' && (mFilename.endsWith('.gif') || mFilename.endsWith('.mp4'))) || mFilename.includes('gif') || mFilename.includes('animation') || m.url?.toLowerCase().includes('klipy'); if (activeTab === 'media' && isGif) return null; if (activeTab === 'gif' && !isGif) return null; const isVideo = mType === 'video' || m.url?.toLowerCase().endsWith('.mp4'); return (
); })).filter(Boolean) ) : ( tabData.map((item, idx) => { const mediaItem = item.media?.[0]; const linkUrl = item.links && item.links.length > 0 ? item.links[0] : (mediaItem?.url || ''); const mType = mediaItem?.type?.toLowerCase() || 'file'; const mFilename = mediaItem?.filename?.toLowerCase() || ''; const isGif = mType === 'gif' || (mType === 'image' && (mFilename.endsWith('.mp4') || mFilename.endsWith('.gif'))) || mFilename.includes('gif') || mFilename.includes('animation') || (mediaItem?.url?.toLowerCase().includes('klipy')); if (activeTab === 'files') { if (!mediaItem) return null; if (mType === 'image' || mType === 'video' || isGif) return null; } if (activeTab === 'links' && (!item.links || item.links.length === 0)) return null; return (
{activeTab === 'files' ? : }
{activeTab === 'links' ? (item.content || linkUrl) : (mediaItem?.filename || item.content || 'File')}
{activeTab === 'files' ? formatSize(mediaItem?.size) : (linkUrl?.length > 50 ? linkUrl.slice(0, 50) + '...' : linkUrl)}
{activeTab === 'files' ? ( ) : ( )}
); }).filter(Boolean) )}
)}
)}
{/* Gallery Viewer */} {lightbox.open && ( setLightbox(prev => ({ ...prev, open: false }))} /> )} , document.body ); }