global update and global fix
This commit is contained in:
173
apps/frontend/components/chat/chat-message-context-menu.tsx
Normal file
173
apps/frontend/components/chat/chat-message-context-menu.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user