Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.2",
"@reduxjs/toolkit": "^2.3.0",
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function App() {
{!isLoading && (
<Routes>
<Route path="/login" Component={Login} />
<Route path="/" Component={Main} />
<Route path="/*" Component={Main} />
</Routes>
)}
</div>
Expand Down
60 changes: 60 additions & 0 deletions frontend/src/components/chats/ChatItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import {
chooseTwoCharsFromName,
randomTailwindBackgroundColor,
} from "@/lib/utils";

interface ChatItemProps {
id: string;
name: string;
lastMessage: string;
avatar: {
image?: string;
fallbackColorGen: string;
};
timestamp: string;
unread: number;
onClick: (chatId: string) => void;
}

export default function ChatRooms({
name,
lastMessage,
avatar,
timestamp,
unread,
id,
onClick,
}: ChatItemProps) {
return (
<div
className="flex items-center space-x-4 p-4 hover:bg-gray-100 dark:hover:bg-slate-800 cursor-pointer"
onClick={() => {
onClick(id);
}}
>
<Avatar>
{avatar.image && <AvatarImage src={avatar.image} alt={name} />}
<AvatarFallback
className={
"text-white font-semibold " +
randomTailwindBackgroundColor(avatar.fallbackColorGen)
}
>
{chooseTwoCharsFromName(name)}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center mb-1">
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{name}
</h3>
<p className="text-xs text-gray-500 truncate">{timestamp}</p>
</div>
<p className="text-sm text-gray-500 truncate">{lastMessage}</p>
</div>
{unread > 0 && <Badge className="rounded-full px-2 py-1">new</Badge>}
</div>
);
}
36 changes: 36 additions & 0 deletions frontend/src/components/chats/ChatList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { getChatById } from "@/statemanagement/chats/chatSlice";
import { useAppDispatch, useAppSelector } from "@/statemanagement/store";
import { useCallback } from "react";
import ChatRooms from "./ChatItem";

export default function ChatList() {
const dispatch = useAppDispatch();
const setChatId = useCallback(
(chatId: string) => {
dispatch(getChatById({ chatId }));
},
[dispatch]
);
const chats = useAppSelector((state) => {
const chats = state.chats.chats;
return chats.map((chat) => {
return {
id: chat.id,
name: chat.name,
lastMessage: "",
avatar: {
fallbackColorGen: chat.id,
},
timestamp: "",
unread: chat.unreadMessages ?? 0,
};
});
});
return (
<div>
{chats.map((chat) => (
<ChatRooms key={chat.id} {...chat} onClick={setChatId} />
))}
</div>
);
}
14 changes: 14 additions & 0 deletions frontend/src/components/chats/ChatSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import ChatList from "./ChatList";

export default function ChatSidebar() {
return (
<div className="w-full h-full md:w-1/3 lg:w-1/4 border-r border-gray-200 dark:border-slate-800 flex flex-col">
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Chats</h1>
</div>
<div className="flex-1 overflow-y-auto">
<ChatList />
</div>
</div>
);
}
111 changes: 111 additions & 0 deletions frontend/src/components/chats/ChatWindow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
chooseTwoCharsFromName,
randomTailwindBackgroundColor,
} from "@/lib/utils";
import { User } from "@/types/user.type";
import { Send } from "lucide-react";
import { useEffect, useRef, useState } from "react";

interface Message {
id: string;
content: string;
ownMessage: boolean;
timestamp: string;
}

export default function ChatWindow({
withUser,
messages,
onMessageSend,
}: {
withUser: User;
messages: Message[];
onMessageSend: (message: { text: string }) => void;
}) {
const [newMessage, setNewMessage] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
console.log(messagesEndRef.current);
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);

const handleSendMessage = () => {
if (newMessage.trim()) {
const message = {
text: newMessage,
};
onMessageSend(message);
setNewMessage("");
}
};

return (
<div className="flex-1 flex flex-col">
<div className="border-b border-gray-200 dark:border-slate-800 p-4 flex items-center space-x-4">
<Avatar>
<AvatarFallback
className={
"text-white font-semibold " +
randomTailwindBackgroundColor(withUser.id)
}
>
{chooseTwoCharsFromName(withUser.display_name)}
</AvatarFallback>
</Avatar>
<div>
<h2 className="text-black dark:text-white text-lg font-semibold">
{withUser.display_name}
</h2>
<p className="text-sm text-gray-500">Unkown Status</p>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-4 max-h-[calc(100vh-265px)]">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.ownMessage ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-xs md:max-w-md lg:max-w-lg xl:max-w-xl px-4 py-2 rounded-lg ${
message.ownMessage
? "bg-slate-800 text-white"
: "bg-gray-200 text-gray-800"
}`}
>
<p>{message.content}</p>
<p
className={`text-xs mt-1 ${
message.ownMessage ? "text-blue-100" : "text-gray-500"
}`}
>
{message.timestamp}
</p>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="border-t border-gray-200 dark:border-slate-800 p-4">
<div className="flex space-x-2">
<Input
type="text"
placeholder="Type a message..."
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
onKeyUp={(e) => e.key === "Enter" && handleSendMessage()}
className="flex-1"
/>
<Button onClick={handleSendMessage}>
<Send className="h-4 w-4" />
<span className="sr-only">Send message</span>
</Button>
</div>
</div>
</div>
);
}
39 changes: 39 additions & 0 deletions frontend/src/components/chats/ChatWindow_wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { addMessage, setReadMessages } from "@/statemanagement/chats/chatSlice";
import { useAppDispatch, useAppSelector } from "@/statemanagement/store";
import { useEffect } from "react";
import ChatWindow from "./ChatWindow";

export default function ChatWindowWrapper() {
const dispatch = useAppDispatch();
const { withUser, chatMessages } = useAppSelector((state) => {
const ownUser = state.users.ownUser;
const chats = state.chats.chats;
const chat = chats.find((chat) => chat.id === state.chats.activeChatId);
let otherUserId = chat?.user_ids.find((user) => user !== ownUser?.id);
let withUser = state.users.users.find((user) => user.id === otherUserId);
return {
withUser,
chatMessages:
chat?.messages?.map((message) => {
return {
id: message.id,
content: message.text,
ownMessage: message.user_id === ownUser?.id,
timestamp: "",
};
}) ?? null,
};
});
useEffect(() => {
dispatch(setReadMessages());
}, [chatMessages]);
return (
<ChatWindow
withUser={withUser || { display_name: "", id: "" }}
messages={chatMessages ?? []}
onMessageSend={(message) => {
dispatch(addMessage(message));
}}
/>
);
}
Loading
Loading