rem
This commit is contained in:
parent
27fa96b698
commit
f5d1530683
9 changed files with 0 additions and 1841 deletions
|
|
@ -1,196 +0,0 @@
|
|||
"use client";
|
||||
import { differenceInDays, differenceInMinutes, format } from "date-fns";
|
||||
import { Paperclip } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ExtIcon } from "~/components/area-riservata/allegati";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle,
|
||||
CredenzaTrigger,
|
||||
} from "~/components/custom_ui/credenza";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import { Button, buttonVariants } from "~/components/ui/button";
|
||||
import { UploadComponent } from "~/components/upload_modal";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import type { Session } from "~/server/api/trpc";
|
||||
import type { ChatUserInfo } from "~/server/services/chat.service";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
type ChatAttachmentsProps = {
|
||||
userData: Session;
|
||||
chatUserData: ChatUserInfo;
|
||||
};
|
||||
export const ChatAttachments = ({
|
||||
userData,
|
||||
chatUserData,
|
||||
}: ChatAttachmentsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data: attachments,
|
||||
isLoading: attachmentsLoading,
|
||||
refetch,
|
||||
} = api.storage.retrieveUserFileData.useQuery(
|
||||
{ userId: chatUserData.id },
|
||||
{ refetchOnMount: true },
|
||||
);
|
||||
|
||||
return (
|
||||
<Credenza onOpenChange={(o) => o && refetch()}>
|
||||
<CredenzaTrigger asChild>
|
||||
<Button
|
||||
aria-label="Allegati Chat"
|
||||
className="relative size-9"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
{attachments && attachments.length > 0 && (
|
||||
<span className="absolute -top-1 right-0 rounded-full bg-blue-500 px-1 font-bold text-white text-xs">
|
||||
{attachments.length}
|
||||
</span>
|
||||
)}
|
||||
<Paperclip className="text-muted-foreground" size={20} />
|
||||
</Button>
|
||||
</CredenzaTrigger>
|
||||
<CredenzaContent className="max-h-[90vh] md:max-w-xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle className="sr-only">Allegati</CredenzaTitle>
|
||||
<CredenzaDescription className="sr-only">
|
||||
Chat Attachments
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody className="max-h-[80vh] space-y-5 overflow-auto pb-5 md:px-1">
|
||||
<UploadComponent
|
||||
isAdmin={userData.isAdmin}
|
||||
userId={chatUserData.id}
|
||||
/>
|
||||
|
||||
<div className="h-auto max-h-160 max-w-lg overflow-auto rounded-lg">
|
||||
{attachmentsLoading ? (
|
||||
<LoadingPage />
|
||||
) : (
|
||||
<>
|
||||
{!attachments || attachments.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="text-center font-semibold text-gray-500 text-lg">
|
||||
{t.allegati.nessun_allegato}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="px-2">
|
||||
<h3 className="font-semibold text-gray-700 text-lg">
|
||||
{t.allegati.allegati_recenti}
|
||||
</h3>
|
||||
{attachments?.map((file, idx) => {
|
||||
if (idx >= 3) return null;
|
||||
|
||||
return (
|
||||
<li
|
||||
className="flex flex-row items-center justify-between border-b py-1"
|
||||
key={`${file.originalName}-${
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <ok>
|
||||
idx
|
||||
}`}
|
||||
>
|
||||
<Link
|
||||
aria-label="Visualizza Allegato"
|
||||
href={`/area-riservata/allegato-view/${file.id}`}
|
||||
onTouchStart={() => console.log("touching")}
|
||||
target="_blank"
|
||||
>
|
||||
<Button
|
||||
className="h-8 truncate p-0 text-base has-[>svg]:px-0"
|
||||
variant="link"
|
||||
>
|
||||
<ExtIcon mimeType={file.mimeType} />
|
||||
<span className="max-w-[60vw] truncate">
|
||||
{file.originalName}
|
||||
</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<span
|
||||
className="text-muted-foreground text-xs"
|
||||
key={new Date().toString()}
|
||||
>
|
||||
{TimeSince(new Date(file.uploadedAt))}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
aria-label="Visualizza Allegati"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: "outline",
|
||||
}),
|
||||
"relative w-full",
|
||||
)}
|
||||
href="/area-riservata/allegati"
|
||||
>
|
||||
<Paperclip /> {t.allegati.i_tuoi_allegati}
|
||||
{attachments && attachments.length > 0 && (
|
||||
<span className="absolute -top-1 -right-1 flex size-5 items-center justify-center rounded-full bg-red-600 text-white text-xs">
|
||||
{attachments.length}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter className="flex flex-col items-center justify-between gap-2">
|
||||
<CredenzaClose asChild>
|
||||
<Button className="w-full">{t.chiudi}</Button>
|
||||
</CredenzaClose>
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
};
|
||||
|
||||
const TimeSince = (date: Date) => {
|
||||
const minsSince = Math.abs(differenceInMinutes(date, new Date()));
|
||||
const daysSince = Math.abs(differenceInDays(date, new Date()));
|
||||
|
||||
const relevantMinsIncrements = [1, 3, 5, 10, 15, 30, 60, 120];
|
||||
|
||||
if (daysSince === 0) {
|
||||
if (minsSince < 1) {
|
||||
return "ora";
|
||||
}
|
||||
|
||||
const closestToNow = relevantMinsIncrements.reduce((prev, curr) => {
|
||||
return Math.abs(curr - minsSince) < Math.abs(prev - minsSince)
|
||||
? curr
|
||||
: prev;
|
||||
});
|
||||
|
||||
if (closestToNow === 60) {
|
||||
return "~1 ora fa";
|
||||
}
|
||||
|
||||
if (closestToNow === 120 && minsSince < 121) {
|
||||
return "~2 ore fa";
|
||||
}
|
||||
|
||||
return `~${closestToNow} min fa`;
|
||||
}
|
||||
|
||||
if (daysSince === 1) {
|
||||
return "ieri";
|
||||
}
|
||||
|
||||
if (daysSince < 30) {
|
||||
return `${daysSince} giorni fa`;
|
||||
}
|
||||
|
||||
return format(date, "dd/MM/yyyy");
|
||||
};
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { SendHorizontal, SmilePlusIcon, ThumbsUp } from "lucide-react";
|
||||
import {
|
||||
type ChangeEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { ChatAttachments } from "~/components/chat/chat-attachments";
|
||||
import {
|
||||
type AutosizeTextAreaRef,
|
||||
AutosizeTextarea,
|
||||
} from "~/components/custom_ui/autoResizeTextArea";
|
||||
import {
|
||||
EmojiPicker,
|
||||
EmojiPickerContent,
|
||||
EmojiPickerSearch,
|
||||
} from "~/components/custom_ui/emoji-picker";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "~/components/ui/popover";
|
||||
import { useThrottledIsTypingMutation } from "~/hooks/chatHooks";
|
||||
import { useMediaQuery } from "~/hooks/use-media-query";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||
import type { Session } from "~/server/api/trpc";
|
||||
import type { ChatUserInfo } from "~/server/services/chat.service";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
type ChatBottombarProps = {
|
||||
chatId: ChatsChatid;
|
||||
userData: Session;
|
||||
chatUserData: ChatUserInfo;
|
||||
};
|
||||
|
||||
export default function ChatBottombar({
|
||||
chatId,
|
||||
userData,
|
||||
chatUserData,
|
||||
}: ChatBottombarProps) {
|
||||
const { locale } = useTranslation();
|
||||
const [message, setMessage] = useState("");
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const inputRef = useRef<AutosizeTextAreaRef>(null);
|
||||
|
||||
const { mutate: addMutation } = api.messagesSSE.add.useMutation({});
|
||||
|
||||
const handleThumbsUp = useCallback(() => {
|
||||
addMutation({
|
||||
chatId,
|
||||
message: "👍",
|
||||
sender: userData.id,
|
||||
});
|
||||
setMessage("");
|
||||
}, [chatId, addMutation, userData]);
|
||||
|
||||
const handleSend = () => {
|
||||
if (message.length !== 0) {
|
||||
addMutation({
|
||||
chatId,
|
||||
message: message.trim(),
|
||||
sender: userData.id,
|
||||
});
|
||||
|
||||
setMessage("");
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.textArea.focus();
|
||||
inputRef.current.textArea.style.height = "auto";
|
||||
}
|
||||
}
|
||||
};
|
||||
const isTypingMutation = useThrottledIsTypingMutation({
|
||||
chatId,
|
||||
username: userData.username,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// update isTyping state
|
||||
isTypingMutation(isFocused && message.trim().length > 0);
|
||||
}, [isFocused, message, isTypingMutation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.textArea.focus();
|
||||
}
|
||||
}, []);
|
||||
const [openEmoji, setOpenEmoji] = useState(false);
|
||||
|
||||
const isMobile = useMediaQuery("(min-width: 426px)");
|
||||
return (
|
||||
<div className="flex min-h-18.5 w-full items-center justify-between gap-2 p-2">
|
||||
<div className="flex">
|
||||
<ChatAttachments chatUserData={chatUserData} userData={userData} />
|
||||
</div>
|
||||
<div className="relative w-full">
|
||||
<AutosizeTextarea
|
||||
autoComplete="off"
|
||||
className="flex h-14 min-h-14 w-full resize-none items-center overflow-hidden rounded-xl border bg-background pr-8 outline-hidden ring-0 hover:border-neutral-400 focus:border-neutral-600 focus:ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
minHeight={56}
|
||||
name="message"
|
||||
onBlur={() => setIsFocused(false)}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setMessage(e.target.value);
|
||||
}}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
setMessage((prev) => `${prev}\n`);
|
||||
} else {
|
||||
handleSend();
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder="Aa"
|
||||
ref={inputRef}
|
||||
value={message}
|
||||
/>
|
||||
{isMobile && (
|
||||
<div className="absolute top-0 right-2 flex h-full items-center">
|
||||
<Popover onOpenChange={setOpenEmoji} open={openEmoji}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost">
|
||||
<SmilePlusIcon className="stroke-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-fit p-0">
|
||||
<EmojiPicker
|
||||
className="h-84"
|
||||
locale={locale as "it" | "en"}
|
||||
onEmojiSelect={({ emoji }) => {
|
||||
setMessage(message + emoji);
|
||||
// if (inputRef.current) {
|
||||
// inputRef.current.textArea.focus();
|
||||
// }
|
||||
}}
|
||||
>
|
||||
<EmojiPickerSearch
|
||||
placeholder={locale === "en" ? "Search..." : "Cerca..."}
|
||||
/>
|
||||
<EmojiPickerContent />
|
||||
</EmojiPicker>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
aria-label="Send Message"
|
||||
className="shrink-0"
|
||||
onClick={
|
||||
message.length !== 0 ? () => handleSend() : () => handleThumbsUp()
|
||||
}
|
||||
variant="ghost"
|
||||
>
|
||||
{message.length !== 0 ? (
|
||||
<SendHorizontal className="text-muted-foreground" size={20} />
|
||||
) : (
|
||||
<ThumbsUp className="text-muted-foreground" size={20} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Check, CheckCheck } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import MessageLoading from "~/components/chat/message-loading";
|
||||
import { Button, type ButtonProps } from "~/components/ui/button";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
// ChatBubble
|
||||
const chatBubbleVariant = cva(
|
||||
"flex gap-2 max-w-[60%] items-end relative group",
|
||||
{
|
||||
defaultVariants: {
|
||||
layout: "default",
|
||||
variant: "received",
|
||||
},
|
||||
variants: {
|
||||
layout: {
|
||||
ai: "max-w-full w-full items-center",
|
||||
default: "",
|
||||
},
|
||||
variant: {
|
||||
received: "self-start",
|
||||
sent: "self-end flex-row-reverse",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface ChatBubbleProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof chatBubbleVariant> {}
|
||||
|
||||
const ChatBubble = React.forwardRef<HTMLDivElement, ChatBubbleProps>(
|
||||
({ className, variant, layout, children, ...props }, ref) => (
|
||||
<div
|
||||
className={cn(
|
||||
chatBubbleVariant({ className, layout, variant }),
|
||||
"group relative",
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{React.Children.map(children, (child) =>
|
||||
React.isValidElement(child) && typeof child.type !== "string"
|
||||
? React.cloneElement(child, {
|
||||
layout,
|
||||
variant,
|
||||
} as Partial<React.ComponentProps<typeof child.type>>)
|
||||
: child,
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
ChatBubble.displayName = "ChatBubble";
|
||||
|
||||
type ChatBubbleReadStatusProps = {
|
||||
isread: boolean;
|
||||
};
|
||||
|
||||
export const ChatBubbleReadStatus = ({ isread }: ChatBubbleReadStatusProps) => {
|
||||
if (isread) {
|
||||
return <CheckCheck className="mt-2 size-4 text-green-500" />;
|
||||
}
|
||||
return <Check className="mt-2 size-4 text-muted-foreground" />;
|
||||
};
|
||||
|
||||
// ChatBubbleMessage
|
||||
const chatBubbleMessageVariants = cva("p-2", {
|
||||
defaultVariants: {
|
||||
layout: "default",
|
||||
variant: "received",
|
||||
},
|
||||
variants: {
|
||||
layout: {
|
||||
ai: "border-t w-full rounded-none bg-transparent",
|
||||
default: "",
|
||||
},
|
||||
variant: {
|
||||
received:
|
||||
"bg-secondary text-secondary-foreground rounded-r-lg rounded-tl-lg",
|
||||
sent: "bg-primary text-primary-foreground rounded-l-lg rounded-tr-lg",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
interface ChatBubbleMessageProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof chatBubbleMessageVariants> {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const ChatBubbleMessage = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChatBubbleMessageProps
|
||||
>(
|
||||
(
|
||||
{ className, variant, layout, isLoading = false, children, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<div
|
||||
className={cn(
|
||||
chatBubbleMessageVariants({ className, layout, variant }),
|
||||
"wrap-break-word max-w-full whitespace-pre-wrap",
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<MessageLoading />
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
ChatBubbleMessage.displayName = "ChatBubbleMessage";
|
||||
|
||||
// ChatBubbleTimestamp
|
||||
interface ChatBubbleTimestampProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
const ChatBubbleTimestamp: React.FC<ChatBubbleTimestampProps> = ({
|
||||
timestamp,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div className={cn("mt-2 text-right text-xs", className)} {...props}>
|
||||
{timestamp}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ChatBubbleAction
|
||||
type ChatBubbleActionProps = ButtonProps & {
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
const ChatBubbleAction: React.FC<ChatBubbleActionProps> = ({
|
||||
icon,
|
||||
onClick,
|
||||
className,
|
||||
variant = "ghost",
|
||||
size = "icon",
|
||||
...props
|
||||
}) => (
|
||||
<Button
|
||||
aria-label="Chat Bubble Action"
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
size={size}
|
||||
variant={variant}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
);
|
||||
|
||||
interface ChatBubbleActionWrapperProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: "sent" | "received";
|
||||
className?: string;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
const ChatBubbleActionWrapper = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChatBubbleActionWrapperProps
|
||||
>(({ variant, className, show, children, ...props }, ref) => (
|
||||
<>
|
||||
{show && (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-1/2 flex -translate-y-1/2 opacity-0 transition-opacity duration-200 group-hover:opacity-100",
|
||||
variant === "sent"
|
||||
? "-left-1 -translate-x-full flex-row-reverse"
|
||||
: "-right-1 translate-x-full",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
));
|
||||
ChatBubbleActionWrapper.displayName = "ChatBubbleActionWrapper";
|
||||
|
||||
export {
|
||||
ChatBubble,
|
||||
ChatBubbleMessage,
|
||||
ChatBubbleTimestamp,
|
||||
ChatBubbleAction,
|
||||
ChatBubbleActionWrapper,
|
||||
};
|
||||
|
|
@ -1,439 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import {
|
||||
type FormEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import {
|
||||
ChatBubble,
|
||||
ChatBubbleAction,
|
||||
ChatBubbleActionWrapper,
|
||||
ChatBubbleMessage,
|
||||
ChatBubbleReadStatus,
|
||||
ChatBubbleTimestamp,
|
||||
} from "~/components/chat/chat-bubble";
|
||||
import { ChatMessageList } from "~/components/chat/chat-message-list";
|
||||
import LoadingButton from "~/components/custom_ui/loading-button";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "~/components/ui/alert-dialog";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog";
|
||||
import Input from "~/components/ui/input";
|
||||
import { getUserColor, UserAvatar } from "~/components/user_avatar";
|
||||
import type { LiveChat } from "~/hooks/chatHooks";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||
import type { MessagesMessageid } from "~/schemas/public/Messages";
|
||||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import type { Session } from "~/server/api/trpc";
|
||||
import type { ChatMessage } from "~/server/services/messages.service";
|
||||
import type { WhoIsTyping } from "~/server/sse";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
type ChatListProps = {
|
||||
chatId: ChatsChatid;
|
||||
messages: ChatMessage[];
|
||||
userData: Session;
|
||||
query: LiveChat["query"];
|
||||
typingStatus: WhoIsTyping[];
|
||||
};
|
||||
|
||||
export const ChatList = ({
|
||||
chatId,
|
||||
messages,
|
||||
userData,
|
||||
query,
|
||||
typingStatus,
|
||||
}: ChatListProps) => {
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [msgSelected, setMsgSelected] = useState<ChatMessage | null>(null);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
|
||||
const handleEditOpen = (status: boolean) => {
|
||||
setEditModalOpen(status);
|
||||
};
|
||||
const handleDeleteOpen = (status: boolean) => {
|
||||
setDeleteModalOpen(status);
|
||||
};
|
||||
|
||||
const { mutate: deleteMutation } = api.messagesSSE.delete.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.messagesSSE.edit.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);
|
||||
if (!formData.get("message-edit") || !formData.get("message-id")) {
|
||||
return;
|
||||
}
|
||||
editMutation({
|
||||
chatId,
|
||||
message: formData.get("message-edit") as string,
|
||||
messageId: formData.get("message-id") as MessagesMessageid,
|
||||
});
|
||||
setEditModalOpen(false);
|
||||
},
|
||||
[editMutation],
|
||||
);
|
||||
const [locked, setLocked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
messagesContainerRef.current &&
|
||||
!locked &&
|
||||
!query.isLoading &&
|
||||
!query.isFetching
|
||||
) {
|
||||
messagesContainerRef.current.scrollTop =
|
||||
messagesContainerRef.current.scrollHeight;
|
||||
}
|
||||
setLocked(false);
|
||||
}, [messages, typingStatus]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||
<ChatMessageList 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}
|
||||
onClick={async () => {
|
||||
setLocked(true);
|
||||
await query.fetchNextPage();
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
{query.isFetchingNextPage
|
||||
? "Caricamento..."
|
||||
: "Carica messaggi precedenti"}
|
||||
</LoadingButton>
|
||||
)}
|
||||
|
||||
{messages.map((message, index) => {
|
||||
const variant = getMessageVariant(message.sender, userData.id);
|
||||
|
||||
const isNextSameSender =
|
||||
index + 1 < messages.length &&
|
||||
messages[index + 1]?.sender === message.sender;
|
||||
|
||||
const isPreviousSameSender =
|
||||
index - 1 >= 0 && messages[index - 1]?.sender === message.sender;
|
||||
|
||||
const isPrevSameDay =
|
||||
index - 1 >= 0 &&
|
||||
// biome-ignore lint/style/noNonNullAssertion: <non lo so>
|
||||
new Date(messages[index - 1]!.time).toDateString() ===
|
||||
new Date(message.time).toDateString();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
|
||||
className={cn(
|
||||
"flex flex-col gap-2 pb-4",
|
||||
isNextSameSender && "pb-0.5",
|
||||
)}
|
||||
exit={{ opacity: 0, scale: 1, x: 0, y: 1 }}
|
||||
initial={{ opacity: 0, scale: 1, x: 0, y: 50 }}
|
||||
key={message.messageid}
|
||||
layout
|
||||
style={{ originX: 0.5, originY: 0.5 }}
|
||||
transition={{
|
||||
layout: {
|
||||
bounce: 0.3,
|
||||
duration: 0.3,
|
||||
type: "spring",
|
||||
},
|
||||
opacity: { duration: 0.1 },
|
||||
}}
|
||||
>
|
||||
{!isPrevSameDay && (
|
||||
<span className="mx-auto rounded-md bg-muted px-2 py-1 text-center text-muted-foreground text-xs">
|
||||
{new Date(message.time).toLocaleDateString("it", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
<ChatBubble variant={variant}>
|
||||
{isNextSameSender ? (
|
||||
<div className="size-6"></div>
|
||||
) : (
|
||||
<UserAvatar className="size-6" userId={message.sender} />
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
{!isPreviousSameSender && (
|
||||
<p
|
||||
className={cn(
|
||||
"font-semibold text-sm",
|
||||
variant === "sent" && "text-right",
|
||||
)}
|
||||
style={{
|
||||
color: getUserColor(message.sender),
|
||||
}}
|
||||
>
|
||||
{message.sender_nome}
|
||||
</p>
|
||||
)}
|
||||
<ChatBubbleMessage
|
||||
className={cn(isNextSameSender && "rounded-lg")}
|
||||
isLoading={false}
|
||||
>
|
||||
{message.message}
|
||||
<div className="flex items-center gap-2">
|
||||
{userData.id === message.sender && (
|
||||
<ChatBubbleReadStatus isread={message.isread} />
|
||||
)}
|
||||
{message.time && (
|
||||
<ChatBubbleTimestamp
|
||||
timestamp={new Date(message.time).toLocaleString(
|
||||
"it",
|
||||
{
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
month: "short",
|
||||
},
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ChatBubbleMessage>
|
||||
</div>
|
||||
<ChatBubbleActionWrapper show={userData.isAdmin}>
|
||||
<ChatBubbleAction
|
||||
className="size-7 hover:bg-red-50 hover:text-red-500"
|
||||
icon={<Trash2 className="size-4" />}
|
||||
key={"delete"}
|
||||
onClick={() => {
|
||||
setMsgSelected(message);
|
||||
handleDeleteOpen(true);
|
||||
}}
|
||||
/>
|
||||
<ChatBubbleAction
|
||||
className="size-7 hover:bg-blue-50 hover:text-blue-500"
|
||||
icon={<Pencil className="size-4" />}
|
||||
key={"edit"}
|
||||
onClick={() => {
|
||||
setMsgSelected(message);
|
||||
handleEditOpen(true);
|
||||
}}
|
||||
/>
|
||||
</ChatBubbleActionWrapper>
|
||||
</ChatBubble>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
<Typers
|
||||
currentTypers={typingStatus}
|
||||
messageLenght={messages.length}
|
||||
userId={userData.id}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
</ChatMessageList>
|
||||
<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: WhoIsTyping[];
|
||||
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 }}
|
||||
className="flex flex-col gap-2 py-2"
|
||||
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}>
|
||||
<UserAvatar className="size-6" userId={typer.userId} />
|
||||
|
||||
<ChatBubbleMessage isLoading={true}></ChatBubbleMessage>
|
||||
</ChatBubble>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EditMessageDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
msgEdit,
|
||||
handleEditSubmit,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (status: boolean) => void;
|
||||
msgEdit: ChatMessage | 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>
|
||||
<Input
|
||||
className="mt-0"
|
||||
defaultValue={msgEdit.message || undefined}
|
||||
id="message-edit"
|
||||
name="message-edit"
|
||||
placeholder="Modifica messaggio"
|
||||
type="text"
|
||||
/>
|
||||
<Button type="submit">{t.salva}</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteMessageDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
msgDelete,
|
||||
handleDeleteSubmit,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (status: boolean) => void;
|
||||
msgDelete: ChatMessage | 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getMessageVariant = (messageName: string, selectedUserName: string) =>
|
||||
messageName !== selectedUserName ? "received" : "sent";
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { forwardRef, type HTMLAttributes } from "react";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
type ChatMessageListProps = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const ChatMessageList = forwardRef<HTMLDivElement, ChatMessageListProps>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-y-auto p-4",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
ChatMessageList.displayName = "ChatMessageList";
|
||||
|
||||
export { ChatMessageList };
|
||||
|
|
@ -1,447 +0,0 @@
|
|||
import { 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 { ActiveChatsType } from "~/server/controllers/chat.controller";
|
||||
import type { NonNullableUser } from "~/server/controllers/user.controller";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
type LastMessage = {
|
||||
message: string | null;
|
||||
time: Date;
|
||||
isLastSender: boolean;
|
||||
};
|
||||
|
||||
export const ChatSidebar = ({ chatId }: { chatId: ChatsChatid | null }) => {
|
||||
const { data: activeChats, isLoading: loadingActiveChats } =
|
||||
api.chat.getActiveChats.useQuery(undefined, {
|
||||
refetchInterval: 5000, // Refetch every 5 seconds, Good enough for a chat sidebar
|
||||
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 utils = api.useUtils();
|
||||
const { mutate: deletechat } = api.chat.deleteChat.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.chat.getActiveChats.invalidate();
|
||||
await utils.users.getActiveUsersWithoutChat.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const getVariant = (lastmessage: LastMessage | null) => {
|
||||
if (lastmessage) {
|
||||
return lastmessage.isLastSender ? "secondary" : "grey";
|
||||
}
|
||||
return "grey";
|
||||
};
|
||||
const getLastMessage = (lastmessage: LastMessage | null) => {
|
||||
if (lastmessage) {
|
||||
return `${lastmessage.isLastSender ? "Tu: " : ""}${lastmessage.message}`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
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.userinfo.username}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label="Select Chat"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: !isDesktop ? "icon" : "xl",
|
||||
variant: getVariant(chat.lastmessage),
|
||||
}),
|
||||
|
||||
!isDesktop ? "p-1" : "justify-start gap-4",
|
||||
"h-fit w-full",
|
||||
)}
|
||||
onClick={() => handleChatSelection(chat.id)}
|
||||
>
|
||||
<UserAvatar
|
||||
className="size-10"
|
||||
userId={chat.userinfo.id}
|
||||
username={chat.userinfo.username}
|
||||
/>
|
||||
|
||||
{isDesktop && (
|
||||
<div className="flex h-full max-w-28 flex-col text-left align-top">
|
||||
<span>{chat.userinfo.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>
|
||||
|
||||
<span className="truncate text-muted-foreground text-xs">
|
||||
{getLastMessage(chat.lastmessage)}
|
||||
</span>
|
||||
<span className="truncate text-muted-foreground text-xs">
|
||||
{chat.lastmessage &&
|
||||
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.id}
|
||||
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.id);
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<UserAvatar
|
||||
className="size-8"
|
||||
userId={chat.userinfo.id}
|
||||
username={chat.userinfo.username}
|
||||
/>
|
||||
|
||||
<span>
|
||||
<span className="block font-medium">
|
||||
{chat.userinfo.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.create.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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
"use client";
|
||||
import {
|
||||
Info,
|
||||
Paperclip,
|
||||
ShieldUser,
|
||||
ShoppingBag,
|
||||
User,
|
||||
UserCog,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { EtichetteDisplayer } from "~/components/etichette";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "~/components/ui/alert-dialog";
|
||||
import { Button, buttonVariants } from "~/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "~/components/ui/popover";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "~/components/ui/tooltip";
|
||||
import { UserAvatar } from "~/components/user_avatar";
|
||||
import { cn } from "~/lib/utils";
|
||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||
import type { Session } from "~/server/api/trpc";
|
||||
import type { ChatUserInfo } from "~/server/services/chat.service";
|
||||
import type { OnlineStatus } from "~/server/sse";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
type ChatTopbarProps = {
|
||||
chatId: ChatsChatid;
|
||||
userData: Session;
|
||||
chatUserData: ChatUserInfo;
|
||||
onlineStatus: OnlineStatus[];
|
||||
};
|
||||
export default function ChatTopbar({
|
||||
chatId,
|
||||
userData,
|
||||
chatUserData,
|
||||
onlineStatus,
|
||||
}: ChatTopbarProps) {
|
||||
const utils = api.useUtils();
|
||||
const { mutate: deletechat } = api.chat.deleteChat.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.chat.getActiveChats.invalidate();
|
||||
await utils.users.getActiveUsersWithoutChat.invalidate();
|
||||
await router.push("/area-riservata/admin/chats/");
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const { mutate: swap } = api.auth.swapUser.useMutation({
|
||||
onSuccess: async () => {
|
||||
toast.success("Utente cambiato con successo");
|
||||
await router.push("/area-riservata/load-page");
|
||||
},
|
||||
});
|
||||
|
||||
const handleDeleteChat = useCallback(() => {
|
||||
deletechat({ chatId });
|
||||
}, [chatId, deletechat]);
|
||||
|
||||
const handleSwapUser = useCallback(() => {
|
||||
swap({ id: chatUserData.id });
|
||||
}, [chatUserData, swap]);
|
||||
|
||||
return (
|
||||
<div className="flex h-18 w-full items-center justify-between border-b px-4 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{!userData.isAdmin ? (
|
||||
<Image
|
||||
alt="Infoalloggi logo"
|
||||
className="size-10 rounded-full"
|
||||
height={40}
|
||||
src={"/favicon/favicon.png"}
|
||||
width={40}
|
||||
/>
|
||||
) : (
|
||||
<UserAvatar
|
||||
className="size-10"
|
||||
userId={chatUserData.id}
|
||||
username={chatUserData.username}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-4">
|
||||
<span className="font-medium">
|
||||
{!userData.isAdmin ? "Chat Infoalloggi" : chatUserData.username}
|
||||
</span>
|
||||
{userData.isAdmin && <EtichetteDisplayer chatId={chatId} />}
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<UserStatus onlineStatus={onlineStatus} userData={userData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{userData.isAdmin && (
|
||||
<AdminPopover
|
||||
chatUserData={chatUserData}
|
||||
handleDeleteChat={handleDeleteChat}
|
||||
handleSwapUser={handleSwapUser}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const UserStatus = ({
|
||||
onlineStatus,
|
||||
userData,
|
||||
}: {
|
||||
onlineStatus: OnlineStatus[];
|
||||
userData: Session;
|
||||
}) => (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{onlineStatus.map((user, idx) =>
|
||||
user.isAdmin ? (
|
||||
<TooltipProvider delayDuration={0} key={user.userId}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span
|
||||
className={cn(
|
||||
"flex flex-row items-center font-bold text-green-500 text-xs",
|
||||
)}
|
||||
>
|
||||
<ShieldUser className="mr-[0.2rem]" size={15} />
|
||||
<span>
|
||||
{user.nome}
|
||||
{idx < onlineStatus.length - 1 && ","}
|
||||
</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Admin Infoalloggi</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<span
|
||||
className={cn("flex flex-row font-bold text-green-500 text-xs")}
|
||||
key={user.userId}
|
||||
>
|
||||
<span>
|
||||
{user.nome}
|
||||
{!userData.isAdmin && " (Tu)"}
|
||||
{idx < onlineStatus.length - 1 && ", "}
|
||||
</span>
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const AdminPopover = ({
|
||||
chatUserData,
|
||||
handleSwapUser,
|
||||
handleDeleteChat,
|
||||
}: {
|
||||
chatUserData: ChatUserInfo;
|
||||
handleSwapUser: () => void;
|
||||
handleDeleteChat: () => void;
|
||||
}) => (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
buttonVariants({ size: "icon", variant: "outline" }),
|
||||
"size-10",
|
||||
"dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white",
|
||||
)}
|
||||
>
|
||||
<Info className="text-muted-foreground" size={24} />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="mr-4 w-fit px-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link
|
||||
aria-label="Visualizza Profilo Utente"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"justify-start gap-2",
|
||||
)}
|
||||
href={`/area-riservata/admin/user-view/edit-user/${chatUserData.id}`}
|
||||
>
|
||||
<UserCog /> Profilo
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
aria-label="Visualizza Ordini Utente"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"justify-start gap-2",
|
||||
)}
|
||||
href={`/area-riservata/admin/user-view/ordini/${chatUserData.id}`}
|
||||
>
|
||||
<ShoppingBag /> Ordini
|
||||
</Link>
|
||||
<Link
|
||||
aria-label="Visualizza Allegati Utente"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"justify-start gap-2",
|
||||
)}
|
||||
href={`/area-riservata/admin/user-view/allegati/${chatUserData.id}`}
|
||||
>
|
||||
<Paperclip /> Allegati
|
||||
</Link>
|
||||
<button
|
||||
aria-label="Cambia Utente"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"justify-start gap-2",
|
||||
)}
|
||||
onClick={handleSwapUser}
|
||||
type="button"
|
||||
>
|
||||
<User /> Login
|
||||
</button>
|
||||
<Separator />
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button size="sm" variant="destructive">
|
||||
Cancella Chat
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<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="Continua Eliminazione Chat"
|
||||
className={cn(buttonVariants({ variant: "destructive" }))}
|
||||
onClick={handleDeleteChat}
|
||||
>
|
||||
Continua
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import ChatBottombar from "~/components/chat/chat-bottombar";
|
||||
import { ChatList } from "~/components/chat/chat-list";
|
||||
import ChatTopbar from "~/components/chat/chat-topbar";
|
||||
import { useLiveChat } from "~/hooks/chatHooks";
|
||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||
import type { Session } from "~/server/api/trpc";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
const Chat = memo(
|
||||
({ chatId, userData }: { chatId: ChatsChatid; userData: Session }) => {
|
||||
const liveChat = useLiveChat(chatId, userData);
|
||||
const [userinfo] = api.chat.getUserInfosByChat.useSuspenseQuery({
|
||||
chatId,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<div className="shrink-0">
|
||||
<ChatTopbar
|
||||
chatId={chatId}
|
||||
chatUserData={userinfo}
|
||||
onlineStatus={liveChat.onlineUsers}
|
||||
userData={userData}
|
||||
/>
|
||||
</div>
|
||||
<ChatList
|
||||
chatId={chatId}
|
||||
messages={liveChat.messages}
|
||||
query={liveChat.query}
|
||||
typingStatus={liveChat.typingStatus}
|
||||
userData={userData}
|
||||
/>
|
||||
|
||||
<div className="shrink-0 border-t">
|
||||
<ChatBottombar
|
||||
chatId={chatId}
|
||||
chatUserData={userinfo}
|
||||
userData={userData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.chatId === nextProps.chatId &&
|
||||
prevProps.userData.id === nextProps.userData.id,
|
||||
);
|
||||
Chat.displayName = "Chat";
|
||||
|
||||
export default Chat;
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
// @hidden
|
||||
export default function MessageLoading() {
|
||||
return (
|
||||
<svg
|
||||
className="text-foreground"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>Loading</title>
|
||||
<circle cx="4" cy="12" fill="currentColor" r="2">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
begin="0;spinner_OcgL.end+0.25s"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
id="spinner_qFRN"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
values="12;6;12"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="12" cy="12" fill="currentColor" r="2">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
begin="spinner_qFRN.begin+0.1s"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
values="12;6;12"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="20" cy="12" fill="currentColor" r="2">
|
||||
<animate
|
||||
attributeName="cy"
|
||||
begin="spinner_qFRN.begin+0.2s"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
id="spinner_OcgL"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
values="12;6;12"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue