From 0c4fae5cb24b4b706db4aec6b3fdf4286f433363 Mon Sep 17 00:00:00 2001 From: JRB958 <141108231+JRB958@users.noreply.github.com> Date: Fri, 28 Feb 2025 02:13:08 -0500 Subject: [PATCH 01/28] GH-365: [feat] UI template for Gifted Chat library, might have to change later due to incompatibility with react native expo --- ClientApp/app/(tabs)/chats/index.tsx | 16 ++--- ClientApp/app/_layout.tsx | 31 +++++----- ClientApp/app/messaging/[id].tsx | 52 ++++++++++++++++ ClientApp/components/chat/ChatCard.tsx | 79 ++++++++++++++++++++++++ ClientApp/components/chat/ChatList.tsx | 83 ++++++++++++++++++++++++++ ClientApp/package.json | 1 + 6 files changed, 237 insertions(+), 25 deletions(-) create mode 100644 ClientApp/app/messaging/[id].tsx create mode 100644 ClientApp/components/chat/ChatCard.tsx create mode 100644 ClientApp/components/chat/ChatList.tsx diff --git a/ClientApp/app/(tabs)/chats/index.tsx b/ClientApp/app/(tabs)/chats/index.tsx index 75e92fb25..9f5b50cf9 100644 --- a/ClientApp/app/(tabs)/chats/index.tsx +++ b/ClientApp/app/(tabs)/chats/index.tsx @@ -1,15 +1,9 @@ import React from 'react'; -import { SafeAreaView, StyleSheet, View, Text } from 'react-native'; - - -const Chats = () => { - - - return ( - - Chats - - ); +import ChatList from '@/components/chat/ChatList'; +const Chats =() => { + return ( + + ) } export default Chats \ No newline at end of file diff --git a/ClientApp/app/_layout.tsx b/ClientApp/app/_layout.tsx index 4f229db69..305e83ec6 100644 --- a/ClientApp/app/_layout.tsx +++ b/ClientApp/app/_layout.tsx @@ -1,6 +1,7 @@ import { Stack } from 'expo-router'; import { Provider } from 'react-redux'; import { store } from '@/state/store'; +import "../global.css"; import { setupAxiosInstance } from '@/services/axiosInstance'; import { NotificationProvider } from "@/context/NotificationContext"; import * as Notifications from "expo-notifications" @@ -23,20 +24,22 @@ export default function RootLayout() { return ( - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ); } diff --git a/ClientApp/app/messaging/[id].tsx b/ClientApp/app/messaging/[id].tsx new file mode 100644 index 000000000..f16b89aae --- /dev/null +++ b/ClientApp/app/messaging/[id].tsx @@ -0,0 +1,52 @@ +import { useLocalSearchParams, useRouter } from 'expo-router'; +import React, { useCallback, useEffect, useState } from 'react'; +import { StyleSheet } from 'react-native'; +import { GiftedChat, IMessage } from 'react-native-gifted-chat'; + +const ChatScreen = () => { + const { id } = useLocalSearchParams(); + const router = useRouter(); + const [messages, setMessages] = useState([]); + + useEffect(() => { + setMessages([ + { + _id: 1, + text: 'Hello developer', + createdAt: new Date(), + user: { + _id: 2, + name: 'React Native', + }, + }, + ]); + }, []); + + const onSend = useCallback((newMessages: IMessage[] = []) => { + if (!Array.isArray(newMessages)) return; + setMessages(previousMessages => GiftedChat.append(previousMessages, newMessages)); + }, []); + + return ( + + onSend(messages)} + user={{ _id: 1 }} + /> + ); +}; + +export default ChatScreen; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#fff', + }, + title: { + fontSize: 20, + fontWeight: 'bold', + textAlign: 'center', + }, +}); diff --git a/ClientApp/components/chat/ChatCard.tsx b/ClientApp/components/chat/ChatCard.tsx new file mode 100644 index 000000000..7d112c9ea --- /dev/null +++ b/ClientApp/components/chat/ChatCard.tsx @@ -0,0 +1,79 @@ +import { mvs } from '@/utils/helpers/uiScaler'; +import { useRouter } from 'expo-router'; +import React from 'react'; +import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native'; + +interface CardProps { + chatId: string; + userName: string; + userImg: any; + messageTime: string; + messageText: string; +} + +const ChatCard: React.FC = ({ chatId, userName, userImg, messageTime, messageText }) => { + const router = useRouter(); + + return ( + router.push(`/messaging/${chatId}`)} +> + + + + + + + {userName} + {messageTime} + + {messageText} + + + + ); +}; + +export default ChatCard; + +const styles = StyleSheet.create({ + card: { + width: '100%', + padding: 10, + borderBottomWidth: 1, + borderBottomColor: '#ccc', + }, + userInfo: { + flexDirection: 'row', + alignItems: 'center', + }, + userImgWrapper: { + paddingVertical: 10, + }, + userImg: { + width: mvs(60), + height: mvs(60), + borderRadius: 25, + }, + textSection: { + flex: 1, + marginLeft: 10, + }, + userInfoText: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + userName: { + fontSize: 14, + fontWeight: 'bold', + }, + postTime: { + fontSize: 12, + color: '#666', + }, + messageText: { + fontSize: 14, + color: '#333', + }, +}); diff --git a/ClientApp/components/chat/ChatList.tsx b/ClientApp/components/chat/ChatList.tsx new file mode 100644 index 000000000..48270930a --- /dev/null +++ b/ClientApp/components/chat/ChatList.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import ChatCard from '@/components/chat/ChatCard'; +import { FlatList } from 'react-native'; +const Chats =() => { + + interface CardProps { + chatId: string; + userName: string; + userImg: any; + messageTime: string; + messageText: string; + } + + const chatData: CardProps[] = [ + { + chatId: '1', + userName: 'Alice Smith', + userImg: require('@/assets/images/avatar-placeholder.png'), + messageTime: '2 mins ago', + messageText: 'Hey Alice! How have you been?', + }, + { + chatId: '2', + userName: 'Bob Johnson', + userImg: require('@/assets/images/avatar-placeholder.png'), + messageTime: '10 mins ago', + messageText: 'Are we still on for tonight?', + }, + { + chatId: '3', + userName: 'Charlie Davis', + userImg: require('@/assets/images/avatar-placeholder.png'), + messageTime: '30 mins ago', + messageText: 'That was a great game yesterday!', + }, + { + chatId: '4', + userName: 'Diana Prince', + userImg: require('@/assets/images/avatar-placeholder.png'), + messageTime: '1 hour ago', + messageText: 'Let’s catch up soon!', + }, + { + chatId: '5', + userName: 'Edward Norton', + userImg: require('@/assets/images/avatar-placeholder.png'), + messageTime: '3 hours ago', + messageText: 'Do you have the notes from the meeting?', + }, + { + chatId: '6', + userName: 'Fiona Gallagher', + userImg: require('@/assets/images/avatar-placeholder.png'), + messageTime: 'Yesterday', + messageText: 'Hope you had a great weekend!', + }, + { + chatId: '7', + userName: 'George Michael', + userImg: require('@/assets/images/avatar-placeholder.png'), + messageTime: '2 days ago', + messageText: 'Thanks for the recommendation!', + }, + ]; + + return ( + item.chatId} + renderItem={({ item }) => ( + + )} + /> + ) +} + +export default Chats \ No newline at end of file diff --git a/ClientApp/package.json b/ClientApp/package.json index fdcdb1e32..95a3ce197 100644 --- a/ClientApp/package.json +++ b/ClientApp/package.json @@ -103,6 +103,7 @@ "react-native-elements": "^3.4.3", "react-native-gesture-handler": "~2.20.2", "react-native-get-random-values": "^1.11.0", + "react-native-gifted-chat": "^2.6.5", "react-native-google-places-autocomplete": "^2.5.7", "react-native-image-viewing": "^0.2.2", "react-native-keyboard-aware-scroll-view": "^0.9.5", From 0d27e17b35b5de131ff656301ff1a3a5e460edc6 Mon Sep 17 00:00:00 2001 From: JRB958 <141108231+JRB958@users.noreply.github.com> Date: Fri, 28 Feb 2025 19:06:59 -0500 Subject: [PATCH 02/28] add the library back to package.json --- ClientApp/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ClientApp/package.json b/ClientApp/package.json index 95a3ce197..4acf0d297 100644 --- a/ClientApp/package.json +++ b/ClientApp/package.json @@ -107,7 +107,7 @@ "react-native-google-places-autocomplete": "^2.5.7", "react-native-image-viewing": "^0.2.2", "react-native-keyboard-aware-scroll-view": "^0.9.5", - "react-native-maps": "1.18.0", + "react-native-maps": "1.20.1", "react-native-mime-types": "^2.5.0", "react-native-modal-datetime-picker": "^18.0.0", "react-native-pager-view": "6.5.1", From e2011b43c7117633d84c4a66ef81f16abc0fbbb1 Mon Sep 17 00:00:00 2001 From: DanDuguay Date: Wed, 5 Mar 2025 20:42:33 -0500 Subject: [PATCH 03/28] GH-365: [Feat] Initial commit --- ClientApp/components/chat/ChatList.tsx | 104 +++++++++++++++---------- ClientApp/services/chatService.ts | 15 ++++ ClientApp/utils/api/endpoints.tsx | 3 + 3 files changed, 81 insertions(+), 41 deletions(-) create mode 100644 ClientApp/services/chatService.ts diff --git a/ClientApp/components/chat/ChatList.tsx b/ClientApp/components/chat/ChatList.tsx index 48270930a..c861239ae 100644 --- a/ClientApp/components/chat/ChatList.tsx +++ b/ClientApp/components/chat/ChatList.tsx @@ -1,79 +1,101 @@ -import React from 'react'; +import React, {useEffect, useState} from 'react'; import ChatCard from '@/components/chat/ChatCard'; -import { FlatList } from 'react-native'; +import {Alert, FlatList} from 'react-native'; +import {useSelector} from "react-redux"; +import {getChatrooms} from "@/services/chatService"; +import {User} from "react-native-gifted-chat"; + const Chats =() => { + const [chatrooms, setChatrooms] = useState([]); + const [user, setUser] = useState(null); interface CardProps { - chatId: string; - userName: string; + chatroomId: string; + userId: string; userImg: any; - messageTime: string; - messageText: string; + createdAt: string; + content: string; + } + + const fetchChatrooms = async () => { + try { + const user = useSelector((state: {user: any }) => state.user); + setUser(user); + const chatroomData = await getChatrooms(user.id); + setChatrooms(chatroomData); + } catch (error) { + Alert.alert("Error", "Failed to fetch chatrooms."); + } } + + useEffect(() => { + fetchChatrooms(); + }) + const chatData: CardProps[] = [ { - chatId: '1', - userName: 'Alice Smith', + chatroomId: '1', + userId: 'Alice Smith', userImg: require('@/assets/images/avatar-placeholder.png'), - messageTime: '2 mins ago', - messageText: 'Hey Alice! How have you been?', + createdAt: '2 mins ago', + content: 'Hey Alice! How have you been?', }, { - chatId: '2', - userName: 'Bob Johnson', + chatroomId: '2', + userId: 'Bob Johnson', userImg: require('@/assets/images/avatar-placeholder.png'), - messageTime: '10 mins ago', - messageText: 'Are we still on for tonight?', + createdAt: '10 mins ago', + content: 'Are we still on for tonight?', }, { - chatId: '3', - userName: 'Charlie Davis', + chatroomId: '3', + userId: 'Charlie Davis', userImg: require('@/assets/images/avatar-placeholder.png'), - messageTime: '30 mins ago', - messageText: 'That was a great game yesterday!', + createdAt: '30 mins ago', + content: 'That was a great game yesterday!', }, { - chatId: '4', - userName: 'Diana Prince', + chatroomId: '4', + userId: 'Diana Prince', userImg: require('@/assets/images/avatar-placeholder.png'), - messageTime: '1 hour ago', - messageText: 'Let’s catch up soon!', + createdAt: '1 hour ago', + content: 'Let’s catch up soon!', }, { - chatId: '5', - userName: 'Edward Norton', + chatroomId: '5', + userId: 'Edward Norton', userImg: require('@/assets/images/avatar-placeholder.png'), - messageTime: '3 hours ago', - messageText: 'Do you have the notes from the meeting?', + createdAt: '3 hours ago', + content: 'Do you have the notes from the meeting?', }, { - chatId: '6', - userName: 'Fiona Gallagher', + chatroomId: '6', + userId: 'Fiona Gallagher', userImg: require('@/assets/images/avatar-placeholder.png'), - messageTime: 'Yesterday', - messageText: 'Hope you had a great weekend!', + createdAt: 'Yesterday', + content: 'Hope you had a great weekend!', }, { - chatId: '7', - userName: 'George Michael', + chatroomId: '7', + userId: 'George Michael', userImg: require('@/assets/images/avatar-placeholder.png'), - messageTime: '2 days ago', - messageText: 'Thanks for the recommendation!', + createdAt: '2 days ago', + content: 'Thanks for the recommendation!', }, ]; return ( item.chatId} + data={chatrooms} + keyExtractor={(item) => item.chatroomId} renderItem={({ item }) => ( )} /> diff --git a/ClientApp/services/chatService.ts b/ClientApp/services/chatService.ts new file mode 100644 index 000000000..4ca5931b2 --- /dev/null +++ b/ClientApp/services/chatService.ts @@ -0,0 +1,15 @@ +import { getAxiosInstance } from '@/services/axiosInstance'; +import { API_ENDPOINTS } from '@/utils/api/endpoints'; +// API_ENDPOINTS.GET_CHATROOMS + +export const getChatrooms = async (userId: string) => { + try { + const axiosInstance = await getAxiosInstance(); + const response = await axiosInstance.get( + API_ENDPOINTS.GET_CHATROOMS.replace("{userId}", userId)); + return response.data; + } catch (error) { + console.error('Error fetching chatrooms:', error); + throw error; + } +}; diff --git a/ClientApp/utils/api/endpoints.tsx b/ClientApp/utils/api/endpoints.tsx index d104a0f91..45a191cc0 100644 --- a/ClientApp/utils/api/endpoints.tsx +++ b/ClientApp/utils/api/endpoints.tsx @@ -31,6 +31,7 @@ export const API_ENDPOINTS = { GET_ALL_FRIENDS: 'user-service/user/{userId}/friends', RESPOND_TO_FRIEND_REQUEST: 'user-service/user/{userId}/friend-requests/{requestId}', GET_PROFILE_BY_ID: 'user-service/user/{userId}/profile', + GET_PUBLIC_PROFILE: 'user-service/user/{userId}/profile', GET_ALL_POSTS: "event-service/event/{eventId}/social/post", @@ -38,4 +39,6 @@ export const API_ENDPOINTS = { UPLOAD_FILE: "storage-service/objects/upload", GET_FILE: "storage-service/objects/file{objectPath}", + + GET_CHATROOMS: 'messaging-service/messaging/chatrooms/{userId}', }; From 083528759528533162300bb61e1c02fc69c6f286 Mon Sep 17 00:00:00 2001 From: DanDuguay Date: Mon, 24 Mar 2025 11:23:38 -0400 Subject: [PATCH 04/28] GH-365: [Feat] Connecting to the websocket with authorization and subscribing to a specific topic working --- ClientApp/app/_layout.tsx | 2 + ClientApp/app/messaging/[id].tsx | 128 +++++++++++++++++-- ClientApp/components/chat/ChatList.tsx | 79 ++++++------ ClientApp/package.json | 3 + ClientApp/services/axiosLocalInstance.ts | 155 +++++++++++++++++++++++ ClientApp/services/chatService.ts | 33 ++++- ClientApp/services/tokenService.ts | 4 +- ClientApp/utils/api/endpoints.tsx | 1 + 8 files changed, 349 insertions(+), 56 deletions(-) create mode 100644 ClientApp/services/axiosLocalInstance.ts diff --git a/ClientApp/app/_layout.tsx b/ClientApp/app/_layout.tsx index 305e83ec6..6ad88feb7 100644 --- a/ClientApp/app/_layout.tsx +++ b/ClientApp/app/_layout.tsx @@ -3,6 +3,7 @@ import { Provider } from 'react-redux'; import { store } from '@/state/store'; import "../global.css"; import { setupAxiosInstance } from '@/services/axiosInstance'; +import { setupLocalAxiosInstance } from "@/services/axiosLocalInstance"; import { NotificationProvider } from "@/context/NotificationContext"; import * as Notifications from "expo-notifications" import 'react-native-get-random-values'; @@ -21,6 +22,7 @@ export default function RootLayout() { // Inject Redux dispatch into Axios const { dispatch } = store; setupAxiosInstance(dispatch); + setupLocalAxiosInstance(dispatch); return ( diff --git a/ClientApp/app/messaging/[id].tsx b/ClientApp/app/messaging/[id].tsx index f16b89aae..5b07b0fb5 100644 --- a/ClientApp/app/messaging/[id].tsx +++ b/ClientApp/app/messaging/[id].tsx @@ -1,31 +1,131 @@ import { useLocalSearchParams, useRouter } from 'expo-router'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import { StyleSheet } from 'react-native'; import { GiftedChat, IMessage } from 'react-native-gifted-chat'; +import {useSelector} from "react-redux"; +import {getMessages, getChatroom} from "@/services/chatService"; +import { Client } from "@stomp/stompjs"; +import { getAccessToken } from "@/services/tokenService" + +interface message { + messageId: string; + chatroomId: string; + senderId: string; + receiverIds: string[]; + content: string; + createdAt: string; + attachments: string[]; +} + +interface chatroomProps { + chatroomId: string; + createdAt: string, + createdBy: string, + members: string[], + messages: message[], + isEvent: boolean, + unread: boolean, +} const ChatScreen = () => { const { id } = useLocalSearchParams(); const router = useRouter(); const [messages, setMessages] = useState([]); + const [finalToken, setFinalToken] = useState(""); + const user = useSelector((state: {user: any}) => state.user); + const [connected, setConnected] = useState(false); + const clientRef = useRef(null); + const [chatroom, setChatroom] = useState([]); useEffect(() => { - setMessages([ - { - _id: 1, - text: 'Hello developer', - createdAt: new Date(), - user: { - _id: 2, - name: 'React Native', - }, - }, - ]); + const response = async () => { + try { + const token = await getAccessToken(); + if (token) { + setFinalToken(token); + } + } catch (error) { + console.error('Failed to get access token:', error); + } + }; + + response(); }, []); + + useEffect(() => { + if (!finalToken) return; + + const client = new Client({ + brokerURL: "ws://localhost:8080/api/messaging-service/ws", + heartbeatIncoming: 0, + heartbeatOutgoing: 0, + connectHeaders: { + Authorization: "Bearer " + finalToken, + }, + //forceBinaryWSFrames: true, + //appendMissingNULLonIncoming: true, + + onWebSocketError: (error: any) => console.error("Websocket error:", error), + onConnect: () => { + setConnected(true); + + client.subscribe(`/topic/chatroom/${id}`, (message: any) => { + console.log(JSON.parse(message.body)); + setMessages((prev: any) => [...prev, message.body]); + }, + { + "Authorization": "Bearer " + finalToken, + }); + }, + onDisconnect: () => setConnected(false), + debug: (msg: any) => console.log(msg), + onStompError: (frame: any) => console.error("Stomp error:", frame), + + }); + + client.activate(); + clientRef.current = client; + + const fetchChatroom = async () => { + try { + const messagesData = await getMessages(id.toString()) + const chatroomData = await getChatroom(id.toString()) + setChatroom(chatroomData); + setMessages(messagesData); + } catch (error) { + console.error("Failed to fetch messages", error); + throw error; + } + } + fetchChatroom(); + + return () => { + client.deactivate(); + } + }, [finalToken]); + const onSend = useCallback((newMessages: IMessage[] = []) => { + + console.log(finalToken); if (!Array.isArray(newMessages)) return; - setMessages(previousMessages => GiftedChat.append(previousMessages, newMessages)); - }, []); + try { + const message = newMessages[0]; + + + + // @ts-ignore + clientRef.current.publish({ + destination: "/app/message", + headers: {Authorization: "Bearer " + finalToken}, + body: JSON.stringify(message), + }) + } catch (error) { + console.error("Failed to publish messages", error); + } + + //setMessages(previousMessages => GiftedChat.append(previousMessages, newMessages)); + }, [finalToken]); return ( diff --git a/ClientApp/components/chat/ChatList.tsx b/ClientApp/components/chat/ChatList.tsx index c861239ae..cda060e75 100644 --- a/ClientApp/components/chat/ChatList.tsx +++ b/ClientApp/components/chat/ChatList.tsx @@ -5,85 +5,88 @@ import {useSelector} from "react-redux"; import {getChatrooms} from "@/services/chatService"; import {User} from "react-native-gifted-chat"; -const Chats =() => { - const [chatrooms, setChatrooms] = useState([]); - const [user, setUser] = useState(null); +interface CardProps { + chatroomId: string; + createdBy: string; + //userImg: any; + createdAt: string; + //content: string; +} - interface CardProps { - chatroomId: string; - userId: string; - userImg: any; - createdAt: string; - content: string; - } - const fetchChatrooms = async () => { - try { - const user = useSelector((state: {user: any }) => state.user); - setUser(user); - const chatroomData = await getChatrooms(user.id); - setChatrooms(chatroomData); - } catch (error) { - Alert.alert("Error", "Failed to fetch chatrooms."); - } - } +const Chats: React.FC = () => { + const [chatrooms, setChatrooms] = useState([]); + const user = useSelector((state: {user: any}) => state.user); useEffect(() => { - fetchChatrooms(); - }) + const fetchChatrooms = async (user: any) => { + try { + const chatroomData = await getChatrooms(user.id); + setChatrooms(chatroomData); + + } catch (error) { + console.error("Failed to fetch chatrooms:", error); + throw error; + } + } + fetchChatrooms(user); + }, []); + + /* const chatData: CardProps[] = [ { chatroomId: '1', - userId: 'Alice Smith', - userImg: require('@/assets/images/avatar-placeholder.png'), + createdBy: 'Alice Smith', + //userImg: require('@/assets/images/avatar-placeholder.png'), createdAt: '2 mins ago', content: 'Hey Alice! How have you been?', }, { chatroomId: '2', - userId: 'Bob Johnson', - userImg: require('@/assets/images/avatar-placeholder.png'), + createdBy: 'Bob Johnson', + //userImg: require('@/assets/images/avatar-placeholder.png'), createdAt: '10 mins ago', content: 'Are we still on for tonight?', }, { chatroomId: '3', - userId: 'Charlie Davis', - userImg: require('@/assets/images/avatar-placeholder.png'), + createdBy: 'Charlie Davis', + //userImg: require('@/assets/images/avatar-placeholder.png'), createdAt: '30 mins ago', content: 'That was a great game yesterday!', }, { chatroomId: '4', - userId: 'Diana Prince', - userImg: require('@/assets/images/avatar-placeholder.png'), + createdBy: 'Diana Prince', + //userImg: require('@/assets/images/avatar-placeholder.png'), createdAt: '1 hour ago', content: 'Let’s catch up soon!', }, { chatroomId: '5', - userId: 'Edward Norton', - userImg: require('@/assets/images/avatar-placeholder.png'), + createdBy: 'Edward Norton', + //userImg: require('@/assets/images/avatar-placeholder.png'), createdAt: '3 hours ago', content: 'Do you have the notes from the meeting?', }, { chatroomId: '6', - userId: 'Fiona Gallagher', - userImg: require('@/assets/images/avatar-placeholder.png'), + createdBy: 'Fiona Gallagher', + //userImg: require('@/assets/images/avatar-placeholder.png'), createdAt: 'Yesterday', content: 'Hope you had a great weekend!', }, { chatroomId: '7', - userId: 'George Michael', - userImg: require('@/assets/images/avatar-placeholder.png'), + createdBy: 'George Michael', + //userImg: require('@/assets/images/avatar-placeholder.png'), createdAt: '2 days ago', content: 'Thanks for the recommendation!', }, ]; + */ return ( { renderItem={({ item }) => ( )} diff --git a/ClientApp/package.json b/ClientApp/package.json index 4acf0d297..d0bfd1f91 100644 --- a/ClientApp/package.json +++ b/ClientApp/package.json @@ -62,6 +62,7 @@ "@react-navigation/native": "^7.0.14", "@react-navigation/native-stack": "^7.2.0", "@reduxjs/toolkit": "^2.4.0", + "@stomp/stompjs": "^7.0.1", "@testing-library/react": "^16.0.1", "date-fns": "^2.30.0", "eas-cli": "^15.0.12", @@ -122,6 +123,8 @@ "react-native-vector-icons": "^10.2.0", "react-native-web": "~0.19.10", "react-redux": "^9.1.2", + "react-stomp": "^5.1.0", + "sockjs-client": "^1.6.1", "tailwindcss": "^3.4.14", "use-debounce": "^10.0.4", "expo-file-system": "~18.0.12" diff --git a/ClientApp/services/axiosLocalInstance.ts b/ClientApp/services/axiosLocalInstance.ts new file mode 100644 index 000000000..096b58060 --- /dev/null +++ b/ClientApp/services/axiosLocalInstance.ts @@ -0,0 +1,155 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios'; +import { refreshAccessToken, getAuthHeaders } from './tokenService'; +import { API_ENDPOINTS } from '@/utils/api/endpoints'; +import { logoutUser } from './authService'; +import { ALERT_MESSAGES, consoleError } from '@/utils/api/errorHandlers'; + +interface ErrorResponseData { + error?: string; + [key: string]: any; // To account for additional properties in the response +} + +let axiosLocalInstance: AxiosInstance | null = null; + +export const getAxiosLocalInstance = (): AxiosInstance => { + if (!axiosLocalInstance) { + throw new Error('Axios instance not configured. Call setupAxiosInstance(dispatch) before using it.'); + } + return axiosLocalInstance; +}; + +export const setupLocalAxiosInstance = (dispatch: any): AxiosInstance => { + if ((global as any).axiosLocalInstance) { + return (global as any).axiosLocalInstance; + } + const config: AxiosRequestConfig = { + baseURL: 'http://localhost:8080/api/', // Fallback for baseURL + timeout: 5000, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }; + + axiosLocalInstance = axios.create(config); // Assign to global variable + + // Request interceptor + axiosLocalInstance.interceptors.request.use( + async (config) => { + try { + const AUTH_ENDPOINTS = [ + API_ENDPOINTS.LOGIN, + API_ENDPOINTS.REFRESH_TOKEN, + API_ENDPOINTS.REGISTER, + API_ENDPOINTS.RESET_PASSWORD, + ]; + + // Skip adding Authorization header for auth-related endpoints + if (AUTH_ENDPOINTS.some((endpoint) => config.url?.includes(endpoint))) { + return config; + } + + const headers = await getAuthHeaders(); + config.headers.Authorization = headers.Authorization; + return config; + } catch (error) { + console.error('Error attaching headers:', error); + throw error; + } + }, + (error) => Promise.reject(error) + ); + + // Response interceptor + axiosLocalInstance.interceptors.response.use( + (response: AxiosResponse) => response, // Pass through successful responses + async (error: AxiosError) => { + const status = error.response?.status; + const errorData = error.response?.data; + console.log(axiosLocalInstance); + + switch (status) { + case 400: + consoleError(ALERT_MESSAGES.badRequest.title, ALERT_MESSAGES.badRequest.message, status); + break; + + case 401: { + const retryCount = (error.config as any)?._retryCount || 0; + const MAX_RETRY_LIMIT = 1; + + // Handle invalid credentials + if (errorData?.error && errorData.error.includes('invalid_grant')) { + consoleError( + ALERT_MESSAGES.invalidCredentials.title, + ALERT_MESSAGES.invalidCredentials.message, + status + ); + return Promise.reject(new Error(ALERT_MESSAGES.invalidCredentials.message)); + } + + // Retry logic for token expiration + if (retryCount >= MAX_RETRY_LIMIT) { + console.error('Retry limit exceeded.'); + await logoutUser(dispatch); + return Promise.reject(error); + } + + (error.config as any)._retryCount = retryCount + 1; + + try { + await refreshAccessToken(); + const headers = await getAuthHeaders(); + if (!axiosLocalInstance) { + throw new Error('Axios instance is not configured.'); + } + const retryConfig = { + ...error.config, + headers: { ...error.config?.headers, ...headers }, + }; + return axiosLocalInstance.request(retryConfig); + } catch (refreshError) { + await logoutUser(dispatch); + console.error('Token refresh failed:', refreshError); + return Promise.reject(refreshError); + } + } + + case 403: + consoleError(ALERT_MESSAGES.forbidden.title, ALERT_MESSAGES.forbidden.message, status); + break; + + case 404: + consoleError(ALERT_MESSAGES.notFound.title, ALERT_MESSAGES.notFound.message, status); + break; + + case 409: + consoleError(ALERT_MESSAGES.conflict.title, ALERT_MESSAGES.conflict.message, status); + break; + + case 500: + consoleError(ALERT_MESSAGES.serverError.title, ALERT_MESSAGES.serverError.message, status); + break; + + case 503: + consoleError(ALERT_MESSAGES.serviceUnavailable.title, ALERT_MESSAGES.serviceUnavailable.message, status); + break; + + default: + consoleError(ALERT_MESSAGES.defaultError.title, ALERT_MESSAGES.defaultError.message); + break; + } + + // Handle network errors or cases without a response + if (!error.response) { + console.error('Network error or no response:', error.message); + return Promise.reject(new Error(ALERT_MESSAGES.networkError.message)); + } + + return Promise.reject(error); + } + ); + (global as any).axiosInstance = axiosLocalInstance; + return axiosLocalInstance; +}; + + \ No newline at end of file diff --git a/ClientApp/services/chatService.ts b/ClientApp/services/chatService.ts index 4ca5931b2..d6748ec24 100644 --- a/ClientApp/services/chatService.ts +++ b/ClientApp/services/chatService.ts @@ -1,11 +1,25 @@ import { getAxiosInstance } from '@/services/axiosInstance'; +import { getAxiosLocalInstance } from '@/services/axiosLocalInstance'; import { API_ENDPOINTS } from '@/utils/api/endpoints'; +import {store} from "@/state/store"; // API_ENDPOINTS.GET_CHATROOMS -export const getChatrooms = async (userId: string) => { +export const getChatroom = async (chatroomId: string) => { try { const axiosInstance = await getAxiosInstance(); - const response = await axiosInstance.get( + const response = await axiosInstance.get(`/chatroom/${chatroomId}`); + return response.data; + } catch (error) { + console.error("Error fetching chatroom:", error); + throw error; + } +}; + +export const getChatrooms = async (userId: string) => { + + try { + const axiosLocalInstance = await getAxiosLocalInstance(); + const response = await axiosLocalInstance.get( API_ENDPOINTS.GET_CHATROOMS.replace("{userId}", userId)); return response.data; } catch (error) { @@ -13,3 +27,18 @@ export const getChatrooms = async (userId: string) => { throw error; } }; + +// API_ENDPOINTS.GET_MESSAGES +export const getMessages = async (chatroomId: string) => { + + try { + const axiosLocalInstance = await getAxiosLocalInstance(); + const response = await axiosLocalInstance.get( + API_ENDPOINTS.GET_MESSAGES.replace("{chatroomId}", chatroomId)); + return response.data; + } catch (error) { + console.error('Error fetching messages:', error); + throw error; + } +}; + diff --git a/ClientApp/services/tokenService.ts b/ClientApp/services/tokenService.ts index dea48c7ce..9dd4c27a7 100644 --- a/ClientApp/services/tokenService.ts +++ b/ClientApp/services/tokenService.ts @@ -82,7 +82,7 @@ export function stopTokenRefresh() { //#endregion //#region Helpers -function decodeJWT(jwt: string): { exp: number } { +export function decodeJWT(jwt: string): { exp: number } { try { const payload = JSON.parse( Buffer.from( @@ -112,7 +112,7 @@ function joinToken(part1: string, part2: string): string { return part1 + part2; } -async function getAccessToken() { +export async function getAccessToken() { try { const part1 = await getFromSecureStore('accessTokenPart1'); const part2 = await getFromSecureStore('accessTokenPart2'); diff --git a/ClientApp/utils/api/endpoints.tsx b/ClientApp/utils/api/endpoints.tsx index 45a191cc0..b50b8cd1a 100644 --- a/ClientApp/utils/api/endpoints.tsx +++ b/ClientApp/utils/api/endpoints.tsx @@ -41,4 +41,5 @@ export const API_ENDPOINTS = { GET_FILE: "storage-service/objects/file{objectPath}", GET_CHATROOMS: 'messaging-service/messaging/chatrooms/{userId}', + GET_MESSAGES: 'messaging/chatrooms/messages/{chatroomId}', }; From 89509713d6bf02b2c94a3e16d6a7c1e97afc5400 Mon Sep 17 00:00:00 2001 From: DanDuguay Date: Mon, 24 Mar 2025 13:39:23 -0400 Subject: [PATCH 05/28] GH-365: [Feat] Sending messages works --- ClientApp/app/messaging/[id].tsx | 116 +++++++++++++++++-------- ClientApp/components/chat/ChatList.tsx | 56 ------------ ClientApp/services/chatService.ts | 11 +-- ClientApp/utils/api/endpoints.tsx | 3 +- 4 files changed, 90 insertions(+), 96 deletions(-) diff --git a/ClientApp/app/messaging/[id].tsx b/ClientApp/app/messaging/[id].tsx index 5b07b0fb5..e3de8e0d3 100644 --- a/ClientApp/app/messaging/[id].tsx +++ b/ClientApp/app/messaging/[id].tsx @@ -13,18 +13,26 @@ interface message { senderId: string; receiverIds: string[]; content: string; - createdAt: string; + createdAt: Number | Date; attachments: string[]; } interface chatroomProps { chatroomId: string; - createdAt: string, - createdBy: string, - members: string[], - messages: message[], - isEvent: boolean, - unread: boolean, + createdAt: Number | Date; + createdBy: string; + members: string[]; + messages: message[]; + isEvent: boolean; + unread: boolean; +} + +interface messageRequest { + chatroomId: string; + senderId: string; + receiverIds: string[]; + content: string; + attachments: string[]; } const ChatScreen = () => { @@ -35,7 +43,7 @@ const ChatScreen = () => { const user = useSelector((state: {user: any}) => state.user); const [connected, setConnected] = useState(false); const clientRef = useRef(null); - const [chatroom, setChatroom] = useState([]); + const [chatroom, setChatroom] = useState(); useEffect(() => { const response = async () => { @@ -63,8 +71,6 @@ const ChatScreen = () => { connectHeaders: { Authorization: "Bearer " + finalToken, }, - //forceBinaryWSFrames: true, - //appendMissingNULLonIncoming: true, onWebSocketError: (error: any) => console.error("Websocket error:", error), onConnect: () => { @@ -72,7 +78,19 @@ const ChatScreen = () => { client.subscribe(`/topic/chatroom/${id}`, (message: any) => { console.log(JSON.parse(message.body)); - setMessages((prev: any) => [...prev, message.body]); + setMessages((prev: IMessage[]) => [ + ...prev, + { + _id: JSON.parse(message.body).messageId, + text: JSON.parse(message.body).content, + createdAt: new Date(JSON.parse(message.body).createdAt), + user: { + _id: JSON.parse(message.body).senderId, + name: 'Sender Name', // Replace with actual sender name if available + avatar: 'https://example.com/sender-avatar.png', // Replace with actual avatar URL if available + }, + }, + ]); }, { "Authorization": "Bearer " + finalToken, @@ -87,17 +105,33 @@ const ChatScreen = () => { client.activate(); clientRef.current = client; - const fetchChatroom = async () => { - try { - const messagesData = await getMessages(id.toString()) - const chatroomData = await getChatroom(id.toString()) - setChatroom(chatroomData); - setMessages(messagesData); - } catch (error) { - console.error("Failed to fetch messages", error); - throw error; - } + const fetchChatroom = async () => { + try { + const messagesData = await getMessages(id.toString()); + console.log("messageData: ", messagesData); + + // Ensure messagesData is mapped to IMessage structure + const formattedMessages = messagesData.map((message: any) => ({ + _id: message.messageId, + text: message.content, + createdAt: new Date(message.createdAt), + user: { + _id: message.senderId, + name: message.senderName || "Unknown", // Replace with sender's name if available + avatar: message.senderAvatar || "https://example.com/default-avatar.png", // Replace with avatar URL if available + }, + })); + + const chatroomData = await getChatroom(id.toString()); + console.log("chatroomData: ", chatroomData); + + setChatroom(chatroomData); + setMessages(formattedMessages); + } catch (error) { + console.error("Failed to fetch messages", error); + throw error; } + }; fetchChatroom(); return () => { @@ -106,33 +140,47 @@ const ChatScreen = () => { }, [finalToken]); const onSend = useCallback((newMessages: IMessage[] = []) => { + let attachments: string[] = []; + console.log("newMessages: ", newMessages); + console.log("chatroom: ", chatroom); - console.log(finalToken); if (!Array.isArray(newMessages)) return; try { - const message = newMessages[0]; - - - + const newMessage = newMessages[0]; + if (newMessage.audio != undefined) { + attachments.push(newMessage.audio) + } + if (newMessage.video != undefined) { + attachments.push(newMessage.video) + } + if (chatroom != undefined) { + const newMessageRequest: messageRequest = { + chatroomId: chatroom.chatroomId, + attachments: attachments, + content: newMessage.text, + receiverIds: chatroom.members, + senderId: user.id, + } + console.log("newMessageRequest: ",newMessageRequest); // @ts-ignore - clientRef.current.publish({ - destination: "/app/message", + clientRef.current.publish({ + destination: "/app/message", headers: {Authorization: "Bearer " + finalToken}, - body: JSON.stringify(message), - }) - } catch (error) { + body: JSON.stringify(newMessageRequest), + }) + } + } catch (error) { console.error("Failed to publish messages", error); } - //setMessages(previousMessages => GiftedChat.append(previousMessages, newMessages)); - }, [finalToken]); + }, [finalToken, chatroom]); return ( onSend(messages)} - user={{ _id: 1 }} + user={{ _id: user.id }} /> ); }; diff --git a/ClientApp/components/chat/ChatList.tsx b/ClientApp/components/chat/ChatList.tsx index cda060e75..000523e54 100644 --- a/ClientApp/components/chat/ChatList.tsx +++ b/ClientApp/components/chat/ChatList.tsx @@ -13,8 +13,6 @@ interface CardProps { //content: string; } - - const Chats: React.FC = () => { const [chatrooms, setChatrooms] = useState([]); const user = useSelector((state: {user: any}) => state.user); @@ -34,60 +32,6 @@ const Chats: React.FC = () => { fetchChatrooms(user); }, []); - /* - const chatData: CardProps[] = [ - { - chatroomId: '1', - createdBy: 'Alice Smith', - //userImg: require('@/assets/images/avatar-placeholder.png'), - createdAt: '2 mins ago', - content: 'Hey Alice! How have you been?', - }, - { - chatroomId: '2', - createdBy: 'Bob Johnson', - //userImg: require('@/assets/images/avatar-placeholder.png'), - createdAt: '10 mins ago', - content: 'Are we still on for tonight?', - }, - { - chatroomId: '3', - createdBy: 'Charlie Davis', - //userImg: require('@/assets/images/avatar-placeholder.png'), - createdAt: '30 mins ago', - content: 'That was a great game yesterday!', - }, - { - chatroomId: '4', - createdBy: 'Diana Prince', - //userImg: require('@/assets/images/avatar-placeholder.png'), - createdAt: '1 hour ago', - content: 'Let’s catch up soon!', - }, - { - chatroomId: '5', - createdBy: 'Edward Norton', - //userImg: require('@/assets/images/avatar-placeholder.png'), - createdAt: '3 hours ago', - content: 'Do you have the notes from the meeting?', - }, - { - chatroomId: '6', - createdBy: 'Fiona Gallagher', - //userImg: require('@/assets/images/avatar-placeholder.png'), - createdAt: 'Yesterday', - content: 'Hope you had a great weekend!', - }, - { - chatroomId: '7', - createdBy: 'George Michael', - //userImg: require('@/assets/images/avatar-placeholder.png'), - createdAt: '2 days ago', - content: 'Thanks for the recommendation!', - }, - ]; - */ - return ( { try { - const axiosInstance = await getAxiosInstance(); - const response = await axiosInstance.get(`/chatroom/${chatroomId}`); + const axiosLocalInstance = getAxiosLocalInstance(); + const response = await axiosLocalInstance.get(API_ENDPOINTS.GET_CHATROOM.replace("{chatroomId}", chatroomId)); return response.data; } catch (error) { console.error("Error fetching chatroom:", error); @@ -15,10 +15,11 @@ export const getChatroom = async (chatroomId: string) => { } }; +// API_ENDPOINTS.GET_CHATROOMS export const getChatrooms = async (userId: string) => { try { - const axiosLocalInstance = await getAxiosLocalInstance(); + const axiosLocalInstance = getAxiosLocalInstance(); const response = await axiosLocalInstance.get( API_ENDPOINTS.GET_CHATROOMS.replace("{userId}", userId)); return response.data; @@ -32,7 +33,7 @@ export const getChatrooms = async (userId: string) => { export const getMessages = async (chatroomId: string) => { try { - const axiosLocalInstance = await getAxiosLocalInstance(); + const axiosLocalInstance = getAxiosLocalInstance(); const response = await axiosLocalInstance.get( API_ENDPOINTS.GET_MESSAGES.replace("{chatroomId}", chatroomId)); return response.data; diff --git a/ClientApp/utils/api/endpoints.tsx b/ClientApp/utils/api/endpoints.tsx index b50b8cd1a..b3f3270e2 100644 --- a/ClientApp/utils/api/endpoints.tsx +++ b/ClientApp/utils/api/endpoints.tsx @@ -41,5 +41,6 @@ export const API_ENDPOINTS = { GET_FILE: "storage-service/objects/file{objectPath}", GET_CHATROOMS: 'messaging-service/messaging/chatrooms/{userId}', - GET_MESSAGES: 'messaging/chatrooms/messages/{chatroomId}', + GET_MESSAGES: 'messaging-service/messaging/chatrooms/messages/{chatroomId}', + GET_CHATROOM: 'messaging-service/messaging/chatroom/{chatroomId}', }; From 3a5593cc595fd8fb27a3a955acf3d7405847a092 Mon Sep 17 00:00:00 2001 From: Joud Babik Date: Fri, 28 Mar 2025 11:11:55 -0400 Subject: [PATCH 06/28] GH-365: [feat] Getting started on frontend --- ClientApp/components/chat/ChatList.tsx | 4 ++-- ClientApp/services/chatService.ts | 13 ++++++------- ClientApp/utils/api/endpoints.tsx | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/ClientApp/components/chat/ChatList.tsx b/ClientApp/components/chat/ChatList.tsx index 000523e54..2b565e8ab 100644 --- a/ClientApp/components/chat/ChatList.tsx +++ b/ClientApp/components/chat/ChatList.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useState} from 'react'; import ChatCard from '@/components/chat/ChatCard'; import {Alert, FlatList} from 'react-native'; import {useSelector} from "react-redux"; -import {getChatrooms} from "@/services/chatService"; +import {getAllChatrooms} from "@/services/chatService"; import {User} from "react-native-gifted-chat"; interface CardProps { @@ -21,7 +21,7 @@ const Chats: React.FC = () => { const fetchChatrooms = async (user: any) => { try { - const chatroomData = await getChatrooms(user.id); + const chatroomData = await getAllChatrooms(user.id); setChatrooms(chatroomData); } catch (error) { diff --git a/ClientApp/services/chatService.ts b/ClientApp/services/chatService.ts index a3f81c5ea..c7434e103 100644 --- a/ClientApp/services/chatService.ts +++ b/ClientApp/services/chatService.ts @@ -1,12 +1,11 @@ import { getAxiosInstance } from '@/services/axiosInstance'; -import { getAxiosLocalInstance } from '@/services/axiosLocalInstance'; import { API_ENDPOINTS } from '@/utils/api/endpoints'; import {store} from "@/state/store"; // API_ENDPOINTS.GET_CHATROOM export const getChatroom = async (chatroomId: string) => { try { - const axiosLocalInstance = getAxiosLocalInstance(); + const axiosLocalInstance = getAxiosInstance(); const response = await axiosLocalInstance.get(API_ENDPOINTS.GET_CHATROOM.replace("{chatroomId}", chatroomId)); return response.data; } catch (error) { @@ -16,12 +15,12 @@ export const getChatroom = async (chatroomId: string) => { }; // API_ENDPOINTS.GET_CHATROOMS -export const getChatrooms = async (userId: string) => { +export const getAllChatrooms = async (userId: string) => { try { - const axiosLocalInstance = getAxiosLocalInstance(); - const response = await axiosLocalInstance.get( - API_ENDPOINTS.GET_CHATROOMS.replace("{userId}", userId)); + const axiosLocalInstance = getAxiosInstance(); + const response = await axiosLocalInstance.get(API_ENDPOINTS.GET_All_CHATROOMS.replace("{userId}", userId)); + console.log("Chatrooms response:", response.data); return response.data; } catch (error) { console.error('Error fetching chatrooms:', error); @@ -33,7 +32,7 @@ export const getChatrooms = async (userId: string) => { export const getMessages = async (chatroomId: string) => { try { - const axiosLocalInstance = getAxiosLocalInstance(); + const axiosLocalInstance = getAxiosInstance(); const response = await axiosLocalInstance.get( API_ENDPOINTS.GET_MESSAGES.replace("{chatroomId}", chatroomId)); return response.data; diff --git a/ClientApp/utils/api/endpoints.tsx b/ClientApp/utils/api/endpoints.tsx index b3f3270e2..3086d6200 100644 --- a/ClientApp/utils/api/endpoints.tsx +++ b/ClientApp/utils/api/endpoints.tsx @@ -40,7 +40,7 @@ export const API_ENDPOINTS = { UPLOAD_FILE: "storage-service/objects/upload", GET_FILE: "storage-service/objects/file{objectPath}", - GET_CHATROOMS: 'messaging-service/messaging/chatrooms/{userId}', + GET_All_CHATROOMS: 'messaging-service/messaging/chatrooms/{userId}', GET_MESSAGES: 'messaging-service/messaging/chatrooms/messages/{chatroomId}', GET_CHATROOM: 'messaging-service/messaging/chatroom/{chatroomId}', }; From b7399604723a2d5d9a2026a48e9e6c3cb4f153c4 Mon Sep 17 00:00:00 2001 From: Joud Babik Date: Fri, 28 Mar 2025 13:35:52 -0400 Subject: [PATCH 07/28] GH-365: [refactor] Add messaging types to message.ts --- ClientApp/app/messaging/[id].tsx | 30 ++-------------------- ClientApp/components/chat/ChatCard.tsx | 14 +++++++++-- ClientApp/components/chat/ChatList.tsx | 26 +++++++++++++++++-- ClientApp/package.json | 1 + ClientApp/services/chatService.ts | 35 ++++++++++++++++++++++++++ ClientApp/types/messaging.ts | 28 +++++++++++++++++++++ ClientApp/utils/api/endpoints.tsx | 2 ++ 7 files changed, 104 insertions(+), 32 deletions(-) create mode 100644 ClientApp/types/messaging.ts diff --git a/ClientApp/app/messaging/[id].tsx b/ClientApp/app/messaging/[id].tsx index e3de8e0d3..bad200121 100644 --- a/ClientApp/app/messaging/[id].tsx +++ b/ClientApp/app/messaging/[id].tsx @@ -6,34 +6,8 @@ import {useSelector} from "react-redux"; import {getMessages, getChatroom} from "@/services/chatService"; import { Client } from "@stomp/stompjs"; import { getAccessToken } from "@/services/tokenService" +import {message, chatroomProps, messageRequest} from "@/types/messaging"; -interface message { - messageId: string; - chatroomId: string; - senderId: string; - receiverIds: string[]; - content: string; - createdAt: Number | Date; - attachments: string[]; -} - -interface chatroomProps { - chatroomId: string; - createdAt: Number | Date; - createdBy: string; - members: string[]; - messages: message[]; - isEvent: boolean; - unread: boolean; -} - -interface messageRequest { - chatroomId: string; - senderId: string; - receiverIds: string[]; - content: string; - attachments: string[]; -} const ChatScreen = () => { const { id } = useLocalSearchParams(); @@ -65,7 +39,7 @@ const ChatScreen = () => { if (!finalToken) return; const client = new Client({ - brokerURL: "ws://localhost:8080/api/messaging-service/ws", + brokerURL: "wss://api.sportahub.app/api/messaging-service/ws", heartbeatIncoming: 0, heartbeatOutgoing: 0, connectHeaders: { diff --git a/ClientApp/components/chat/ChatCard.tsx b/ClientApp/components/chat/ChatCard.tsx index 7d112c9ea..7625467da 100644 --- a/ClientApp/components/chat/ChatCard.tsx +++ b/ClientApp/components/chat/ChatCard.tsx @@ -9,16 +9,26 @@ interface CardProps { userImg: any; messageTime: string; messageText: string; + onLongPress?: () => void; + } -const ChatCard: React.FC = ({ chatId, userName, userImg, messageTime, messageText }) => { +const ChatCard: React.FC = ({ + chatId, + userName, + userImg, + messageTime, + messageText, + onLongPress +}) => { const router = useRouter(); return ( router.push(`/messaging/${chatId}`)} -> + onLongPress={onLongPress} + > diff --git a/ClientApp/components/chat/ChatList.tsx b/ClientApp/components/chat/ChatList.tsx index 2b565e8ab..335dc0de7 100644 --- a/ClientApp/components/chat/ChatList.tsx +++ b/ClientApp/components/chat/ChatList.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useState} from 'react'; import ChatCard from '@/components/chat/ChatCard'; import {Alert, FlatList} from 'react-native'; import {useSelector} from "react-redux"; -import {getAllChatrooms} from "@/services/chatService"; +import {deleteChatroom, getAllChatrooms} from "@/services/chatService"; import {User} from "react-native-gifted-chat"; interface CardProps { @@ -17,8 +17,29 @@ const Chats: React.FC = () => { const [chatrooms, setChatrooms] = useState([]); const user = useSelector((state: {user: any}) => state.user); + const handleDelete = (chatroomId: string) => { + Alert.alert( + "Delete Chat", + "Do you want to delete this chat?", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + style: "destructive", + onPress: async () => { + try { + await deleteChatroom(chatroomId); + setChatrooms(prev => prev.filter(c => c.chatroomId !== chatroomId)); + } catch (e) { + console.error("Failed to delete chatroom:", e); + Alert.alert("Error", "Failed to delete chat. Please try again."); + } + } + } + ] + ); + }; useEffect(() => { - const fetchChatrooms = async (user: any) => { try { const chatroomData = await getAllChatrooms(user.id); @@ -43,6 +64,7 @@ const Chats: React.FC = () => { messageTime={item.createdAt} userName={user.username} chatId={item.chatroomId} + onLongPress={() => handleDelete(item.chatroomId)} /> )} /> diff --git a/ClientApp/package.json b/ClientApp/package.json index d0bfd1f91..05e9f937f 100644 --- a/ClientApp/package.json +++ b/ClientApp/package.json @@ -118,6 +118,7 @@ "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.4.0", "react-native-svg": "15.8.0", + "react-native-swipe-list-view": "^3.2.9", "react-native-tab-view": "^4.0.1", "react-native-toast-message": "^2.2.1", "react-native-vector-icons": "^10.2.0", diff --git a/ClientApp/services/chatService.ts b/ClientApp/services/chatService.ts index c7434e103..61bb7a326 100644 --- a/ClientApp/services/chatService.ts +++ b/ClientApp/services/chatService.ts @@ -1,6 +1,7 @@ import { getAxiosInstance } from '@/services/axiosInstance'; import { API_ENDPOINTS } from '@/utils/api/endpoints'; import {store} from "@/state/store"; +import { message } from '@/types/messaging'; // API_ENDPOINTS.GET_CHATROOM export const getChatroom = async (chatroomId: string) => { @@ -42,3 +43,37 @@ export const getMessages = async (chatroomId: string) => { } }; +// API_ENDPOINTS.CREATE_CHATROOM +export const createUserChatroom = async (createrId: string, nameOfChatRoom: string, members: message [], messages: string[], isEvent: boolean, unread: boolean) => { + try { + const axiosLocalInstance = getAxiosInstance(); + const response = await axiosLocalInstance.post(API_ENDPOINTS.CREATE_CHATROOM, + { + createdBy: createrId, + chatroomName: nameOfChatRoom, + members, + messages, + isEvent, + unread + } + ); + return response.data; + } catch (error) { + console.error('Error creating chatroom:', error); + throw error; + } +} + + + +// API_ENDPOINTS.DELETE_CHATROOM +export const deleteChatroom = async (chatroomId: string) => { + try { + const axiosLocalInstance = getAxiosInstance(); + const response = await axiosLocalInstance.delete(API_ENDPOINTS.DELETE_CHATROOM.replace("{chatroomId}", chatroomId)); + return response.data; + } catch (error) { + console.error('Error deleting chatroom:', error); + throw error; + } +}; diff --git a/ClientApp/types/messaging.ts b/ClientApp/types/messaging.ts new file mode 100644 index 000000000..9d593286d --- /dev/null +++ b/ClientApp/types/messaging.ts @@ -0,0 +1,28 @@ +export interface message { + messageId: string; + chatroomId: string; //required + senderId: string; //required + receiverIds: string[]; //required + senderName: string; + content: string; //required + createdAt: Number | Date; + attachments: string[]; + } + +export interface chatroomProps { + chatroomId: string; + createdAt: Number | Date; + createdBy: string; + members: string[]; + messages: message[]; + isEvent: boolean; + unread: boolean; + } + +export interface messageRequest { + chatroomId: string; + senderId: string; + receiverIds: string[]; + content: string; + attachments: string[]; + } \ No newline at end of file diff --git a/ClientApp/utils/api/endpoints.tsx b/ClientApp/utils/api/endpoints.tsx index 3086d6200..aa64fa02e 100644 --- a/ClientApp/utils/api/endpoints.tsx +++ b/ClientApp/utils/api/endpoints.tsx @@ -43,4 +43,6 @@ export const API_ENDPOINTS = { GET_All_CHATROOMS: 'messaging-service/messaging/chatrooms/{userId}', GET_MESSAGES: 'messaging-service/messaging/chatrooms/messages/{chatroomId}', GET_CHATROOM: 'messaging-service/messaging/chatroom/{chatroomId}', + CREATE_CHATROOM: 'messaging-service/messaging/chatroom', + DELETE_CHATROOM: 'messaging-service/messaging/chatroom/{chatroomId}', }; From 86a58ddd55a644a00094301b097605130247cf9b Mon Sep 17 00:00:00 2001 From: Joud Babik Date: Fri, 28 Mar 2025 19:12:00 -0400 Subject: [PATCH 08/28] GH-365: [feat] Create a chat from a friend's profile page, doesn't route to chatroom yet --- ClientApp/app/userProfiles/[id].tsx | 1 + .../components/Profile/ProfileSection.tsx | 28 +++++++++---- ClientApp/components/chat/ChatCard.tsx | 6 +-- ClientApp/components/chat/ChatList.tsx | 42 +++++++++++-------- ClientApp/services/chatService.ts | 5 ++- 5 files changed, 51 insertions(+), 31 deletions(-) diff --git a/ClientApp/app/userProfiles/[id].tsx b/ClientApp/app/userProfiles/[id].tsx index 16a599ba7..a852ba4e8 100644 --- a/ClientApp/app/userProfiles/[id].tsx +++ b/ClientApp/app/userProfiles/[id].tsx @@ -273,6 +273,7 @@ const ProfilePage: React.FC = () => { return ( void | Promise | null; @@ -22,13 +26,16 @@ interface ProfileRequest { } const ProfileSection: React.FC = ({ + visitedId, user, friendStatus, handleFriendRequest, handleRemoveFriend, isUserProfile, }) => { + const router = useRouter(); const [loading, setLoading] = useState(false); + const loggedInUser = useSelector((state: { user: any }) => state.user); const { t } = useTranslation(); if (loading) { @@ -47,6 +54,16 @@ const ProfileSection: React.FC = ({ ); } + const handlePressMessage = async () => { + // Handle press event + console.log("Message button pressed"); + try{ + const response = await createUserChatroom(loggedInUser.id, visitedId, user.username, [], false, false); + }catch(e){ + console.log("Error in handlePress", e); + } + }; + return ( <> = ({ )} {/* Message button */} - + = ({ > {t('profile_section.message')} + = ({ chatId, - userName, + cardTitle, userImg, messageTime, messageText, @@ -35,7 +35,7 @@ const ChatCard: React.FC = ({ - {userName} + {cardTitle} {messageTime} {messageText} diff --git a/ClientApp/components/chat/ChatList.tsx b/ClientApp/components/chat/ChatList.tsx index 335dc0de7..dfdc2afea 100644 --- a/ClientApp/components/chat/ChatList.tsx +++ b/ClientApp/components/chat/ChatList.tsx @@ -1,19 +1,21 @@ -import React, {useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import ChatCard from '@/components/chat/ChatCard'; import {Alert, FlatList} from 'react-native'; import {useSelector} from "react-redux"; import {deleteChatroom, getAllChatrooms} from "@/services/chatService"; import {User} from "react-native-gifted-chat"; +import { useFocusEffect } from '@react-navigation/native'; interface CardProps { chatroomId: string; createdBy: string; - //userImg: any; + userImg: any; createdAt: string; - //content: string; + content: string; + chatroomName: string } -const Chats: React.FC = () => { +const Chats = () => { const [chatrooms, setChatrooms] = useState([]); const user = useSelector((state: {user: any}) => state.user); @@ -39,19 +41,25 @@ const Chats: React.FC = () => { ] ); }; - useEffect(() => { - const fetchChatrooms = async (user: any) => { - try { - const chatroomData = await getAllChatrooms(user.id); - setChatrooms(chatroomData); - } catch (error) { - console.error("Failed to fetch chatrooms:", error); - throw error; - } - } - fetchChatrooms(user); - }, []); + useFocusEffect( + useCallback(() => { + const fetchChatrooms = async (user: any) => { + try { + const chatroomData = await getAllChatrooms(user.id); + setChatrooms(chatroomData); + } catch (error) { + console.error("Failed to fetch chatrooms:", error); + throw error; + } + }; + fetchChatrooms(user); + + return () => { + setChatrooms([]); // Cleanup if needed + }; + }, [user]) + ); return ( = () => { userImg={require('@/assets/images/avatar-placeholder.png')} messageText={"hello"} messageTime={item.createdAt} - userName={user.username} + cardTitle={item.chatroomName} chatId={item.chatroomId} onLongPress={() => handleDelete(item.chatroomId)} /> diff --git a/ClientApp/services/chatService.ts b/ClientApp/services/chatService.ts index 61bb7a326..1ab28fcff 100644 --- a/ClientApp/services/chatService.ts +++ b/ClientApp/services/chatService.ts @@ -44,19 +44,20 @@ export const getMessages = async (chatroomId: string) => { }; // API_ENDPOINTS.CREATE_CHATROOM -export const createUserChatroom = async (createrId: string, nameOfChatRoom: string, members: message [], messages: string[], isEvent: boolean, unread: boolean) => { +export const createUserChatroom = async (createrId: string, chatWithUserId: string, nameOfChatRoom: string, messages: string[], isEvent: boolean, unread: boolean) => { try { const axiosLocalInstance = getAxiosInstance(); const response = await axiosLocalInstance.post(API_ENDPOINTS.CREATE_CHATROOM, { createdBy: createrId, chatroomName: nameOfChatRoom, - members, + members : [createrId, chatWithUserId], messages, isEvent, unread } ); + console.log("createUserChatroom response:", response.data); return response.data; } catch (error) { console.error('Error creating chatroom:', error); From 5ad11fc7418e11fc18afc86a07e6fcc0d2cbad4b Mon Sep 17 00:00:00 2001 From: Joud Babik Date: Fri, 28 Mar 2025 19:25:04 -0400 Subject: [PATCH 09/28] GH-365: [refactor] Some temporary cleanups --- ClientApp/app/messaging/[id].tsx | 218 +++++++++++++++--------------- ClientApp/services/chatService.ts | 1 - 2 files changed, 109 insertions(+), 110 deletions(-) diff --git a/ClientApp/app/messaging/[id].tsx b/ClientApp/app/messaging/[id].tsx index bad200121..6d7d5c9ba 100644 --- a/ClientApp/app/messaging/[id].tsx +++ b/ClientApp/app/messaging/[id].tsx @@ -35,117 +35,117 @@ const ChatScreen = () => { }, []); - useEffect(() => { - if (!finalToken) return; - - const client = new Client({ - brokerURL: "wss://api.sportahub.app/api/messaging-service/ws", - heartbeatIncoming: 0, - heartbeatOutgoing: 0, - connectHeaders: { - Authorization: "Bearer " + finalToken, - }, - - onWebSocketError: (error: any) => console.error("Websocket error:", error), - onConnect: () => { - setConnected(true); - - client.subscribe(`/topic/chatroom/${id}`, (message: any) => { - console.log(JSON.parse(message.body)); - setMessages((prev: IMessage[]) => [ - ...prev, - { - _id: JSON.parse(message.body).messageId, - text: JSON.parse(message.body).content, - createdAt: new Date(JSON.parse(message.body).createdAt), - user: { - _id: JSON.parse(message.body).senderId, - name: 'Sender Name', // Replace with actual sender name if available - avatar: 'https://example.com/sender-avatar.png', // Replace with actual avatar URL if available - }, - }, - ]); - }, - { - "Authorization": "Bearer " + finalToken, - }); - }, - onDisconnect: () => setConnected(false), - debug: (msg: any) => console.log(msg), - onStompError: (frame: any) => console.error("Stomp error:", frame), - - }); - - client.activate(); - clientRef.current = client; - - const fetchChatroom = async () => { - try { - const messagesData = await getMessages(id.toString()); - console.log("messageData: ", messagesData); - - // Ensure messagesData is mapped to IMessage structure - const formattedMessages = messagesData.map((message: any) => ({ - _id: message.messageId, - text: message.content, - createdAt: new Date(message.createdAt), - user: { - _id: message.senderId, - name: message.senderName || "Unknown", // Replace with sender's name if available - avatar: message.senderAvatar || "https://example.com/default-avatar.png", // Replace with avatar URL if available - }, - })); - - const chatroomData = await getChatroom(id.toString()); - console.log("chatroomData: ", chatroomData); - - setChatroom(chatroomData); - setMessages(formattedMessages); - } catch (error) { - console.error("Failed to fetch messages", error); - throw error; - } - }; - fetchChatroom(); - - return () => { - client.deactivate(); - } - }, [finalToken]); + // useEffect(() => { + // if (!finalToken) return; + + // const client = new Client({ + // brokerURL: "wss://api.sportahub.app/api/messaging-service/ws", + // heartbeatIncoming: 0, + // heartbeatOutgoing: 0, + // connectHeaders: { + // Authorization: "Bearer " + finalToken, + // }, + + // onWebSocketError: (error: any) => console.error("Websocket error:", error), + // onConnect: () => { + // setConnected(true); + + // client.subscribe(`/topic/chatroom/${id}`, (message: any) => { + // console.log(JSON.parse(message.body)); + // setMessages((prev: IMessage[]) => [ + // ...prev, + // { + // _id: JSON.parse(message.body).messageId, + // text: JSON.parse(message.body).content, + // createdAt: new Date(JSON.parse(message.body).createdAt), + // user: { + // _id: JSON.parse(message.body).senderId, + // name: 'Sender Name', // Replace with actual sender name if available + // avatar: 'https://example.com/sender-avatar.png', // Replace with actual avatar URL if available + // }, + // }, + // ]); + // }, + // { + // "Authorization": "Bearer " + finalToken, + // }); + // }, + // onDisconnect: () => setConnected(false), + // debug: (msg: any) => console.log(msg), + // onStompError: (frame: any) => console.error("Stomp error:", frame), + + // }); + + // client.activate(); + // clientRef.current = client; + + // const fetchChatroom = async () => { + // try { + // const messagesData = await getMessages(id.toString()); + // console.log("messageData: ", messagesData); + + // // Ensure messagesData is mapped to IMessage structure + // const formattedMessages = messagesData.map((message: any) => ({ + // _id: message.messageId, + // text: message.content, + // createdAt: new Date(message.createdAt), + // user: { + // _id: message.senderId, + // name: message.senderName || "Unknown", // Replace with sender's name if available + // avatar: message.senderAvatar || "https://example.com/default-avatar.png", // Replace with avatar URL if available + // }, + // })); + + // const chatroomData = await getChatroom(id.toString()); + // console.log("chatroomData: ", chatroomData); + + // setChatroom(chatroomData); + // setMessages(formattedMessages); + // } catch (error) { + // console.error("Failed to fetch messages", error); + // throw error; + // } + // }; + // fetchChatroom(); + + // return () => { + // client.deactivate(); + // } + // }, [finalToken]); const onSend = useCallback((newMessages: IMessage[] = []) => { - let attachments: string[] = []; - console.log("newMessages: ", newMessages); - console.log("chatroom: ", chatroom); - - if (!Array.isArray(newMessages)) return; - try { - const newMessage = newMessages[0]; - if (newMessage.audio != undefined) { - attachments.push(newMessage.audio) - } - if (newMessage.video != undefined) { - attachments.push(newMessage.video) - } - if (chatroom != undefined) { - const newMessageRequest: messageRequest = { - chatroomId: chatroom.chatroomId, - attachments: attachments, - content: newMessage.text, - receiverIds: chatroom.members, - senderId: user.id, - } - console.log("newMessageRequest: ",newMessageRequest); - // @ts-ignore - clientRef.current.publish({ - destination: "/app/message", - headers: {Authorization: "Bearer " + finalToken}, - body: JSON.stringify(newMessageRequest), - }) - } - } catch (error) { - console.error("Failed to publish messages", error); - } + // let attachments: string[] = []; + // console.log("newMessages: ", newMessages); + // console.log("chatroom: ", chatroom); + + // if (!Array.isArray(newMessages)) return; + // try { + // const newMessage = newMessages[0]; + // if (newMessage.audio != undefined) { + // attachments.push(newMessage.audio) + // } + // if (newMessage.video != undefined) { + // attachments.push(newMessage.video) + // } + // if (chatroom != undefined) { + // const newMessageRequest: messageRequest = { + // chatroomId: chatroom.chatroomId, + // attachments: attachments, + // content: newMessage.text, + // receiverIds: chatroom.members, + // senderId: user.id, + // } + // console.log("newMessageRequest: ",newMessageRequest); + // // @ts-ignore + // clientRef.current.publish({ + // destination: "/app/message", + // headers: {Authorization: "Bearer " + finalToken}, + // body: JSON.stringify(newMessageRequest), + // }) + // } + // } catch (error) { + // console.error("Failed to publish messages", error); + // } }, [finalToken, chatroom]); diff --git a/ClientApp/services/chatService.ts b/ClientApp/services/chatService.ts index 1ab28fcff..59dca6914 100644 --- a/ClientApp/services/chatService.ts +++ b/ClientApp/services/chatService.ts @@ -21,7 +21,6 @@ export const getAllChatrooms = async (userId: string) => { try { const axiosLocalInstance = getAxiosInstance(); const response = await axiosLocalInstance.get(API_ENDPOINTS.GET_All_CHATROOMS.replace("{userId}", userId)); - console.log("Chatrooms response:", response.data); return response.data; } catch (error) { console.error('Error fetching chatrooms:', error); From 71f48c7885c9ebf799d0b95f445ba21a2bf8a9a2 Mon Sep 17 00:00:00 2001 From: Joud Babik Date: Fri, 28 Mar 2025 21:08:39 -0400 Subject: [PATCH 10/28] GH-365: [feat] Route to chatroom from friend user profile and fix tabs and navigation --- ClientApp/app/(tabs)/_layout.tsx | 8 ++--- .../app/{messaging => (tabs)/chats}/[id].tsx | 29 +++++++++++++++++-- ClientApp/app/(tabs)/chats/_layout.tsx | 14 +++++++++ ClientApp/app/_layout.tsx | 1 - .../components/Profile/ProfileSection.tsx | 7 +++++ ClientApp/components/chat/ChatCard.tsx | 10 ++++++- 6 files changed, 61 insertions(+), 8 deletions(-) rename ClientApp/app/{messaging => (tabs)/chats}/[id].tsx (89%) create mode 100644 ClientApp/app/(tabs)/chats/_layout.tsx diff --git a/ClientApp/app/(tabs)/_layout.tsx b/ClientApp/app/(tabs)/_layout.tsx index 2650d4e25..677d85576 100644 --- a/ClientApp/app/(tabs)/_layout.tsx +++ b/ClientApp/app/(tabs)/_layout.tsx @@ -56,7 +56,7 @@ export default function TabLayout() { }} /> ( @@ -64,9 +64,9 @@ export default function TabLayout() { ), }} listeners={{ - tabPress: (e) => { - console.log('Chats tab pressed'); - }, + // tabPress: (e) => { + // console.log('Chats tab pressed'); + // }, }} /> { - const { id } = useLocalSearchParams(); + const { id, title } = useLocalSearchParams(); const router = useRouter(); const [messages, setMessages] = useState([]); const [finalToken, setFinalToken] = useState(""); @@ -18,7 +18,32 @@ const ChatScreen = () => { const [connected, setConnected] = useState(false); const clientRef = useRef(null); const [chatroom, setChatroom] = useState(); + const navigation = useNavigation(); + useEffect(() => { + // Hide tab bar when this screen is focused + const parent = navigation.getParent(); + parent?.setOptions({ + tabBarStyle: { + display: 'none' + } + }); + + + // Restore it when unmounted + return () => { + parent?.setOptions({ + tabBarStyle: undefined + }); + }; + }, []); + + useEffect(() => { + if (title && typeof title === 'string') { + navigation.setOptions({ headerTitle: title }); + } + }, [title]); + useEffect(() => { const response = async () => { try { diff --git a/ClientApp/app/(tabs)/chats/_layout.tsx b/ClientApp/app/(tabs)/chats/_layout.tsx new file mode 100644 index 000000000..55e97015c --- /dev/null +++ b/ClientApp/app/(tabs)/chats/_layout.tsx @@ -0,0 +1,14 @@ +import { Stack } from 'expo-router'; + +export default function ChatsLayout() { + return ( + + + + + ); +} diff --git a/ClientApp/app/_layout.tsx b/ClientApp/app/_layout.tsx index 6ad88feb7..deb806971 100644 --- a/ClientApp/app/_layout.tsx +++ b/ClientApp/app/_layout.tsx @@ -36,7 +36,6 @@ export default function RootLayout() { - diff --git a/ClientApp/components/Profile/ProfileSection.tsx b/ClientApp/components/Profile/ProfileSection.tsx index 8de563b1f..712e2869d 100644 --- a/ClientApp/components/Profile/ProfileSection.tsx +++ b/ClientApp/components/Profile/ProfileSection.tsx @@ -59,6 +59,13 @@ const ProfileSection: React.FC = ({ console.log("Message button pressed"); try{ const response = await createUserChatroom(loggedInUser.id, visitedId, user.username, [], false, false); + router.push({ + pathname: '/(tabs)/chats/[id]', + params: { + id: response.chatroomId, + title: user.username }, + }); + }catch(e){ console.log("Error in handlePress", e); } diff --git a/ClientApp/components/chat/ChatCard.tsx b/ClientApp/components/chat/ChatCard.tsx index db4bd697d..a00f06993 100644 --- a/ClientApp/components/chat/ChatCard.tsx +++ b/ClientApp/components/chat/ChatCard.tsx @@ -26,7 +26,15 @@ const ChatCard: React.FC = ({ return ( router.push(`/messaging/${chatId}`)} + onPress={() => + router.push({ + pathname: '/(tabs)/chats/[id]', + params: { + id: chatId, + title: cardTitle, + }, + }) + } onLongPress={onLongPress} > From 367255594c14f92ef6ab006d1bcda04ca3f25771 Mon Sep 17 00:00:00 2001 From: Joud Babik Date: Sat, 29 Mar 2025 02:06:43 -0400 Subject: [PATCH 11/28] GH-365: [feat] Starter code for creating a group chat - changes to createUserChatroom are now caushing 400 (but over postamn too) --- ClientApp/app/(tabs)/_layout.tsx | 1 + ClientApp/app/(tabs)/chats/index.tsx | 8 +- ClientApp/app/(tabs)/profile/index.tsx | 1 + ClientApp/components/chat/ChatListHeader.tsx | 83 +++++++++++ ClientApp/components/chat/NewChatModal.tsx | 138 +++++++++++++++++++ ClientApp/services/chatService.ts | 21 ++- 6 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 ClientApp/components/chat/ChatListHeader.tsx create mode 100644 ClientApp/components/chat/NewChatModal.tsx diff --git a/ClientApp/app/(tabs)/_layout.tsx b/ClientApp/app/(tabs)/_layout.tsx index 677d85576..5dc34a4c3 100644 --- a/ClientApp/app/(tabs)/_layout.tsx +++ b/ClientApp/app/(tabs)/_layout.tsx @@ -59,6 +59,7 @@ export default function TabLayout() { name="chats" options={{ title: t('tab_layout.chats'), + headerShown: false, tabBarIcon: ({ color }) => ( ), diff --git a/ClientApp/app/(tabs)/chats/index.tsx b/ClientApp/app/(tabs)/chats/index.tsx index 9f5b50cf9..172118eb3 100644 --- a/ClientApp/app/(tabs)/chats/index.tsx +++ b/ClientApp/app/(tabs)/chats/index.tsx @@ -1,8 +1,14 @@ import React from 'react'; import ChatList from '@/components/chat/ChatList'; +import ChatListHeader from '@/components/chat/ChatListHeader'; +import { SafeAreaView } from 'react-native-safe-area-context'; const Chats =() => { + return ( - + + + + ) } diff --git a/ClientApp/app/(tabs)/profile/index.tsx b/ClientApp/app/(tabs)/profile/index.tsx index c7509cf3f..f044e216e 100644 --- a/ClientApp/app/(tabs)/profile/index.tsx +++ b/ClientApp/app/(tabs)/profile/index.tsx @@ -116,6 +116,7 @@ const ProfilePage: React.FC = () => { { + + const [modalVisible, setModalVisible] = useState(false); + const [friends, setFriends] = useState([]); + const [selectedFriends, setSelectedFriends] = useState([]); + + const user = useSelector((state: { user: any }) => state.user); + + const fetchFriends = async () => { + setLoading(true); + try { + const friendList = await getFriendsOfUser(user.id); + const friendsWithProfiles = await Promise.all( + friendList.map(async (friend: any) => { + const profile = await getUserProfile(friend.friendUserId); + return { ...friend, profile }; + }) + ); + setFriends(friendsWithProfiles); + } catch (error) { + console.error("Failed to fetch friends:", error); + setFriends([]); + } finally { + setLoading(false); + } + }; + + const handleNewChatPress = () => { + setModalVisible(true); + fetchFriends(); + }; + const handleCreateGroup = async() => { + // Call your API or navigate to group chat screen + + }; + + + return ( + + + + + + setModalVisible(false)} + onCreateGroup={handleCreateGroup} + /> + + ); +}; + +const styles = StyleSheet.create({ + headerContainer: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + backgroundColor: ThemeColors.background.lightGrey, + borderBottomWidth: 1, + borderBottomColor: '#ddd', + }, + headerTitle: { + fontSize: 20, + fontWeight: 'bold', + color: '#333', + }, + spacer: { + flex: 1, + }, +}); + +export default ChatListHeader; diff --git a/ClientApp/components/chat/NewChatModal.tsx b/ClientApp/components/chat/NewChatModal.tsx new file mode 100644 index 000000000..8226db5ed --- /dev/null +++ b/ClientApp/components/chat/NewChatModal.tsx @@ -0,0 +1,138 @@ +import React, { useState } from 'react'; +import {Modal,View,Text,FlatList,TouchableOpacity,StyleSheet,Pressable,} from 'react-native'; +import FontAwesome from '@expo/vector-icons/FontAwesome'; + +const sampleFriends = [ + { id: '1', name: 'User1' }, + { id: '2', name: 'User2' }, + { id: '3', name: 'User3' }, + { id: '4', name: 'User4' }, + ]; + +interface NewChatModalProps { + visible: boolean; + onClose: () => void; + onCreateGroup: (selected: typeof sampleFriends) => void; +} + +const NewChatModal: React.FC = ({ + visible, + onClose, + onCreateGroup, +}) => { + const [selected, setSelected] = useState([]); + + const toggleSelect = (id: string) => { + setSelected((prev) => + prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] + ); + }; + + const handleCreate = () => { + const selectedFriends = sampleFriends.filter((f) => + selected.includes(f.id) + ); + onCreateGroup(selectedFriends); + onClose(); + setSelected([]); + }; + + return ( + + + + Select Friends + item.id} + renderItem={({ item }) => { + const isSelected = selected.includes(item.id); + return ( + toggleSelect(item.id)} + > + {item.name} + {isSelected && ( + + )} + + ); + }} + /> + + Start Group Chat + + + + Cancel + + + + + ); +}; + +export default NewChatModal; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.4)', + justifyContent: 'center', + paddingHorizontal: 20, + }, + modalContent: { + backgroundColor: 'white', + borderRadius: 16, + padding: 20, + maxHeight: '80%', + }, + title: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 12, + }, + friendItem: { + flexDirection: 'row', + justifyContent: 'space-between', + padding: 12, + borderRadius: 10, + borderWidth: 1, + borderColor: '#ddd', + marginBottom: 10, + backgroundColor: '#f9f9f9', + }, + selectedFriend: { + backgroundColor: '#e0f0ff', + borderColor: '#007AFF', + }, + friendName: { + fontSize: 16, + }, + createBtn: { + backgroundColor: '#007AFF', + padding: 12, + borderRadius: 10, + alignItems: 'center', + marginTop: 10, + }, + createBtnText: { + color: 'white', + fontWeight: 'bold', + }, + closeBtn: { + alignItems: 'center', + marginTop: 10, + }, + closeBtnText: { + color: '#666', + }, +}); diff --git a/ClientApp/services/chatService.ts b/ClientApp/services/chatService.ts index 59dca6914..96804086a 100644 --- a/ClientApp/services/chatService.ts +++ b/ClientApp/services/chatService.ts @@ -43,17 +43,28 @@ export const getMessages = async (chatroomId: string) => { }; // API_ENDPOINTS.CREATE_CHATROOM -export const createUserChatroom = async (createrId: string, chatWithUserId: string, nameOfChatRoom: string, messages: string[], isEvent: boolean, unread: boolean) => { +export const createUserChatroom = async (createrId: string, chatWithUserId: string, nameOfChatRoom: string, messages: string[], isEvent: boolean, unread: boolean, creatorUsername: string, participantUsername: string) => { try { const axiosLocalInstance = getAxiosInstance(); const response = await axiosLocalInstance.post(API_ENDPOINTS.CREATE_CHATROOM, { createdBy: createrId, chatroomName: nameOfChatRoom, - members : [createrId, chatWithUserId], - messages, - isEvent, - unread + members : [ + { + userId: createrId, + username: creatorUsername, + userImage: null + }, + { + userId: chatWithUserId, + username: participantUsername, + userImage: null + } + ], + messages: messages, + isEvent: isEvent, + unread: unread } ); console.log("createUserChatroom response:", response.data); From a358aa3c7425df852cf8c376f44d913b6da92440 Mon Sep 17 00:00:00 2001 From: Joud Babik Date: Sat, 29 Mar 2025 11:01:13 -0400 Subject: [PATCH 12/28] GH-365: [feat] Customize the list of friends inside the modal to match the api call --- ClientApp/components/chat/ChatListHeader.tsx | 5 +- ClientApp/components/chat/NewChatModal.tsx | 105 ++++++++++++++----- ClientApp/services/chatService.ts | 57 +++++++--- 3 files changed, 129 insertions(+), 38 deletions(-) diff --git a/ClientApp/components/chat/ChatListHeader.tsx b/ClientApp/components/chat/ChatListHeader.tsx index 2862da7db..3c202ca11 100644 --- a/ClientApp/components/chat/ChatListHeader.tsx +++ b/ClientApp/components/chat/ChatListHeader.tsx @@ -40,7 +40,7 @@ const ChatListHeader: React.FC = () => { setModalVisible(true); fetchFriends(); }; - const handleCreateGroup = async() => { + const handleCreateGroupButton = async() => { // Call your API or navigate to group chat screen }; @@ -53,9 +53,10 @@ const ChatListHeader: React.FC = () => { setModalVisible(false)} - onCreateGroup={handleCreateGroup} + onCreateGroup={handleCreateGroupButton} /> ); diff --git a/ClientApp/components/chat/NewChatModal.tsx b/ClientApp/components/chat/NewChatModal.tsx index 8226db5ed..6983ea7d8 100644 --- a/ClientApp/components/chat/NewChatModal.tsx +++ b/ClientApp/components/chat/NewChatModal.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; -import {Modal,View,Text,FlatList,TouchableOpacity,StyleSheet,Pressable,} from 'react-native'; +import {Modal,View,Text,FlatList,TouchableOpacity,StyleSheet,Pressable,Image} from 'react-native'; import FontAwesome from '@expo/vector-icons/FontAwesome'; +import { hs, mhs, mvs, vs } from '@/utils/helpers/uiScaler'; const sampleFriends = [ { id: '1', name: 'User1' }, @@ -10,32 +11,44 @@ const sampleFriends = [ ]; interface NewChatModalProps { + friends: any []; visible: boolean; onClose: () => void; onCreateGroup: (selected: typeof sampleFriends) => void; } const NewChatModal: React.FC = ({ + friends, visible, onClose, onCreateGroup, }) => { - const [selected, setSelected] = useState([]); + const [selected, setSelected] = useState<{ userId: string; username: string; userImage: string }[]>([]); + console.log("Selected friends:", selected); + const toggleSelect = (item: any) => { + const exists = selected.find((u) => u.userId === item.friendUserId); + + if (exists) { + setSelected((prev) => prev.filter((u) => u.userId !== item.friendUserId)); + } else { + const newUser = { + userId: item.friendUserId, + username: item.friendUsername, + userImage: item.profile.profileImage || '', // use fallback if needed + }; + setSelected((prev) => [...prev, newUser]); + } + }; - const toggleSelect = (id: string) => { - setSelected((prev) => - prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] - ); - }; - - const handleCreate = () => { - const selectedFriends = sampleFriends.filter((f) => - selected.includes(f.id) - ); - onCreateGroup(selectedFriends); - onClose(); - setSelected([]); - }; + const handleCreate = () => { + const selectedFriends = friends.filter((f) => + selected.includes(f.id) + ); + console.log("Selected friends:", selectedFriends); + onCreateGroup(selectedFriends); + onClose(); + setSelected([]); + }; return ( @@ -43,19 +56,27 @@ const NewChatModal: React.FC = ({ Select Friends item.id} + data={friends} + keyExtractor={(item) => item.friendUserId} // fix: use actual unique friend ID renderItem={({ item }) => { - const isSelected = selected.includes(item.id); - return ( + const isSelected = selected.some((u) => u.userId === item.friendUserId); + return ( toggleSelect(item.id)} - > - {item.name} + onPress={() => toggleSelect(item)} > + + + + {item.friendUsername } + + {isSelected && ( )} @@ -110,13 +131,19 @@ const styles = StyleSheet.create({ marginBottom: 10, backgroundColor: '#f9f9f9', }, + pictureSection: { + flex: 1, + justifyContent: "center", + alignItems: "flex-start", + }, selectedFriend: { backgroundColor: '#e0f0ff', borderColor: '#007AFF', }, friendName: { fontSize: 16, - }, + color: '#333', +}, createBtn: { backgroundColor: '#007AFF', padding: 12, @@ -135,4 +162,34 @@ const styles = StyleSheet.create({ closeBtnText: { color: '#666', }, + participantAvatar: { + width: mhs(45), + height: mvs(45), + borderRadius: mhs(30), +}, +infoSection: { + display: "flex", + flex: 3, + flexDirection: "column", + justifyContent: "center", + alignItems: "flex-start", + marginLeft: vs(16), + }, + userInfo: { + fontSize: 14, + fontWeight: "bold", + marginBottom: hs(5), + color: "#333", + }, + friendInfo: { + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + avatar: { + width: 40, + height: 40, + borderRadius: 20, + marginRight: 12, + } }); diff --git a/ClientApp/services/chatService.ts b/ClientApp/services/chatService.ts index 96804086a..4782a83c4 100644 --- a/ClientApp/services/chatService.ts +++ b/ClientApp/services/chatService.ts @@ -50,18 +50,7 @@ export const createUserChatroom = async (createrId: string, chatWithUserId: stri { createdBy: createrId, chatroomName: nameOfChatRoom, - members : [ - { - userId: createrId, - username: creatorUsername, - userImage: null - }, - { - userId: chatWithUserId, - username: participantUsername, - userImage: null - } - ], + members : [ createrId, chatWithUserId], messages: messages, isEvent: isEvent, unread: unread @@ -75,6 +64,50 @@ export const createUserChatroom = async (createrId: string, chatWithUserId: stri } } +export const createUserChatroomV2 = async ( + creatorId: string, + participantIds: string[], + nameOfChatRoom: string, + messages: string[], + isEvent: boolean, + unread: boolean, + usernames: string[], + userImages: string[], + creatorUsername: string, + creatorImage: string +) => { + try { + const axiosLocalInstance = getAxiosInstance(); + + const members = [ + { + userId: creatorId, + username: creatorUsername, + userImage: creatorImage, + }, + ...participantIds.map((id, index) => ({ + userId: id, + username: usernames[index], + userImage: userImages[index], + })), + ]; + + const response = await axiosLocalInstance.post(API_ENDPOINTS.CREATE_CHATROOM, { + createdBy: creatorId, + chatroomName: nameOfChatRoom, + members: members, + messages: messages, + isEvent: isEvent, + unread: unread, + }); + + console.log("createUserChatroom response:", response.data); + return response.data; + } catch (error) { + console.error('Error creating chatroom:', error); + throw error; + } +}; // API_ENDPOINTS.DELETE_CHATROOM From 859ad1ec9095d03d29ecb921c1b0dabb2670c1b9 Mon Sep 17 00:00:00 2001 From: Joud Babik Date: Sun, 30 Mar 2025 19:56:13 -0400 Subject: [PATCH 13/28] GH-365: [feat] starter code for group/user chat creation --- ClientApp/app/(tabs)/chats/[id].tsx | 137 ++++++++++++++++-- .../components/Profile/ProfileSection.tsx | 5 +- ClientApp/components/chat/ChatList.tsx | 7 +- ClientApp/components/chat/ChatListHeader.tsx | 5 - ClientApp/components/chat/NewChatModal.tsx | 36 +++-- ClientApp/services/chatService.ts | 83 +++++++---- 6 files changed, 206 insertions(+), 67 deletions(-) diff --git a/ClientApp/app/(tabs)/chats/[id].tsx b/ClientApp/app/(tabs)/chats/[id].tsx index cf2e5ec61..2d7ef8bfc 100644 --- a/ClientApp/app/(tabs)/chats/[id].tsx +++ b/ClientApp/app/(tabs)/chats/[id].tsx @@ -1,12 +1,13 @@ import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import { StyleSheet } from 'react-native'; +import { Button, Modal, StyleSheet, View, Text } from 'react-native'; import { GiftedChat, IMessage } from 'react-native-gifted-chat'; import {useSelector} from "react-redux"; import {getMessages, getChatroom} from "@/services/chatService"; import { Client } from "@stomp/stompjs"; import { getAccessToken } from "@/services/tokenService" import {message, chatroomProps, messageRequest} from "@/types/messaging"; +import { mvs } from '@/utils/helpers/uiScaler'; const ChatScreen = () => { @@ -17,9 +18,15 @@ const ChatScreen = () => { const user = useSelector((state: {user: any}) => state.user); const [connected, setConnected] = useState(false); const clientRef = useRef(null); + const [chatroom, setChatroom] = useState(); + // TODO This will be a duplicate of the chatroom state + const [chatroomInformation, setChatroomInformation] = useState(); + const navigation = useNavigation(); + const [infoVisible, setInfoVisible] = useState(false); + // use effect to control the bottom tab bar visibility of the part "chat" useEffect(() => { // Hide tab bar when this screen is focused const parent = navigation.getParent(); @@ -29,18 +36,27 @@ const ChatScreen = () => { } }); - // Restore it when unmounted return () => { parent?.setOptions({ - tabBarStyle: undefined + tabBarStyle: { + paddingHorizontal: 10, + height: mvs(65), + }, }); }; }, []); + + useEffect(() => { if (title && typeof title === 'string') { - navigation.setOptions({ headerTitle: title }); + navigation.setOptions({ + headerTitle: title, + headerRight: () => ( +