174 lines
5.1 KiB
TypeScript
174 lines
5.1 KiB
TypeScript
'use client';
|
||
|
||
import { CheckCircle2, Copy, Forward, Pencil, Reply, Trash2 } from 'lucide-react';
|
||
import { ChatMessageReactions } from '@/components/chat/chat-message-reactions';
|
||
import {
|
||
ContextMenu,
|
||
ContextMenuContent,
|
||
ContextMenuItem,
|
||
ContextMenuSeparator,
|
||
ContextMenuTrigger
|
||
} from '@/components/ui/context-menu';
|
||
import type { ChatMessage } from '@/lib/api';
|
||
import { QUICK_REACTIONS, canDeleteMessage, canEditMessage, getMessageCopyText } from '@/lib/chat-message-utils';
|
||
import { cn } from '@/lib/utils';
|
||
|
||
interface ChatMessageContextMenuProps {
|
||
message: ChatMessage;
|
||
userId?: string;
|
||
isE2E?: boolean;
|
||
visibleText?: string;
|
||
children: React.ReactNode;
|
||
onEdit?: () => void;
|
||
onDelete?: () => void;
|
||
onReply?: () => void;
|
||
onForward?: () => void;
|
||
onSelect?: () => void;
|
||
onToggleReaction?: (emoji: string) => void;
|
||
onCopy?: () => void;
|
||
}
|
||
|
||
export function ChatMessageContextMenu({
|
||
message,
|
||
userId,
|
||
isE2E,
|
||
visibleText,
|
||
children,
|
||
onEdit,
|
||
onDelete,
|
||
onReply,
|
||
onForward,
|
||
onSelect,
|
||
onToggleReaction,
|
||
onCopy
|
||
}: ChatMessageContextMenuProps) {
|
||
const editable = canEditMessage(message, userId, isE2E);
|
||
const deletable = canDeleteMessage(message, userId);
|
||
const copyText = getMessageCopyText(message, visibleText);
|
||
const disabled = message.isDeleted;
|
||
|
||
return (
|
||
<ContextMenu>
|
||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||
<ContextMenuContent className="min-w-[240px]">
|
||
{!disabled ? (
|
||
<>
|
||
<div className="mb-1 flex items-center gap-1 overflow-x-auto px-1 py-1">
|
||
{QUICK_REACTIONS.map((emoji) => (
|
||
<button
|
||
key={emoji}
|
||
type="button"
|
||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-lg transition hover:bg-[#eef1f6]"
|
||
onClick={() => onToggleReaction?.(emoji)}
|
||
>
|
||
{emoji}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<ContextMenuSeparator />
|
||
</>
|
||
) : null}
|
||
{onReply && !disabled ? (
|
||
<ContextMenuItem onClick={onReply}>
|
||
<Reply className="h-4 w-4" />
|
||
Ответить
|
||
</ContextMenuItem>
|
||
) : null}
|
||
{editable ? (
|
||
<ContextMenuItem onClick={onEdit}>
|
||
<Pencil className="h-4 w-4" />
|
||
Изменить
|
||
</ContextMenuItem>
|
||
) : null}
|
||
{copyText ? (
|
||
<ContextMenuItem
|
||
onClick={() => {
|
||
void navigator.clipboard.writeText(copyText);
|
||
onCopy?.();
|
||
}}
|
||
>
|
||
<Copy className="h-4 w-4" />
|
||
Копировать текст
|
||
</ContextMenuItem>
|
||
) : null}
|
||
{onForward && !disabled && !message.isEncrypted ? (
|
||
<ContextMenuItem onClick={onForward}>
|
||
<Forward className="h-4 w-4" />
|
||
Переслать
|
||
</ContextMenuItem>
|
||
) : null}
|
||
{deletable ? (
|
||
<ContextMenuItem className="text-red-600 data-[highlighted]:bg-red-50 data-[highlighted]:text-red-700" onClick={onDelete}>
|
||
<Trash2 className="h-4 w-4" />
|
||
Удалить
|
||
</ContextMenuItem>
|
||
) : null}
|
||
{onSelect && !disabled ? (
|
||
<>
|
||
<ContextMenuSeparator />
|
||
<ContextMenuItem onClick={onSelect}>
|
||
<CheckCircle2 className="h-4 w-4" />
|
||
Выделить
|
||
</ContextMenuItem>
|
||
</>
|
||
) : null}
|
||
{!disabled && message.reactions?.length ? (
|
||
<div className="px-2 pb-1 pt-2">
|
||
<ChatMessageReactions reactions={message.reactions} onToggle={onToggleReaction} compact />
|
||
</div>
|
||
) : null}
|
||
</ContextMenuContent>
|
||
</ContextMenu>
|
||
);
|
||
}
|
||
|
||
interface SelectableMessageWrapperProps {
|
||
selected?: boolean;
|
||
selectionMode?: boolean;
|
||
align?: 'left' | 'right';
|
||
onClick?: () => void;
|
||
className?: string;
|
||
children: React.ReactNode;
|
||
}
|
||
|
||
export function SelectableMessageWrapper({
|
||
selected,
|
||
selectionMode,
|
||
align = 'left',
|
||
onClick,
|
||
className,
|
||
children
|
||
}: SelectableMessageWrapperProps) {
|
||
return (
|
||
<div
|
||
className={cn(
|
||
'relative w-fit max-w-[78%] shrink-0',
|
||
selectionMode && 'cursor-pointer',
|
||
selectionMode && (align === 'right' ? 'mr-7' : 'ml-7'),
|
||
className
|
||
)}
|
||
onClick={selectionMode ? onClick : undefined}
|
||
>
|
||
{selectionMode ? (
|
||
<span
|
||
className={cn(
|
||
'absolute top-1/2 z-10 flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded-full border text-[10px] shadow-sm transition',
|
||
align === 'right' ? 'right-0 translate-x-[calc(100%+8px)]' : 'left-0 -translate-x-[calc(100%+8px)]',
|
||
selected ? 'border-[#3390ec] bg-[#3390ec] text-white' : 'border-[#dce3ec] bg-white text-transparent'
|
||
)}
|
||
>
|
||
✓
|
||
</span>
|
||
) : null}
|
||
<div
|
||
className={cn(
|
||
'rounded-[18px] transition',
|
||
selected && 'ring-2 ring-[#3390ec] ring-offset-2 ring-offset-[#eef1f6]'
|
||
)}
|
||
>
|
||
{children}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|