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();