diff --git a/app.json b/app.json
index 1380c91..adefbc0 100644
--- a/app.json
+++ b/app.json
@@ -3,7 +3,7 @@
"name": "tethr",
"slug": "expo-starter",
"scheme": "tethr",
- "version": "1.0.3",
+ "version": "1.0.5",
"orientation": "portrait",
"icon": "./src/assets/splash-icon.png",
"userInterfaceStyle": "dark",
diff --git a/src/app/addfriends.tsx b/src/app/addfriends.tsx
index 7f3cd90..75133ff 100644
--- a/src/app/addfriends.tsx
+++ b/src/app/addfriends.tsx
@@ -105,7 +105,7 @@ export default function AddFriendsScreen() {
-
+
Add Friends
diff --git a/src/app/auth/index.tsx b/src/app/auth/index.tsx
index aa134ab..06b811e 100644
--- a/src/app/auth/index.tsx
+++ b/src/app/auth/index.tsx
@@ -1,10 +1,11 @@
'use client';
import { View, TextInput, Alert, Text, TouchableOpacity } from 'react-native';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { supabase } from '@/lib/supabase';
import { Redirect } from 'expo-router';
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
+import * as SplashScreen from 'expo-splash-screen';
import Tethr from '@/components/tethr';
@@ -15,8 +16,14 @@ export default function IndexScreen() {
const [otp, setOtp] = useState('');
const [authMode, setAuthMode] = useState<'login' | 'signup'>('signup');
const [currentView, setCurrentView] = useState<'email' | 'verify' | 'authenticated'>('email');
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ SplashScreen.hideAsync();
+ }, []);
const OTP = async () => {
+ setLoading(true);
console.log(authMode);
if (authMode === 'signup') {
const { data: existingUsers, error: checkError } = await supabase
@@ -69,9 +76,11 @@ export default function IndexScreen() {
setCurrentView('verify');
console.log('Success! Check your email');
}
+ setLoading(false);
};
const verifyOTP = async () => {
+ setLoading(true);
const { data, error } = await supabase.auth.verifyOtp({
email: email,
token: otp,
@@ -103,6 +112,7 @@ export default function IndexScreen() {
setCurrentView('authenticated');
console.log('User authenticated:', data);
}
+ setLoading(true);
};
if (currentView === 'email') {
@@ -149,8 +159,11 @@ export default function IndexScreen() {
autoCapitalize="none"
/>
)}
-
- Continue
+
+ 0 ? 'text-tethr-purple' : 'text-gray-500'}`}>
+ Continue
+
@@ -198,7 +211,7 @@ export default function IndexScreen() {
keyboardType="number-pad"
returnKeyType="done"
/>
-
+
Verify Email
diff --git a/src/app/main/_layout.tsx b/src/app/main/_layout.tsx
index 91d4950..9932ffc 100644
--- a/src/app/main/_layout.tsx
+++ b/src/app/main/_layout.tsx
@@ -38,13 +38,16 @@ export default function RootLayout() {
initializeTaskObservers();
const unsubscribe = friendRequestObserver.subscribe((data) => {
- console.log('Layout observer: reduce friends badge', data);
+ console.log('Layout observer: modify friends badge', data);
if (data.action === 'accept') {
setIncoming((prev) => Math.max(0, prev - 1));
} else if (data.action === 'reject') {
setIncoming((prev) => Math.max(0, prev - 1));
}
+ if (data.action === 'manualUpdate' && data.count) {
+ setIncoming(data.count);
+ }
});
return () => unsubscribe();
diff --git a/src/app/main/camera/chooseGroup.tsx b/src/app/main/camera/chooseGroup.tsx
index b717df6..b09ffbe 100644
--- a/src/app/main/camera/chooseGroup.tsx
+++ b/src/app/main/camera/chooseGroup.tsx
@@ -67,7 +67,7 @@ const ChooseGroup = () => {
/>
-
+
Select Group
@@ -79,11 +79,11 @@ const ChooseGroup = () => {
)}
{!loading && (
-
+
{filtered.length === 0 ? (
No matching groups.
) : (
-
+
{filtered.map((group, index) => (
{
-
+
Select Task for
{group_name}
@@ -90,7 +90,7 @@ const ChooseTask = () => {
{filteredTasks.length === 0 ? (
No matching tasks.
) : (
-
+
{filteredTasks.map((task, idx) => {
const taskCompleted = isCompleted(task.task_name, groupId);
@@ -118,7 +118,7 @@ const ChooseTask = () => {
);
})}
-
+
)}
diff --git a/src/app/main/friends.tsx b/src/app/main/friends.tsx
index 0b7bded..498fc63 100644
--- a/src/app/main/friends.tsx
+++ b/src/app/main/friends.tsx
@@ -51,6 +51,12 @@ export default function FriendsScreen() {
getFriendsList.getOutgoingFriendRequests(),
]);
+ console.log(incomingRes.length);
+ friendRequestObserver.notify({
+ action: 'manualUpdate',
+ count: incomingRes.length,
+ });
+
const addedFriends = friendsRes ?? [];
const incomingReqs = incomingRes ?? [];
const outgoingReqs = outgoingRes ?? [];
@@ -140,6 +146,22 @@ export default function FriendsScreen() {
}
};
+ const handleRejectRequest = async (friendId: string) => {
+ try {
+ setIncoming((prev) => prev.filter((f) => f.userId !== friendId));
+
+ friendRequestObserver.notify({
+ action: 'reject',
+ friendId,
+ });
+
+ await getFriendsList.removeRequest(friendId);
+ } catch (error) {
+ console.error(error);
+ loadFriends();
+ }
+ };
+
const onRefresh = useCallback(async () => {
setRefreshing(true);
await loadFriends();
@@ -179,7 +201,7 @@ export default function FriendsScreen() {
}
return (
-
+
@@ -203,12 +225,14 @@ export default function FriendsScreen() {
item.username}
+ stickySectionHeadersEnabled={false}
renderItem={({ item, section, index }) => (
{
if (section.title === 'Friends') {
@@ -219,6 +243,9 @@ export default function FriendsScreen() {
handleRemoveRequest(item.userId);
}
}}
+ secondPressFunction={
+ section.title === 'Incoming' ? () => handleRejectRequest(item.userId) : undefined
+ }
cardType={getCardType(index, section.data.length)}
/>
diff --git a/src/app/main/groups/[groupId]/index.tsx b/src/app/main/groups/[groupId]/index.tsx
index 5f8557f..78cc9cd 100644
--- a/src/app/main/groups/[groupId]/index.tsx
+++ b/src/app/main/groups/[groupId]/index.tsx
@@ -1,5 +1,5 @@
import { View, Text, TouchableOpacity, ActivityIndicator, Pressable, FlatList } from 'react-native';
-import { useEffect, useState } from 'react';
+import { useEffect, useState, useRef } from 'react';
import { useLocalSearchParams, router } from 'expo-router';
import { FontAwesome6 } from '@expo/vector-icons';
import { taskController } from '@/controllers/tasks';
@@ -47,6 +47,7 @@ const GroupPage = () => {
const [myUsername, setMyUsername] = useState('');
const [photos, setPhotos] = useState([]);
const [completedTasks, setCompletedTasks] = useState([]);
+ const flatListRef = useRef(null);
useEffect(() => {
if (groupId) {
@@ -130,6 +131,10 @@ const GroupPage = () => {
return completedTasks.includes(taskKey);
};
+ const scrollToTop = () => {
+ flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
+ };
+
if (loading) {
return (
@@ -171,6 +176,7 @@ const GroupPage = () => {
Leaderboard
@@ -267,6 +273,11 @@ const GroupPage = () => {
No photos in this group yet.
}
/>
+
+
+
);
diff --git a/src/app/main/groups/index.tsx b/src/app/main/groups/index.tsx
index 0c58205..0c8b7ad 100644
--- a/src/app/main/groups/index.tsx
+++ b/src/app/main/groups/index.tsx
@@ -45,7 +45,7 @@ const Index = () => {
}, [parsedObject, searchQuery]);
return (
-
+
diff --git a/src/app/main/profile.tsx b/src/app/main/profile.tsx
index 55176e2..b193f50 100644
--- a/src/app/main/profile.tsx
+++ b/src/app/main/profile.tsx
@@ -5,6 +5,7 @@ import Tethr from '@/components/tethr';
import { ProfileProps, userController } from '@/controllers/userInfo';
import { useState, useCallback, useEffect } from 'react';
import { useRouter } from 'expo-router';
+import { scoreUpdateObserver } from '@/controllers/observers/scoreUpdateObserver';
export default function ProfileScreen() {
const [loading, setLoading] = useState(true);
@@ -27,7 +28,14 @@ export default function ProfileScreen() {
}, []);
useEffect(() => {
loadProfile();
- }, [loadProfile]);
+
+ const unsubscribe = scoreUpdateObserver.subscribe(() => {
+ loadProfile();
+ });
+
+ return () => unsubscribe();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
const handleLogout = async () => {
const success = await userController.logout();
diff --git a/src/app/main/tasks/index.tsx b/src/app/main/tasks/index.tsx
index d1fbd8e..caa1c96 100644
--- a/src/app/main/tasks/index.tsx
+++ b/src/app/main/tasks/index.tsx
@@ -55,7 +55,7 @@ const Index = () => {
};
return (
-
+
diff --git a/src/components/camera/photopreview.tsx b/src/components/camera/photopreview.tsx
index e15447c..70aba33 100644
--- a/src/components/camera/photopreview.tsx
+++ b/src/components/camera/photopreview.tsx
@@ -45,15 +45,10 @@ const PhotoPreview = ({
if (exifOrientation === 1) {
rotation = 90;
- console.log('Applying 90 rotation for landscape');
} else if (exifOrientation === 3) {
rotation = 270;
- console.log('Applying 270 rotation for landscape');
} else if (exifOrientation === 8) {
rotation = 180;
- console.log('Applying 180° rotation for upsidedown');
- } else {
- console.log('No rotation needed for portrait');
}
if (rotation !== 0) {
diff --git a/src/components/exploreUI.tsx b/src/components/exploreUI.tsx
index 2ac08fa..028c0f2 100644
--- a/src/components/exploreUI.tsx
+++ b/src/components/exploreUI.tsx
@@ -1,4 +1,11 @@
-import { View, FlatList, ActivityIndicator, Text, RefreshControl } from 'react-native';
+import {
+ View,
+ FlatList,
+ ActivityIndicator,
+ Text,
+ RefreshControl,
+ TouchableOpacity,
+} from 'react-native';
import { useState, useEffect, useMemo, useRef } from 'react';
import { photoRetrieve, PhotoSubmission } from '@/controllers/photoRetrieve';
import { groupController } from '@/controllers/group';
@@ -10,7 +17,7 @@ import {
import Tethr from '@/components/tethr';
import Fyp from '@/components/fyp';
import SearchBar from '@/components/searchbar';
-
+import Entypo from '@expo/vector-icons/Entypo';
interface PhotoWithGroup extends PhotoSubmission {
groupName: string;
}
@@ -21,6 +28,7 @@ export default function ExploreUI() {
const [photos, setPhotos] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
const groupsMapRef = useRef>({});
+ const flatListRef = useRef(null);
const loadPhotos = async () => {
try {
@@ -62,6 +70,10 @@ export default function ExploreUI() {
setRefreshing(false);
};
+ const scrollToTop = () => {
+ flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
+ };
+
useEffect(() => {
loadPhotos();
@@ -84,6 +96,7 @@ export default function ExploreUI() {
return () => unregisterExploreObserver();
}, []);
+
const filteredPhotos = useMemo(() => {
if (!searchQuery.trim()) return photos;
@@ -109,15 +122,10 @@ export default function ExploreUI() {
item.name}
renderItem={({ item }) => {
- // console.log('Photo item:', {
- // groupId: item.groupId,
- // groupName: item.groupName,
- // taskName: item.taskName,
- // });
-
return (
{searchQuery
? 'No photos match your search.'
- : 'No photos yet. Join a group to start completing tasks!'}
+ : 'No photos yet. Time to complete your tasks!'}
}
/>
+
+
+
+
);
}
diff --git a/src/components/friendcard.tsx b/src/components/friendcard.tsx
index e281382..c670ba4 100644
--- a/src/components/friendcard.tsx
+++ b/src/components/friendcard.tsx
@@ -1,14 +1,18 @@
import { View, Image, Text, TouchableOpacity } from 'react-native';
import { roundedMap } from '@/utils/cardType';
+import AntDesign from '@expo/vector-icons/AntDesign';
+
type CardType = 'top' | 'middle' | 'bottom' | 'solo';
export interface FriendProps {
pfpUrl: string;
username: string;
buttonText: string;
+ secondButtonText?: string;
cardType: CardType;
userId: string;
pressFunction?: (userId: string) => void;
+ secondPressFunction?: (userId: string) => void;
}
const FriendCard = ({
@@ -16,10 +20,23 @@ const FriendCard = ({
username,
userId,
buttonText,
+ secondButtonText,
cardType,
pressFunction,
+ secondPressFunction,
}: FriendProps) => {
const roundedClass = roundedMap[cardType];
+
+ const renderContent = (text: string) => {
+ if (text === 'check') {
+ return ;
+ }
+ if (text === 'close') {
+ return ;
+ }
+ return {text};
+ };
+
return (
@@ -27,17 +44,26 @@ const FriendCard = ({
{username}
- {
- if (pressFunction) {
- pressFunction(userId);
- } else {
- console.log('error with button function');
- }
- }}>
- {buttonText}
-
+
+ {
+ if (pressFunction) {
+ pressFunction(userId);
+ } else {
+ console.log('error with button function');
+ }
+ }}>
+ {renderContent(buttonText)}
+
+ {secondButtonText && (
+ secondPressFunction?.(userId)}>
+ {renderContent(secondButtonText)}
+
+ )}
+
);
};
diff --git a/src/components/groups/groups.tsx b/src/components/groups/groups.tsx
index 498b896..bd06403 100644
--- a/src/components/groups/groups.tsx
+++ b/src/components/groups/groups.tsx
@@ -53,15 +53,13 @@ const Groups = ({ groups }: GroupsProps) => {
)}
keyExtractor={(item) => item.group_id}
ListEmptyComponent={
-
-
- Create or a group to get started!
- router.push('/main/groups/createGroup')}>
-
-
-
+
+ Create a group to get started!
+ router.push('/main/groups/createGroup')}>
+
+
}
/>
diff --git a/src/controllers/getFriends.ts b/src/controllers/getFriends.ts
index 4dbaa94..5048831 100644
--- a/src/controllers/getFriends.ts
+++ b/src/controllers/getFriends.ts
@@ -99,7 +99,7 @@ class GetFriendController {
pfpUrl: pfpUrl,
username: user?.username ?? 'Unknown',
userId: user?.user_id ?? '',
- buttonText: 'Accept',
+ buttonText: 'check',
cardType: getCardType(index, arr.length),
};
});
diff --git a/src/controllers/group.ts b/src/controllers/group.ts
index 2a1c5ca..8de7c61 100644
--- a/src/controllers/group.ts
+++ b/src/controllers/group.ts
@@ -3,6 +3,7 @@ import {
taskCompletionObserver,
TaskCompletionData,
} from '@/controllers/observers/taskCompletionObserver';
+import { scoreUpdateObserver } from '@/controllers/observers/scoreUpdateObserver';
interface GroupType {
group_id: string;
@@ -152,6 +153,8 @@ class GroupController {
return false;
}
+ scoreUpdateObserver.notify();
+
return true;
} catch (err) {
console.error('Increasing Score Error:', err);
diff --git a/src/controllers/observers/friendRequestObserver.ts b/src/controllers/observers/friendRequestObserver.ts
index 2940904..9bbbe14 100644
--- a/src/controllers/observers/friendRequestObserver.ts
+++ b/src/controllers/observers/friendRequestObserver.ts
@@ -1,7 +1,8 @@
export interface FriendRequestData {
- action: 'accept' | 'reject' | 'cancel';
- friendId: string;
+ action: 'accept' | 'reject' | 'cancel' | 'manualUpdate';
+ friendId?: string;
username?: string;
+ count?: number;
}
class FriendRequestObserver {
@@ -24,6 +25,10 @@ class FriendRequestObserver {
}
});
}
+
+ notifyChange(count: number) {
+ this.notify({ action: 'manualUpdate', count });
+ }
}
export const friendRequestObserver = new FriendRequestObserver();
diff --git a/src/controllers/observers/scoreUpdateObserver.ts b/src/controllers/observers/scoreUpdateObserver.ts
new file mode 100644
index 0000000..c40657c
--- /dev/null
+++ b/src/controllers/observers/scoreUpdateObserver.ts
@@ -0,0 +1,23 @@
+class ScoreUpdateObserver {
+ private observers: (() => void)[] = [];
+
+ subscribe(observer: () => void) {
+ this.observers.push(observer);
+ return () => {
+ this.observers = this.observers.filter((obs) => obs !== observer);
+ };
+ }
+
+ notify() {
+ console.log('Notifying profile observer');
+ this.observers.forEach((observer) => {
+ try {
+ observer();
+ } catch (error) {
+ console.error('Score update observer error:', error);
+ }
+ });
+ }
+}
+
+export const scoreUpdateObserver = new ScoreUpdateObserver();