.net10 сервер с рабочими звонками и файлами

This commit is contained in:
Халимов Рустам
2026-03-12 01:48:58 +03:00
parent 50d4d75c04
commit d025c98e61
741 changed files with 35231 additions and 1863 deletions

View File

@@ -302,12 +302,19 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
), '→ hasRemoteVideo:', hasVideo);
if (hasVideo !== hasRemoteVideoRef.current) {
console.log(`[checkVideo] hasRemoteVideo changed: ${hasRemoteVideoRef.current} -> ${hasVideo}`);
hasRemoteVideoRef.current = hasVideo;
setHasRemoteVideo(hasVideo);
}
if (hasVideo && callType !== 'video') {
// Auto-switch UI mode if remote starts sending video or STOPS sending it
if (hasVideo && callType === 'voice') {
console.log('[checkVideo] Remote started video, switching UI to video mode');
setCallType('video');
} else if (!hasVideo && callType === 'video') {
// If we were in video mode but tracks are gone, revert to voice
console.log('[checkVideo] Remote stopped video, reverting UI to voice mode');
setCallType('voice');
}
};
@@ -1401,13 +1408,17 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
setCallType(data.callType);
// If remote switched to video, nudge the video element
if (data.callType === 'video') {
if (data.callType === 'video' || data.callType === 'voice') {
setTimeout(() => {
if (remoteVideoRef.current && remoteStreamRef.current) {
console.log(`[onCallTypeChanged] Nudging playback for ${data.callType}`);
remoteVideoRef.current.srcObject = remoteStreamRef.current;
remoteVideoRef.current.play().catch(() => {});
remoteVideoRef.current.play().catch(e => {
if (e.name === 'NotAllowedError') setNeedsInteraction(true);
console.warn('Auto-play failed:', e);
});
}
}, 500);
}, 1000);
}
};
@@ -1456,15 +1467,46 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
}
});
// Sync remote video ref with remote stream (only when srcObject actually changes)
// Sync remote video/audio ref with remote stream
useEffect(() => {
if (!remoteVideoRef.current || !remoteStreamRef.current) return;
if (remoteVideoRef.current.srcObject !== remoteStreamRef.current) {
console.log('[useEffect] Syncing remote video srcObject');
remoteVideoRef.current.srcObject = remoteStreamRef.current;
remoteVideoRef.current.play().catch(() => { });
}
}, [hasRemoteVideo, callType]);
if (!remoteStreamRef.current) return;
const stream = remoteStreamRef.current;
const bindAndPlay = async () => {
// 1) Bind to video element (primary for video, also handles audio)
if (remoteVideoRef.current) {
const video = remoteVideoRef.current as any;
if (video.srcObject !== stream) {
console.log('[useEffect] Binding stream to remoteVideoRef');
video.srcObject = stream;
}
video.muted = false;
video.volume = remoteVolume;
if (stream.getTracks().length > 0) {
video.play().catch((e: any) => {
if (e.name === 'NotAllowedError') setNeedsInteraction(true);
});
}
}
// 2) Bind to audio element (dedicated fallback for sound, especially in voice calls)
if (remoteAudioRef.current) {
const audio = remoteAudioRef.current;
if (audio.srcObject !== stream) {
console.log('[useEffect] Binding stream to remoteAudioRef');
audio.srcObject = stream;
}
audio.muted = false;
audio.volume = remoteVolume;
if (stream.getAudioTracks().length > 0) {
audio.play().catch(() => {});
}
}
};
bindAndPlay();
}, [hasRemoteVideo, callType, callState, remoteVolume]);
// Cleanup on unmount
useEffect(() => {
@@ -1804,6 +1846,9 @@ export default function CallModal({ isOpen, onClose, targetUser, callType: initi
)}
</div>
)}
{/* Dedicated hidden audio element to ensure sound in ALL cases once connected */}
<audio ref={remoteAudioRef} autoPlay style={{ display: 'none' }} />
</div>
{/* === Controls === */}

View File

@@ -266,14 +266,25 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
}, [chatMessages.length, isLoadingMessages]);
// Scroll detection
const handleScroll = () => {
const checkScrollPosition = useCallback(() => {
const container = messagesContainerRef.current;
if (!container) return;
const isNearBottom =
container.scrollHeight - container.scrollTop - container.clientHeight < 200;
setShowScrollDown(!isNearBottom);
}, []);
const handleScroll = () => {
checkScrollPosition();
};
useEffect(() => {
// Check scroll position when messages change or scroll ready state changes
if (scrollReady) {
checkScrollPosition();
}
}, [chatMessages.length, scrollReady, checkScrollPosition]);
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!chatViewRef.current) return;
const { left, top } = chatViewRef.current.getBoundingClientRect();
@@ -890,7 +901,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
)}
{/* Ввод сообщения */}
<MessageInput chatId={activeChat} />
{activeChat && <MessageInput chatId={activeChat} />}
{/* Профиль пользователя */}
<AnimatePresence>

