diff --git a/app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx b/app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx index 8377153..07640f3 100644 --- a/app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx +++ b/app/(main)/(routes)/servers/[serverId]/conversations/[memberId]/page.tsx @@ -1,4 +1,6 @@ import { ChatHeader } from "@/components/chat/chat-header"; +import { ChatInput } from "@/components/chat/chat-input"; +import { ChatMessages } from "@/components/chat/chat-messages"; import { getOrCreateConversation } from "@/lib/conversation"; import { currentProfile } from "@/lib/current-profile"; import { db } from "@/lib/db"; @@ -53,6 +55,27 @@ const MemberIdPage = async ({ serverId={params.serverId} type={"conversation"} /> + + ) } diff --git a/app/api/direct-messages/route.ts b/app/api/direct-messages/route.ts new file mode 100644 index 0000000..674796e --- /dev/null +++ b/app/api/direct-messages/route.ts @@ -0,0 +1,83 @@ +import { currentProfile } from "@/lib/current-profile"; +import { db } from "@/lib/db"; +import { DirectMessage } from "@prisma/client"; + +import { NextResponse } from "next/server"; + +const MESSAGES_BATCH = 10; + +export async function GET( + req: Request +) { + try { + const profile = await currentProfile(); + const { searchParams } = new URL(req.url); + + const cursor = searchParams.get("cursor"); + const conversationId = searchParams.get("conversationId"); + + if (!profile) { + return new NextResponse("Unauthorized", { status: 401 }); + } + + if (!conversationId) { + return new NextResponse("Conversation ID missing", { status: 400 }); + } + + let messages: DirectMessage[] = []; + + if (cursor) { + messages = await db.directMessage.findMany({ + take: MESSAGES_BATCH, + skip: 1, + cursor: { + id: cursor, + }, + where: { + conversationId, + }, + include: { + member: { + include: { + profile: true, + } + } + }, + orderBy: { + createdAt: "desc", + }, + }); + } else { + messages = await db.directMessage.findMany({ + take: MESSAGES_BATCH, + where: { + conversationId, + }, + include: { + member: { + include: { + profile: true, + } + } + }, + orderBy: { + createdAt: "desc", + }, + }); + } + + let nextCursor = null; + + if (messages.length === MESSAGES_BATCH) { + nextCursor = messages[MESSAGES_BATCH - 1].id; + } + + return NextResponse.json({ + items: messages, + nextCursor, + }); + } catch (error) { + console.log("[DIRECT_MESSAGES_GET]", error); + return new NextResponse("Internal Error", { status: 500 }) + } +} \ No newline at end of file diff --git a/pages/api/socket/direct-messages/[directmessageId].ts b/pages/api/socket/direct-messages/[directmessageId].ts new file mode 100644 index 0000000..4c32321 --- /dev/null +++ b/pages/api/socket/direct-messages/[directmessageId].ts @@ -0,0 +1,145 @@ +import { currentProfilePages } from "@/lib/current-profile-pages"; +import { db } from "@/lib/db"; +import { NextApiResponseServerIo } from "@/types"; +import { MemberRole } from "@prisma/client"; +import { NextApiRequest } from "next"; + +export default async function handler(req: NextApiRequest, res: NextApiResponseServerIo) { + if (req.method !== "DELETE" && req.method !== "PATCH") { + return res.status(405).json({ error: "Method not allowed" }); + } + + try { + const profile = await currentProfilePages(req); + const { directMessageId, conversationId } = req.query; + const { content } = req.body; + + if (!profile) { + return res.status(401).json({ error: "Unauthorized" }); + } + + if (!conversationId) { + return res.status(400).json({ error: "Conversation ID Missing" }); + } + + const conversation = await db.conversation.findFirst({ + where: { + id: conversationId as string, + OR: [ + { + memberOne: { + profileId: profile.id + } + }, + { + memberTwo: { + profileId: profile.id + } + }, + ] + }, + include: { + memberOne: { + include: { + profile: true + } + }, + memberTwo: { + include: { + profile: true + } + } + } + }); + + if (!conversation) { + return res.status(404).json({ error: "Conversation not found" }); + } + + const member = conversation.memberOne.profileId === profile.id ? + conversation.memberOne : conversation.memberTwo; + + if (!member) { + return res.status(404).json({ error: "Member not found" }); + } + + let directMessage = await db.directMessage.findFirst({ + where: { + id: directMessageId as string, + conversationId: conversationId as string, + }, + include: { + member: { + include: { + profile: true, + } + } + } + }); + + if (!directMessage || directMessage.deleted) { + return res.status(404).json({ error: "Message not found" }); + } + + const isMessageOwner = directMessage.memberId === member.id; + const isAdmin = member.role === MemberRole.ADMIN; + const isModerator = member.role === MemberRole.MODERATOR; + const canModify = isMessageOwner || isAdmin || isModerator; + + if (!canModify) { + return res.status(401).json({ error: "Unauthorized" }); + } + + if (req.method === "DELETE") { + directMessage = await db.directMessage.update({ + where: { + id: directMessageId as string, + }, + data: { + fileUrl: null, + content: "This message has been deleted.", + deleted: true, + }, + include: { + member: { + include: { + profile: true, + } + } + } + }); + } + + if (req.method === "PATCH") { + if (!isMessageOwner) { + return res.status(401).json({ error: "Unauthorized" }); + } + + directMessage = await db.directMessage.update({ + where: { + id: directMessageId as string, + }, + data: { + content, + }, + include: { + member: { + include: { + profile: true, + } + } + } + }); + } + + const updateKey = `chat:${conversationId}:messages:update`; + + res?.socket?.server?.io?.to(updateKey).emit(updateKey, directMessage); + + return res.status(200).json(directMessage); + + } catch (error) { + console.error(error); + return res.status(500).json({ error: "Internal Error" }); + } +} \ No newline at end of file diff --git a/pages/api/socket/direct-messages/index.ts b/pages/api/socket/direct-messages/index.ts new file mode 100644 index 0000000..7dfb852 --- /dev/null +++ b/pages/api/socket/direct-messages/index.ts @@ -0,0 +1,95 @@ +import { currentProfilePages } from "@/lib/current-profile-pages"; +import { NextApiResponseServerIo } from "@/types"; +import { NextApiRequest } from "next"; +import { db } from "@/lib/db"; + +export default async function handler(req: NextApiRequest, res: NextApiResponseServerIo) { + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + try { + const profile = await currentProfilePages(req); + const { content, fileUrl } = req.body; + const { conversationId } = req.query; + + if (!profile) { + return res.status(401).json({ error: "Unauthorized" }); + } + + if (!conversationId) { + return res.status(400).json({ error: "Conversation ID Missing" }); + } + + if (!content) { + return res.status(400).json({ error: "Content Missing" }); + } + + const conversation = await db.conversation.findFirst({ + where: { + id: conversationId as string, + OR: [ + { + memberOne: { + profileId: profile.id + } + }, + { + memberTwo: { + profileId: profile.id + } + }, + ] + }, + include: { + memberOne: { + include: { + profile: true + } + }, + memberTwo: { + include: { + profile: true + } + } + } + }); + + if (!conversation) { + return res.status(404).json({ error: "Conversation not found" }); + } + + const member = conversation.memberOne.profileId === profile.id ? + conversation.memberOne : conversation.memberTwo; + + if (!member) { + return res.status(401).json({ error: "Member not found" }); + } + + const message = await db.directMessage.create({ + data: { + content, + fileUrl, + conversationId: conversationId as string, + memberId: member.id, + }, + include: { + member: { + include: { + profile: true + } + } + } + }); + + const channelKey = `chat:${conversationId}:messages`; + + res?.socket?.server?.io?.emit(channelKey, message); + + return res.status(200).json({ message }); + } + catch (error) { + console.log("[DIRECT_MESSAGES_POST]", error); + return res.status(500).json({ message: "Internal Error" }); + } +} \ No newline at end of file