Improved functionlity of messages page

This commit is contained in:
Keannu Bernasol 2023-10-01 00:54:31 +08:00
parent 63f863fa1e
commit 2cd770e5e1
7 changed files with 414 additions and 96 deletions

9
package-lock.json generated
View file

@ -22,6 +22,7 @@
"expo-linking": "~4.0.1", "expo-linking": "~4.0.1",
"expo-location": "~15.1.1", "expo-location": "~15.1.1",
"expo-status-bar": "~1.4.4", "expo-status-bar": "~1.4.4",
"moment": "^2.29.4",
"moti": "^0.25.3", "moti": "^0.25.3",
"react": "18.2.0", "react": "18.2.0",
"react-native": "0.71.8", "react-native": "0.71.8",
@ -11123,6 +11124,14 @@
"mkdirp": "bin/cmd.js" "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": { "node_modules/moti": {
"version": "0.25.3", "version": "0.25.3",
"resolved": "https://registry.npmjs.org/moti/-/moti-0.25.3.tgz", "resolved": "https://registry.npmjs.org/moti/-/moti-0.25.3.tgz",

View file

@ -23,6 +23,7 @@
"expo-linking": "~4.0.1", "expo-linking": "~4.0.1",
"expo-location": "~15.1.1", "expo-location": "~15.1.1",
"expo-status-bar": "~1.4.4", "expo-status-bar": "~1.4.4",
"moment": "^2.29.4",
"moti": "^0.25.3", "moti": "^0.25.3",
"react": "18.2.0", "react": "18.2.0",
"react-native": "0.71.8", "react-native": "0.71.8",

View file

@ -4,6 +4,7 @@ import {
ActivationType, ActivationType,
LocationType, LocationType,
LoginType, LoginType,
MessagePostType,
OnboardingType, OnboardingType,
PatchUserInfoType, PatchUserInfoType,
RegistrationType, RegistrationType,
@ -311,7 +312,6 @@ export async function GetStudyGroupListFiltered() {
return instance return instance
.get("/api/v1/study_groups/near/", config) .get("/api/v1/study_groups/near/", config)
.then((response) => { .then((response) => {
console.log("DEBUGGG", response.data);
return [true, response.data]; return [true, response.data];
}) })
.catch((error) => { .catch((error) => {
@ -346,3 +346,56 @@ export async function CreateStudyGroup(info: StudyGroupCreateType) {
return [false, error_message]; 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];
});
}

View file

@ -185,13 +185,31 @@ export interface StudyGroupCreateType {
subject: string; 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 StudyGroupReturnType = [boolean, StudyGroupType[]];
export type StudentStatusReturnType = [boolean, StudentStatusType]; export type StudentStatusReturnType = [boolean, StudentStatusType];
export type StudentStatusListType = Array<StudentStatusFilterType>; export type StudentStatusListType = Array<StudentStatusFilterType>;
export type StudentStatusListReturnType = [boolean, StudentStatusListType]; export type StudentStatusListReturnType = [boolean, StudentStatusListType];
export type RawLocationType = Location.LocationObject; export type RawLocationType = Location.LocationObject;
export interface UserInfoType { export interface UserInfoType {

View file

@ -1,10 +1,38 @@
import * as React from "react"; import * as React from "react";
import { ActivityIndicator, Image } from "react-native";
import styles from "../../styles"; 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 { colors } from "../../styles";
import { useState } from "react"; import { useRef, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
const convStyles = StyleSheet.create({}); 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 = { type ConversationType = {
id: number; id: number;
@ -15,91 +43,293 @@ type ConversationType = {
}; };
export default function ConversationPage() { export default function ConversationPage() {
const [conversation, setConversation] = useState<ConversationType[]>([ const toast = useToast();
{ // Student Status
user: "You", const [student_status, setStudentStatus] = useState<StudentStatusType>();
message_content: "Hello World naa ko diri canteen gutom sh*t.", const StudentStatusQuery = useQuery({
id: Math.floor(Math.random() * 1000), queryKey: ["user_status"],
color: Math.floor(Math.random() * 16777215).toString(16), queryFn: async () => {
study_group: "Heh group", const data = await GetStudentStatus();
if (data[0] == false) {
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
}, },
{ onSuccess: (data: StudentStatusReturnType) => {
user: "User 2", setStudentStatus(data[1]);
message_content: "Hahahah shor oy.",
id: Math.floor(Math.random() * 1000),
color: Math.floor(Math.random() * 16777215).toString(16),
study_group: "Heh group",
}, },
]); onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
return ( // Study Group Detail
<ScrollView style={styles.messageScrollViewContainer}> const [studygroup, setStudyGroup] = useState<StudyGroupType>();
<View const StudyGroupQuery = useQuery({
style={{ enabled:
display: "flex", student_status?.study_group != "" && student_status?.study_group != null,
backgroundColor: colors.secondary_2, queryKey: ["study_group"],
borderRadius: 20, queryFn: async () => {
}} const data = await GetStudyGroup(student_status?.study_group || "");
> if (data[0] == false) {
<View style={{ padding: 15 }}> return Promise.reject(new Error(JSON.stringify(data[1])));
<View style={{ flexDirection: "row" }}> }
<Text style={{ ...styles.text_white_medium }}>Group#57605</Text> return data;
</View> },
<Text> onSuccess: (data: StudyGroupDetailReturnType) => {
3 students if (data[1]) {
<View style={{ ...styles.badge, backgroundColor: "blue" }}></View> setStudyGroup(data[1]);
<View style={{ ...styles.badge, backgroundColor: `green` }}></View> }
<View style={{ ...styles.badge, backgroundColor: `red` }}></View> },
</Text> onError: (error: Error) => {
</View> toast.show(String(error), {
<View> type: "warning",
{conversation.map((item: ConversationType, index: number) => { placement: "top",
const color = `rgba(${Math.floor( duration: 2000,
Math.random() * 256 animationType: "slide-in",
)}, ${Math.floor(Math.random() * 256)}, ${Math.floor( });
Math.random() * 256 },
)}, 0.7)`; });
return (
<View // Study Group Messages
key={item.id} const [messages, setMessages] = useState<MessageType[]>([]);
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<GroupMessageAvatarType[]>([]);
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<ScrollView>(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 (
<View style={styles.background}>
<AnimatedContainer>
<View
style={{
padding: 15,
alignSelf: "flex-start",
}}
>
<View style={styles.flex_row}>
<Text style={{ ...styles.text_white_medium }}>
{`Group: ${studygroup?.name ? studygroup.name : ""}`}
</Text>
</View>
<View style={{ ...styles.flex_row }}>
<Text
style={{ style={{
...styles.message_contentContainer, ...styles.text_white_small,
alignItems: index % 2 == 0 ? "flex-end" : "flex-start", textAlign: "left",
paddingRight: 4,
}} }}
> >
<View style={styles.flex_row}> {studygroup.students.length} studying
{index % 2 == 0 ? ( </Text>
<View {users.map((user: GroupMessageAvatarType, index: number) => {
style={{ if (index > 6) {
...styles.badge, return <React.Fragment key={index} />;
...{ paddingRight: 2, backgroundColor: color }, }
}} return (
/> <React.Fragment key={index}>
) : ( {user.avatar != null && user.avatar != "" ? (
<View <Image
style={{ source={{ uri: user.avatar }}
...styles.badge, style={styles.profile_mini}
...{ paddingLeft: 2, backgroundColor: color }, />
}} ) : (
/> <Image
)} source={require("../../img/user_profile_placeholder.png")}
<Text style={styles.text_white_small}>{item.user}</Text> style={styles.profile_mini}
</View> />
)}
</React.Fragment>
);
})}
</View>
</View>
<ScrollView
style={{ width: 320 }}
onContentSizeChange={() => 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 (
<View
key={message.id}
style={{
...styles.message_contentContainer,
alignItems: index % 2 == 0 ? "flex-end" : "flex-start",
}}
>
<View style={styles.flex_row}>
{avatar != null && avatar != "" ? (
<Image
source={{ uri: avatar }}
style={styles.profile_mini}
/>
) : (
<Image
source={require("../../img/user_profile_placeholder.png")}
style={styles.profile_mini}
/>
)}
<Text style={styles.text_white_small}> <Text style={styles.text_white_small}>
{item.message_content} {message.user}
</Text> </Text>
</View> <Text
); style={{
})} ...styles.text_white_tiny,
</View> ...{ marginLeft: 4, alignContent: "center" },
}}
>
{message.timestamp}
</Text>
</View>
<Text style={styles.text_white_small}>
{message.message_content}
</Text>
</View>
);
})
) : (
<Text style={styles.text_white_small}>There are no messages</Text>
)}
</ScrollView>
<TextInput
style={styles.chatbox}
placeholder="Send a message..."
placeholderTextColor="white"
value={message}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setMessage(e.nativeEvent.text);
}}
onSubmitEditing={() => {
send_message.mutate({
message_content: message,
});
setMessage("");
}}
/>
</AnimatedContainer>
</View> </View>
<TextInput );
style={styles.chatbox} } else if (!student_status?.study_group) {
placeholder="type here...." return (
placeholderTextColor="white" <View style={styles.background}>
autoCapitalize="none" <AnimatedContainer>
/> <Text style={styles.text_white_medium}>
</ScrollView> You are not in a study group. Join one to start a conversation!
</Text>
</AnimatedContainer>
</View>
);
}
return (
<View style={styles.background}>
<AnimatedContainer>
<ActivityIndicator size={96} color={colors.secondary_1} />
<Text style={styles.text_white_medium}>Loading...</Text>
</AnimatedContainer>
</View>
); );
} }

View file

@ -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 // Refresh when screen loads
useEffect(() => { useEffect(() => {
// Refresh every 15 seconds
const interval = setInterval(async () => {
await requestLocation();
}, 15000);
requestLocation(); requestLocation();
return () => clearInterval(interval);
}, []); }, []);
async function DistanceHandler(location: RawLocationType) { async function DistanceHandler(location: RawLocationType) {
@ -829,7 +825,7 @@ export default function Home() {
return ( return (
<> <>
<Text style={styles.text_white_medium}>{feedback}</Text> <Text style={styles.text_white_medium}>{feedback}</Text>
<Button onPress={() => requestLocation()}> <Button onPress={async () => await requestLocation()}>
<Text style={styles.text_white_medium}>Allow Access</Text> <Text style={styles.text_white_medium}>Allow Access</Text>
</Button> </Button>
</> </>

View file

@ -186,6 +186,16 @@ const styles = StyleSheet.create({
borderColor: colors.primary_2, borderColor: colors.primary_2,
borderWidth: 3, borderWidth: 3,
}, },
profile_mini: {
height: 32,
width: 32,
alignSelf: "center",
borderRadius: 150 / 2,
overflow: "hidden",
padding: 0,
borderColor: colors.primary_2,
borderWidth: 3,
},
input: { input: {
paddingHorizontal: 8, paddingHorizontal: 8,
marginVertical: 2, marginVertical: 2,
@ -212,6 +222,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.primary_2, backgroundColor: colors.primary_2,
borderRadius: 20, borderRadius: 20,
borderColor: colors.primary_3, borderColor: colors.primary_3,
width: 256,
}, },
messageScrollViewContainer: { messageScrollViewContainer: {
backgroundColor: colors.secondary_1, backgroundColor: colors.secondary_1,