File "MessageBubble-20260606084522.tsx"

Full Path: /home/trinadezambia/public_html/student_panel/src/components/chat/MessageBubble-20260606084522.tsx
File size: 16.59 KB
MIME-type: text/x-java
Charset: utf-8

'use client';
// app/components/chat/MessageBubble.tsx
import React, { useState } from 'react';
import {
  FileText,
  Download,
  CheckCheck,
  MoreVertical,
  Trash2,
} from 'lucide-react';
import Image from 'next/image';
import { SharedMediaModal } from './SharedMediaModal';
import { useDeleteMessage } from '@/lib/api/student/queryHooks';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';

// Message type definition
type Message = {
  id: number;
  sender: string;
  isSender: boolean;
  time: string;
  status: 'sent' | 'read';
  content: string;
  attachment?: { name: string; size?: string; url?: string };
  images?: string[];
  avatar?: string;
};

type MessageBubbleProps = {
  message: Message;
  receiverId?: number | null;
};

// Receiver message content component (left-aligned, gray background)
const MessageContent: React.FC<{
  content: string;
  attachment?: { name: string; size?: string; url?: string };
  images?: string[];
}> = ({ content, attachment, images }) => {
  const [isModalOpen, setIsModalOpen] = useState(false);

  const handleOpenModal = () => {
    setIsModalOpen(true);
  };

  const handleCloseModal = () => {
    setIsModalOpen(false);
  };

  const handleDownloadFile = async (
    imageUrl: string,
    fileName?: string,
    index?: number,
  ) => {
    try {
      const response = await fetch(imageUrl);
      const blob = await response.blob();
      const url = window.URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = fileName || `image-${(index ?? 0) + 1}.jpg`;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      window.URL.revokeObjectURL(url);
    } catch (error) {
      console.error('Failed to download file:', error);
      window.open(imageUrl, '_blank');
    }
  };

  return (
    <div className="flex flex-col gap-1.5 sm:gap-2 items-start">
      {/* Text content */}
      {content && content.trim() !== '' && (
        <div className="px-3 py-2 sm:px-3.5 sm:py-2.5 md:px-4 md:py-3 bg-[#EAEAEA] rounded-xl rounded-tl-none max-w-[calc(100vw-120px)] xs:max-w-[280px] sm:max-w-[400px] md:max-w-[500px] lg:max-w-[600px] break-words">
          <p className="text-[13px] sm:text-sm md:text-[15px] text-gray-800 whitespace-pre-wrap leading-relaxed">
            {content}
          </p>
        </div>
      )}

      {/* File Attachments */}
      {attachment && (
        <div className="px-3 py-2 sm:px-3.5 sm:py-2.5 md:px-4 md:py-3 bg-[#EAEAEA] rounded-xl rounded-tl-none max-w-[calc(100vw-120px)] xs:max-w-[280px] sm:max-w-[400px] md:max-w-[500px]">
          <div className="flex items-center bg-white border border-gray-200 rounded-lg p-2 sm:p-2.5 md:p-3 gap-2 sm:gap-2.5 md:gap-3">
            <div className="w-9 h-9 sm:w-10 sm:h-10 md:w-12 md:h-12 shrink-0 bg-[#D1F4E0] rounded-lg flex items-center justify-center">
              <FileText className="w-5 h-5 sm:w-5.5 sm:h-5.5 md:w-6 md:h-6 text-gray-700" />
            </div>
            <div className="grow min-w-0">
              <p className="text-xs sm:text-sm md:text-base font-medium text-gray-800 truncate">
                {attachment.name}
              </p>
              <p className="text-[10px] sm:text-xs text-gray-500 mt-0.5">
                {attachment.size}
              </p>
            </div>
            <button
              onClick={(e) => {
                e.stopPropagation();
                if (attachment?.url) {
                  handleDownloadFile(attachment.url, attachment.name);
                }
              }}
              className="p-1.5 sm:p-2 md:p-2.5 rounded-lg bg-gray-100 hover:bg-gray-200 active:bg-gray-300 transition-colors shrink-0"
            >
              <Download className="w-3.5 h-3.5 sm:w-4 sm:h-4 md:w-4.5 md:h-4.5 text-gray-700" />
            </button>
          </div>
        </div>
      )}

      {/* Image Attachments */}
      {images && images.length > 0 && (
        <div className="px-2.5 py-2 sm:px-3 sm:py-2.5 md:px-3.5 md:py-3 bg-[#EAEAEA] rounded-xl rounded-tl-none max-w-[calc(100vw-120px)] xs:max-w-[300px] sm:max-w-[380px] md:max-w-[450px]">
          <div
            className={`grid ${
              images.length === 1
                ? 'grid-cols-1 max-w-[200px] sm:max-w-[250px]'
                : 'grid-cols-2'
            } gap-1.5 sm:gap-2 md:gap-2.5`}
          >
            {images.slice(0, 3).map((src, index) => (
              <div
                key={index}
                className="relative aspect-square rounded-lg overflow-hidden group bg-gray-100"
              >
                <Image
                  width={300}
                  height={300}
                  src={src}
                  alt={`attachment ${index + 1}`}
                  className="w-full h-full object-cover"
                  priority
                />
                <button
                  onClick={(e) => {
                    e.stopPropagation();
                    handleDownloadFile(src, undefined, index);
                  }}
                  className="absolute top-1.5 right-1.5 sm:top-2 sm:right-2 bg-white/95 hover:bg-white text-gray-700 p-1.5 sm:p-2 rounded-md shadow-lg transition-all opacity-0 group-hover:opacity-100 touch:opacity-100"
                >
                  <Download className="w-3 h-3 sm:w-3.5 sm:h-3.5 md:w-4 md:h-4" />
                </button>
              </div>
            ))}
            {images.length > 3 && (
              <div className="relative aspect-square rounded-lg overflow-hidden bg-gray-100">
                <Image
                  width={300}
                  height={300}
                  src={images[3]}
                  alt="more images"
                  className="w-full h-full object-cover"
                  priority
                />
                <button
                  onClick={handleOpenModal}
                  className="absolute inset-0 bg-black/60 flex items-center justify-center rounded-lg hover:bg-black/70 transition-colors cursor-pointer"
                >
                  <span className="text-gray-900 font-semibold text-xs sm:text-sm md:text-base bg-white rounded-lg px-3 py-1.5 sm:px-4 sm:py-2 shadow-md">
                    +{images.length - 3} More
                  </span>
                </button>
              </div>
            )}
          </div>
        </div>
      )}

      {/* Shared Media Modal */}
      {images && images.length > 3 && (
        <SharedMediaModal
          isOpen={isModalOpen}
          onClose={handleCloseModal}
          images={images}
        />
      )}
    </div>
  );
};

