Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
89 lines
2.6 KiB
TypeScript
89 lines
2.6 KiB
TypeScript
import { formatChatDayLabel, toLocalDayKey } from '@/lib/formatters'
|
|
|
|
interface DisplayableChatMessage {
|
|
id: string
|
|
senderId: string
|
|
createdAt: string
|
|
}
|
|
|
|
export interface ChatDateSeparatorItem {
|
|
kind: 'date'
|
|
key: string
|
|
label: string
|
|
}
|
|
|
|
export interface ChatMessageDisplayItem<TMessage extends DisplayableChatMessage = DisplayableChatMessage> {
|
|
kind: 'message'
|
|
key: string
|
|
message: TMessage
|
|
showSenderName: boolean
|
|
showAvatar: boolean
|
|
isClusterStart: boolean
|
|
isClusterEnd: boolean
|
|
}
|
|
|
|
export type ChatDisplayItem<TMessage extends DisplayableChatMessage = DisplayableChatMessage> =
|
|
| ChatDateSeparatorItem
|
|
| ChatMessageDisplayItem<TMessage>
|
|
|
|
const SAME_SENDER_GAP_MS = 5 * 60 * 1000
|
|
|
|
const getTime = (value: string) => {
|
|
const time = new Date(value).getTime()
|
|
|
|
return Number.isFinite(time) ? time : 0
|
|
}
|
|
|
|
/**
|
|
* Builds Telegram-like display rows: day separators + clustered messages.
|
|
* Name on first of consecutive same-sender run; avatar on last of that run.
|
|
*/
|
|
export const groupMessagesForDisplay = <TMessage extends DisplayableChatMessage>(
|
|
messages: TMessage[],
|
|
options?: { showSenderNames?: boolean; now?: Date }
|
|
): ChatDisplayItem<TMessage>[] => {
|
|
const showSenderNames = options?.showSenderNames ?? false
|
|
const now = options?.now ?? new Date()
|
|
const items: ChatDisplayItem<TMessage>[] = []
|
|
let lastDayKey: string | null = null
|
|
|
|
messages.forEach((message, index) => {
|
|
const dayKey = toLocalDayKey(message.createdAt) ?? message.id
|
|
|
|
if (dayKey !== lastDayKey) {
|
|
items.push({
|
|
kind: 'date',
|
|
key: `date-${dayKey}`,
|
|
label: formatChatDayLabel(message.createdAt, now),
|
|
})
|
|
lastDayKey = dayKey
|
|
}
|
|
|
|
const previousSameSender =
|
|
index > 0 &&
|
|
messages[index - 1].senderId === message.senderId &&
|
|
toLocalDayKey(messages[index - 1].createdAt) === toLocalDayKey(message.createdAt) &&
|
|
getTime(message.createdAt) - getTime(messages[index - 1].createdAt) <= SAME_SENDER_GAP_MS
|
|
const nextSameSender =
|
|
index < messages.length - 1 &&
|
|
messages[index + 1].senderId === message.senderId &&
|
|
toLocalDayKey(messages[index + 1].createdAt) === toLocalDayKey(message.createdAt) &&
|
|
getTime(messages[index + 1].createdAt) - getTime(message.createdAt) <= SAME_SENDER_GAP_MS
|
|
|
|
const isClusterStart = !previousSameSender
|
|
const isClusterEnd = !nextSameSender
|
|
|
|
items.push({
|
|
kind: 'message',
|
|
key: message.id,
|
|
message,
|
|
showSenderName: showSenderNames && isClusterStart,
|
|
showAvatar: showSenderNames && isClusterEnd,
|
|
isClusterStart,
|
|
isClusterEnd,
|
|
})
|
|
})
|
|
|
|
return items
|
|
}
|