2026-04-27 16:52:32 +02:00
|
|
|
|
import { format, isSameDay } from "date-fns";
|
2026-04-22 19:17:54 +02:00
|
|
|
|
import { AnimatePresence, motion } from "framer-motion";
|
2026-04-27 16:52:32 +02:00
|
|
|
|
import { Copy, Forward, Pencil, Reply, Trash2 } from "lucide-react";
|
|
|
|
|
|
import { useRouter } from "next/router";
|
2026-04-22 19:17:54 +02:00
|
|
|
|
import {
|
|
|
|
|
|
type FormEvent,
|
|
|
|
|
|
useCallback,
|
|
|
|
|
|
useEffect,
|
|
|
|
|
|
useRef,
|
|
|
|
|
|
useState,
|
|
|
|
|
|
} from "react";
|
|
|
|
|
|
import toast from "react-hot-toast";
|
2026-04-28 14:05:54 +02:00
|
|
|
|
import { ChatAttachments } from "~/components/chat/chat-attachments";
|
2026-04-22 19:17:54 +02:00
|
|
|
|
import {
|
|
|
|
|
|
ChatBubble,
|
|
|
|
|
|
ChatBubbleMessage,
|
|
|
|
|
|
ChatBubbleReadStatus,
|
|
|
|
|
|
} from "~/components/chat/chat-bubble";
|
|
|
|
|
|
import LoadingButton from "~/components/custom_ui/loading-button";
|
2026-04-27 16:52:32 +02:00
|
|
|
|
import { LoadingPage } from "~/components/loading";
|
2026-04-22 19:17:54 +02:00
|
|
|
|
import {
|
|
|
|
|
|
AlertDialog,
|
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
|
} from "~/components/ui/alert-dialog";
|
|
|
|
|
|
import { Button } from "~/components/ui/button";
|
2026-04-27 16:52:32 +02:00
|
|
|
|
import {
|
|
|
|
|
|
CommandDialog,
|
|
|
|
|
|
CommandEmpty,
|
|
|
|
|
|
CommandGroup,
|
|
|
|
|
|
CommandInput,
|
|
|
|
|
|
CommandItem,
|
|
|
|
|
|
CommandList,
|
|
|
|
|
|
} from "~/components/ui/command";
|
2026-04-22 19:17:54 +02:00
|
|
|
|
import {
|
|
|
|
|
|
ContextMenu,
|
|
|
|
|
|
ContextMenuContent,
|
|
|
|
|
|
ContextMenuItem,
|
|
|
|
|
|
ContextMenuTrigger,
|
|
|
|
|
|
} from "~/components/ui/context-menu";
|
|
|
|
|
|
import {
|
|
|
|
|
|
Dialog,
|
|
|
|
|
|
DialogContent,
|
|
|
|
|
|
DialogDescription,
|
|
|
|
|
|
DialogHeader,
|
|
|
|
|
|
DialogTitle,
|
|
|
|
|
|
} from "~/components/ui/dialog";
|
|
|
|
|
|
import Input from "~/components/ui/input";
|
|
|
|
|
|
import { Textarea } from "~/components/ui/textarea";
|
|
|
|
|
|
import { getUserColorV2, UserAvatar } from "~/components/user_avatar";
|
2026-04-27 16:52:32 +02:00
|
|
|
|
import { cn } from "~/lib/utils";
|
|
|
|
|
|
import { useChatContext } from "~/providers/ChatProvider";
|
2026-04-22 19:17:54 +02:00
|
|
|
|
import { useTranslation } from "~/providers/I18nProvider";
|
|
|
|
|
|
import type { MessagesMessageid } from "~/schemas/public/Messages";
|
|
|
|
|
|
import type { UsersId } from "~/schemas/public/Users";
|
|
|
|
|
|
import type { Message, TypingData } from "~/server/api/routers/chat_sse";
|
|
|
|
|
|
import { api } from "~/utils/api";
|
|
|
|
|
|
|
2026-04-27 16:52:32 +02:00
|
|
|
|
export const ChatList = () => {
|
|
|
|
|
|
const {
|
|
|
|
|
|
chatId,
|
|
|
|
|
|
session,
|
|
|
|
|
|
liveChat,
|
|
|
|
|
|
inputRef,
|
|
|
|
|
|
setReplyTrg,
|
|
|
|
|
|
messagesContainerRef,
|
|
|
|
|
|
} = useChatContext();
|
|
|
|
|
|
const { messages, query, typingStatus } = liveChat;
|
2026-04-22 19:17:54 +02:00
|
|
|
|
|
|
|
|
|
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
|
|
|
|
|
const [msgSelected, setMsgSelected] = useState<Message | null>(null);
|
|
|
|
|
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
2026-04-27 16:52:32 +02:00
|
|
|
|
const [inoltraModalOpen, setInoltraModalOpen] = useState(false);
|
2026-04-22 19:17:54 +02:00
|
|
|
|
|
|
|
|
|
|
const handleEditOpen = (status: boolean) => {
|
|
|
|
|
|
setEditModalOpen(status);
|
|
|
|
|
|
};
|
|
|
|
|
|
const handleDeleteOpen = (status: boolean) => {
|
|
|
|
|
|
setDeleteModalOpen(status);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const { mutate: deleteMutation } = api.chatSSE.deleteMessage.useMutation({
|
|
|
|
|
|
onError: () => {
|
|
|
|
|
|
toast.error("Error deleting message", { id: "deleteMessage" });
|
|
|
|
|
|
},
|
|
|
|
|
|
onMutate: () => {
|
|
|
|
|
|
toast("Deleting message...", { icon: "🗑️", id: "deleteMessage" });
|
|
|
|
|
|
},
|
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
|
toast("Message deleted", { icon: "🗑️", id: "deleteMessage" });
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
const { mutate: editMutation } = api.chatSSE.editMessage.useMutation({
|
|
|
|
|
|
onError: () => {
|
|
|
|
|
|
toast.error("Error editing message", { id: "editMessage" });
|
|
|
|
|
|
},
|
|
|
|
|
|
onMutate: () => {
|
|
|
|
|
|
toast("Editing message...", { icon: "📝", id: "editMessage" });
|
|
|
|
|
|
},
|
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
|
toast("Message edited", { icon: "📝", id: "editMessage" });
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const handleEditSubmit = useCallback(
|
|
|
|
|
|
(e: FormEvent<HTMLFormElement>) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
const form = e.currentTarget;
|
|
|
|
|
|
const formData = new FormData(form);
|
|
|
|
|
|
|
|
|
|
|
|
const raw_messageId = formData.get("message-id");
|
|
|
|
|
|
const raw_message = formData.get("message-edit");
|
|
|
|
|
|
|
|
|
|
|
|
if (!raw_message || !raw_messageId) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const messageId = raw_messageId.toString() as MessagesMessageid;
|
|
|
|
|
|
const message = raw_message.toString();
|
|
|
|
|
|
editMutation({
|
|
|
|
|
|
chatId,
|
|
|
|
|
|
message,
|
|
|
|
|
|
messageId,
|
|
|
|
|
|
});
|
|
|
|
|
|
setEditModalOpen(false);
|
|
|
|
|
|
},
|
|
|
|
|
|
[editMutation],
|
|
|
|
|
|
);
|
|
|
|
|
|
const [locked, setLocked] = useState(false);
|
2026-04-27 16:52:32 +02:00
|
|
|
|
const focusInputOnCloseRef = useRef(false);
|
2026-04-22 19:17:54 +02:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (
|
|
|
|
|
|
messagesContainerRef.current &&
|
|
|
|
|
|
!locked &&
|
|
|
|
|
|
!query.isLoading &&
|
|
|
|
|
|
!query.isFetching
|
|
|
|
|
|
) {
|
2026-04-27 16:52:32 +02:00
|
|
|
|
messagesContainerRef.current.scrollTo({
|
|
|
|
|
|
top: messagesContainerRef.current.scrollHeight,
|
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
|
});
|
2026-04-22 19:17:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
setLocked(false);
|
|
|
|
|
|
}, [messages, typingStatus]);
|
|
|
|
|
|
|
2026-04-27 16:52:32 +02:00
|
|
|
|
const handleFetchPrevious = useCallback(async () => {
|
|
|
|
|
|
if (query.hasNextPage && !query.isFetchingNextPage) {
|
|
|
|
|
|
setLocked(true);
|
|
|
|
|
|
await query.fetchNextPage();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [query]);
|
|
|
|
|
|
|
|
|
|
|
|
const utils = api.useUtils();
|
|
|
|
|
|
|
|
|
|
|
|
const handleScrollTo = (messageId: MessagesMessageid): boolean => {
|
|
|
|
|
|
const el = document.getElementById(messageId);
|
|
|
|
|
|
if (el) {
|
|
|
|
|
|
el.scrollIntoView({
|
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
|
block: "center",
|
|
|
|
|
|
});
|
|
|
|
|
|
el.classList.add("bg-primary/50");
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
el.classList.remove("bg-primary/50");
|
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
console.warn("Element not found for messageId:", messageId);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleJumpToMessage = async (messageId: MessagesMessageid) => {
|
|
|
|
|
|
const scroll = handleScrollTo(messageId);
|
|
|
|
|
|
if (scroll) return;
|
|
|
|
|
|
|
|
|
|
|
|
const context = await utils.chatSSE.getChat.fetch({
|
|
|
|
|
|
chatId,
|
|
|
|
|
|
targetMsgId: messageId,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
utils.chatSSE.getChat.setInfiniteData({ chatId }, () => {
|
|
|
|
|
|
return {
|
|
|
|
|
|
pages: [context],
|
|
|
|
|
|
pageParams: [null] as (MessagesMessageid | null)[],
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
handleScrollTo(messageId);
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
console.log(query.data);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-22 19:17:54 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full w-full flex-col overflow-y-auto">
|
|
|
|
|
|
<div
|
2026-04-27 16:52:32 +02:00
|
|
|
|
className={cn("flex h-full w-full flex-col overflow-y-auto p-1")}
|
2026-04-22 19:17:54 +02:00
|
|
|
|
ref={messagesContainerRef}
|
|
|
|
|
|
>
|
|
|
|
|
|
<AnimatePresence key="chat-messages">
|
|
|
|
|
|
{query.hasNextPage && (
|
|
|
|
|
|
<LoadingButton
|
|
|
|
|
|
aria-label="Load More Messages"
|
|
|
|
|
|
className="mb-3"
|
|
|
|
|
|
disabled={!query.hasNextPage || query.isFetchingNextPage}
|
|
|
|
|
|
key="load-more"
|
|
|
|
|
|
loading={query.isFetchingNextPage}
|
2026-04-27 16:52:32 +02:00
|
|
|
|
onClick={handleFetchPrevious}
|
2026-04-22 19:17:54 +02:00
|
|
|
|
variant="secondary"
|
|
|
|
|
|
>
|
|
|
|
|
|
{query.isFetchingNextPage
|
|
|
|
|
|
? "Caricamento..."
|
|
|
|
|
|
: "Carica messaggi precedenti"}
|
|
|
|
|
|
</LoadingButton>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{messages.map((message, index) => {
|
|
|
|
|
|
const isNextSameSender =
|
|
|
|
|
|
index + 1 < messages.length &&
|
|
|
|
|
|
messages[index + 1]?.sender === message.sender;
|
|
|
|
|
|
|
2026-04-27 16:52:32 +02:00
|
|
|
|
const isPrevSameDay = (() => {
|
|
|
|
|
|
const prevMsg = messages[index - 1];
|
|
|
|
|
|
if (!prevMsg) return false;
|
|
|
|
|
|
return isSameDay(new Date(prevMsg.time), new Date(message.time));
|
|
|
|
|
|
})();
|
2026-04-22 19:17:54 +02:00
|
|
|
|
const isPreviousSameSender =
|
|
|
|
|
|
index - 1 >= 0 && messages[index - 1]?.sender === message.sender;
|
|
|
|
|
|
|
2026-04-27 16:52:32 +02:00
|
|
|
|
const isSender = message.sender === session.id;
|
2026-04-22 19:17:54 +02:00
|
|
|
|
|
2026-04-27 16:52:32 +02:00
|
|
|
|
const userColor = getUserColorV2(message.color_str);
|
|
|
|
|
|
const reply_userColor = message.reply_source
|
|
|
|
|
|
? getUserColorV2(message.reply_source.color_str)
|
|
|
|
|
|
: undefined;
|
2026-04-22 19:17:54 +02:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-04-27 16:52:32 +02:00
|
|
|
|
<div
|
2026-04-22 19:17:54 +02:00
|
|
|
|
className={cn(
|
2026-04-27 16:52:32 +02:00
|
|
|
|
"flex flex-col gap-0.5 pb-4",
|
2026-04-22 19:17:54 +02:00
|
|
|
|
isNextSameSender && "pb-0.5",
|
|
|
|
|
|
)}
|
|
|
|
|
|
key={message.messageid}
|
|
|
|
|
|
>
|
|
|
|
|
|
{!isPrevSameDay && (
|
2026-04-28 14:05:54 +02:00
|
|
|
|
<span className="mx-auto mt-1 rounded-md bg-primary px-2 py-1 text-center text-secondary text-xs">
|
2026-04-22 19:17:54 +02:00
|
|
|
|
{new Date(message.time).toLocaleDateString("it", {
|
|
|
|
|
|
day: "2-digit",
|
|
|
|
|
|
month: "2-digit",
|
|
|
|
|
|
year: "2-digit",
|
|
|
|
|
|
})}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2026-04-27 16:52:32 +02:00
|
|
|
|
{!isPreviousSameSender && (
|
|
|
|
|
|
<p
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"px-1.5 font-semibold text-sm",
|
|
|
|
|
|
isSender && "text-right",
|
|
|
|
|
|
)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: userColor,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{message.sender_nome}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<ContextMenu>
|
|
|
|
|
|
<ContextMenuTrigger asChild>
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex flex-col gap-2 rounded-md px-1",
|
|
|
|
|
|
"data-[state=open]:bg-muted",
|
|
|
|
|
|
)}
|
|
|
|
|
|
exit={{ opacity: 0, scale: 1, x: 0, y: 1 }}
|
|
|
|
|
|
id={message.messageid}
|
|
|
|
|
|
initial={{ opacity: 0, scale: 1, x: 0, y: 25 }}
|
|
|
|
|
|
layout
|
|
|
|
|
|
style={{ originX: 0.25, originY: 0.25 }}
|
|
|
|
|
|
transition={{
|
|
|
|
|
|
layout: {
|
|
|
|
|
|
bounce: 0.2,
|
|
|
|
|
|
duration: 0.2,
|
|
|
|
|
|
type: "spring",
|
|
|
|
|
|
},
|
|
|
|
|
|
opacity: { duration: 0.1 },
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ChatBubble variant={isSender ? "sent" : "received"}>
|
2026-04-22 19:17:54 +02:00
|
|
|
|
<ChatBubbleMessage
|
2026-04-27 16:52:32 +02:00
|
|
|
|
className={cn(
|
2026-04-28 14:05:54 +02:00
|
|
|
|
"flex flex-col items-end justify-end gap-x-2 gap-y-1",
|
2026-04-27 16:52:32 +02:00
|
|
|
|
isNextSameSender && "rounded-lg",
|
|
|
|
|
|
"",
|
|
|
|
|
|
)}
|
|
|
|
|
|
variant={isSender ? "sent" : "received"}
|
2026-04-22 19:17:54 +02:00
|
|
|
|
>
|
2026-04-27 16:52:32 +02:00
|
|
|
|
{message.reply_source && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="group block w-full cursor-pointer text-left"
|
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
|
if (!message.reply_source) return;
|
|
|
|
|
|
await handleJumpToMessage(
|
|
|
|
|
|
message.reply_source.messageid,
|
|
|
|
|
|
);
|
|
|
|
|
|
}}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="mt-1 w-full rounded-md border-l-4 bg-secondary px-2 py-0.5 text-foreground group-hover:bg-secondary/90"
|
|
|
|
|
|
style={{ borderColor: reply_userColor }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className="font-semibold text-xs"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: reply_userColor,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{message.reply_source.sender === session.id
|
|
|
|
|
|
? "Tu"
|
|
|
|
|
|
: message.reply_source.nome}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="line-clamp-2 block break-all text-xs italic">
|
|
|
|
|
|
{message.reply_source.message}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
2026-04-28 14:05:54 +02:00
|
|
|
|
{message.has_attachments && (
|
|
|
|
|
|
<ChatAttachments messageId={message.messageid} />
|
|
|
|
|
|
)}
|
2026-04-22 19:17:54 +02:00
|
|
|
|
<span className="flex shrink break-all">
|
|
|
|
|
|
{message.message}
|
|
|
|
|
|
</span>
|
2026-04-27 16:52:32 +02:00
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex items-center gap-1",
|
|
|
|
|
|
isSender && "justify-end",
|
2026-04-22 19:17:54 +02:00
|
|
|
|
)}
|
2026-04-27 16:52:32 +02:00
|
|
|
|
>
|
2026-04-22 19:17:54 +02:00
|
|
|
|
{message.time && (
|
2026-04-27 16:52:32 +02:00
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"text-[0.70rem]",
|
|
|
|
|
|
isSender
|
|
|
|
|
|
? "text-muted"
|
|
|
|
|
|
: "text-muted-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{format(new Date(message.time), "HH:mm")}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{isSender && (
|
|
|
|
|
|
<ChatBubbleReadStatus isread={message.isread} />
|
2026-04-22 19:17:54 +02:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</ChatBubbleMessage>
|
2026-04-27 16:52:32 +02:00
|
|
|
|
</ChatBubble>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</ContextMenuTrigger>
|
|
|
|
|
|
<ContextMenuContent
|
|
|
|
|
|
onCloseAutoFocus={(e) => {
|
|
|
|
|
|
if (focusInputOnCloseRef.current) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
inputRef.current?.textArea.focus();
|
|
|
|
|
|
focusInputOnCloseRef.current = false;
|
|
|
|
|
|
if (messagesContainerRef.current) {
|
|
|
|
|
|
messagesContainerRef.current.scrollTo({
|
|
|
|
|
|
top: messagesContainerRef.current.scrollHeight,
|
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ContextMenuItem
|
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
navigator.clipboard.writeText(message.message);
|
|
|
|
|
|
toast.success("Messaggio copiato negli appunti", {
|
|
|
|
|
|
id: "copyMessage",
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Copy />
|
|
|
|
|
|
Copia
|
|
|
|
|
|
</ContextMenuItem>
|
|
|
|
|
|
<ContextMenuItem
|
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setReplyTrg(message);
|
|
|
|
|
|
|
|
|
|
|
|
focusInputOnCloseRef.current = true;
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Reply />
|
|
|
|
|
|
Rispondi
|
|
|
|
|
|
</ContextMenuItem>
|
|
|
|
|
|
{session.isAdmin && (
|
2026-04-22 19:17:54 +02:00
|
|
|
|
<ContextMenuItem
|
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setMsgSelected(message);
|
2026-04-27 16:52:32 +02:00
|
|
|
|
setInoltraModalOpen(true);
|
2026-04-22 19:17:54 +02:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-04-27 16:52:32 +02:00
|
|
|
|
<Forward />
|
|
|
|
|
|
Inoltra
|
2026-04-22 19:17:54 +02:00
|
|
|
|
</ContextMenuItem>
|
2026-04-27 16:52:32 +02:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{(isSender || session.isAdmin) && (
|
2026-04-22 19:17:54 +02:00
|
|
|
|
<ContextMenuItem
|
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setMsgSelected(message);
|
|
|
|
|
|
handleEditOpen(true);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Pencil />
|
|
|
|
|
|
Modifica
|
|
|
|
|
|
</ContextMenuItem>
|
2026-04-27 16:52:32 +02:00
|
|
|
|
)}
|
|
|
|
|
|
{session.isAdmin && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<ContextMenuItem
|
|
|
|
|
|
className="flex items-center gap-2"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
setMsgSelected(message);
|
|
|
|
|
|
handleDeleteOpen(true);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 />
|
|
|
|
|
|
Cancella
|
|
|
|
|
|
</ContextMenuItem>{" "}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</ContextMenuContent>
|
|
|
|
|
|
</ContextMenu>
|
|
|
|
|
|
</div>
|
2026-04-22 19:17:54 +02:00
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
|
|
<Typers
|
|
|
|
|
|
currentTypers={typingStatus}
|
|
|
|
|
|
messageLenght={messages.length}
|
2026-04-27 16:52:32 +02:00
|
|
|
|
userId={session.id}
|
2026-04-22 19:17:54 +02:00
|
|
|
|
/>
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
</div>
|
2026-04-27 16:52:32 +02:00
|
|
|
|
{session.isAdmin && (
|
|
|
|
|
|
<InoltraDialog
|
|
|
|
|
|
isOpen={inoltraModalOpen}
|
|
|
|
|
|
msgSelected={msgSelected}
|
|
|
|
|
|
openChange={(status) => setInoltraModalOpen(status)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-04-22 19:17:54 +02:00
|
|
|
|
<EditMessageDialog
|
|
|
|
|
|
handleEditSubmit={handleEditSubmit}
|
|
|
|
|
|
msgEdit={msgSelected}
|
|
|
|
|
|
onOpenChange={handleEditOpen}
|
|
|
|
|
|
open={editModalOpen}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<DeleteMessageDialog
|
|
|
|
|
|
handleDeleteSubmit={(messageId) => {
|
|
|
|
|
|
deleteMutation({ chatId, messageId });
|
|
|
|
|
|
}}
|
|
|
|
|
|
msgDelete={msgSelected}
|
|
|
|
|
|
onOpenChange={handleDeleteOpen}
|
|
|
|
|
|
open={deleteModalOpen}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const Typers = ({
|
|
|
|
|
|
currentTypers,
|
|
|
|
|
|
messageLenght,
|
|
|
|
|
|
userId,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
currentTypers: TypingData;
|
|
|
|
|
|
messageLenght: number;
|
|
|
|
|
|
userId: UsersId;
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{currentTypers.map((typer) => {
|
|
|
|
|
|
if (typer.userId === userId) return null;
|
|
|
|
|
|
const variant = "received";
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
|
2026-04-27 16:52:32 +02:00
|
|
|
|
className="flex flex-col gap-2 px-1 py-2"
|
2026-04-22 19:17:54 +02:00
|
|
|
|
exit={{ opacity: 0, scale: 1, x: 0, y: 1 }}
|
|
|
|
|
|
initial={{ opacity: 0, scale: 1, x: 0, y: 50 }}
|
|
|
|
|
|
key={`typers${messageLenght}${typer.userId}`}
|
|
|
|
|
|
layout
|
|
|
|
|
|
style={{ originX: 0.5, originY: 0.5 }}
|
|
|
|
|
|
transition={{
|
|
|
|
|
|
layout: {
|
|
|
|
|
|
bounce: 0.3,
|
|
|
|
|
|
duration: 0.3,
|
|
|
|
|
|
type: "spring",
|
|
|
|
|
|
},
|
|
|
|
|
|
opacity: { duration: 0.1 },
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<ChatBubble variant={variant}>
|
2026-04-27 16:52:32 +02:00
|
|
|
|
<ChatBubbleMessage className="text-muted-foreground text-xs">
|
|
|
|
|
|
✏️ {typer.username} sta scrivendo...
|
|
|
|
|
|
</ChatBubbleMessage>
|
2026-04-22 19:17:54 +02:00
|
|
|
|
</ChatBubble>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const EditMessageDialog = ({
|
|
|
|
|
|
open,
|
|
|
|
|
|
onOpenChange,
|
|
|
|
|
|
msgEdit,
|
|
|
|
|
|
handleEditSubmit,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
open: boolean;
|
|
|
|
|
|
onOpenChange: (status: boolean) => void;
|
|
|
|
|
|
msgEdit: Message | null;
|
|
|
|
|
|
handleEditSubmit: (e: FormEvent<HTMLFormElement>) => void;
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Dialog onOpenChange={onOpenChange} open={open}>
|
|
|
|
|
|
<DialogContent>
|
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
|
<DialogTitle>{t.modifica}</DialogTitle>
|
|
|
|
|
|
<DialogDescription className="sr-only">Edit</DialogDescription>
|
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
{msgEdit && (
|
|
|
|
|
|
<form method="post" onSubmit={handleEditSubmit}>
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<label className="sr-only" htmlFor="id">
|
|
|
|
|
|
ID
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
className="hidden"
|
|
|
|
|
|
id="id"
|
|
|
|
|
|
name="message-id"
|
|
|
|
|
|
readOnly
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={msgEdit.messageid}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<label className="sr-only" htmlFor="message-edit">
|
|
|
|
|
|
Modifica messaggio
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<Textarea
|
|
|
|
|
|
className="mt-0 min-h-64"
|
|
|
|
|
|
defaultValue={msgEdit.message || undefined}
|
|
|
|
|
|
id="message-edit"
|
|
|
|
|
|
name="message-edit"
|
|
|
|
|
|
placeholder="Modifica messaggio"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Button type="submit">{t.salva}</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</DialogContent>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const DeleteMessageDialog = ({
|
|
|
|
|
|
open,
|
|
|
|
|
|
onOpenChange,
|
|
|
|
|
|
msgDelete,
|
|
|
|
|
|
handleDeleteSubmit,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
open: boolean;
|
|
|
|
|
|
onOpenChange: (status: boolean) => void;
|
|
|
|
|
|
msgDelete: Message | null;
|
|
|
|
|
|
handleDeleteSubmit: (msgId: MessagesMessageid) => void;
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{msgDelete && (
|
|
|
|
|
|
<AlertDialog onOpenChange={onOpenChange} open={open}>
|
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogTitle>Vuoi eliminare il messaggio?</AlertDialogTitle>
|
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
|
Questa azione non può essere annullata.
|
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
|
<AlertDialogCancel>{t.annulla}</AlertDialogCancel>
|
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
|
aria-label="Delete Message"
|
|
|
|
|
|
onClick={() => handleDeleteSubmit(msgDelete.messageid)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{t.procedi}
|
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-27 16:52:32 +02:00
|
|
|
|
const InoltraDialog = ({
|
|
|
|
|
|
isOpen,
|
|
|
|
|
|
openChange,
|
|
|
|
|
|
msgSelected,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
|
openChange: (status: boolean) => void;
|
|
|
|
|
|
msgSelected: Message | null;
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
const { chatId, session } = useChatContext();
|
|
|
|
|
|
const { data: activeChats, isLoading } = api.chat.getActiveChats.useQuery(
|
|
|
|
|
|
undefined,
|
|
|
|
|
|
{
|
|
|
|
|
|
enabled: isOpen,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
const { mutate } = api.chatSSE.sendMessage.useMutation({
|
|
|
|
|
|
onError: () => {
|
|
|
|
|
|
toast.error("Errore nell'inoltro del messaggio", {
|
|
|
|
|
|
id: "forwardMessage",
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
onMutate: () => {
|
|
|
|
|
|
toast("Inoltrando messaggio...", { icon: "📨", id: "forwardMessage" });
|
|
|
|
|
|
},
|
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
|
toast("Messaggio inoltrato", { icon: "📨", id: "forwardMessage" });
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
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>
|
|
|
|
|
|
{isLoading && <LoadingPage />}
|
|
|
|
|
|
<CommandGroup className="pb-2" heading={"Suggerimenti"}>
|
|
|
|
|
|
{activeChats
|
|
|
|
|
|
?.filter((chat) => chat.chatid !== chatId)
|
|
|
|
|
|
.map((chat) => (
|
|
|
|
|
|
<CommandItem
|
|
|
|
|
|
className="cursor-pointer rounded-md"
|
|
|
|
|
|
key={chat.id}
|
|
|
|
|
|
onSelect={() => {
|
|
|
|
|
|
if (!msgSelected) return;
|
|
|
|
|
|
mutate({
|
|
|
|
|
|
chatId: chat.chatid,
|
|
|
|
|
|
message: msgSelected.message,
|
|
|
|
|
|
sender: session.id,
|
|
|
|
|
|
is_forwarded: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
router.push(`/area-riservata/admin/chats/${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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|