// Sender message content component (right-aligned, green background)
const MessageContentSender: React.FC<{
  content: string;
  attachment?: { name: string; size?: string; url?: string };
  images?: string[];
}> = ({ content, attachment, images }) => {
  const [isModalOpen, setIsModalOpen] = useState(false);

  const handleOpenModal = () => {
    setIsModalOpen(true);
  };

  const handleCloseModal = () => {
    setIsModalOpen(false);
  };

  const handleDownloadFile = async (
    imageUrl: string,
    fileName?: string,
    index?: number,
  ) => {
    try {
      const response = await fetch(imageUrl);
      const blob = await response.blob();
      const url = window.URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = fileName || `image-${(index ?? 0) + 1}.jpg`;
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      window.URL.revokeObjectURL(url);
    } catch (error) {
      console.error('Failed to download file:', error);
      window.open(imageUrl, '_blank');
    }
  };

  return (
    <div className="flex flex-col gap-1.5 sm:gap-2 items-end">
      {/* Text content */}
      {content && content.trim() !== '' && (
        <div className="px-3 py-2 sm:px-3.5 sm:py-2.5 md:px-4 md:py-3 bg-[#CEF0E0] rounded-xl rounded-tr-none max-w-[calc(100vw-120px)] xs:max-w-[280px] sm:max-w-[400px] md:max-w-[500px] lg:max-w-[600px] break-words">
          <p className="text-[13px] sm:text-sm md:text-[15px] text-gray-800 whitespace-pre-wrap leading-relaxed text-left">
            {content}
          </p>
        </div>
      )}

      {/* File Attachments */}
      {attachment && (
        <div className="px-3 py-2 sm:px-3.5 sm:py-2.5 md:px-4 md:py-3 bg-[#CEF0E0] rounded-xl rounded-tr-none max-w-[calc(100vw-120px)] xs:max-w-[280px] sm:max-w-[400px] md:max-w-[500px]">
          <div className="flex items-center bg-white border border-gray-200 rounded-lg p-2 sm:p-2.5 md:p-3 gap-2 sm:gap-2.5 md:gap-3">
            <div className="w-9 h-9 sm:w-10 sm:h-10 md:w-12 md:h-12 shrink-0 bg-[#CEF0E0] rounded-lg flex items-center justify-center">
              <FileText className="w-5 h-5 sm:w-5.5 sm:h-5.5 md:w-6 md:h-6 text-gray-700" />
            </div>
            <div className="grow min-w-0">
              <p className="text-xs sm:text-sm md:text-base font-medium text-gray-800 truncate">
                {attachment.name}
              </p>
              <p className="text-[10px] sm:text-xs text-gray-500 mt-0.5">
                {attachment.size}
              </p>
            </div>
            <button
              onClick={(e) => {
                e.stopPropagation();
                if (attachment?.url) {
                  handleDownloadFile(attachment.url, attachment.name);
                }
              }}
              className="p-1.5 sm:p-2 md:p-2.5 rounded-lg bg-gray-100 hover:bg-gray-200 active:bg-gray-300 transition-colors shrink-0"
            >
              <Download className="w-3.5 h-3.5 sm:w-4 sm:h-4 md:w-4.5 md:h-4.5 text-gray-700" />
            </button>
          </div>
        </div>
      )}

      {/* Image Attachments */}
      {images && images.length > 0 && (
        <div className="px-2.5 py-2 sm:px-3 sm:py-2.5 md:px-3.5 md:py-3 bg-[#CEF0E0] rounded-xl rounded-tr-none max-w-[calc(100vw-120px)] xs:max-w-[300px] sm:max-w-[380px] md:max-w-[450px]">
          <div
            className={`grid ${
              images.length === 1
                ? 'grid-cols-1 max-w-[200px] sm:max-w-[250px]'
                : 'grid-cols-2'
            } gap-1.5 sm:gap-2 md:gap-2.5`}
          >
            {images.slice(0, 3).map((src, index) => (
              <div
                key={index}
                className="relative aspect-square rounded-lg overflow-hidden group bg-gray-100"
              >
                <Image
                  width={300}
                  height={300}
                  src={src}
                  alt={`attachment ${index + 1}`}
                  className="w-full h-full object-cover"
                  priority
                />
                <button
                  onClick={(e) => {
                    e.stopPropagation();
                    handleDownloadFile(src, undefined, index);
                  }}
                  className="absolute top-1.5 right-1.5 sm:top-2 sm:right-2 bg-white/95 hover:bg-white text-gray-700 p-1.5 sm:p-2 rounded-md shadow-lg transition-all opacity-0 group-hover:opacity-100 touch:opacity-100"
                >
                  <Download className="w-3 h-3 sm:w-3.5 sm:h-3.5 md:w-4 md:h-4" />
                </button>
              </div>
            ))}
            {images.length > 3 && (
              <div className="relative aspect-square rounded-lg overflow-hidden bg-gray-100">
                <Image
                  width={300}
                  height={300}
                  src={images[3]}
                  alt="more images"
                  className="w-full h-full object-cover"
                  priority
                />
                <button
                  onClick={handleOpenModal}
                  className="absolute inset-0 bg-black/60 flex items-center justify-center rounded-lg hover:bg-black/70 transition-colors cursor-pointer"
                >
                  <span className="text-gray-900 font-semibold text-xs sm:text-sm md:text-base bg-white rounded-lg px-3 py-1.5 sm:px-4 sm:py-2 shadow-md">
                    +{images.length - 3} More
                  </span>
                </button>
              </div>
            )}
          </div>
        </div>
      )}

      {/* Shared Media Modal */}
      {images && images.length > 3 && (
        <SharedMediaModal
          isOpen={isModalOpen}
          onClose={handleCloseModal}
          images={images}
        />
      )}
    </div>
  );
};

