From 2f953b3a2ca693feb3608ba9216df68a1823f4f1 Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Mon, 2 Dec 2024 23:19:39 +0100 Subject: [PATCH 01/15] Update chat functionality: add Chats page and components; refactor routing; update avatar dependency --- frontend/package.json | 2 +- frontend/src/App.tsx | 2 +- frontend/src/components/chats/ChatItem.tsx | 39 ++++++ frontend/src/components/chats/ChatList.tsx | 55 ++++++++ frontend/src/components/chats/ChatSidebar.tsx | 14 ++ frontend/src/components/chats/ChatWindow.tsx | 121 ++++++++++++++++++ frontend/src/components/header/header.tsx | 21 +-- .../src/components/social-network-layout.tsx | 15 --- frontend/src/components/ui/badge.tsx | 36 ++++++ frontend/src/pages/main.tsx | 13 +- frontend/src/pages/subpages/Chats.tsx | 16 +++ frontend/src/pages/subpages/Main.tsx | 15 +++ frontend/yarn.lock | 10 +- 13 files changed, 325 insertions(+), 34 deletions(-) create mode 100644 frontend/src/components/chats/ChatItem.tsx create mode 100644 frontend/src/components/chats/ChatList.tsx create mode 100644 frontend/src/components/chats/ChatSidebar.tsx create mode 100644 frontend/src/components/chats/ChatWindow.tsx delete mode 100644 frontend/src/components/social-network-layout.tsx create mode 100644 frontend/src/components/ui/badge.tsx create mode 100644 frontend/src/pages/subpages/Chats.tsx create mode 100644 frontend/src/pages/subpages/Main.tsx diff --git a/frontend/package.json b/frontend/package.json index 3e6ccb3..8d0a3da 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "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", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 58194bd..b1b30bc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -51,7 +51,7 @@ function App() { {!isLoading && ( - + )} diff --git a/frontend/src/components/chats/ChatItem.tsx b/frontend/src/components/chats/ChatItem.tsx new file mode 100644 index 0000000..1669a48 --- /dev/null +++ b/frontend/src/components/chats/ChatItem.tsx @@ -0,0 +1,39 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; + +interface ChatItemProps { + name: string; + lastMessage: string; + avatar: string; + timestamp: string; + unread: number; +} + +export default function ChatItem({ + name, + lastMessage, + avatar, + timestamp, + unread, +}: ChatItemProps) { + return ( +
+ + + {name.slice(0, 2)} + +
+
+

{name}

+

{timestamp}

+
+

{lastMessage}

+
+ {unread > 0 && ( + + {unread} + + )} +
+ ); +} diff --git a/frontend/src/components/chats/ChatList.tsx b/frontend/src/components/chats/ChatList.tsx new file mode 100644 index 0000000..d170983 --- /dev/null +++ b/frontend/src/components/chats/ChatList.tsx @@ -0,0 +1,55 @@ +import ChatItem from "./ChatItem"; + +const chats = [ + { + id: 1, + name: "Alice Johnson", + lastMessage: "Hey, how are you doing?", + avatar: "/placeholder.svg?height=40&width=40", + timestamp: "5m ago", + unread: 2, + }, + { + id: 2, + name: "Bob Smith", + lastMessage: "Did you see the latest post?", + avatar: "/placeholder.svg?height=40&width=40", + timestamp: "1h ago", + unread: 0, + }, + { + id: 3, + name: "Carol Williams", + lastMessage: "Let's meet up this weekend!", + avatar: "/placeholder.svg?height=40&width=40", + timestamp: "2h ago", + unread: 1, + }, + // Add more chat items to make the list scrollable + { + id: 4, + name: "David Brown", + lastMessage: "Thanks for your help!", + avatar: "/placeholder.svg?height=40&width=40", + timestamp: "1d ago", + unread: 0, + }, + { + id: 5, + name: "Emma Davis", + lastMessage: "Can you send me the files?", + avatar: "/placeholder.svg?height=40&width=40", + timestamp: "2d ago", + unread: 3, + }, +]; + +export default function ChatList() { + return ( +
+ {chats.map((chat) => ( + + ))} +
+ ); +} diff --git a/frontend/src/components/chats/ChatSidebar.tsx b/frontend/src/components/chats/ChatSidebar.tsx new file mode 100644 index 0000000..3fd19d9 --- /dev/null +++ b/frontend/src/components/chats/ChatSidebar.tsx @@ -0,0 +1,14 @@ +import ChatList from "./ChatList"; + +export default function ChatSidebar() { + return ( +
+
+

Chats

+
+
+ +
+
+ ); +} diff --git a/frontend/src/components/chats/ChatWindow.tsx b/frontend/src/components/chats/ChatWindow.tsx new file mode 100644 index 0000000..c32de13 --- /dev/null +++ b/frontend/src/components/chats/ChatWindow.tsx @@ -0,0 +1,121 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Send } from "lucide-react"; +import { useState } from "react"; + +interface Message { + id: number; + content: string; + sender: "user" | "other"; + timestamp: string; +} + +const initialMessages: Message[] = [ + { + id: 1, + content: "Hey there! How's it going?", + sender: "other", + timestamp: "10:00 AM", + }, + { + id: 2, + content: "Hi! I'm doing well, thanks. How about you?", + sender: "user", + timestamp: "10:02 AM", + }, + { + id: 3, + content: "I'm great! Just working on some new features for our app.", + sender: "other", + timestamp: "10:05 AM", + }, + { + id: 4, + content: "That sounds exciting! Can't wait to see what you come up with.", + sender: "user", + timestamp: "10:07 AM", + }, +]; + +export default function ChatWindow() { + const [messages, setMessages] = useState(initialMessages); + const [newMessage, setNewMessage] = useState(""); + + const handleSendMessage = () => { + if (newMessage.trim()) { + const message: Message = { + id: messages.length + 1, + content: newMessage, + sender: "user", + timestamp: new Date().toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }), + }; + setMessages([...messages, message]); + setNewMessage(""); + } + }; + + return ( +
+
+ + + AJ + +
+

Alice Johnson

+

Online

+
+
+
+ {messages.map((message) => ( +
+
+

{message.content}

+

+ {message.timestamp} +

+
+
+ ))} +
+
+
+ setNewMessage(e.target.value)} + onKeyPress={(e) => e.key === "Enter" && handleSendMessage()} + className="flex-1" + /> + +
+
+
+ ); +} diff --git a/frontend/src/components/header/header.tsx b/frontend/src/components/header/header.tsx index 707a6de..f6f68d5 100644 --- a/frontend/src/components/header/header.tsx +++ b/frontend/src/components/header/header.tsx @@ -7,6 +7,7 @@ import { } from "@/lib/utils"; import { useAppSelector } from "@/statemanagement/store"; import { Bell, Home, Mail, Users } from "lucide-react"; +import { Link } from "react-router-dom"; import { ModeToggle } from "../theming/themetoggle"; export const Header = () => { const user = useAppSelector((state) => state.users.ownUser); @@ -18,18 +19,22 @@ export const Header = () => { PRYVT
diff --git a/frontend/src/components/social-network-layout.tsx b/frontend/src/components/social-network-layout.tsx deleted file mode 100644 index 68e56b7..0000000 --- a/frontend/src/components/social-network-layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { CreatePostWrapper } from "./posts/createpost_wrapper"; -import { PostList } from "./posts/postlist"; -import { UserPanel } from "./users/usersPanel"; - -export function SocialNetworkLayout() { - return ( -
-
- - -
- -
- ); -} diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 0000000..da6c375 --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border border-slate-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2 dark:border-slate-800 dark:focus:ring-slate-300", + { + variants: { + variant: { + default: + "border-transparent bg-slate-900 text-slate-50 shadow hover:bg-slate-900/80 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/80", + secondary: + "border-transparent bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-50 dark:hover:bg-slate-800/80", + destructive: + "border-transparent bg-red-500 text-slate-50 shadow hover:bg-red-500/80 dark:bg-red-900 dark:text-slate-50 dark:hover:bg-red-900/80", + outline: "text-slate-950 dark:text-slate-50", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/frontend/src/pages/main.tsx b/frontend/src/pages/main.tsx index 2d16e17..fd7168c 100644 --- a/frontend/src/pages/main.tsx +++ b/frontend/src/pages/main.tsx @@ -1,14 +1,19 @@ import { Header } from "@/components/header/header"; -import { SocialNetworkLayout } from "@/components/social-network-layout"; +import { MainPage } from "@/pages/subpages/Main"; import { WebsocketProvider } from "@/websocket/websocketProvider"; +import { Route, Routes } from "react-router-dom"; +import { ChatsPage } from "./subpages/Chats"; export const Main = () => { return (
-
-
- +
+
+ + } /> + } /> +
diff --git a/frontend/src/pages/subpages/Chats.tsx b/frontend/src/pages/subpages/Chats.tsx new file mode 100644 index 0000000..48616a5 --- /dev/null +++ b/frontend/src/pages/subpages/Chats.tsx @@ -0,0 +1,16 @@ +import ChatSidebar from "@/components/chats/ChatSidebar"; +import ChatWindow from "@/components/chats/ChatWindow"; +import { Card, CardContent } from "@/components/ui/card"; + +export const ChatsPage = () => { + return ( + + +
+ + +
+
+
+ ); +}; diff --git a/frontend/src/pages/subpages/Main.tsx b/frontend/src/pages/subpages/Main.tsx new file mode 100644 index 0000000..a89f150 --- /dev/null +++ b/frontend/src/pages/subpages/Main.tsx @@ -0,0 +1,15 @@ +import { CreatePostWrapper } from "../../components/posts/createpost_wrapper"; +import { PostList } from "../../components/posts/postlist"; +import { UserPanel } from "../../components/users/usersPanel"; + +export function MainPage() { + return ( +
+
+ + +
+ +
+ ); +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8b89c8b..81ed81f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -301,12 +301,12 @@ dependencies: "@radix-ui/react-primitive" "2.0.0" -"@radix-ui/react-avatar@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.0.tgz#457c81334c93f4608df15f081e7baa286558d6a2" - integrity sha512-Q/PbuSMk/vyAd/UoIShVGZ7StHHeRFYU7wXmi5GV+8cLXflZAEpHL/F697H1klrzxKXNtZ97vWiC0q3RKUH8UA== +"@radix-ui/react-avatar@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.1.tgz#5848d2ed5f34d18b36fc7e2d227c41fca8600ea1" + integrity sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw== dependencies: - "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-context" "1.1.1" "@radix-ui/react-primitive" "2.0.0" "@radix-ui/react-use-callback-ref" "1.1.0" "@radix-ui/react-use-layout-effect" "1.1.0" From 73eaf0325f463884872c82d229d6825c9ed9178b Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Tue, 3 Dec 2024 19:00:21 +0100 Subject: [PATCH 02/15] Refactor Chat components: update avatar structure, enhance styling, and improve responsiveness --- frontend/src/components/chats/ChatItem.tsx | 33 +++++++++++++---- frontend/src/components/chats/ChatList.tsx | 37 +++++-------------- frontend/src/components/chats/ChatSidebar.tsx | 2 +- frontend/src/components/chats/ChatWindow.tsx | 12 +++--- frontend/src/pages/main.tsx | 2 +- frontend/src/pages/subpages/Chats.tsx | 4 +- 6 files changed, 46 insertions(+), 44 deletions(-) diff --git a/frontend/src/components/chats/ChatItem.tsx b/frontend/src/components/chats/ChatItem.tsx index 1669a48..73a8ff7 100644 --- a/frontend/src/components/chats/ChatItem.tsx +++ b/frontend/src/components/chats/ChatItem.tsx @@ -1,10 +1,17 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; +import { + chooseTwoCharsFromName, + randomTailwindBackgroundColor, +} from "@/lib/utils"; interface ChatItemProps { name: string; lastMessage: string; - avatar: string; + avatar: { + image?: string; + fallbackColorGen: string; + }; timestamp: string; unread: number; } @@ -17,20 +24,32 @@ export default function ChatItem({ unread, }: ChatItemProps) { return ( -
+
- - {name.slice(0, 2)} + {avatar.image && } + + {chooseTwoCharsFromName(name)} +
-

{name}

-

{timestamp}

+

+ {name} +

+

{timestamp}

{lastMessage}

{unread > 0 && ( - + {unread} )} diff --git a/frontend/src/components/chats/ChatList.tsx b/frontend/src/components/chats/ChatList.tsx index d170983..f8e6097 100644 --- a/frontend/src/components/chats/ChatList.tsx +++ b/frontend/src/components/chats/ChatList.tsx @@ -3,9 +3,12 @@ import ChatItem from "./ChatItem"; const chats = [ { id: 1, - name: "Alice Johnson", + name: "Alice Johnsonn", lastMessage: "Hey, how are you doing?", - avatar: "/placeholder.svg?height=40&width=40", + avatar: { + image: "/placeholder.svg?height=40&width=40", + fallbackColorGen: "Alice Johnsonn", + }, timestamp: "5m ago", unread: 2, }, @@ -13,35 +16,13 @@ const chats = [ id: 2, name: "Bob Smith", lastMessage: "Did you see the latest post?", - avatar: "/placeholder.svg?height=40&width=40", + avatar: { + image: "/placeholder.svg?height=40&width=40", + fallbackColorGen: "Bob Smith", + }, timestamp: "1h ago", unread: 0, }, - { - id: 3, - name: "Carol Williams", - lastMessage: "Let's meet up this weekend!", - avatar: "/placeholder.svg?height=40&width=40", - timestamp: "2h ago", - unread: 1, - }, - // Add more chat items to make the list scrollable - { - id: 4, - name: "David Brown", - lastMessage: "Thanks for your help!", - avatar: "/placeholder.svg?height=40&width=40", - timestamp: "1d ago", - unread: 0, - }, - { - id: 5, - name: "Emma Davis", - lastMessage: "Can you send me the files?", - avatar: "/placeholder.svg?height=40&width=40", - timestamp: "2d ago", - unread: 3, - }, ]; export default function ChatList() { diff --git a/frontend/src/components/chats/ChatSidebar.tsx b/frontend/src/components/chats/ChatSidebar.tsx index 3fd19d9..cafb034 100644 --- a/frontend/src/components/chats/ChatSidebar.tsx +++ b/frontend/src/components/chats/ChatSidebar.tsx @@ -2,7 +2,7 @@ import ChatList from "./ChatList"; export default function ChatSidebar() { return ( -
+

Chats

diff --git a/frontend/src/components/chats/ChatWindow.tsx b/frontend/src/components/chats/ChatWindow.tsx index c32de13..893c12b 100644 --- a/frontend/src/components/chats/ChatWindow.tsx +++ b/frontend/src/components/chats/ChatWindow.tsx @@ -59,8 +59,8 @@ export default function ChatWindow() { }; return ( -
-
+
+
AJ
-

Alice Johnson

+

+ Alice Johnson +

Online

@@ -84,7 +86,7 @@ export default function ChatWindow() {
@@ -100,7 +102,7 @@ export default function ChatWindow() {
))}
-
+
{
-
+
} /> } /> diff --git a/frontend/src/pages/subpages/Chats.tsx b/frontend/src/pages/subpages/Chats.tsx index 48616a5..a37a3cb 100644 --- a/frontend/src/pages/subpages/Chats.tsx +++ b/frontend/src/pages/subpages/Chats.tsx @@ -4,8 +4,8 @@ import { Card, CardContent } from "@/components/ui/card"; export const ChatsPage = () => { return ( - - + +
From 88cfc386b31a081020fc957a0f852876c39ea6da Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Thu, 5 Dec 2024 18:27:55 +0100 Subject: [PATCH 03/15] Add chat management: implement chat slice, integrate chat functionality, and refactor Chat components --- frontend/src/components/chats/ChatItem.tsx | 2 +- frontend/src/components/chats/ChatList.tsx | 45 ++++----- frontend/src/components/posts/postlist.tsx | 8 +- frontend/src/pages/main.tsx | 9 ++ .../src/statemanagement/chats/chatSlice.ts | 97 +++++++++++++++++++ frontend/src/statemanagement/store.ts | 2 + frontend/src/types/chatmessage.type.ts | 7 ++ frontend/src/types/chatroom.type.ts | 8 ++ 8 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 frontend/src/statemanagement/chats/chatSlice.ts create mode 100644 frontend/src/types/chatmessage.type.ts create mode 100644 frontend/src/types/chatroom.type.ts diff --git a/frontend/src/components/chats/ChatItem.tsx b/frontend/src/components/chats/ChatItem.tsx index 73a8ff7..ee123f4 100644 --- a/frontend/src/components/chats/ChatItem.tsx +++ b/frontend/src/components/chats/ChatItem.tsx @@ -16,7 +16,7 @@ interface ChatItemProps { unread: number; } -export default function ChatItem({ +export default function ChatRooms({ name, lastMessage, avatar, diff --git a/frontend/src/components/chats/ChatList.tsx b/frontend/src/components/chats/ChatList.tsx index f8e6097..f3e285c 100644 --- a/frontend/src/components/chats/ChatList.tsx +++ b/frontend/src/components/chats/ChatList.tsx @@ -1,35 +1,26 @@ -import ChatItem from "./ChatItem"; - -const chats = [ - { - id: 1, - name: "Alice Johnsonn", - lastMessage: "Hey, how are you doing?", - avatar: { - image: "/placeholder.svg?height=40&width=40", - fallbackColorGen: "Alice Johnsonn", - }, - timestamp: "5m ago", - unread: 2, - }, - { - id: 2, - name: "Bob Smith", - lastMessage: "Did you see the latest post?", - avatar: { - image: "/placeholder.svg?height=40&width=40", - fallbackColorGen: "Bob Smith", - }, - timestamp: "1h ago", - unread: 0, - }, -]; +import { useAppSelector } from "@/statemanagement/store"; +import ChatRooms from "./ChatItem"; export default function ChatList() { + 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: 0, + }; + }); + }); return (
{chats.map((chat) => ( - + ))}
); diff --git a/frontend/src/components/posts/postlist.tsx b/frontend/src/components/posts/postlist.tsx index ef0ef0c..aac06cb 100644 --- a/frontend/src/components/posts/postlist.tsx +++ b/frontend/src/components/posts/postlist.tsx @@ -1,7 +1,5 @@ -import { getAllPosts } from "@/statemanagement/posting/postSlice"; -import { useAppDispatch, useAppSelector } from "@/statemanagement/store"; +import { useAppSelector } from "@/statemanagement/store"; import { EnhancedPost } from "@/types/enhanced_post.type"; -import { useEffect } from "react"; import { ContentCard } from "./contentcard"; export const PostList = () => { @@ -22,10 +20,6 @@ export const PostList = () => { }); return enahncedPosts; }); - const dispatch = useAppDispatch(); - useEffect(() => { - dispatch(getAllPosts()); - }, []); return ( <> diff --git a/frontend/src/pages/main.tsx b/frontend/src/pages/main.tsx index d5bb177..88c2cb6 100644 --- a/frontend/src/pages/main.tsx +++ b/frontend/src/pages/main.tsx @@ -1,10 +1,19 @@ import { Header } from "@/components/header/header"; import { MainPage } from "@/pages/subpages/Main"; +import { getAllChats } from "@/statemanagement/chats/chatSlice"; +import { getAllPosts } from "@/statemanagement/posting/postSlice"; +import { useAppDispatch } from "@/statemanagement/store"; import { WebsocketProvider } from "@/websocket/websocketProvider"; +import { useEffect } from "react"; import { Route, Routes } from "react-router-dom"; import { ChatsPage } from "./subpages/Chats"; export const Main = () => { + const dispatch = useAppDispatch(); + useEffect(() => { + dispatch(getAllPosts()); + dispatch(getAllChats()); + }, []); return (
diff --git a/frontend/src/statemanagement/chats/chatSlice.ts b/frontend/src/statemanagement/chats/chatSlice.ts new file mode 100644 index 0000000..690c13f --- /dev/null +++ b/frontend/src/statemanagement/chats/chatSlice.ts @@ -0,0 +1,97 @@ +import { ChatRoom } from "@/types/chatroom.type"; +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +export interface ChatState { + chats: ChatRoom[]; + chatsLoading: boolean; + messageSending: boolean; +} + +const initialState: ChatState = { + chats: [], + chatsLoading: false, + messageSending: false, +}; + +export const getAllChats = createAsyncThunk("chats/getAll", async () => { + let token = window.sessionStorage.getItem("token"); + + const response = await fetch( + "https://" + window.envUrl + "/api/v1/chats/query/chats/", + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + if (response.status > 299) { + throw new Error("Request failed with " + response.status); + } + const data = await response.json(); + console.log(data); + return data; +}); + +export const addMessage = createAsyncThunk( + "chats/add", + async (payload: { text?: string; imageBase64?: string }) => { + /*let token = window.sessionStorage.getItem("token"); + const tokenDec = jwtDecode(token!); + const chat = { + id: uuidv4(), + text: payload.text, + image_base64: payload.imageBase64, + user_id: tokenDec.sub, + change_date: new Date().toISOString(), + } as ChatRoom; + const response = await fetch( + "https://" + window.envUrl + "/api/v1/chats/command/chats/", + { + headers: { + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(chat), + method: "Chat", + } + ); + if (response.status > 299) { + throw new Error("Request failed with " + response.status); + } + return chat;*/ + } +); + +export const chatSlice = createSlice({ + name: "chats", + initialState, + reducers: { + addChatSync: (state, action) => { + state.chats.unshift(action.payload); + }, + }, + extraReducers: (builder) => { + builder.addCase(getAllChats.pending, (state) => { + state.chatsLoading = true; + }); + builder.addCase(getAllChats.rejected, (state) => { + state.chatsLoading = false; + }); + builder.addCase(getAllChats.fulfilled, (state, action) => { + state.chatsLoading = false; + state.chats = action.payload; + }); + builder.addCase(addMessage.pending, (state) => { + state.messageSending = true; + }); + builder.addCase(addMessage.rejected, (state, err) => { + state.messageSending = false; + console.log(err); + }); + builder.addCase(addMessage.fulfilled, (state) => { + state.messageSending = false; + }); + }, +}); + +export const { addChatSync } = chatSlice.actions; + +export default chatSlice.reducer; diff --git a/frontend/src/statemanagement/store.ts b/frontend/src/statemanagement/store.ts index 914ae41..59796d8 100644 --- a/frontend/src/statemanagement/store.ts +++ b/frontend/src/statemanagement/store.ts @@ -1,6 +1,7 @@ import { configureStore } from "@reduxjs/toolkit"; import { useDispatch, useSelector } from "react-redux"; import authenticationReducer from "./authentication/authenticationSlice"; +import chatReducer from "./chats/chatSlice"; import postReducer from "./posting/postSlice"; import usersReducer from "./users/usersSlice"; @@ -9,6 +10,7 @@ export const store = configureStore({ authentication: authenticationReducer, users: usersReducer, posts: postReducer, + chats: chatReducer, }, }); diff --git a/frontend/src/types/chatmessage.type.ts b/frontend/src/types/chatmessage.type.ts new file mode 100644 index 0000000..b1fc85b --- /dev/null +++ b/frontend/src/types/chatmessage.type.ts @@ -0,0 +1,7 @@ +export interface ChatMessage { + id: string; + text: string; + image_base64: string; + user_id: string; + creation_date: string; +} diff --git a/frontend/src/types/chatroom.type.ts b/frontend/src/types/chatroom.type.ts new file mode 100644 index 0000000..2cdedde --- /dev/null +++ b/frontend/src/types/chatroom.type.ts @@ -0,0 +1,8 @@ +import { ChatMessage } from "./chatmessage.type"; + +export interface ChatRoom { + id: string; + name: string; + messages?: ChatMessage[]; + isLoading?: boolean; +} From 24ce777da1fefd58285659a600b3687d2b875f8c Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Thu, 5 Dec 2024 18:37:55 +0100 Subject: [PATCH 04/15] Refactor ChatWindow: replace numeric IDs with UUIDs, update message structure, and create ChatWindowWrapper for state management --- frontend/src/components/chats/ChatWindow.tsx | 47 ++++--------------- .../components/chats/ChatWindow_wrapper.tsx | 21 +++++++++ frontend/src/pages/subpages/Chats.tsx | 4 +- .../src/statemanagement/chats/chatSlice.ts | 1 + 4 files changed, 34 insertions(+), 39 deletions(-) create mode 100644 frontend/src/components/chats/ChatWindow_wrapper.tsx diff --git a/frontend/src/components/chats/ChatWindow.tsx b/frontend/src/components/chats/ChatWindow.tsx index 893c12b..329a83f 100644 --- a/frontend/src/components/chats/ChatWindow.tsx +++ b/frontend/src/components/chats/ChatWindow.tsx @@ -3,57 +3,30 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Send } from "lucide-react"; import { useState } from "react"; +import { v4 as uuidv4 } from "uuid"; interface Message { - id: number; + id: string; content: string; - sender: "user" | "other"; + ownMessage: boolean; timestamp: string; } -const initialMessages: Message[] = [ - { - id: 1, - content: "Hey there! How's it going?", - sender: "other", - timestamp: "10:00 AM", - }, - { - id: 2, - content: "Hi! I'm doing well, thanks. How about you?", - sender: "user", - timestamp: "10:02 AM", - }, - { - id: 3, - content: "I'm great! Just working on some new features for our app.", - sender: "other", - timestamp: "10:05 AM", - }, - { - id: 4, - content: "That sounds exciting! Can't wait to see what you come up with.", - sender: "user", - timestamp: "10:07 AM", - }, -]; - -export default function ChatWindow() { - const [messages, setMessages] = useState(initialMessages); +export default function ChatWindow({ messages }: { messages: Message[] }) { const [newMessage, setNewMessage] = useState(""); const handleSendMessage = () => { if (newMessage.trim()) { const message: Message = { - id: messages.length + 1, + id: uuidv4(), content: newMessage, - sender: "user", + ownMessage: true, timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }), }; - setMessages([...messages, message]); + console.log(message); setNewMessage(""); } }; @@ -80,12 +53,12 @@ export default function ChatWindow() {
{message.content}

{message.timestamp} diff --git a/frontend/src/components/chats/ChatWindow_wrapper.tsx b/frontend/src/components/chats/ChatWindow_wrapper.tsx new file mode 100644 index 0000000..7407a14 --- /dev/null +++ b/frontend/src/components/chats/ChatWindow_wrapper.tsx @@ -0,0 +1,21 @@ +import { useAppSelector } from "@/statemanagement/store"; +import ChatWindow from "./ChatWindow"; + +export default function ChatWindowWrapper() { + const chatMessages = useAppSelector((state) => { + const ownUser = state.users.ownUser; + const chats = state.chats.chats; + const chat = chats.find((chat) => chat.id === state.chats.activeChatId); + return ( + chat?.messages?.map((message) => { + return { + id: message.id, + content: message.text, + ownMessage: message.user_id === ownUser?.id, + timestamp: "", + }; + }) ?? null + ); + }); + return ; +} diff --git a/frontend/src/pages/subpages/Chats.tsx b/frontend/src/pages/subpages/Chats.tsx index a37a3cb..bd7c3b1 100644 --- a/frontend/src/pages/subpages/Chats.tsx +++ b/frontend/src/pages/subpages/Chats.tsx @@ -1,5 +1,5 @@ import ChatSidebar from "@/components/chats/ChatSidebar"; -import ChatWindow from "@/components/chats/ChatWindow"; +import ChatWindowWrapper from "@/components/chats/ChatWindow_wrapper"; import { Card, CardContent } from "@/components/ui/card"; export const ChatsPage = () => { @@ -8,7 +8,7 @@ export const ChatsPage = () => {

- +
diff --git a/frontend/src/statemanagement/chats/chatSlice.ts b/frontend/src/statemanagement/chats/chatSlice.ts index 690c13f..15874f5 100644 --- a/frontend/src/statemanagement/chats/chatSlice.ts +++ b/frontend/src/statemanagement/chats/chatSlice.ts @@ -4,6 +4,7 @@ export interface ChatState { chats: ChatRoom[]; chatsLoading: boolean; messageSending: boolean; + activeChatId?: string; } const initialState: ChatState = { From 8422d9110adb5d8e4332bfdf20dcc1ef696224ec Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Fri, 6 Dec 2024 16:35:18 +0100 Subject: [PATCH 05/15] Enhance chat functionality: add onClick handler to ChatItem, update ChatList to manage active chat, and implement getChatById in chatSlice for improved chat loading --- frontend/src/components/chats/ChatItem.tsx | 11 ++- frontend/src/components/chats/ChatList.tsx | 8 +- frontend/src/components/chats/ChatSidebar.tsx | 16 +++- .../src/statemanagement/chats/chatSlice.ts | 74 +++++++++++++++++-- frontend/src/types/chatroom.type.ts | 1 + 5 files changed, 98 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/chats/ChatItem.tsx b/frontend/src/components/chats/ChatItem.tsx index ee123f4..6cb86d5 100644 --- a/frontend/src/components/chats/ChatItem.tsx +++ b/frontend/src/components/chats/ChatItem.tsx @@ -6,6 +6,7 @@ import { } from "@/lib/utils"; interface ChatItemProps { + id: string; name: string; lastMessage: string; avatar: { @@ -14,6 +15,7 @@ interface ChatItemProps { }; timestamp: string; unread: number; + onClick: (chatId: string) => void; } export default function ChatRooms({ @@ -22,9 +24,16 @@ export default function ChatRooms({ avatar, timestamp, unread, + id, + onClick, }: ChatItemProps) { return ( -
+
{ + onClick(id); + }} + > {avatar.image && } void; +}) { const chats = useAppSelector((state) => { const chats = state.chats.chats; return chats.map((chat) => { @@ -20,7 +24,7 @@ export default function ChatList() { return (
{chats.map((chat) => ( - + ))}
); diff --git a/frontend/src/components/chats/ChatSidebar.tsx b/frontend/src/components/chats/ChatSidebar.tsx index cafb034..fcf4084 100644 --- a/frontend/src/components/chats/ChatSidebar.tsx +++ b/frontend/src/components/chats/ChatSidebar.tsx @@ -1,13 +1,27 @@ +import { + getChatById, + setActiveChatId, +} from "@/statemanagement/chats/chatSlice"; +import { useAppDispatch } from "@/statemanagement/store"; +import { useCallback } from "react"; import ChatList from "./ChatList"; export default function ChatSidebar() { + const dispatch = useAppDispatch(); + const setChatId = useCallback( + (chatId: string) => { + dispatch(setActiveChatId(chatId)); + dispatch(getChatById({ chatId })); + }, + [dispatch] + ); return (

Chats

- +
); diff --git a/frontend/src/statemanagement/chats/chatSlice.ts b/frontend/src/statemanagement/chats/chatSlice.ts index 15874f5..04a7864 100644 --- a/frontend/src/statemanagement/chats/chatSlice.ts +++ b/frontend/src/statemanagement/chats/chatSlice.ts @@ -1,21 +1,23 @@ +import { ChatMessage } from "@/types/chatmessage.type"; import { ChatRoom } from "@/types/chatroom.type"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; export interface ChatState { chats: ChatRoom[]; - chatsLoading: boolean; + chatsAreLoading: boolean; + chatIsLoading: boolean; messageSending: boolean; activeChatId?: string; } const initialState: ChatState = { chats: [], - chatsLoading: false, + chatsAreLoading: false, + chatIsLoading: false, messageSending: false, }; -export const getAllChats = createAsyncThunk("chats/getAll", async () => { +export const getAllChats = createAsyncThunk("chats/getAll", async (_, s) => { let token = window.sessionStorage.getItem("token"); - const response = await fetch( "https://" + window.envUrl + "/api/v1/chats/query/chats/", { @@ -29,9 +31,42 @@ export const getAllChats = createAsyncThunk("chats/getAll", async () => { } const data = await response.json(); console.log(data); + if (data.length > 0) { + s.dispatch(getChatById({ chatId: data[0].id })); + } + return data; }); +export const getChatById = createAsyncThunk( + "chats/getById", + async ({ chatId }: { chatId: string }, t) => { + let token = window.sessionStorage.getItem("token"); + + const activeChatId = (t.getState() as { chats: ChatState }).chats + .activeChatId; + if (activeChatId == chatId) { + return { messages: [], chatId }; + } + t.dispatch(setActiveChatId(chatId)); + + const response = await fetch( + "https://" + window.envUrl + `/api/v1/chats/query/chats/${chatId}`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + if (response.status > 299) { + throw new Error("Request failed with " + response.status); + } + const data = await response.json(); + console.log(data); + return { messages: data.messages, chatId }; + } +); + export const addMessage = createAsyncThunk( "chats/add", async (payload: { text?: string; imageBase64?: string }) => { @@ -68,18 +103,41 @@ export const chatSlice = createSlice({ addChatSync: (state, action) => { state.chats.unshift(action.payload); }, + setActiveChatId: (state, action: { payload: string }) => { + state.activeChatId = action.payload; + }, }, extraReducers: (builder) => { builder.addCase(getAllChats.pending, (state) => { - state.chatsLoading = true; + state.chatsAreLoading = true; }); builder.addCase(getAllChats.rejected, (state) => { - state.chatsLoading = false; + state.chatsAreLoading = false; }); builder.addCase(getAllChats.fulfilled, (state, action) => { - state.chatsLoading = false; + state.chatsAreLoading = false; state.chats = action.payload; }); + builder.addCase(getChatById.pending, (state) => { + state.chatIsLoading = true; + }); + builder.addCase(getChatById.rejected, (state) => { + state.chatIsLoading = false; + }); + builder.addCase( + getChatById.fulfilled, + ( + state, + action: { payload: { chatId: string; messages: ChatMessage[] } } + ) => { + state.chatIsLoading = false; + const chat = state.chats.find((x) => x.id == action.payload.chatId); + if (chat != null) { + const prevMessages = chat.messages ?? []; + chat.messages = [...prevMessages, ...action.payload.messages]; + } + } + ); builder.addCase(addMessage.pending, (state) => { state.messageSending = true; }); @@ -93,6 +151,6 @@ export const chatSlice = createSlice({ }, }); -export const { addChatSync } = chatSlice.actions; +export const { addChatSync, setActiveChatId } = chatSlice.actions; export default chatSlice.reducer; diff --git a/frontend/src/types/chatroom.type.ts b/frontend/src/types/chatroom.type.ts index 2cdedde..9dfcff1 100644 --- a/frontend/src/types/chatroom.type.ts +++ b/frontend/src/types/chatroom.type.ts @@ -3,6 +3,7 @@ import { ChatMessage } from "./chatmessage.type"; export interface ChatRoom { id: string; name: string; + users: string[]; messages?: ChatMessage[]; isLoading?: boolean; } From b1c4c39039bb6a76238cc210ac3aebaa132a1273 Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Fri, 6 Dec 2024 16:54:22 +0100 Subject: [PATCH 06/15] Refactor ChatWindow and ChatWindowWrapper: add user display name, enhance avatar handling, and improve message structure; update user fetching in Main component --- frontend/src/components/chats/ChatWindow.tsx | 27 ++++++++++----- .../components/chats/ChatWindow_wrapper.tsx | 34 ++++++++++++------- frontend/src/components/users/usersPanel.tsx | 9 +---- frontend/src/lib/utils.ts | 3 ++ frontend/src/pages/main.tsx | 3 ++ frontend/src/types/chatroom.type.ts | 2 +- 6 files changed, 49 insertions(+), 29 deletions(-) diff --git a/frontend/src/components/chats/ChatWindow.tsx b/frontend/src/components/chats/ChatWindow.tsx index 329a83f..1e683ac 100644 --- a/frontend/src/components/chats/ChatWindow.tsx +++ b/frontend/src/components/chats/ChatWindow.tsx @@ -1,6 +1,11 @@ -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +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 { useState } from "react"; import { v4 as uuidv4 } from "uuid"; @@ -12,7 +17,13 @@ interface Message { timestamp: string; } -export default function ChatWindow({ messages }: { messages: Message[] }) { +export default function ChatWindow({ + withUser, + messages, +}: { + withUser: User; + messages: Message[]; +}) { const [newMessage, setNewMessage] = useState(""); const handleSendMessage = () => { @@ -35,15 +46,15 @@ export default function ChatWindow({ messages }: { messages: Message[] }) {
- - AJ + + {chooseTwoCharsFromName(withUser.display_name)} +

- Alice Johnson + {withUser.display_name}

Online

diff --git a/frontend/src/components/chats/ChatWindow_wrapper.tsx b/frontend/src/components/chats/ChatWindow_wrapper.tsx index 7407a14..724a87c 100644 --- a/frontend/src/components/chats/ChatWindow_wrapper.tsx +++ b/frontend/src/components/chats/ChatWindow_wrapper.tsx @@ -2,20 +2,30 @@ import { useAppSelector } from "@/statemanagement/store"; import ChatWindow from "./ChatWindow"; export default function ChatWindowWrapper() { - const chatMessages = useAppSelector((state) => { + 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); - return ( - chat?.messages?.map((message) => { - return { - id: message.id, - content: message.text, - ownMessage: message.user_id === ownUser?.id, - timestamp: "", - }; - }) ?? null - ); + let otherUserId = chat?.user_ids.find((user) => user !== ownUser?.id); + let withUser = state.users.users.find((user) => user.id === otherUserId); + console.log(state.users); + return { + withUser, + chatMessages: + chat?.messages?.map((message) => { + return { + id: message.id, + content: message.text, + ownMessage: message.user_id === ownUser?.id, + timestamp: "", + }; + }) ?? null, + }; }); - return ; + return ( + + ); } diff --git a/frontend/src/components/users/usersPanel.tsx b/frontend/src/components/users/usersPanel.tsx index 21477d0..0b45b63 100644 --- a/frontend/src/components/users/usersPanel.tsx +++ b/frontend/src/components/users/usersPanel.tsx @@ -1,15 +1,8 @@ -import { useAppDispatch, useAppSelector } from "@/statemanagement/store"; -import { getAllUsers, getOwnUser } from "@/statemanagement/users/usersSlice"; -import { useEffect } from "react"; +import { useAppSelector } from "@/statemanagement/store"; import { SidePanel } from "../SidePanel/SidePanel"; export const UserPanel = () => { const userState = useAppSelector((state) => state.users); - const dispatch = useAppDispatch(); - useEffect(() => { - dispatch(getOwnUser()); - dispatch(getAllUsers()); - }, []); return ( <> diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 489425e..66fb80d 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -25,6 +25,9 @@ export function randomTailwindBackgroundColor(uniqueValue: string) { export function chooseTwoCharsFromName(name: string) { const [first, second] = name.split(" "); + if (!first || !second) { + return ""; + } if (second) { return (first[0] + second[0]).toUpperCase(); } else { diff --git a/frontend/src/pages/main.tsx b/frontend/src/pages/main.tsx index 88c2cb6..928d3f6 100644 --- a/frontend/src/pages/main.tsx +++ b/frontend/src/pages/main.tsx @@ -3,6 +3,7 @@ import { MainPage } from "@/pages/subpages/Main"; import { getAllChats } from "@/statemanagement/chats/chatSlice"; import { getAllPosts } from "@/statemanagement/posting/postSlice"; import { useAppDispatch } from "@/statemanagement/store"; +import { getAllUsers, getOwnUser } from "@/statemanagement/users/usersSlice"; import { WebsocketProvider } from "@/websocket/websocketProvider"; import { useEffect } from "react"; import { Route, Routes } from "react-router-dom"; @@ -13,6 +14,8 @@ export const Main = () => { useEffect(() => { dispatch(getAllPosts()); dispatch(getAllChats()); + dispatch(getOwnUser()); + dispatch(getAllUsers()); }, []); return ( diff --git a/frontend/src/types/chatroom.type.ts b/frontend/src/types/chatroom.type.ts index 9dfcff1..612c7ab 100644 --- a/frontend/src/types/chatroom.type.ts +++ b/frontend/src/types/chatroom.type.ts @@ -3,7 +3,7 @@ import { ChatMessage } from "./chatmessage.type"; export interface ChatRoom { id: string; name: string; - users: string[]; + user_ids: string[]; messages?: ChatMessage[]; isLoading?: boolean; } From feabc5c8170920301ce2efa23afef307916f448d Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Fri, 6 Dec 2024 16:55:43 +0100 Subject: [PATCH 07/15] Refactor ChatWindow: enhance avatar styling, update user status display, and improve name handling in utility function --- frontend/src/components/chats/ChatWindow.tsx | 7 +++++-- frontend/src/lib/utils.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/chats/ChatWindow.tsx b/frontend/src/components/chats/ChatWindow.tsx index 1e683ac..612f6c9 100644 --- a/frontend/src/components/chats/ChatWindow.tsx +++ b/frontend/src/components/chats/ChatWindow.tsx @@ -47,7 +47,10 @@ export default function ChatWindow({
{chooseTwoCharsFromName(withUser.display_name)} @@ -56,7 +59,7 @@ export default function ChatWindow({

{withUser.display_name}

-

Online

+

Unkown Status

diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 66fb80d..cd65c69 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -25,7 +25,7 @@ export function randomTailwindBackgroundColor(uniqueValue: string) { export function chooseTwoCharsFromName(name: string) { const [first, second] = name.split(" "); - if (!first || !second) { + if (!first) { return ""; } if (second) { From 446d5df271996bcf607a6e1bf08015a5d08ff1d5 Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Fri, 6 Dec 2024 17:11:02 +0100 Subject: [PATCH 08/15] Refactor chat components: update ChatList to use dispatch for fetching chat by ID, simplify ChatSidebar by removing setChatId prop, enhance ChatWindow to handle message sending, and implement unique array utility for message management --- frontend/src/components/chats/ChatList.tsx | 17 +++++++---- frontend/src/components/chats/ChatSidebar.tsx | 16 +---------- frontend/src/components/chats/ChatWindow.tsx | 17 ++++------- .../components/chats/ChatWindow_wrapper.tsx | 8 ++++-- .../src/statemanagement/chats/chatSlice.ts | 28 +++++++++++-------- frontend/src/utils/unique_array.ts | 7 +++++ 6 files changed, 47 insertions(+), 46 deletions(-) create mode 100644 frontend/src/utils/unique_array.ts diff --git a/frontend/src/components/chats/ChatList.tsx b/frontend/src/components/chats/ChatList.tsx index b5db1e6..61b20d0 100644 --- a/frontend/src/components/chats/ChatList.tsx +++ b/frontend/src/components/chats/ChatList.tsx @@ -1,11 +1,16 @@ -import { useAppSelector } from "@/statemanagement/store"; +import { getChatById } from "@/statemanagement/chats/chatSlice"; +import { useAppDispatch, useAppSelector } from "@/statemanagement/store"; +import { useCallback } from "react"; import ChatRooms from "./ChatItem"; -export default function ChatList({ - setChatId, -}: { - setChatId: (chatId: string) => void; -}) { +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) => { diff --git a/frontend/src/components/chats/ChatSidebar.tsx b/frontend/src/components/chats/ChatSidebar.tsx index fcf4084..cafb034 100644 --- a/frontend/src/components/chats/ChatSidebar.tsx +++ b/frontend/src/components/chats/ChatSidebar.tsx @@ -1,27 +1,13 @@ -import { - getChatById, - setActiveChatId, -} from "@/statemanagement/chats/chatSlice"; -import { useAppDispatch } from "@/statemanagement/store"; -import { useCallback } from "react"; import ChatList from "./ChatList"; export default function ChatSidebar() { - const dispatch = useAppDispatch(); - const setChatId = useCallback( - (chatId: string) => { - dispatch(setActiveChatId(chatId)); - dispatch(getChatById({ chatId })); - }, - [dispatch] - ); return (

Chats

- +
); diff --git a/frontend/src/components/chats/ChatWindow.tsx b/frontend/src/components/chats/ChatWindow.tsx index 612f6c9..93b303b 100644 --- a/frontend/src/components/chats/ChatWindow.tsx +++ b/frontend/src/components/chats/ChatWindow.tsx @@ -8,7 +8,6 @@ import { import { User } from "@/types/user.type"; import { Send } from "lucide-react"; import { useState } from "react"; -import { v4 as uuidv4 } from "uuid"; interface Message { id: string; @@ -20,24 +19,20 @@ interface Message { export default function ChatWindow({ withUser, messages, + onMessageSend, }: { withUser: User; messages: Message[]; + onMessageSend: (message: { text: string }) => void; }) { const [newMessage, setNewMessage] = useState(""); const handleSendMessage = () => { if (newMessage.trim()) { - const message: Message = { - id: uuidv4(), - content: newMessage, - ownMessage: true, - timestamp: new Date().toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - }), + const message = { + text: newMessage, }; - console.log(message); + onMessageSend(message); setNewMessage(""); } }; @@ -96,7 +91,7 @@ export default function ChatWindow({ placeholder="Type a message..." value={newMessage} onChange={(e) => setNewMessage(e.target.value)} - onKeyPress={(e) => e.key === "Enter" && handleSendMessage()} + onKeyUp={(e) => e.key === "Enter" && handleSendMessage()} className="flex-1" />

{lastMessage}

- {unread > 0 && ( - - {unread} - - )} + {unread > 0 && new}
); } diff --git a/frontend/src/components/chats/ChatList.tsx b/frontend/src/components/chats/ChatList.tsx index 61b20d0..dada916 100644 --- a/frontend/src/components/chats/ChatList.tsx +++ b/frontend/src/components/chats/ChatList.tsx @@ -22,7 +22,7 @@ export default function ChatList() { fallbackColorGen: chat.id, }, timestamp: "", - unread: 0, + unread: chat.unreadMessages ?? 0, }; }); }); diff --git a/frontend/src/statemanagement/chats/chatSlice.ts b/frontend/src/statemanagement/chats/chatSlice.ts index 63a2c84..86fd256 100644 --- a/frontend/src/statemanagement/chats/chatSlice.ts +++ b/frontend/src/statemanagement/chats/chatSlice.ts @@ -1,9 +1,9 @@ import { ChatMessage } from "@/types/chatmessage.type"; -import { ChatRoom } from "@/types/chatroom.type"; -import { unique } from "@/utils/unique_array"; +import { ChatRoom, ChatRoomWithNotifications } from "@/types/chatroom.type"; +import { createUniqueMessages } from "@/utils/unreadMessages"; import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; export interface ChatState { - chats: ChatRoom[]; + chats: ChatRoomWithNotifications[]; chatsAreLoading: boolean; chatIsLoading: boolean; messageSending: boolean; @@ -17,6 +17,21 @@ const initialState: ChatState = { messageSending: false, }; +const setMessagesForChat = ( + chat: ChatRoomWithNotifications, + activeChatId?: string, + messagesToAdd?: ChatMessage[] +) => { + const { messages, noNewMessages } = createUniqueMessages( + chat.messages, + messagesToAdd + ); + chat.messages = messages; + if (activeChatId != chat.id) { + chat.unreadMessages = noNewMessages; + } +}; + export const getAllChats = createAsyncThunk("chats/getAll", async (_, s) => { let token = window.sessionStorage.getItem("token"); const response = await fetch( @@ -30,8 +45,10 @@ export const getAllChats = createAsyncThunk("chats/getAll", async (_, s) => { if (response.status > 299) { throw new Error("Request failed with " + response.status); } - const data = await response.json(); - console.log(data); + const data = (await response.json()) as ChatRoomWithNotifications[]; + data.forEach((chat) => { + chat.unreadMessages = 0; + }); if (data.length > 0) { s.dispatch(getChatById({ chatId: data[0].id })); } @@ -103,16 +120,21 @@ export const chatSlice = createSlice({ reducers: { setActiveChatId: (state, action: { payload: string }) => { state.activeChatId = action.payload; + const chat = state.chats.find((x) => x.id == action.payload); + if (chat != null) { + chat.unreadMessages = 0; + } }, updateChatRoomMessagesSync: (state, action: { payload: ChatRoom }) => { const chatRoom = state.chats.find((x) => x.id == action.payload.id); if (chatRoom != null) { - chatRoom.messages = unique( - [...(chatRoom.messages ?? []), ...(action.payload.messages ?? [])], - (x) => x.id + setMessagesForChat( + chatRoom, + state.activeChatId, + action.payload.messages ); } else { - state.chats.push(action.payload); + state.chats.push({ ...action.payload, unreadMessages: 1 }); } }, }, @@ -142,11 +164,7 @@ export const chatSlice = createSlice({ state.chatIsLoading = false; const chat = state.chats.find((x) => x.id == action.payload.chatId); if (chat != null) { - const prevMessages = chat.messages ?? []; - chat.messages = unique( - [...prevMessages, ...action.payload.messages], - (x) => x.id - ); + setMessagesForChat(chat, state.activeChatId, action.payload.messages); } } ); diff --git a/frontend/src/types/chatroom.type.ts b/frontend/src/types/chatroom.type.ts index 612c7ab..4a7e721 100644 --- a/frontend/src/types/chatroom.type.ts +++ b/frontend/src/types/chatroom.type.ts @@ -7,3 +7,7 @@ export interface ChatRoom { messages?: ChatMessage[]; isLoading?: boolean; } + +export interface ChatRoomWithNotifications extends ChatRoom { + unreadMessages?: number; +} diff --git a/frontend/src/utils/unreadMessages.ts b/frontend/src/utils/unreadMessages.ts new file mode 100644 index 0000000..a01fead --- /dev/null +++ b/frontend/src/utils/unreadMessages.ts @@ -0,0 +1,14 @@ +import { ChatMessage } from "@/types/chatmessage.type"; +import { unique } from "./unique_array"; + +export const createUniqueMessages = ( + oldMessages?: ChatMessage[], + newMessages?: ChatMessage[] +) => { + const messages = unique( + [...(oldMessages ?? []), ...(newMessages ?? [])], + (x) => x.id + ); + const noNewMessages = messages.length - (oldMessages?.length ?? 0); + return { messages, noNewMessages }; +}; From 0d93133d26e3e25d47f201c263bbe9cff879d2cc Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Fri, 6 Dec 2024 18:38:22 +0100 Subject: [PATCH 11/15] Enhance header notifications: integrate Radix UI popover for notifications, update unread message handling, and add user avatar display in the header component --- frontend/package.json | 1 + .../components/chats/ChatWindow_wrapper.tsx | 6 +- frontend/src/components/header.tsx | 78 +++++++++++++++++++ frontend/src/components/header/header.tsx | 66 +++++++++++++++- frontend/src/components/ui/popover.tsx | 31 ++++++++ frontend/src/pages/main.tsx | 1 + .../src/statemanagement/chats/chatSlice.ts | 11 +-- frontend/yarn.lock | 39 +++++++++- 8 files changed, 223 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/header.tsx create mode 100644 frontend/src/components/ui/popover.tsx diff --git a/frontend/package.json b/frontend/package.json index 8d0a3da..450841b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@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", diff --git a/frontend/src/components/chats/ChatWindow_wrapper.tsx b/frontend/src/components/chats/ChatWindow_wrapper.tsx index 799a82f..3b2e82b 100644 --- a/frontend/src/components/chats/ChatWindow_wrapper.tsx +++ b/frontend/src/components/chats/ChatWindow_wrapper.tsx @@ -1,5 +1,6 @@ -import { addMessage } from "@/statemanagement/chats/chatSlice"; +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() { @@ -23,6 +24,9 @@ export default function ChatWindowWrapper() { }) ?? null, }; }); + useEffect(() => { + dispatch(setReadMessages()); + }, [chatMessages]); return ( +
+
+
+ + Logo + + +
+
+ + + + + +
+

Notifications

+
+
+ + + UD + +
+

New message from User123

+

Hey, how's it going?

+
+
+
+ + + SY + +
+

System Update

+

Your account has been upgraded.

+
+
+
+
+
+
+ + + JD + +
+
+
+
+ ) +} + diff --git a/frontend/src/components/header/header.tsx b/frontend/src/components/header/header.tsx index f6f68d5..5be7e5c 100644 --- a/frontend/src/components/header/header.tsx +++ b/frontend/src/components/header/header.tsx @@ -6,11 +6,23 @@ import { randomTailwindBackgroundColor, } from "@/lib/utils"; import { useAppSelector } from "@/statemanagement/store"; +import { AvatarImage } from "@radix-ui/react-avatar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@radix-ui/react-popover"; import { Bell, Home, Mail, Users } from "lucide-react"; import { Link } from "react-router-dom"; import { ModeToggle } from "../theming/themetoggle"; +import { Card, CardContent, CardHeader } from "../ui/card"; export const Header = () => { const user = useAppSelector((state) => state.users.ownUser); + const notificate = useAppSelector( + (state) => + state.chats.chats?.filter((chat) => chat.unreadMessages ?? 0 > 0).length > + 0 + ); return (
@@ -41,9 +53,57 @@ export const Header = () => {
- + + + + + + + +

Notifications

+
+ +
+
+
+ + + SY + +
+

+ System Info +

+

+ New messages ! +

+
+
+
+
+
+
+
+
{user && ( , + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/frontend/src/pages/main.tsx b/frontend/src/pages/main.tsx index c8fcda8..3cbfe3a 100644 --- a/frontend/src/pages/main.tsx +++ b/frontend/src/pages/main.tsx @@ -20,6 +20,7 @@ export const Main = () => { dispatch(getOwnUser()); dispatch(getAllUsers()); }, []); + return ( <>
diff --git a/frontend/src/statemanagement/chats/chatSlice.ts b/frontend/src/statemanagement/chats/chatSlice.ts index 86fd256..ec6ca52 100644 --- a/frontend/src/statemanagement/chats/chatSlice.ts +++ b/frontend/src/statemanagement/chats/chatSlice.ts @@ -27,9 +27,7 @@ const setMessagesForChat = ( messagesToAdd ); chat.messages = messages; - if (activeChatId != chat.id) { - chat.unreadMessages = noNewMessages; - } + chat.unreadMessages = noNewMessages; }; export const getAllChats = createAsyncThunk("chats/getAll", async (_, s) => { @@ -82,6 +80,7 @@ export const getChatById = createAsyncThunk( } const data = await response.json(); console.log(data); + return { messages: data.messages, chatId }; } ); @@ -120,7 +119,9 @@ export const chatSlice = createSlice({ reducers: { setActiveChatId: (state, action: { payload: string }) => { state.activeChatId = action.payload; - const chat = state.chats.find((x) => x.id == action.payload); + }, + setReadMessages: (state) => { + const chat = state.chats.find((x) => x.id == state.activeChatId); if (chat != null) { chat.unreadMessages = 0; } @@ -181,7 +182,7 @@ export const chatSlice = createSlice({ }, }); -export const { updateChatRoomMessagesSync, setActiveChatId } = +export const { updateChatRoomMessagesSync, setActiveChatId, setReadMessages } = chatSlice.actions; export default chatSlice.reducer; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 81ed81f..e3d068d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -381,6 +381,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13" integrity sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw== +"@radix-ui/react-focus-guards@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe" + integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg== + "@radix-ui/react-focus-scope@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2" @@ -433,6 +438,27 @@ aria-hidden "^1.1.1" react-remove-scroll "2.5.7" +"@radix-ui/react-popover@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.2.tgz#a0cab25f69aa49ad0077d91e9e9dcd323758020c" + integrity sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.1" + "@radix-ui/react-dismissable-layer" "1.1.1" + "@radix-ui/react-focus-guards" "1.1.1" + "@radix-ui/react-focus-scope" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.0" + "@radix-ui/react-portal" "1.1.2" + "@radix-ui/react-presence" "1.1.1" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + aria-hidden "^1.1.1" + react-remove-scroll "2.6.0" + "@radix-ui/react-popper@1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a" @@ -1909,7 +1935,7 @@ react-redux@^9.1.2: "@types/use-sync-external-store" "^0.0.3" use-sync-external-store "^1.0.0" -react-remove-scroll-bar@^2.3.4: +react-remove-scroll-bar@^2.3.4, react-remove-scroll-bar@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== @@ -1928,6 +1954,17 @@ react-remove-scroll@2.5.7: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-remove-scroll@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz#fb03a0845d7768a4f1519a99fdb84983b793dc07" + integrity sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ== + dependencies: + react-remove-scroll-bar "^2.3.6" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + react-router-dom@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-7.0.1.tgz#b1438100800313e1b4c48da0c5fdb498f81c7f96" From 354f6b36d4132eb7216d31753a7a186d7a2150ac Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Fri, 6 Dec 2024 18:39:43 +0100 Subject: [PATCH 12/15] Refactor header component: remove legacy header.tsx file and enhance notification styling in header.tsx --- frontend/src/components/header.tsx | 78 ----------------------- frontend/src/components/header/header.tsx | 2 +- 2 files changed, 1 insertion(+), 79 deletions(-) delete mode 100644 frontend/src/components/header.tsx diff --git a/frontend/src/components/header.tsx b/frontend/src/components/header.tsx deleted file mode 100644 index ba4f6e1..0000000 --- a/frontend/src/components/header.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import Link from "'next/link'" -import { Bell } from "'lucide-react'" -import { Button } from "@/components/ui/button" -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@/components/ui/popover" - -export default function Header() { - return ( -
-
-
-
- - Logo - - -
-
- - - - - -
-

Notifications

-
-
- - - UD - -
-

New message from User123

-

Hey, how's it going?

-
-
-
- - - SY - -
-

System Update

-

Your account has been upgraded.

-
-
-
-
-
-
- - - JD - -
-
-
-
- ) -} - diff --git a/frontend/src/components/header/header.tsx b/frontend/src/components/header/header.tsx index 5be7e5c..492d771 100644 --- a/frontend/src/components/header/header.tsx +++ b/frontend/src/components/header/header.tsx @@ -81,7 +81,7 @@ export const Header = () => {
-
+
Date: Fri, 6 Dec 2024 19:08:59 +0100 Subject: [PATCH 13/15] Enhance ChatWindow component: implement auto-scrolling for new messages and adjust layout for better visibility --- frontend/src/components/chats/ChatWindow.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/chats/ChatWindow.tsx b/frontend/src/components/chats/ChatWindow.tsx index 93b303b..4e70112 100644 --- a/frontend/src/components/chats/ChatWindow.tsx +++ b/frontend/src/components/chats/ChatWindow.tsx @@ -7,7 +7,7 @@ import { } from "@/lib/utils"; import { User } from "@/types/user.type"; import { Send } from "lucide-react"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; interface Message { id: string; @@ -26,6 +26,11 @@ export default function ChatWindow({ onMessageSend: (message: { text: string }) => void; }) { const [newMessage, setNewMessage] = useState(""); + const messagesEndRef = useRef(null); + useEffect(() => { + console.log(messagesEndRef.current); + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); const handleSendMessage = () => { if (newMessage.trim()) { @@ -57,7 +62,7 @@ export default function ChatWindow({

Unkown Status

-
+
{messages.map((message) => (
))} +
From ab82fddc2257afbace0f40096729e81385829d40 Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Fri, 6 Dec 2024 19:11:57 +0100 Subject: [PATCH 14/15] Fix Main component: add dispatch dependencies for getAllPosts and getAllChats in useEffect --- frontend/src/pages/main.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/main.tsx b/frontend/src/pages/main.tsx index 3cbfe3a..36148e0 100644 --- a/frontend/src/pages/main.tsx +++ b/frontend/src/pages/main.tsx @@ -15,11 +15,11 @@ export const Main = () => { usePostWebsocket(); useChatWebsocket(); useEffect(() => { - dispatch(getAllPosts()); - dispatch(getAllChats()); dispatch(getOwnUser()); dispatch(getAllUsers()); - }, []); + dispatch(getAllPosts()); + dispatch(getAllChats()); + }, [dispatch]); return ( <> From 6bdc73fb64e65f18502884ce5a8efa430d1b7c07 Mon Sep 17 00:00:00 2001 From: L4B0MB4 Date: Fri, 6 Dec 2024 19:13:56 +0100 Subject: [PATCH 15/15] Refactor chatSlice: remove unused activeChatId parameter from setMessagesForChat and update message handling --- frontend/src/statemanagement/chats/chatSlice.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/frontend/src/statemanagement/chats/chatSlice.ts b/frontend/src/statemanagement/chats/chatSlice.ts index ec6ca52..ee0ce02 100644 --- a/frontend/src/statemanagement/chats/chatSlice.ts +++ b/frontend/src/statemanagement/chats/chatSlice.ts @@ -19,7 +19,6 @@ const initialState: ChatState = { const setMessagesForChat = ( chat: ChatRoomWithNotifications, - activeChatId?: string, messagesToAdd?: ChatMessage[] ) => { const { messages, noNewMessages } = createUniqueMessages( @@ -129,11 +128,7 @@ export const chatSlice = createSlice({ updateChatRoomMessagesSync: (state, action: { payload: ChatRoom }) => { const chatRoom = state.chats.find((x) => x.id == action.payload.id); if (chatRoom != null) { - setMessagesForChat( - chatRoom, - state.activeChatId, - action.payload.messages - ); + setMessagesForChat(chatRoom, action.payload.messages); } else { state.chats.push({ ...action.payload, unreadMessages: 1 }); } @@ -165,7 +160,7 @@ export const chatSlice = createSlice({ state.chatIsLoading = false; const chat = state.chats.find((x) => x.id == action.payload.chatId); if (chat != null) { - setMessagesForChat(chat, state.activeChatId, action.payload.messages); + setMessagesForChat(chat, action.payload.messages); } } );