Compare commits

...

44 commits

Author SHA1 Message Date
0b3af716a5 limit studying to within 250m of the center of ustp 2023-12-10 22:50:53 +08:00
05cee78d31 Format with Prettier and turn off debug flag 2023-11-24 21:15:47 +08:00
cbd82a05f9 Added callout information for study groups when pressing on maps and improved responsiveness of the homepage map renderer 2023-11-24 21:14:33 +08:00
d2aecbd89c Turn off debug flags 2023-11-24 00:26:02 +08:00
51b7b24430 Show landmarks if it exists in homepage and in conversation page 2023-11-24 00:22:14 +08:00
c0a8a8efc8 Always refresh location when pressing refresh button 2023-11-23 23:29:27 +08:00
7cd549cad7 Refetch user location when refreshing and added code snippet to stop user from studying if they stray too far from where they set their studying locatio. Also allow modal to be shown even when not currently studying 2023-11-23 23:27:38 +08:00
Keannu Bernasol
0cad7458be
Merge pull request #16 from lemeow125/feature/qol-fixes
Feature/qol fixes
2023-10-28 09:42:34 +08:00
2603741aab Turn off debug flags and clear study group messages notification cache when switching study groups 2023-10-28 00:22:17 +08:00
5d7327ef26 Do not redirect to conversations page if leaving a group 2023-10-27 23:13:41 +08:00
856621fe06 Remove a redundant refresh on load as it prevented users from overriding their location 2023-10-27 22:16:23 +08:00
8a32d2b32c Fixed left and right caret icons 2023-10-27 22:06:52 +08:00
a65a3a84aa Added enter button to conversation page and fixed error not properly displaying when sending an invalid message. Also added refresh interval of 20 seconds to study group query in conversations page to automatically refresh students count 2023-10-27 22:03:46 +08:00
88d8ce05b8 Possible fix to race conditions between queries in homepage which resulted in laggy rerenders of map 2023-10-27 21:43:34 +08:00
bd42b5418e Allow register and create group page to be scrollable when onscreen keyboard is open 2023-10-27 21:16:43 +08:00
3891f12f5d Redirect to conversations page instead of homepage when joining or creating a study group 2023-10-27 20:52:49 +08:00
6564b52dc0 Bump expo version 2023-10-22 00:57:01 +08:00
946e455b83 Add spacing between entries in list view 2023-10-19 20:04:37 +08:00
e354335590 Move refresh button to the bottom alongside studying and group create buttons 2023-10-19 20:03:41 +08:00
a0d27aaa38 Convert onboarding dropdown menus into modals 2023-10-19 18:53:54 +08:00
f098db0dca Added create group button below start studying and increased refresh interval to 30 seconds 2023-10-19 18:46:54 +08:00
e501bc2c91 Hotfix on incorrect condition on login button 2023-10-15 12:57:44 +08:00
a11c9dff65 Homepage loading improvements 2023-10-15 12:35:53 +08:00
ecf62a1008 Homepage optimizations 2023-10-15 12:27:49 +08:00
b82b9d332f Prevent registers/logins multiple times when spamming buttons 2023-10-15 11:56:39 +08:00
be21689639 Added confirm password field 2023-10-15 11:54:48 +08:00
8867306bd0 Made button visually responsive on pressing 2023-10-15 11:44:31 +08:00
de33fe30fd Remvoe locationFetched variable as it seemed to cause performance issues with rerenders 2023-10-14 11:12:16 +08:00
22820e139e Move conditional statements in homepage 2023-10-14 10:51:28 +08:00
a11ff2ee6f Reverted query invalidation on mount in homepage as it was causing performance issues 2023-10-14 10:35:28 +08:00
4caf86c6be Fixed map elements not rendering on first open when on poor network conditions 2023-10-13 14:28:22 +08:00
8e5e0546df Align own user messages to the right and other user messages to the left 2023-10-13 14:06:04 +08:00
0c9f53b84d Added refresh button to homepage and made it possible to join and leave groups from list view 2023-10-13 14:01:11 +08:00
02eabd2b41 Fixed loading screen in start studying page 2023-10-13 13:56:05 +08:00
7b175c44df Added stop-gaps to help with refreshing queries on slower connections and increased refresh interval for homepage from 15 seconds to 10 seconds 2023-10-13 12:45:55 +08:00
e54fe893a0 Fixed UserInfo page dropdown menu clipping 2023-10-13 12:21:40 +08:00
963eaef628 Rounded edges for MapViews 2023-10-11 20:19:16 +08:00
4a406957b5 Added conversation icon 2023-10-11 20:05:01 +08:00
ec693a7bb6 Fixed loading indicator container size 2023-10-11 20:01:22 +08:00
f9c3a5c5d4 Added possible fix to allow permissions prompt being shown briefly in homepage 2023-10-11 19:50:02 +08:00
369a00a0b3 Added loading pages inidcators to subject and user info page and redirect login page to homepage if user is already logged in 2023-10-11 19:48:19 +08:00
6e63f86805 Added back button to create group and start studying pages and adding loading indicator for start studying 2023-10-11 19:43:59 +08:00
7ac6a6745f Added loading screen to homepage and proper rendering if location permission is denied 2023-10-11 19:33:02 +08:00
64cb7aabdd Fix google map overlay still rendering under open street map in homepage 2023-10-11 18:37:53 +08:00
19 changed files with 1027 additions and 1649 deletions

1403
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,7 @@
"moment": "^2.29.4",
"moti": "^0.25.3",
"react": "18.2.0",
"react-native": "0.71.13",
"react-native": "0.71.14",
"react-native-bouncy-checkbox": "^3.0.7",
"react-native-dropdown-picker": "^5.4.6",
"react-native-gesture-handler": "~2.9.0",

View file