// Sender message component (right-aligned, green background)
const SenderMessage: React.FC<{
  messageId: number;
  content: string;
  time: string;
  status: 'sent' | 'read';
  attachment?: { name: string; size?: string; url?: string };
  images?: string[];
  avatar?: string;
  receiverId?: number | null;
}> = ({ messageId, content, time, attachment, images, receiverId }) => {
  const deleteMessageMutation = useDeleteMessage(receiverId || null);

  const handleDeleteMessage = () => {
    deleteMessageMutation.mutate({ id: [messageId] });
  };

  return (
    <div className="flex justify-end items-start gap-1.5 sm:gap-2 md:gap-2.5 mb-2.5 sm:mb-3 md:mb-4 px-2 sm:px-3 group">
      {/* Message content */}
      <div className="flex flex-col items-end flex-1 min-w-0">
        <MessageContentSender
          content={content}
          attachment={attachment}
          images={images}
        />
        {/* Time and status */}
        <div className="flex items-center gap-1 mt-1 px-1">
          <span className="text-[10px] sm:text-xs text-gray-500">{time}</span>
          <CheckCheck className="w-3 h-3 sm:w-3.5 sm:h-3.5 text-(--primary-color)" />
        </div>
      </div>

      {/* Avatar and menu */}
      <div className="flex items-start gap-0.5 sm:gap-1 shrink-0">
        <DropdownMenu>
          <DropdownMenuTrigger asChild>
            <button
              className="p-0.5 sm:p-1 opacity-0 group-hover:opacity-100 transition-opacity rounded hover:bg-gray-100"
              aria-label="Message options"
            >
              <MoreVertical className="w-4 h-4 sm:w-4.5 sm:h-4.5 text-gray-500" />
            </button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align="end" className="min-w-[140px]">
            <DropdownMenuItem
              variant="destructive"
              onClick={handleDeleteMessage}
              disabled={deleteMessageMutation.isPending}
              className="cursor-pointer gap-2"
            >
              <Trash2 className="w-4 h-4" />
              <span>Delete</span>
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>

        <div className="w-7 h-7 sm:w-8 sm:h-8 md:w-9 md:h-9 rounded-full bg-gray-200 overflow-hidden shrink-0">
          <Image
            src={'/assets/images/common/user.png'}
            alt="avatar"
            width={36}
            height={36}
            className="w-full h-full object-cover"
          />
        </div>
      </div>
    </div>
  );
};

