126 lines
4.4 KiB
TypeScript
126 lines
4.4 KiB
TypeScript
import { useInfiniteQuery, type QueryFunctionContext } from "@tanstack/react-query";
|
|
import type { Channel, Message, MessageId } from "~/lib/api/types";
|
|
import ChatMessage from "./chat-message";
|
|
import MessageBox from "./message-box";
|
|
import { Separator } from "./ui/separator";
|
|
import VisibleTrigger from "./visible-trigger";
|
|
|
|
interface ChannelAreaProps {
|
|
channel: Channel;
|
|
}
|
|
|
|
export default function ChannelArea({ channel }: ChannelAreaProps) {
|
|
const channelId = channel.id;
|
|
|
|
const fetchMessages = async ({ pageParam }: QueryFunctionContext) => {
|
|
return await import("~/lib/api/client/channel").then((m) =>
|
|
m.default.paginatedMessages(channelId, 50, pageParam as MessageId | undefined),
|
|
);
|
|
};
|
|
|
|
const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, isPending, status } =
|
|
useInfiniteQuery({
|
|
queryKey: ["messages", channelId],
|
|
initialPageParam: undefined,
|
|
queryFn: fetchMessages,
|
|
getNextPageParam: (lastPage) => (lastPage.length < 50 ? undefined : lastPage[lastPage.length - 1]?.id),
|
|
staleTime: Infinity,
|
|
});
|
|
|
|
const fetchNextPageVisible = () => {
|
|
if (!isFetchingNextPage && hasNextPage) fetchNextPage();
|
|
};
|
|
|
|
let messageArea = null;
|
|
|
|
if (isPending) {
|
|
messageArea = (
|
|
<div className="flex items-center justify-center size-full">
|
|
<span>Loading...</span>
|
|
</div>
|
|
);
|
|
} else {
|
|
messageArea = (
|
|
<>
|
|
<div className="flex-1" />
|
|
<div className="flex flex-col-reverse overflow-auto gap-2 pb-2 pr-2">
|
|
{status === "success" && renderMessages(data.pages)}
|
|
<VisibleTrigger triggerOnce={false} onVisible={fetchNextPageVisible} />
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="flex flex-col size-full">
|
|
<div className="w-full min-h-12 border-b-2 flex items-center justify-center">{channel?.name}</div>
|
|
<div className="flex-1 overflow-y-auto flex flex-col pl-2 pr-0.5">{messageArea}</div>
|
|
<div className="w-full">
|
|
<MessageBox channelId={channelId} />
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function renderMessages(pages: Message[][]) {
|
|
const messageElements: React.ReactNode[] = [];
|
|
let lastDate: string | null = null;
|
|
|
|
const formatMessageDate = (date: Date) => {
|
|
const now = new Date();
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
const yesterday = new Date(today);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
|
|
const messageDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
|
|
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
|
|
if (messageDate.getTime() === today.getTime()) {
|
|
const rtf = new Intl.RelativeTimeFormat(undefined, {
|
|
numeric: "auto",
|
|
});
|
|
return capitalize(rtf.format(0, "day"));
|
|
} else if (messageDate.getTime() === yesterday.getTime()) {
|
|
const rtf = new Intl.RelativeTimeFormat(undefined, {
|
|
numeric: "auto",
|
|
});
|
|
return capitalize(rtf.format(-1, "day"));
|
|
} else {
|
|
return date.toLocaleDateString(undefined, {
|
|
month: "short",
|
|
day: "numeric",
|
|
});
|
|
}
|
|
};
|
|
|
|
pages.forEach((page) => {
|
|
page.forEach((message) => {
|
|
const messageDate = message.createdAt.toDateString();
|
|
if (messageDate != lastDate) {
|
|
if (lastDate)
|
|
messageElements.push(
|
|
<div className="flex items-center justify-center py-2 w-full">
|
|
<Separator className="flex-1" />
|
|
<span className="mx-4 text-sm text-muted-foreground">
|
|
{formatMessageDate(new Date(lastDate))}
|
|
</span>
|
|
<Separator className="flex-1" />
|
|
</div>,
|
|
);
|
|
lastDate = messageDate;
|
|
}
|
|
|
|
messageElements.push(
|
|
<div key={message.id} className="w-full">
|
|
<ChatMessage message={message} />
|
|
</div>,
|
|
);
|
|
});
|
|
});
|
|
|
|
return messageElements;
|
|
}
|