@@ -17,5 +13,9 @@ export function Status({ className, textStatus, online, classNameTextContainer,
);
}
- return online &&
;
+ return (
+ online && (
+
+ )
+ );
}
diff --git a/frontend/src/components/ThemeToggle/ThemeToggle.tsx b/frontend/src/components/ThemeToggle/ThemeToggle.tsx
index 24f40440..b66da984 100644
--- a/frontend/src/components/ThemeToggle/ThemeToggle.tsx
+++ b/frontend/src/components/ThemeToggle/ThemeToggle.tsx
@@ -1,32 +1,33 @@
"use client";
-import "@theme-toggles/react/css/Expand.css";
-
-import dynamic from "next/dynamic";
import { useTheme } from "next-themes";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
+import { DarkModeSwitch } from "react-toggle-dark-mode";
import { DARK_THEME, LIGHT_THEME } from "@/components/ThemeToggle/themeToggle.config";
-const Expand = dynamic(() => import("@theme-toggles/react").then(module => module.Expand), { ssr: false });
-
export function ThemeToggle() {
const { setTheme, resolvedTheme } = useTheme();
const [isLightTheme, setIsLightTheme] = useState(LIGHT_THEME === resolvedTheme);
+ useEffect(() => {
+ setIsLightTheme(LIGHT_THEME === resolvedTheme);
+ }, [resolvedTheme]);
+
const handleToggleTheme = () => {
+ const newTheme = isLightTheme ? DARK_THEME : LIGHT_THEME;
+ setTheme(newTheme);
setIsLightTheme(!isLightTheme);
- setTheme(isLightTheme ? DARK_THEME : LIGHT_THEME);
};
return (
-
);
}
diff --git a/frontend/src/constants/colors.ts b/frontend/src/constants/colors.ts
index ba05102f..cb4ffb0a 100644
--- a/frontend/src/constants/colors.ts
+++ b/frontend/src/constants/colors.ts
@@ -33,6 +33,7 @@ export const COLORS = {
red: {
50: "#ff5555",
100: "#ff4444",
+ 200: "#992E2E",
},
green: "#4DC886",
black: "#000",
diff --git a/frontend/src/context/SocketContext/SocketProvider/SocketProvider.tsx b/frontend/src/context/SocketContext/SocketProvider/SocketProvider.tsx
index 814a88ee..75cb1920 100644
--- a/frontend/src/context/SocketContext/SocketProvider/SocketProvider.tsx
+++ b/frontend/src/context/SocketContext/SocketProvider/SocketProvider.tsx
@@ -4,19 +4,22 @@ import { useRouter } from "next/navigation";
import { PropsWithChildren, useEffect, useMemo, useState } from "react";
import { refreshToken } from "@/api/http/auth/auth";
+import { AuthTypes } from "@/api/http/auth/auth.types";
import { connectToSocket } from "@/api/socket";
import { SocketContext } from "@/context/SocketContext/SocketContext";
-import { MessageContextParams, SocketContextProps } from "@/context/SocketContext/socketContext.types";
-import { RECONNECT_TIMEOUT } from "@/context/SocketContext/SocketProvider/socketProvider.config";
-import { setAccessToken } from "@/lib/features/user/userSlice";
+import { SocketContextProps, SocketMessageType } from "@/context/SocketContext/socketContext.types";
+import { isTokenExpired } from "@/helpers/DecodeToken/decodeToken";
import { useAppDispatch, useAppSelector } from "@/lib/hooks";
+const MAX_RECONNECT_ATTEMPTS = 3;
+
export function SocketProvider({ children }: Readonly
) {
const dispatch = useAppDispatch();
const { accessToken } = useAppSelector(state => state.user);
const route = useRouter();
const [webSocket, setWebSocket] = useState(null);
- const [message, setMessage] = useState();
+ const [socketMessage, setSocketMessage] = useState();
+ const [reconnectAttempts, setReconnectAttempts] = useState(0);
useEffect(() => {
if (!accessToken) {
@@ -26,15 +29,23 @@ export function SocketProvider({ children }: Readonly) {
const newSocket = connectToSocket(accessToken);
- newSocket.onclose = () => {
- setTimeout(async () => {
+ newSocket.onclose = async () => {
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
try {
- const data = await refreshToken();
- dispatch(setAccessToken(data.accessToken));
+ let data: AuthTypes = { accessToken, refreshExpiration: 0 };
+ if (isTokenExpired(accessToken)) {
+ data = await refreshToken();
+ }
+ const newWebSocket = connectToSocket(data.accessToken);
+ setWebSocket(newWebSocket);
+ setReconnectAttempts(prevAttempts => prevAttempts + 1);
} catch (error) {
- setWebSocket(null);
+ console.error("Failed to refresh token:", error);
+ route.push("/sign-in");
}
- }, RECONNECT_TIMEOUT);
+ } else {
+ route.push("/sign-in");
+ }
};
newSocket.onerror = () => {
@@ -47,7 +58,7 @@ export function SocketProvider({ children }: Readonly) {
};
newSocket.onmessage = event => {
- setMessage(JSON.parse(event.data));
+ setSocketMessage(JSON.parse(event.data));
};
setWebSocket(newSocket);
@@ -57,14 +68,14 @@ export function SocketProvider({ children }: Readonly) {
newSocket.close();
}
};
- }, [accessToken, dispatch, route]);
+ }, [accessToken, dispatch, route, reconnectAttempts]);
const value = useMemo(() => {
return {
webSocket,
- message,
+ socketMessage,
};
- }, [message, webSocket]);
+ }, [socketMessage, webSocket]);
return {children};
}
diff --git a/frontend/src/context/SocketContext/socketContext.types.ts b/frontend/src/context/SocketContext/socketContext.types.ts
index 3a6f9235..ebcbd735 100644
--- a/frontend/src/context/SocketContext/socketContext.types.ts
+++ b/frontend/src/context/SocketContext/socketContext.types.ts
@@ -1,15 +1,42 @@
export enum MessageAction {
ONLINE = "ONLINE",
OFFLINE = "OFFLINE",
+ BIND = "BIND",
+ MESSAGE = "MESSAGE",
+ UNREAD_MESSAGES = "UNREAD_MESSAGES",
}
-export type MessageContextParams = {
+export type MessageActionParams = {
action: MessageAction;
+};
+
+export type OnlineOfflineMessage = {
senderId: number;
timestamp: string;
-};
+} & MessageActionParams;
+
+export type BindMessage = {
+ chatId: string;
+ tmpMessageId: string;
+ messageId: string;
+} & MessageActionParams;
+
+export type MessageLikeMessage = {
+ timestamp: number;
+ id: string;
+ chatId: string;
+ senderId: string;
+ text: string;
+} & MessageActionParams;
+
+export type UnreadMessagesMessage = {
+ timestamp: number;
+ chats: Record;
+} & MessageActionParams;
+
+export type SocketMessageType = OnlineOfflineMessage | BindMessage | MessageLikeMessage | UnreadMessagesMessage;
export type SocketContextProps = {
webSocket?: WebSocket | null;
- message?: MessageContextParams;
+ socketMessage?: SocketMessageType;
};
diff --git a/frontend/src/context/SoundContext/SoundContext.tsx b/frontend/src/context/SoundContext/SoundContext.tsx
new file mode 100644
index 00000000..a5936acb
--- /dev/null
+++ b/frontend/src/context/SoundContext/SoundContext.tsx
@@ -0,0 +1,13 @@
+import { createContext } from "react";
+
+export type SoundContextProps = {
+ messageSound: () => void;
+};
+
+const messageSound = () => {
+ new Audio("/sound/message.mp3").play();
+};
+
+const SoundContext = createContext({ messageSound });
+
+export { SoundContext };
diff --git a/frontend/src/context/SoundContext/SoundProvider.tsx b/frontend/src/context/SoundContext/SoundProvider.tsx
new file mode 100644
index 00000000..816e66b5
--- /dev/null
+++ b/frontend/src/context/SoundContext/SoundProvider.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+import { PropsWithChildren } from "react";
+
+import { SoundContext } from "@/context/SoundContext/SoundContext";
+
+export function SocketProvider({ children }: Readonly) {
+ const messageSound = () => {
+ new Audio("/sound/message.mp3").play();
+ };
+ const value = { messageSound };
+
+ return {children};
+}
diff --git a/frontend/src/helpers/DecodeToken/decodeToken.ts b/frontend/src/helpers/DecodeToken/decodeToken.ts
index e4322807..481d67be 100644
--- a/frontend/src/helpers/DecodeToken/decodeToken.ts
+++ b/frontend/src/helpers/DecodeToken/decodeToken.ts
@@ -9,3 +9,10 @@ export const decodeToken = (token: string) => {
return null;
}
};
+
+export const isTokenExpired = (token: string) => {
+ const decodedToken = decodeToken(token);
+ if (!decodedToken) return true;
+
+ return Date.now() >= decodedToken.exp * 1000;
+};
diff --git a/frontend/src/helpers/FormatDate/formatDate.types.ts b/frontend/src/helpers/FormatDate/formatDate.types.ts
index 4b7e8178..0d60dbc2 100644
--- a/frontend/src/helpers/FormatDate/formatDate.types.ts
+++ b/frontend/src/helpers/FormatDate/formatDate.types.ts
@@ -1,4 +1,5 @@
export enum DateFormat {
HOURS_AND_MINUTES = "HH:mm",
EUROPEAN = "dd.MM.yyyy",
+ MONTH_AND_DAY = "MMMM d",
}
diff --git a/frontend/src/helpers/formatMessage.ts b/frontend/src/helpers/formatSystemAlertMessage.ts
similarity index 68%
rename from frontend/src/helpers/formatMessage.ts
rename to frontend/src/helpers/formatSystemAlertMessage.ts
index 86fe9127..83c4f2ae 100644
--- a/frontend/src/helpers/formatMessage.ts
+++ b/frontend/src/helpers/formatSystemAlertMessage.ts
@@ -1,4 +1,4 @@
-export const formatMessage = (input: string | undefined): string | undefined => {
+export const formatSystemAlertMessage = (input: string | undefined): string | undefined => {
if (!input) {
return input;
}
diff --git a/frontend/src/hooks/useAccessTokenEffect.ts b/frontend/src/hooks/useAccessTokenEffect.ts
index 6d9553e2..b3117584 100644
--- a/frontend/src/hooks/useAccessTokenEffect.ts
+++ b/frontend/src/hooks/useAccessTokenEffect.ts
@@ -1,5 +1,5 @@
import { useRouter } from "next/navigation";
-import { DependencyList, EffectCallback, useEffect } from "react";
+import { DependencyList, EffectCallback, useEffect, useLayoutEffect } from "react";
import { useAppSelector } from "@/lib/hooks";
@@ -15,3 +15,16 @@ export function useAccessTokenEffect(callback: EffectCallback, dependencies: Dep
callback();
}, [accessToken, router, ...dependencies]);
}
+
+export function useAccessTokenLayoutEffect(callback: EffectCallback, dependencies: DependencyList) {
+ const { accessToken } = useAppSelector(state => state.user);
+ const router = useRouter();
+
+ useLayoutEffect(() => {
+ if (!accessToken) {
+ router.push("/sign-in");
+ return;
+ }
+ callback();
+ }, [accessToken, router, ...dependencies]);
+}
diff --git a/frontend/src/lib/features/user/userSlice.types.ts b/frontend/src/lib/features/user/userSlice.types.ts
index fca5c362..9c6834da 100644
--- a/frontend/src/lib/features/user/userSlice.types.ts
+++ b/frontend/src/lib/features/user/userSlice.types.ts
@@ -7,13 +7,13 @@ export enum UserStatus {
OFFLINE = "Disconnected",
}
-export type UserStateParams = {
- accessToken: string | null;
- currentUser: UserParams | null;
-};
-
export type TokenParams = {
"token-id": string;
"user-id": string;
authorities: string[];
} & JwtPayload;
+
+export type UserStateParams = {
+ accessToken: string | null;
+ currentUser: UserParams | null;
+};