From 2739a908a6421ec627cf2610be73bd05eb72d48b Mon Sep 17 00:00:00 2001 From: khyrulAlam Date: Mon, 13 Jan 2025 18:36:08 +0900 Subject: [PATCH 1/4] UnregisterServiceWorker --- .gitignore | 5 ++++- src/{config.ts.emaple => config.ts.example} | 0 src/main.tsx | 5 ++++- src/registerServiceWorker.ts | 15 +++++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) rename src/{config.ts.emaple => config.ts.example} (100%) create mode 100644 src/registerServiceWorker.ts diff --git a/.gitignore b/.gitignore index 6b724d1..edf4ec7 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ dist-ssr /dist #config -/src/config.ts \ No newline at end of file +/src/config.ts + +.firebase +.vscode \ No newline at end of file diff --git a/src/config.ts.emaple b/src/config.ts.example similarity index 100% rename from src/config.ts.emaple rename to src/config.ts.example diff --git a/src/main.tsx b/src/main.tsx index 22aed0f..8b8ac89 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,8 +5,11 @@ import App from './App.tsx' import { Provider } from './components/ui/provider.tsx' import AuthProvider from './context/Auth/index.tsx' +// unregister service worker +import './registerServiceWorker.ts' + // firebase initialization -import "./config.ts"; +import "@/config.ts"; createRoot(document.getElementById('root')!).render( diff --git a/src/registerServiceWorker.ts b/src/registerServiceWorker.ts new file mode 100644 index 0000000..1757390 --- /dev/null +++ b/src/registerServiceWorker.ts @@ -0,0 +1,15 @@ +// unregister previously registered service worker + +if ("serviceWorker" in navigator) { + navigator.serviceWorker.getRegistrations().then(function (registrations) { + for (let registration of registrations) { + if ( + registration.active && + registration.active.scriptURL == + "https://chatroom-67e21.web.app/service-worker.js" + ) { + registration.unregister(); + } + } + }); +} From 62ab0f3b89a7d9f852d0e1d60ca3b9d7b1b6b349 Mon Sep 17 00:00:00 2001 From: khyrulAlam Date: Mon, 13 Jan 2025 19:11:12 +0900 Subject: [PATCH 2/4] DB name on utils file --- src/context/Auth/types.ts | 1 + src/context/Chat/chatReducer.ts | 3 ++- src/context/Chat/types.ts | 4 ++-- src/services/index.ts | 5 +++-- src/utils/index.ts | 25 ++++++++++++++++++------- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/context/Auth/types.ts b/src/context/Auth/types.ts index c0d28ba..4538428 100644 --- a/src/context/Auth/types.ts +++ b/src/context/Auth/types.ts @@ -4,6 +4,7 @@ export type User = { profile_picture: string; userName: string; uid: string; + createdAt: number; } export type AuthState = { diff --git a/src/context/Chat/chatReducer.ts b/src/context/Chat/chatReducer.ts index 10ea5ad..1105b2f 100644 --- a/src/context/Chat/chatReducer.ts +++ b/src/context/Chat/chatReducer.ts @@ -1,9 +1,10 @@ +import { DB_NAME } from "@/utils"; import { ACTION_TYPE, ChatState } from "./types"; export const initialState: ChatState = { userList: {}, isLoading: false, - chatRoomId: "chatRoom", + chatRoomId: DB_NAME.CHAT_ROOM, oneToOneRoomId: "", }; diff --git a/src/context/Chat/types.ts b/src/context/Chat/types.ts index 54ae986..9a7b0dd 100644 --- a/src/context/Chat/types.ts +++ b/src/context/Chat/types.ts @@ -15,7 +15,7 @@ export type MessageSnapshotResponse = { export type ChatState = { userList: {[key: string]: User}; isLoading: boolean; - chatRoomId: string | 'chatRoom'; + chatRoomId: string; oneToOneRoomId: string; }; @@ -38,7 +38,7 @@ type SET_LOADING = { type SET_CHAT_ROOM_ID = { type: ChatActionType.SET_CHAT_ROOM_ID; payload: { - chatRoomId: string | 'chatRoom'; + chatRoomId: string; oneToOneRoomId: string; }; }; diff --git a/src/services/index.ts b/src/services/index.ts index bd00096..e3276c7 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,6 +1,7 @@ import { firebaseDatabase } from "@/config"; import { User } from "@/context/Auth/types"; import { MessageSnapshotResponse } from "@/context/Chat/types"; +import { DB_NAME } from "@/utils"; import { child, get, @@ -13,7 +14,7 @@ import { } from "firebase/database"; export const addUserInfo = async (user: User) => { - const collectionRef = ref(firebaseDatabase, "usersTable"); + const collectionRef = ref(firebaseDatabase, DB_NAME.USER_TABLE); const snapshot = await child(collectionRef, user.uid); if (!snapshot.key) { set(snapshot, user); @@ -33,7 +34,7 @@ const fetchMessages = async ( export const fetchCommonRoomMessages = async (): Promise => { - const collectionRef = ref(firebaseDatabase, "chatRoom"); + const collectionRef = ref(firebaseDatabase, DB_NAME.CHAT_ROOM); // By default the fetch item is sorted by timestamp return fetchMessages(collectionRef); }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 757ad8d..7a7e265 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,11 +1,22 @@ export const dateFormatter = new Intl.DateTimeFormat(navigator.language, { - month: "short", - day: "numeric", - year: "numeric", - hour: "numeric", - minute: "2-digit", + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "2-digit", +}); + +export const dateFormatterShort = new Intl.DateTimeFormat(navigator.language, { + month: "short", + day: "numeric", + year: "numeric", }); export const numberFormatter = new Intl.NumberFormat(navigator.language, { - notation: "compact", -}); \ No newline at end of file + notation: "compact", +}); + +export const DB_NAME: { [key: string]: string } = { + USER_TABLE: "usersTable", + CHAT_ROOM: "chatRoom", +}; From d45e50885d1ca3f74980338d1167cdd0ca8d6ad0 Mon Sep 17 00:00:00 2001 From: khyrulAlam Date: Mon, 13 Jan 2025 19:13:05 +0900 Subject: [PATCH 3/4] User serach feature --- src/components/chat/searchBox.tsx | 35 +++++++++ src/components/chat/usersList.tsx | 121 ++++++++++++++++++------------ src/utils/useDebounce.ts | 17 +++++ 3 files changed, 127 insertions(+), 46 deletions(-) create mode 100644 src/components/chat/searchBox.tsx create mode 100644 src/utils/useDebounce.ts diff --git a/src/components/chat/searchBox.tsx b/src/components/chat/searchBox.tsx new file mode 100644 index 0000000..b8b67d6 --- /dev/null +++ b/src/components/chat/searchBox.tsx @@ -0,0 +1,35 @@ +import { LuSearch } from "react-icons/lu"; +import { InputGroup } from "../ui/input-group"; +import { Input } from "@chakra-ui/react/input"; +import { useEffect, useState } from "react"; +import useDebounce from "@/utils/useDebounce"; + +interface SearchBoxProps { + setSearch: (value: string) => void; + setLoading: (value: boolean) => void; +} + +const SearchBox = ({ setSearch, setLoading }: SearchBoxProps) => { + const [searchToken, setSearchToken] = useState(null); + const debouncedSearchToken = useDebounce(searchToken, 500); + + useEffect(() => { + setSearch(debouncedSearchToken || ""); + }, [debouncedSearchToken]); + + return ( + }> + { + setLoading(true); + setSearchToken(e.currentTarget.value) + } + } + /> + + ); +}; + +export default SearchBox; diff --git a/src/components/chat/usersList.tsx b/src/components/chat/usersList.tsx index 91a4794..bf177a6 100644 --- a/src/components/chat/usersList.tsx +++ b/src/components/chat/usersList.tsx @@ -8,30 +8,27 @@ import { ChatActionType } from "@/context/Chat/types"; import { unSubscribeDatabase } from "@/services"; import { firebaseDatabase } from "@/config"; import { onValue, ref } from "firebase/database"; -import { numberFormatter } from "@/utils"; +import { DB_NAME, numberFormatter } from "@/utils"; +import SearchBox from "./searchBox"; const UsersList = () => { const [loading, setLoading] = useState(true); const { user: authUser } = useAuth(); const { chatRoomId, oneToOneRoomId } = useChat(); const dispatch = useChatDispatch(); + const { userList: storeUserList } = useChat(); const [userList, setUserList] = useState([]); const [userCount, setUserCount] = useState(0); + const [startSearch, setStartSearch] = useState(false); useEffect(() => { setLoading(true); - const collectionRef = ref(firebaseDatabase, "usersTable"); + const collectionRef = ref(firebaseDatabase, DB_NAME.USER_TABLE); const unsubscribe = onValue(collectionRef, (snapshot) => { const data = snapshot.val(); if (data) { const users = Object.values(data) as User[]; setUserCount(users.length || 0); - // Remove duplicate users and the current user - const uniqueUsers = users.filter( - (user, index, self) => - user.uid && user.uid !== authUser?.uid && index === self.findIndex((u) => u.uid === user.uid) - ); - setUserList(uniqueUsers); dispatch({ type: ChatActionType.SET_USER_LIST, payload: data, @@ -45,11 +42,13 @@ const UsersList = () => { }; }, [authUser?.uid, dispatch]); - const handleUserClick = (user: User | "chatRoom") => { + const handleUserClick = (user: User | string) => { const newChatRoomId = - user === "chatRoom" ? "chatRoom" : `${authUser?.uid}+${user.uid}`; + typeof user === "string" + ? DB_NAME.CHAT_ROOM + : `${authUser?.uid}+${user.uid}`; const newOneToOneRoomId = - user !== "chatRoom" ? `${user.uid}+${authUser?.uid}` : ""; + typeof user !== "string" ? `${user.uid}+${authUser?.uid}` : ""; // Unsubscribe from the database const roomList = [chatRoomId, oneToOneRoomId].filter(Boolean); @@ -65,6 +64,16 @@ const UsersList = () => { }); }; + const handleSearchUserList = (token: string) => { + const sourceList = Object.values(storeUserList) as User[]; + const regex = new RegExp(token, "i"); + const result = sourceList + .filter((user) => user.userName.toLowerCase().match(regex)) + .sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); + setUserList(result); + setStartSearch(false); + }; + return ( { {loading && } {!loading && ( + handleUserClick("chatRoom")} + onClick={() => handleUserClick(DB_NAME.CHAT_ROOM)} > { src={authUser?.profile_picture} colorPalette={"orange"} /> - + Common Group - {userList?.map((user) => ( - handleUserClick(user)} - > - - - - {user.userName} - - + + {startSearch ? ( + + + + ) : userList.length < 1 ? ( + + + No Search result found + - ))} + ) : ( + userList?.map((user) => ( + handleUserClick(user)} + > + + + + {user.userName} + + + + )) + )} )} diff --git a/src/utils/useDebounce.ts b/src/utils/useDebounce.ts new file mode 100644 index 0000000..d1484d0 --- /dev/null +++ b/src/utils/useDebounce.ts @@ -0,0 +1,17 @@ +import { useState, useEffect } from "react"; + +const useDebounce = (value: T, delay = 500) => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => clearTimeout(timer); + }, [value, delay]); + + return debouncedValue; +}; + +export default useDebounce; From f35351737f960cd0f6521352dcb37f52723cd834 Mon Sep 17 00:00:00 2001 From: khyrulAlam Date: Mon, 13 Jan 2025 19:24:08 +0900 Subject: [PATCH 4/4] Update readme file for version: 2.1.0 --- README.md | 6 ++++++ package.json | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 67c2c2a..d78eaf9 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,9 @@ Quick Start: - Refactored codebase from class components to function components. - Utilized React Context API for state management. - Various code optimizations and improvements. + + +### [Version 2.1.0](https://github.com/khyrulAlam/react-firebase-chat/releases/tag/v2.1.0) + +- Contacts Search by name. +- User list sort by created at. diff --git a/package.json b/package.json index 0a7e195..1c3bb2a 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,14 @@ { "name": "react-firebase-chat", "private": true, - "version": "2.0.0", + "version": "2.1.0", "type": "module", + "author": { + "name": "Khayrul Alam", + "email": "khyrulalam69@gmail.com", + "github": "https://github.com/khyrulAlam", + "linkedin": "https://www.linkedin.com/in/khayrul-alam-360291aa/" + }, "scripts": { "dev": "vite", "build": "tsc -b && vite build",