Skip to content

Commit

Permalink
添加聊天部分功能
Browse files Browse the repository at this point in the history
  • Loading branch information
floatGray committed Dec 4, 2023
1 parent ca7c658 commit 8da3988
Show file tree
Hide file tree
Showing 34 changed files with 2,119 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { ChannelType } from '@prisma/client';

import { currentProfile } from '@/lib/current-profile';
import { ChatHeader } from '@/components/chat/chat-header';
import { ChatInput } from '@/components/chat/chat-input';
import { ChatMessages } from '@/components/chat/chat-messages';
import { MediaRoom } from '@/components/media-room';
import { db } from '@/lib/db';

interface ChannelIdPageProps {
Expand Down Expand Up @@ -44,6 +47,39 @@ const ChannelIdPage = async ({ params }: ChannelIdPageProps) => {
serverId={channel.serverId}
type="channel"
/>
{channel.type === ChannelType.TEXT && (
<>
<ChatMessages
member={member}
name={channel.name}
chatId={channel.id}
type="channel"
apiUrl="/api/messages"
socketUrl="/api/socket/messages"
socketQuery={{
channelId: channel.id,
serverId: channel.serverId,
}}
paramKey="channelId"
paramValue={channel.id}
/>
<ChatInput
name={channel.name}
type="channel"
apiUrl="/api/socket/messages"
query={{
channelId: channel.id,
serverId: channel.serverId,
}}
/>
</>
)}
{channel.type === ChannelType.AUDIO && (
<MediaRoom chatId={channel.id} video={false} audio={true} />
)}
{channel.type === ChannelType.VIDEO && (
<MediaRoom chatId={channel.id} video={true} audio={true} />
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,97 @@
const MemberIdPage = () => {
return <div>MemberIdPage</div>;
import { redirectToSignIn } from '@clerk/nextjs';
import { redirect } from 'next/navigation';

import { db } from '@/lib/db';
import { getOrCreateConversation } from '@/lib/conversation';
import { currentProfile } from '@/lib/current-profile';
import { ChatHeader } from '@/components/chat/chat-header';
import { ChatMessages } from '@/components/chat/chat-messages';
import { ChatInput } from '@/components/chat/chat-input';
import { MediaRoom } from '@/components/media-room';

interface MemberIdPageProps {
params: {
memberId: string;
serverId: string;
};
searchParams: {
video?: boolean;
};
}

const MemberIdPage = async ({ params, searchParams }: MemberIdPageProps) => {
const profile = await currentProfile();

if (!profile) {
return redirectToSignIn();
}

const currentMember = await db.member.findFirst({
where: {
serverId: params.serverId,
profileId: profile.id,
},
include: {
profile: true,
},
});

if (!currentMember) {
return redirect('/');
}

const conversation = await getOrCreateConversation(
currentMember.id,
params.memberId
);

if (!conversation) {
return redirect(`/servers/${params.serverId}`);
}

const { memberOne, memberTwo } = conversation;

const otherMember =
memberOne.profileId === profile.id ? memberTwo : memberOne;

return (
<div className="bg-white dark:bg-[#313338] flex flex-col h-full">
<ChatHeader
imageUrl={otherMember.profile.imageUrl}
name={otherMember.profile.name}
serverId={params.serverId}
type="conversation"
/>
{searchParams.video && (
<MediaRoom chatId={conversation.id} video={true} audio={true} />
)}
{!searchParams.video && (
<>
<ChatMessages
member={currentMember}
name={otherMember.profile.name}
chatId={conversation.id}
type="conversation"
apiUrl="/api/direct-messages"
paramKey="conversationId"
paramValue={conversation.id}
socketUrl="/api/socket/direct-messages"
socketQuery={{
conversationId: conversation.id,
}}
/>
<ChatInput
name={otherMember.profile.name}
type="conversation"
apiUrl="/api/socket/direct-messages"
query={{
conversationId: conversation.id,
}}
/>
</>
)}
</div>
);
};

export default MemberIdPage;
81 changes: 81 additions & 0 deletions app/api/messages/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 });
}
}
4 changes: 2 additions & 2 deletions app/api/uploadthing/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ const f = createUploadthing();

const handleAuth = () => {
const { userId } = auth();
if (!userId) throw new Error('没有登录');
return { userId };
if (!userId) throw new Error('Unauthorized');
return { userId: userId };
};

export const ourFileRouter = {
Expand Down
15 changes: 10 additions & 5 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import './globals.css';
import type { Metadata } from 'next';
import { Open_Sans } from 'next/font/google';
import './globals.css';
import { ClerkProvider } from '@clerk/nextjs';

import { cn } from '@/lib/utils';
import { ThemeProvider } from '@/components/providers/theme-provider';
import { ModalProvider } from '@/components/providers/modal-provider';
import { cn } from '@/lib/utils';
import { SocketProvider } from '@/components/providers/socket-provider';
import { QueryProvider } from '@/components/providers/query-provider';

const font = Open_Sans({ subsets: ['latin'] });

export const metadata: Metadata = {
title: '团队聊天',
title: 'Team Chat Application',
description: 'Generated by create next app',
};

Expand All @@ -28,8 +31,10 @@ export default function RootLayout({
enableSystem={false}
storageKey="discord-theme"
>
<ModalProvider />
{children}
<SocketProvider>
<ModalProvider />
<QueryProvider>{children}</QueryProvider>
</SocketProvider>
</ThemeProvider>
</body>
</html>
Expand Down
4 changes: 4 additions & 0 deletions components/chat/chat-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Hash } from 'lucide-react';

import { MobileToggle } from '@/components/mobile-toggle';
import { UserAvatar } from '@/components/user-avatar';
import { SocketIndicator } from '@/components/socket-indicator';

interface ChatHeaderProps {
serverId: string;
Expand All @@ -26,6 +27,9 @@ export const ChatHeader = ({
<UserAvatar src={imageUrl} className="h-8 w-8 md:h-8 md:w-8 mr-2" />
)}
<p className="font-semibold text-md text-black dark:text-white">{name}</p>
<div className="ml-auto flex items-center">
<SocketIndicator />
</div>
</div>
);
};
96 changes: 96 additions & 0 deletions components/chat/chat-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use client';

import * as z from 'zod';
import axios from 'axios';
import qs from 'query-string';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Plus } from 'lucide-react';
import { useRouter } from 'next/navigation';

import { Form, FormControl, FormField, FormItem } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useModal } from '@/hooks/use-modal-store';
import { EmojiPicker } from '@/components/emoji-picker';

