From 489013948d7677b71caac702479cce0561e3f6f2 Mon Sep 17 00:00:00 2001 From: Kevin Loritsch Date: Sat, 29 Nov 2025 11:40:41 -0800 Subject: [PATCH 1/9] orientation photo taking fix --- app.json | 2 +- src/app/main/camera/takePhoto.tsx | 2 + src/components/camera/photopreview.tsx | 102 ++++++++++++++++++++----- 3 files changed, 86 insertions(+), 20 deletions(-) diff --git a/app.json b/app.json index be4bb54..1380c91 100644 --- a/app.json +++ b/app.json @@ -3,7 +3,7 @@ "name": "tethr", "slug": "expo-starter", "scheme": "tethr", - "version": "1.0.0", + "version": "1.0.3", "orientation": "portrait", "icon": "./src/assets/splash-icon.png", "userInterfaceStyle": "dark", diff --git a/src/app/main/camera/takePhoto.tsx b/src/app/main/camera/takePhoto.tsx index 8e354a4..f649c5e 100644 --- a/src/app/main/camera/takePhoto.tsx +++ b/src/app/main/camera/takePhoto.tsx @@ -89,6 +89,7 @@ export default function Camera() { quality: 1, base64: true, exif: true, + skipProcessing: false, }); setPhoto(takenPhoto); @@ -164,6 +165,7 @@ export default function Camera() { zoom={zoom} ref={cameraRef} mirror={facing === 'front'} + responsiveOrientationWhenOrientationLocked /> {taskName} diff --git a/src/components/camera/photopreview.tsx b/src/components/camera/photopreview.tsx index cadef32..67bc42c 100644 --- a/src/components/camera/photopreview.tsx +++ b/src/components/camera/photopreview.tsx @@ -1,7 +1,8 @@ import { CameraCapturedPicture } from 'expo-camera'; import { TouchableOpacity, Image, View, ActivityIndicator, Text } from 'react-native'; import { router } from 'expo-router'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; +import { SaveFormat, useImageManipulator } from 'expo-image-manipulator'; import Tethr from '@/components/tethr'; import { storagePush } from '@/controllers/photoUpload'; import { userController } from '@/controllers/userInfo'; @@ -26,11 +27,63 @@ const PhotoPreview = ({ weekly: boolean; }) => { const [uploading, setUploading] = useState(false); + const [correctedPhoto, setCorrectedPhoto] = useState(photo.uri); + const [processing, setProcessing] = useState(true); + const manipulator = useImageManipulator(photo.uri); - const handleUpload = async () => { + useEffect(() => { + fixOrientation(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const fixOrientation = async () => { try { - if (!photo.base64) return; + setProcessing(true); + console.log('Photo URI:', photo.uri); + console.log('Photo EXIF orientation:', photo.exif?.Orientation); + + let rotation = 0; + const exifOrientation = photo.exif?.Orientation || 6; + + // Apply 180 degree fix for landscape orientations + 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) { + manipulator.rotate(rotation); // Use the hook from top level + const imageRef = await manipulator.renderAsync(); + + const result = await imageRef.saveAsync({ + compress: 0.7, + format: SaveFormat.JPEG, + }); + + console.log('Corrected photo URI:', result.uri); + setCorrectedPhoto(result.uri); + } else { + setCorrectedPhoto(photo.uri); + } + } catch (error) { + console.error('Error fixing orientation:', error); + setCorrectedPhoto(photo.uri); + } finally { + setProcessing(false); + } + }; + + const handleUpload = async () => { + try { setUploading(true); const userId = await userController.getId(); @@ -43,17 +96,19 @@ const PhotoPreview = ({ await groupController.increaseMemberScore(userId, group_id); storagePush.uploadImage({ - uri: photo.uri, + uri: correctedPhoto, userId: userId, groupId: group_id, taskName: task_name, }); + handleRetakePhoto(); router.push('/'); } catch (err: any) { console.error('Upload error:', err); + } finally { + setUploading(false); } - setUploading(false); }; return ( @@ -65,7 +120,7 @@ const PhotoPreview = ({ + disabled={uploading || processing}> - - - - {task_name} - - - {uploading && ( - + {processing ? ( + + ) : ( + <> + + + + {task_name} + + + {uploading && ( + + + + )} + )} Post To {group_name} From 1d3a384aad3235d3dc78052e634ede1c09364e85 Mon Sep 17 00:00:00 2001 From: Kevin Loritsch Date: Sat, 29 Nov 2025 12:02:23 -0800 Subject: [PATCH 2/9] photo cleanups, increased compression, clean out orientation bugs --- src/components/camera/photopreview.tsx | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/camera/photopreview.tsx b/src/components/camera/photopreview.tsx index 67bc42c..6cb3b5e 100644 --- a/src/components/camera/photopreview.tsx +++ b/src/components/camera/photopreview.tsx @@ -39,14 +39,12 @@ const PhotoPreview = ({ const fixOrientation = async () => { try { setProcessing(true); - console.log('Photo URI:', photo.uri); console.log('Photo EXIF orientation:', photo.exif?.Orientation); let rotation = 0; const exifOrientation = photo.exif?.Orientation || 6; - // Apply 180 degree fix for landscape orientations if (exifOrientation === 1) { rotation = 90; console.log('Applying 90 rotation for landscape'); @@ -61,25 +59,22 @@ const PhotoPreview = ({ } if (rotation !== 0) { - manipulator.rotate(rotation); // Use the hook from top level - const imageRef = await manipulator.renderAsync(); + manipulator.rotate(rotation); + } - const result = await imageRef.saveAsync({ - compress: 0.7, - format: SaveFormat.JPEG, - }); + const imageRef = await manipulator.renderAsync(); - console.log('Corrected photo URI:', result.uri); - setCorrectedPhoto(result.uri); - } else { - setCorrectedPhoto(photo.uri); - } + const result = await imageRef.saveAsync({ + compress: 0.35, + format: SaveFormat.JPEG, + }); + + setCorrectedPhoto(result.uri); } catch (error) { console.error('Error fixing orientation:', error); setCorrectedPhoto(photo.uri); - } finally { - setProcessing(false); } + setProcessing(false); }; const handleUpload = async () => { @@ -135,6 +130,7 @@ const PhotoPreview = ({ {processing ? ( + Processing Photo! ) : ( <> From 9838f07e734c682e7a93084a5bd0a142117617f2 Mon Sep 17 00:00:00 2001 From: Kevin Loritsch Date: Sat, 29 Nov 2025 12:05:15 -0800 Subject: [PATCH 3/9] choosetask spacing --- src/app/main/camera/chooseTask.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/app/main/camera/chooseTask.tsx b/src/app/main/camera/chooseTask.tsx index 003267c..b859b15 100644 --- a/src/app/main/camera/chooseTask.tsx +++ b/src/app/main/camera/chooseTask.tsx @@ -94,7 +94,7 @@ const ChooseTask = () => { return ( @@ -108,14 +108,11 @@ const ChooseTask = () => { }, }) }> - - - {task.task_name} {task.recurring ? '(Recurring)' : ''}{' '} - {task.weekly ? '(Weekly)' : ''} {taskCompleted ? '(Completed)' : ''} - - + + {task.task_name} {task.recurring ? '(Recurring)' : ''}{' '} + {task.weekly ? '(Weekly)' : ''} {taskCompleted ? '(Completed)' : ''} + ); })} From d56d4e7f0994fd4a64fe60dfbeaac141237eb9e5 Mon Sep 17 00:00:00 2001 From: Kevin Loritsch Date: Sat, 29 Nov 2025 12:06:33 -0800 Subject: [PATCH 4/9] fixed spacing --- src/app/main/groups/[groupId]/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/main/groups/[groupId]/index.tsx b/src/app/main/groups/[groupId]/index.tsx index 66ab394..c334714 100644 --- a/src/app/main/groups/[groupId]/index.tsx +++ b/src/app/main/groups/[groupId]/index.tsx @@ -163,7 +163,7 @@ const GroupPage = () => { /> - + {groupNameState} Leave Group From de7f30a86de56ff5e8bd695b1e675fccd9301b84 Mon Sep 17 00:00:00 2001 From: Kevin Loritsch Date: Sat, 29 Nov 2025 16:28:24 -0800 Subject: [PATCH 5/9] badge for incoming, pending observer --- src/app/main/_layout.tsx | 14 +++++++++++++- src/app/main/camera/chooseTask.tsx | 6 ++++-- src/components/camera/photopreview.tsx | 1 + src/components/groups/circleProgress.tsx | 2 +- src/controllers/getFriends.ts | 18 ++++++++++++++++++ 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/app/main/_layout.tsx b/src/app/main/_layout.tsx index 062d5a4..2f56054 100644 --- a/src/app/main/_layout.tsx +++ b/src/app/main/_layout.tsx @@ -1,19 +1,30 @@ import { Tabs, useSegments } from 'expo-router'; import '../../../global.css'; -import React from 'react'; import { TouchableOpacity, View } from 'react-native'; import { StatusBar } from 'expo-status-bar'; +import React, { useState, useEffect, useCallback } from 'react'; +import { getFriendsList } from '@/controllers/getFriends'; import AntDesign from '@expo/vector-icons/AntDesign'; import Feather from '@expo/vector-icons/Feather'; import Ionicons from '@expo/vector-icons/Ionicons'; export default function RootLayout() { + const [incomingRequests, setIncoming] = useState(0); const segments = useSegments(); const hideTabBar = segments.includes('camera') || segments.includes('groups') || segments.includes('tasks'); + const loadFriends = useCallback(async () => { + const incomingRes = await getFriendsList.getIncomingFriendRequestsCount(); + setIncoming(incomingRes); + }, [setIncoming]); + + useEffect(() => { + loadFriends(); + }, [loadFriends]); + return ( @@ -68,6 +79,7 @@ export default function RootLayout() { options={{ tabBarIcon: ({ color, size }) => , tabBarButton: (props) => , + tabBarBadge: incomingRequests && incomingRequests > 0 ? incomingRequests : undefined, }} /> { - Select Task for {group_name} - + + Select Task for + {group_name} + diff --git a/src/components/camera/photopreview.tsx b/src/components/camera/photopreview.tsx index 6cb3b5e..f655b6f 100644 --- a/src/components/camera/photopreview.tsx +++ b/src/components/camera/photopreview.tsx @@ -148,6 +148,7 @@ const PhotoPreview = ({ {uploading && ( + Uploading Photo! )} diff --git a/src/components/groups/circleProgress.tsx b/src/components/groups/circleProgress.tsx index 2c91c83..a4c1d33 100644 --- a/src/components/groups/circleProgress.tsx +++ b/src/components/groups/circleProgress.tsx @@ -37,7 +37,7 @@ const CircularProgress = ({ percentage }: CircularProgressProps) => { /> - {percentage}% + {percentage}% ); diff --git a/src/controllers/getFriends.ts b/src/controllers/getFriends.ts index 6e53e23..4dbaa94 100644 --- a/src/controllers/getFriends.ts +++ b/src/controllers/getFriends.ts @@ -107,6 +107,24 @@ class GetFriendController { return requests; } + async getIncomingFriendRequestsCount(): Promise { + const { + data: { user }, + error: authError, + } = await supabase.auth.getUser(); + if (!user) throw authError; + + const userId = user.id; + const { data, error } = await supabase + .from(this.friendRequestsTableName) + .select('sender_id, users!sender_id(user_id, username)') + .eq('recipient_id', userId); + + if (error) throw error; + + return data.length; + } + async getOutgoingFriendRequests(): Promise { const { data: { user }, From c75e706fd7520801f0fd06d796b8f363f59fba32 Mon Sep 17 00:00:00 2001 From: Kevin Loritsch Date: Sat, 29 Nov 2025 20:53:50 -0800 Subject: [PATCH 6/9] base observer model --- src/app/main/_layout.tsx | 12 ++++++ src/app/main/camera/chooseGroup.tsx | 2 +- src/app/main/groups/[groupId]/createTask.tsx | 10 ++++- src/app/main/groups/[groupId]/index.tsx | 1 - src/app/main/index.tsx | 31 +++++++++++++- src/components/camera/photopreview.tsx | 10 ++--- src/components/exploreUI.tsx | 44 ++++++++++++++++---- src/controllers/completeTask.ts | 11 +++++ src/controllers/group.ts | 38 ++++++++++------- src/controllers/photoUpload.ts | 13 +++++- src/controllers/taskCompletionObserver.ts | 32 ++++++++++++++ src/controllers/uiObservers.ts | 36 ++++++++++++++++ 12 files changed, 204 insertions(+), 36 deletions(-) create mode 100644 src/controllers/taskCompletionObserver.ts create mode 100644 src/controllers/uiObservers.ts diff --git a/src/app/main/_layout.tsx b/src/app/main/_layout.tsx index 2f56054..d910055 100644 --- a/src/app/main/_layout.tsx +++ b/src/app/main/_layout.tsx @@ -4,6 +4,8 @@ import { TouchableOpacity, View } from 'react-native'; import { StatusBar } from 'expo-status-bar'; import React, { useState, useEffect, useCallback } from 'react'; import { getFriendsList } from '@/controllers/getFriends'; +import { completedTasksController } from '@/controllers/completeTask'; +import { groupController } from '@/controllers/group'; import AntDesign from '@expo/vector-icons/AntDesign'; import Feather from '@expo/vector-icons/Feather'; @@ -21,10 +23,20 @@ export default function RootLayout() { setIncoming(incomingRes); }, [setIncoming]); + const initializeObservers = () => { + console.log('Initializing observers'); + completedTasksController.initialize(); + groupController.initialize(); + }; + useEffect(() => { loadFriends(); }, [loadFriends]); + useEffect(() => { + initializeObservers(); + }, []); + return ( diff --git a/src/app/main/camera/chooseGroup.tsx b/src/app/main/camera/chooseGroup.tsx index 6c01b20..b717df6 100644 --- a/src/app/main/camera/chooseGroup.tsx +++ b/src/app/main/camera/chooseGroup.tsx @@ -24,7 +24,7 @@ const ChooseGroup = () => { const fetchGroupData = async () => { try { - const allGroups = await getAllGroups.fetchUserData(); + const allGroups = await getAllGroups.fetchUserData('Choose group'); setGroups(allGroups); setFiltered(allGroups); } catch (err) { diff --git a/src/app/main/groups/[groupId]/createTask.tsx b/src/app/main/groups/[groupId]/createTask.tsx index 983e57b..ec0f0f3 100644 --- a/src/app/main/groups/[groupId]/createTask.tsx +++ b/src/app/main/groups/[groupId]/createTask.tsx @@ -42,6 +42,11 @@ const CreateTask = () => { setLoading(false); }; + const handleTaskNameChange = (text: string) => { + const filtered = text.replace(/[^a-zA-Z0-9 ]/g, ''); + setTask(filtered); + }; + if (loading) { return ( @@ -52,7 +57,8 @@ const CreateTask = () => { } const handleCreateTask = async () => { - const result = await taskController.createTask(groupId, task, recurring, weekly); + const sanitizedTask = task.replace(/[^a-zA-Z0-9 ]/g, '').trim(); + const result = await taskController.createTask(groupId, sanitizedTask, recurring, weekly); setUploading(true); if (result.success) { @@ -108,7 +114,7 @@ const CreateTask = () => { { {tasks.map((task, idx) => { const taskCompleted = isCompleted(task.task_name, groupId); - console.log(task); return ( { loadPageData(); + registerHomeObserver((data) => { + console.log('Home: Observer, adding photos to relevant states', data); + + setGroupsWithPhotos((prevGroups) => + prevGroups.map((group) => { + if (group.group_id === data.groupId) { + return { + ...group, + current_points: group.current_points + 1, + photos: [ + { + name: data.photoUri.split('/').pop() || '', + publicUrl: data.photoUri, + createdAt: data.timestamp, + username: name, + taskName: data.taskName, + }, + ...group.photos, + ], + }; + } + return group; + }) + ); + }); + + return () => unregisterHomeObserver(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const loadPageData = async () => { @@ -44,7 +73,7 @@ export default function Index() { const userName = await userController.getName(); if (userName) setName(userName); - const allGroups = await getAllGroups.fetchUserData(); + const allGroups = await getAllGroups.fetchUserData('Home'); const groupIds = allGroups.map((g) => g.group_id); diff --git a/src/components/camera/photopreview.tsx b/src/components/camera/photopreview.tsx index f655b6f..e15447c 100644 --- a/src/components/camera/photopreview.tsx +++ b/src/components/camera/photopreview.tsx @@ -6,8 +6,6 @@ import { SaveFormat, useImageManipulator } from 'expo-image-manipulator'; import Tethr from '@/components/tethr'; import { storagePush } from '@/controllers/photoUpload'; import { userController } from '@/controllers/userInfo'; -import { completedTasksController } from '@/controllers/completeTask'; -import { groupController } from '@/controllers/group'; import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons'; @@ -87,18 +85,16 @@ const PhotoPreview = ({ return; } - await completedTasksController.addTask(task_name, group_id, weekly); - await groupController.increaseMemberScore(userId, group_id); - - storagePush.uploadImage({ + await storagePush.uploadImage({ uri: correctedPhoto, userId: userId, groupId: group_id, taskName: task_name, + weekly: weekly, }); handleRetakePhoto(); - router.push('/'); + router.replace('/'); } catch (err: any) { console.error('Upload error:', err); } finally { diff --git a/src/components/exploreUI.tsx b/src/components/exploreUI.tsx index bdfb9d6..97d563b 100644 --- a/src/components/exploreUI.tsx +++ b/src/components/exploreUI.tsx @@ -1,7 +1,8 @@ import { View, FlatList, ActivityIndicator, Text, RefreshControl } from 'react-native'; -import { useState, useEffect, useMemo } from 'react'; +import { useState, useEffect, useMemo, useRef } from 'react'; import { photoRetrieve, PhotoSubmission } from '@/controllers/photoRetrieve'; -import { getAllGroups } from '@/controllers/group'; +import { groupController } from '@/controllers/group'; +import { registerExploreObserver, unregisterExploreObserver } from '@/controllers/uiObservers'; import Tethr from '@/components/tethr'; import Fyp from '@/components/fyp'; @@ -16,14 +17,21 @@ export default function ExploreUI() { const [refreshing, setRefreshing] = useState(false); const [photos, setPhotos] = useState([]); const [searchQuery, setSearchQuery] = useState(''); + const groupsMapRef = useRef>({}); const loadPhotos = async () => { try { setLoading(true); - const allGroups = await getAllGroups.fetchUserData(); + const allGroups = await groupController.fetchUserData('EXPLORE_SCREEN'); const groupIds = allGroups.map((g) => g.group_id); + const gMap: Record = {}; + allGroups.forEach((g) => { + gMap[g.group_id] = g.group_name; + }); + groupsMapRef.current = gMap; + if (groupIds.length > 0) { const allPhotos = await photoRetrieve.getPhotosByGroups(groupIds); const photosWithGroupNames = allPhotos.map((photo) => { @@ -53,8 +61,26 @@ export default function ExploreUI() { useEffect(() => { loadPhotos(); - }, []); + registerExploreObserver((data) => { + console.log('Explore: Observer, adding photos to relevant states...', data); + + const newPhoto: PhotoWithGroup = { + name: data.photoUri.split('/').pop() || '', + publicUrl: data.photoUri, + createdAt: data.timestamp, + groupId: data.groupId, + userId: data.userId, + username: 'You', + taskName: data.taskName, + groupName: groupsMapRef.current[data.groupId] || 'Unknown Group', + }; + + setPhotos((prev) => [newPhoto, ...prev]); + }); + + return () => unregisterExploreObserver(); + }, []); const filteredPhotos = useMemo(() => { if (!searchQuery.trim()) return photos; @@ -83,11 +109,11 @@ export default function ExploreUI() { data={filteredPhotos} keyExtractor={(item) => item.name} renderItem={({ item }) => { - console.log('Photo item:', { - groupId: item.groupId, - groupName: item.groupName, - taskName: item.taskName, - }); + // console.log('Photo item:', { + // groupId: item.groupId, + // groupName: item.groupName, + // taskName: item.taskName, + // }); return ( diff --git a/src/controllers/completeTask.ts b/src/controllers/completeTask.ts index 111f000..b4f7b5c 100644 --- a/src/controllers/completeTask.ts +++ b/src/controllers/completeTask.ts @@ -1,4 +1,5 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; +import { taskCompletionObserver, TaskCompletionData } from '@/controllers/taskCompletionObserver'; const getWeekKey = () => { const now = new Date(); @@ -8,6 +9,16 @@ const getWeekKey = () => { }; export const completedTasksController = { + initialized: false, + initialize: () => { + if (completedTasksController.initialized) return; + completedTasksController.initialized = true; + taskCompletionObserver.subscribe(async (data: TaskCompletionData) => { + console.log('Tasks: Observer, storing completion in local data...'); + await completedTasksController.addTask(data.taskName, data.groupId, data.weekly); + }); + }, + addTask: async (taskName: string, groupId: string, weekly: boolean) => { try { const taskKey = `${groupId}-${taskName}`; diff --git a/src/controllers/group.ts b/src/controllers/group.ts index 91ea39d..6a0d34d 100644 --- a/src/controllers/group.ts +++ b/src/controllers/group.ts @@ -1,4 +1,5 @@ import { supabase } from '@/lib/supabase'; +import { taskCompletionObserver, TaskCompletionData } from '@/controllers/taskCompletionObserver'; interface GroupType { group_id: string; @@ -14,6 +15,19 @@ interface LeaderboardEntry { } class GroupController { + private initialized = false; + + initialize() { + if (this.initialized) { + console.log('Reinitialization check: already done'); + return; + } + this.initialized = true; + taskCompletionObserver.subscribe(async (data: TaskCompletionData) => { + console.log('Scores: Observer, increasing relevant scores...'); + await this.increaseMemberScore(data.userId, data.groupId); + }); + } async getGroupName(groupId: string): Promise { const { data: group, error } = await supabase .from('groups') @@ -25,17 +39,16 @@ class GroupController { return group?.group_name || null; } - async fetchUserData(): Promise { + async fetchUserData(source?: string): Promise { + console.log(`fetchUserData called by: ${source || 'unknown'}`); try { const { data: sessionData, error: sessionError } = await supabase.auth.getSession(); if (sessionError) { - console.error('Error fetching session:', sessionError); return []; } const user = sessionData?.session?.user; if (!user) { - console.log('No user logged in.'); return []; } @@ -45,20 +58,17 @@ class GroupController { .eq('user_id', user.id); if (groupError) console.error('Error fetching groups:', groupError); - else { - const formattedGroups: GroupType[] = (groupData || []).map((item: any) => ({ - group_id: item.group_id, - group_name: item.groups?.group_name || 'N/A Group Name', - current_points: item.current_points, - })); - - return formattedGroups; - } + const formattedGroups: GroupType[] = (groupData || []).map((item: any) => ({ + group_id: item.group_id, + group_name: item.groups?.group_name || 'N/A Group Name', + current_points: item.current_points, + })); + + return formattedGroups; } catch (err) { console.error('Unexpected error fetching data:', err); return []; } - return []; } async getLeaderboardData(groupId: string): Promise { @@ -253,4 +263,4 @@ class GroupController { } export const groupController = new GroupController(); -export const getAllGroups = new GroupController(); +export const getAllGroups = groupController; diff --git a/src/controllers/photoUpload.ts b/src/controllers/photoUpload.ts index b736945..b38ac7b 100644 --- a/src/controllers/photoUpload.ts +++ b/src/controllers/photoUpload.ts @@ -1,11 +1,13 @@ import { supabase } from '@/lib/supabase'; import { imageCompressor } from '@/utils/compress'; +import { taskCompletionObserver } from '@/controllers/taskCompletionObserver'; export interface UploadParams { uri: string; userId: string; groupId: string; taskName: string; + weekly: boolean; } class StoragePushController { @@ -13,7 +15,7 @@ class StoragePushController { async uploadImage(params: UploadParams) { try { - const { uri, userId, groupId, taskName } = params; + const { uri, userId, groupId, taskName, weekly } = params; const compressedUri = await imageCompressor.compress(uri, { maxWidth: 1080, @@ -34,6 +36,15 @@ class StoragePushController { const { data } = supabase.storage.from(this.bucketName).getPublicUrl(fileName); + taskCompletionObserver.notify({ + taskName, + groupId, + userId, + photoUri: data.publicUrl, + weekly, + timestamp: new Date().toISOString(), + }); + return { success: true, url: data.publicUrl }; } catch (err: any) { return { success: false, error: err.message }; diff --git a/src/controllers/taskCompletionObserver.ts b/src/controllers/taskCompletionObserver.ts new file mode 100644 index 0000000..3a767b0 --- /dev/null +++ b/src/controllers/taskCompletionObserver.ts @@ -0,0 +1,32 @@ +class TaskCompletionObserver { + private observers: ((data: TaskCompletionData) => void)[] = []; + + subscribe(observer: (data: TaskCompletionData) => void) { + this.observers.push(observer); + return () => { + this.observers = this.observers.filter((obs) => obs !== observer); + }; + } + + notify(data: TaskCompletionData) { + console.log('Notifying all observers:', data); + this.observers.forEach((observer) => { + try { + observer(data); + } catch (error) { + console.error('Observer error:', error); + } + }); + } +} + +export interface TaskCompletionData { + taskName: string; + groupId: string; + userId: string; + photoUri: string; + weekly: boolean; + timestamp: string; +} + +export const taskCompletionObserver = new TaskCompletionObserver(); diff --git a/src/controllers/uiObservers.ts b/src/controllers/uiObservers.ts new file mode 100644 index 0000000..606e244 --- /dev/null +++ b/src/controllers/uiObservers.ts @@ -0,0 +1,36 @@ +import { taskCompletionObserver, TaskCompletionData } from '@/controllers/taskCompletionObserver'; + +let homeUpdateCallback: ((data: TaskCompletionData) => void) | null = null; +let exploreUpdateCallback: ((data: TaskCompletionData) => void) | null = null; + +taskCompletionObserver.subscribe((data) => { + console.log('Call to update UI:', data); + if (homeUpdateCallback) { + homeUpdateCallback(data); + } + if (exploreUpdateCallback) { + exploreUpdateCallback(data); + } +}); + +// Home screen registration +export const registerHomeObserver = (callback: (data: TaskCompletionData) => void) => { + console.log('Home observer registered'); + homeUpdateCallback = callback; +}; + +export const unregisterHomeObserver = () => { + console.log('Home observer unregistered'); + homeUpdateCallback = null; +}; + +// Explore screen registration +export const registerExploreObserver = (callback: (data: TaskCompletionData) => void) => { + console.log('Explore observer registered'); + exploreUpdateCallback = callback; +}; + +export const unregisterExploreObserver = () => { + console.log('Explore observer unregistered'); + exploreUpdateCallback = null; +}; From d429af95d5f35fdc64c53a1b5c6caa7e79a1cbc6 Mon Sep 17 00:00:00 2001 From: Kevin Loritsch Date: Sat, 29 Nov 2025 21:11:10 -0800 Subject: [PATCH 7/9] observer for task badge --- src/app/main/_layout.tsx | 19 ++++++++++-- src/app/main/friends.tsx | 14 ++++++++- src/app/main/index.tsx | 2 +- src/components/exploreUI.tsx | 5 +++- src/controllers/completeTask.ts | 5 +++- src/controllers/group.ts | 5 +++- .../observers/friendRequestObserver.ts | 29 +++++++++++++++++++ .../{ => observers}/taskCompletionObserver.ts | 2 +- .../{ => observers}/uiObservers.ts | 5 +++- src/controllers/photoUpload.ts | 2 +- 10 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 src/controllers/observers/friendRequestObserver.ts rename src/controllers/{ => observers}/taskCompletionObserver.ts (92%) rename src/controllers/{ => observers}/uiObservers.ts (90%) diff --git a/src/app/main/_layout.tsx b/src/app/main/_layout.tsx index d910055..91d4950 100644 --- a/src/app/main/_layout.tsx +++ b/src/app/main/_layout.tsx @@ -6,6 +6,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { getFriendsList } from '@/controllers/getFriends'; import { completedTasksController } from '@/controllers/completeTask'; import { groupController } from '@/controllers/group'; +import { friendRequestObserver } from '@/controllers/observers/friendRequestObserver'; import AntDesign from '@expo/vector-icons/AntDesign'; import Feather from '@expo/vector-icons/Feather'; @@ -23,8 +24,8 @@ export default function RootLayout() { setIncoming(incomingRes); }, [setIncoming]); - const initializeObservers = () => { - console.log('Initializing observers'); + const initializeTaskObservers = () => { + console.log('Initializing task observers'); completedTasksController.initialize(); groupController.initialize(); }; @@ -34,7 +35,19 @@ export default function RootLayout() { }, [loadFriends]); useEffect(() => { - initializeObservers(); + initializeTaskObservers(); + + const unsubscribe = friendRequestObserver.subscribe((data) => { + console.log('Layout observer: reduce 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)); + } + }); + + return () => unsubscribe(); }, []); return ( diff --git a/src/app/main/friends.tsx b/src/app/main/friends.tsx index aaa79b0..0b7bded 100644 --- a/src/app/main/friends.tsx +++ b/src/app/main/friends.tsx @@ -14,6 +14,7 @@ import SearchBar from '@/components/searchbar'; import { useRouter } from 'expo-router'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { getCardType } from '@/utils/cardType'; +import { friendRequestObserver } from '@/controllers/observers/friendRequestObserver'; export default function FriendsScreen() { const [friends, setFriends] = useState([]); @@ -117,10 +118,21 @@ export default function FriendsScreen() { }; const handleAcceptRequest = async (friendId: string) => { try { - setFriends((prev) => prev.filter((f) => f.userId !== friendId)); + const acceptedFriend = incomingRequests.find((f) => f.userId === friendId); + setIncoming((prev) => prev.filter((f) => f.userId !== friendId)); setOutgoing((prev) => prev.filter((f) => f.userId !== friendId)); + if (acceptedFriend) { + setFriends((prev) => [...prev, { ...acceptedFriend, buttonText: 'Remove' }]); + } + + friendRequestObserver.notify({ + action: 'accept', + friendId, + username: acceptedFriend?.username, + }); + await getFriendsList.acceptRequest(friendId); } catch (error) { console.error(error); diff --git a/src/app/main/index.tsx b/src/app/main/index.tsx index 09b8a16..aec2392 100644 --- a/src/app/main/index.tsx +++ b/src/app/main/index.tsx @@ -7,7 +7,7 @@ import { photoRetrieve } from '@/controllers/photoRetrieve'; import { getAllGroups } from '@/controllers/group'; import { taskController, Task } from '@/controllers/tasks'; import { LinearGradient } from 'expo-linear-gradient'; -import { registerHomeObserver, unregisterHomeObserver } from '@/controllers/uiObservers'; +import { registerHomeObserver, unregisterHomeObserver } from '@/controllers/observers/uiObservers'; import Tethr from '@/components/tethr'; import Groups from '@/components/groups/groups'; diff --git a/src/components/exploreUI.tsx b/src/components/exploreUI.tsx index 97d563b..2ac08fa 100644 --- a/src/components/exploreUI.tsx +++ b/src/components/exploreUI.tsx @@ -2,7 +2,10 @@ import { View, FlatList, ActivityIndicator, Text, RefreshControl } from 'react-n import { useState, useEffect, useMemo, useRef } from 'react'; import { photoRetrieve, PhotoSubmission } from '@/controllers/photoRetrieve'; import { groupController } from '@/controllers/group'; -import { registerExploreObserver, unregisterExploreObserver } from '@/controllers/uiObservers'; +import { + registerExploreObserver, + unregisterExploreObserver, +} from '@/controllers/observers/uiObservers'; import Tethr from '@/components/tethr'; import Fyp from '@/components/fyp'; diff --git a/src/controllers/completeTask.ts b/src/controllers/completeTask.ts index b4f7b5c..e8dc236 100644 --- a/src/controllers/completeTask.ts +++ b/src/controllers/completeTask.ts @@ -1,5 +1,8 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { taskCompletionObserver, TaskCompletionData } from '@/controllers/taskCompletionObserver'; +import { + taskCompletionObserver, + TaskCompletionData, +} from '@/controllers/observers/taskCompletionObserver'; const getWeekKey = () => { const now = new Date(); diff --git a/src/controllers/group.ts b/src/controllers/group.ts index 6a0d34d..2a1c5ca 100644 --- a/src/controllers/group.ts +++ b/src/controllers/group.ts @@ -1,5 +1,8 @@ import { supabase } from '@/lib/supabase'; -import { taskCompletionObserver, TaskCompletionData } from '@/controllers/taskCompletionObserver'; +import { + taskCompletionObserver, + TaskCompletionData, +} from '@/controllers/observers/taskCompletionObserver'; interface GroupType { group_id: string; diff --git a/src/controllers/observers/friendRequestObserver.ts b/src/controllers/observers/friendRequestObserver.ts new file mode 100644 index 0000000..2940904 --- /dev/null +++ b/src/controllers/observers/friendRequestObserver.ts @@ -0,0 +1,29 @@ +export interface FriendRequestData { + action: 'accept' | 'reject' | 'cancel'; + friendId: string; + username?: string; +} + +class FriendRequestObserver { + private observers: ((data: FriendRequestData) => void)[] = []; + + subscribe(observer: (data: FriendRequestData) => void) { + this.observers.push(observer); + return () => { + this.observers = this.observers.filter((obs) => obs !== observer); + }; + } + + notify(data: FriendRequestData) { + console.log('Notifying all friendrequest observers:', data); + this.observers.forEach((observer) => { + try { + observer(data); + } catch (error) { + console.error('Friend request observer error:', error); + } + }); + } +} + +export const friendRequestObserver = new FriendRequestObserver(); diff --git a/src/controllers/taskCompletionObserver.ts b/src/controllers/observers/taskCompletionObserver.ts similarity index 92% rename from src/controllers/taskCompletionObserver.ts rename to src/controllers/observers/taskCompletionObserver.ts index 3a767b0..e142bdc 100644 --- a/src/controllers/taskCompletionObserver.ts +++ b/src/controllers/observers/taskCompletionObserver.ts @@ -9,7 +9,7 @@ class TaskCompletionObserver { } notify(data: TaskCompletionData) { - console.log('Notifying all observers:', data); + console.log('Notifying all task observers:', data); this.observers.forEach((observer) => { try { observer(data); diff --git a/src/controllers/uiObservers.ts b/src/controllers/observers/uiObservers.ts similarity index 90% rename from src/controllers/uiObservers.ts rename to src/controllers/observers/uiObservers.ts index 606e244..e8608dc 100644 --- a/src/controllers/uiObservers.ts +++ b/src/controllers/observers/uiObservers.ts @@ -1,4 +1,7 @@ -import { taskCompletionObserver, TaskCompletionData } from '@/controllers/taskCompletionObserver'; +import { + taskCompletionObserver, + TaskCompletionData, +} from '@/controllers/observers/taskCompletionObserver'; let homeUpdateCallback: ((data: TaskCompletionData) => void) | null = null; let exploreUpdateCallback: ((data: TaskCompletionData) => void) | null = null; diff --git a/src/controllers/photoUpload.ts b/src/controllers/photoUpload.ts index b38ac7b..634461e 100644 --- a/src/controllers/photoUpload.ts +++ b/src/controllers/photoUpload.ts @@ -1,6 +1,6 @@ import { supabase } from '@/lib/supabase'; import { imageCompressor } from '@/utils/compress'; -import { taskCompletionObserver } from '@/controllers/taskCompletionObserver'; +import { taskCompletionObserver } from '@/controllers/observers/taskCompletionObserver'; export interface UploadParams { uri: string; From 8c69444ee302596792c181e6d14a8bc7ee409c36 Mon Sep 17 00:00:00 2001 From: Kevin Loritsch Date: Sun, 30 Nov 2025 13:42:37 -0800 Subject: [PATCH 8/9] weekly tasks deletion fixed for non recurring --- supabase/table_setup.SQL | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/supabase/table_setup.SQL b/supabase/table_setup.SQL index 1e297cc..f8a7e93 100644 --- a/supabase/table_setup.SQL +++ b/supabase/table_setup.SQL @@ -222,8 +222,7 @@ BEGIN DELETE FROM public.tasks WHERE recurring = false AND weekly = true - AND DATE(created_at AT TIME ZONE 'America/Los_Angeles') < - DATE(NOW() AT TIME ZONE 'America/Los_Angeles') - INTERVAL '6 days'; + AND EXTRACT(DOW FROM NOW() AT TIME ZONE 'America/Los_Angeles') = 0; END; $$; From 8e02624ff01af1eb4a0a36504ad4a2f5d85e9b92 Mon Sep 17 00:00:00 2001 From: Kevin Loritsch Date: Sun, 30 Nov 2025 13:50:58 -0800 Subject: [PATCH 9/9] initial load uses splash screen --- package-lock.json | 13 +++++++++++++ package.json | 1 + src/app/main/index.tsx | 9 +++++++++ 3 files changed, 23 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9079d8c..bc6b2e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "expo-linear-gradient": "~15.0.7", "expo-linking": "~8.0.9", "expo-router": "~6.0.15", + "expo-splash-screen": "~31.0.11", "expo-status-bar": "~3.0.8", "nativewind": "^4.1.23", "prettier-plugin-tailwindcss": "^0.7.1", @@ -7641,6 +7642,18 @@ "node": ">=20.16.0" } }, + "node_modules/expo-splash-screen": { + "version": "31.0.11", + "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-31.0.11.tgz", + "integrity": "sha512-D7MQflYn/PAN3+fACSyxHO4oxZMBezllbgFdVY8roAS1gXpCy8SS6LrGHTD0VpOPEp3X4Gn7evTnXSI9nFoI5Q==", + "license": "MIT", + "dependencies": { + "@expo/prebuild-config": "^54.0.6" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-status-bar": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.8.tgz", diff --git a/package.json b/package.json index 184f0ab..5527630 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "expo-linear-gradient": "~15.0.7", "expo-linking": "~8.0.9", "expo-router": "~6.0.15", + "expo-splash-screen": "~31.0.11", "expo-status-bar": "~3.0.8", "nativewind": "^4.1.23", "prettier-plugin-tailwindcss": "^0.7.1", diff --git a/src/app/main/index.tsx b/src/app/main/index.tsx index aec2392..be319ac 100644 --- a/src/app/main/index.tsx +++ b/src/app/main/index.tsx @@ -8,6 +8,7 @@ import { getAllGroups } from '@/controllers/group'; import { taskController, Task } from '@/controllers/tasks'; import { LinearGradient } from 'expo-linear-gradient'; import { registerHomeObserver, unregisterHomeObserver } from '@/controllers/observers/uiObservers'; +import * as SplashScreen from 'expo-splash-screen'; import Tethr from '@/components/tethr'; import Groups from '@/components/groups/groups'; @@ -27,6 +28,8 @@ interface GroupWithPhotos { }[]; } +SplashScreen.preventAutoHideAsync(); + export default function Index() { const [name, setName] = useState(''); const [loading, setLoading] = useState(true); @@ -66,6 +69,12 @@ export default function Index() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + if (!loading) { + SplashScreen.hideAsync(); + } + }, [loading]); + const loadPageData = async () => { try { setLoading(true);