Починка реакций

This commit is contained in:
Халимов Рустам
2026-03-13 12:46:01 +03:00
parent 9a32442b5c
commit ca1d88191c
438 changed files with 374 additions and 20961 deletions

View File

@@ -152,8 +152,8 @@ function MessageBubble({
const chatForDelete = chats.find(c => c.id === message.chatId);
const otherMemberName = chatForDelete?.type === 'personal'
? chatForDelete.members.find(m => m.user.id !== user?.id)?.user.displayName
|| chatForDelete.members.find(m => m.user.id !== user?.id)?.user.username
|| ''
|| chatForDelete.members.find(m => m.user.id !== user?.id)?.user.username
|| ''
: '';
const isPinned = pinnedMessages[message.chatId]?.id === message.id;
@@ -176,11 +176,22 @@ function MessageBubble({
const existingReaction = message.reactions?.find(
(r) => r.userId === user?.id && r.emoji === emoji
);
console.log('[Reaction] handleReaction:', {
emoji,
messageId: message.id,
chatId: message.chatId,
existingReaction: !!existingReaction,
userId: user?.id
});
if (existingReaction) {
console.log('[Reaction] Emitting remove_reaction');
socket.emit('remove_reaction', { messageId: message.id, chatId: message.chatId, emoji });
} else {
console.log('[Reaction] Emitting add_reaction');
socket.emit('add_reaction', { messageId: message.id, chatId: message.chatId, emoji });
}
} else {
console.warn('[Reaction] Socket not available');
}
setShowContext(false);
};
@@ -393,7 +404,7 @@ function MessageBubble({
{/* Reply */}
{message.replyTo && (
<div
<div
className="mx-3 mb-1 px-3 py-1.5 rounded-lg border-l-2 border-vortex-500 bg-vortex-500/10 max-w-full cursor-pointer hover:bg-vortex-500/20 transition-colors"
onClick={() => {
const el = document.getElementById(`msg-${message.replyToId}`);
@@ -430,17 +441,16 @@ function MessageBubble({
onContextMenu={handleContextMenu}
onDoubleClick={handleReply}
title={t('reply') ? `${t('reply')} (Double Click)` : 'Double click to reply'}
className={`cursor-pointer rounded-[1.25rem] overflow-hidden transition-all duration-300 ${
hasImage && !message.content
className={`cursor-pointer rounded-[1.25rem] overflow-hidden transition-all duration-300 ${hasImage && !message.content
? 'p-0 shadow-none border-none'
: isMine
? 'bubble-sent text-white shadow-sm px-4 py-2.5 hover:shadow-md hover:brightness-105'
: 'bubble-received text-zinc-100 shadow-sm px-4 py-2.5 hover:shadow-md hover:brightness-105'
}`}
}`}
>
{/* Рендер пересланного сообщения */}
{message.forwardedFrom && (
<div
<div
className="mb-2 text-[13px] opacity-90 border-l-[2px] border-white/40 pl-3 py-0.5 cursor-pointer hover:bg-white/5 transition-colors -mx-1 px-1 rounded-sm"
onClick={() => onViewProfile?.(message.forwardedFromId!)}
>
@@ -456,13 +466,12 @@ function MessageBubble({
{/* Изображения и Видео (Галерея) */}
{(hasImage || hasVideo) && (
<div className={`${message.content ? 'mb-2 -mx-4 -mt-2.5' : ''} bg-black/20 overflow-hidden`}>
<div className={`grid gap-[2px] ${
media.filter(m => m.type === 'image' || m.type === 'video').length >= 3
<div className={`grid gap-[2px] ${media.filter(m => m.type === 'image' || m.type === 'video').length >= 3
? 'grid-cols-3'
: media.filter(m => m.type === 'image' || m.type === 'video').length === 2
? 'grid-cols-2'
: 'grid-cols-1'
}`}>
}`}>
{media
.filter((m) => m.type === 'image' || m.type === 'video')
.map((m, idx) => (
@@ -471,17 +480,15 @@ function MessageBubble({
key={m.id}
src={m.url}
alt=""
className={`w-full h-full object-cover cursor-pointer hover:brightness-90 transition-all ${
media.filter(i => i.type === 'image' || i.type === 'video').length > 1 ? 'aspect-square' : 'max-h-[500px]'
}`}
className={`w-full h-full object-cover cursor-pointer hover:brightness-90 transition-all ${media.filter(i => i.type === 'image' || i.type === 'video').length > 1 ? 'aspect-square' : 'max-h-[500px]'
}`}
onClick={() => setLightboxData({ index: idx })}
/>
) : (
<div
key={m.id}
className={`relative cursor-pointer group/video ${
media.filter(i => i.type === 'image' || i.type === 'video').length > 1 ? 'aspect-square' : ''
}`}
<div
key={m.id}
className={`relative cursor-pointer group/video ${media.filter(i => i.type === 'image' || i.type === 'video').length > 1 ? 'aspect-square' : ''
}`}
onClick={() => setLightboxData({ index: idx })}
>
<video
@@ -489,7 +496,7 @@ function MessageBubble({
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/20 group-hover/video:bg-black/40 transition-colors">
<Play size={media.filter(i => i.type === 'image' || i.type === 'video').length > 1 ? 24 : 48} className="text-white opacity-80" />
<Play size={media.filter(i => i.type === 'image' || i.type === 'video').length > 1 ? 24 : 48} className="text-white opacity-80" />
</div>
</div>
)
@@ -766,84 +773,84 @@ function MessageBubble({
</>
) : (
<>
<div className="flex items-center gap-1 px-3 py-2 border-b border-border">
{['👍', '❤️', '😂', '😮', '😢', '🔥'].map((emoji) => (
<div className="flex items-center gap-1 px-3 py-2 border-b border-border">
{['👍', '❤️', '😂', '😮', '😢', '🔥'].map((emoji) => (
<button
key={emoji}
onClick={() => handleReaction(emoji)}
className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-surface-hover transition-colors text-lg"
>
{emoji}
</button>
))}
</div>
<button
key={emoji}
onClick={() => handleReaction(emoji)}
className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-surface-hover transition-colors text-lg"
onClick={handleReply}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
{emoji}
<Reply size={16} />
{quotedText ? t('replyWithQuote') : t('reply')}
</button>
))}
</div>
<button
onClick={handleReply}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<Reply size={16} />
{quotedText ? t('replyWithQuote') : t('reply')}
</button>
<button
onClick={() => {
setShowContext(false);
onStartSelectionMode?.(message.id);
}}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<CheckCheck size={16} />
{t('select')}
</button>
<button
onClick={() => {
setShowContext(false);
onStartSelectionMode?.(message.id);
}}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<CheckCheck size={16} />
{t('select')}
</button>
<button
onClick={() => {
setShowContext(false);
onForward?.(message.id);
}}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<Forward size={16} />
{t('forward')}
</button>
<button
onClick={() => {
setShowContext(false);
onForward?.(message.id);
}}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<Forward size={16} />
{t('forward')}
</button>
<button
onClick={handlePin}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<Pin size={16} />
{isPinned ? t('unpinMessage') : t('pinMessage')}
</button>
<button
onClick={handlePin}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<Pin size={16} />
{isPinned ? t('unpinMessage') : t('pinMessage')}
</button>
{message.content && (
<button
onClick={handleCopy}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<Copy size={16} />
{t('copy')}
</button>
)}
{message.content && (
<button
onClick={handleCopy}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<Copy size={16} />
{t('copy')}
</button>
)}
{isMine && message.content && (
<button
onClick={handleEdit}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<Pencil size={16} />
{t('edit')}
</button>
)}
{isMine && message.content && (
<button
onClick={handleEdit}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-zinc-300 hover:bg-surface-hover hover:text-white transition-colors"
>
<Pencil size={16} />
{t('edit')}
</button>
)}
<div className="border-t border-border my-1" />
<button
onClick={() => setDeleteMenuMode(true)}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-red-400 hover:bg-red-500/10 transition-colors"
>
<Trash2 size={16} />
{t('delete')}
</button>
<div className="border-t border-border my-1" />
<button
onClick={() => setDeleteMenuMode(true)}
className="flex items-center gap-3 w-full px-4 py-2.5 text-sm text-red-400 hover:bg-red-500/10 transition-colors"
>
<Trash2 size={16} />
{t('delete')}
</button>
</>
)}
</motion.div>
@@ -854,10 +861,10 @@ function MessageBubble({
<AnimatePresence>
{lightboxData && (
<ImageLightbox
images={media.filter(m => m.type === 'image' || m.type === 'video').map(m => ({ url: m.url, type: m.type }))}
<ImageLightbox
images={media.filter(m => m.type === 'image' || m.type === 'video').map(m => ({ url: m.url, type: m.type }))}
initialIndex={lightboxData.index}
onClose={() => setLightboxData(null)}
onClose={() => setLightboxData(null)}
/>
)}
</AnimatePresence>

View File

@@ -27,6 +27,7 @@ export function connectSocket(token: string): SocketCompat {
// Обертка для совместимости с Socket.io API
socketWrapper = {
on: (event: string, callback: (...args: any[]) => void) => {
console.log(`[SignalR] Registering handler for: ${event}`);
connection?.on(event, callback);
},
off: (event: string, callback?: (...args: any[]) => void) => {
@@ -37,12 +38,16 @@ export function connectSocket(token: string): SocketCompat {
}
},
emit: (event: string, ...args: any[]) => {
const state = connection?.state;
console.log(`[SignalR] emit('${event}') called, connection state: ${state}`);
if (connection?.state === 'Connected') {
// В SignalR invoke возвращает Promise, но Socket.io emit - нет.
// Мы просто запускаем и логируем ошибки.
connection.invoke(event, ...args).catch(err => console.error(`SignalR emit error (${event}):`, err));
connection.invoke(event, ...args)
.then(() => console.log(`[SignalR] invoke('${event}') completed successfully`))
.catch(err => console.error(`SignalR emit error (${event}):`, err));
} else {
console.warn(`SignalR emit skipped (${event}): connection state is ${connection?.state}`);
console.warn(`SignalR emit skipped (${event}): connection state is ${state}`);
}
},
disconnect: () => {
@@ -54,8 +59,23 @@ export function connectSocket(token: string): SocketCompat {
};
connection.start()
.then(() => console.log('SignalR подключён'))
.catch(err => console.error('Ошибка подключения SignalR:', err.toString()));
.then(() => {
console.log('[SignalR] Connected successfully');
console.log('[SignalR] Connection ID:', connection?.connectionId);
})
.catch(err => console.error('[SignalR] Connection error:', err.toString()));
connection.onclose((error) => {
console.log('[SignalR] Connection closed', error);
});
connection.onreconnecting((error) => {
console.log('[SignalR] Reconnecting...', error);
});
connection.onreconnected((connectionId) => {
console.log('[SignalR] Reconnected, new connection ID:', connectionId);
});
return socketWrapper;
}

View File

@@ -153,10 +153,12 @@ export default function ChatPage() {
});
socket.on('reaction_added', (data: { messageId: string; chatId: string; userId: string; username: string; emoji: string }) => {
console.log('[Socket] reaction_added received:', data);
addReaction(data.messageId, data.chatId, data.userId, data.username, data.emoji);
});
socket.on('reaction_removed', (data: { messageId: string; chatId: string; userId: string; emoji: string }) => {
console.log('[Socket] reaction_removed received:', data);
removeReaction(data.messageId, data.chatId, data.userId, data.emoji);
});

View File

@@ -93,7 +93,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
try {
const favChat = await api.getOrCreateFavorites();
chats.unshift(favChat);
} catch {}
} catch { }
}
// Extract pinned messages from chats
const pinnedMessages: Record<string, Message> = {};
@@ -175,7 +175,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
set((state) => {
const chatMessages = state.messages[message.chatId] || [];
const updatedMessages = chatMessages.map((m) => (m.id === message.id ? message : m));
const updatedChats = state.chats.map((chat) => {
if (chat.id === message.chatId) {
return {
@@ -295,16 +295,22 @@ export const useChatStore = create<ChatState>((set, get) => ({
},
addReaction: (messageId, chatId, userId, username, emoji) => {
console.log('[ChatStore] addReaction called:', { messageId, chatId, userId, username, emoji });
set((state) => {
const chatMessages = state.messages[chatId] || [];
const updateMsg = (m: Message) => {
if (m.id === messageId) {
const exists = m.reactions.some((r) => r.userId === userId && r.emoji === emoji);
if (exists) return m;
const reactions = m.reactions || [];
const exists = reactions.some((r) => r.userId === userId && r.emoji === emoji);
if (exists) {
console.log('[ChatStore] Reaction already exists, skipping');
return m;
}
console.log('[ChatStore] Adding reaction to message:', m.id);
return {
...m,
reactions: [
...m.reactions,
...reactions,
{ id: `${messageId}-${userId}-${emoji}`, emoji, userId, user: { id: userId, username, displayName: username } },
],
};
@@ -323,6 +329,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
return chat;
});
console.log('[ChatStore] State updated for chatId:', chatId);
return {
messages: {
...state.messages,
@@ -334,13 +341,15 @@ export const useChatStore = create<ChatState>((set, get) => ({
},
removeReaction: (messageId, chatId, userId, emoji) => {
console.log('[ChatStore] removeReaction called:', { messageId, chatId, userId, emoji });
set((state) => {
const chatMessages = state.messages[chatId] || [];
const updateMsg = (m: Message) => {
if (m.id === messageId) {
console.log('[ChatStore] Removing reaction from message:', m.id);
return {
...m,
reactions: m.reactions.filter((r) => !(r.userId === userId && r.emoji === emoji)),
reactions: (m.reactions || []).filter((r) => !(r.userId === userId && r.emoji === emoji)),
};
}
return m;
@@ -357,6 +366,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
return chat;
});
console.log('[ChatStore] State updated for chatId:', chatId);
return {
messages: {
...state.messages,

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/callmodal.tsx","./src/components/chatlistitem.tsx","./src/components/chatview.tsx","./src/components/confirmmodal.tsx","./src/components/datepicker.tsx","./src/components/emojipicker.tsx","./src/components/forwardmodal.tsx","./src/components/groupsettings.tsx","./src/components/imagelightbox.tsx","./src/components/messagebubble.tsx","./src/components/messageinput.tsx","./src/components/newchatmodal.tsx","./src/components/sidemenu.tsx","./src/components/sidebar.tsx","./src/components/storyviewer.tsx","./src/components/typingindicator.tsx","./src/components/userprofile.tsx","./src/lib/api.ts","./src/lib/i18n.ts","./src/lib/socket.ts","./src/lib/sounds.ts","./src/lib/utils.ts","./src/pages/authpage.tsx","./src/pages/chatpage.tsx","./src/stores/authstore.ts","./src/stores/chatstore.ts","./src/stores/themestore.ts"],"version":"5.9.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/avatar.tsx","./src/components/callmodal.tsx","./src/components/chatlistitem.tsx","./src/components/chatview.tsx","./src/components/confirmmodal.tsx","./src/components/datepicker.tsx","./src/components/emojipicker.tsx","./src/components/forwardmodal.tsx","./src/components/groupcallmodal.tsx","./src/components/groupsettings.tsx","./src/components/imagelightbox.tsx","./src/components/messagebubble.tsx","./src/components/messageinput.tsx","./src/components/newchatmodal.tsx","./src/components/notificationprovider.tsx","./src/components/sidemenu.tsx","./src/components/sidebar.tsx","./src/components/storyviewer.tsx","./src/components/typingindicator.tsx","./src/components/userprofile.tsx","./src/lib/api.ts","./src/lib/hooks.ts","./src/lib/i18n.ts","./src/lib/socket.ts","./src/lib/sounds.ts","./src/lib/types.ts","./src/lib/utils.ts","./src/pages/authpage.tsx","./src/pages/chatpage.tsx","./src/stores/authstore.ts","./src/stores/chatstore.ts","./src/stores/notificationstore.ts","./src/stores/themestore.ts"],"version":"5.9.3"}