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-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",

View file

@ -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",

View file

@ -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];
});
}

View file

@ -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<StudentStatusFilterType>;
export type StudentStatusListReturnType = [boolean, StudentStatusListType];
export type RawLocationType = Location.LocationObject;
export interface UserInfoType {

View file

@ -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<ConversationType[]>([
{
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<StudentStatusType>();
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",
});
},
});
// Study Group Detail
const [studygroup, setStudyGroup] = useState<StudyGroupType>();
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<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 (
<ScrollView style={styles.messageScrollViewContainer}>
<View style={styles.background}>
<AnimatedContainer>
<View
style={{
display: "flex",
backgroundColor: colors.secondary_2,
borderRadius: 20,
padding: 15,
alignSelf: "flex-start",
}}
>
<View style={{ padding: 15 }}>
<View style={{ flexDirection: "row" }}>
<Text style={{ ...styles.text_white_medium }}>Group#57605</Text>
</View>
<Text>
3 students
<View style={{ ...styles.badge, backgroundColor: "blue" }}></View>
<View style={{ ...styles.badge, backgroundColor: `green` }}></View>
<View style={{ ...styles.badge, backgroundColor: `red` }}></View>
<View style={styles.flex_row}>
<Text style={{ ...styles.text_white_medium }}>
{`Group: ${studygroup?.name ? studygroup.name : ""}`}
</Text>
</View>
<View>
{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)`;
<View style={{ ...styles.flex_row }}>
<Text
style={{
...styles.text_white_small,
textAlign: "left",
paddingRight: 4,
}}
>
{studygroup.students.length} studying
</Text>
{users.map((user: GroupMessageAvatarType, index: number) => {
if (index > 6) {
return <React.Fragment key={index} />;
}
return (
<React.Fragment key={index}>
{user.avatar != null && user.avatar != "" ? (
<Image
source={{ uri: user.avatar }}
style={styles.profile_mini}
/>
) : (
<Image
source={require("../../img/user_profile_placeholder.png")}
style={styles.profile_mini}
/>
)}
</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={item.id}
key={message.id}
style={{
...styles.message_contentContainer,
alignItems: index % 2 == 0 ? "flex-end" : "flex-start",
}}
>
<View style={styles.flex_row}>
{index % 2 == 0 ? (
<View
style={{
...styles.badge,
...{ paddingRight: 2, backgroundColor: color },
}}
{avatar != null && avatar != "" ? (
<Image
source={{ uri: avatar }}
style={styles.profile_mini}
/>
) : (
<View
style={{
...styles.badge,
...{ paddingLeft: 2, backgroundColor: color },
}}
<Image
source={require("../../img/user_profile_placeholder.png")}
style={styles.profile_mini}
/>
)}
<Text style={styles.text_white_small}>{item.user}</Text>
<Text style={styles.text_white_small}>
{message.user}
</Text>
<Text
style={{
...styles.text_white_tiny,
...{ marginLeft: 4, alignContent: "center" },
}}
>
{message.timestamp}
</Text>
</View>
<Text style={styles.text_white_small}>
{item.message_content}
{message.message_content}
</Text>
</View>
);
})}
</View>
</View>
})
) : (
<Text style={styles.text_white_small}>There are no messages</Text>
)}
</ScrollView>
<TextInput
style={styles.chatbox}
placeholder="type here...."
placeholder="Send a message..."
placeholderTextColor="white"
autoCapitalize="none"
value={message}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setMessage(e.nativeEvent.text);
}}
onSubmitEditing={() => {
send_message.mutate({
message_content: message,
});
setMessage("");
}}
/>
</ScrollView>
</AnimatedContainer>
</View>
);
} else if (!student_status?.study_group) {
return (
<View style={styles.background}>
<AnimatedContainer>
<Text style={styles.text_white_medium}>
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
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 (
<>
<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>
</Button>
</>

View file

@ -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,