Skip to content

Commit 11e563d

Browse files
committed
Chat components
1 parent 599bc78 commit 11e563d

File tree

18 files changed

+984
-10
lines changed

18 files changed

+984
-10
lines changed

src/components/chat/header.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Box, Flex, HStack, Image, Text } from "@chakra-ui/react";
2+
import { Avatar } from "@/components/ui/avatar";
3+
import { useAuth } from "@/context/Auth";
4+
import LogoutModal from "../logout";
5+
6+
const Header = () => {
7+
const { user } = useAuth();
8+
9+
return (
10+
<Flex
11+
px={4}
12+
py={2}
13+
borderWidth={"1px"}
14+
borderTopRadius={"md"}
15+
justifyContent={"space-between"}
16+
>
17+
<Box alignContent={"center"}>
18+
<HStack>
19+
<Image src="/cat.png" alt="cat" width={"35px"} />
20+
<Text color={"orange.300"} fontWeight={800}>
21+
Cat Chat
22+
</Text>
23+
</HStack>
24+
</Box>
25+
26+
<Box>
27+
<HStack borderRadius={"md"} py={1}>
28+
<Avatar size="sm" name={user?.fullName} src={user?.profile_picture} />
29+
<Text color="orange.400" textStyle="sm" fontWeight={"medium"}>
30+
{user?.fullName}
31+
</Text>
32+
<LogoutModal />
33+
</HStack>
34+
</Box>
35+
</Flex>
36+
);
37+
};
38+
39+
export default Header;

src/components/chat/index.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
1+
import { lazy } from "react";
2+
import { Flex } from "@chakra-ui/react";
3+
const Header = lazy(() => import("./header"));
4+
const UsersList = lazy(() => import("./usersList"));
5+
const Message = lazy(() => import("./message"));
6+
import { ChatProvider } from "@/context/Chat";
7+
18
const ChatComponent = () => {
2-
return <div>Hello from Chat</div>;
9+
return (
10+
<Flex direction={"column"} minW={"70%"}>
11+
<Header />
12+
<Flex minH={"80vh"} maxW={"76vw"}>
13+
<ChatProvider>
14+
<UsersList />
15+
<Message />
16+
</ChatProvider>
17+
</Flex>
18+
</Flex>
19+
);
320
};
421

522
export default ChatComponent;

src/components/chat/inputBox.tsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Box, Button, Grid, Input, Text } from "@chakra-ui/react";
2+
import { useEffect, useState } from "react";
3+
import { InputGroup } from "../ui/input-group";
4+
import { emojis } from "@/utils/emoji";
5+
import { firebaseDatabase } from "@/config";
6+
import { push, ref } from "firebase/database";
7+
import { useAuth } from "@/context/Auth";
8+
import { useChat } from "@/context/Chat";
9+
10+
const InputBox = () => {
11+
const { user } = useAuth();
12+
const { chatRoomId } = useChat();
13+
const [showEmoji, setShowEmoji] = useState(false);
14+
const [message, setMessage] = useState("");
15+
16+
const addNewMessage = (e: React.FormEvent<HTMLFormElement>) => {
17+
e.preventDefault();
18+
if (message.trim()) {
19+
sendMessage();
20+
}
21+
};
22+
23+
const toggleEmoji = () => setShowEmoji((prev) => !prev);
24+
25+
const handleEmoji = (emoji: string) => setMessage((prev) => prev + emoji);
26+
27+
const sendMessage = () => {
28+
const collectionRef = ref(firebaseDatabase, chatRoomId);
29+
push(collectionRef, {
30+
uid: user?.uid,
31+
name: user?.userName,
32+
text: message,
33+
time: new Date().toISOString(),
34+
});
35+
setMessage("");
36+
};
37+
38+
useEffect(() => {
39+
return () => {
40+
setShowEmoji(false);
41+
setMessage("");
42+
}
43+
}, [chatRoomId]);
44+
45+
return (
46+
<Box p={4} pt={0} borderTop={"none"} position={"relative"}>
47+
{/* @todo: add emoji button */}
48+
<form onSubmit={addNewMessage}>
49+
<InputGroup
50+
endElement={
51+
<Button mx={0} px={0} background="none" onClick={toggleEmoji}>
52+
<Text fontSize="2xl">😀</Text>
53+
</Button>
54+
}
55+
width={"full"}
56+
>
57+
<Input
58+
placeholder="Type Your Message"
59+
height={"50px"}
60+
value={message}
61+
onChange={(e) => setMessage(e.target.value)}
62+
/>
63+
</InputGroup>
64+
</form>
65+
<Box
66+
position={"absolute"}
67+
bottom={"66px"}
68+
right={4}
69+
p={2}
70+
width={"240px"}
71+
height={"150px"}
72+
overflowX={"auto"}
73+
bg={"orange.100"}
74+
borderTopRadius={"md"}
75+
display={showEmoji ? "block" : "none"}
76+
>
77+
<Grid templateColumns="repeat(6, 1fr)" gap={4}>
78+
{emojis.map((emoji, index) => (
79+
<Text
80+
cursor={"pointer"}
81+
key={index}
82+
onClick={() => handleEmoji(emoji)}
83+
>
84+
{emoji}
85+
</Text>
86+
))}
87+
</Grid>
88+
</Box>
89+
</Box>
90+
);
91+
};
92+
93+
export default InputBox;

