diff --git a/frontend/package.json b/frontend/package.json index 3e6ccb3..450841b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", 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..927f2d6 --- /dev/null +++ b/frontend/src/components/chats/ChatItem.tsx @@ -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 ( +
{ + onClick(id); + }} + > + + {avatar.image && } + + {chooseTwoCharsFromName(name)} + + +
+
+

+ {name} +

+

{timestamp}

+
+

{lastMessage}

+
+ {unread > 0 && new} +
+ ); +} diff --git a/frontend/src/components/chats/ChatList.tsx b/frontend/src/components/chats/ChatList.tsx new file mode 100644 index 0000000..dada916 --- /dev/null +++ b/frontend/src/components/chats/ChatList.tsx @@ -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 ( +
+ {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..cafb034 --- /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..4e70112 --- /dev/null +++ b/frontend/src/components/chats/ChatWindow.tsx @@ -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(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 ( +
+
+ + + {chooseTwoCharsFromName(withUser.display_name)} + + +
+

+ {withUser.display_name} +

+

Unkown Status

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

{message.content}

+

+ {message.timestamp} +

+
+
+ ))} +
+
+
+
+ setNewMessage(e.target.value)} + onKeyUp={(e) => e.key === "Enter" && handleSendMessage()} + className="flex-1" + /> + +
+
+
+ ); +} diff --git a/frontend/src/components/chats/ChatWindow_wrapper.tsx b/frontend/src/components/chats/ChatWindow_wrapper.tsx new file mode 100644 index 0000000..3b2e82b --- /dev/null +++ b/frontend/src/components/chats/ChatWindow_wrapper.tsx @@ -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 ( + { + dispatch(addMessage(message)); + }} + /> + ); +} diff --git a/frontend/src/components/header/header.tsx b/frontend/src/components/header/header.tsx index 707a6de..492d771 100644 --- a/frontend/src/components/header/header.tsx +++ b/frontend/src/components/header/header.tsx @@ -6,10 +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 (
@@ -18,27 +31,79 @@ export const Header = () => { PRYVT
- + + + + + + + +

Notifications

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

+ System Info +

+

+ New messages ! +

+
+
+
+
+
+
+
+
{user && ( { @@ -22,10 +20,6 @@ export const PostList = () => { }); return enahncedPosts; }); - const dispatch = useAppDispatch(); - useEffect(() => { - dispatch(getAllPosts()); - }, []); return ( <> 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/components/ui/popover.tsx b/frontend/src/components/ui/popover.tsx new file mode 100644 index 0000000..890b54f --- /dev/null +++ b/frontend/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverAnchor = PopoverPrimitive.Anchor + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 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..cd65c69 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) { + 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 2d16e17..36148e0 100644 --- a/frontend/src/pages/main.tsx +++ b/frontend/src/pages/main.tsx @@ -1,16 +1,37 @@ import { Header } from "@/components/header/header"; -import { SocialNetworkLayout } from "@/components/social-network-layout"; -import { WebsocketProvider } from "@/websocket/websocketProvider"; +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 { useChatWebsocket } from "@/websocket/chatWebsocket"; +import { usePostWebsocket } from "@/websocket/postWebsocket"; +import { useEffect } from "react"; +import { Route, Routes } from "react-router-dom"; +import { ChatsPage } from "./subpages/Chats"; export const Main = () => { + const dispatch = useAppDispatch(); + usePostWebsocket(); + useChatWebsocket(); + useEffect(() => { + dispatch(getOwnUser()); + dispatch(getAllUsers()); + dispatch(getAllPosts()); + dispatch(getAllChats()); + }, [dispatch]); + return ( - + <>
-
-
- +
+
+ + } /> + } /> +
- + ); }; diff --git a/frontend/src/pages/subpages/Chats.tsx b/frontend/src/pages/subpages/Chats.tsx new file mode 100644 index 0000000..bd7c3b1 --- /dev/null +++ b/frontend/src/pages/subpages/Chats.tsx @@ -0,0 +1,16 @@ +import ChatSidebar from "@/components/chats/ChatSidebar"; +import ChatWindowWrapper from "@/components/chats/ChatWindow_wrapper"; +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/src/statemanagement/chats/chatSlice.ts b/frontend/src/statemanagement/chats/chatSlice.ts new file mode 100644 index 0000000..ee0ce02 --- /dev/null +++ b/frontend/src/statemanagement/chats/chatSlice.ts @@ -0,0 +1,183 @@ +import { ChatMessage } from "@/types/chatmessage.type"; +import { ChatRoom, ChatRoomWithNotifications } from "@/types/chatroom.type"; +import { createUniqueMessages } from "@/utils/unreadMessages"; +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; +export interface ChatState { + chats: ChatRoomWithNotifications[]; + chatsAreLoading: boolean; + chatIsLoading: boolean; + messageSending: boolean; + activeChatId?: string; +} + +const initialState: ChatState = { + chats: [], + chatsAreLoading: false, + chatIsLoading: false, + messageSending: false, +}; + +const setMessagesForChat = ( + chat: ChatRoomWithNotifications, + messagesToAdd?: ChatMessage[] +) => { + const { messages, noNewMessages } = createUniqueMessages( + chat.messages, + messagesToAdd + ); + chat.messages = messages; + chat.unreadMessages = noNewMessages; +}; + +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/", + { + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + if (response.status > 299) { + throw new Error("Request failed with " + response.status); + } + const data = (await response.json()) as ChatRoomWithNotifications[]; + data.forEach((chat) => { + chat.unreadMessages = 0; + }); + if (data.length > 0) { + s.dispatch(getChatById({ chatId: data[0].id })); + } + + return data; +}); + +export const getChatById = createAsyncThunk( + "chats/getById", + async ({ chatId }: { chatId: string }, t) => { + console.log(chatId); + 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 }, t) => { + let token = window.sessionStorage.getItem("token"); + const activeChatId = (t.getState() as { chats: ChatState }).chats + .activeChatId; + const chat = { + text: payload.text, + }; + const response = await fetch( + "https://" + + window.envUrl + + `/api/v1/chats/command/chats/${activeChatId}/message`, + { + headers: { + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(chat), + method: "POST", + } + ); + if (response.status > 299) { + throw new Error("Request failed with " + response.status); + } + return chat; + } +); + +export const chatSlice = createSlice({ + name: "chats", + initialState, + reducers: { + setActiveChatId: (state, action: { payload: string }) => { + state.activeChatId = action.payload; + }, + setReadMessages: (state) => { + const chat = state.chats.find((x) => x.id == state.activeChatId); + 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) { + setMessagesForChat(chatRoom, action.payload.messages); + } else { + state.chats.push({ ...action.payload, unreadMessages: 1 }); + } + }, + }, + extraReducers: (builder) => { + builder.addCase(getAllChats.pending, (state) => { + state.chatsAreLoading = true; + }); + builder.addCase(getAllChats.rejected, (state) => { + state.chatsAreLoading = false; + }); + builder.addCase(getAllChats.fulfilled, (state, action) => { + 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) { + setMessagesForChat(chat, action.payload.messages); + } + } + ); + 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 { updateChatRoomMessagesSync, setActiveChatId, setReadMessages } = + 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..4a7e721 --- /dev/null +++ b/frontend/src/types/chatroom.type.ts @@ -0,0 +1,13 @@ +import { ChatMessage } from "./chatmessage.type"; + +export interface ChatRoom { + id: string; + name: string; + user_ids: string[]; + messages?: ChatMessage[]; + isLoading?: boolean; +} + +export interface ChatRoomWithNotifications extends ChatRoom { + unreadMessages?: number; +} diff --git a/frontend/src/utils/unique_array.ts b/frontend/src/utils/unique_array.ts new file mode 100644 index 0000000..a1eea2c --- /dev/null +++ b/frontend/src/utils/unique_array.ts @@ -0,0 +1,7 @@ +export const unique = (a: Array, idAccessor: (item: T) => string) => { + var seen = {} as { [key: string]: boolean }; + return a.filter(function (item) { + const id = idAccessor(item); + return seen.hasOwnProperty(id) ? false : (seen[id] = true); + }); +}; 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 }; +}; diff --git a/frontend/src/websocket/chatWebsocket.tsx b/frontend/src/websocket/chatWebsocket.tsx new file mode 100644 index 0000000..49a3358 --- /dev/null +++ b/frontend/src/websocket/chatWebsocket.tsx @@ -0,0 +1,17 @@ +import { updateChatRoomMessagesSync } from "@/statemanagement/chats/chatSlice"; +import { useAppDispatch } from "@/statemanagement/store"; +import { ChatRoom } from "@/types/chatroom.type"; +import { useCallback } from "react"; +import { useWebsocket } from "./websocketProvider"; + +export const useChatWebsocket = () => { + const dispatch = useAppDispatch(); + const onChat = useCallback( + (data: unknown) => { + dispatch(updateChatRoomMessagesSync(data as ChatRoom)); + }, + [dispatch] + ); + const url = `ws://${window.envUrl}/api/v1/chats/query/ws`; + useWebsocket(url, [{ eventName: "default", onEvent: onChat }]); +}; diff --git a/frontend/src/websocket/postWebsocket.tsx b/frontend/src/websocket/postWebsocket.tsx new file mode 100644 index 0000000..24329d6 --- /dev/null +++ b/frontend/src/websocket/postWebsocket.tsx @@ -0,0 +1,17 @@ +import { addPostSync } from "@/statemanagement/posting/postSlice"; +import { useAppDispatch } from "@/statemanagement/store"; +import { useCallback } from "react"; +import { useWebsocket } from "./websocketProvider"; + +export const usePostWebsocket = () => { + const dispatch = useAppDispatch(); + const onPost = useCallback( + (data: any) => { + console.log(data); + dispatch(addPostSync(data)); + }, + [dispatch] + ); + const url = `ws://${window.envUrl}/api/v1/posting/query/ws`; + useWebsocket(url, [{ eventName: "default", onEvent: onPost }]); +}; diff --git a/frontend/src/websocket/websocketProvider.tsx b/frontend/src/websocket/websocketProvider.tsx index 88f3d17..f0d01c2 100644 --- a/frontend/src/websocket/websocketProvider.tsx +++ b/frontend/src/websocket/websocketProvider.tsx @@ -1,45 +1,50 @@ -import { addPostSync } from "@/statemanagement/posting/postSlice"; -import { useAppDispatch, useAppSelector } from "@/statemanagement/store"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useAppSelector } from "@/statemanagement/store"; +import { useEffect } from "react"; -interface WebsocketProviderProps { - children: React.ReactNode; -} +export const useWebsocket = ( + url: string, + listenToEvents: { eventName: string; onEvent: (data: unknown) => void }[] +) => { + const token = useAppSelector((state) => state.authentication.token) || ""; -export const WebsocketProvider = ({ children }: WebsocketProviderProps) => { - const url = `ws://${window.envUrl}/api/v1/posting/query/ws`; - const token = useAppSelector((state) => state.authentication.token); - const dispatch = useAppDispatch(); - const [newWebSocket, setNewWebSocket] = useState(0); - const ws = useMemo(() => { - return new WebSocket(url); - }, [newWebSocket]); - - const onPost = useCallback( - (data: any) => { - console.log(data); - dispatch(addPostSync(data)); - }, - [ws, dispatch] - ); useEffect(() => { + websocketInitializer(url, token, listenToEvents); + }, [url, token]); +}; + +const websocketInitializer = ( + url: string, + token: string, + listenToEvents: { eventName: string; onEvent: (data: unknown) => void }[] +) => { + const initWebsocket = () => { + const ws = new WebSocket(url); + let isReconnecting = false; ws.onmessage = (event) => { - onPost(JSON.parse(event.data)); + listenToEvents.forEach((listenDef) => { + if (listenDef.eventName === "default") { + //todo add event name to the event + listenDef.onEvent(JSON.parse(event.data)); + } + }); }; ws.onopen = () => { ws.send(JSON.stringify({ token: token, data: "" })); }; + const reconnect = () => { + if (!isReconnecting) { + isReconnecting = true; + setTimeout(() => { + initWebsocket(); + }, 500); + } + }; ws.onclose = () => { - setTimeout(() => { - setNewWebSocket(newWebSocket + 1); - }, 500); + reconnect(); }; ws.onerror = () => { - setTimeout(() => { - setNewWebSocket(newWebSocket + 1); - }, 500); + reconnect(); }; - }, [ws, newWebSocket, onPost]); - - return <>{children}; + }; + initWebsocket(); }; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8b89c8b..e3d068d 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" @@ -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"