Skip to content

Commit

Permalink
Merge pull request #68 from L1nkWave/feature/LWF-16_chat-box
Browse files Browse the repository at this point in the history
feature/LWF-16_chat-box
  • Loading branch information
JeriRov committed May 11, 2024
2 parents 4d4ddd0 + 5a0b847 commit 11e5dc8
Show file tree
Hide file tree
Showing 56 changed files with 2,191 additions and 1,586 deletions.
2 changes: 1 addition & 1 deletion frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
Expand Down
3,107 changes: 1,621 additions & 1,486 deletions frontend/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions frontend/public/icons/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/public/icons/cut-check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/public/icons/folder-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions frontend/public/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export { default as AddChatOutlineIcon } from "./add-chat-outline.svg";
export { default as AddCircleOutlineIcon } from "./add-circle-outline.svg";
export { default as ChatPlusOutlineIcon } from "./chat-plus-outline.svg";
export { default as CheckIcon } from "./check.svg";
export { default as CutCheckIcon } from "./cut-check.svg";
export { default as FindContactsOutlineIcon } from "./find-people-outline.svg";
export { default as FolderOutlineIcon } from "./folder-outline.svg";
export { default as FormOutlineIcon } from "./form-outline.svg";
export { default as GroupOutlineIcon } from "./group-outline.svg";
export { default as LeftAngleIcon } from "./left-angle.svg";
Expand All @@ -11,6 +14,7 @@ export { default as LockOutlineIcon } from "./lock-outline.svg";
export { default as PenWithMessageIcon } from "./pen-with-message.svg";
export { default as PinOutlineIcon } from "./pin-outline.svg";
export { default as RemoveCircleOutlineIcon } from "./remove-circle-outline.svg";
export { default as SendOutlineIcon } from "./send-outline.svg";
export { default as SettingOutlineIcon } from "./setting-outline.svg";
export { default as SignOutCircleIcon } from "./sign-out-circle.svg";
export { default as TimeOutlineIcon } from "./time-outline.svg";
Expand Down
6 changes: 6 additions & 0 deletions frontend/public/icons/send-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion frontend/src/api/http/chat/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ export async function addDuoChat(userId: string) {
return data;
}

export async function getChatByUserId(userId: string) {
const chatId = await instance.get<string>(`chats/${userId}`);
return chatId.data;
}

export async function getChats(offset: number = 0, limit: number = 10) {
const { data } = await instance.get<ChatParams[]>(`chats?limit=${limit}&offset=${offset}`);
return data;
const chats = new Map<string, ChatParams>();
data.forEach(chat => {
chats.set(chat.id, chat);
});
return chats;
}
10 changes: 3 additions & 7 deletions frontend/src/api/http/contacts/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ export async function getContacts() {
return contacts;
}

