171 lines
5.3 KiB
TypeScript
171 lines
5.3 KiB
TypeScript
/**
|
|
* @file components/chat/ChatMessages.tsx
|
|
* @description Scrollable container component that displays message history, handles date dividers, and renders individual chat items for both channels and direct messages.
|
|
*/
|
|
|
|
"use client";
|
|
|
|
import { useState, useEffect, useRef } from "react";
|
|
import { ChatItem, type MessageWithMember } from "./ChatItem";
|
|
|
|
/** Properties for the ChatMessages component. */
|
|
export interface ChatMessagesProps {
|
|
type: "chat" | "dm";
|
|
name: string;
|
|
initialMessages: MessageWithMember[];
|
|
userFriendships: Array<{
|
|
senderId: string;
|
|
receiverId: string;
|
|
status: string;
|
|
}>;
|
|
currentUserId: string;
|
|
onDeleteMessage?: (id: string) => void;
|
|
onEditMessage?: (id: string, newContent: string) => void;
|
|
}
|
|
|
|
/** Formats a date string or Date object into a readable label (Today, Yesterday, or formatted date). */
|
|
function formatDateLabel(dateString: string | Date): string {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
|
|
const isToday =
|
|
date.getDate() === now.getDate() &&
|
|
date.getMonth() === now.getMonth() &&
|
|
date.getFullYear() === now.getFullYear();
|
|
|
|
const yesterday = new Date(now);
|
|
yesterday.setDate(now.getDate() - 1);
|
|
const isYesterday =
|
|
date.getDate() === yesterday.getDate() &&
|
|
date.getMonth() === yesterday.getMonth() &&
|
|
date.getFullYear() === yesterday.getFullYear();
|
|
|
|
if (isToday) return "Today";
|
|
if (isYesterday) return "Yesterday";
|
|
|
|
return date.toLocaleDateString("de-DE", {
|
|
day: "numeric",
|
|
month: "long",
|
|
year: "numeric",
|
|
});
|
|
}
|
|
|
|
/** Renders the scrollable message feed with greetings, date dividers, and interactive chat items. */
|
|
export function ChatMessages({
|
|
type,
|
|
name,
|
|
initialMessages,
|
|
currentUserId,
|
|
userFriendships,
|
|
onDeleteMessage,
|
|
onEditMessage,
|
|
}: ChatMessagesProps) {
|
|
const [messages, setMessages] =
|
|
useState<MessageWithMember[]>(initialMessages);
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
setMessages(initialMessages);
|
|
}, [initialMessages]);
|
|
|
|
const scrollToBottom = () => {
|
|
if (scrollRef.current) {
|
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
requestAnimationFrame(() => {
|
|
scrollToBottom();
|
|
});
|
|
}, [messages]);
|
|
|
|
/**
|
|
* Handles local state update and triggers parent callback upon successful message deletion.
|
|
*
|
|
* @function handleDeleteMessage
|
|
* @param {string} id - The unique identifier of the deleted message.
|
|
* @returns {void}
|
|
*/
|
|
const handleDeleteMessage = (id: string) => {
|
|
setMessages((prev) => prev.filter((m) => m.id !== id));
|
|
onDeleteMessage?.(id);
|
|
};
|
|
|
|
/**
|
|
* Handles local state update and triggers parent callback upon successful message edit.
|
|
*
|
|
* @function handleEditMessage
|
|
* @param {string} id - The unique identifier of the edited message.
|
|
* @param {string} newContent - The updated content text of the message.
|
|
* @returns {void}
|
|
*/
|
|
const handleEditMessage = (id: string, newContent: string) => {
|
|
setMessages((prev) =>
|
|
prev.map((m) =>
|
|
m.id === id ? { ...m, content: newContent, updatedAt: new Date() } : m,
|
|
),
|
|
);
|
|
onEditMessage?.(id, newContent);
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={scrollRef}
|
|
className="flex-1 overflow-y-auto flex flex-col min-h-0 py-2 mt-4"
|
|
>
|
|
<div className="flex flex-col mt-auto mb-4">
|
|
{/* Chat Header Greeting */}
|
|
<div className="mb-6">
|
|
<h2 className="text-2xl font-bold text-white">
|
|
{type === "dm"
|
|
? `Direct Messages with @${name}`
|
|
: `Welcome to #${name}!`}
|
|
</h2>
|
|
<p className="text-muted text-sm">
|
|
{type === "dm"
|
|
? `This is the start of your direct message history with @${name}.`
|
|
: `This is the beginning of the channel #${name}.`}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Message Feed */}
|
|
<div className="space-y-4">
|
|
{messages.map((message, index) => {
|
|
const currentDateLabel = formatDateLabel(message.createdAt);
|
|
const previousMessage = messages[index - 1];
|
|
const previousDateLabel = previousMessage
|
|
? formatDateLabel(previousMessage.createdAt)
|
|
: null;
|
|
|
|
const showDateDivider = currentDateLabel !== previousDateLabel;
|
|
|
|
return (
|
|
<div className="m-0" key={message.id}>
|
|
{showDateDivider && (
|
|
<div className="relative flex items-center justify-center my-2">
|
|
<div className="absolute inset-0 flex items-center">
|
|
<div className="w-full border-t border-muted/30" />
|
|
</div>
|
|
<div className="relative bg-background px-2 text-xs font-semibold text-muted rounded-full border border-muted/30">
|
|
{currentDateLabel}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<ChatItem
|
|
type={type}
|
|
message={message}
|
|
currentUserId={currentUserId}
|
|
userFriendships={userFriendships}
|
|
onDeleteSuccess={handleDeleteMessage}
|
|
onEditSuccess={handleEditMessage}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|