Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Implement chat DMs between friends #128

Merged
merged 6 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ export class ChatGateway
@SubscribeMessage('listChats')
async listChats(@ConnectedSocket() client: Socket) {
const chats = await this.chatService.listChats();
// TODO: do not display PRIVATE CHATS
client.emit('listChats', chats);
}

Expand Down Expand Up @@ -180,6 +179,18 @@ export class ChatGateway
client.emit('error', { error: 'Protected chat must have password' });
return;
}
if (chatType === 'PRIVATE') {
// check if a chat with that name already exists
const createdChat = await this.chatService.getChatByName(chatName);
if (createdChat) {
client.join(`chat:${createdChat.id}`);
client.emit('joinChat', {
message: `You joined chat ${createdChat.id}`,
});
client.emit('createChat', createdChat);
return;
}
}
const createdChat = await this.chatService.createChat(
login,
chatName,
Expand Down
13 changes: 13 additions & 0 deletions backend/src/chat/chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,4 +665,17 @@ export class ChatService {

return isPasswordValid;
}

async getChatByName(name: string): Promise<Chat> {
try {
const chat = await this.prisma.chat.findFirst({
where: {
name,
},
});
return chat;
} catch (error) {
return null;
}
}
}
10 changes: 6 additions & 4 deletions frontend/src/components/Chat/ChannelCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArrowRight, Crown, Lock, TrashSimple } from "@phosphor-icons/react";
import { ArrowRight, Crown, EnvelopeSimple, Lock, TrashSimple } from "@phosphor-icons/react";
import { useContext } from "react";
import { Chat, ChatContext } from "@/contexts/ChatContext";
import chatService from "@/services/chatClient";
Expand All @@ -22,28 +22,30 @@ export default function ChannelCard({ chat }: ChannelCardProps) {
};

