Vitron-Front/app/components/thread/MessageBubble.tsx
2026-04-29 01:44:16 +03:30

215 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Reply, MoreVertical, Trash2, Check } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { formatTime, formatDate2 } from "../../utils/helpers";
import { MessageList, ThreadParticipant } from "../../../src/api/types/models";
import { Dialog, DialogContent, DialogOverlay } from "../ui/dialog";
import { Button } from "../ui/button";
import { useState, useRef, useEffect } from "react";
// Message bubble component
export const MessageBubble = ({
message,
isCurrentUser,
onReply,
onDelete,
replyToMessageContent,
onReplyClick,
isHighlighted = false,
otherParticipant,
}: {
message: MessageList;
isCurrentUser: boolean;
onReply: (message: MessageList) => void;
onDelete?: (messageId: string) => void;
replyToMessageContent?: string | undefined;
onReplyClick?: (replyToId: string) => void;
isHighlighted?: boolean;
otherParticipant: ThreadParticipant | undefined;
}) => {
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showDropdownMenu, setShowDropdownMenu] = useState(false);
const messageRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isHighlighted && messageRef.current) {
messageRef.current.classList.add("highlight-message");
// Remove highlight after animation completes
const timeout = setTimeout(() => {
if (messageRef.current) {
messageRef.current.classList.remove("highlight-message");
}
}, 1500);
return () => clearTimeout(timeout);
}
}, [isHighlighted]);
return (
<>
<div
className={`flex items-end gap-1 ${!isCurrentUser ? "flex-row-reverse" : "justify-start flex-row"} mb-2`}
>
<div
ref={messageRef}
className={`rounded-xl px-4 py-3 max-w-[70%] ${
isCurrentUser ? "bg-BLUE2 text-WHITE" : "bg-GRAY3"
}`}
>
{message.replyTo && (
<div
className={`text-xs italic mb-1 pb-1 border-b ${
isCurrentUser ? "border-WHITE/30" : "border-GRAY2/30"
} cursor-pointer hover:opacity-80`}
onClick={() => onReplyClick?.(message.replyTo || "")}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onReplyClick?.(message.replyTo || "");
}
}}
role="button"
tabIndex={0}
aria-label="پیام اصلی را نمایش بده"
>
{/* Display replied message content if available */}
{replyToMessageContent || "پاسخ به پیام"}
</div>
)}
<p>{message.content}</p>
</div>
<div className="flex flex-col justify-between items-start">
<div className="flex flex-col gap-1 items-start">
<DropdownMenu
open={showDropdownMenu}
onOpenChange={setShowDropdownMenu}
modal={false}
>
<DropdownMenuTrigger asChild>
<button
onClick={() => setShowDropdownMenu(true)}
className="w-5 h-5 hover:bg-GRAY2 rounded-full flex items-center justify-center cursor-pointer"
aria-label="more options icon"
>
<MoreVertical size={14} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32 z-1">
<DropdownMenuItem
onClick={() => onReply(message)}
className="cursor-pointer justify-end"
>
<Reply size={14} />
<span className="text-R12">پاسخ</span>
</DropdownMenuItem>
{isCurrentUser && (
<DropdownMenuItem
onClick={() => {
setShowDropdownMenu(false);
setShowDeleteDialog(true);
}}
className="cursor-pointer text-red-500 justify-end"
>
<Trash2 size={14} />
<span className="text-R12">حذف پیام</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center gap-1">
<p
className={`text-R10 ${
!isCurrentUser ? "text-right" : "text-left"
} text-GRAY`}
>
{formatTime(message.createdAt || "")}
</p>
{isCurrentUser && (
<div className="text-GRAY">
{new Date(message.createdAt || "").getTime() <
new Date(otherParticipant?.lastReadAt || "").getTime() ? (
<div className="flex items-center">
<Check size={12} className="text-blue-500" />
<Check
size={12}
className="text-blue-500 relative left-2"
/>
</div>
) : (
<Check size={12} />
)}
</div>
)}
</div>
</div>
</div>
<DeleteConfirmBox
showDeleteDialog={showDeleteDialog}
setShowDeleteDialog={setShowDeleteDialog}
confirmDelete={() => onDelete?.(message.id || "")}
/>
</>
);
};
const DeleteConfirmBox = ({
showDeleteDialog,
setShowDeleteDialog,
confirmDelete,
}: {
showDeleteDialog: boolean;
setShowDeleteDialog: (show: boolean) => void;
confirmDelete: () => void;
}) => {
return (
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogOverlay className="bg-BLACK/20" />
<DialogContent className="p-4 bg-WHITE rounded-[15px] w-[calc(100%-40px)]">
<div className="flex flex-col gap-10">
<div className="flex flex-col gap-3 items-center w-full">
<Trash2 size={24} className="text-RED" />
<p className="text-B12 font-bold mt-1">حذف پیام</p>
<p className="text-R12">میخواهید این پیام را حذف کنید؟</p>
</div>
<div className="flex flex-row gap-6 w-full">
<Button
className="w-[100%]"
size={"sm"}
variant="dark"
onClick={() => setShowDeleteDialog(false)}
>
انصراف
</Button>
<Button
className="w-[100%]"
size={"sm"}
variant="primary"
onClick={() => {
confirmDelete();
setShowDeleteDialog(false);
}}
>
حذف پیام
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};
// Date separator component
export const DateSeparator = ({ date }: { date: string }) => {
return (
<div className="flex items-center justify-center my-4">
<div className="border-t border-GRAY3 flex-grow"></div>
<div className="px-3 text-R12 text-GRAY">{formatDate2(date)}</div>
<div className="border-t border-GRAY3 flex-grow"></div>
</div>
);
};