// Non-sender message component (left-aligned, gray background)
const NonSenderMessage: React.FC<{
  content: string;
  time: string;
  avatar?: string;
  attachment?: { name: string; size?: string; url?: string };
  images?: string[];
}> = ({ content, time, avatar, attachment, images }) => {
  return (
    <div className="flex justify-start items-start gap-1.5 sm:gap-2 md:gap-2.5 mb-2.5 sm:mb-3 md:mb-4 px-2 sm:px-3 group">
      {/* Avatar */}
      <div className="w-7 h-7 sm:w-8 sm:h-8 md:w-9 md:h-9 rounded-full bg-gray-200 overflow-hidden shrink-0">
        <Image
          src={avatar || '/assets/images/common/user.png'}
          alt="avatar"
          width={36}
          height={36}
          className="w-full h-full object-cover"
        />
      </div>

      {/* Message content */}
      <div className="flex flex-col items-start flex-1 min-w-0">
        <MessageContent
          content={content}
          attachment={attachment}
          images={images}
        />
        {/* Time */}
        <div className="flex items-center mt-1 px-1">
          <span className="text-[10px] sm:text-xs text-gray-500">{time}</span>
        </div>
      </div>
    </div>
  );
};

// Main MessageBubble component
export const MessageBubble: React.FC<MessageBubbleProps> = ({
  message,
  receiverId,
}) => {
  const { id, isSender, content, time, status, attachment, images, avatar } =
    message;

  if (isSender) {
    return (
      <SenderMessage
        messageId={id}
        content={content}
        time={time}
        status={status}
        attachment={attachment}
        images={images}
        avatar={avatar}
        receiverId={receiverId}
      />
    );
  }

  return (
    <NonSenderMessage
      content={content}
      time={time}
      avatar={avatar}
      attachment={attachment}
      images={images}
    />
  );
};