-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
654 additions
and
84 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { NextResponse } from "next/server"; | ||
import { Message } from "@prisma/client"; | ||
|
||
import { currentProfile } from "@/lib/current-profile"; | ||
import { db } from "@/lib/db"; | ||
|
||
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 channelId = searchParams.get("channelId"); | ||
|
||
if (!profile) { | ||
return new NextResponse("Unauthorized", { status: 401 }); | ||
} | ||
|
||
if (!channelId) { | ||
return new NextResponse("Channel ID missing", { status: 400 }); | ||
} | ||
|
||
let messages: Message[] = []; | ||
|
||
if (cursor) { | ||
messages = await db.message.findMany({ | ||
take: MESSAGES_BATCH, | ||
skip: 1, | ||
cursor: { | ||
id: cursor, | ||
}, | ||
where: { | ||
channelId, | ||
}, | ||
include: { | ||
member: { | ||
include: { | ||
profile: true, | ||
} | ||
} | ||
}, | ||
orderBy: { | ||
createdAt: "desc", | ||
} | ||
}) | ||
} else { | ||
messages = await db.message.findMany({ | ||
take: MESSAGES_BATCH, | ||
where: { | ||
channelId, | ||
}, | ||
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("[MESSAGES_GET]", error); | ||
return new NextResponse("Internal Error", { status: 500 }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
"use client"; | ||
|
||
import * as z from "zod"; | ||
import axios from "axios"; | ||
import qs from "query-string"; | ||
import { Member, Profile } from "@prisma/client"; | ||
import { UserAvatar } from "@/components/user-avatar"; | ||
import { ActionTooltip } from "@/components/action-tooltip"; | ||
import { Edit, FileIcon, ShieldCheck, Trash } from "lucide-react"; | ||
import Image from "next/image"; | ||
import { cn } from "@/lib/utils"; | ||
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; | ||
import { Input } from "@/components/ui/input"; | ||
import { Button } from "@/components/ui/button"; | ||
import { useEffect, useState } from "react"; | ||
import { useForm } from "react-hook-form"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { useModal } from "@/hooks/use-modal-store"; | ||
import { useParams, useRouter } from "next/navigation"; | ||
|
||
interface ChatItemProps { | ||
id: string; | ||
content: string; | ||
member: Member & { | ||
profile: Profile; | ||
}; | ||
timestamp: string; | ||
fileUrl: string | null; | ||
deleted: boolean; | ||
currentMember: Member; | ||
isUpdated: boolean; | ||
socketUrl: string; | ||
socketQuery: Record<string, string>; | ||
} | ||
|
||
const roleIconMap = { | ||
GUEST: null, | ||
MODERATOR: <ShieldCheck className="h-4 w-4 ml-2 text-amber-500" />, | ||
ADMIN: <ShieldCheck className="h-4 w-4 ml-2 text-rose-500" />, | ||
}; | ||
|
||
const formSchema = z.object({ | ||
content: z.string().min(1), | ||
}); | ||
|
||
export const ChatItem = ({ | ||
id, | ||
content, | ||
member, | ||
timestamp, | ||
fileUrl, | ||
deleted, | ||
currentMember, | ||
isUpdated, | ||
socketUrl, | ||
socketQuery, | ||
}: ChatItemProps) => { | ||
const [isEditing, setIsEditing] = useState(false); | ||
const { onOpen } = useModal(); | ||
const params = useParams(); | ||
const router = useRouter(); | ||
|
||
const onMemberClick = () => { | ||
if (member.id === currentMember.id) { | ||
return; | ||
} | ||
|
||
router.push(`/servers/${params?.serverId}/conversations/${member.id}`); | ||
}; | ||
|
||
useEffect(() => { | ||
const handleKeyDown = (event: any) => { | ||
if (event.key === "Escape" || event.keyCode === 27) { | ||
setIsEditing(false); | ||
} | ||
}; | ||
|
||
window.addEventListener("keydown", handleKeyDown); | ||
|
||
return () => window.removeEventListener("keyDown", handleKeyDown); | ||
}, []); | ||
|
||
const form = useForm<z.infer<typeof formSchema>>({ | ||
resolver: zodResolver(formSchema), | ||
defaultValues: { | ||
content: content, | ||
}, | ||
}); | ||
|
||
const isLoading = form.formState.isSubmitting; | ||
|
||
const onSubmit = async (values: z.infer<typeof formSchema>) => { | ||
try { | ||
const url = qs.stringifyUrl({ | ||
url: `${socketUrl}/${id}`, | ||
query: socketQuery, | ||
}); | ||
|
||
await axios.patch(url, values); | ||
|
||
form.reset(); | ||
setIsEditing(false); | ||
} catch (error) { | ||
console.log(error); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
form.reset({ | ||
content: content, | ||
}); | ||
}, [content, form]); | ||
|
||
const fileType = fileUrl?.split(".").pop(); | ||
|
||
const isAdmin = currentMember.role === "ADMIN"; | ||
const isModerator = currentMember.role === "MODERATOR"; | ||
const isOwner = currentMember.id === member.id; | ||
const canDeleteMessage = !deleted && (isAdmin || isModerator || isOwner); | ||
const canEditMessage = !deleted && isOwner && !fileUrl; | ||
const isPDF = fileType === "pdf" && fileUrl; | ||
const isImage = !isPDF && fileUrl; | ||
|
||
return ( | ||
<div className="relative group flex items-center hover:bg-black/5 p-4 transition w-full"> | ||
<div className="group flex gap-x-2 items-start w-full"> | ||
<div onClick={onMemberClick} className="cursor-pointer hover:drop-shadow-md transition"> | ||
<UserAvatar src={member.profile.imageUrl} /> | ||
</div> | ||
|
||
<div className="flex flex-col w-full"> | ||
<div className="flex items-center gap-x-2"> | ||
<div className="flex items-center"> | ||
<p onClick={onMemberClick} className="font-semibold text-amber-600 text-sm hover:underline cursor-pointer"> | ||
{member.profile.name} | ||
</p> | ||
<ActionTooltip label={member.role}> | ||
{roleIconMap[member.role]} | ||
</ActionTooltip> | ||
</div> | ||
<span className="text-xs text-zinc-400 ">{timestamp}</span> | ||
</div> | ||
{isImage && ( | ||
<a | ||
href={fileUrl} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
className="relative aspect-square rounded-md mt-2 overflow-hidden border flex items-center bg-secondary h-48 w-48" | ||
> | ||
<Image | ||
src={fileUrl} | ||
alt={content} | ||
fill | ||
className="object-cover" | ||
sizes="100vw" | ||
priority={true} | ||
loading="eager" | ||
/> | ||
</a> | ||
)} | ||
{isPDF && ( | ||
<div className="relative flex items-center p-2 mt-2 rounded-md bg-background/10"> | ||
<FileIcon className="h-10 w-10 fill-amber-500 stroke-amber-800" /> | ||
<a | ||
href={fileUrl} | ||
target="_blank" | ||
rel="noopener noreferrer" | ||
className="ml-2 text-sm text-amber-500 hover:underline" | ||
> | ||
PDF File | ||
</a> | ||
</div> | ||
)} | ||
{!fileUrl && !isEditing && ( | ||
<p | ||
className={cn( | ||
"text-xl text-[#ffbd80]", | ||
deleted && | ||
"italic text-amber-400 text-md mt-1" | ||
)} | ||
> | ||
{content} | ||
{isUpdated && !deleted && ( | ||
<span className="text-[10px] mx-2 text-amber-400"> | ||
(edited) | ||
</span> | ||
)} | ||
</p> | ||
)} | ||
{!fileUrl && isEditing && ( | ||
<Form {...form}> | ||
<form | ||
className="flex items-center w-full gap-x-2 pt-2" | ||
onSubmit={form.handleSubmit(onSubmit)} | ||
> | ||
<FormField | ||
control={form.control} | ||
name="content" | ||
render={({ field }) => ( | ||
<FormItem className="flex-1"> | ||
<FormControl> | ||
<div className="relative w-full"> | ||
<Input | ||
disabled={isLoading} | ||
className="p-2 bg-zinc-700/75 border-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 text-zinc-200" | ||
placeholder="Edited message" | ||
{...field} | ||
/> | ||
</div> | ||
</FormControl> | ||
</FormItem> | ||
)} | ||
/> | ||
<Button disabled={isLoading} size="sm" variant="theme"> | ||
Save | ||
</Button> | ||
</form> | ||
<span className="text-[10px] mt-1 text-zinc-400"> | ||
Press escape to cancel, enter to save | ||
</span> | ||
</Form> | ||
)} | ||
</div> | ||
</div> | ||
{canDeleteMessage && ( | ||
<div className="hidden group-hover:flex items-center gap-x-2 absolute p-1 -top-2 right-5 bg-white dark:bg-zinc-800 border rounded-sm"> | ||
{canEditMessage && ( | ||
<ActionTooltip label="Edit"> | ||
<Edit | ||
onClick={() => setIsEditing(true)} | ||
className="cursor-pointer ml-auto w-4 h-4 text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 transition" | ||
/> | ||
</ActionTooltip> | ||
)} | ||
<ActionTooltip label="Delete"> | ||
<Trash | ||
onClick={() => onOpen("deleteMessage", { | ||
apiUrl: `${socketUrl}/${id}`, | ||
query: socketQuery, | ||
})} | ||
className="cursor-pointer ml-auto w-4 h-4 text-zinc-500 hover:text-zinc-300 transition" | ||
/> | ||
</ActionTooltip> | ||
</div> | ||
)} | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.