Skip to content

Commit 68ac7b0

Browse files
authored
Merge pull request #39 from khyrulAlam/feature/user-search
Feature/user search
2 parents d327e45 + f353517 commit 68ac7b0

File tree

14 files changed

+189
-61
lines changed

14 files changed

+189
-61
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,7 @@ dist-ssr
2828
/dist
2929

3030
#config
31-
/src/config.ts
31+
/src/config.ts
32+
33+
.firebase
34+
.vscode

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,9 @@ Quick Start:
3838
- Refactored codebase from class components to function components.
3939
- Utilized React Context API for state management.
4040
- Various code optimizations and improvements.
41+
42+
43+
### [Version 2.1.0](https://github.com/khyrulAlam/react-firebase-chat/releases/tag/v2.1.0)
44+
45+
- Contacts Search by name.
46+
- User list sort by created at.

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
{
22
"name": "react-firebase-chat",
33
"private": true,
4-
"version": "2.0.0",
4+
"version": "2.1.0",
55
"type": "module",
6+
"author": {
7+
"name": "Khayrul Alam",
8+
"email": "khyrulalam69@gmail.com",
9+
"github": "https://github.com/khyrulAlam",
10+
"linkedin": "https://www.linkedin.com/in/khayrul-alam-360291aa/"
11+
},
612
"scripts": {
713
"dev": "vite",
814
"build": "tsc -b && vite build",

src/components/chat/searchBox.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { LuSearch } from "react-icons/lu";
2+
import { InputGroup } from "../ui/input-group";
3+
import { Input } from "@chakra-ui/react/input";
4+
import { useEffect, useState } from "react";
5+
import useDebounce from "@/utils/useDebounce";
6+
7+
interface SearchBoxProps {
8+
setSearch: (value: string) => void;
9+
setLoading: (value: boolean) => void;
10+
}
11+
12+
const SearchBox = ({ setSearch, setLoading }: SearchBoxProps) => {
13+
const [searchToken, setSearchToken] = useState<string | null>(null);
14+
const debouncedSearchToken = useDebounce(searchToken, 500);
15+
16+
useEffect(() => {
17+
setSearch(debouncedSearchToken || "");
18+
}, [debouncedSearchToken]);
19+
20+
return (
21+
<InputGroup flex="1" startElement={<LuSearch />}>
22+
<Input
23+
placeholder="Search contacts"
24+
value={searchToken || ""}
25+
onChange={(e) => {
26+
setLoading(true);
27+
setSearchToken(e.currentTarget.value)
28+
}
29+
}
30+
/>
31+
</InputGroup>
32+
);
33+
};
34+
35+
export default SearchBox;

src/components/chat/usersList.tsx

Lines changed: 75 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,27 @@ import { ChatActionType } from "@/context/Chat/types";
88
import { unSubscribeDatabase } from "@/services";
99
import { firebaseDatabase } from "@/config";
1010
import { onValue, ref } from "firebase/database";
11-
import { numberFormatter } from "@/utils";
11+
import { DB_NAME, numberFormatter } from "@/utils";
12+
import SearchBox from "./searchBox";
1213

1314
const UsersList = () => {
1415
const [loading, setLoading] = useState(true);
1516
const { user: authUser } = useAuth();
1617
const { chatRoomId, oneToOneRoomId } = useChat();
1718
const dispatch = useChatDispatch();
19+
const { userList: storeUserList } = useChat();
1820
const [userList, setUserList] = useState<User[] | []>([]);
1921
const [userCount, setUserCount] = useState<number>(0);
22+
const [startSearch, setStartSearch] = useState(false);
2023

2124
useEffect(() => {
2225
setLoading(true);
23-
const collectionRef = ref(firebaseDatabase, "usersTable");
26+
const collectionRef = ref(firebaseDatabase, DB_NAME.USER_TABLE);
2427
const unsubscribe = onValue(collectionRef, (snapshot) => {
2528
const data = snapshot.val();
2629
if (data) {
2730
const users = Object.values(data) as User[];
2831
setUserCount(users.length || 0);
29-
// Remove duplicate users and the current user
30-
const uniqueUsers = users.filter(
31-
(user, index, self) =>
32-
user.uid && user.uid !== authUser?.uid && index === self.findIndex((u) => u.uid === user.uid)
33-
);
34-
setUserList(uniqueUsers);
3532
dispatch({
3633
type: ChatActionType.SET_USER_LIST,
3734
payload: data,
@@ -45,11 +42,13 @@ const UsersList = () => {
4542
};
4643
}, [authUser?.uid, dispatch]);
4744

48-
const handleUserClick = (user: User | "chatRoom") => {
45+
const handleUserClick = (user: User | string) => {
4946
const newChatRoomId =
50-
user === "chatRoom" ? "chatRoom" : `${authUser?.uid}+${user.uid}`;
47+
typeof user === "string"
48+
? DB_NAME.CHAT_ROOM
49+
: `${authUser?.uid}+${user.uid}`;
5150
const newOneToOneRoomId =
52-
user !== "chatRoom" ? `${user.uid}+${authUser?.uid}` : "";
51+
typeof user !== "string" ? `${user.uid}+${authUser?.uid}` : "";
5352

5453
// Unsubscribe from the database
5554
const roomList = [chatRoomId, oneToOneRoomId].filter(Boolean);
@@ -65,6 +64,16 @@ const UsersList = () => {
6564
});
6665
};
6766

67+
const handleSearchUserList = (token: string) => {
68+
const sourceList = Object.values(storeUserList) as User[];
69+
const regex = new RegExp(token, "i");
70+
const result = sourceList
71+
.filter((user) => user.userName.toLowerCase().match(regex))
72+
.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
73+
setUserList(result);
74+
setStartSearch(false);
75+
};
76+
6877
return (
6978
<Box
7079
p={2}
@@ -78,61 +87,81 @@ const UsersList = () => {
7887
{loading && <Spinner />}
7988
{!loading && (
8089
<Stack gap="2">
90+
<SearchBox
91+
setSearch={handleSearchUserList}
92+
setLoading={setStartSearch}
93+
/>
8194
<HStack
8295
cursor={"pointer"}
8396
_hover={{ bg: "orange.50", color: "orange.400" }}
84-
bg={chatRoomId === "chatRoom" ? "orange.50" : "gray.subtle"}
97+
bg={chatRoomId === DB_NAME.CHAT_ROOM ? "orange.50" : "gray.subtle"}
8598
color={"orange.400"}
8699
borderRadius={"md"}
87100
title="Common Group"
88101
p={"2"}
89-
onClick={() => handleUserClick("chatRoom")}
102+
onClick={() => handleUserClick(DB_NAME.CHAT_ROOM)}
90103
>
91104
<AvatarGroup size="sm">
92105
<Avatar
93106
name={authUser?.userName}
94107
src={authUser?.profile_picture}
95108
colorPalette={"orange"}
96109
/>
97-
<Avatar colorPalette={'orange'} fallback={`+${numberFormatter.format(userCount)}`} />
110+
<Avatar
111+
colorPalette={"orange"}
112+
fallback={`+${numberFormatter.format(userCount)}`}
113+
/>
98114
</AvatarGroup>
99115
<Text fontWeight="medium" textStyle={"sm"} lineClamp={1}>
100116
Common Group
101117
</Text>
102118
</HStack>
103-
{userList?.map((user) => (
104-
<HStack
105-
key={user.uid}
106-
cursor={"pointer"}
107-
bg={
108-
chatRoomId === `${authUser?.uid}+${user.uid}`
109-
? "orange.50"
110-
: "gray.subtle"
111-
}
112-
color={
113-
chatRoomId === `${authUser?.uid}+${user.uid}`
114-
? "orange.400"
115-
: "initial"
116-
}
117-
_hover={{ bg: "orange.50", color: "orange.400" }}
118-
borderRadius={"md"}
119-
title={user.userName}
120-
p={"2"}
121-
onClick={() => handleUserClick(user)}
122-
>
123-
<Avatar
124-
name={user.userName}
125-
size="sm"
126-
src={user.profile_picture}
127-
captionSide={"bottom"}
128-
/>
129-
<Stack gap="0">
130-
<Text fontWeight="medium" textStyle={"sm"} lineClamp={1}>
131-
{user.userName}
132-
</Text>
133-
</Stack>
119+
120+
{startSearch ? (
121+
<Stack alignItems={"center"}>
122+
<Spinner />
123+
</Stack>
124+
) : userList.length < 1 ? (
125+
<HStack justifyContent={"center"}>
126+
<Text fontWeight="medium" textStyle={"xs"}>
127+
No Search result found
128+
</Text>
134129
</HStack>
135-
))}
130+
) : (
131+
userList?.map((user) => (
132+
<HStack
133+
key={user.uid}
134+
cursor={"pointer"}
135+
bg={
136+
chatRoomId === `${authUser?.uid}+${user.uid}`
137+
? "orange.50"
138+
: "gray.subtle"
139+
}
140+
color={
141+
chatRoomId === `${authUser?.uid}+${user.uid}`
142+
? "orange.400"
143+
: "initial"
144+
}
145+
_hover={{ bg: "orange.50", color: "orange.400" }}
146+
borderRadius={"md"}
147+
title={user.userName}
148+
p={"2"}
149+
onClick={() => handleUserClick(user)}
150+
>
151+
<Avatar
152+
name={user.userName}
153+
size="sm"
154+
src={user.profile_picture}
155+
captionSide={"bottom"}
156+
/>
157+
<Stack gap="1" direction={"column"} align={"flex-start"}>
158+
<Text fontWeight="medium" textStyle={"sm"} lineClamp={1}>
159+
{user.userName}
160+
</Text>
161+
</Stack>
162+
</HStack>
163+
))
164+
)}
136165
</Stack>
137166
)}
138167
</Box>
File renamed without changes.

src/context/Auth/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type User = {
44
profile_picture: string;
55
userName: string;
66
uid: string;
7+
createdAt: number;
78
}
89

910
export type AuthState = {

src/context/Chat/chatReducer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import { DB_NAME } from "@/utils";
12
import { ACTION_TYPE, ChatState } from "./types";
23

34
export const initialState: ChatState = {
45
userList: {},
56
isLoading: false,
6-
chatRoomId: "chatRoom",
7+
chatRoomId: DB_NAME.CHAT_ROOM,
78
oneToOneRoomId: "",
89
};
910

src/context/Chat/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export type MessageSnapshotResponse = {
1515
export type ChatState = {
1616
userList: {[key: string]: User};
1717
isLoading: boolean;
18-
chatRoomId: string | 'chatRoom';
18+
chatRoomId: string;
1919
oneToOneRoomId: string;
2020
};
2121

@@ -38,7 +38,7 @@ type SET_LOADING = {
3838
type SET_CHAT_ROOM_ID = {
3939
type: ChatActionType.SET_CHAT_ROOM_ID;
4040
payload: {
41-
chatRoomId: string | 'chatRoom';
41+
chatRoomId: string;
4242
oneToOneRoomId: string;
4343
};
4444
};

src/main.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import App from './App.tsx'
55
import { Provider } from './components/ui/provider.tsx'
66
import AuthProvider from './context/Auth/index.tsx'
77

8+
// unregister service worker
9+
import './registerServiceWorker.ts'
10+
811
// firebase initialization
9-
import "./config.ts";
12+
import "@/config.ts";
1013

1114
createRoot(document.getElementById('root')!).render(
1215
<StrictMode>

0 commit comments

Comments
 (0)