262 lines
9.3 KiB
TypeScript
262 lines
9.3 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
CheckCircle2Icon,
|
|
CheckIcon,
|
|
ChevronDownIcon,
|
|
HeadphonesIcon,
|
|
MessageSquarePlusIcon,
|
|
} from "lucide-react"
|
|
import { useMemo } from "react"
|
|
import { useShallow } from "zustand/react/shallow"
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import type { ImConversation } from "@/lib/api/im"
|
|
import { getConversationStatusMessageKey } from "@/lib/support-conversation-i18n"
|
|
import {
|
|
isOpenImConversation,
|
|
resolveConversationTitle,
|
|
totalUnreadConversations,
|
|
} from "@/lib/support-conversation-list"
|
|
import { useSupportChatStore } from "@/lib/stores/support-chat"
|
|
import { cn, formatDateTime } from "@/lib/utils"
|
|
import { useI18n } from "@/i18n/provider"
|
|
|
|
type SupportChatConversationSwitcherProps = {
|
|
title: string
|
|
subtitle: string
|
|
status: string
|
|
onNewRequest: () => void
|
|
onCloseActiveRequest?: () => void
|
|
}
|
|
|
|
function getStatusBadgeClass(status: number) {
|
|
if (status === 4) {
|
|
return "bg-muted text-muted-foreground"
|
|
}
|
|
if (status === 2) {
|
|
return "bg-amber-500/12 text-amber-700 dark:bg-amber-400/15 dark:text-amber-300"
|
|
}
|
|
return "bg-emerald-500/12 text-emerald-700 dark:bg-emerald-400/15 dark:text-emerald-300"
|
|
}
|
|
|
|
function getStatusDotClass(status: string) {
|
|
if (status === "connected") {
|
|
return "bg-emerald-500 shadow-[0_0_0_3px_rgba(16,185,129,0.14)]"
|
|
}
|
|
if (status === "connecting") {
|
|
return "bg-amber-500 shadow-[0_0_0_3px_rgba(245,158,11,0.16)]"
|
|
}
|
|
return "bg-muted-foreground shadow-[0_0_0_3px_rgba(148,163,184,0.14)]"
|
|
}
|
|
|
|
export function SupportChatConversationSwitcher({
|
|
title,
|
|
subtitle,
|
|
status,
|
|
onNewRequest,
|
|
onCloseActiveRequest,
|
|
}: SupportChatConversationSwitcherProps) {
|
|
const t = useI18n()
|
|
|
|
const {
|
|
conversations,
|
|
activeConversationId,
|
|
creatingConversation,
|
|
selectConversation,
|
|
} = useSupportChatStore(
|
|
useShallow((state) => ({
|
|
conversations: state.conversations,
|
|
activeConversationId: state.activeConversationId,
|
|
creatingConversation: state.creatingConversation,
|
|
selectConversation: state.selectConversation,
|
|
}))
|
|
)
|
|
|
|
const openCount = useMemo(
|
|
() => conversations.filter((item) => isOpenImConversation(item)).length,
|
|
[conversations]
|
|
)
|
|
const totalUnread = useMemo(
|
|
() => totalUnreadConversations(conversations),
|
|
[conversations]
|
|
)
|
|
const active = useMemo(
|
|
() => conversations.find((item) => item.id === activeConversationId) ?? null,
|
|
[conversations, activeConversationId]
|
|
)
|
|
|
|
const activeSubject = active?.subject?.trim()
|
|
const hasMultiple = conversations.length > 1
|
|
|
|
const displayTitle = activeSubject
|
|
? activeSubject
|
|
: hasMultiple && active
|
|
? resolveConversationTitle(active, t("supportChat.requestFallbackTitle"))
|
|
: title
|
|
|
|
const displaySubtitle = activeSubject || hasMultiple
|
|
? title
|
|
: subtitle
|
|
|
|
const avatarElement = (
|
|
<div className="relative flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary ring-1 ring-primary/15 dark:bg-primary/15 dark:ring-primary/25">
|
|
<HeadphonesIcon className="size-4" />
|
|
<span
|
|
className={cn(
|
|
"absolute -right-0.5 -bottom-0.5 size-2.5 rounded-full ring-2 ring-card",
|
|
getStatusDotClass(status)
|
|
)}
|
|
aria-hidden="true"
|
|
/>
|
|
</div>
|
|
)
|
|
|
|
if (!hasMultiple) {
|
|
return (
|
|
<div className="flex min-w-0 flex-1 items-center gap-2.5 p-1 -m-1">
|
|
{avatarElement}
|
|
<div className="min-w-0 flex-1 space-y-0.5">
|
|
<div className="truncate text-sm font-semibold leading-5 text-foreground">
|
|
{displayTitle}
|
|
</div>
|
|
<div className="truncate text-[11px] leading-4 text-muted-foreground">
|
|
{displaySubtitle}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger
|
|
render={
|
|
<button
|
|
type="button"
|
|
className="flex min-w-0 flex-1 items-center gap-2.5 rounded-md p-1 -m-1 text-left transition-colors hover:bg-foreground/[0.04] group focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring cursor-pointer"
|
|
aria-label={t("supportChat.myRequests")}
|
|
>
|
|
{avatarElement}
|
|
<div className="min-w-0 flex-1 space-y-0.5">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className="truncate text-sm font-semibold leading-5 text-foreground">
|
|
{displayTitle}
|
|
</span>
|
|
<ChevronDownIcon className="size-3.5 shrink-0 text-muted-foreground opacity-60 group-hover:opacity-100 transition-opacity" />
|
|
{totalUnread > 0 ? (
|
|
<span className="inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-none text-primary-foreground">
|
|
{totalUnread > 99 ? "99+" : totalUnread}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<div className="truncate text-[11px] leading-4 text-muted-foreground">
|
|
{displaySubtitle}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
}
|
|
>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-[300px] p-0">
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuLabel className="flex items-center justify-between gap-2 px-3 py-2 text-xs font-medium text-muted-foreground">
|
|
<span>{t("supportChat.myRequests")}</span>
|
|
<span className="shrink-0 font-normal">
|
|
{t("supportChat.openRequestCount", {
|
|
open: openCount,
|
|
total: conversations.length,
|
|
})}
|
|
</span>
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuSeparator className="my-0" />
|
|
<div className="max-h-64 overflow-y-auto py-1">
|
|
{conversations.map((conversation: ImConversation) => (
|
|
<DropdownMenuItem
|
|
key={conversation.id}
|
|
onClick={() => void selectConversation(conversation.id)}
|
|
className="flex-col items-start gap-1 px-3 py-2"
|
|
>
|
|
<span className="flex w-full min-w-0 items-center gap-1.5">
|
|
<CheckIcon
|
|
className={cn(
|
|
"size-3.5 shrink-0 text-primary",
|
|
conversation.id === activeConversationId
|
|
? "opacity-100"
|
|
: "opacity-0"
|
|
)}
|
|
aria-hidden="true"
|
|
/>
|
|
<span className="min-w-0 flex-1 truncate text-xs font-medium text-foreground">
|
|
{resolveConversationTitle(
|
|
conversation,
|
|
t("supportChat.requestFallbackTitle")
|
|
)}
|
|
</span>
|
|
{Number(conversation.customerUnreadCount) > 0 ? (
|
|
<span className="inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold leading-none text-primary-foreground">
|
|
{conversation.customerUnreadCount}
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
<span className="flex w-full items-center gap-1.5 pl-5 text-[11px] text-muted-foreground">
|
|
<span
|
|
className={cn(
|
|
"inline-flex shrink-0 items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium",
|
|
getStatusBadgeClass(conversation.status)
|
|
)}
|
|
>
|
|
{t(getConversationStatusMessageKey(conversation.status))}
|
|
</span>
|
|
<span className="truncate">
|
|
{formatDateTime(conversation.lastActiveAt)}
|
|
</span>
|
|
</span>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</div>
|
|
</DropdownMenuGroup>
|
|
<DropdownMenuSeparator className="my-0" />
|
|
<div className="flex items-center gap-1.5 p-1.5">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={creatingConversation}
|
|
onClick={onNewRequest}
|
|
className="h-8 flex-1 justify-start gap-1.5 text-xs"
|
|
>
|
|
<MessageSquarePlusIcon className="size-3.5" />
|
|
<span className="truncate">
|
|
{creatingConversation
|
|
? t("supportChat.creatingRequest")
|
|
: t("supportChat.newRequest")}
|
|
</span>
|
|
</Button>
|
|
{active && active.status !== 4 && onCloseActiveRequest ? (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={onCloseActiveRequest}
|
|
className="h-8 shrink-0 gap-1 px-2 text-xs text-muted-foreground hover:bg-rose-50 hover:text-rose-600 dark:hover:bg-rose-950/45 dark:hover:text-rose-300"
|
|
title={t("supportChat.confirmClose")}
|
|
>
|
|
<CheckCircle2Icon className="size-3.5" />
|
|
<span>{t("supportChat.confirmClose")}</span>
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
} |