.net10 сервер с рабочими звонками и файлами
This commit is contained in:
@@ -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 === */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user