|
|
|
@ -1,12 +1,80 @@ |
|
|
|
import { MessageItem } from "@components/PageComponents/Messages/MessageItem.tsx"; |
|
|
|
import { Separator } from "@components/UI/Seperator"; |
|
|
|
import type { Message } from "@core/stores/messageStore/types.ts"; |
|
|
|
import type { TFunction } from "i18next"; |
|
|
|
import { InboxIcon } from "lucide-react"; |
|
|
|
import { Fragment, useMemo } from "react"; |
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
|
|
|
|
export interface ChannelChatProps { |
|
|
|
messages?: Message[]; |
|
|
|
} |
|
|
|
|
|
|
|
function toTs(d: Message["date"]): number { |
|
|
|
return typeof d === "number" ? d : Date.parse(String(d)); |
|
|
|
} |
|
|
|
|
|
|
|
function startOfLocalDay(ts: number): number { |
|
|
|
const d = new Date(ts); |
|
|
|
d.setHours(0, 0, 0, 0); |
|
|
|
return d.getTime(); |
|
|
|
} |
|
|
|
|
|
|
|
function formatDateLabelFromDayKey( |
|
|
|
dayKey: number, |
|
|
|
t: TFunction<"common", undefined>, |
|
|
|
fmt: Intl.DateTimeFormat, |
|
|
|
): string { |
|
|
|
const todayKey = startOfLocalDay(Date.now()); |
|
|
|
const yestKey = todayKey - 24 * 60 * 60 * 1000; |
|
|
|
|
|
|
|
if (dayKey === todayKey) { |
|
|
|
return t("unit.day.today"); // "Today" from common.json
|
|
|
|
} |
|
|
|
if (dayKey === yestKey) { |
|
|
|
return t("unit.day.yesterday"); // "Yesterday" from common.json
|
|
|
|
} |
|
|
|
return fmt.format(new Date(dayKey)); |
|
|
|
} |
|
|
|
|
|
|
|
type DayGroup = { dayKey: number; label: string; items: Message[] }; |
|
|
|
|
|
|
|
function groupMessagesByDay( |
|
|
|
messages: Message[], |
|
|
|
t: TFunction<"common", undefined>, |
|
|
|
fmt: Intl.DateTimeFormat, |
|
|
|
): DayGroup[] { |
|
|
|
const out: DayGroup[] = []; |
|
|
|
|
|
|
|
for (const msg of messages) { |
|
|
|
const ts = toTs(msg.date); |
|
|
|
const dayKey = startOfLocalDay(ts); |
|
|
|
const last = out[out.length - 1]; |
|
|
|
if (last && last.dayKey === dayKey) { |
|
|
|
last.items.push(msg); |
|
|
|
} else { |
|
|
|
out.push({ |
|
|
|
dayKey, |
|
|
|
label: formatDateLabelFromDayKey(dayKey, t, fmt), |
|
|
|
items: [msg], |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
return out; |
|
|
|
} |
|
|
|
|
|
|
|
const DateDelimiter = ({ label }: { label: string }) => ( |
|
|
|
<li aria-label={label}> |
|
|
|
<div className="my-2 flex h-3 items-center justify-center"> |
|
|
|
<Separator className="bg-slate-100 dark:bg-slate-800" /> |
|
|
|
<div className="mx-5 whitespace-nowrap text-center text-xs text-slate-400"> |
|
|
|
{label} |
|
|
|
</div> |
|
|
|
<Separator className="bg-slate-100 dark:bg-slate-800" /> |
|
|
|
</div> |
|
|
|
</li> |
|
|
|
); |
|
|
|
|
|
|
|
const EmptyState = () => { |
|
|
|
const { t } = useTranslation("messages"); |
|
|
|
return ( |
|
|
|
@ -18,7 +86,37 @@ const EmptyState = () => { |
|
|
|
}; |
|
|
|
|
|
|
|
export const ChannelChat = ({ messages = [] }: ChannelChatProps) => { |
|
|
|
if (!messages?.length) { |
|
|
|
const { i18n, t } = useTranslation(); |
|
|
|
|
|
|
|
const locale = useMemo( |
|
|
|
() => |
|
|
|
i18n.language || |
|
|
|
(typeof navigator !== "undefined" ? navigator.language : "en-US"), |
|
|
|
[i18n.language], |
|
|
|
); |
|
|
|
|
|
|
|
const dayLabelFmt = useMemo( |
|
|
|
() => |
|
|
|
new Intl.DateTimeFormat(locale, { |
|
|
|
year: "numeric", |
|
|
|
month: "long", |
|
|
|
day: "numeric", |
|
|
|
}), |
|
|
|
[locale], |
|
|
|
); |
|
|
|
|
|
|
|
// Sort messages by date in case they are stored out of order
|
|
|
|
const sorted = useMemo( |
|
|
|
() => [...messages].sort((a, b) => toTs(b.date) - toTs(a.date)), |
|
|
|
[messages], |
|
|
|
); |
|
|
|
|
|
|
|
const groups = useMemo( |
|
|
|
() => groupMessagesByDay(sorted, t, dayLabelFmt), |
|
|
|
[sorted, dayLabelFmt, t], |
|
|
|
); |
|
|
|
|
|
|
|
if (!messages.length) { |
|
|
|
return ( |
|
|
|
<div className="flex flex-1 flex-col items-center justify-center"> |
|
|
|
<EmptyState /> |
|
|
|
@ -27,12 +125,18 @@ export const ChannelChat = ({ messages = [] }: ChannelChatProps) => { |
|
|
|
} |
|
|
|
|
|
|
|
return ( |
|
|
|
<ul className="flex flex-col-reverse flex-grow overflow-y-auto px-3 py-2 "> |
|
|
|
{messages?.map((message) => ( |
|
|
|
<MessageItem |
|
|
|
key={message.messageId ?? `${message.from}-${message.date}`} |
|
|
|
message={message} |
|
|
|
/> |
|
|
|
<ul className="flex flex-col-reverse flex-grow overflow-y-auto px-3 py-2"> |
|
|
|
{groups.map(({ dayKey, label, items }) => ( |
|
|
|
<Fragment key={dayKey}> |
|
|
|
{/* Render messages first, then delimiter — with flex-col-reverse this shows the delimiter above that day's messages */} |
|
|
|
{items.map((message) => ( |
|
|
|
<MessageItem |
|
|
|
key={message.messageId ?? `${message.from}-${message.date}`} |
|
|
|
message={message} |
|
|
|
/> |
|
|
|
))} |
|
|
|
<DateDelimiter label={label} /> |
|
|
|
</Fragment> |
|
|
|
))} |
|
|
|
</ul> |
|
|
|
); |
|
|
|
|