diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs
index 1eeb68d..0ee7361 100644
--- a/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs
+++ b/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/ChatCreatedDomainEventHandler.cs
@@ -60,14 +60,14 @@ public sealed class ChatCreatedDomainEventHandler : INotificationHandler(),
- UnreadCount = 0
+ id = chat.Id,
+ type = chat.Type.ToString().ToLowerInvariant(),
+ name = chat.Type == ChatType.Favorites ? "Избранное" : (chat.Type == ChatType.Personal ? null : chat.Name),
+ avatar = chat.Avatar,
+ createdAt = chat.CreatedAt,
+ members = members,
+ messages = new List
+
+ {/* Общие группы */}
+ {!isSelf && commonGroups.length > 0 && (
+
+
+
+
+
+
+
+
+ {commonGroups.map(cg => (
+
+ ))}
+
+
+ )}
{/* Медиа / Файлы / Ссылки */}
diff --git a/apps/web/src/lib/i18n.ts b/apps/web/src/lib/i18n.ts
index 7ee991e..5bbe386 100644
--- a/apps/web/src/lib/i18n.ts
+++ b/apps/web/src/lib/i18n.ts
@@ -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',
diff --git a/apps/web/src/pages/ChatPage.tsx b/apps/web/src/pages/ChatPage.tsx
index 3b13b08..118099b 100644
--- a/apps/web/src/pages/ChatPage.tsx
+++ b/apps/web/src/pages/ChatPage.tsx
@@ -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() {
)}
+
+ {/* Incoming Group Call Overlay */}
+
+ {incomingGroupCall && (
+
+
+
+
+
+ {incomingGroupCall.callerInfo?.avatar ? (
+

+ ) : (
+ <>{incomingGroupCall.chatName.charAt(0)}>
+ )}
+
+
+
+ {incomingGroupCall.chatName}
+
+
+ {incomingGroupCall.callerInfo?.displayName || incomingGroupCall.callerInfo?.username || 'User'} {t('calling' as any) || 'звонит...'}
+
+
+ {t('group' as any) || 'Групповой звонок'}
+
+
+
+
+
+
+
+
+ )}
+
);
}
diff --git a/apps/web/src/stores/chatStore.ts b/apps/web/src/stores/chatStore.ts
index ae44a4e..f674ffd 100644
--- a/apps/web/src/stores/chatStore.ts
+++ b/apps/web/src/stores/chatStore.ts
@@ -152,7 +152,7 @@ export const useChatStore = create((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((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] };
});
},