215 lines
7.2 KiB
TypeScript
215 lines
7.2 KiB
TypeScript
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>
|
||
);
|
||
};
|