Починка реакций
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"}
|
||||
Reference in New Issue
Block a user