@ -17,7 +17,10 @@ export default function Button({ disabled = false, ...props }: props) {
<Pressable
disabled={disabled}
onPress={props.onPress}
style={{ ...styles.button_template, ...{ backgroundColor: props.color } }}
style={({ pressed }) => [
styles.button_template,
{ backgroundColor: pressed ? colors.primary_2 : props.color },
]}
>
{props.children}
</Pressable>

View file

@ -24,6 +24,7 @@ import SubjectIcon from "../../icons/SubjectIcon/SubjectIcon";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { PatchStudentStatus } from "../Api/Api";
import { useToast } from "react-native-toast-notifications";
import MessageIcon from "../../icons/MessageIcon/MessageIcon";
export default function CustomDrawerContent(props: {}) {
const debug = false;
@ -136,13 +137,13 @@ export default function CustomDrawerContent(props: {}) {
navigation.navigate("Conversation");
}}
>
<SubjectIcon size={32} />
<MessageIcon size={32} />
<Text style={styles.text_white_medium}>Conversation</Text>
</DrawerButton>
<DrawerButton
onPress={async () => {
// We don't clear student statuses when logging out on debug
if (!debug) {
if (debug) {
queryClient.clear();
dispatch(logout());
await AsyncStorage.clear();

View file

@ -0,0 +1,17 @@
import * as React from "react";
import styles from "../../styles";
import { View, Text, ActivityIndicator } from "react-native";
import { colors } from "../../styles";
import AnimatedContainer from "../AnimatedContainer/AnimatedContainer";
export default function LoadingFeedback() {
return (
<View style={styles.background}>
<AnimatedContainer>
<View style={{ paddingVertical: 8 }} />
<ActivityIndicator size={128} color={colors.secondary_1} />
<Text style={styles.text_white_medium}>Loading...</Text>
</AnimatedContainer>
</View>
);
}

View file

@ -0,0 +1,28 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
export default function CaretRightIcon(props: IconProps) {
return (
<>
<Svg
height={props.size + "px"}
width={props.size + "px"}
viewBox="0 0 24 24"
stroke-width="2"
stroke={colors.icon_color}
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path
d="M13.883 5.007l.058 -.005h.118l.058 .005l.06 .009l.052 .01l.108 .032l.067 .027l.132 .07l.09 .065l.081 .073l.083 .094l.054 .077l.054 .096l.017 .036l.027 .067l.032 .108l.01 .053l.01 .06l.004 .057l.002 .059v12c0 .852 -.986 1.297 -1.623 .783l-.084 -.076l-6 -6a1 1 0 0 1 -.083 -1.32l.083 -.094l6 -6l.094 -.083l.077 -.054l.096 -.054l.036 -.017l.067 -.027l.108 -.032l.053 -.01l.06 -.01z"
stroke-width="0"
fill={colors.icon_color}
></Path>
</Svg>
</>
);
}

View file

@ -0,0 +1,28 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
export default function CaretRightIcon(props: IconProps) {
return (
<>
<Svg
height={props.size + "px"}
width={props.size + "px"}
viewBox="0 0 24 24"
stroke-width="2"
stroke={colors.icon_color}
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path
d="M13.883 5.007l.058 -.005h.118l.058 .005l.06 .009l.052 .01l.108 .032l.067 .027l.132 .07l.09 .065l.081 .073l.083 .094l.054 .077l.054 .096l.017 .036l.027 .067l.032 .108l.01 .053l.01 .06l.004 .057l.002 .059v12c0 .852 -.986 1.297 -1.623 .783l-.084 -.076l-6 -6a1 1 0 0 1 -.083 -1.32l.083 -.094l6 -6l.094 -.083l.077 -.054l.096 -.054l.036 -.017l.067 -.027l.108 -.032l.053 -.01l.06 -.01z"
stroke-width="0"
fill={colors.icon_color}
></Path>
</Svg>
</>
);
}

View file

@ -0,0 +1,29 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
export default function MessageIcon(props: IconProps) {
return (
<>
<Svg
width={props.size}
height={props.size}
viewBox="0 0 24 24"
strokeWidth="2"
stroke={colors.icon_color}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path d="M8 9h8"></Path>
<Path d="M8 13h6"></Path>
<Path d="M12.5 20.5l-.5 .5l-3 -3h-3a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v4"></Path>
<Path d="M21.121 20.121a3 3 0 1 0 -4.242 0c.418 .419 1.125 1.045 2.121 1.879c1.051 -.89 1.759 -1.516 2.121 -1.879z"></Path>
<Path d="M19 18v.01"></Path>
</Svg>
</>
);
}

View file

@ -0,0 +1,26 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
export default function RefreshIcon(props: IconProps) {
return (
<>
<Svg
width={props.size}
height={props.size}
viewBox="0 0 24 24"
strokeWidth="2"
stroke={colors.icon_color}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></Path>
<Path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></Path>
</Svg>
</>
);
}

View file

@ -1,5 +1,5 @@
import * as React from "react";
import { ActivityIndicator, Image } from "react-native";
import { ActivityIndicator, Image, Pressable } from "react-native";
import styles from "../../styles";
import {
View,
@ -34,9 +34,13 @@ import { useToast } from "react-native-toast-notifications";
import { useQueryClient } from "@tanstack/react-query";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useSelector } from "react-redux";
import { RootState } from "../../features/redux/Store/Store";
import CaretRightIcon from "../../icons/CaretLeftIcon/CaretLeftIcon";
export default function ConversationPage() {
const toast = useToast();
const user = useSelector((state: RootState) => state.user);
// Student Status
const [student_status, setStudentStatus] = useState<StudentStatusType>();
const StudentStatusQuery = useQuery({
@ -67,6 +71,7 @@ export default function ConversationPage() {
enabled:
student_status?.study_group != "" && student_status?.study_group != null,
queryKey: ["study_group"],
refetchInterval: 10000,
queryFn: async () => {
const data = await GetStudyGroup(student_status?.study_group || "");
if (data[0] == false) {
@ -124,7 +129,7 @@ export default function ConversationPage() {
// Avatar List
const [users, setUsers] = useState<GroupMessageAvatarType[]>([]);
const AvatarsQuery = useQuery({
refetchInterval: 3000,
refetchInterval: 10000,
enabled:
student_status?.study_group != null ||
(student_status?.study_group != "" &&
@ -159,7 +164,7 @@ export default function ConversationPage() {
mutationFn: async (info: MessagePostType) => {
const data = await PostMessage(info);
if (data[0] != true) {
return Promise.reject(new Error());
return Promise.reject(new Error(data[1]));
}
return data;
},
@ -195,9 +200,17 @@ export default function ConversationPage() {
>
<View style={styles.flex_row}>
<Text style={{ ...styles.text_white_medium }}>
{`Group: ${studygroup?.name ? studygroup.name : ""}`}
{`Group: ${studygroup?.name ? studygroup.name : "Loading..."}`}
</Text>
</View>
{studygroup.landmark ? (
<Text style={{...styles.text_white_tiny_bold,...{textAlign:'left'}}}>
{studygroup.landmark}
</Text>
) : (
<></>
)}
<View style={{ ...styles.flex_row }}>
<Text
style={{
@ -206,7 +219,9 @@ export default function ConversationPage() {
paddingRight: 4,
}}
>
{studygroup.students.length} studying
{!StudyGroupQuery.isFetching
? studygroup.students.length + " studying"
: "Loading"}
</Text>
{users.map((user: GroupMessageAvatarType, index: number) => {
if (index > 6) {
@ -248,7 +263,10 @@ export default function ConversationPage() {
key={message.id}
style={{
...styles.message_contentContainer,
alignItems: index % 2 == 0 ? "flex-end" : "flex-start",
alignItems:
message.user === user.user.username
? "flex-end"
: "flex-start",
}}
>
<View style={styles.flex_row}>
@ -287,6 +305,7 @@ export default function ConversationPage() {
<Text style={styles.text_white_small}>There are no messages</Text>
)}
</ScrollView>
<View style={styles.flex_row}>
<TextInput
style={styles.chatbox}
placeholder="Send a message..."
@ -304,6 +323,23 @@ export default function ConversationPage() {
setMessage("");
}}
/>
<Pressable
style={{
backgroundColor: colors.secondary_3,
borderRadius: 16,
alignSelf: "center",
marginLeft: 16,
}}
onPress={() => {
send_message.mutate({
message_content: message,
});
setMessage("");
}}
>
<CaretRightIcon size={48} />
</Pressable>
</View>
</AnimatedContainer>
</View>
);

View file

@ -6,6 +6,7 @@ import {
TextInput,
NativeSyntheticEvent,
TextInputChangeEventData,
Pressable,
} from "react-native";
import { useState } from "react";
import {
@ -22,6 +23,8 @@ import { urlProvider } from "../../components/Api/Api";
import MapView, { UrlTile, Marker } from "react-native-maps";
import { useNavigation } from "@react-navigation/native";
import { useToast } from "react-native-toast-notifications";
import CaretLeftIcon from "../../icons/CaretLeftIcon/CaretLeftIcon";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
export default function CreateGroup({ route }: any) {
const { location, subject } = route.params;
@ -41,7 +44,6 @@ export default function CreateGroup({ route }: any) {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["user_status"] });
queryClient.invalidateQueries({ queryKey: ["study_group_list"] });
student_status_patch.mutate({
study_group: name,
});
@ -73,13 +75,18 @@ export default function CreateGroup({ route }: any) {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["user_status"] });
queryClient.invalidateQueries({ queryKey: ["user_status_list"] });
queryClient.invalidateQueries({ queryKey: ["study_group_list"] });
toast.show(`Joined group ${name} successfully`, {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
navigation.navigate("Home");
// Set a delay before going back to conversation page to hopefully let the queries refresh in time
setTimeout(() => {
navigation.navigate("Conversation");
}, 200);
},
onError: (error: Error) => {
toast.show(String(error), {
@ -94,9 +101,10 @@ export default function CreateGroup({ route }: any) {
if (location) {
return (
<View style={styles.background}>
<AnimatedContainerNoScroll>
<AnimatedContainer>
<View style={{ zIndex: -1 }}>
<View style={styles.padding} />
<View style={{ borderRadius: 16, overflow: "hidden" }}>
<MapView
style={{
height: Viewport.height * 0.4,
@ -142,6 +150,7 @@ export default function CreateGroup({ route }: any) {
pinColor={colors.primary_1}
/>
</MapView>
</View>
<View style={styles.padding} />
</View>
<TextInput
@ -157,6 +166,10 @@ export default function CreateGroup({ route }: any) {
}}
/>
<View style={styles.padding} />
<View style={styles.flex_row}>
<Pressable onPress={() => navigation.navigate("Home")}>
<CaretLeftIcon size={32} />
</Pressable>
<Button
onPress={() => {
study_group_create.mutate({
@ -168,8 +181,10 @@ export default function CreateGroup({ route }: any) {
>
<Text style={styles.text_white_small}>Start Studying</Text>
</Button>
</View>
<View style={styles.padding} />
</AnimatedContainerNoScroll>
</AnimatedContainer>
</View>
);
}

View file

@ -1,8 +1,16 @@
import styles, { colors } from "../../styles";
import { View, Text, Pressable, ScrollView, Switch } from "react-native";
import {
View,
Text,
Pressable,
ScrollView,
Switch,
ActivityIndicator,
TouchableHighlight,
} from "react-native";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import { useState, useEffect } from "react";
import MapView, { Circle, Marker, UrlTile } from "react-native-maps";
import MapView, { Callout, Circle, Marker, UrlTile } from "react-native-maps";
import * as Location from "expo-location";
import GetDistance from "../../components/GetDistance/GetDistance";
import Button from "../../components/Button/Button";
@ -36,12 +44,16 @@ import GetDistanceFromUSTP from "../../components/GetDistance/GetDistanceFromUST
import Modal from "react-native-modal";
import DropdownIcon from "../../icons/CaretDownIcon/CaretDownIcon";
import CaretUpIcon from "../../icons/CaretUpIcon/CaretUpIcon";
import RefreshIcon from "../../icons/RefreshIcon/RefreshIcon";
import AsyncStorage from "@react-native-async-storage/async-storage";
import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll";
export default function Home() {
// Switch this condition to see the main map when debugging
const map_distance_override = false;
const navigation = useNavigation<RootDrawerParamList>();
const [location, setLocation] = useState<RawLocationType | null>(null);
const [locationPermitted, setLocationPermitted] = useState(false);
const [dist, setDist] = useState<number | null>(null);
const [feedback, setFeedback] = useState(
"To continue, please allow Stud-E permission to location services"
@ -52,7 +64,7 @@ export default function Home() {
const [modalOpen, setModalOpen] = useState(false);
const [modalByGroup, setModalByGroup] = useState(false);
async function requestLocation() {
async function requestLocationPermission() {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") {
setFeedback("Allow location permissions to continue");
@ -66,47 +78,51 @@ export default function Home() {
}
);
return;
}
if (status == "granted") {
let newLocation = await Location.getCurrentPositionAsync({});
if (newLocation) {
// Only update location state if user's location has changed
if (
!location ||
newLocation.coords.latitude !== location.coords.latitude ||
newLocation.coords.longitude !== location.coords.longitude
) {
setLocation(newLocation);
DistanceHandler(newLocation);
}
}
} else {
setLocationPermitted(true);
}
}
async function requestLocation() {
if (locationPermitted) {
let newLocation = await Location.getCurrentPositionAsync();
setLocation(newLocation);
await DistanceHandler(newLocation);
}
}
// Refresh every 15 seconds
useEffect(() => {
// console.log("Location Update");
// console.log(locationPermitted);
requestLocation();
}, [locationPermitted]);
useEffect(() => {
requestLocationPermission();
// Refresh every 30 seconds
const interval = setInterval(() => {
requestLocation();
}, 15000);
}, 30000);
return () => clearInterval(interval);
});
// Refresh when screen loads
useEffect(() => {
requestLocation();
}, []);
const [stopping_toofar, setStopping] = useState(false);
async function DistanceHandler(location: RawLocationType) {
let dist = GetDistanceFromUSTP(location.coords);
setDist(dist);
// Deactivate student status if too far away and still studying
if (dist >= 2 && !map_distance_override && studying)
// Deactivate student status if too far away from USTP and still studying
if (dist >= 1 && !map_distance_override && studying && !stopping_toofar) {
stop_studying.mutate({
active: false,
});
setStopping(true);
}
}
async function clear_messages_notification_cache() {
AsyncStorage.setItem("messages", "");
}
// Student Status
const [studying, setStudying] = useState(false);
const [subject, setSubject] = useState("");
@ -130,6 +146,22 @@ export default function Home() {
setSubject(data[1].subject);
setStudying(data[1].active);
setStudentStatus(data[1]);
// Deactivate student status if too far away from current location you are studying in
if (student_status && location) {
const dist = GetDistance(
student_status.location.latitude,
student_status.location.longitude,
location.coords.latitude,
location.coords.longitude
);
if (dist > 0.5 && studying && !stopping_toofar) {
console.log("Too far from current studying location");
stop_studying.mutate({
active: false,
});
setStopping(true);
}
}
},
onError: (error: Error) => {
toast.show(String(error), {
@ -145,7 +177,7 @@ export default function Home() {
mutationFn: async (info: StudentStatusPatchType) => {
const data = await PatchStudentStatus(info);
if (data[0] != true) {
return Promise.reject(new Error());
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
},
@ -175,6 +207,7 @@ export default function Home() {
}, 500);
setStudyGroups([]);
setStudying(false);
setStopping(false);
},
onError: (error: Error) => {
toast.show(String(error), {
@ -195,7 +228,7 @@ export default function Home() {
return data;
},
onSuccess: () => {
if (student_status?.study_group) {
if (student_status?.study_group == "") {
// Display separate toast if you stop studying while in a study group
toast.show("You left study group \n" + student_status?.study_group, {
type: "success",
@ -203,7 +236,9 @@ export default function Home() {
duration: 2000,
animationType: "slide-in",
});
clear_messages_notification_cache();
}
queryClient.invalidateQueries({ queryKey: ["user_status"] });
// Delay refetching for study groups since backend still needs to delete groups without students after leaving a study group
@ -345,11 +380,51 @@ export default function Home() {
});
function CustomMap() {
if (dist && location) {
if (dist <= 1 || map_distance_override) {
if (!locationPermitted) {
return (
<>
<Text style={styles.text_white_medium}>{feedback}</Text>
<Button onPress={async () => await requestLocation()}>
<Text style={styles.text_white_medium}>Allow Access</Text>
</Button>
</>
);
} else if (
(!StudentStatusQuery.isSuccess &&
studying &&
!StudentStatusListQuery.isSuccess &&
!StudyGroupQuery.isSuccess) ||
(!StudentStatusQuery.isSuccess &&
!studying &&
!StudentStatusListGlobalQuery.isSuccess &&
!StudyGroupGlobalQuery.isSuccess)
) {
return (
<>
<View style={{ paddingVertical: 8 }} />
<ActivityIndicator size={96} color={colors.secondary_1} />
<Text style={styles.text_white_medium}>Loading...</Text>
</>
);
} else if (
study_groups == undefined ||
study_groups_global == undefined ||
student_statuses == undefined ||
student_statuses_global == undefined
) {
return (
<>
<View style={{ paddingVertical: 8 }} />
<ActivityIndicator size={96} color={colors.secondary_1} />
<Text style={styles.text_white_medium}>Loading...</Text>
</>
);
} else if (dist && location) {
if (dist <= 0.25 || map_distance_override) {
return (
<>
<MapView
mapType={"none"}
style={styles.map}
customMapStyle={[
{
@ -404,6 +479,7 @@ export default function Home() {
zIndex={1000}
onPress={() => {
toast.hideAll();
toast.show(
<View
style={{
@ -432,7 +508,26 @@ export default function Home() {
}
);
}}
/>
>
<Callout>
<Text style={styles.text_white_tiny_bold}>
Student: {student_status.user}
</Text>
<Text style={styles.text_white_tiny_bold}>
{`Studying ${student_status.subject}`}
</Text>
<Text style={styles.text_black_tiny}>
{`${Math.round(
GetDistance(
student_status.location.latitude,
student_status.location.longitude,
location.coords.latitude,
location.coords.longitude
) * 1000
)}m away`}
</Text>
</Callout>
</Marker>
);
}
)
@ -513,6 +608,7 @@ export default function Home() {
zIndex={1000}
onPress={() => {
toast.hideAll();
toast.show(
<View
style={{
@ -524,6 +620,13 @@ export default function Home() {
<Text style={styles.text_white_tiny_bold}>
Study Group: {studygroup.name}
</Text>
{studygroup.landmark ? (
<Text style={styles.text_white_tiny_bold}>
{studygroup.landmark}
</Text>
) : (
<></>
)}
<Text style={styles.text_white_tiny_bold}>
{`Studying ${studygroup.subject}`}
</Text>
@ -548,6 +651,7 @@ export default function Home() {
study_group: studygroup.name,
subject: studygroup.subject,
});
navigation.navigate("Conversation");
}}
>
<Text style={styles.text_white_tiny_bold}>
@ -587,7 +691,35 @@ export default function Home() {
}
);
}}
/>
>
<Callout>
<Text style={styles.text_black_tiny}>
Study Group: {studygroup.name}
</Text>
<Text style={styles.text_black_tiny}>
Studying: {studygroup.subject}
</Text>
{studygroup.landmark ? (
<Text style={styles.text_black_tiny}>
{studygroup.landmark}
</Text>
) : (
<></>
)}
<Text style={styles.text_black_tiny}>
{`${studygroup.students.length} ${
studygroup.students.length > 1
? "students"
: "student"
} studying`}
</Text>
<Text style={styles.text_black_tiny}>
{`${Math.round(
studygroup.distance * 1000
)}m away`}
</Text>
</Callout>
</Marker>
<Circle
center={studygroup.location}
radius={studygroup.radius}
@ -630,6 +762,13 @@ export default function Home() {
<Text style={styles.text_white_tiny_bold}>
Study Group: {studygroup.name}
</Text>
{studygroup.landmark ? (
<Text style={styles.text_white_tiny_bold}>
{studygroup.landmark}
</Text>
) : (
<></>
)}
<Text style={styles.text_white_tiny_bold}>
{`Studying ${studygroup.subject}`}
</Text>
@ -662,7 +801,40 @@ export default function Home() {
}
);
}}
/>
>
<Callout>
<Text style={styles.text_black_tiny}>
Study Group: {studygroup.name}
</Text>
<Text style={styles.text_black_tiny}>
Studying: {studygroup.subject}
</Text>
{studygroup.landmark ? (
<Text style={styles.text_black_tiny}>
{studygroup.landmark}
</Text>
) : (
<></>
)}
<Text style={styles.text_black_tiny}>
{`${studygroup.students.length} ${
studygroup.students.length > 1
? "students"
: "student"
} studying`}
</Text>
<Text style={styles.text_black_tiny}>
{`${Math.round(
GetDistance(
studygroup.location.latitude,
studygroup.location.longitude,
location.coords.latitude,
location.coords.longitude
)
)}m away`}
</Text>
</Callout>
</Marker>
<Circle
center={studygroup.location}
radius={studygroup.radius}
@ -812,9 +984,40 @@ export default function Home() {
>
<Text style={styles.text_white_small}>{buttonLabel}</Text>
</Button>
<View
style={{
backgroundColor: colors.secondary_3,
borderRadius: 16,
alignSelf: "center",
marginHorizontal: 8,
}}
>
<Pressable
onPress={() => {
queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({
queryKey: ["user_status"],
});
queryClient.invalidateQueries({
queryKey: ["user_status_list"],
});
queryClient.invalidateQueries({
queryKey: ["study_group_list"],
});
toast.show("Refreshed", {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
requestLocation();
}}
>
<RefreshIcon size={32} />
</Pressable>
</View>
<Pressable
style={{
display: modalOpen ? "none" : "flex",
backgroundColor: colors.secondary_3,
borderRadius: 16,
alignSelf: "center",
@ -823,10 +1026,28 @@ export default function Home() {
setModalOpen(true);
}}
>
{studying ? <CaretUpIcon size={32} /> : <></>}
<CaretUpIcon size={32} />
</Pressable>
</View>
{student_status?.active && !student_status?.study_group ? (
<Button
onPress={() => {
if (student_status?.subject) {
navigation.navigate("Create Group", {
location: {
latitude: student_status?.location.latitude,
longitude: student_status?.location.longitude,
},
subject: student_status?.subject,
});
}
}}
>
<Text style={styles.text_white_tiny_bold}>Create Group</Text>
</Button>
) : (
<></>
)}
<View style={styles.padding} />
</>
);
@ -834,12 +1055,13 @@ export default function Home() {
return <MapRendererFar location={location.coords} dist={dist} />;
}
} else {
requestLocationPermission();
requestLocation();
return (
<>
<Text style={styles.text_white_medium}>{feedback}</Text>
<Button onPress={async () => await requestLocation()}>
<Text style={styles.text_white_medium}>Allow Access</Text>
</Button>
<View style={{ paddingVertical: 8 }} />
<ActivityIndicator size={96} color={colors.secondary_1} />
<Text style={styles.text_white_medium}>Loading...</Text>
</>
);
}
@ -863,14 +1085,15 @@ export default function Home() {
>
<DropdownIcon size={32} />
</Pressable>
<View style={styles.flex_row}>
<View style={styles.flex_column}>
<Text style={styles.text_white_medium}>List View</Text>
<Switch
value={modalByGroup}
onChange={() => {
setModalByGroup(!modalByGroup);
}}
style={{ alignSelf: "center" }}
/>
<Text style={styles.text_white_medium}>List View</Text>
</View>
<ScrollView>
@ -889,6 +1112,7 @@ export default function Home() {
borderWidth: 1,
borderRadius: 16,
width: 256,
marginVertical: 4,
}}
>
<Text style={styles.text_white_tiny_bold}>
@ -900,6 +1124,20 @@ export default function Home() {
<Text style={styles.text_white_tiny_bold}>
{`${Math.round(student_status.distance * 1000)}m away`}
</Text>
{location && location.coords ? (
<Text style={styles.text_black_tiny}>
{`${Math.round(
GetDistance(
student_status.location.latitude,
student_status.location.longitude,
location.coords.latitude,
location.coords.longitude
)
)}m away`}
</Text>
) : (
<></>
)}
</View>
);
}
@ -921,11 +1159,19 @@ export default function Home() {
borderWidth: 1,
borderRadius: 16,
width: 256,
marginVertical: 4,
}}
>
<Text style={styles.text_white_tiny_bold}>
Group Name: {studygroup.name}
</Text>
{studygroup.landmark ? (
<Text style={styles.text_white_tiny_bold}>
{studygroup.landmark}
</Text>
) : (
<></>
)}
<Text style={styles.text_white_tiny_bold}>
{`Studying ${studygroup.subject}`}
</Text>
@ -939,6 +1185,48 @@ export default function Home() {
) : (
<></>
)}
{student_status?.study_group != studygroup.name ? (
<Pressable
style={{
...styles.button_template,
backgroundColor: colors.secondary_2,
}}
onPress={() => {
change_study_group.mutate({
study_group: studygroup.name,
subject: studygroup.subject,
});
navigation.navigate("Conversation");
setModalOpen(!modalOpen);
}}
>
<Text style={styles.text_white_tiny_bold}>
Join Group
</Text>
</Pressable>
) : (
<></>
)}
{student_status?.study_group == studygroup.name ? (
<Pressable
style={{
...styles.button_template,
backgroundColor: colors.secondary_2,
}}
onPress={() => {
change_study_group.mutate({
study_group: "",
});
setModalOpen(!modalOpen);
}}
>
<Text style={styles.text_white_tiny_bold}>
Leave Group
</Text>
</Pressable>
) : (
<></>
)}
</View>
);
})
@ -948,8 +1236,143 @@ export default function Home() {
</ScrollView>
</AnimatedContainer>
</Modal>
<Modal
coverScreen={false}
isVisible={modalOpen && !studying}
style={{ opacity: 0.85 }}
hasBackdrop={false}
>
<AnimatedContainer>
<Pressable
style={{
alignContent: "flex-start",
backgroundColor: colors.secondary_3,
borderRadius: 16,
}}
onPress={() => setModalOpen(false)}
>
<DropdownIcon size={32} />
</Pressable>
<View style={styles.flex_column}>
<Text style={styles.text_white_medium}>List View</Text>
<Switch
value={modalByGroup}
onChange={() => {
setModalByGroup(!modalByGroup);
}}
style={{ alignSelf: "center" }}
/>
</View>
<ScrollView>
{!modalByGroup ? (
student_statuses_global.map(
(student_status: StudentStatusFilterType, index: number) => {
return (
<View
key={index}
style={{
alignContent: "center",
alignSelf: "center",
justifyContent: "center",
backgroundColor: colors.secondary_3,
borderColor: colors.primary_2,
borderWidth: 1,
borderRadius: 16,
width: 256,
marginVertical: 4,
}}
>
<Text style={styles.text_white_tiny_bold}>
Student: {student_status.user}
</Text>
<Text style={styles.text_white_tiny_bold}>
{`Studying ${student_status.subject}`}
</Text>
{location && location.coords ? (
<Text style={styles.text_white_tiny}>
{`${Math.round(
GetDistance(
student_status.location.latitude,
student_status.location.longitude,
location.coords.latitude,
location.coords.longitude
)
)}m away`}
</Text>
) : (
<></>
)}
</View>
);
}
)
) : (
<></>
)}
{modalByGroup ? (
study_groups_global.map(
(studygroup: StudyGroupType, index: number) => {
return (
<View
key={index}
style={{
alignContent: "center",
alignSelf: "center",
justifyContent: "center",
backgroundColor: colors.secondary_3,
borderColor: colors.primary_2,
borderWidth: 1,
borderRadius: 16,
width: 256,
marginVertical: 4,
}}
>
<Text style={styles.text_white_tiny_bold}>
Group Name: {studygroup.name}
</Text>
{studygroup.landmark ? (
<Text style={styles.text_white_tiny_bold}>
{studygroup.landmark}
</Text>
) : (
<></>
)}
<Text style={styles.text_white_tiny_bold}>
{`Studying ${studygroup.subject}`}
</Text>
<Text style={styles.text_white_tiny_bold}>
Students Studying: {studygroup.students.length}
</Text>
{location && location.coords ? (
<Text style={styles.text_white_tiny}>
{`${Math.round(
GetDistance(
studygroup.location.latitude,
studygroup.location.longitude,
location.coords.latitude,
location.coords.longitude
)
)}m away`}
</Text>
) : (
<></>
)}
</View>
);
}
)
) : (
<></>
)}
</ScrollView>
</AnimatedContainer>
</Modal>
<AnimatedContainer>
<View style={{ borderRadius: 16, overflow: "hidden" }}>
<CustomMap />
</View>
</AnimatedContainer>
</View>
);

View file

@ -7,8 +7,8 @@ import {
NativeSyntheticEvent,
TextInputChangeEventData,
} from "react-native";
import { useDispatch } from "react-redux";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEffect, useState } from "react";
import LoginIcon from "../../icons/LoginIcon/LoginIcon";
import Button from "../../components/Button/Button";
import { useNavigation } from "@react-navigation/native";
@ -22,15 +22,23 @@ import {
unsetOnboarding,
} from "../../features/redux/slices/StatusSlice/StatusSlice";
import { useToast } from "react-native-toast-notifications";
import { RootState } from "../../features/redux/Store/Store";
export default function Login() {
const navigation = useNavigation<RootDrawerParamList>();
const status = useSelector((state: RootState) => state.status);
const [logging_in, setLoggingIn] = useState(false);
const dispatch = useDispatch();
const [creds, setCreds] = useState({
username: "",
password: "",
});
const toast = useToast();
useEffect(() => {
if (status.logged_in) {
navigation.navigate("Home");
}
}, []);
return (
<View style={styles.background}>
<AnimatedContainer>
@ -69,6 +77,7 @@ export default function Login() {
<View style={{ paddingVertical: 4 }} />
<Button
onPress={async () => {
if (!logging_in) {
await UserLogin({
username: creds.username,
password: creds.password,
@ -111,7 +120,9 @@ export default function Login() {
animationType: "slide-in",
});
}
setLoggingIn(false);
});
}
}}
>
<Text style={styles.text_white_small}>Login</Text>

View file

@ -195,7 +195,13 @@ export default function Onboarding() {
...styles.text_white_small_bold,
...{ textAlign: "center" },
}}
dropDownContainerStyle={{ backgroundColor: colors.primary_2 }}
modalContentContainerStyle={{
backgroundColor: colors.primary_2,
borderWidth: 0,
zIndex: 1000,
}}
dropDownDirection="BOTTOM"
listMode="MODAL"
/>
<DropDownPicker
zIndex={2000}
@ -215,7 +221,13 @@ export default function Onboarding() {
...styles.text_white_small_bold,
...{ textAlign: "center" },
}}
dropDownContainerStyle={{ backgroundColor: colors.primary_2 }}
modalContentContainerStyle={{
backgroundColor: colors.primary_2,
borderWidth: 0,
zIndex: 1000,
}}
dropDownDirection="BOTTOM"
listMode="MODAL"
/>
<DropDownPicker
zIndex={1000}
@ -235,7 +247,13 @@ export default function Onboarding() {
...styles.text_white_small_bold,
...{ textAlign: "center" },
}}
dropDownContainerStyle={{ backgroundColor: colors.primary_2 }}
modalContentContainerStyle={{
backgroundColor: colors.primary_2,
borderWidth: 0,
zIndex: 1000,
}}
dropDownDirection="BOTTOM"
listMode="MODAL"
/>
</MotiView>
<MotiView

View file

@ -17,12 +17,14 @@ import { UserRegister } from "../../components/Api/Api";
import IsNumber from "../../components/IsNumber/IsNumber";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import { useToast } from "react-native-toast-notifications";
import { ScrollView } from "react-native-gesture-handler";
export default function Register() {
const navigation = useNavigation<RootDrawerParamList>();
const toast = useToast();
// const dispatch = useDispatch();
// const creds = useSelector((state: RootState) => state.auth.creds);
const [registering, setRegistering] = useState(false);
const [user, setUser] = useState({
first_name: "",
last_name: "",
@ -30,6 +32,7 @@ export default function Register() {
username: "",
email: "",
password: "",
confirm_password: "",
});
return (
<View style={styles.background}>
@ -121,8 +124,25 @@ export default function Register() {
}}
/>
<View style={{ paddingVertical: 4 }} />
<TextInput
style={styles.text_input}
placeholder="Confirm Password"
placeholderTextColor={colors.text_default}
secureTextEntry={true}
value={user.confirm_password}
autoCapitalize={"none"}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, confirm_password: e.nativeEvent.text });
}}
/>
<View style={{ paddingVertical: 4 }} />
<Button
onPress={async () => {
if (!registering) {
if (user.password === user.confirm_password) {
setRegistering(true);
await UserRegister({
username: user.username,
email: user.email,
@ -130,7 +150,7 @@ export default function Register() {
student_id_number: user.student_id_number,
first_name: user.first_name,
last_name: user.last_name,
}).then((result) => {
}).then((result: any) => {
console.log(result);
if (result[0]) {
setUser({
@ -155,15 +175,26 @@ export default function Register() {
navigation.navigate("Login");
}, 10000);
} else {
toast.show(JSON.stringify(result[1]), {
toast.show(JSON.parse(JSON.stringify(result[1])), {
type: "warning",
placement: "top",
duration: 6000,
animationType: "slide-in",
});
}
setRegistering(false);
});
} else {
toast.show(
"Password does not match confirm password. Please try again"
),
{
type: "warning",
placement: "top",
duration: 6000,
animationType: "slide-in",
};
}
}
}}
>

View file

@ -1,6 +1,12 @@
import * as React from "react";
import styles, { Viewport } from "../../styles";
import { View, Text, ToastAndroid } from "react-native";
import {
View,
Text,
ToastAndroid,
Pressable,
ActivityIndicator,
} from "react-native";
import { useState } from "react";
import {
UserInfoReturnType,
@ -24,6 +30,8 @@ import { urlProvider } from "../../components/Api/Api";
import MapView, { UrlTile, Marker } from "react-native-maps";
import { useNavigation } from "@react-navigation/native";
import { useToast } from "react-native-toast-notifications";
import CaretLeftIcon from "../../icons/CaretLeftIcon/CaretLeftIcon";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
export default function StartStudying({ route }: any) {
const { location } = route.params;
@ -72,13 +80,18 @@ export default function StartStudying({ route }: any) {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["user_status"] });
queryClient.invalidateQueries({ queryKey: ["user_status_list"] });
queryClient.invalidateQueries({ queryKey: ["study_group_list"] });
toast.show("You are now studying \n" + selected_subject, {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
// Set a delay before going back to homepage to hopefully let the queries refresh in time
setTimeout(() => {
navigation.navigate("Home");
}, 200);
},
onError: (error: Error) => {
toast.show(String(error), {
@ -90,12 +103,24 @@ export default function StartStudying({ route }: any) {
},
});
if (StudentInfo.isLoading) {
return (
<View style={styles.background}>
<AnimatedContainer>
<View style={{ paddingVertical: 8 }} />
<ActivityIndicator size={96} color={colors.secondary_1} />
<Text style={styles.text_white_medium}>Loading...</Text>
</AnimatedContainer>
</View>
);
}
if (location && location.coords) {
return (
<View style={styles.background}>
<AnimatedContainerNoScroll>
<View style={{ zIndex: -1 }}>
<View style={styles.padding} />
<View style={{ borderRadius: 16, overflow: "hidden" }}>
<MapView
style={{
height: Viewport.height * 0.4,
@ -141,6 +166,7 @@ export default function StartStudying({ route }: any) {
pinColor={colors.primary_1}
/>
</MapView>
</View>
<View style={styles.padding} />
</View>
<DropDownPicker
@ -174,6 +200,10 @@ export default function StartStudying({ route }: any) {
listMode="MODAL"
/>
<View style={styles.padding} />
<View style={styles.flex_row}>
<Pressable onPress={() => navigation.navigate("Home")}>
<CaretLeftIcon size={32} />
</Pressable>
<Button
onPress={() => {
console.log({
@ -195,7 +225,7 @@ export default function StartStudying({ route }: any) {
>
<Text style={styles.text_white_small}>Start Studying</Text>
</Button>
<View style={styles.padding} />
</View>
</AnimatedContainerNoScroll>
</View>
);

View file

@ -1,6 +1,6 @@
import * as React from "react";
import styles from "../../styles";
import { View, Text } from "react-native";
import { View, Text, ActivityIndicator } from "react-native";
import { useState } from "react";
import {
UserInfoReturnType,
@ -26,6 +26,9 @@ import AnimatedContainerNoScroll from "../../components/AnimatedContainer/Animat
import { useSelector } from "react-redux";
import { RootState } from "../../features/redux/Store/Store";
import { useToast } from "react-native-toast-notifications";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import Loading from "../Loading/Loading";
import LoadingFeedback from "../../components/LoadingFeedback/LoadingFeedback";
export default function SubjectsPage() {
const logged_in_user = useSelector((state: RootState) => state.user.user);
@ -183,7 +186,9 @@ export default function SubjectsPage() {
);
}
}
if (StudentInfo.isLoading || Subjects.isLoading) {
return <LoadingFeedback />;
}
return (
<View style={styles.background}>
<AnimatedContainerNoScroll>

View file

@ -7,6 +7,7 @@ import {
NativeSyntheticEvent,
TextInputChangeEventData,
Pressable,
ActivityIndicator,
} from "react-native";
import { useState } from "react";
import {
@ -42,6 +43,9 @@ import { setUser as setUserinState } from "../../features/redux/slices/UserSlice
import * as ImagePicker from "expo-image-picker";
import * as FileSystem from "expo-file-system";
import { useToast } from "react-native-toast-notifications";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import Loading from "../Loading/Loading";
import LoadingFeedback from "../../components/LoadingFeedback/LoadingFeedback";
export default function UserInfoPage() {
const logged_in_user = useSelector((state: RootState) => state.user.user);
@ -285,7 +289,14 @@ export default function UserInfoPage() {
);
}
}
if (
StudentInfo.isLoading ||
Semesters.isLoading ||
yearlevel_query.isLoading ||
course_query.isLoading
) {
return <LoadingFeedback />;
}
return (
<View style={styles.background}>
<AnimatedContainerNoScroll>
@ -339,7 +350,7 @@ export default function UserInfoPage() {
</View>
<View style={{ flex: 3 }}>
<DropDownPicker
zIndex={4000}
zIndex={1000}
open={yearLevelOpen}
value={selected_yearlevel}
items={year_levels}
@ -361,7 +372,7 @@ export default function UserInfoPage() {
}}
dropDownContainerStyle={{
backgroundColor: colors.primary_2,
zIndex: 4000,
zIndex: 1000,
borderWidth: 0,
}}
dropDownDirection="TOP"
@ -374,7 +385,7 @@ export default function UserInfoPage() {
</View>
<View style={{ flex: 3 }}>
<DropDownPicker
zIndex={3000}
zIndex={2000}
open={semesterOpen}
value={selected_semester}
items={semesters}
@ -396,7 +407,7 @@ export default function UserInfoPage() {
}}
dropDownContainerStyle={{
backgroundColor: colors.primary_2,
zIndex: 3000,
zIndex: 2000,
borderWidth: 0,
}}
dropDownDirection="TOP"
@ -409,7 +420,7 @@ export default function UserInfoPage() {
</View>
<View style={{ flex: 3 }}>
<DropDownPicker
zIndex={2000}
zIndex={3000}
open={courseOpen}
value={selected_course}
items={courses}
@ -431,7 +442,7 @@ export default function UserInfoPage() {
}}
dropDownContainerStyle={{
backgroundColor: colors.primary_2,
zIndex: 2000,
zIndex: 3000,
borderWidth: 0,
}}
dropDownDirection="TOP"

View file

@ -44,8 +44,9 @@ const styles = StyleSheet.create({
justifyContent: "center",
display: "flex",
flexDirection: "column",
flex: 1,
flexGrow: 1,
paddingHorizontal: 4,
paddingVertical: 32,
},
flex_row: {
display: "flex",