Групповые звонки
This commit is contained in:
@@ -268,7 +268,15 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
localStreamRef.current.addTrack(videoTrack);
|
||||
// Add to all peer connections
|
||||
for (const [targetUserId, peer] of peersRef.current) {
|
||||
peer.pc.addTrack(videoTrack, localStreamRef.current);
|
||||
const transceiver = peer.pc.getTransceivers().find(t => t.receiver?.track?.kind === 'video' || t.sender?.track?.kind === 'video');
|
||||
if (transceiver) {
|
||||
await transceiver.sender.replaceTrack(videoTrack).catch(() => {});
|
||||
if (transceiver.direction === 'recvonly' || transceiver.direction === 'inactive') {
|
||||
transceiver.direction = 'sendrecv';
|
||||
}
|
||||
} else {
|
||||
peer.pc.addTrack(videoTrack, localStreamRef.current);
|
||||
}
|
||||
const offer = await peer.pc.createOffer();
|
||||
await peer.pc.setLocalDescription(offer);
|
||||
const socket = getSocket();
|
||||
@@ -291,13 +299,17 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
screenStreamRef.current.getTracks().forEach(t => t.stop());
|
||||
screenStreamRef.current = null;
|
||||
}
|
||||
// Replace screen track with null on all peers
|
||||
// Replace screen track with camera track on all peers
|
||||
for (const [targetUserId, peer] of peersRef.current) {
|
||||
const sender = peer.pc.getSenders().find(s => s.track?.kind === 'video');
|
||||
if (sender) {
|
||||
await sender.replaceTrack(null);
|
||||
const transceiver = peer.pc.getTransceivers().find(t => t.sender === sender);
|
||||
if (transceiver) transceiver.direction = 'recvonly';
|
||||
const transceiver = peer.pc.getTransceivers().find(t => t.receiver?.track?.kind === 'video' || t.sender?.track?.kind === 'video');
|
||||
if (transceiver) {
|
||||
const camTrack = localStreamRef.current?.getVideoTracks().find(t => t.enabled);
|
||||
await transceiver.sender.replaceTrack(camTrack || null).catch(() => {});
|
||||
if (!camTrack) {
|
||||
transceiver.direction = 'recvonly';
|
||||
} else {
|
||||
transceiver.direction = 'sendrecv';
|
||||
}
|
||||
const offer = await peer.pc.createOffer();
|
||||
await peer.pc.setLocalDescription(offer);
|
||||
const socket = getSocket();
|
||||
@@ -317,24 +329,19 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
const screenTrack = screenStream.getVideoTracks()[0];
|
||||
|
||||
for (const [targetUserId, peer] of peersRef.current) {
|
||||
const sender = peer.pc.getSenders().find(s => s.track?.kind === 'video');
|
||||
if (sender) {
|
||||
await sender.replaceTrack(screenTrack);
|
||||
const transceiver = peer.pc.getTransceivers().find(t => t.sender === sender);
|
||||
if (transceiver && (transceiver.direction === 'recvonly' || transceiver.direction === 'inactive')) {
|
||||
const transceiver = peer.pc.getTransceivers().find(t => t.receiver?.track?.kind === 'video' || t.sender?.track?.kind === 'video');
|
||||
if (transceiver) {
|
||||
await transceiver.sender.replaceTrack(screenTrack).catch(() => {});
|
||||
if (transceiver.direction === 'recvonly' || transceiver.direction === 'inactive') {
|
||||
transceiver.direction = 'sendrecv';
|
||||
const offer = await peer.pc.createOffer();
|
||||
await peer.pc.setLocalDescription(offer);
|
||||
const socket = getSocket();
|
||||
socket?.emit('group_call_renegotiate', { chatId, targetUserId, offer: peer.pc.localDescription });
|
||||
}
|
||||
} else {
|
||||
peer.pc.addTrack(screenTrack, localStreamRef.current || screenStream);
|
||||
const offer = await peer.pc.createOffer();
|
||||
await peer.pc.setLocalDescription(offer);
|
||||
const socket = getSocket();
|
||||
socket?.emit('group_call_renegotiate', { chatId, targetUserId, offer: peer.pc.localDescription });
|
||||
}
|
||||
const offer = await peer.pc.createOffer();
|
||||
await peer.pc.setLocalDescription(offer);
|
||||
const socket = getSocket();
|
||||
socket?.emit('group_call_renegotiate', { chatId, targetUserId, offer: peer.pc.localDescription });
|
||||
}
|
||||
|
||||
screenTrack.onended = () => {
|
||||
@@ -540,6 +547,7 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
if (data.chatId !== chatId) return;
|
||||
const newMap = new Map<string, ParticipantInfo>();
|
||||
for (const p of data.participants) {
|
||||
if (p.id === useAuthStore.getState().user?.id) continue;
|
||||
newMap.set(p.id, p);
|
||||
if (p.isSharingScreen && p.id !== useAuthStore.getState().user?.id) {
|
||||
setSharingUserId(p.id);
|
||||
@@ -557,7 +565,7 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
};
|
||||
|
||||
const onUserJoined = (data: { chatId: string; userId: string; userInfo: ParticipantInfo }) => {
|
||||
if (data.chatId !== chatId) return;
|
||||
if (data.chatId !== chatId || data.userId === useAuthStore.getState().user?.id) return;
|
||||
setParticipants(prev => {
|
||||
const next = new Map(prev);
|
||||
next.set(data.userId, data.userInfo);
|
||||
|
||||
@@ -162,7 +162,7 @@ export default function StoryViewer({ stories, initialUserIndex, initialStoryInd
|
||||
}, [storyIndex, userIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (paused || !currentStory || isVideo) return;
|
||||
if (paused || showReactions || showReplyInput || showViewers || !currentStory || isVideo) return;
|
||||
|
||||
const duration = STORY_DURATION;
|
||||
const step = (TICK / duration) * 100;
|
||||
@@ -180,12 +180,12 @@ export default function StoryViewer({ stories, initialUserIndex, initialStoryInd
|
||||
return () => {
|
||||
if (timerRef.current) clearInterval(timerRef.current);
|
||||
};
|
||||
}, [storyIndex, userIndex, paused, goNext, isVideo, currentStory]);
|
||||
}, [storyIndex, userIndex, paused, showReactions, showReplyInput, showViewers, goNext, isVideo, currentStory]);
|
||||
|
||||
// Handle video progress
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video || !isVideo || paused) return;
|
||||
if (!video || !isVideo || paused || showReactions || showReplyInput || showViewers) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (video.duration) {
|
||||
@@ -195,7 +195,7 @@ export default function StoryViewer({ stories, initialUserIndex, initialStoryInd
|
||||
}, TICK);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isVideo, paused, storyIndex, userIndex]);
|
||||
}, [isVideo, paused, showReactions, showReplyInput, showViewers, storyIndex, userIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 } from 'lucide-react';
|
||||
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 { api } from '../lib/api';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
@@ -14,6 +14,8 @@ import { useStoryStore } from '../stores/useStoryStore';
|
||||
import { getMediaUrl } from '../lib/utils';
|
||||
import { getCroppedImg } from '../lib/imageCrop';
|
||||
import DatePicker from './DatePicker';
|
||||
import { useChatStore } from '../stores/chatStore';
|
||||
import { toggleMuteChat, isChatMuted } from '../lib/sounds';
|
||||
|
||||
interface UserProfileProps {
|
||||
userId: string;
|
||||
@@ -31,6 +33,48 @@ export default function UserProfile({ userId, chatId, onClose, onGoToMessage, is
|
||||
const [profile, setProfile] = useState<User | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<MediaTab>('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 api.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<Message[]>([]);
|
||||
@@ -507,6 +551,48 @@ export default function UserProfile({ userId, chatId, onClose, onGoToMessage, is
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Быстрые действия (только для других пользователей) */}
|
||||
{!isSelf && (
|
||||
<div className="flex w-full items-center justify-center gap-2 mt-6 px-4">
|
||||
<button
|
||||
onClick={handleOpenChat}
|
||||
className="flex-1 flex flex-col items-center gap-1.5 py-2.5 rounded-2xl bg-white/5 hover:bg-white/10 transition-colors border border-white/5"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-black/40 flex items-center justify-center">
|
||||
<MessageSquare size={16} className="text-white" />
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-white">{t('chat')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleToggleMute}
|
||||
disabled={!personalChat}
|
||||
className={`flex-1 flex flex-col items-center gap-1.5 py-2.5 rounded-2xl transition-colors border ${!personalChat ? 'opacity-50 cursor-not-allowed bg-white/5 border-white/5' : 'bg-white/5 hover:bg-white/10 border-white/5'}`}
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-black/40 flex items-center justify-center">
|
||||
{isMutedLocally ? <BellOff size={16} className="text-white" /> : <Bell size={16} className="text-white" />}
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-white shadow-sm">{isMutedLocally ? t('enableSound') : t('disableSound')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleStartCall('voice')}
|
||||
className="flex-1 flex flex-col items-center gap-1.5 py-2.5 rounded-2xl bg-white/5 hover:bg-white/10 transition-colors border border-white/5"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-black/40 flex items-center justify-center">
|
||||
<Phone size={16} className="text-white" />
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-white">{t('call')}</span>
|
||||
</button>
|
||||
<button
|
||||
className="flex-1 flex flex-col items-center gap-1.5 py-2.5 rounded-2xl bg-white/5 border border-white/5 opacity-50 cursor-not-allowed"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-black/40 flex items-center justify-center">
|
||||
<MoreHorizontal size={16} className="text-white" />
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-white">{t('more')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Информация */}
|
||||
@@ -585,6 +671,35 @@ export default function UserProfile({ userId, chatId, onClose, onGoToMessage, is
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Общие группы */}
|
||||
{!isSelf && commonGroups.length > 0 && (
|
||||
<div className="bg-black/20 backdrop-blur-xl border border-white/5 rounded-2xl p-4 transition-all hover:bg-black/30 hover:border-white/10 group mt-2 text-left">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="w-6 h-6 rounded-full bg-purple-500/20 flex items-center justify-center border border-purple-500/30">
|
||||
<Users size={12} className="text-purple-400" />
|
||||
</div>
|
||||
<label className="text-xs font-semibold text-purple-200/50 uppercase tracking-widest">
|
||||
{t('commonGroups')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{commonGroups.map(cg => (
|
||||
<button key={cg.id} onClick={() => { setActiveChat(cg.id); loadMessages(cg.id); onClose(); }} className="flex items-center gap-3 w-full bg-white/5 hover:bg-white/10 p-2 rounded-xl transition-colors border border-white/5">
|
||||
{cg.avatar ? (
|
||||
<img src={getMediaUrl(cg.avatar)} className="w-10 h-10 rounded-full object-cover" />
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex flex-col items-center justify-center text-white font-bold">{(cg.name || 'G').charAt(0)}</div>
|
||||
)}
|
||||
<div className="flex-1 text-left min-w-0">
|
||||
<p className="text-sm font-medium text-white truncate">{cg.name || 'Group'}</p>
|
||||
<p className="text-xs text-zinc-500 truncate">{cg.members.length} {t('participants')}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Медиа / Файлы / Ссылки */}
|
||||
|
||||
@@ -186,6 +186,8 @@ const translations = {
|
||||
noStories: 'Публикаций пока нет',
|
||||
goToMessage: 'Перейти к сообщению',
|
||||
story: 'История',
|
||||
commonGroups: 'Общие группы',
|
||||
more: 'Ещё',
|
||||
// Typing
|
||||
typingText: 'печатает',
|
||||
// Emoji
|
||||
@@ -451,6 +453,8 @@ const translations = {
|
||||
noStories: 'No stories yet',
|
||||
goToMessage: 'Go to message',
|
||||
story: 'Story',
|
||||
commonGroups: 'Common groups',
|
||||
more: 'More',
|
||||
typingText: 'typing',
|
||||
emojiFrequent: 'Frequent',
|
||||
emojiGestures: 'Gestures',
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useChatStore } from '../stores/chatStore';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { getSocket, disconnectSocket } from '../lib/socket';
|
||||
import { api } from '../lib/api';
|
||||
import { playNotificationSound, isChatMuted } from '../lib/sounds';
|
||||
import { playNotificationSound, isChatMuted, playCallRingtone, stopCallRingtone } from '../lib/sounds';
|
||||
import { useLang } from '../lib/i18n';
|
||||
import type { Message, UserBasic, CallInfo } from '../lib/types';
|
||||
import { Send, Check } from 'lucide-react';
|
||||
import { Send, Check, Phone, PhoneOff } from 'lucide-react';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import ChatView from '../components/ChatView';
|
||||
import CallModal from '../components/CallModal';
|
||||
@@ -51,8 +51,18 @@ export default function ChatPage() {
|
||||
const [groupCallType, setGroupCallType] = useState<'voice' | 'video'>('voice');
|
||||
const [groupCallSessionId, setGroupCallSessionId] = useState(0);
|
||||
|
||||
const [incomingGroupCall, setIncomingGroupCall] = useState<{ chatId: string; from: string; callerInfo: any; callType: string; chatName: string } | null>(null);
|
||||
|
||||
const groupCallOpenRef = useRef(false);
|
||||
const groupCallChatIdRef = useRef('');
|
||||
|
||||
const { t } = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
groupCallOpenRef.current = groupCallOpen;
|
||||
groupCallChatIdRef.current = groupCallChatId;
|
||||
}, [groupCallOpen, groupCallChatId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialized.current) return;
|
||||
initialized.current = true;
|
||||
@@ -244,6 +254,45 @@ export default function ChatPage() {
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('group_call_incoming', (data: { chatId: string; from: string; callerInfo: any; callType: string }) => {
|
||||
if (data.from === user?.id) return;
|
||||
if (groupCallOpenRef.current && groupCallChatIdRef.current === data.chatId) return;
|
||||
|
||||
const { chats } = useChatStore.getState();
|
||||
const chat = chats.find(c => c.id === data.chatId);
|
||||
if (!chat) return;
|
||||
|
||||
playCallRingtone();
|
||||
setIncomingGroupCall({
|
||||
chatId: data.chatId,
|
||||
from: data.from,
|
||||
callerInfo: data.callerInfo,
|
||||
callType: data.callType,
|
||||
chatName: chat.name || 'Group',
|
||||
});
|
||||
|
||||
// Auto-dismiss after 15 seconds if ignored
|
||||
setTimeout(() => {
|
||||
setIncomingGroupCall(prev => {
|
||||
if (prev?.chatId === data.chatId) {
|
||||
stopCallRingtone();
|
||||
return null;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
socket.on('group_call_ended', (data: { chatId: string }) => {
|
||||
setIncomingGroupCall(prev => {
|
||||
if (prev?.chatId === data.chatId) {
|
||||
stopCallRingtone();
|
||||
return null;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off('new_message');
|
||||
socket.off('scheduled_delivered');
|
||||
@@ -265,6 +314,8 @@ export default function ChatPage() {
|
||||
socket.off('story_viewed');
|
||||
socket.off('story_reply');
|
||||
socket.off('story_reaction');
|
||||
socket.off('group_call_incoming');
|
||||
socket.off('group_call_ended');
|
||||
};
|
||||
}, [user?.id]);
|
||||
|
||||
@@ -276,6 +327,16 @@ export default function ChatPage() {
|
||||
setCallOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleCustomCallEvent = ((e: CustomEvent) => {
|
||||
if (e.detail?.targetUser && e.detail?.type) {
|
||||
handleStartCall(e.detail.targetUser, e.detail.type);
|
||||
}
|
||||
}) as EventListener;
|
||||
window.addEventListener('START_CALL', handleCustomCallEvent);
|
||||
return () => window.removeEventListener('START_CALL', handleCustomCallEvent);
|
||||
}, []);
|
||||
|
||||
const handleStartGroupCall = (chatId: string, chatName: string, type: 'voice' | 'video') => {
|
||||
setGroupCallChatId(chatId);
|
||||
setGroupCallChatName(chatName);
|
||||
@@ -342,6 +403,67 @@ export default function ChatPage() {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Incoming Group Call Overlay */}
|
||||
<AnimatePresence>
|
||||
{incomingGroupCall && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[200] flex items-center justify-center bg-black/80 backdrop-blur-sm"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, y: 20 }}
|
||||
animate={{ scale: 1, y: 0 }}
|
||||
exit={{ scale: 0.9, y: 20 }}
|
||||
className="bg-zinc-900 border border-white/10 p-8 rounded-3xl w-full max-w-sm flex flex-col items-center shadow-2xl"
|
||||
>
|
||||
<div className="relative mb-6">
|
||||
<div className="absolute inset-0 rounded-full bg-emerald-500/20 animate-call-wave" />
|
||||
<div className="relative w-24 h-24 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center text-4xl font-bold text-white uppercase overflow-hidden">
|
||||
{incomingGroupCall.callerInfo?.avatar ? (
|
||||
<img src={incomingGroupCall.callerInfo.avatar} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<>{incomingGroupCall.chatName.charAt(0)}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-2xl text-white font-semibold mb-2 text-center break-words w-full max-w-full">
|
||||
{incomingGroupCall.chatName}
|
||||
</h2>
|
||||
<p className="text-emerald-400 font-medium mb-1 truncate w-full text-center">
|
||||
{incomingGroupCall.callerInfo?.displayName || incomingGroupCall.callerInfo?.username || 'User'} {t('calling' as any) || 'звонит...'}
|
||||
</p>
|
||||
<p className="text-zinc-400 text-sm mb-8 bg-white/5 px-3 py-1 rounded-full border border-white/5">
|
||||
{t('group' as any) || 'Групповой звонок'}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-8 w-full justify-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
stopCallRingtone();
|
||||
setIncomingGroupCall(null);
|
||||
}}
|
||||
className="w-16 h-16 rounded-full bg-red-500 hover:bg-red-600 flex items-center justify-center text-white transition-colors shadow-lg shadow-red-500/20"
|
||||
>
|
||||
<PhoneOff size={28} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
stopCallRingtone();
|
||||
handleStartGroupCall(incomingGroupCall.chatId, incomingGroupCall.chatName, incomingGroupCall.callType as any);
|
||||
setIncomingGroupCall(null);
|
||||
}}
|
||||
className="w-16 h-16 rounded-full bg-emerald-500 hover:bg-emerald-600 flex items-center justify-center text-white transition-colors animate-pulse shadow-lg shadow-emerald-500/20"
|
||||
>
|
||||
<Phone size={28} className="animate-wiggle" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
return {
|
||||
...chat,
|
||||
messages: [message],
|
||||
unreadCount: (chat.id === state.activeChat || message.senderId === userId || message.storyId) ? chat.unreadCount : chat.unreadCount + 1,
|
||||
unreadCount: (chat.id === state.activeChat || message.senderId === userId) ? chat.unreadCount : chat.unreadCount + 1,
|
||||
};
|
||||
}
|
||||
return chat;
|
||||
@@ -446,12 +446,24 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
addChat: (chat) => {
|
||||
set((state) => {
|
||||
const existing = state.chats.find((c) => c.id === chat.id);
|
||||
|
||||
const messagesFromState = state.messages[chat.id] || [];
|
||||
const messagesToUse = messagesFromState.length > 0 ? messagesFromState : (chat.messages || []);
|
||||
|
||||
let unreadCount = chat.unreadCount || 0;
|
||||
if (!existing && messagesFromState.length > 0) {
|
||||
const userId = useAuthStore.getState().user?.id;
|
||||
unreadCount = messagesFromState.filter((m) => m.senderId !== userId && !m.readBy?.some(r => r.userId === userId)).length;
|
||||
}
|
||||
|
||||
const updatedChat = { ...chat, messages: messagesToUse.length > 0 ? [messagesToUse[messagesToUse.length - 1]] : [], unreadCount };
|
||||
|
||||
if (existing) {
|
||||
return {
|
||||
chats: state.chats.map((c) => (c.id === chat.id ? { ...c, ...chat } : c)),
|
||||
chats: state.chats.map((c) => (c.id === chat.id ? { ...c, ...updatedChat } : c)),
|
||||
};
|
||||
}
|
||||
return { chats: [chat, ...state.chats] };
|
||||
return { chats: [updatedChat, ...state.chats] };
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user