export async function searchContacts(username: string = "", limit: number = 10, offset: number = 0) {
const { data } = await instance.get<UserParams[]>(`users?username=${username}&limit=${limit}&offset=${offset}`);
const users = new Map<number, UserParams>();
data.forEach(user => {
users.set(user.id, user);
});
return users;
export async function getContactById(contactId: string) {
const { data } = await instance.get<ContactParams>(`users/contacts/${contactId}`);
return data;
}

export async function addContact(userId: string, alias: string) {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/http/contacts/contacts.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type ContactParams = {

export type MessageParams = {
action: string;
author: Omit<UserParams, "">;
author: UserParams;
createdAt: string;
edited: boolean;
id: string;
Expand All @@ -28,7 +28,7 @@ export type MessageParams = {

export type ChatParams = {
createdAt: string;
id: number;
id: string;
lastMessage: MessageParams;
type: number;
};
2 changes: 1 addition & 1 deletion frontend/src/api/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ instance.interceptors.request.use(
config => {
const newConfig = config;

newConfig.headers.Authorization = `Bearer ${store.getState().auth.accessToken}`;
newConfig.headers.Authorization = `Bearer ${store.getState().user.accessToken}`;
return newConfig;
},
() => {
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/api/http/user/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { instance } from "@/api/http";
import { UserParams } from "@/api/http/contacts/contacts.types";

export async function searchUser(username: string = "", limit: number = 10, offset: number = 0) {
const { data } = await instance.get<UserParams[]>(`users?username=${username}&limit=${limit}&offset=${offset}`);
const users = new Map<number, UserParams>();
data.forEach(user => {
users.set(user.id, user);
});
return users;
}

export async function getUserById(userId: string) {
const { data } = await instance.get<UserParams>(`users/${userId}`);
return data;
}
22 changes: 17 additions & 5 deletions frontend/src/components/Auth/SignInForm/SignInForm.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
"use client";

import { useFormik } from "formik";
import { jwtDecode } from "jwt-decode";
import { useRouter } from "next/navigation";
import React from "react";
import { toast } from "react-toastify";

import { signIn } from "@/api/http/auth/auth";
import { UserParams } from "@/api/http/contacts/contacts.types";
import { getUserById } from "@/api/http/user/user";
import { passwordInput, signInForm, signInValidationSchema, usernameInput } from "@/components/Auth/auth.config";
import { axiosErrorHandler, handleUsernameBlur } from "@/components/Auth/auth.utils";
import { CustomInput } from "@/components/CustomInput/CustomInput";
import { Form } from "@/components/Form/Form";
import { setAccessToken } from "@/lib/features/user/userSlice";
import { setAccessToken, setCurrentUser } from "@/lib/features/user/userSlice";
import { TokenParams } from "@/lib/features/user/userSlice.types";
import { useAppDispatch } from "@/lib/hooks";

export function SignInForm() {
Expand All @@ -24,8 +28,14 @@ export function SignInForm() {
validationSchema: signInValidationSchema,
onSubmit: async values => {
try {
const data = await signIn(values.username, values.password);
dispatch(setAccessToken(data.accessToken));
const { accessToken } = await signIn(values.username, values.password);
dispatch(setAccessToken(accessToken));
const decodedToken = jwtDecode<TokenParams>(accessToken);

const userFromToken: UserParams = await getUserById(decodedToken["user-id"]);
userFromToken.online = true;
dispatch(setCurrentUser(userFromToken));

toast.dismiss();
router.push("/chat");
} catch (error) {
Expand All @@ -47,7 +57,8 @@ export function SignInForm() {
placeholder={usernameInput.placeholder}
label={usernameInput.label}
className="text-base"
containerClassName="w-3/5"
containerClassName="w-3/5 mb-6"
innerContainerClassName="rounded-lg"
icon={usernameInput.icon}
value={formik.values.username}
onChange={formik.handleChange}
Expand All @@ -59,7 +70,8 @@ export function SignInForm() {
placeholder={passwordInput.placeholder}
type={passwordInput.type}
className="text-base"
containerClassName="w-3/5"
containerClassName="w-3/5 mb-6"
innerContainerClassName="rounded-lg"
label={passwordInput.label}
icon={passwordInput.icon}
value={formik.values.password}
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/components/Auth/SignUpForm/SignUpForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export function SignUpForm() {
placeholder={fullNameInput.placeholder}
label={fullNameInput.label}
className="text-base"
containerClassName="w-3/5"
containerClassName="w-3/5 mb-6"
innerContainerClassName="rounded-lg"
icon={fullNameInput.icon}
value={formik.values.fullName}
onChange={formik.handleChange}
Expand All @@ -63,7 +64,8 @@ export function SignUpForm() {
placeholder={usernameInput.placeholder}
label={usernameInput.label}
className="text-base"
containerClassName="w-3/5"
containerClassName="w-3/5 mb-6"
innerContainerClassName="rounded-lg"
icon={usernameInput.icon}
value={formik.values.username}
onChange={formik.handleChange}
Expand All @@ -75,7 +77,8 @@ export function SignUpForm() {
placeholder={passwordInput.placeholder}
type={passwordInput.type}
className="text-base"
containerClassName="w-3/5"
containerClassName="w-3/5 mb-6"
innerContainerClassName="rounded-lg"
label={passwordInput.label}
icon={passwordInput.icon}
value={formik.values.password}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function Avatar({ item, className, online, status = true, ...props }: Ava
/>
{status && online && (
<div className="flex justify-end items-end">
<Status className="absolute" />
<Status className="absolute" online={online} />
</div>
)}
</span>
Expand Down
37 changes: 20 additions & 17 deletions frontend/src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import React, { useCallback, useContext, useState } from "react";
import { toast } from "react-toastify";

import { addDuoChat, getChats } from "@/api/http/chat/chat";
import { addContact, getContacts, removeContact, searchContacts } from "@/api/http/contacts/contacts";
import { ChatParams, ContactParams, UserParams } from "@/api/http/contacts/contacts.types";
import { ListStateEnum } from "@/components/Chat/chat.types";
import { addDuoChat, getChatByUserId, getChats } from "@/api/http/chat/chat";
import { addContact, getContacts, removeContact } from "@/api/http/contacts/contacts";
import { ContactParams, UserParams } from "@/api/http/contacts/contacts.types";
import { searchUser } from "@/api/http/user/user";
import { ListStateEnum, MainBoxStateEnum } from "@/components/Chat/chat.types";
import { InteractiveList } from "@/components/Chat/InteractiveList/InteractiveList";
import { ContactsMap, UserMap } from "@/components/Chat/InteractiveList/interactiveList.types";
import { ChatMap, ContactsMap, UserMap } from "@/components/Chat/InteractiveList/interactiveList.types";
import { MainBox } from "@/components/Chat/MainBox/MainBox";
import { SideBar } from "@/components/Chat/SideBar/SideBar";
import { SIDEBAR_ITEM } from "@/components/Chat/SideBar/sidebar.config";
Expand All @@ -34,13 +35,13 @@ export function Chat() {
const [globalUsers, setGlobalUsers] = useState<UserMap>(new Map());
const [globalUser, setGlobalUser] = useState<UserParams | undefined>(undefined);

const [chats, setChats] = useState<ChatParams[]>([]);
const [chats, setChats] = useState<ChatMap>(new Map());

const [currentSidebarItem, setCurrentSidebarItem] = useState<string>("chat" as SidebarButtonName);

const fetchGlobalContacts = useCallback(async () => {
try {
const fetchedContacts = await searchContacts();
const fetchedContacts = await searchUser();
setGlobalUsers(fetchedContacts);
} catch (error) {
toast.error("Error fetching global users");
Expand All @@ -58,7 +59,7 @@ export function Chat() {

useAccessTokenEffect(() => {
fetchContacts();
}, [globalUsers]);
}, []);

useAccessTokenEffect(() => {
fetchGlobalContacts();
Expand Down Expand Up @@ -103,13 +104,13 @@ export function Chat() {
const handleContactClick = (currentContact: ContactParams) => {
setGlobalUser(undefined);
setContact(currentContact);
dispatch(setCurrentMainBoxState("user-info"));
dispatch(setCurrentMainBoxState(MainBoxStateEnum.USER_INFO));
};

const handleGlobalContactClick = (currentGlobalUser: UserParams) => {
setContact(undefined);
setGlobalUser(currentGlobalUser);
dispatch(setCurrentMainBoxState("user-info"));
dispatch(setCurrentMainBoxState(MainBoxStateEnum.USER_INFO));
};

const handleAddContact = async (userId: string, alias: string) => {
Expand All @@ -130,17 +131,12 @@ export function Chat() {
updatedUsers.delete(parseInt(userId, 10));
return updatedUsers;
});
await fetchContacts();

if (globalUser) {
setContact({
alias: globalUser.name,
addedAt: new Date().toISOString(),
user: globalUser,
});
setContact(contacts.get(globalUser.id));
}
setGlobalUser(undefined);

await fetchContacts();
} catch (error) {
toast.error("Error adding contact");
}
Expand All @@ -167,6 +163,12 @@ export function Chat() {
}
};

const handleMessageButtonClick = async (userId: string) => {
const chatId = await getChatByUserId(userId);
console.log(chatId);
dispatch(setCurrentMainBoxState(MainBoxStateEnum.CHAT));
};

SIDEBAR_ITEM.buttons[ListStateEnum.CONTACTS].onClick = () => {
dispatch(setCurrentInteractiveListState(ListStateEnum.CONTACTS));
};
Expand Down Expand Up @@ -228,6 +230,7 @@ export function Chat() {
globalUser={globalUser}
onAddContactClick={handleAddContact}
onRemoveContactClick={handleRemoveContact}
onMessageButtonClick={handleMessageButtonClick}
/>
</div>
);
Expand Down
22 changes: 8 additions & 14 deletions frontend/src/components/Chat/InteractiveList/InteractiveList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ export function InteractiveList({
}: Readonly<InteractiveListProps>) {
let interactiveList;
const { webSocket } = useContext(SocketContext);
const { accessToken } = useAppSelector(state => state.auth);
const { accessToken, currentUser } = useAppSelector(state => state.user);
const router = useRouter();
if (!currentUser) {
router.replace("/sign-in");
return null;
}

const handleLogout = async () => {
if (!webSocket || !accessToken) {
Expand Down Expand Up @@ -73,20 +77,10 @@ export function InteractiveList({
{interactiveList}
<div className="bg-dark-400 px-8 py-4 left-0 bottom-0 w-full flex justify-between items-center h-auto">
<div className="flex items-center gap-2">
<Avatar
item={{
id: 1,
avatarPath: undefined,
}}
alt="Avatar"
status={false}
/>
<Avatar item={currentUser} alt="Avatar" status={false} />
<div className="flex flex-col gap-0">
<p className="text-lg">Name</p>
<div className="flex items-center text-gray-300">
<Status />
Active
</div>
<p className="text-lg">{currentUser.name}</p>
<Status online={currentUser.online} textStatus />
</div>
</div>
<CustomButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type InteractiveListVariant = ListStateEnum.CONTACTS | ListStateEnum.CHAT

export type ContactsMap = Map<number, ContactParams>;
export type UserMap = Map<number, UserParams>;
export type ChatMap = Map<string, ChatParams>;

export type InteractiveContactParams = {
currentContact?: ContactParams;
Expand All @@ -14,7 +15,7 @@ export type InteractiveContactParams = {

export type InteractiveChatParams = {
currentChat?: ChatParams;
chats?: ChatParams[];
chats?: ChatMap;
onChatClick?: (id: ChatParams) => void;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { ScrollList } from "@/components/ScrollList/ScrollList";
export function ChatList({ chats }: Readonly<InteractiveChatParams>) {
return (
<ScrollList>
{!chats || (chats && chats.length === 0) ? (
{!chats || (chats && chats.size === 0) ? (
<div className="h-full bg-dark-500 flex items-center justify-center">No chats</div>
) : (
chats.map(chat => <ChatItem key={chat.id} chat={chat} />)
Array.from(chats.values()).map(chat => <ChatItem key={chat.id} chat={chat} />)
)}
</ScrollList>
);
Expand Down
Loading

0 comments on commit 11e5dc8

Please sign in to comment.