return (
// TODO: add user context for chat owner
<div className="bg-black42-200 flex justify-between rounded-lg items-center p-3 my-1">
<div className="flex space-x-2 items-center">
<span className="cursor-pointer" onClick={() => handleOpenChannel()}>
{chat.name}
</span>
<div className="flex ml-1 space-x-1">
{chat.chatType === 'PROTECTED' && <Lock color="white" size={12} />}
{chat.owner === user.login && <Crown className="text-orange42-500" size={12} />}
{chat.owner === user.login && <Crown className="text-orange42-500" alt="Channel owner" size={12} />}
{chat.chatType === 'PROTECTED' && <Lock color="white" alt="Password protected" size={12} />}
{chat.chatType === 'PRIVATE' && <EnvelopeSimple color="white" alt="Direct message" size={12} />}
</div>
</div>
<div className="flex space-x-5 items-center">
{chat.owner === user.login && (
<TrashSimple
className="text-red-400 cursor-pointer"
size={18}
alt="Delete channel"
onClick={() => handleDeleteChannel()}
/>
)}
<ArrowRight
className="text-purple42-200 cursor-pointer"
size={18}
alt="Open channel"
onClick={() => handleOpenChannel()}
/>
</div>
Expand Down
13 changes: 6 additions & 7 deletions frontend/src/components/Chat/ChatUsersChannelPopOver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ export default function ChatUsersChannelPopOver({
{...attributes.popper}
>
<div className="p-3">
{/*TODO: Find a way to remove this validation without breaking the database seed*/}
{myUser && (
<div
className="flex items-center space-x-4 mb-4 justify-between"
Expand Down Expand Up @@ -238,7 +237,7 @@ export default function ChatUsersChannelPopOver({
handleDemoteToMember(user);
}}
/>
) /*TODO: Make this command responsive */
)
}
{
user.role !== "MEMBER" && (
Expand All @@ -248,7 +247,7 @@ export default function ChatUsersChannelPopOver({
aria-label="Channel Admin"
alt="Channel Admin"
/>
) /*TODO: Make this command responsive */
)
}
{
myUser &&
Expand All @@ -262,7 +261,7 @@ export default function ChatUsersChannelPopOver({
alt="Mute user"
onClick={() => handleMuteUser(user)}
/>
) /*TODO: Make this command responsive */
)
}
{
myUser &&
Expand All @@ -276,7 +275,7 @@ export default function ChatUsersChannelPopOver({
alt="Unmute user"
onClick={() => handleUnmuteUser(user)}
/>
) /*TODO: Make this command responsive */
)
}
{
myUser &&
Expand All @@ -289,7 +288,7 @@ export default function ChatUsersChannelPopOver({
alt="Ban user"
onClick={() => handleBanUser(user)}
/>
) /*TODO: Make this command responsive */
)
}
{
myUser &&
Expand All @@ -302,7 +301,7 @@ export default function ChatUsersChannelPopOver({
alt="Kick user"
onClick={() => handleKickUser(user)}
/>
) /*TODO: Make this command responsive */
)
}
</div>
</div>
Expand Down
21 changes: 20 additions & 1 deletion frontend/src/components/Chat/FriendCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { api } from "@/services/apiClient";
import { queryClient } from "@/services/queryClient";
import { UserStatus } from "@/types/user";
import { EnvelopeSimple, Sword, UserMinus } from "@phosphor-icons/react";
import chatService from "@/services/chatClient";

import { useMutation } from "@tanstack/react-query";
import Link from "next/link";
import toast from "react-hot-toast";
import { ChatContext } from "@/contexts/ChatContext";
import { useContext } from "react";

type FriendCardProps = {
displayName: string;
Expand All @@ -17,6 +21,8 @@ export default function FriendCard({
id,
status,
}: FriendCardProps) {
const { user } = useContext(ChatContext);

const deleteFriendMutation = useMutation({
mutationFn: (friendData: any) => {
return api.delete("/friends", {
Expand All @@ -36,6 +42,15 @@ export default function FriendCard({
deleteFriendMutation.mutate({ friend_id: id });
};

const handleOpenDirectMessage = () => {
const chatName = `${displayName} - ${user.displayName}`;
chatService.socket?.emit("createChat", {
chatName,
chatType: "PRIVATE",
password: "",
});
};

return (
<div className="bg-black42-200 flex justify-between rounded-lg items-center p-3 my-1">
<div className="flex justify-between items-center cursor-pointer gap-2">
Expand All @@ -52,7 +67,11 @@ export default function FriendCard({
></div>
</div>
<div className="flex space-x-5 items-center">
<EnvelopeSimple className="text-purple42-200" size={18} />
<EnvelopeSimple
className="text-purple42-200 cursor-pointer"
size={18}
onClick={handleOpenDirectMessage}
/>
<Sword className="text-purple42-200" size={18} />
<UserMinus
className="text-purple42-200 cursor-pointer"
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/Chat/ListChannels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ type ListChannelsProps = {
export function ListChannels({ handleShowCreateChannel }: ListChannelsProps) {
const { isLoading, chatList } = useContext(ChatContext);

// filter private chats
const publicChats = chatList?.filter(
(chat: Chat) => chat.chatType !== "PRIVATE"
);

return (
<div className="flex flex-col flex-1 justify-between">
<div className="flex flex-row justify-between items-center h-9">
Expand All @@ -32,8 +37,7 @@ export function ListChannels({ handleShowCreateChannel }: ListChannelsProps) {
className="flex flex-col flex-1 max-h-[80vh] bg-black42-300 overflow-y-scroll overscroll-contain my-4
scrollbar scrollbar-w-1 scrollbar-rounded-lg scrollbar-thumb-rounded-lg scrollbar-thumb-black42-100 scrollbar-track-black42-300"
>
{chatList?.map((channel: Chat) => (
// TODO: add user context for chat owner
{publicChats?.map((channel: Chat) => (
<ChannelCard key={channel.id} chat={channel} />
))}
</div>
Expand Down
36 changes: 21 additions & 15 deletions frontend/src/components/Chat/OpenChannel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ interface Message {
userId: string;
}

type FormInputs = {
password: string;
};

export function OpenChannel() {
const {
selectedChat,
Expand Down Expand Up @@ -68,6 +72,7 @@ export function OpenChannel() {

const handleSendMessage = () => {
if (myUser && myUser.status === "MUTED") return;

chatService.socket?.emit("message", {
chatId: selectedChat.id,
content: message,
Expand All @@ -83,10 +88,6 @@ export function OpenChannel() {
}, 100);
};

type FormInputs = {
password: string;
};

const {
register,
setError,
Expand Down Expand Up @@ -159,14 +160,22 @@ export function OpenChannel() {
<div className="flex flex-col flex-1 justify-between">
<div className="flex flex-row justify-between items-center h-8">
<div className="flex items-center">
<ChatUsersChannelPopOver users={users}>
<div className="flex space-x-1 items-center">
<span className="text-xs">({numberOfUsersInChat})</span>
<UsersThree className="text-white" size={20} />
</div>
</ChatUsersChannelPopOver>
{selectedChat.chatType !== "PRIVATE" && (
<ChatUsersChannelPopOver users={users}>
<div className="flex space-x-1 items-center">
<span className="text-xs">({numberOfUsersInChat})</span>
<UsersThree className="text-white" size={20} />
</div>
</ChatUsersChannelPopOver>
)}
</div>
<h3 className="text-white text-lg">{selectedChat.name}</h3>
<h3 className="text-white text-lg">
{selectedChat.chatType === "PRIVATE"
? `DM: ${selectedChat.name
.split(" - ")
.filter((name) => name !== user.displayName)}`
: selectedChat.name}
</h3>
<XCircle
className="cursor-pointer"
color="white"
Expand All @@ -183,7 +192,6 @@ export function OpenChannel() {
{messages.map((message: any) => (
<div
key={message.id}
// TODO: Implement user context to compare user login with message user
className={`${
message.userLogin === user.login
? "text-white bg-purple42-200 self-end"
Expand Down Expand Up @@ -216,9 +224,7 @@ export function OpenChannel() {
/>
<button
className="bg-purple42-200 text-white rounded-lg p-3 placeholder-gray-700 absolute z-10 right-4"
onClick={() =>
handleSendMessage()
} /*TODO: Check if other users are receiving the message */
onClick={() => handleSendMessage()}
>
{myUser && myUser.status !== "MUTED" ? (
<PaperPlaneTilt size={20} color="white" />
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function Chat() {
const { showElement, setShowElement } = useContext(ChatContext);

return (
<div className="flex flex-col flex-1 min-w-[309px] my-4 bg-black42-300 rounded-lg p-4">
<div className="flex flex-col flex-1 w-[309px] my-4 bg-black42-300 rounded-lg p-4">
<div className="flex flex-col flex-1 justify-end">
<div className={"flex flex-col flex-1 text-white"}>
<div className="flex flex-col flex-1 justify-between">
Expand Down
25 changes: 18 additions & 7 deletions frontend/src/contexts/ChatContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ export const ChatProvider = ({ children }: ChatProviderProps) => {
const handleOpenChannel = (chat: Chat) => {
setSelectedChat(chat);
// check if chat has an id
if (chat.id) {
chatService.socket?.emit("listMessages", { chatId: chat.id });
chatService.socket?.emit("listMembers", { chatId: chat.id });
}
setShowElement("showChannelOpen");
try {
if (chat.id) {
chatService.socket?.emit("listMessages", { chatId: chat.id });
chatService.socket?.emit("listMembers", { chatId: chat.id });
setShowElement("showChannelOpen");
}
} catch (error) {}; // required to avoid raising an error when chat.id is undefined
};

useEffect(() => {
Expand All @@ -78,7 +80,11 @@ export const ChatProvider = ({ children }: ChatProviderProps) => {

// Listen for incoming messages recursively every 10 seconds
chatService.socket?.on("listChats", (newChatList: ChatList) => {
setChatList(() => newChatList);
// remove chats which chatType is PRIVATE
const filteredChats = newChatList.filter(chat => {
return chat.chatType !== 'PRIVATE';
});
setChatList(() => filteredChats);
setIsLoading(false);
});

Expand All @@ -93,7 +99,12 @@ export const ChatProvider = ({ children }: ChatProviderProps) => {
chatService.socket?.on("createChat", (chat: Chat) => {
setValidationRequired(false);
handleOpenChannel(chat);
setChatList((chatList) => [...chatList, chat]);
const isDuplicateChat = chatList.some(existingChat =>
existingChat.name === chat.name && existingChat.chatType === 'PRIVATE'
);
if (!isDuplicateChat) {
setChatList((chatList) => [...chatList, chat]);
}
});

chatService.socket?.on("joinChat", (response: any) => {
Expand Down
Loading