From 487edb4a62e2e1a3fdcdcab14f5dad67c42d4503 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Fri, 20 Feb 2026 20:10:07 -0500 Subject: [PATCH] Improve feed/profile cache logs and add Expo app agent notes --- Agent.md | 113 +++++++++++++++++++++++++++++++++++++++++++++++ Views/Feed.js | 34 +++++++++++--- Views/Profile.js | 73 +++++++++++++++++++++++++----- 3 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 Agent.md diff --git a/Agent.md b/Agent.md new file mode 100644 index 0000000..7a782b6 --- /dev/null +++ b/Agent.md @@ -0,0 +1,113 @@ +# EMI Expo App Agent Notes + +This file summarizes the current `/Users/adolforeyna/Projects/EMI/expoApp` codebase so future work can start with context. + +## What This App Is + +- Expo React Native app for EMI Fellowship social network (iOS + Android + limited web support). +- Main features: auth, feed/posts/comments, profile pages, groups, courses, invite flow, notifications, media playback/upload. +- Backend is expected to be the EMI API and cookie-authenticated. + +## Run + Build Basics + +- Install: `npm install` +- Start dev server: `npm start` +- Device targets: `npm run ios`, `npm run android` +- Expo config: `/Users/adolforeyna/Projects/EMI/expoApp/app.json` +- EAS config: `/Users/adolforeyna/Projects/EMI/expoApp/eas.json` + +## High-Level Architecture + +- App entry + navigation: `/Users/adolforeyna/Projects/EMI/expoApp/App.js` +- API abstraction layer: `/Users/adolforeyna/Projects/EMI/expoApp/API.js` +- Global state (Valtio proxy): `/Users/adolforeyna/Projects/EMI/expoApp/contexts/GlobalState.js` +- Screen modules: `/Users/adolforeyna/Projects/EMI/expoApp/Views/*` +- Reusable UI pieces: `/Users/adolforeyna/Projects/EMI/expoApp/components/*` + +### Navigation Shape + +- Root stack (`createNativeStackNavigator`) contains: + - `MainNavigation` (bottom tabs) + - Details/screens like `Profile`, `SinglePost`, `Search`, `Menu`, `Notifications`, `ProfileSettings`, etc. +- Bottom tabs (`createBottomTabNavigator`) inside `MainNavigation`: + - `Feed`, `Groups`, `NewPost`, `Courses`, `MyProfile` +- Custom header in `App.js` provides menu/search/notifications shortcuts globally. + +## State + Data Flow + +- Server source of truth via `API.js`. +- `GlobalState.me` stores active viewer profile and is refreshed: + - on Feed load + - periodically in `MainNavigation` (30s interval) + - on profile switch in Menu +- Local caching with `AsyncStorage`: + - feed cache + - profile post cache + - courses cache + - profile/name cache in card/name components +- `API.js` has defensive networking: + - fallback responses on non-OK and invalid JSON + - request error cooldown logging + - profile failure cooldown cache to avoid repeated failing calls + +## API Layer Notes + +- Base URL default in `/Users/adolforeyna/Projects/EMI/expoApp/API.js`: + - `https://emiapi.reynafamily.com` + - can be overridden with `global.baseUrl` +- Uses `fetch(..., { credentials: 'include' })`, so backend cookie/CORS config must be correct. +- Covers endpoints for: + - auth/session + - posts/comments/reactions/bookmarks + - profiles/follow/change profile + - groups/courses + - invites + - payments + - notifications token registration + +## Feature Map (Key Files) + +- Login/register UI: `/Users/adolforeyna/Projects/EMI/expoApp/Views/Login.js`, `/Users/adolforeyna/Projects/EMI/expoApp/components/Login.js`, `/Users/adolforeyna/Projects/EMI/expoApp/components/Register.js` +- Feed: `/Users/adolforeyna/Projects/EMI/expoApp/Views/Feed.js` +- Post rendering/interactions: `/Users/adolforeyna/Projects/EMI/expoApp/components/Post.js` +- Media parser/player/slideshow handoff: `/Users/adolforeyna/Projects/EMI/expoApp/components/Media.js` +- Profile page + tagged profile posts: `/Users/adolforeyna/Projects/EMI/expoApp/Views/Profile.js` +- Create post + image uploads: `/Users/adolforeyna/Projects/EMI/expoApp/Views/NewPost.js` +- Groups + courses browsing: `/Users/adolforeyna/Projects/EMI/expoApp/Views/Groups.js`, `/Users/adolforeyna/Projects/EMI/expoApp/Views/Courses.js` +- Profile settings and invite flow: `/Users/adolforeyna/Projects/EMI/expoApp/Views/ProfileSettings.js`, `/Users/adolforeyna/Projects/EMI/expoApp/Views/Invite.js` + +## Integrations In Use + +- Expo Notifications (`expo-notifications`) including push token registration. +- Expo Updates (`expo-updates`) check/fetch/reload in Feed (disabled in `__DEV__`). +- PostHog analytics: + - setup in `/Users/adolforeyna/Projects/EMI/expoApp/PostHog.js` + - provider in `/Users/adolforeyna/Projects/EMI/expoApp/App.js` +- Media upload currently points to `https://social.emmint.com/upload.php` in multiple places. + +## Known Risks / Footguns (Current Code) + +- Missing imports likely to crash at runtime: + - `AsyncStorage` used but not imported in `/Users/adolforeyna/Projects/EMI/expoApp/Views/Courses.js` + - `Platform` used but not imported in `/Users/adolforeyna/Projects/EMI/expoApp/Views/ProfileSettings.js` + - `Platform` used but not imported in `/Users/adolforeyna/Projects/EMI/expoApp/components/NewPost.js` +- A few typos are intentionally present in route/prop names and work only because both sides share the typo: + - `intialContent`, `ceatedAt`, `skiptOnPress`, `skiptVideo` +- `Store.js` appears unused and stale (`easy-peasy` store not wired into app). +- `Views/NewGroup.js` is mostly scaffolded (UI shell, no creation flow yet). + +## Debugging Checklist (Most Common Production Issues) + +1. Confirm API base URL in `/Users/adolforeyna/Projects/EMI/expoApp/API.js` (or `global.baseUrl`) targets intended backend. +2. Validate backend session cookie/CORS behavior for current environment. +3. If login/feed fails, verify `API.isLoggedIn()` response shape and cookie persistence. +4. For phone testing against local backend, never use `localhost`; use machine LAN IP. +5. If feed/profile seems stale, clear AsyncStorage caches and re-open app. + +## Recommended Working Style For This Repo + +- Prefer small defensive changes over broad refactors. +- Preserve API response guards and null-safe rendering patterns. +- When touching profile/post rendering, test both cached and fresh-network paths. +- Keep changes isolated by feature screen/component; this code relies on many implicit shared assumptions. + diff --git a/Views/Feed.js b/Views/Feed.js index 8ceb543..3319cc7 100644 --- a/Views/Feed.js +++ b/Views/Feed.js @@ -10,6 +10,11 @@ import * as Updates from 'expo-updates'; import AsyncStorage from '@react-native-async-storage/async-storage'; +const FEED_LOG_PREFIX = '[Feed]'; +const logFeed = (...args) => { + if (__DEV__) console.log(FEED_LOG_PREFIX, ...args); +}; + const storeFeed = async (value) => { try { const jsonValue = JSON.stringify(value) @@ -66,26 +71,38 @@ async function onFetchUpdateAsync() { let Feed = ({ navigation, route }) => { let [Posts, setPosts] = useState([]); const flatListRef = React.useRef() - console.log("Render Feed"); + logFeed('render', { + posts: Posts.length, + hasRouteParams: !!route?.params, + reRender: !!route?.params?.reRender, + targetProfile: route?.params?.profileid || null, + }); const url = Linking.useURL(); useEffect(() => { if (prevLink === url || !url) return; prevLink = url; + logFeed('deep-link', url); handleURL(url, navigation); }, [url]); useEffect(() => { let subscribed = true; const getData = async () => { + logFeed('load:start', { + reRender: !!route?.params?.reRender, + targetProfile: route?.params?.profileid || null, + }); // TODO: Check for internet connection const internet = true; if (internet) { //byPass and load let loggedIn = await API.isLoggedIn(); + logFeed('session:checked', { loggedIn }); if (!loggedIn) return navigation.reset({ index: 0, routes: [{ name: 'Login' }], }); if (route.params && route.params.profileid) { + logFeed('redirect:profile', { profileid: route.params.profileid }); return navigation.navigate('Profile', { profileid: route.params.profileid }); } } @@ -103,10 +120,9 @@ let Feed = ({ navigation, route }) => { posthog.capture('login'); } }); - console.log("Feed from cache") let cacheFeed = await getFeed() || []; + logFeed('cache:read', { count: cacheFeed.length }); if (cacheFeed.length && subscribed) setPosts(cacheFeed); - console.log("Feed from server") } await onFetchUpdateAsync(); flatListRef.current?.scrollToOffset({ animated: true, offset: 0 }) @@ -115,12 +131,14 @@ let Feed = ({ navigation, route }) => { const safePosts = Array.isArray(posts) ? posts : []; setPosts(safePosts); storeFeed(safePosts); + logFeed('network:loaded', { count: safePosts.length, cached: true }); } - console.log("Feed, end useEffect") + logFeed('load:end'); } getData() return () => { subscribed = false; + logFeed('load:cleanup'); } }, [route.params]); const renderPost = (({ item }) => { @@ -143,7 +161,13 @@ let Feed = ({ navigation, route }) => { //ListHeaderComponent={ setPosts([newPost, ...Posts])} />} refreshing={Posts.length === 0} onRefresh={() => { - API.getPosts().then((data) => setPosts(Array.isArray(data) ? data : [])); + logFeed('refresh:start'); + API.getPosts().then((data) => { + const safePosts = Array.isArray(data) ? data : []; + setPosts(safePosts); + storeFeed(safePosts); + logFeed('refresh:end', { count: safePosts.length, cached: true }); + }); }} initialNumToRender={3} maxToRenderPerBatch={3} diff --git a/Views/Profile.js b/Views/Profile.js index b142ced..df329af 100644 --- a/Views/Profile.js +++ b/Views/Profile.js @@ -8,6 +8,11 @@ import NewPost from "./../components/NewPost.js"; import ProfileHeader from '../components/ProfileHeader.js'; import AsyncStorage from '@react-native-async-storage/async-storage'; +const PROFILE_LOG_PREFIX = '[Profile]'; +const logProfile = (...args) => { + if (__DEV__) console.log(PROFILE_LOG_PREFIX, ...args); +}; + const storeProfilePosts = async (profileid, value) => { try { const jsonValue = JSON.stringify(value) @@ -39,41 +44,74 @@ let Profile = ({ navigation, route }) => { useEffect(() => { let subscribed = true; const getData = async () => { + logProfile('load:start', { + profileid: route?.params?.profileid || null, + tag, + }); + setLoading(true); setPosts([]); if (route.params && route.params.profileid && tag == '') { - console.log('Loading Cache Profile:' + route.params.profileid); + logProfile('cache:read:start', { profileid: route.params.profileid }); await API.getUserProfile(route.params.profileid).then((profileObj) => { if(!subscribed) return 0; const nextProfile = profileObj && profileObj._id ? profileObj : {}; const profileData = nextProfile.profile || {}; setProfile(nextProfile); navigation.setOptions({ title: (profileData.firstName || "") + " " + (profileData.lastName || "") }); + logProfile('profile:loaded', { + profileid: nextProfile._id || route.params.profileid, + hasName: !!(profileData.firstName || profileData.lastName), + }); + }); + await getProfilePosts(route.params.profileid).then((cachedPosts) => { + const safeCachedPosts = Array.isArray(cachedPosts) ? cachedPosts : []; + setPosts(safeCachedPosts); + logProfile('cache:read:end', { + profileid: route.params.profileid, + count: safeCachedPosts.length, + }); }); - await getProfilePosts(route.params.profileid).then(setPosts); - console.log('Loaded Cache Profile:' + route.params.profileid); API.getPosts(route.params.profileid).then((data) => { - setLoading(false); if(!subscribed) return 0; - setPosts(Array.isArray(data) ? data : []); - storeProfilePosts(route.params.profileid, data); - console.log('Store Cache Profile:' + route.params.profileid); + const safePosts = Array.isArray(data) ? data : []; + setPosts(safePosts); + storeProfilePosts(route.params.profileid, safePosts); + setLoading(false); + logProfile('network:loaded', { + profileid: route.params.profileid, + count: safePosts.length, + cached: true, + }); }); } else { if(route.params && route.params.profileid){ - console.log('Getting posts with tag', tag) + logProfile('tag:load:start', { + profileid: route.params.profileid, + tag, + }); API.getPostsWithTag(route.params.profileid, tag).then((data) => { if(!subscribed) return 0; - setPosts(Array.isArray(data?.posts) ? data.posts : []); + const safeTagPosts = Array.isArray(data?.posts) ? data.posts : []; + setPosts(safeTagPosts); + setLoading(false); + logProfile('tag:load:end', { + profileid: route.params.profileid, + tag, + count: safeTagPosts.length, + }); }); } else { // if no profile information is pressent should load feed + logProfile('redirect:feed'); navigation.navigate('Feed') } } + logProfile('load:end'); } getData(); return ()=>{ subscribed = false; + logProfile('load:cleanup'); } }, [tag, route.params?.profileid]); @@ -149,7 +187,22 @@ let Profile = ({ navigation, route }) => { maxToRenderPerBatch={3} removeClippedSubviews={true} onRefresh={() => { - API.getPosts(route.params.profileid).then((data) => setPosts(Array.isArray(data) ? data : [])); + logProfile('refresh:start', { + profileid: route?.params?.profileid || null, + tag, + }); + setLoading(true); + API.getPosts(route.params.profileid).then((data) => { + const safePosts = Array.isArray(data) ? data : []; + setPosts(safePosts); + storeProfilePosts(route.params.profileid, safePosts); + setLoading(false); + logProfile('refresh:end', { + profileid: route.params.profileid, + count: safePosts.length, + cached: true, + }); + }); }} /> : <> //TODO: Add empty profile card here