Починка реакций
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>
|
||||
|
||||
Reference in New Issue
Block a user