feat(chat): refactor chat components to use ChatProvider for context management
- Updated ChatTopbar and Chat components to utilize ChatProvider for managing chat state and user information. - Removed unnecessary props from ChatTopbar and adjusted its implementation to fetch data from context. - Added ChatProvider to encapsulate chat-related state and functionality. - Enhanced message handling by introducing reply and forward features in the chat schema and API. - Cleaned up unused components and optimized imports across chat-related files. - Improved user avatar color generation logic and removed hardcoded color values. - Updated loading states and error handling in admin chat management pages.
This commit is contained in:
parent
1971251490
commit
98463da038
17 changed files with 905 additions and 552 deletions
12
apps/db/migrations/47_messages.up.sql
Normal file
12
apps/db/migrations/47_messages.up.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-- Migration: 47_messages.up.sql
|
||||
|
||||
UPDATE public.messages
|
||||
SET message = ''
|
||||
WHERE message IS NULL;
|
||||
|
||||
ALTER TABLE IF EXISTS public.messages
|
||||
ALTER COLUMN IF EXISTS message SET NOT NULL;
|
||||
|
||||
ALTER TABLE IF EXISTS public.messages
|
||||
ADD COLUMN IF NOT EXISTS reply_to_id UUID REFERENCES messages(messageid) ON DELETE SET NULL,
|
||||
ADD COLUMN IF NOT EXISTS is_forwarded BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
|
@ -17,26 +17,19 @@ import { LoadingPage } from "~/components/loading";
|
|||
import { Button, buttonVariants } from "~/components/ui/button";
|
||||
import { UploadComponent } from "~/components/upload_modal";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useChatContext } from "~/providers/ChatProvider";
|
||||
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) => {
|
||||
export const ChatAttachments = () => {
|
||||
const { session, userinfo } = useChatContext();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data: attachments,
|
||||
isLoading: attachmentsLoading,
|
||||
refetch,
|
||||
} = api.storage.retrieveUserFileData.useQuery(
|
||||
{ userId: chatUserData.id },
|
||||
{ userId: userinfo.id },
|
||||
{ refetchOnMount: true },
|
||||
);
|
||||
|
||||
|
|
@ -65,10 +58,7 @@ export const ChatAttachments = ({
|
|||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody className="max-h-[80vh] space-y-5 overflow-auto pb-5 md:px-1">
|
||||
<UploadComponent
|
||||
isAdmin={userData.isAdmin}
|
||||
userId={chatUserData.id}
|
||||
/>
|
||||
<UploadComponent isAdmin={session.isAdmin} userId={userinfo.id} />
|
||||
|
||||
<div className="h-auto max-h-160 max-w-lg overflow-auto rounded-lg">
|
||||
{attachmentsLoading ? (
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { SendHorizontal, SmilePlusIcon, ThumbsUp } from "lucide-react";
|
||||
import {
|
||||
type ChangeEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
ArrowDown,
|
||||
SendHorizontal,
|
||||
SmilePlusIcon,
|
||||
ThumbsUp,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { type ChangeEvent, useCallback, useEffect, useState } from "react";
|
||||
import { ChatAttachments } from "~/components/chat/chat-attachments";
|
||||
import {
|
||||
type AutosizeTextAreaRef,
|
||||
AutosizeTextarea,
|
||||
} from "~/components/custom_ui/autoResizeTextArea";
|
||||
import { AutosizeTextarea } from "~/components/custom_ui/autoResizeTextArea";
|
||||
import {
|
||||
EmojiPicker,
|
||||
EmojiPickerContent,
|
||||
|
|
@ -26,59 +21,60 @@ import {
|
|||
} from "~/components/ui/popover";
|
||||
import { useThrottledIsTypingMutation } from "~/hooks/chatHooks";
|
||||
import { useMediaQuery } from "~/hooks/use-media-query";
|
||||
import { useChatContext } from "~/providers/ChatProvider";
|
||||
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) {
|
||||
export default function ChatBottombar() {
|
||||
const {
|
||||
chatId,
|
||||
session,
|
||||
inputRef,
|
||||
replyTrg,
|
||||
setReplyTrg,
|
||||
messagesContainerRef,
|
||||
} = useChatContext();
|
||||
const { locale } = useTranslation();
|
||||
const [message, setMessage] = useState("");
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const inputRef = useRef<AutosizeTextAreaRef>(null);
|
||||
|
||||
const { mutate: addMutation } = api.chatSSE.sendMessage.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,
|
||||
});
|
||||
|
||||
const { mutate: addMutation } = api.chatSSE.sendMessage.useMutation({
|
||||
onError: (err) => {
|
||||
console.error("Failed to send message", err);
|
||||
},
|
||||
onMutate: () => {
|
||||
setReplyTrg(null);
|
||||
setMessage("");
|
||||
|
||||
if (inputRef.current) {
|
||||
inputRef.current.textArea.focus();
|
||||
inputRef.current.textArea.style.height = "auto";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const handleThumbsUp = () => {
|
||||
addMutation({
|
||||
reply_to_id: replyTrg?.messageid || null,
|
||||
chatId,
|
||||
message: "👍",
|
||||
sender: session.id,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSend = () => {
|
||||
if (message.length !== 0) {
|
||||
addMutation({
|
||||
reply_to_id: replyTrg?.messageid || null,
|
||||
chatId,
|
||||
message: message.trim(),
|
||||
sender: session.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
const isTypingMutation = useThrottledIsTypingMutation({
|
||||
chatId,
|
||||
username: userData.username,
|
||||
userId: userData.id,
|
||||
username: session.username,
|
||||
userId: session.id,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -92,81 +88,129 @@ export default function ChatBottombar({
|
|||
}
|
||||
}, []);
|
||||
const [openEmoji, setOpenEmoji] = useState(false);
|
||||
|
||||
const handleJumpToBottom = useCallback(() => {
|
||||
if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTo({
|
||||
top: messagesContainerRef.current.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
const isMobile = useMediaQuery("(min-width: 426px)");
|
||||
const [showScrollDown, setShowScrollDown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = messagesContainerRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
const distanceFromBottom =
|
||||
el.scrollHeight - el.scrollTop - el.clientHeight;
|
||||
setShowScrollDown(distanceFromBottom > 150);
|
||||
};
|
||||
|
||||
el.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => el.removeEventListener("scroll", handleScroll);
|
||||
}, [messagesContainerRef]);
|
||||
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 className="relative flex w-full flex-col border-t">
|
||||
{showScrollDown && (
|
||||
<Button
|
||||
className="absolute -top-1/2 left-1/2 size-12 -translate-x-1/2 -translate-y-1/2 rounded-full opacity-80"
|
||||
onClick={handleJumpToBottom}
|
||||
variant="outline"
|
||||
>
|
||||
<ArrowDown className="size-6" />
|
||||
</Button>
|
||||
)}
|
||||
{replyTrg && (
|
||||
<div className="p-2">
|
||||
<div className="relative flex-1 rounded-lg border bg-muted p-2 text-sm">
|
||||
<span>Rispondi:</span>
|
||||
<div className="line-clamp-2 overflow-hidden text-ellipsis whitespace-pre-wrap text-sm">
|
||||
{replyTrg.message}
|
||||
</div>
|
||||
<Button
|
||||
className="absolute top-1 right-1"
|
||||
onClick={() => setReplyTrg(null)}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-h-18.5 w-full items-center justify-between gap-2 p-2">
|
||||
<div className="flex">
|
||||
<ChatAttachments />
|
||||
</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);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<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,12 +1,11 @@
|
|||
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 { cn } from "~/lib/utils";
|
||||
|
||||
// ChatBubble
|
||||
const chatBubbleVariant = cva(
|
||||
"flex gap-2 max-w-[85%] items-end relative group ",
|
||||
"flex gap-2 max-w-[85%] items-end relative group rounded-md",
|
||||
{
|
||||
defaultVariants: {
|
||||
layout: "default",
|
||||
|
|
@ -52,28 +51,19 @@ const ChatBubble = React.forwardRef<HTMLDivElement, ChatBubbleProps>(
|
|||
);
|
||||
ChatBubble.displayName = "ChatBubble";
|
||||
|
||||
type ChatBubbleReadStatusProps = {
|
||||
isread: boolean;
|
||||
};
|
||||
|
||||
export const ChatBubbleReadStatus = ({ isread }: ChatBubbleReadStatusProps) => {
|
||||
export const ChatBubbleReadStatus = ({ isread }: { isread: boolean }) => {
|
||||
if (isread) {
|
||||
return <CheckCheck className="mt-2 size-4 text-green-500" />;
|
||||
return <CheckCheck className="size-4 text-green-500" />;
|
||||
}
|
||||
return <Check className="mt-2 size-4 text-muted-foreground" />;
|
||||
return <Check className="size-4 text-muted" />;
|
||||
};
|
||||
|
||||
// ChatBubbleMessage
|
||||
const chatBubbleMessageVariants = cva("px-1.5 py-0.75 ", {
|
||||
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",
|
||||
|
|
@ -84,52 +74,23 @@ const chatBubbleMessageVariants = cva("px-1.5 py-0.75 ", {
|
|||
|
||||
interface ChatBubbleMessageProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof chatBubbleMessageVariants> {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
VariantProps<typeof chatBubbleMessageVariants> {}
|
||||
|
||||
const ChatBubbleMessage = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChatBubbleMessageProps
|
||||
>(
|
||||
(
|
||||
{ className, variant, layout, isLoading = false, children, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<div
|
||||
className={cn(
|
||||
chatBubbleMessageVariants({ className, layout, variant }),
|
||||
"wrap-break-word whitespace-pre-wrap",
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<MessageLoading />
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
>(({ className, variant, children, ...props }, ref) => (
|
||||
<div
|
||||
className={cn(
|
||||
chatBubbleMessageVariants({ className, variant }),
|
||||
"wrap-break-word whitespace-pre-wrap",
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{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>
|
||||
);
|
||||
|
||||
export { ChatBubble, ChatBubbleMessage, ChatBubbleTimestamp };
|
||||
export { ChatBubble, ChatBubbleMessage };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { format, isSameDay } from "date-fns";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import { Copy, Forward, Pencil, Reply, Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
type FormEvent,
|
||||
useCallback,
|
||||
|
|
@ -12,9 +14,9 @@ import {
|
|||
ChatBubble,
|
||||
ChatBubbleMessage,
|
||||
ChatBubbleReadStatus,
|
||||
ChatBubbleTimestamp,
|
||||
} from "~/components/chat/chat-bubble";
|
||||
import LoadingButton from "~/components/custom_ui/loading-button";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
|
|
@ -26,6 +28,14 @@ import {
|
|||
AlertDialogTitle,
|
||||
} from "~/components/ui/alert-dialog";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "~/components/ui/command";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
|
|
@ -42,35 +52,29 @@ import {
|
|||
import Input from "~/components/ui/input";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import { getUserColorV2, UserAvatar } from "~/components/user_avatar";
|
||||
import type { LiveChat } from "~/hooks/chatHooks";
|
||||
import { cn, getInitials } from "~/lib/utils";
|
||||
import { cn } from "~/lib/utils";
|
||||
import { useChatContext } from "~/providers/ChatProvider";
|
||||
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 { Message, TypingData } from "~/server/api/routers/chat_sse";
|
||||
import type { Session } from "~/server/api/trpc";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
type ChatListProps = {
|
||||
chatId: ChatsChatid;
|
||||
messages: Message[];
|
||||
userData: Session;
|
||||
query: LiveChat["query"];
|
||||
typingStatus: TypingData;
|
||||
};
|
||||
export const ChatList = () => {
|
||||
const {
|
||||
chatId,
|
||||
session,
|
||||
liveChat,
|
||||
inputRef,
|
||||
setReplyTrg,
|
||||
messagesContainerRef,
|
||||
} = useChatContext();
|
||||
const { messages, query, typingStatus } = liveChat;
|
||||
|
||||
export const ChatList = ({
|
||||
chatId,
|
||||
messages,
|
||||
userData,
|
||||
query,
|
||||
typingStatus,
|
||||
}: ChatListProps) => {
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [msgSelected, setMsgSelected] = useState<Message | null>(null);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const [inoltraModalOpen, setInoltraModalOpen] = useState(false);
|
||||
|
||||
const handleEditOpen = (status: boolean) => {
|
||||
setEditModalOpen(status);
|
||||
|
|
@ -126,7 +130,7 @@ export const ChatList = ({
|
|||
[editMutation],
|
||||
);
|
||||
const [locked, setLocked] = useState(false);
|
||||
|
||||
const focusInputOnCloseRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (
|
||||
messagesContainerRef.current &&
|
||||
|
|
@ -134,16 +138,65 @@ export const ChatList = ({
|
|||
!query.isLoading &&
|
||||
!query.isFetching
|
||||
) {
|
||||
messagesContainerRef.current.scrollTop =
|
||||
messagesContainerRef.current.scrollHeight;
|
||||
messagesContainerRef.current.scrollTo({
|
||||
top: messagesContainerRef.current.scrollHeight,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
setLocked(false);
|
||||
}, [messages, typingStatus]);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||
<div
|
||||
className={cn("flex h-full w-full flex-col overflow-y-auto p-4")}
|
||||
className={cn("flex h-full w-full flex-col overflow-y-auto p-1")}
|
||||
ref={messagesContainerRef}
|
||||
>
|
||||
<AnimatePresence key="chat-messages">
|
||||
|
|
@ -154,10 +207,7 @@ export const ChatList = ({
|
|||
disabled={!query.hasNextPage || query.isFetchingNextPage}
|
||||
key="load-more"
|
||||
loading={query.isFetchingNextPage}
|
||||
onClick={async () => {
|
||||
setLocked(true);
|
||||
await query.fetchNextPage();
|
||||
}}
|
||||
onClick={handleFetchPrevious}
|
||||
variant="secondary"
|
||||
>
|
||||
{query.isFetchingNextPage
|
||||
|
|
@ -167,45 +217,32 @@ export const ChatList = ({
|
|||
)}
|
||||
|
||||
{messages.map((message, index) => {
|
||||
const variant = getMessageVariant(message.sender, userData.id);
|
||||
|
||||
const isNextSameSender =
|
||||
index + 1 < messages.length &&
|
||||
messages[index + 1]?.sender === message.sender;
|
||||
|
||||
const isPrevSameDay = (() => {
|
||||
const prevMsg = messages[index - 1];
|
||||
if (!prevMsg) return false;
|
||||
return isSameDay(new Date(prevMsg.time), new Date(message.time));
|
||||
})();
|
||||
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();
|
||||
const isSender = message.sender === session.id;
|
||||
|
||||
const userColor = getUserColorV2(
|
||||
`${getInitials(message.sender_nome)}${message.sender}`,
|
||||
);
|
||||
const userColor = getUserColorV2(message.color_str);
|
||||
const reply_userColor = message.reply_source
|
||||
? getUserColorV2(message.reply_source.color_str)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-2 pb-4",
|
||||
"flex flex-col gap-0.5 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">
|
||||
|
|
@ -216,72 +253,162 @@ export const ChatList = ({
|
|||
})}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<ChatBubble variant={variant}>
|
||||
{isNextSameSender ? (
|
||||
<div className="size-6 shrink-0"></div>
|
||||
) : (
|
||||
<UserAvatar
|
||||
className="size-6 shrink-0"
|
||||
showInitials={false}
|
||||
userId={message.sender}
|
||||
username={message.sender_nome}
|
||||
/>
|
||||
)}
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild disabled={!userData.isAdmin}>
|
||||
<div className="flex flex-col gap-1">
|
||||
{!isPreviousSameSender && (
|
||||
<p
|
||||
className={cn(
|
||||
"font-semibold text-sm",
|
||||
variant === "sent" && "text-right",
|
||||
)}
|
||||
style={{
|
||||
color: userColor,
|
||||
}}
|
||||
>
|
||||
{message.sender_nome}
|
||||
</p>
|
||||
)}
|
||||
{!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"}>
|
||||
<ChatBubbleMessage
|
||||
className={cn(isNextSameSender && "rounded-lg", "")}
|
||||
isLoading={false}
|
||||
className={cn(
|
||||
"flex flex-wrap items-end justify-end gap-x-2 gap-y-1",
|
||||
isNextSameSender && "rounded-lg",
|
||||
"",
|
||||
)}
|
||||
variant={isSender ? "sent" : "received"}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
<span className="flex shrink break-all">
|
||||
{message.message}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{userData.id === message.sender && (
|
||||
<ChatBubbleReadStatus isread={message.isread} />
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1",
|
||||
isSender && "justify-end",
|
||||
)}
|
||||
>
|
||||
{message.time && (
|
||||
<ChatBubbleTimestamp
|
||||
timestamp={new Date(
|
||||
message.time,
|
||||
).toLocaleString("it", {
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
month: "short",
|
||||
})}
|
||||
/>
|
||||
<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} />
|
||||
)}
|
||||
</div>
|
||||
</ChatBubbleMessage>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
</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 && (
|
||||
<ContextMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setMsgSelected(message);
|
||||
handleDeleteOpen(true);
|
||||
setInoltraModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 />
|
||||
Cancella
|
||||
<Forward />
|
||||
Inoltra
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
|
||||
{(isSender || session.isAdmin) && (
|
||||
<ContextMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
|
|
@ -292,20 +419,41 @@ export const ChatList = ({
|
|||
<Pencil />
|
||||
Modifica
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</ChatBubble>
|
||||
</motion.div>
|
||||
)}
|
||||
{session.isAdmin && (
|
||||
<>
|
||||
<ContextMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setMsgSelected(message);
|
||||
handleDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 />
|
||||
Cancella
|
||||
</ContextMenuItem>{" "}
|
||||
</>
|
||||
)}
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<Typers
|
||||
currentTypers={typingStatus}
|
||||
messageLenght={messages.length}
|
||||
userId={userData.id}
|
||||
userId={session.id}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
{session.isAdmin && (
|
||||
<InoltraDialog
|
||||
isOpen={inoltraModalOpen}
|
||||
msgSelected={msgSelected}
|
||||
openChange={(status) => setInoltraModalOpen(status)}
|
||||
/>
|
||||
)}
|
||||
<EditMessageDialog
|
||||
handleEditSubmit={handleEditSubmit}
|
||||
msgEdit={msgSelected}
|
||||
|
|
@ -342,7 +490,7 @@ const Typers = ({
|
|||
return (
|
||||
<motion.div
|
||||
animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}
|
||||
className="flex flex-col gap-2 py-2"
|
||||
className="flex flex-col gap-2 px-1 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}`}
|
||||
|
|
@ -358,14 +506,9 @@ const Typers = ({
|
|||
}}
|
||||
>
|
||||
<ChatBubble variant={variant}>
|
||||
<UserAvatar
|
||||
className="size-6"
|
||||
showInitials={false}
|
||||
userId={typer.userId}
|
||||
username={typer.username}
|
||||
/>
|
||||
|
||||
<ChatBubbleMessage isLoading={true}></ChatBubbleMessage>
|
||||
<ChatBubbleMessage className="text-muted-foreground text-xs">
|
||||
✏️ {typer.username} sta scrivendo...
|
||||
</ChatBubbleMessage>
|
||||
</ChatBubble>
|
||||
</motion.div>
|
||||
);
|
||||
|
|
@ -466,5 +609,78 @@ const DeleteMessageDialog = ({
|
|||
);
|
||||
};
|
||||
|
||||
const getMessageVariant = (messageName: string, selectedUserName: string) =>
|
||||
messageName !== selectedUserName ? "received" : "sent";
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ const SearchUserComponent = ({
|
|||
>
|
||||
{activeChats?.map((chat) => (
|
||||
<CommandItem
|
||||
className="rounded-md"
|
||||
className="cursor-pointer rounded-md"
|
||||
key={chat.id}
|
||||
onSelect={() => {
|
||||
handleChatSelection(chat.chatid);
|
||||
|
|
@ -344,7 +344,7 @@ const NewUserComponent = ({
|
|||
>
|
||||
{inactiveUsers?.map((user) => (
|
||||
<CommandItem
|
||||
className="rounded-md"
|
||||
className="cursor-pointer rounded-md"
|
||||
key={user.id}
|
||||
onSelect={() => {
|
||||
createNewChat({
|
||||
|
|
|
|||
|
|
@ -25,21 +25,11 @@ import {
|
|||
import { Separator } from "~/components/ui/separator";
|
||||
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 { useChatContext } from "~/providers/ChatProvider";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
type ChatTopbarProps = {
|
||||
chatId: ChatsChatid;
|
||||
userData: Session;
|
||||
chatUserData: ChatUserInfo;
|
||||
};
|
||||
export default function ChatTopbar({
|
||||
chatId,
|
||||
userData,
|
||||
chatUserData,
|
||||
}: ChatTopbarProps) {
|
||||
export default function ChatTopbar() {
|
||||
const { chatId, session, userinfo } = useChatContext();
|
||||
const utils = api.useUtils();
|
||||
const { mutate: deletechat } = api.chat.deleteChat.useMutation({
|
||||
onSuccess: async () => {
|
||||
|
|
@ -62,13 +52,13 @@ export default function ChatTopbar({
|
|||
}, [chatId, deletechat]);
|
||||
|
||||
const handleSwapUser = useCallback(() => {
|
||||
swap({ id: chatUserData.id });
|
||||
}, [chatUserData, swap]);
|
||||
swap({ id: userinfo.id });
|
||||
}, [userinfo, 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 ? (
|
||||
{!session.isAdmin ? (
|
||||
<Image
|
||||
alt="Infoalloggi logo"
|
||||
className="size-10 rounded-full"
|
||||
|
|
@ -79,24 +69,23 @@ export default function ChatTopbar({
|
|||
) : (
|
||||
<UserAvatar
|
||||
className="size-10"
|
||||
userId={chatUserData.id}
|
||||
username={chatUserData.username}
|
||||
userId={userinfo.id}
|
||||
username={userinfo.username}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex gap-4">
|
||||
<span className="font-medium">
|
||||
{!userData.isAdmin ? "Chat Infoalloggi" : chatUserData.username}
|
||||
{!session.isAdmin ? "Chat Infoalloggi" : userinfo.username}
|
||||
</span>
|
||||
{userData.isAdmin && <EtichetteDisplayer chatId={chatId} />}
|
||||
{session.isAdmin && <EtichetteDisplayer chatId={chatId} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{userData.isAdmin && (
|
||||
{session.isAdmin && (
|
||||
<AdminPopover
|
||||
chatUserData={chatUserData}
|
||||
handleDeleteChat={handleDeleteChat}
|
||||
handleSwapUser={handleSwapUser}
|
||||
/>
|
||||
|
|
@ -107,96 +96,97 @@ export default function ChatTopbar({
|
|||
}
|
||||
|
||||
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>
|
||||
}) => {
|
||||
const { userinfo } = useChatContext();
|
||||
return (
|
||||
<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/${userinfo.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>
|
||||
);
|
||||
<Link
|
||||
aria-label="Visualizza Ordini Utente"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"justify-start gap-2",
|
||||
)}
|
||||
href={`/area-riservata/admin/user-view/ordini/${userinfo.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/${userinfo.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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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 { ChatProvider } from "~/providers/ChatProvider";
|
||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||
import type { Session } from "~/server/api/trpc";
|
||||
import { api } from "~/utils/api";
|
||||
|
|
@ -15,30 +16,17 @@ const Chat = memo(
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<div className="shrink-0">
|
||||
<ChatTopbar
|
||||
chatId={chatId}
|
||||
chatUserData={userinfo}
|
||||
userData={userData}
|
||||
/>
|
||||
<ChatProvider values={{ liveChat, chatId, userinfo, session: userData }}>
|
||||
<div className="flex h-full w-full flex-col justify-between">
|
||||
<div className="shrink-0">
|
||||
<ChatTopbar />
|
||||
</div>
|
||||
<ChatList />
|
||||
<div className="shrink-0">
|
||||
<ChatBottombar />
|
||||
</div>
|
||||
</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>
|
||||
</ChatProvider>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ import { cn, getInitials } from "~/lib/utils";
|
|||
import type { UsersId } from "~/schemas/public/Users";
|
||||
|
||||
const user_colors = [
|
||||
"#2D3436",
|
||||
"#535C68",
|
||||
//"#2D3436",
|
||||
//"#535C68",
|
||||
"#636E72",
|
||||
"#FAB1A0",
|
||||
"#FF7675",
|
||||
|
|
@ -23,11 +23,11 @@ const user_colors = [
|
|||
"#81ECEC",
|
||||
"#74B9FF",
|
||||
"#0984E3",
|
||||
"#4834D4",
|
||||
"#6C5CE7",
|
||||
//"#5e4ae8",
|
||||
//"#6C5CE7",
|
||||
"#A29BFE",
|
||||
"#A55EEA",
|
||||
"#6D214F",
|
||||
//"#6D214F",
|
||||
];
|
||||
|
||||
function stringHash(str: string): number {
|
||||
|
|
@ -80,9 +80,8 @@ export const UserAvatar = ({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
const value = `${getInitials(username)}${userId}`;
|
||||
|
||||
const userColor = getUserColorV2(value);
|
||||
const userColor = getUserColorV2(username + userId);
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ import { Sidebar } from "~/components/area-riservata/sidebar";
|
|||
import { ChatSidebar } from "~/components/chat/chat-sidebar";
|
||||
import { LoadingPage } from "~/components/loading";
|
||||
import { SiteHeader } from "~/components/navbar/site-header";
|
||||
import { Status500 } from "~/components/status-page";
|
||||
import type { NextPageWithLayout } from "~/pages/_app";
|
||||
import { useSession } from "~/providers/SessionProvider";
|
||||
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
/**
|
||||
* Pagina di gestione chat per admin: /area-riservata/admin/chats
|
||||
|
|
@ -17,20 +15,12 @@ import { api } from "~/utils/api";
|
|||
const AdminChats: NextPageWithLayout = () => {
|
||||
const { status, user } = useSession();
|
||||
|
||||
const { data: activeChats, isLoading: isLoadingActiveChats } =
|
||||
api.chat.getActiveChats.useQuery();
|
||||
|
||||
const { data: inactiveUsers, isLoading: isLoadingInactiveUsers } =
|
||||
api.users.getActiveUsersWithoutChat.useQuery();
|
||||
|
||||
if (status === "LOADING" || isLoadingActiveChats || isLoadingInactiveUsers)
|
||||
return <LoadingPage />;
|
||||
if (status === "LOADING") return <LoadingPage />;
|
||||
|
||||
if (status !== "AUTHENTICATED") {
|
||||
window.location.reload();
|
||||
return <LoadingPage />;
|
||||
}
|
||||
if (!activeChats || !inactiveUsers) return <Status500 />;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
56
apps/infoalloggi/src/providers/ChatProvider.tsx
Normal file
56
apps/infoalloggi/src/providers/ChatProvider.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { createContext, useContext, useRef, useState } from "react";
|
||||
import type { AutosizeTextAreaRef } from "~/components/custom_ui/autoResizeTextArea";
|
||||
import type { LiveChat } from "~/hooks/chatHooks";
|
||||
import type { ChatsChatid } from "~/schemas/public/Chats";
|
||||
import type { MessagesMessageid } from "~/schemas/public/Messages";
|
||||
import type { Session } from "~/server/api/trpc";
|
||||
import type { ChatUserInfo } from "~/server/services/chat.service";
|
||||
|
||||
type ReplyTrg = {
|
||||
messageid: MessagesMessageid;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type ChatContextType = {
|
||||
liveChat: LiveChat;
|
||||
chatId: ChatsChatid;
|
||||
userinfo: ChatUserInfo;
|
||||
session: Session;
|
||||
replyTrg: ReplyTrg | null;
|
||||
setReplyTrg: React.Dispatch<React.SetStateAction<ReplyTrg | null>>;
|
||||
inputRef: React.RefObject<AutosizeTextAreaRef | null>;
|
||||
messagesContainerRef: React.RefObject<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
type ChatContextProps = {
|
||||
liveChat: LiveChat;
|
||||
chatId: ChatsChatid;
|
||||
userinfo: ChatUserInfo;
|
||||
session: Session;
|
||||
};
|
||||
const ChatContext = createContext<ChatContextType | null>(null);
|
||||
|
||||
export const ChatProvider = ({
|
||||
children,
|
||||
values,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
values: ChatContextProps;
|
||||
}) => {
|
||||
const [replyTrg, setReplyTrg] = useState<ReplyTrg | null>(null);
|
||||
const inputRef = useRef<AutosizeTextAreaRef>(null);
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
return (
|
||||
<ChatContext.Provider
|
||||
value={{ ...values, replyTrg, setReplyTrg, inputRef, messagesContainerRef }}
|
||||
>
|
||||
{children}
|
||||
</ChatContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useChatContext = () => {
|
||||
const ctx = useContext(ChatContext);
|
||||
if (!ctx) throw new Error("useChatContext must be used inside ChatProvider");
|
||||
return ctx;
|
||||
};
|
||||
|
|
@ -14,13 +14,17 @@ export default interface MessagesTable {
|
|||
|
||||
chatid: ColumnType<ChatsChatid, ChatsChatid, ChatsChatid>;
|
||||
|
||||
message: ColumnType<string | null, string | null, string | null>;
|
||||
message: ColumnType<string, string, string>;
|
||||
|
||||
time: ColumnType<Date, Date | string, Date | string>;
|
||||
|
||||
isread: ColumnType<boolean, boolean | undefined, boolean>;
|
||||
|
||||
sender: ColumnType<UsersId, UsersId, UsersId>;
|
||||
|
||||
reply_to_id: ColumnType<MessagesMessageid | null, MessagesMessageid | null, MessagesMessageid | null>;
|
||||
|
||||
is_forwarded: ColumnType<boolean, boolean | undefined, boolean>;
|
||||
}
|
||||
|
||||
export type Messages = Selectable<MessagesTable>;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { on } from "node:events";
|
||||
import { TRPCError, tracked } from "@trpc/server";
|
||||
import { sql } from "kysely";
|
||||
import { jsonObjectFrom } from "kysely/helpers/postgres";
|
||||
import { z } from "zod/v4";
|
||||
import type { MessagesMessageid } from "~/schemas/public/Messages";
|
||||
import {
|
||||
|
|
@ -25,6 +27,7 @@ import {
|
|||
zUserId,
|
||||
} from "~/server/utils/zod_types";
|
||||
import { getKeydbClient, getSubClient } from "~/utils/keydb";
|
||||
import { withUserColorStr } from "~/utils/kysely-helper";
|
||||
|
||||
const getChatChannel = (chatId: string) => `chat:${chatId}:messages`;
|
||||
const getTypingChannel = (chatId: string) => `chat:${chatId}:typing`;
|
||||
|
|
@ -55,9 +58,17 @@ export const chatSSERouter = createTRPCRouter({
|
|||
chatId: zChatId,
|
||||
cursor: zMessageId.nullish(),
|
||||
take: z.number().min(1).max(50).nullish(),
|
||||
targetMsgId: zMessageId.nullish(),
|
||||
}),
|
||||
)
|
||||
.output(
|
||||
z.object({
|
||||
items: z.array(MessageDataSchema),
|
||||
nextCursor: zMessageId.nullable(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const take = input.take ?? 30;
|
||||
try {
|
||||
const didUpdate = await setReadMessage({
|
||||
chatId: input.chatId,
|
||||
|
|
@ -75,17 +86,15 @@ export const chatSSERouter = createTRPCRouter({
|
|||
}
|
||||
}
|
||||
|
||||
const take = input.take ?? 20;
|
||||
const cursor = input.cursor;
|
||||
|
||||
let qry = db
|
||||
.selectFrom("messages")
|
||||
.innerJoin("users", "users.id", "messages.sender")
|
||||
.select([
|
||||
"messages.message",
|
||||
"messages.messageid",
|
||||
"messages.chatid",
|
||||
(eb) =>
|
||||
if (input.targetMsgId) {
|
||||
// If a targetMsgId is provided, we want to fetch messages around that message
|
||||
const msgs = await db
|
||||
.selectFrom("messages")
|
||||
.innerJoin("users", "users.id", "messages.sender")
|
||||
.select((eb) => [
|
||||
"messages.message",
|
||||
"messages.messageid",
|
||||
"messages.chatid",
|
||||
eb
|
||||
.cast<string>(
|
||||
eb.fn("to_char", [
|
||||
|
|
@ -95,11 +104,91 @@ export const chatSSERouter = createTRPCRouter({
|
|||
"text",
|
||||
)
|
||||
.as("time"),
|
||||
"messages.sender",
|
||||
"messages.isread",
|
||||
"messages.is_forwarded",
|
||||
"messages.reply_to_id",
|
||||
"users.nome as sender_nome",
|
||||
"users.isAdmin as sender_isAdmin",
|
||||
withUserColorStr(),
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("messages as reply_source")
|
||||
.select([
|
||||
"reply_source.messageid",
|
||||
"reply_source.message",
|
||||
"reply_source.sender",
|
||||
])
|
||||
.innerJoin("users", "users.id", "reply_source.sender")
|
||||
.select(["users.nome", withUserColorStr()])
|
||||
.whereRef(
|
||||
"messages.reply_to_id",
|
||||
"=",
|
||||
"reply_source.messageid",
|
||||
)
|
||||
.limit(1),
|
||||
).as("reply_source"),
|
||||
])
|
||||
.where("messages.chatid", "=", input.chatId)
|
||||
.where("messages.messageid", ">=", input.targetMsgId)
|
||||
.orderBy("messages.messageid", "asc")
|
||||
.execute();
|
||||
const prevCheck = await db
|
||||
.selectFrom("messages")
|
||||
.where("chatid", "=", input.chatId)
|
||||
.where("messageid", "<", input.targetMsgId)
|
||||
.select("messageid")
|
||||
.orderBy("messageid", "desc")
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
|
||||
return {
|
||||
items: msgs,
|
||||
nextCursor: prevCheck?.messageid ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
const cursor = input.cursor;
|
||||
|
||||
let qry = db
|
||||
.selectFrom("messages")
|
||||
.innerJoin("users", "users.id", "messages.sender")
|
||||
.select((eb) => [
|
||||
"messages.message",
|
||||
"messages.messageid",
|
||||
"messages.chatid",
|
||||
|
||||
eb
|
||||
.cast<string>(
|
||||
eb.fn("to_char", [
|
||||
"messages.time",
|
||||
eb.val("YYYY-MM-DD HH24:MI:SS"),
|
||||
]),
|
||||
"text",
|
||||
)
|
||||
.as("time"),
|
||||
"messages.sender",
|
||||
"messages.isread",
|
||||
"messages.is_forwarded",
|
||||
"messages.reply_to_id",
|
||||
"users.nome as sender_nome",
|
||||
"users.isAdmin as sender_isAdmin",
|
||||
withUserColorStr(),
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("messages as reply_source")
|
||||
.select([
|
||||
"reply_source.messageid",
|
||||
"reply_source.message",
|
||||
"reply_source.sender",
|
||||
])
|
||||
.innerJoin("users", "users.id", "reply_source.sender")
|
||||
.select(["users.nome", withUserColorStr()])
|
||||
.whereRef("messages.reply_to_id", "=", "reply_source.messageid")
|
||||
.limit(1),
|
||||
).as("reply_source"),
|
||||
])
|
||||
|
||||
.where("users.nome", "is not", null)
|
||||
.where("messages.chatid", "=", input.chatId)
|
||||
.orderBy("messages.messageid", "desc")
|
||||
|
|
@ -134,6 +223,8 @@ export const chatSSERouter = createTRPCRouter({
|
|||
chatId: zChatId,
|
||||
message: z.string(),
|
||||
sender: zUserId,
|
||||
is_forwarded: z.boolean().default(false),
|
||||
reply_to_id: zMessageId.nullable().default(null),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
|
|
@ -144,15 +235,33 @@ export const chatSSERouter = createTRPCRouter({
|
|||
message: input.message,
|
||||
sender: input.sender,
|
||||
time: new Date(),
|
||||
is_forwarded: input.is_forwarded,
|
||||
reply_to_id: input.reply_to_id,
|
||||
})
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow(
|
||||
() => new Error("Failed to insert message into database"),
|
||||
);
|
||||
const rs = await db
|
||||
.selectFrom("messages")
|
||||
.innerJoin("users", "users.id", "messages.sender")
|
||||
.select([
|
||||
"messages.messageid",
|
||||
"messages.message",
|
||||
"messages.sender",
|
||||
"users.nome",
|
||||
withUserColorStr(),
|
||||
])
|
||||
.where("messages.messageid", "=", newMessage.reply_to_id)
|
||||
.executeTakeFirst();
|
||||
|
||||
const userInfos = await db
|
||||
.selectFrom("users")
|
||||
.select(["nome as sender_nome", "isAdmin as sender_isAdmin"])
|
||||
.select([
|
||||
"nome as sender_nome",
|
||||
"isAdmin as sender_isAdmin",
|
||||
sql<string>`concat(username, '', id)`.as("color_str"),
|
||||
])
|
||||
.where("id", "=", input.sender)
|
||||
.executeTakeFirst();
|
||||
|
||||
|
|
@ -165,6 +274,16 @@ export const chatSSERouter = createTRPCRouter({
|
|||
const payload: Message = {
|
||||
...newMessage,
|
||||
...userInfos,
|
||||
|
||||
reply_source: rs
|
||||
? {
|
||||
messageid: rs.messageid,
|
||||
message: rs.message,
|
||||
nome: rs.nome,
|
||||
sender: rs.sender,
|
||||
color_str: rs.color_str,
|
||||
}
|
||||
: null,
|
||||
time: newMessage.time.toISOString(),
|
||||
};
|
||||
|
||||
|
|
@ -393,24 +512,39 @@ export const chatSSERouter = createTRPCRouter({
|
|||
const newMessagesSinceLast = await db
|
||||
.selectFrom("messages")
|
||||
.innerJoin("users", "users.id", "messages.sender")
|
||||
.select([
|
||||
.select((eb) => [
|
||||
"messages.message",
|
||||
"messages.messageid",
|
||||
"messages.chatid",
|
||||
(eb) =>
|
||||
eb
|
||||
.cast<string>(
|
||||
eb.fn("to_char", [
|
||||
"messages.time",
|
||||
eb.val("YYYY-MM-DD HH24:MI:SS"),
|
||||
]),
|
||||
"text",
|
||||
)
|
||||
.as("time"),
|
||||
eb
|
||||
.cast<string>(
|
||||
eb.fn("to_char", [
|
||||
"messages.time",
|
||||
eb.val("YYYY-MM-DD HH24:MI:SS"),
|
||||
]),
|
||||
"text",
|
||||
)
|
||||
.as("time"),
|
||||
"messages.sender",
|
||||
"messages.isread",
|
||||
"messages.is_forwarded",
|
||||
"messages.reply_to_id",
|
||||
"users.nome as sender_nome",
|
||||
"users.isAdmin as sender_isAdmin",
|
||||
withUserColorStr(),
|
||||
jsonObjectFrom(
|
||||
eb
|
||||
.selectFrom("messages as reply_source")
|
||||
.select([
|
||||
"reply_source.messageid",
|
||||
"reply_source.message",
|
||||
"reply_source.sender",
|
||||
])
|
||||
.innerJoin("users", "users.id", "reply_source.sender")
|
||||
.select(["users.nome", withUserColorStr()])
|
||||
.whereRef("messages.reply_to_id", "=", "reply_source.messageid")
|
||||
.limit(1),
|
||||
).as("reply_source"),
|
||||
])
|
||||
.where("messages.chatid", "=", input.chatId)
|
||||
.where("messages.messageid", ">", lastMessageId)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import { getUserInfo } from "~/utils/kysely-helper";
|
|||
|
||||
export type ActiveChatsType = Pick<Chats, "chatid" | "created_at"> &
|
||||
Pick<Users, "id" | "username" | "email" | "nome"> & {
|
||||
lastmessage: Message | null;
|
||||
lastmessage: Pick<
|
||||
Message,
|
||||
"messageid" | "message" | "time" | "sender" | "isread"
|
||||
> | null;
|
||||
etichette: Etichette[];
|
||||
};
|
||||
|
||||
|
|
@ -31,24 +34,20 @@ export const getActiveInfos = async (): Promise<ActiveChatsType[]> => {
|
|||
.selectFrom("messages")
|
||||
.whereRef("messages.chatid", "=", "chats.chatid")
|
||||
.innerJoin("users", "users.id", "messages.sender")
|
||||
.select([
|
||||
.select((eb) => [
|
||||
"messages.message",
|
||||
"messages.messageid",
|
||||
"messages.chatid",
|
||||
(eb) =>
|
||||
eb
|
||||
.cast<string>(
|
||||
eb.fn("to_char", [
|
||||
"messages.time",
|
||||
eb.val("YYYY-MM-DD HH24:MI:SS"),
|
||||
]),
|
||||
"text",
|
||||
)
|
||||
.as("time"),
|
||||
eb
|
||||
.cast<string>(
|
||||
eb.fn("to_char", [
|
||||
"messages.time",
|
||||
eb.val("YYYY-MM-DD HH24:MI:SS"),
|
||||
]),
|
||||
"text",
|
||||
)
|
||||
.as("time"),
|
||||
"messages.sender",
|
||||
"messages.isread",
|
||||
"users.nome as sender_nome",
|
||||
"users.isAdmin as sender_isAdmin",
|
||||
])
|
||||
.orderBy("messages.messageid", "desc")
|
||||
.limit(1),
|
||||
|
|
|
|||
|
|
@ -100,13 +100,25 @@ export const zEventId = z.custom<EventQueueEventId>(
|
|||
|
||||
export const MessageDataSchema = z.object({
|
||||
messageid: zMessageId,
|
||||
message: z.string().nullable(),
|
||||
message: z.string(),
|
||||
chatid: zChatId,
|
||||
time: z.string(),
|
||||
sender_nome: z.string(),
|
||||
sender_isAdmin: z.boolean(),
|
||||
sender: zUserId,
|
||||
isread: z.boolean(),
|
||||
color_str: z.string(),
|
||||
is_forwarded: z.boolean(),
|
||||
reply_to_id: zMessageId.nullable(),
|
||||
reply_source: z
|
||||
.object({
|
||||
messageid: zMessageId,
|
||||
message: z.string(),
|
||||
nome: z.string(),
|
||||
sender: zUserId,
|
||||
color_str: z.string(),
|
||||
})
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export const TypingDataSchema = z.array(
|
||||
|
|
@ -114,7 +126,7 @@ export const TypingDataSchema = z.array(
|
|||
userId: zUserId,
|
||||
username: z.string(),
|
||||
}),
|
||||
);
|
||||
);
|
||||
|
||||
export const SidebarUpdateSchema = z.object({
|
||||
chatId: zChatId,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expressionBuilder } from "kysely";
|
||||
import { expressionBuilder, sql } from "kysely";
|
||||
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
|
||||
import type Database from "~/schemas/Database";
|
||||
|
||||
|
|
@ -41,3 +41,7 @@ export function withVideos() {
|
|||
// const date = new Date(str);
|
||||
// return isValid(date) ? date : null;
|
||||
// }
|
||||
|
||||
export function withUserColorStr() {
|
||||
return sql<string>`concat(users.username, '', users.id)`.as("color_str");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue