import { useState, useEffect, useCallback, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, Calendar, AtSign, Edit3, Check, Loader2, Image as ImageIcon, FileText, Link as LinkIcon, Download, ExternalLink, Play, UserPlus, UserMinus, UserCheck, Clock, Search, ChevronLeft, Eye, Users, Video, Camera, Trash2, MessageSquare, Phone, Bell, BellOff, MoreHorizontal } from 'lucide-react'; import Cropper from 'react-easy-crop'; import { UserApi } from '../../infrastructure/userApi'; import { ChatApi } from '../../../chats/infrastructure/chatApi'; import { FriendApi } from '../../../friends/infrastructure/friendApi'; import { StoryApi } from '../../../stories/infrastructure/storyApi'; import { useAuthStore } from '../../../auth/application/authStore'; import { useLang } from '../../../../lib/i18n'; import { User, Message, FriendshipStatus, StoryGroup } from '../../../../lib/types'; import ConfirmModal from '../../../../components/ConfirmModal'; import ImageLightbox from '../../../../components/ImageLightbox'; import StoryViewer from '../../../../modules/stories/presentation/components/StoryViewer'; import { getSocket } from '../../../../lib/socket'; import { useStoryStore } from '../../../../modules/stories/application/storyStore'; import { getMediaUrl } from '../../../../lib/utils'; import { getCroppedImg } from '../../../../lib/imageCrop'; import DatePicker from '../../../../components/DatePicker'; import { useChatStore } from '../../../chats/application/chatStore'; import { toggleMuteChat, isChatMuted } from '../../../../lib/sounds'; interface UserProfileProps { userId: string; chatId?: string; onClose: () => void; onGoToMessage?: (messageId: string) => void; isSelf?: boolean; } type MediaTab = 'publications' | 'gifs' | 'media' | 'files' | 'links'; export default function UserProfile({ userId, chatId, onClose, onGoToMessage, isSelf }: UserProfileProps) { const { user: authUser, config } = useAuthStore(); const { t, lang } = useLang(); const [profile, setProfile] = useState(null); const [isLoading, setIsLoading] = useState(true); const [activeTab, setActiveTab] = useState('publications'); const { chats, addChat, setActiveChat, loadMessages } = useChatStore(); const personalChat = chats.find(c => c.type === 'personal' && c.members.some(m => m.user.id === userId)); const commonGroups = chats.filter(c => c.type === 'group' && c.members.some(m => m.user.id === userId)); const [isMutedLocally, setIsMutedLocally] = useState(() => personalChat ? isChatMuted(personalChat.id) : false); useEffect(() => { if (personalChat) setIsMutedLocally(isChatMuted(personalChat.id)); }, [personalChat?.id]); const handleOpenChat = async () => { try { let targetChatId = personalChat?.id; if (!targetChatId) { const chat = await ChatApi.createPersonalChat(userId); addChat(chat); const socket = getSocket(); if (socket) socket.emit('join_chat', chat.id); targetChatId = chat.id; } setActiveChat(targetChatId); loadMessages(targetChatId); onClose(); } catch (e) { console.error(e); } }; const handleToggleMute = () => { if (!personalChat) return; const muted = toggleMuteChat(personalChat.id); setIsMutedLocally(muted); }; const handleStartCall = (type: 'voice' | 'video') => { if (profile) { window.dispatchEvent(new CustomEvent('START_CALL', { detail: { targetUser: profile, type } })); onClose(); } }; // Shared media state const [sharedMedia, setSharedMedia] = useState([]); const [sharedGifs, setSharedGifs] = useState([]); const [sharedFiles, setSharedFiles] = useState([]); const [sharedLinks, setSharedLinks] = useState>([]); const [tabLoading, setTabLoading] = useState(false); const [userStories, setUserStories] = useState([]); // Changed from StoryGroup | null to any[] as per instruction const [loadedTabs, setLoadedTabs] = useState>(new Set()); const [lightboxIndex, setLightboxIndex] = useState(null); const { openViewer } = useStoryStore(); const [storyViewerOpen, setStoryViewerOpen] = useState(false); const [initialStoryIdx, setInitialStoryIdx] = useState(0); // Friend state const [friendStatus, setFriendStatus] = useState(null); const [friendLoading, setFriendLoading] = useState(false); // Profile Edit State const [isEditing, setIsEditing] = useState(false); const [displayName, setDisplayName] = useState(''); const [bio, setBio] = useState(''); const [birthday, setBirthday] = useState(''); const [isSaving, setIsSaving] = useState(false); // Avatar edit state const [isCropping, setIsCropping] = useState(false); const [cropFile, setCropFile] = useState(null); const [cropImage, setCropImage] = useState(null); const [crop, setCrop] = useState({ x: 0, y: 0 }); const [zoom, setZoom] = useState(1); const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); const API_URL = import.meta.env.VITE_API_URL || ''; const allMedia = sharedMedia.flatMap(msg => (msg.media || []).map(m => ({ ...m, url: getMediaUrl(m.url), messageId: msg.id, createdAt: msg.createdAt }))); const allGifs = sharedGifs.flatMap(msg => (msg.media || []).map(m => ({ ...m, url: getMediaUrl(m.url), messageId: msg.id, createdAt: msg.createdAt }))); const sortedStories = [...userStories].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); 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))}
))}
); }; useEffect(() => { loadProfile(); if (!isSelf) { FriendApi.getFriendshipStatus(userId).then(setFriendStatus).catch(() => {}); } }, [userId, isSelf]); // Load shared media/files/links when tab changes const loadTabData = useCallback(async (tab: MediaTab) => { if (loadedTabs.has(tab)) return; setTabLoading(true); try { if (tab === 'publications') { const data = await StoryApi.getUserStories(userId); setUserStories(data.stories || []); } else if (chatId) { // Only load media/files/links if chatId is available const data = await ChatApi.getSharedMedia(chatId, 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); } }, [chatId, userId, loadedTabs]); useEffect(() => { const tabsToLoad: MediaTab[] = ['publications']; if (chatId) { tabsToLoad.push('gifs', 'media', 'files', 'links'); } tabsToLoad.forEach(tab => loadTabData(tab)); }, [chatId, loadTabData]); const loadProfile = async () => { try { setIsLoading(true); if (isSelf && authUser) { setProfile(authUser); setDisplayName(authUser.displayName || ''); setBio(authUser.bio || ''); setBirthday(authUser.birthday || ''); } else { const data = await UserApi.getUser(userId); setProfile(data); if (isSelf) { setDisplayName(data.displayName || ''); setBio(data.bio || ''); setBirthday(data.birthday || ''); } } } catch (e) { console.error(e); } finally { setIsLoading(false); } }; const handleSave = async () => { try { setIsSaving(true); const dateToSave = birthday ? new Date(birthday).toISOString() : undefined; const updated = await UserApi.updateProfile({ displayName: displayName.trim(), bio: bio.trim(), birthday: dateToSave, }); setProfile(updated); useAuthStore.getState().updateUser(updated); setIsEditing(false); } catch (e) { console.error(e); } finally { setIsSaving(false); } }; const handleSendFriendRequest = async () => { try { setFriendLoading(true); const result = await FriendApi.sendFriendRequest(userId); if (result.status === 'accepted') { setFriendStatus({ status: 'accepted', friendshipId: null }); } else { setFriendStatus({ status: 'pending', friendshipId: null, direction: 'outgoing' }); } // Notify via socket const socket = getSocket(); if (socket) socket.emit('friend_request', { friendId: userId }); } catch (e) { console.error(e); } finally { setFriendLoading(false); } }; const handleAcceptFriend = async () => { if (!friendStatus?.friendshipId) return; try { setFriendLoading(true); await FriendApi.acceptFriendRequest(friendStatus.friendshipId); setFriendStatus({ status: 'accepted', friendshipId: friendStatus.friendshipId }); const socket = getSocket(); if (socket) socket.emit('friend_accepted', { friendId: userId }); } catch (e) { console.error(e); } finally { setFriendLoading(false); } }; const handleRemoveFriend = async () => { if (!friendStatus?.friendshipId) return; try { setFriendLoading(true); await FriendApi.removeFriend(friendStatus.friendshipId); setFriendStatus({ status: 'none', friendshipId: null }); const socket = getSocket(); if (socket) socket.emit('friend_removed', { friendId: userId }); } catch (e) { console.error(e); } finally { setFriendLoading(false); } }; const handleAvatarSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setCropFile(file); const url = URL.createObjectURL(file); setCropImage(url); setIsCropping(true); }; const handleCropSave = async () => { if (!cropImage || !croppedAreaPixels) return; try { setTabLoading(true); // Reusing tabLoading for avatar upload const croppedFile = await getCroppedImg(cropImage, croppedAreaPixels); if (!croppedFile) throw new Error("Could not crop image"); const updatedUser = await UserApi.uploadAvatar(croppedFile); setProfile(updatedUser); useAuthStore.getState().updateUser(updatedUser); setIsCropping(false); setCropImage(null); setCropFile(null); setCroppedAreaPixels(null); setCrop({ x: 0, y: 0 }); setZoom(1); } catch (e) { console.error('Failed to save avatar', e); } finally { setTabLoading(false); } }; const handleRemoveAvatar = async () => { try { setTabLoading(true); await UserApi.removeAvatar(); const updatedUser = { ...profile!, avatar: null }; setProfile(updatedUser); useAuthStore.getState().updateUser({ avatar: null }); } catch (err) { console.error(err); } finally { setTabLoading(false); } }; const initials = (profile?.displayName || profile?.username || '??') .split(' ') .map((w: string) => w[0]) .join('') .slice(0, 2) .toUpperCase(); const tabsConfig = [ { key: 'publications' as const, label: t('publicationsTab') || 'Публикации', icon: Play, count: sortedStories.length }, ...(chatId ? [ { 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 => !loadedTabs.has(tab.key) || tab.count > 0); useEffect(() => { const expected = chatId ? 5 : 1; if (loadedTabs.size === expected && availableTabs.length > 0 && !availableTabs.find(t => t.key === activeTab)) { setActiveTab(availableTabs[0].key as any); } }, [loadedTabs, activeTab, chatId, availableTabs]); return ( <> {/* Шапка */}

{(isSelf ? t('myProfile') : t('profileTitle')) as string}

{isSelf && ( !isEditing ? ( ) : ( ) )}
{isLoading ? (
) : profile ? (
{/* Аватар */}
{profile.avatar ? ( ) : (
{initials}
)}
{isSelf && (
)} {isSelf && profile.avatar && ( )}
{/* Имя */} {isEditing ? (
setDisplayName(e.target.value)} placeholder={t('enterName')} className="relative text-lg font-bold text-center text-white bg-black/40 border border-white/20 outline-none px-4 py-2.5 w-full rounded-2xl transition-colors focus:bg-black/60 focus:border-knot-400 placeholder-white/30 truncate" />
) : (

{profile.displayName || profile.username}

)} {/* Username (неизменяемый) */}
{profile.username}
{/* Онлайн статус */}

{profile.isOnline ? ( {t('online')} ) : ( {t('wasRecently')} )}

{/* Friend button (for other users only) */} {!isSelf && friendStatus && (
{friendStatus.status === 'none' && ( )} {friendStatus.status === 'pending' && friendStatus.direction === 'outgoing' && (
{t('requestSent')}
)} {friendStatus.status === 'pending' && friendStatus.direction === 'incoming' && (
)} {friendStatus.status === 'accepted' && ( )}
)} {/* Быстрые действия (только для других пользователей) */} {!isSelf && (
{config?.enableCalls && ( )}
)}
{/* Информация */}
{/* О себе */}
{isEditing ? (