View File

@@ -37,8 +37,9 @@ class ApiClient {
clearTimeout(timer);
if (!response.ok) {
const error = await response.json().catch(() => ({ error: '\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430' }));
throw new Error(error.error || '\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0430');
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData.error || errorData.message || 'Ошибка запроса';
throw new Error(errorMessage);
}
return response.json();
@@ -106,7 +107,7 @@ class ApiClient {
async searchMessages(query: string, chatId?: string) {
const params = new URLSearchParams({ q: query });
if (chatId) params.append('chatId', chatId);
return this.request<Message[]>(`/users/messages/search?${params}`);
return this.request<Message[]>(`/messages/search?${params}`);
}
// \u0427\u0430\u0442\u044b
@@ -215,10 +216,6 @@ class ApiClient {
return this.request<Message[]>(`/messages/chat/${chatId}/shared?type=${type}`);
}
// ICE серверы для WebRTC
async getIceServers() {
return this.request<{ iceServers: RTCIceServer[] }>('/ice-servers');
}
// Stories
async getStories() {
@@ -292,6 +289,10 @@ class ApiClient {
async removeFriend(friendshipId: string) {
return this.request<{ success: boolean }>(`/friends/${friendshipId}`, { method: 'DELETE' });
}
async getIceServers() {
return this.request<{ iceServers: RTCIceServer[] }>('/webrtc/ice-servers');
}
}
export const api = new ApiClient();

View File

@@ -1,42 +1,73 @@
import { io, Socket } from 'socket.io-client';
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
let socket: Socket | null = null;
export function connectSocket(token: string): Socket {
if (socket?.connected) {
return socket;
}
// Clean up old socket instance if it exists but is disconnected
if (socket) {
socket.removeAllListeners();
socket.disconnect();
socket = null;
}
socket = io(window.location.origin, {
auth: { token },
transports: ['websocket', 'polling'],
});
socket.on('connect', () => {
console.log('Socket подключён');
});
socket.on('connect_error', (err) => {
console.error('Ошибка подключения Socket:', err.message);
});
return socket;
export interface SocketCompat {
on(event: string, callback: (...args: any[]) => void): void;
off(event: string, callback?: (...args: any[]) => void): void;
emit(event: string, ...args: any[]): void;
disconnect(): void;
status: string;
}
export function getSocket(): Socket | null {
return socket;
let connection: HubConnection | null = null;
let socketWrapper: SocketCompat | null = null;
export function connectSocket(token: string): SocketCompat {
if (connection && (connection.state === 'Connected' || connection.state === 'Connecting')) {
return socketWrapper!;
}
connection = new HubConnectionBuilder()
.withUrl('/hubs/chat', {
accessTokenFactory: () => token
})
.withAutomaticReconnect()
.configureLogging(LogLevel.Information)
.build();
// Обертка для совместимости с Socket.io API
socketWrapper = {
on: (event: string, callback: (...args: any[]) => void) => {
connection?.on(event, callback);
},
off: (event: string, callback?: (...args: any[]) => void) => {
if (callback) {
connection?.off(event, callback);
} else {
connection?.off(event);
}
},
emit: (event: string, ...args: any[]) => {
if (connection?.state === 'Connected') {
// В SignalR invoke возвращает Promise, но Socket.io emit - нет.
// Мы просто запускаем и логируем ошибки.
connection.invoke(event, ...args).catch(err => console.error(`SignalR emit error (${event}):`, err));
} else {
console.warn(`SignalR emit skipped (${event}): connection state is ${connection?.state}`);
}
},
disconnect: () => {
connection?.stop();
},
get status() {
return connection?.state || 'Disconnected';
}
};
connection.start()
.then(() => console.log('SignalR подключён'))
.catch(err => console.error('Ошибка подключения SignalR:', err.toString()));
return socketWrapper;
}
export function getSocket(): SocketCompat | null {
return socketWrapper;
}
export function disconnectSocket() {
if (socket) {
socket.disconnect();
socket = null;
if (connection) {
connection.stop();
connection = null;
socketWrapper = null;
}
}