Информация о звонках в чате
This commit is contained in:
@@ -108,6 +108,9 @@ export interface Message {
|
||||
media: MediaItem[];
|
||||
reactions: Reaction[];
|
||||
readBy: Array<{ userId: string }>;
|
||||
callType?: 'voice' | 'video' | string | null;
|
||||
callStatus?: 'missed' | 'completed' | 'cancelled' | 'declined' | string | null;
|
||||
duration?: number | null;
|
||||
}
|
||||
|
||||
export interface Chat {
|
||||
|
||||
@@ -109,6 +109,11 @@ const translations = {
|
||||
endCall: 'Завершить',
|
||||
callEnded: 'Звонок завершён',
|
||||
callDeclined: 'Звонок отклонён',
|
||||
audioCall: 'Голосовой звонок',
|
||||
missedCall: 'Пропущенный звонок',
|
||||
declinedCall: 'Отклонённый звонок',
|
||||
cancelledCall: 'Отменённый звонок',
|
||||
completedCall: 'Вызов завершён',
|
||||
// Photo/video
|
||||
photoVideo: 'Фото / видео',
|
||||
fileBtn: 'Файл',
|
||||
@@ -470,6 +475,11 @@ const translations = {
|
||||
endCall: 'End call',
|
||||
callEnded: 'Call ended',
|
||||
callDeclined: 'Call declined',
|
||||
audioCall: 'Audio call',
|
||||
missedCall: 'Missed call',
|
||||
declinedCall: 'Declined call',
|
||||
cancelledCall: 'Cancelled call',
|
||||
completedCall: 'Call completed',
|
||||
photoVideo: 'Photo / video',
|
||||
fileBtn: 'File',
|
||||
sendError: 'Send error',
|
||||
|
||||
@@ -1542,15 +1542,32 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
|
||||
}
|
||||
}, [isOpen, incoming, targetUser, callState, startCall]);
|
||||
|
||||
// Sync local video ref with stream (only when srcObject actually changes)
|
||||
// Sync local video ref with stream
|
||||
useEffect(() => {
|
||||
if (!localVideoRef.current) return;
|
||||
const video = localVideoRef.current as any;
|
||||
const desired = isScreenSharing && screenStreamRef.current
|
||||
? screenStreamRef.current
|
||||
: localStreamRef.current;
|
||||
if (desired && localVideoRef.current.srcObject !== desired) {
|
||||
localVideoRef.current.srcObject = desired;
|
||||
}
|
||||
|
||||
const syncLocal = async () => {
|
||||
if (desired && video.srcObject !== desired) {
|
||||
console.log('[WebRTC] Syncing local video srcObject');
|
||||
video.srcObject = desired;
|
||||
video.muted = true;
|
||||
try {
|
||||
if (video._playPromise) await video._playPromise;
|
||||
video._playPromise = video.play();
|
||||
await video._playPromise;
|
||||
video._playPromise = null;
|
||||
} catch (e) {
|
||||
video._playPromise = null;
|
||||
console.warn('[WebRTC] Local video play failed:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
syncLocal();
|
||||
});
|
||||
|
||||
// Sync remote video/audio ref with remote stream
|
||||
|
||||
@@ -62,6 +62,13 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) {
|
||||
: lastMessage.media?.[0]?.type === 'video'
|
||||
? t('video')
|
||||
: t('file')
|
||||
: lastMessage.type === 'call'
|
||||
? `${lastMessage.callType === 'video' ? '🎬' : '📞'} ${t(
|
||||
lastMessage.callStatus === 'missed' ? 'missedCall' :
|
||||
lastMessage.callStatus === 'declined' ? 'declinedCall' :
|
||||
lastMessage.callStatus === 'cancelled' ? 'cancelledCall' :
|
||||
'completedCall'
|
||||
)}`
|
||||
: lastMessage.content || ''
|
||||
: '';
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ import {
|
||||
Pin,
|
||||
Clock,
|
||||
Forward,
|
||||
Phone,
|
||||
Video,
|
||||
PhoneMissed,
|
||||
PhoneIncoming,
|
||||
PhoneOutgoing,
|
||||
} from 'lucide-react';
|
||||
import { useAuthStore } from '../../../auth/application/authStore';
|
||||
import { useChatStore } from '../../application/chatStore';
|
||||
@@ -286,6 +291,57 @@ function MessageBubble({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (message.type === 'call') {
|
||||
const isMissed = message.callStatus === 'missed' || message.callStatus === 'declined' || message.callStatus === 'cancelled';
|
||||
const statusText = isMissed
|
||||
? (message.callStatus === 'missed' ? t('missedCall') : message.callStatus === 'declined' ? t('declinedCall') : t('cancelledCall'))
|
||||
: t('completedCall');
|
||||
|
||||
const StatusIcon = isMissed ? PhoneMissed : (isMine ? PhoneOutgoing : PhoneIncoming);
|
||||
const CallIcon = message.callType === 'video' ? Video : StatusIcon;
|
||||
|
||||
return (
|
||||
<div className={`flex ${isMine ? 'justify-end' : 'justify-start'} mb-3 px-4 group scroll-mt-20`} data-message-id={message.id}>
|
||||
<div
|
||||
onContextMenu={handleContextMenu}
|
||||
className={`group/call relative flex items-center gap-3.5 px-4 py-3 rounded-[1.25rem] border backdrop-blur-sm transition-all duration-300 cursor-default select-none
|
||||
${isMine
|
||||
? 'bg-primary/10 border-primary/20 hover:bg-primary/20 shadow-[0_4px_12px_rgba(48,150,229,0.08)]'
|
||||
: 'bg-surface-variant/10 border-white/5 hover:bg-surface-variant/15 shadow-[0_4px_12px_rgba(0,0,0,0.15)]'}`}>
|
||||
|
||||
<div className={`w-11 h-11 rounded-2xl flex items-center justify-center shrink-0 shadow-inner group-hover/call:scale-105 transition-transform duration-500
|
||||
${isMissed ? 'bg-red-500/15 text-red-400' : 'bg-emerald-500/15 text-emerald-400'}`}>
|
||||
<CallIcon size={22} strokeWidth={2.5} className={isMissed && message.callStatus === 'missed' ? 'animate-wiggle' : ''} />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 pr-4">
|
||||
<h4 className="text-[15px] font-bold text-white tracking-tight leading-tight mb-0.5 truncate">
|
||||
{message.callType === 'video' ? t('videoCall') : t('audioCall')}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1.5 opacity-80">
|
||||
<span className={`text-[13px] font-medium ${isMissed ? 'text-red-400' : 'text-zinc-400'}`}>
|
||||
{statusText} {message.duration && message.duration > 0 ? `• ${formatDuration(message.duration)}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end justify-between self-stretch pt-0.5">
|
||||
<div className="text-[10px] font-bold tracking-tight text-white/30 tabular-nums uppercase">
|
||||
{timeStr}
|
||||
</div>
|
||||
{isMine && (
|
||||
<div className="opacity-60 flex gap-0.5">
|
||||
{isRead ? <CheckCheck size={12} className="text-primary" /> : <Check size={12} className="text-zinc-500" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-0 rounded-[1.25rem] bg-white/[0.03] opacity-0 group-hover/call:opacity-100 transition-opacity pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const media = message.media || [];
|
||||
|
||||
const isMediaGif = (m: MediaItem) => {
|
||||
|
||||
Reference in New Issue
Block a user