src/components/chat/message.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Box, Flex, HStack, Spinner, Text } from "@chakra-ui/react";
2+
import { useCallback, useEffect, useState } from "react";
3+
import InputBox from "./inputBox";
4+
import { MessageSnapshotResponse } from "@/context/Chat/types";
5+
import { useChat } from "@/context/Chat";
6+
import { fetchCommonRoomMessages, fetchOneToOneMessages } from "@/services";
7+
import MessageItems from "./messageItems";
8+
import {
9+
limitToLast,
10+
ref,
11+
query,
12+
onChildAdded,
13+
DataSnapshot,
14+
} from "firebase/database";
15+
import { firebaseDatabase } from "@/config";
16+
17+
const Message = () => {
18+
const { chatRoomId, oneToOneRoomId } = useChat();
19+
const [isLoading, setIsLoading] = useState(true);
20+
const [messages, setMessages] = useState<MessageSnapshotResponse>({});
21+
22+
// Handle child added
23+
const handleChildAdded = (snapshot: DataSnapshot) => {
24+
if (!snapshot.exists() || !snapshot.key) return;
25+
setMessages((prev) => {
26+
if (prev[snapshot.key as string]) return prev;
27+
return { ...prev, [snapshot.key as string]: snapshot.val() };
28+
});
29+
};
30+
31+
// Subscribe to room
32+
const subscribeToRoom = useCallback(() => {
33+
const collectionRef = ref(firebaseDatabase, chatRoomId);
34+
const limitedQuery = query(collectionRef, limitToLast(1));
35+
onChildAdded(limitedQuery, handleChildAdded);
36+
37+
if (oneToOneRoomId) {
38+
const oneToOneCollectionRef = ref(firebaseDatabase, oneToOneRoomId);
39+
const oneToOneLimitedQuery = query(oneToOneCollectionRef, limitToLast(1));
40+
onChildAdded(oneToOneLimitedQuery, handleChildAdded);
41+
}
42+
}, [chatRoomId, oneToOneRoomId]);
43+
44+
// Fetch messages
45+
const fetchData = useCallback(async () => {
46+
setIsLoading(true);
47+
const results =
48+
chatRoomId && oneToOneRoomId
49+
? await fetchOneToOneMessages(chatRoomId, oneToOneRoomId)
50+
: await fetchCommonRoomMessages();
51+
setMessages(results);
52+
subscribeToRoom();
53+
setIsLoading(false);
54+
}, [chatRoomId, oneToOneRoomId, subscribeToRoom]);
55+
56+
useEffect(() => {
57+
if (!chatRoomId) return;
58+
fetchData();
59+
}, [chatRoomId, fetchData, oneToOneRoomId]);
60+
61+
return (
62+
<Box
63+
flex={3}
64+
borderWidth={"1px"}
65+
borderTop={"none"}
66+
borderBottomRightRadius={"md"}
67+
className="right__section"
68+
>
69+
<Flex direction={"column"} h={"full"}>
70+
<Box
71+
p={3}
72+
flex={1}
73+
borderBottom={"none"}
74+
overflowX={"hidden"}
75+
overflowY={"auto"}
76+
height={"100%"}
77+
>
78+
{/* loading */}
79+
{isLoading && (
80+
<HStack justifyContent={"center"} alignItems={"center"} h={"full"}>
81+
<Text textAlign={"center"} color={"gray.500"}>
82+
Loading messages...
83+
</Text>
84+
<Spinner />
85+
</HStack>
86+
)}
87+
{/* messages */}
88+
{!isLoading && <MessageItems messages={messages} />}
89+
</Box>
90+
{/* input */}
91+
<InputBox />
92+
</Flex>
93+
</Box>
94+
);
95+
};
96+
97+
export default Message;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { MessageSnapshotResponse } from "@/context/Chat/types";
2+
import NoMessage from "./noMessage";
3+
import { Box, HStack, Stack, Text } from "@chakra-ui/react";
4+
import { Avatar } from "@/components/ui/avatar";
5+
import { useLayoutEffect, useRef } from "react";
6+
import { useChat } from "@/context/Chat";
7+
import { dateFormatter } from "@/utils";
8+
9+
interface MessageItemsProps {
10+
messages: MessageSnapshotResponse;
11+
}
12+
13+
const MessageItems = ({ messages }: MessageItemsProps) => {
14+
const scrollBottomRef = useRef<HTMLDivElement>(null);
15+
const { userList } = useChat();
16+
17+
useLayoutEffect(() => {
18+
if (scrollBottomRef.current) {
19+
scrollBottomRef.current.scrollIntoView({ behavior: "smooth" });
20+
}
21+
}, [messages]);
22+
23+
if (!Object.keys(messages).length) {
24+
return <NoMessage />;
25+
}
26+
return Object.keys(messages)?.map((key, i) => {
27+
const { text, time, uid } = messages[key];
28+
const isLastMessage = i === Object.keys(messages).length - 1;
29+
return (
30+
<Box
31+
key={key}
32+
p={2}
33+
bg={"gray.muted"}
34+
borderRadius={"4xl"}
35+
mb={2}
36+
ref={isLastMessage ? scrollBottomRef : null}
37+
>
38+
<HStack justifyContent={"flex-start"} alignItems={"center"}>
39+
<Avatar
40+
size="sm"
41+
src={userList[uid]?.profile_picture || ""}
42+
name={userList[uid]?.userName}
43+
colorPalette={"orange"}
44+
title={userList[uid]?.userName}
45+
/>
46+
<Stack gap="1">
47+
<HStack gap={2}>
48+
<Text textStyle={"sm"} fontWeight={800}>
49+
{userList[uid]?.userName ?? "???"}
50+
</Text>
51+
<Text textStyle={"xs"} color={"gray.500"}>
52+
{dateFormatter.format(new Date(time))}
53+
</Text>
54+
</HStack>
55+
<Text textStyle={"xs"} lineBreak={'anywhere'}>{text}</Text>
56+
</Stack>
57+
</HStack>
58+
</Box>
59+
);
60+
});
61+
};
62+
63+
export default MessageItems;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Center, Text } from "@chakra-ui/react";
2+
3+
const NoMessage = () => {
4+
return <Center h={"full"}>
5+
<Text color={"gray.500"}>There are no messages yet.</Text>
6+
</Center>
7+
};
8+
9+
export default NoMessage;

0 commit comments

Comments
 (0)