Skip to content

Commit

Permalink
Feature: Implement chat DMs between friends (#128)
Browse files Browse the repository at this point in the history
* feat: implement chat DMs between friends

- Remove finished todos
- Add some alt text to components
- Private chats don't show in the main chat lobby anymore
- Only private chats created by the user are temporarily displayed in the chat lobby
- If a user resets its chat component, all of the DM chats are removed
- All chat DMs are database persisted

* lint: remove unused imports

* fix: filter private chat to no appear on list chats

* style: resize name and hide users popover on dm

* style: fixed chat width

* style: add cursor pointer into dm icon

---------

Co-authored-by: Italo A <[email protected]>
  • Loading branch information
vcwild and iaurg committed Nov 30, 2023
1 parent f0f85c8 commit e7593b2
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 39 deletions.
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
22 changes: 20 additions & 2 deletions frontend/src/components/Chat/FriendCard.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { api } from "@/services/apiClient";
import { queryClient } from "@/services/queryClient";
import { UserStatus } from "@/types/user";
import { EnvelopeSimple, Sword, UserMinus } from "@phosphor-icons/react";
import { EnvelopeSimple, 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 InviteToGame from "../InviteToGame";
import { ChatContext } from "@/contexts/ChatContext";
import { useContext } from "react";

type FriendCardProps = {
displayName: string;
Expand All @@ -18,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 @@ -37,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 @@ -53,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}
/>
<InviteToGame inviteUserId={id} />
<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

0 comments on commit e7593b2

Please sign in to comment.