diff --git a/package-lock.json b/package-lock.json index e918be3..c490eda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "expo-linking": "~4.0.1", "expo-location": "~15.1.1", "expo-status-bar": "~1.4.4", + "moment": "^2.29.4", "moti": "^0.25.3", "react": "18.2.0", "react-native": "0.71.8", @@ -11123,6 +11124,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/moti": { "version": "0.25.3", "resolved": "https://registry.npmjs.org/moti/-/moti-0.25.3.tgz", diff --git a/package.json b/package.json index df97e93..5b3b404 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "expo-linking": "~4.0.1", "expo-location": "~15.1.1", "expo-status-bar": "~1.4.4", + "moment": "^2.29.4", "moti": "^0.25.3", "react": "18.2.0", "react-native": "0.71.8", diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 0eaad7d..e6111a9 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -4,6 +4,7 @@ import { ActivationType, LocationType, LoginType, + MessagePostType, OnboardingType, PatchUserInfoType, RegistrationType, @@ -311,7 +312,6 @@ export async function GetStudyGroupListFiltered() { return instance .get("/api/v1/study_groups/near/", config) .then((response) => { - console.log("DEBUGGG", response.data); return [true, response.data]; }) .catch((error) => { @@ -346,3 +346,56 @@ export async function CreateStudyGroup(info: StudyGroupCreateType) { return [false, error_message]; }); } + +export async function GetStudyGroup(name: string) { + const config = await GetConfig(); + return instance + .get(`/api/v1/study_groups/${name}`, config) + .then((response) => { + return [true, response.data]; + }) + .catch((error) => { + let error_message = ParseError(error); + return [false, error_message]; + }); +} + +export async function GetStudyGroupMessages() { + const config = await GetConfig(); + return instance + .get(`/api/v1/messages/`, config) + .then((response) => { + return [true, response.data]; + }) + .catch((error) => { + let error_message = ParseError(error); + return [false, error_message]; + }); +} + +export async function GetStudyGroupMemberAvatars() { + const config = await GetConfig(); + return instance + .get(`/api/v1/study_groups/member_avatars`, config) + .then((response) => { + return [true, response.data]; + }) + .catch((error) => { + let error_message = ParseError(error); + return [false, error_message]; + }); +} + +export async function PostMessage(info: MessagePostType) { + const config = await GetConfig(); + return instance + .post(`/api/v1/messages/`, info, config) + .then((response) => { + return [true, response.data]; + }) + .catch((error) => { + console.log("Error:", error.response.data); + let error_message = ParseError(error); + return [false, error_message]; + }); +} diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index ae61d63..717dc4d 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -185,13 +185,31 @@ export interface StudyGroupCreateType { subject: string; } +export interface MessageType { + id: number; + user: string; + study_group: string; + message_content: string; + timestamp: string; +} + +export interface MessagePostType { + message_content: string; +} + +export interface GroupMessageAvatarType { + username: string; + avatar: string; +} + +export type GroupMessageAvatarListType = GroupMessageAvatarType[]; +export type GroupMessageAvatarReturnType = [boolean, GroupMessageAvatarType[]]; +export type MessageReturnType = [boolean, MessageType[]]; +export type StudyGroupDetailReturnType = [boolean, StudyGroupType]; export type StudyGroupReturnType = [boolean, StudyGroupType[]]; - export type StudentStatusReturnType = [boolean, StudentStatusType]; - export type StudentStatusListType = Array; export type StudentStatusListReturnType = [boolean, StudentStatusListType]; - export type RawLocationType = Location.LocationObject; export interface UserInfoType { diff --git a/src/routes/ConversationPage/ConversationPage.tsx b/src/routes/ConversationPage/ConversationPage.tsx index 6c00b9b..e7ce17c 100644 --- a/src/routes/ConversationPage/ConversationPage.tsx +++ b/src/routes/ConversationPage/ConversationPage.tsx @@ -1,10 +1,38 @@ import * as React from "react"; +import { ActivityIndicator, Image } from "react-native"; import styles from "../../styles"; -import { View, Text, TextInput, ScrollView, StyleSheet } from "react-native"; +import { + View, + Text, + TextInput, + ScrollView, + NativeSyntheticEvent, + TextInputChangeEventData, +} from "react-native"; import { colors } from "../../styles"; -import { useState } from "react"; - -const convStyles = StyleSheet.create({}); +import { useRef, useState } from "react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { + GetStudentStatus, + GetStudyGroup, + GetStudyGroupMemberAvatars, + GetStudyGroupMessages, + PostMessage, +} from "../../components/Api/Api"; +import { + StudentStatusType, + StudentStatusReturnType, + StudyGroupType, + StudyGroupDetailReturnType, + MessageType, + MessageReturnType, + MessagePostType, + GroupMessageAvatarType, + GroupMessageAvatarReturnType, +} from "../../interfaces/Interfaces"; +import { useToast } from "react-native-toast-notifications"; +import { useQueryClient } from "@tanstack/react-query"; +import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer"; type ConversationType = { id: number; @@ -15,91 +43,293 @@ type ConversationType = { }; export default function ConversationPage() { - const [conversation, setConversation] = useState([ - { - user: "You", - message_content: "Hello World naa ko diri canteen gutom sh*t.", - id: Math.floor(Math.random() * 1000), - color: Math.floor(Math.random() * 16777215).toString(16), - study_group: "Heh group", + const toast = useToast(); + // Student Status + const [student_status, setStudentStatus] = useState(); + const StudentStatusQuery = useQuery({ + queryKey: ["user_status"], + queryFn: async () => { + const data = await GetStudentStatus(); + if (data[0] == false) { + return Promise.reject(new Error(JSON.stringify(data[1]))); + } + return data; }, - { - user: "User 2", - message_content: "Hahahah shor oy.", - id: Math.floor(Math.random() * 1000), - color: Math.floor(Math.random() * 16777215).toString(16), - study_group: "Heh group", + onSuccess: (data: StudentStatusReturnType) => { + setStudentStatus(data[1]); }, - ]); + onError: (error: Error) => { + toast.show(String(error), { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); + }, + }); - return ( - - - - - Group#57605 - - - 3 students - - - - - - - {conversation.map((item: ConversationType, index: number) => { - const color = `rgba(${Math.floor( - Math.random() * 256 - )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( - Math.random() * 256 - )}, 0.7)`; - return ( - (); + const StudyGroupQuery = useQuery({ + enabled: + student_status?.study_group != "" && student_status?.study_group != null, + queryKey: ["study_group"], + queryFn: async () => { + const data = await GetStudyGroup(student_status?.study_group || ""); + if (data[0] == false) { + return Promise.reject(new Error(JSON.stringify(data[1]))); + } + return data; + }, + onSuccess: (data: StudyGroupDetailReturnType) => { + if (data[1]) { + setStudyGroup(data[1]); + } + }, + onError: (error: Error) => { + toast.show(String(error), { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); + }, + }); + + // Study Group Messages + const [messages, setMessages] = useState([]); + const MessageQuery = useQuery({ + refetchInterval: 3000, + enabled: + !StudentStatusQuery.isLoading && + (student_status?.study_group != null || + student_status?.study_group != ""), + queryKey: ["study_group_messages"], + queryFn: async () => { + const data = await GetStudyGroupMessages(); + if (data[0] == false) { + return Promise.reject(new Error(JSON.stringify(data[1]))); + } + return data; + }, + onSuccess: (data: MessageReturnType) => { + if (data[1]) { + setMessages(data[1]); + } + }, + onError: (error: Error) => { + toast.show(String(error), { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); + }, + }); + + // Avatar List + const [users, setUsers] = useState([]); + const AvatarsQuery = useQuery({ + refetchInterval: 3000, + enabled: + student_status?.study_group != null || + (student_status?.study_group != "" && + studygroup != null && + studygroup.students != null), + queryKey: ["study_group_avatars"], + queryFn: async () => { + const data = await GetStudyGroupMemberAvatars(); + if (data[0] == false) { + return Promise.reject(new Error(JSON.stringify(data[1]))); + } + return data; + }, + onSuccess: (data: GroupMessageAvatarReturnType) => { + if (data[1]) { + setUsers(data[1]); + } + }, + onError: (error: Error) => { + toast.show(String(error), { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); + }, + }); + const scrollViewRef = useRef(null); + const queryClient = useQueryClient(); + const [message, setMessage] = useState(""); + const send_message = useMutation({ + mutationFn: async (info: MessagePostType) => { + const data = await PostMessage(info); + if (data[0] != true) { + return Promise.reject(new Error()); + } + return data; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["study_group_messages"] }); + }, + onError: (error: Error) => { + toast.show(String(error), { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); + }, + }); + + if ( + !StudyGroupQuery.isLoading && + !AvatarsQuery.isLoading && + !MessageQuery.isLoading && + student_status && + studygroup && + studygroup.students + ) { + return ( + + + + + + {`Group: ${studygroup?.name ? studygroup.name : ""}`} + + + + - - {index % 2 == 0 ? ( - - ) : ( - - )} - {item.user} - + {studygroup.students.length} studying + + {users.map((user: GroupMessageAvatarType, index: number) => { + if (index > 6) { + return ; + } + return ( + + {user.avatar != null && user.avatar != "" ? ( + + ) : ( + + )} + + ); + })} + + + scrollViewRef.current?.scrollToEnd()} + ref={scrollViewRef} + > + {messages.length > 0 ? ( + messages.map((message: MessageType, index: number) => { + let avatar = ""; + users.filter((user: GroupMessageAvatarType) => { + if (user.username == message.user) { + avatar = user.avatar; + } + }); + return ( + + + {avatar != null && avatar != "" ? ( + + ) : ( + + )} - - {item.message_content} - - - ); - })} - + + {message.user} + + + {message.timestamp} + + + + + {message.message_content} + + + ); + }) + ) : ( + There are no messages + )} + + + ): void => { + setMessage(e.nativeEvent.text); + }} + onSubmitEditing={() => { + send_message.mutate({ + message_content: message, + }); + setMessage(""); + }} + /> + - - + ); + } else if (!student_status?.study_group) { + return ( + + + + You are not in a study group. Join one to start a conversation! + + + + ); + } + return ( + + + + Loading... + + ); } diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 3f72156..c562fa5 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -82,18 +82,14 @@ export default function Home() { } } - // Refresh every 15 seconds - useEffect(() => { - const interval = setInterval(() => { - requestLocation(); - }, 15000); - - return () => clearInterval(interval); - }); - // Refresh when screen loads useEffect(() => { + // Refresh every 15 seconds + const interval = setInterval(async () => { + await requestLocation(); + }, 15000); requestLocation(); + return () => clearInterval(interval); }, []); async function DistanceHandler(location: RawLocationType) { @@ -829,7 +825,7 @@ export default function Home() { return ( <> {feedback} - diff --git a/src/styles.tsx b/src/styles.tsx index 8e8f1cf..af0d3fa 100644 --- a/src/styles.tsx +++ b/src/styles.tsx @@ -186,6 +186,16 @@ const styles = StyleSheet.create({ borderColor: colors.primary_2, borderWidth: 3, }, + profile_mini: { + height: 32, + width: 32, + alignSelf: "center", + borderRadius: 150 / 2, + overflow: "hidden", + padding: 0, + borderColor: colors.primary_2, + borderWidth: 3, + }, input: { paddingHorizontal: 8, marginVertical: 2, @@ -212,6 +222,7 @@ const styles = StyleSheet.create({ backgroundColor: colors.primary_2, borderRadius: 20, borderColor: colors.primary_3, + width: 256, }, messageScrollViewContainer: { backgroundColor: colors.secondary_1,