infoalloggi-monorepo/apps/infoalloggi/src/components/chat/chat-sidebar.tsx

459 lines
12 KiB
TypeScript
Raw Normal View History

import { Check, CheckCheck, Search, SquarePen } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useCallback, useState } from "react";
import { Etichetta, EtichetteModal } from "~/components/etichette";
import { LoadingPage } from "~/components/loading";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "~/components/ui/alert-dialog";
import { Button, buttonVariants } from "~/components/ui/button";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "~/components/ui/command";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "~/components/ui/context-menu";
import Input from "~/components/ui/input";
import { UserAvatar } from "~/components/user_avatar";
import { useMediaQuery } from "~/hooks/use-media-query";
import { cn } from "~/lib/utils";
import type { ChatsChatid } from "~/schemas/public/Chats";
import type { UsersId } from "~/schemas/public/Users";
import type { ActiveChatsType } from "~/server/controllers/chat.controller";
import type { NonNullableUser } from "~/server/controllers/user.controller";
import { api } from "~/utils/api";
export const ChatSidebar = ({
chatId,
userId,
}: {
chatId: ChatsChatid | null;
userId: UsersId;
}) => {
const utils = api.useUtils();
api.chatSSE.onSidebarUpdate.useSubscription(undefined, {
onData() {
void utils.chat.getActiveChats.invalidate();
},
});
const { data: activeChats, isLoading: loadingActiveChats } =
api.chat.getActiveChats.useQuery(undefined, {
refetchOnWindowFocus: true,
});
const { data: inactiveUsers, isLoading: loadingInactiveUsers } =
api.users.getActiveUsersWithoutChat.useQuery();
const router = useRouter();
const isDesktop = useMediaQuery("(min-width: 768px)");
const [selectedChat, setSelectedChat] = useState<ChatsChatid | null>(null);
const [addDialogOpen, setAddDialogOpen] = useState(false);
const [searchDialogOpen, setSearchDialogOpen] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [editEtichettaModalOpen, setEditEtichettaModalOpen] = useState(false);
const { mutate: deletechat } = api.chat.deleteChat.useMutation({
onSuccess: async () => {
await utils.chat.getActiveChats.invalidate();
await utils.users.getActiveUsersWithoutChat.invalidate();
},
});
const handleChatSelection = useCallback(
async (new_chatid: ChatsChatid) => {
if (chatId === new_chatid) return;
await router.push(`/area-riservata/admin/chats/${new_chatid}`);
setSearchDialogOpen(false);
},
[chatId, router],
);
if (loadingActiveChats || loadingInactiveUsers) return <LoadingPage />;
if (!activeChats || !inactiveUsers) return <div>Errore caricamento</div>;
return (
<div className="group relative flex h-full flex-col gap-4 py-2 data-[collapsed=true]:p-2">
<div className="mx-2 flex flex-col items-center justify-between gap-2 sm:flex-row sm:gap-0">
{isDesktop ? (
<div className="relative">
<Search className="absolute top-[0.8rem] left-2 size-4 text-muted-foreground" />
<label className="sr-only" htmlFor="search">
Search Chats
</label>
<Input className="m-0 pl-8" id="search" placeholder="Search" />
</div>
) : (
<Button
aria-label="Open Search Dialog"
className="size-8"
onClick={() => setSearchDialogOpen(true)}
size="icon"
variant="ghost"
>
<Search size={20} />
</Button>
)}
<SearchUserComponent
activeChats={activeChats}
handleChatSelection={handleChatSelection}
isOpen={searchDialogOpen}
OpenChange={setSearchDialogOpen}
/>
<Button
aria-label="Open Add User Dialog"
className="size-8"
onClick={() => setAddDialogOpen(true)}
size="icon"
variant="ghost"
>
<SquarePen size={20} />
</Button>
<NewUserComponent
inactiveUsers={inactiveUsers}
isOpen={addDialogOpen}
OpenChange={setAddDialogOpen}
/>
</div>
<nav className="grid gap-1 rounded-lg group-data-[collapsed=true]:justify-center group-data-[collapsed=true]:px-2">
{activeChats.map((chat) => (
<ContextMenu key={chat.username}>
<ContextMenuTrigger asChild>
<Button
aria-label="Select Chat"
className={cn(
buttonVariants({
size: !isDesktop ? "icon" : "xl",
variant:
chat.lastmessage &&
!chat.lastmessage.isread &&
chat.lastmessage.sender !== userId
? "secondary"
: "grey",
}),
!isDesktop ? "p-1" : "justify-start gap-4",
"h-fit w-full",
)}
onClick={() => handleChatSelection(chat.chatid)}
>
<UserAvatar
className="size-10"
userId={chat.id}
username={chat.username}
/>
{isDesktop && (
<div className="flex h-full max-w-28 flex-col text-left align-top">
<span>{chat.username}</span>
<div className="flex flex-wrap gap-2">
{chat.etichette.map((etichetta) => (
<Etichetta
className="text-xs"
color_hex={etichetta.color_hex}
key={etichetta.id_etichetta}
title={etichetta.title}
/>
))}
</div>
{chat.lastmessage && (
<>
<div className="flex items-center gap-2">
<span className="truncate text-muted-foreground text-xs">
{chat.lastmessage.sender === userId ? "Tu: " : ""}
{chat.lastmessage.message}
</span>
{chat.lastmessage.sender === userId &&
(chat.lastmessage.isread ? (
<CheckCheck className="size-4 text-green-500" />
) : (
<Check className="size-4" />
))}
</div>
<span className="truncate text-muted-foreground text-xs">
{new Date(chat.lastmessage.time).toLocaleString(
"it",
{
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
month: "2-digit",
year: "numeric",
},
)}
</span>
</>
)}
</div>
)}
</Button>
</ContextMenuTrigger>
<ChatSidebarContextMenu
chatid={chat.chatid}
setDeleteModalOpen={setDeleteModalOpen}
setEditEtichettaModalOpen={setEditEtichettaModalOpen}
setSelectedChat={setSelectedChat}
/>
</ContextMenu>
))}
{selectedChat && (
<>
<AlertDialog
onOpenChange={setDeleteModalOpen}
open={deleteModalOpen}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Sei assolutamente sicuro?</AlertDialogTitle>
<AlertDialogDescription>
Questa azione non può essere annullata. Questo eliminerà
definitivamente la chat e rimuoverà i dati dai nostri
server.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annulla</AlertDialogCancel>
<AlertDialogAction
aria-label="Delete Chat"
className={cn(buttonVariants({ variant: "destructive" }))}
onClick={() => {
if (selectedChat) {
deletechat({
chatId: selectedChat,
});
}
}}
>
Continua
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<EtichetteModal
chatId={selectedChat}
onOpenChange={setEditEtichettaModalOpen}
open={editEtichettaModalOpen}
/>
</>
)}
</nav>
</div>
);
};
const SearchUserComponent = ({
isOpen,
OpenChange,
activeChats,
handleChatSelection,
}: {
isOpen: boolean;
OpenChange: (status: boolean) => void;
activeChats: ActiveChatsType[];
handleChatSelection: (chatId: ChatsChatid) => void;
}) => {
return (
<CommandDialog onOpenChange={OpenChange} open={isOpen}>
<CommandInput
className="border-none ring-0 focus:ring-0 focus-visible:ring-0"
placeholder="Digita il nome utente..."
/>
<CommandList>
<CommandEmpty>Nessun risultato</CommandEmpty>
<CommandGroup
className="pb-2"
heading={activeChats.length > 0 ? "Suggerimenti" : undefined}
>
{activeChats?.map((chat) => (
<CommandItem
className="rounded-md"
key={chat.id}
onSelect={() => {
handleChatSelection(chat.chatid);
}}
>
<span className="flex items-center gap-2">
<UserAvatar
className="size-8"
userId={chat.id}
username={chat.username}
/>
<span>
<span className="block font-medium">{chat.username}</span>
</span>
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
);
};
const NewUserComponent = ({
isOpen,
OpenChange,
inactiveUsers,
}: {
isOpen: boolean;
OpenChange: (status: boolean) => void;
inactiveUsers: NonNullableUser[];
}) => {
const utils = api.useUtils();
const { mutate: createNewChat } = api.chatSSE.createChat.useMutation({
onSuccess: async () => {
await utils.chat.getActiveChats.invalidate();
OpenChange(false);
},
});
return (
<CommandDialog onOpenChange={OpenChange} open={isOpen}>
<CommandInput
className="border-none ring-0 focus:ring-0 focus-visible:ring-0"
placeholder="Digita il nome utente..."
/>
<CommandList>
<CommandEmpty>Nessun risultato</CommandEmpty>
<CommandGroup
className="pb-2"
heading={inactiveUsers.length > 0 ? "Suggerimenti" : undefined}
>
{inactiveUsers?.map((user) => (
<CommandItem
className="rounded-md"
key={user.id}
onSelect={() => {
createNewChat({
userId: user.id,
});
}}
>
<span className="flex items-center gap-2">
<UserAvatar
className="size-8"
userId={user.id}
username={user.username}
/>
<span>
<span className="block font-medium">{user.username}</span>
</span>
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</CommandDialog>
);
};
const ChatSidebarContextMenu = ({
chatid,
setSelectedChat,
setDeleteModalOpen,
setEditEtichettaModalOpen,
}: {
chatid: ChatsChatid;
setSelectedChat: (chat: ChatsChatid | null) => void;
setDeleteModalOpen: (status: boolean) => void;
setEditEtichettaModalOpen: (status: boolean) => void;
}) => {
const { data: chatData } = api.chat.getChatInfobyId.useQuery({
chatId: chatid,
});
if (!chatData) return null;
return (
<>
<ContextMenuContent>
<ContextMenuSub>
<ContextMenuSubTrigger>Etichette</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-48">
<ContextMenuItem asChild>
<button
aria-label="Edit Chat Labels"
className="w-full cursor-pointer"
onClick={() => {
setSelectedChat(chatid);
setEditEtichettaModalOpen(true);
}}
type="button"
>
Modifica
</button>
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem>
<Link
aria-label="View User Profile"
href={`/area-riservata/admin/user-view/edit-user/${chatData.user_ref}`}
target="_blank"
>
Profilo
</Link>
</ContextMenuItem>
<ContextMenuItem>
<Link
aria-label="View User Orders"
href={`/area-riservata/admin/user-view/ordini/${chatData.user_ref}`}
target="_blank"
>
Ordini
</Link>
</ContextMenuItem>
<ContextMenuItem>
<Link
aria-label="View User Attachments"
href={`/area-riservata/admin/user-view/allegati/${chatData.user_ref}`}
target="_blank"
>
Allegati
</Link>
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>Altro</ContextMenuSubTrigger>
<ContextMenuSubContent className="w-48">
<ContextMenuItem
aria-label="Edit Chat"
className="cursor-pointer rounded-md bg-red-500 text-white hover:bg-red-500/90 focus:bg-red-500/90 focus:text-white"
onClick={() => {
setSelectedChat(chatid);
setDeleteModalOpen(true);
}}
>
Elimina Chat
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
</ContextMenuContent>
</>
);
};