Skip to content

Commit

Permalink
feat: add group chat
Browse files Browse the repository at this point in the history
  • Loading branch information
AntonioErdeljac committed Apr 29, 2023
1 parent 5ec6089 commit 6e804ca
Show file tree
Hide file tree
Showing 18 changed files with 1,115 additions and 99 deletions.
1 change: 1 addition & 0 deletions app/(site)/components/AuthInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const AuthInput: React.FC<AuthInputProps> = ({
autoComplete={id}
{...register(id, { required })}
className={`
form-input
block
w-full
rounded-md
Expand Down
4 changes: 2 additions & 2 deletions app/api/conversations/[conversationId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ export async function DELETE(

const deletedConversation = await prisma.conversation.deleteMany({
where: {
id: conversationId,
userIds: {
hasSome: [currentUser.id]
},
id: conversationId
}
},
});

return NextResponse.json(deletedConversation)
Expand Down
26 changes: 6 additions & 20 deletions app/api/conversations/[conversationId]/seen/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export async function POST(
conversationId
} = params;


if (!currentUser?.id || !currentUser?.email) {
return new NextResponse('Unauthorized', { status: 401 });
}
Expand Down Expand Up @@ -66,24 +67,9 @@ export async function POST(
}
});

// Get new synced conversation with new seen
const syncedConversation = await prisma.conversation.findUnique({
where: {
id: conversationId
},
include: {
users: true,
messages: {
include: {
seen: true
}
}
}
})

// Update all connections with new seen
pusherServer.trigger(currentUser.email, 'conversation:update', {
...syncedConversation,
await pusherServer.trigger(currentUser.email, 'conversation:update', {
id: conversationId,
messages: [updatedMessage]
});

Expand All @@ -93,11 +79,11 @@ export async function POST(
}

// Update last message seen
pusherServer.trigger(conversationId!, 'message:update', updatedMessage);
await pusherServer.trigger(conversationId!, 'message:update', updatedMessage);

return NextResponse.json(syncedConversation)
return new NextResponse('Success');
} catch (error) {
console.log(error, 'ERROR_MESSAGES')
console.log(error, 'ERROR_MESSAGES_SEEN')
return new NextResponse('Error', { status: 500 });
}
}
38 changes: 35 additions & 3 deletions app/api/conversations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,49 @@ export async function POST(
const currentUser = await getCurrentUser();
const body = await request.json();
const {
userId
userId,
isGroup,
members,
name
} = body;

if (!currentUser?.id || !currentUser?.email || !userId) {
if (!currentUser?.id || !currentUser?.email) {
return NextResponse.json(null);
}

if (isGroup) {
const newConversation = await prisma.conversation.create({
data: {
name,
isGroup,
users: {
connect: [
...members.map((member: { value: string }) => ({ id: member.value })),
{
id: currentUser.id
}
]
}
},
include: {
users: true,
}
});

// Update all connections with new conversation
newConversation.users.forEach((user) => {
if (user.email) {
pusherServer.trigger(user.email, 'conversation:new', newConversation);
}
});

return NextResponse.json(newConversation);
}

const existingConversations = await prisma.conversation.findMany({
where: {
userIds: {
hasEvery: [currentUser.id, userId]
equals: [currentUser.id, userId]
}
}
});
Expand Down
16 changes: 9 additions & 7 deletions app/api/messages/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,21 @@ export async function POST(
},
include: {
users: true,
messages: true
messages: {
include: {
seen: true
}
}
}
});

const lastMessage = updatedConversation.messages[updatedConversation.messages.length - 1];

updatedConversation.users.map((user) => {
if (user.email) {
pusherServer.trigger(user.email, 'conversation:update', {
...updatedConversation,
messages: [lastMessage]
});
}
pusherServer.trigger(user.email!, 'conversation:update', {
id: conversationId,
messages: [lastMessage]
});
});

return NextResponse.json(newMessage)
Expand Down
44 changes: 44 additions & 0 deletions app/components/AvatarGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';

import { classNames } from "../helpers";
import { User } from "@prisma/client";

interface AvatarGroupProps {
users?: User[];
large?: boolean;
};

const AvatarGroup: React.FC<AvatarGroupProps> = ({ users = [], large }) => {
const slicedUsers = users.slice(0, 3);

const positionMap = {
0: 'top-0 left-[12px]',
1: 'bottom-0',
2: 'bottom-0 right-0'
}

return (
<div className="relative h-11 w-11">
{slicedUsers.map((user, index) => (
<div
key={user.id}
className={`
absolute
inline-block
rounded-full
overflow-hidden
h-[21px]
w-[21px]
${positionMap[index as keyof typeof positionMap]}
`}>
<img
src={user?.imageUrl || '/images/placeholder.jpg'}
alt=""
/>
</div>
))}
</div>
);
}

export default AvatarGroup;
91 changes: 91 additions & 0 deletions app/components/GroupChatModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use client';

import React from 'react'
import { useRouter } from 'next/navigation';
import AuthInput from '@/app/(site)/components/AuthInput';
import { FieldValues, SubmitHandler, useForm } from 'react-hook-form';
import Modal from './Modal';
import { User } from '@prisma/client';
import axios from 'axios';
import Select from './Select';

interface GroupChatModalProps {
isOpen?: boolean;
onClose: () => void;
users: User[];
}

const GroupChatModal: React.FC<GroupChatModalProps> = ({ isOpen, onClose, users = [] }) => {
const router = useRouter();

const {
register,
handleSubmit,
setValue,
watch,
formState: {
errors,
}
} = useForm<FieldValues>({
defaultValues: {
name: '',
members: []
}
});

const members = watch('members');

const onSubmit: SubmitHandler<FieldValues> = (data) => {
axios.post('/api/conversations', {
...data,
isGroup: true
})
.then(() => {
router.refresh();
onClose();
})
}

return (
<Modal isOpen={isOpen} onClose={onClose}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-12">
<div className="border-b border-gray-900/10 pb-12">
<h2 className="text-base font-semibold leading-7 text-gray-900">Create a group chat</h2>
<p className="mt-1 text-sm leading-6 text-gray-600">
Create a chat with more than 2 people.
</p>
<div className="mt-10 flex flex-col gap-y-8">
<AuthInput
label="Name"
id="name"
errors={errors}
required
register={register}
/>
<Select
label="Members"
options={users.map((user) => ({ value: user.id, label: user.name }))}
onChange={(value) => setValue('members', value, { shouldValidate: true })}
value={members}
/>
</div>
</div>
</div>
<div className="mt-6 flex items-center justify-end gap-x-6">
<button onClick={onClose} type="button" className="text-sm font-semibold leading-6 text-gray-900">
Cancel
</button>
<button
type="submit"
className="rounded-md bg-sky-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600"
>
Create
</button>
</div>
</form>
</Modal>
)
}

export default GroupChatModal;
46 changes: 46 additions & 0 deletions app/components/Select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import ReactSelect from 'react-select'

interface SelectProps {
label: string;
value?: Record<string, any>;
onChange: (value: Record<string, any>) => void;
options: Record<string, any>[];
}

const Select: React.FC<SelectProps> = ({
label,
value,
onChange,
options
}) => {
return (
<div>
<label
className="
block
text-sm
font-medium
leading-6
text-gray-900
"
>
{label}
</label>
<div className="mt-2">
<ReactSelect
value={value}
onChange={onChange}
isMulti
options={options}
classNames={{
control: () => 'text-sm',
}}
/>
</div>
</div>
);
}

export default Select;
4 changes: 3 additions & 1 deletion app/conversations/[conversationId]/components/Body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Message, User } from "@prisma/client";

import { pusherClient } from "@/app/libs/pusher";
import MessageBox from "./MessageBox";
import { useSession } from "next-auth/react";

type MessageType = Message & { sender: User, seen: User[] };

Expand All @@ -15,6 +16,7 @@ interface BodyProps {
}

const Body: React.FC<BodyProps> = ({ initialMessages = [] }) => {
const session = useSession();
const bottomRef = useRef<HTMLDivElement>(null);
const [messages, setMessages] = useState(initialMessages);
const params = useParams();
Expand All @@ -29,6 +31,7 @@ const Body: React.FC<BodyProps> = ({ initialMessages = [] }) => {
bottomRef?.current?.scrollIntoView();

const messageHandler = (message: MessageType) => {
console.log('NEW_MESSAGE?', message)
axios.post(`/api/conversations/${conversationId}/seen`);

setMessages((current) => [...current, message]);
Expand All @@ -37,7 +40,6 @@ const Body: React.FC<BodyProps> = ({ initialMessages = [] }) => {
};

const updateMessageHandler = (newMessage: MessageType) => {
console.log('UDPATE_HANDLER', newMessage)
setMessages((current) => current.map((currentMessage) => {
if (currentMessage.id === newMessage.id) {
return newMessage;
Expand Down
Loading

0 comments on commit 6e804ca

Please sign in to comment.