interface ChatInputProps {
apiUrl: string;
query: Record<string, any>;
name: string;
type: 'conversation' | 'channel';
}

const formSchema = z.object({
content: z.string().min(1),
});

export const ChatInput = ({ apiUrl, query, name, type }: ChatInputProps) => {
const { onOpen } = useModal();
const router = useRouter();

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
content: '',
},
});

const isLoading = form.formState.isSubmitting;

const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
const url = qs.stringifyUrl({
url: apiUrl,
query,
});

await axios.post(url, values);

form.reset();
router.refresh();
} catch (error) {
console.log(error);
}
};

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="relative p-4 pb-6">
<button
type="button"
onClick={() => onOpen('messageFile', { apiUrl, query })}
className="absolute top-7 left-8 h-[24px] w-[24px] bg-zinc-500 dark:bg-zinc-400 hover:bg-zinc-600 dark:hover:bg-zinc-300 transition rounded-full p-1 flex items-center justify-center"
>
<Plus className="text-white dark:text-[#313338]" />
</button>
<Input
disabled={isLoading}
className="px-14 py-6 bg-zinc-200/90 dark:bg-zinc-700/75 border-none border-0 focus-visible:ring-0 focus-visible:ring-offset-0 text-zinc-600 dark:text-zinc-200"
placeholder={`Message ${
type === 'conversation' ? name : '#' + name
}`}
{...field}
/>
<div className="absolute top-7 right-8">
<EmojiPicker
onChange={(emoji: string) =>
field.onChange(`${field.value} ${emoji}`)
}
/>
</div>
</div>
</FormControl>
</FormItem>
)}
/>
</form>
</Form>
);
};
Loading

0 comments on commit 8da3988

Please sign in to comment.