diff --git a/android/gradlew b/android/gradlew old mode 100644 new mode 100755 diff --git a/examples/.postsandtotals.swp b/examples/.postsandtotals.swp new file mode 100644 index 0000000..8189585 Binary files /dev/null and b/examples/.postsandtotals.swp differ diff --git a/examples/postsandstats b/examples/postsandstats new file mode 100644 index 0000000..724933f --- /dev/null +++ b/examples/postsandstats @@ -0,0 +1,176 @@ +(NOBRIDGE) LOG === πŸ“ˆ Posts + Stats === + (NOBRIDGE) LOG { + "posts": [ + { + "id": "be15f284-235a-4ef5-916e-46eb4c9ce304", + "authored_by_user": true, + "alias": "Anonymous", + "group_id": "6ed852d5-e2f2-4f56-8ea8-24340d027cd7", + "group": { + "id": "6ed852d5-e2f2-4f56-8ea8-24340d027cd7", + "name": "UCSB", + "index_name": "ucsb", + "analytics_name": "ucsb", + "membership_type": "member", + "color": "#003660", + "should_show_leaderboard": true, + "group_join_type": "email_domain", + "group_visibility": "private", + "icon_url": "https://sidechat-icon-assets-prod.s3.amazonaws.com/ucsb2.png", + "asset_library_visibility": "show", + "roles": [], + "disable_ads": false + }, + "text": "lmfao what happened to that matchmaker that had us fill out a google form", + "created_at": "2025-06-19T20:09:46.868Z", + "vote_total": 37, + "comment_count": 0, + "vote_status": "upvote", + "assets": [], + "attachments": [], + "type": "post", + "dms_disabled": null, + "comments_disabled": null, + "tags": [], + "identity": { + "name": "Anonymous", + "posted_with_username": false + }, + "pinned": false, + "quote_post": { + "post": { + "id": "7372e5f6-0e3c-417b-a91a-de910b6025c3", + "authored_by_user": true, + "alias": "Anonymous", + "group_id": "6ed852d5-e2f2-4f56-8ea8-24340d027cd7", + "group": { + "id": "6ed852d5-e2f2-4f56-8ea8-24340d027cd7", + "name": "UCSB", + "index_name": "ucsb", + "analytics_name": "ucsb", + "membership_type": "member", + "color": "#003660", + "should_show_leaderboard": true, + "group_join_type": "email_domain", + "group_visibility": "private", + "icon_url": "https://sidechat-icon-assets-prod.s3.amazonaws.com/ucsb2.png", + "asset_library_visibility": "show", + "roles": [], + "disable_ads": false + }, + "text": "MATCHMAKER MATCHMAKER WHERE DID YOU GO", + "created_at": "2025-02-18T06:31:20.806Z", + "vote_total": 14, + "comment_count": 0, + "vote_status": "none", + "assets": [], + "attachments": [], + "type": "post", + "dms_disabled": null, + "comments_disabled": null, + "tags": [], + "identity": { + "name": "Anonymous", + "posted_with_username": false + }, + "pinned": false, + "is_saved": false, + "follow_status": "not_following", + "destination": "group", + "index_code": "5c9UyxXv", + "awards": [] + } + }, + "is_saved": false, + "follow_status": "following", + "destination": "group", + "index_code": "apXUHiEE", + "awards": [] + }, + { + "id": "7c138b09-4896-4453-85cc-13c452bb5123", + "authored_by_user": true, + "alias": "Anonymous", + "group_id": "6ed852d5-e2f2-4f56-8ea8-24340d027cd7", + "group": { + "id": "6ed852d5-e2f2-4f56-8ea8-24340d027cd7", + "name": "UCSB", + "index_name": "ucsb", + "analytics_name": "ucsb", + "membership_type": "member", + "color": "#003660", + "should_show_leaderboard": true, + "group_join_type": "email_domain", + "group_visibility": "private", + "icon_url": "https://sidechat-icon-assets-prod.s3.amazonaws.com/ucsb2.png", + "asset_library_visibility": "show", + "roles": [], + "disable_ads": false + }, + "text": "unless it's an A, probably better you don't tell me bc it feels like that shouldn't have been possible", + "created_at": "2025-06-17T00:41:39.645Z", + "vote_total": 15, + "comment_count": 0, + "vote_status": "upvote", + "assets": [], + "attachments": [], + "type": "post", + "dms_disabled": null, + "comments_disabled": null, + "tags": [], + "identity": { + "name": "Anonymous", + "posted_with_username": false + }, + "pinned": false, + "quote_post": { + "post": { + "id": "ed84b106-def7-42ee-9580-a599840d563c", + "authored_by_user": false, + "alias": "Anonymous", + "group_id": "6ed852d5-e2f2-4f56-8ea8-24340d027cd7", + "group": { + "id": "6ed852d5-e2f2-4f56-8ea8-24340d027cd7", + "name": "UCSB", + "index_name": "ucsb", + "analytics_name": "ucsb", + "membership_type": "member", + "color": "#003660", + "should_show_leaderboard": true, + "group_join_type": "email_domain", + "group_visibility": "private", + "icon_url": "https://sidechat-icon-assets-prod.s3.amazonaws.com/ucsb2.png", + "asset_library_visibility": "show", + "roles": [], + "disable_ads": false + }, + "text": "I hate it when professors enter your final course grade but don’t tell you what you got on the final", + "created_at": "2025-06-16T23:42:02.239Z", + "vote_total": 327, + "comment_count": 1, + "vote_status": "none", + "assets": [], + "attachments": [], + "type": "post", + "dms_disabled": false, + "comments_disabled": false, + "tags": [], + "identity": { + "name": "Anonymous", + "posted_with_username": false + }, + "pinned": false, + "is_saved": false, + "follow_status": "not_following", + "destination": "group", + "index_code": "Kp0TABhc", + "awards": [] + } + }, + "is_saved": false, + "follow_status": "following", + "destination": "group", + "index_code": "UcWa9sOw", + "awards": [] + }, + diff --git a/src/App.jsx b/src/App.jsx index 9da5d20..d707b96 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -16,6 +16,9 @@ import EditProfileScreen from './screens/EditProfileScreen'; import WriterScreen from './screens/WriterScreen'; import MessageScreen from './screens/MessagesScreen'; import ThreadScreen from './screens/ThreadScreen'; +import PaginationTestScreen from './screens/PaginationTestScreen'; +import EnhancedAPITestScreen from './screens/EnhancedAPITestScreen'; +import UserStatsScreen from './screens/UserStatsScreen'; import { storage, hasMigratedFromAsyncStorage, migrateFromAsyncStorage } from './utils/mmkv'; import { useMMKVBoolean, useMMKVString } from 'react-native-mmkv'; @@ -124,6 +127,9 @@ export default function App() { animation: 'fade_from_bottom', }} /> + + + )} diff --git a/src/components/AutoVideo.jsx b/src/components/AutoVideo.jsx index 780f75d..6b70fcc 100644 --- a/src/components/AutoVideo.jsx +++ b/src/components/AutoVideo.jsx @@ -28,9 +28,6 @@ const AutoVideo = ({ paused={true} source={{ uri: src, - headers: { - Authorization: `Bearer ${token}`, - }, type: 'm3u8', }} style={{ width: '100%', height: '100%' }} diff --git a/src/components/EnhancedUserContent.jsx b/src/components/EnhancedUserContent.jsx new file mode 100644 index 0000000..adceac7 --- /dev/null +++ b/src/components/EnhancedUserContent.jsx @@ -0,0 +1,348 @@ +import React, { useEffect, useState } from 'react'; +import { FlatList, View } from 'react-native'; +import { + Card, + Text, + IconButton, + useTheme, + SegmentedButtons, + Button, + ActivityIndicator, +} from 'react-native-paper'; +import { useNavigation } from '@react-navigation/native'; +import crashlytics from '@react-native-firebase/crashlytics'; +import ActivityItem from '../components/ActivityItem'; +import Post from './Post'; +import Comment from './Comment'; +import { AppContext } from '../App'; +import { getUserContentPaginated } from '../utils/enhancedAPI'; + +function EnhancedUserContent({ updates }) { + useEffect(() => { + crashlytics().log('Loading EnhancedUserContent'); + }, []); + + const [view, setView] = React.useState('activity'); + const [postsData, setPostsData] = useState({ posts: [], cursor: null, hasMore: false, loading: false }); + const [commentsData, setCommentsData] = useState({ posts: [], cursor: null, hasMore: false, loading: false }); + const API = React.useContext(AppContext).appState.API; + + // Load initial data when view changes + useEffect(() => { + if (view === 'posts' && postsData.posts.length === 0) { + loadInitialPosts(); + } else if (view === 'comments' && commentsData.posts.length === 0) { + loadInitialComments(); + } + }, [view]); + + const loadInitialPosts = async () => { + setPostsData(prev => ({ ...prev, loading: true })); + try { + const data = await getUserContentPaginated(API, 'posts'); + setPostsData({ + posts: data.posts, + cursor: data.cursor, + hasMore: data.hasMore, + loading: false, + }); + } catch (error) { + console.error('Error loading posts:', error); + setPostsData(prev => ({ ...prev, loading: false })); + } + }; + + const loadInitialComments = async () => { + setCommentsData(prev => ({ ...prev, loading: true })); + try { + const data = await getUserContentPaginated(API, 'comments'); + setCommentsData({ + posts: data.posts, + cursor: data.cursor, + hasMore: data.hasMore, + loading: false, + }); + } catch (error) { + console.error('Error loading comments:', error); + setCommentsData(prev => ({ ...prev, loading: false })); + } + }; + + const loadMorePosts = async () => { + if (!postsData.cursor || postsData.loading) return; + + setPostsData(prev => ({ ...prev, loading: true })); + try { + const data = await getUserContentPaginated(API, 'posts', postsData.cursor); + setPostsData(prev => ({ + posts: [...prev.posts, ...data.posts], + cursor: data.cursor, + hasMore: data.hasMore, + loading: false, + })); + } catch (error) { + console.error('Error loading more posts:', error); + setPostsData(prev => ({ ...prev, loading: false })); + } + }; + + const loadMoreComments = async () => { + if (!commentsData.cursor || commentsData.loading) return; + + setCommentsData(prev => ({ ...prev, loading: true })); + try { + const data = await getUserContentPaginated(API, 'comments', commentsData.cursor); + setCommentsData(prev => ({ + posts: [...prev.posts, ...data.posts], + cursor: data.cursor, + hasMore: data.hasMore, + loading: false, + })); + } catch (error) { + console.error('Error loading more comments:', error); + setCommentsData(prev => ({ ...prev, loading: false })); + } + }; + + return ( + + + + + ); +} + +const ItemList = ({ + updates, + view, + style, + postsData, + commentsData, + loadMorePosts, + loadMoreComments, + API +}) => { + const nav = useNavigation(); + const { colors } = useTheme(); + + switch (view) { + case 'activity': { + return ( + + + !i.is_seen)} + contentContainerStyle={{ + gap: 10, + paddingTop: 15, + paddingBottom: 15, + }} + keyExtractor={item => item.id} + renderItem={a => ( + + )} + ListEmptyComponent={ + + + + No recent activity + + + } + /> + + + ); + } + case 'posts': { + return ( + + {postsData.posts.length > 0 ? ( + + ( + + )} + windowSize={10} + ListFooterComponent={() => ( + + {postsData.loading && ( + + )} + {postsData.hasMore && !postsData.loading && ( + + )} + {!postsData.hasMore && postsData.posts.length > 0 && ( + + All posts loaded ({postsData.posts.length} total) + + )} + + )} + /> + + ) : postsData.loading ? ( + + + + Loading posts... + + + ) : ( + + + + No posts yet + + + )} + + ); + } + case 'comments': { + return ( + + {commentsData.posts.length > 0 ? ( + + p.authored_by_user)} + contentContainerStyle={{ + gap: 10, + paddingTop: 15, + paddingBottom: 15, + }} + renderItem={p => ( + + )} + windowSize={10} + ListFooterComponent={() => ( + + {commentsData.loading && ( + + )} + {commentsData.hasMore && !commentsData.loading && ( + + )} + {!commentsData.hasMore && commentsData.posts.length > 0 && ( + + All comments loaded ({commentsData.posts.length} total) + + )} + + )} + /> + + ) : commentsData.loading ? ( + + + + Loading comments... + + + ) : ( + + + + No comments yet + + + )} + + ); + } + } +}; + +export default EnhancedUserContent; \ No newline at end of file diff --git a/src/screens/EnhancedAPITestScreen.jsx b/src/screens/EnhancedAPITestScreen.jsx new file mode 100644 index 0000000..4de011f --- /dev/null +++ b/src/screens/EnhancedAPITestScreen.jsx @@ -0,0 +1,311 @@ +import React, { useState } from 'react'; +import { View, ScrollView } from 'react-native'; +import { + Appbar, + Button, + Text, + Card, + useTheme, + ActivityIndicator, + Chip, + Divider, +} from 'react-native-paper'; +import { AppContext } from '../App'; +import { getUserContentPaginated, getEnhancedUpdates } from '../utils/enhancedAPI'; +import { useMMKVObject } from 'react-native-mmkv'; + +function EnhancedAPITestScreen({ navigation }) { + const { appState } = React.useContext(AppContext); + const API = appState.API; + const { colors } = useTheme(); + const [testResults, setTestResults] = useState([]); + const [testing, setTesting] = useState(false); + const [postsData, setPostsData] = useState({ posts: [], cursor: null, hasMore: false }); + const [commentsData, setCommentsData] = useState({ posts: [], cursor: null, hasMore: false }); + const [rawResponses, setRawResponses] = useState([]); + const [currentGroup] = useMMKVObject('currentGroup'); + + const addResult = (test, status, data, rawData = null) => { + setTestResults(prev => [...prev, { test, status, data, timestamp: Date.now() }]); + if (rawData) { + setRawResponses(prev => [...prev, { test, rawData, timestamp: Date.now() }]); + } + }; + + const inspectObjectForCounts = (obj, path = '') => { + const findings = []; + + const searchKeys = [ + 'total_count', 'totalCount', 'total_posts', 'total_comments', + 'post_count', 'comment_count', 'user_stats', 'stats', + 'count', 'total', 'length', 'size', 'num_posts', 'num_comments' + ]; + + const traverse = (current, currentPath) => { + if (current && typeof current === 'object' && !Array.isArray(current)) { + Object.keys(current).forEach(key => { + const fullPath = currentPath ? `${currentPath}.${key}` : key; + + if (searchKeys.some(searchKey => key.toLowerCase().includes(searchKey.toLowerCase()))) { + findings.push(`${fullPath}: ${JSON.stringify(current[key])}`); + } + + if (typeof current[key] === 'object') { + traverse(current[key], fullPath); + } + }); + } + }; + + traverse(obj, path); + return findings; + }; + + const testRawEndpoint = async (endpoint, params = {}, description) => { + try { + const queryString = new URLSearchParams(params).toString(); + const url = `${endpoint}${queryString ? '?' + queryString : ''}`; + + const response = await fetch(`${API.apiRoot}${url}`, { + method: 'GET', + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "App-Version": "6.0.0", + Dnt: 1, + Authorization: `Bearer ${API.userToken}`, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + addResult(description, 'error', `${response.status}: ${errorText}`); + return null; + } + + const data = await response.json(); + + // Look for count-related fields + const countFindings = inspectObjectForCounts(data, description); + const topLevelKeys = Object.keys(data); + + let summary = `Keys: [${topLevelKeys.join(', ')}]`; + if (data.posts) summary += ` | Posts: ${data.posts.length}`; + if (data.user_posts?.posts) summary += ` | User Posts: ${data.user_posts.posts.length}`; + if (data.user_comments?.posts) summary += ` | User Comments: ${data.user_comments.posts.length}`; + if (data.cursor) summary += ` | Cursor: YES`; + if (countFindings.length > 0) summary += ` | Count Fields: ${countFindings.join(', ')}`; + + addResult(description, 'success', summary, data); + return data; + } catch (error) { + addResult(description, 'error', error.message); + return null; + } + }; + + const testTotalCountsInvestigation = async () => { + setTesting(true); + setTestResults([]); + setRawResponses([]); + + const groupId = currentGroup?.id || ''; + + try { + // Test baseline endpoints looking for total counts + addResult('πŸ” INVESTIGATION: Looking for total counts in API responses', 'info', 'Starting deep inspection...'); + + // Test 1: Base getUpdates call + await testRawEndpoint('/v1/updates', { group_id: groupId }, 'πŸ“Š Base getUpdates'); + + // Test 2: User posts endpoint + await testRawEndpoint('/v1/posts', { type: 'my_posts' }, 'πŸ“ My Posts Endpoint'); + + // Test 3: User comments endpoint + await testRawEndpoint('/v1/posts', { type: 'my_comments' }, 'πŸ’¬ My Comments Endpoint'); + + // Test 4: Try various parameters that might return totals + await testRawEndpoint('/v1/posts', { type: 'my_posts', include_stats: 'true' }, 'πŸ“ˆ Posts + Stats'); + await testRawEndpoint('/v1/posts', { type: 'my_posts', include_totals: 'true' }, 'πŸ”’ Posts + Totals'); + await testRawEndpoint('/v1/posts', { type: 'my_posts', summary: 'true' }, 'πŸ“‹ Posts + Summary'); + + // Test 5: Try user profile/stats endpoints + await testRawEndpoint('/v1/user/stats', {}, 'πŸ‘€ User Stats'); + await testRawEndpoint('/v1/user/profile', {}, 'πŸ‘€ User Profile'); + await testRawEndpoint('/v1/user/summary', {}, 'πŸ‘€ User Summary'); + + // Test 6: Check if different limits reveal metadata + await testRawEndpoint('/v1/posts', { type: 'my_posts', limit: 1 }, 'πŸ“ Posts (limit=1)'); + await testRawEndpoint('/v1/posts', { type: 'my_posts', limit: 100 }, 'πŸ“ Posts (limit=100)'); + + // Test 7: Original enhanced API for comparison + addResult('πŸ”§ Enhanced API Methods', 'running', 'Testing our wrapper methods...'); + const initialPosts = await getUserContentPaginated(API, 'posts'); + setPostsData(initialPosts); + addResult('πŸ“ Enhanced Posts', 'success', `Found ${initialPosts.posts.length} posts, cursor: ${initialPosts.cursor ? 'YES' : 'NO'}, hasMore: ${initialPosts.hasMore}`); + + const initialComments = await getUserContentPaginated(API, 'comments'); + setCommentsData(initialComments); + addResult('πŸ’¬ Enhanced Comments', 'success', `Found ${initialComments.posts.length} comments, cursor: ${initialComments.cursor ? 'YES' : 'NO'}, hasMore: ${initialComments.hasMore}`); + + } catch (error) { + addResult('❌ Error', 'error', error.message); + } + + setTesting(false); + }; + + const testEnhancedAPI = testTotalCountsInvestigation; + + const loadMorePosts = async () => { + if (!postsData.cursor) return; + + try { + const nextPage = await getUserContentPaginated(API, 'posts', postsData.cursor); + setPostsData(prev => ({ + posts: [...prev.posts, ...nextPage.posts], + cursor: nextPage.cursor, + hasMore: nextPage.hasMore, + })); + addResult('Load More Posts', 'success', `Loaded ${nextPage.posts.length} more posts`); + } catch (error) { + addResult('Load More Posts', 'error', error.message); + } + }; + + const loadMoreComments = async () => { + if (!commentsData.cursor) return; + + try { + const nextPage = await getUserContentPaginated(API, 'comments', commentsData.cursor); + setCommentsData(prev => ({ + posts: [...prev.posts, ...nextPage.posts], + cursor: nextPage.cursor, + hasMore: nextPage.hasMore, + })); + addResult('Load More Comments', 'success', `Loaded ${nextPage.posts.length} more comments`); + } catch (error) { + addResult('Load More Comments', 'error', error.message); + } + }; + + return ( + + + navigation.goBack()} /> + + + + + + + πŸ” Total Counts Investigation + + This will deep-inspect API responses to find total post/comment counts for tab labels. + + + Looking for: total_count, post_count, comment_count, stats fields, etc. + + + + + + + {/* Stats */} + + Posts Loaded: {postsData.posts.length} + Comments Loaded: {commentsData.posts.length} + Responses: {rawResponses.length} + + + {/* Load More Buttons */} + + + + + + {testing && } + + {/* Raw Response Inspector */} + {rawResponses.length > 0 && ( + + + πŸ”¬ Raw Response Inspector + + Tap any response to see complete JSON data + + {rawResponses.map((response, index) => ( + + ))} + + + )} + + + + {/* Test Results */} + {testResults.map((result, index) => ( + + + + + {result.status.toUpperCase()} + + + {result.test} + + + + {result.data} + + + + ))} + + + ); +} + +export default EnhancedAPITestScreen; \ No newline at end of file diff --git a/src/screens/MyProfileScreen.jsx b/src/screens/MyProfileScreen.jsx index f91f200..d40ee35 100644 --- a/src/screens/MyProfileScreen.jsx +++ b/src/screens/MyProfileScreen.jsx @@ -17,6 +17,7 @@ import timesago from 'timesago'; import { useFocusEffect } from '@react-navigation/native'; import crashlytics from '@react-native-firebase/crashlytics'; import UserContent from '../components/UserContent'; +import EnhancedUserContent from '../components/EnhancedUserContent'; import { useMMKVObject } from 'react-native-mmkv'; const BORDER_RADIUS = 15; @@ -130,59 +131,35 @@ function MyProfileScreen({ navigation }) { - - - - - {updates?.user?.follower_count || '--'} + + + + Post Karma - - - - - {updates?.karma?.post || '--'} - - - - - - - {updates?.groups?.length || '--'} + + + + Comment Karma - - - - - {updates?.karma?.comment || '--'} - - + navigation.push('UserStats')}> + + + Stats Page + + + + )} diff --git a/src/screens/PaginationTestScreen.jsx b/src/screens/PaginationTestScreen.jsx new file mode 100644 index 0000000..5f98da4 --- /dev/null +++ b/src/screens/PaginationTestScreen.jsx @@ -0,0 +1,158 @@ +import React, { useState } from 'react'; +import { View, ScrollView, StyleSheet } from 'react-native'; +import { + Appbar, + Button, + Text, + Card, + useTheme, + ActivityIndicator, +} from 'react-native-paper'; +import { AppContext } from '../App'; +import { useMMKVString } from 'react-native-mmkv'; + +function PaginationTestScreen({ navigation }) { + const { appState } = React.useContext(AppContext); + const API = appState.API; + const { colors } = useTheme(); + const [currentGroup] = useMMKVString('currentGroup'); + const [testResults, setTestResults] = useState([]); + const [testing, setTesting] = useState(false); + + const addResult = (test, status, data) => { + setTestResults(prev => [...prev, { test, status, data, timestamp: Date.now() }]); + }; + + const testEndpoint = async (endpoint, params = {}, description) => { + try { + const queryString = new URLSearchParams(params).toString(); + const url = `${endpoint}${queryString ? '?' + queryString : ''}`; + + console.log(`Testing: ${url}`); + + const response = await fetch(`https://api.sidechat.lol${url}`, { + method: 'GET', + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "App-Version": "6.0.0", + Dnt: 1, + Authorization: `Bearer ${API.userToken}`, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + addResult(description, 'error', `${response.status}: ${errorText}`); + return null; + } + + const data = await response.json(); + + // Extract useful info + let info = []; + if (data.user_posts?.posts) info.push(`Posts: ${data.user_posts.posts.length}`); + if (data.user_comments?.posts) info.push(`Comments: ${data.user_comments.posts.length}`); + if (data.posts) info.push(`Posts: ${data.posts.length}`); + if (data.cursor) info.push(`Cursor: ${data.cursor}`); + if (data.has_more !== undefined) info.push(`Has more: ${data.has_more}`); + + addResult(description, 'success', info.join(', ') || 'Success'); + return data; + } catch (error) { + addResult(description, 'error', error.message); + return null; + } + }; + + const runPaginationTests = async () => { + setTesting(true); + setTestResults([]); + + const groupId = JSON.parse(currentGroup || '{}')?.id || ''; + + // Test 1: Baseline calls + await testEndpoint('/v1/updates', { group_id: groupId }, 'Baseline getUpdates'); + await testEndpoint('/v1/posts', { type: 'my_posts' }, 'Baseline getUserContent (posts)'); + await testEndpoint('/v1/posts', { type: 'my_comments' }, 'Baseline getUserContent (comments)'); + + // Test 2: Pagination parameters on getUpdates + await testEndpoint('/v1/updates', { group_id: groupId, cursor: 'test' }, 'getUpdates + cursor'); + await testEndpoint('/v1/updates', { group_id: groupId, offset: 50 }, 'getUpdates + offset'); + await testEndpoint('/v1/updates', { group_id: groupId, limit: 100 }, 'getUpdates + limit'); + await testEndpoint('/v1/updates', { group_id: groupId, per_page: 100 }, 'getUpdates + per_page'); + await testEndpoint('/v1/updates', { group_id: groupId, page: 2 }, 'getUpdates + page'); + + // Test 3: Pagination parameters on posts endpoint + await testEndpoint('/v1/posts', { type: 'my_posts', cursor: 'test' }, 'my_posts + cursor'); + await testEndpoint('/v1/posts', { type: 'my_posts', offset: 50 }, 'my_posts + offset'); + await testEndpoint('/v1/posts', { type: 'my_posts', limit: 100 }, 'my_posts + limit'); + await testEndpoint('/v1/posts', { type: 'my_posts', per_page: 100 }, 'my_posts + per_page'); + + // Test 4: Try the pattern from getGroupPosts (which works) + await testEndpoint('/v1/posts', { group_id: groupId, type: 'hot' }, 'Group posts (baseline)'); + await testEndpoint('/v1/posts', { group_id: groupId, type: 'hot', cursor: 'test' }, 'Group posts + cursor'); + + setTesting(false); + }; + + return ( + + + navigation.goBack()} /> + + + + + + + API Pagination Investigation + + This will test various pagination parameters on Sidechat API endpoints + to see if we can access more than ~50 posts/comments. + + + + + + + {testing && } + + + {testResults.map((result, index) => ( + + + + + {result.status.toUpperCase()} + + + {result.test} + + + + {result.data} + + + + ))} + + + + ); +} + +export default PaginationTestScreen; \ No newline at end of file diff --git a/src/screens/SettingsScreen.jsx b/src/screens/SettingsScreen.jsx index dcb1e1f..ad3f12a 100644 --- a/src/screens/SettingsScreen.jsx +++ b/src/screens/SettingsScreen.jsx @@ -85,6 +85,20 @@ function SettingsScreen({ navigation }) { Website + + + + {updateAvailable && ( { + try { + // Check for corrupted state that could cause crashes + if (isBackgroundFetching && !backgroundProgress) { + console.warn('Detected corrupted background state, clearing...'); + setIsBackgroundFetching(false); + } + + if (stats && typeof stats !== 'object') { + console.warn('Detected corrupted stats, clearing...'); + setStats(null); + } + + if (cacheTimestamp && (typeof cacheTimestamp !== 'number' || cacheTimestamp > Date.now())) { + console.warn('Detected corrupted timestamp, clearing...'); + setCacheTimestamp(null); + } + } catch (error) { + console.error('State validation failed, clearing all cache:', error); + crashlytics().recordError(error); + // Emergency state clear + setStats(null); + setAllPosts([]); + setAllComments([]); + setCacheTimestamp(null); + setIsBackgroundFetching(false); + setBackgroundProgress(null); + } + }; + + // Time range filtering - simple presets + const [timeFilter, setTimeFilter] = useState('all'); // 'all', 'month', 'week' + + // Calculate user's timeline information + const getTimelineInfo = () => { + if (!safeAllPosts.length && !safeAllComments.length) { + return { startDate: null, endDate: null, timelineMonths: [] }; + } + + const allContent = [...safeAllPosts, ...safeAllComments].filter(item => item.created_at); + if (allContent.length === 0) { + return { startDate: null, endDate: null, timelineMonths: [] }; + } + + // Find the actual start and end dates of user activity + const dates = allContent.map(item => new Date(item.created_at)); + const startDate = new Date(Math.min(...dates)); + const endDate = new Date(); // Always end at current time + + // Generate month markers for the timeline + const timelineMonths = []; + const current = new Date(startDate.getFullYear(), startDate.getMonth(), 1); + const end = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0); + + while (current <= end) { + timelineMonths.push({ + date: new Date(current), + label: current.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }), + shortLabel: current.toLocaleDateString('en-US', { month: 'short' }), + isYearStart: current.getMonth() === 0 + }); + current.setMonth(current.getMonth() + 1); + } + + return { startDate, endDate, timelineMonths }; + }; + + const timelineInfo = getTimelineInfo(); + + const getFilteredContent = () => { + const filterByTimeframe = (items) => { + if (timeFilter === 'all') return items; + + const now = new Date(); + let cutoffDate; + + if (timeFilter === 'week') { + cutoffDate = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000)); + } else if (timeFilter === 'month') { + cutoffDate = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000)); + } + + return items.filter(item => { + const itemDate = new Date(item.created_at); + return itemDate >= cutoffDate; + }); + }; + + return { + posts: filterByTimeframe(safeAllPosts), + comments: filterByTimeframe(safeAllComments) + }; + }; + + // Cache helper functions - cache persists until manually refreshed + const isCacheValid = () => { + return !!(cacheTimestamp && stats); + }; + + const getCacheAge = () => { + if (!cacheTimestamp) return ''; + const ageMinutes = Math.floor((Date.now() - cacheTimestamp) / (1000 * 60)); + if (ageMinutes < 1) return 'just now'; + if (ageMinutes < 60) return `${ageMinutes} minute${ageMinutes === 1 ? '' : 's'} ago`; + const ageHours = Math.floor(ageMinutes / 60); + if (ageHours < 24) return `${ageHours} hour${ageHours === 1 ? '' : 's'} ago`; + const ageDays = Math.floor(ageHours / 24); + if (ageDays < 7) return `${ageDays} day${ageDays === 1 ? '' : 's'} ago`; + const ageWeeks = Math.floor(ageDays / 7); + return `${ageWeeks} week${ageWeeks === 1 ? '' : 's'} ago`; + }; + + const clearCache = () => { + try { + setStats(null); + setAllPosts([]); + setAllComments([]); + setCacheTimestamp(null); + setIsBackgroundFetching(false); + setBackgroundProgress(null); + setStatus('Cache cleared'); + } catch (error) { + console.error('Cache clear failed:', error); + setStatus('Emergency cache clear - please restart app'); + } + }; + + + // Memory monitoring helpers + const checkMemoryPressure = async (dataSize) => { + try { + const totalMemory = await DeviceInfo.getTotalMemory(); + const usedMemory = await DeviceInfo.getUsedMemory(); + const freeMemory = totalMemory - usedMemory; + const estimatedDataMemory = dataSize * 1000; // Rough estimate + + // If we're using more than 80% of available memory, we're in trouble + const memoryPressure = (usedMemory + estimatedDataMemory) / totalMemory > 0.8; + + if (memoryPressure) { + crashlytics().log(`Memory pressure detected: ${Math.round(usedMemory/1024/1024)}MB used, ${Math.round(freeMemory/1024/1024)}MB free, ${Math.round(totalMemory/1024/1024)}MB total`); + } + + return { memoryPressure, freeMemory, totalMemory, usedMemory }; + } catch (error) { + console.warn('Could not check memory:', error); + return { memoryPressure: false }; + } + }; + + // AppState listener for background processing + useEffect(() => { + const subscription = AppState.addEventListener('change', nextAppState => { + appStateRef.current = nextAppState; + + if (nextAppState === 'active' && isBackgroundFetching) { + // User returned while background fetch is running + if (backgroundProgress) { + setLoading(true); + setProgress(backgroundProgress.progress || 0); + setStatus(backgroundProgress.status || 'Processing in background...'); + } + } + }); + + return () => subscription?.remove(); + }, [isBackgroundFetching, backgroundProgress]); + + // Check cache and background state on component mount + useEffect(() => { + try { + // Validate state first to prevent crashes + validateAndRecoverState(); + + if (stats && isCacheValid()) { + setStatus(`Loaded cached data from ${getCacheAge()}`); + } else if (isBackgroundFetching) { + setLoading(true); + setStatus('Background processing in progress...'); + if (backgroundProgress) { + setProgress(backgroundProgress.progress || 0); + } + } + } catch (error) { + console.error('Component mount failed, entering safe mode:', error); + crashlytics().recordError(error); + setStatus('Error loading - cache cleared. Please try fetching again.'); + } + }, []); + + const fetchAllContent = async (forceRefresh = false) => { + // Check cache first unless forcing refresh + if (!forceRefresh && isCacheValid()) { + setStatus(`Using cached data from ${getCacheAge()}`); + return; + } + + try { + // Validate state before starting + validateAndRecoverState(); + setLoading(true); + setProgress(0); + setIsBackgroundFetching(true); + setStatus('Starting data collection...'); + setBackgroundProgress({ progress: 0, status: 'Starting data collection...', phase: 'init' }); + crashlytics().log('Starting user stats collection'); + + // Fetch all posts + setStatus('Fetching posts...'); + setBackgroundProgress({ progress: 0.1, status: 'Fetching posts...', phase: 'posts' }); + let allUserPosts = []; + let cursor = null; + let pageCount = 0; + + do { + const response = await getUserContentPaginated(API, 'posts', cursor); + // Use concat instead of spread to avoid memory issues with large arrays + allUserPosts = allUserPosts.concat(response.posts); + cursor = response.cursor; + pageCount++; + + const currentProgress = Math.min(0.4, pageCount * 0.02); + const currentStatus = `Fetched ${allUserPosts.length} posts (page ${pageCount})`; + + // Update UI much less frequently to prevent thread blocking + if (pageCount % 25 === 0 || !cursor) { + // Batch state updates to minimize renders + try { + setProgress(currentProgress); + setStatus(currentStatus); + + // Only update background progress when absolutely necessary + if (pageCount % 50 === 0 || !cursor) { + setBackgroundProgress({ + progress: currentProgress, + status: currentStatus, + phase: 'posts', + postsCount: allUserPosts.length, + pageCount + }); + } + } catch (error) { + console.error('State update failed:', error); + // Continue without crashing + } + + // Check memory pressure less frequently + if (pageCount % 50 === 0) { + const memoryStatus = await checkMemoryPressure(allUserPosts.length); + if (memoryStatus.memoryPressure) { + setStatus(`Memory pressure detected - stopping at ${allUserPosts.length} posts`); + break; + } + } + } + + // Safety valve + if (pageCount > 200) { + setStatus('Stopping at 200 pages - you have a lot of posts!'); + break; + } + + // Consistent delay to prevent memory pressure on all devices + await new Promise(resolve => setTimeout(resolve, 500)); + + // Frequent yielding to UI thread to prevent blocking + if (pageCount % 3 === 0) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + + // Longer yield every 10 pages for garbage collection + if (pageCount % 10 === 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } while (cursor); + + setAllPosts(allUserPosts); + + // Fetch all comments + setStatus('Fetching comments...'); + setBackgroundProgress({ + progress: 0.4, + status: 'Fetching comments...', + phase: 'comments', + postsCount: allUserPosts.length + }); + let allUserComments = []; + cursor = null; + pageCount = 0; + + do { + const response = await getUserContentPaginated(API, 'comments', cursor); + const userComments = response.posts.filter(p => p.authored_by_user); + // Use concat instead of spread to avoid memory issues with large arrays + allUserComments = allUserComments.concat(userComments); + cursor = response.cursor; + pageCount++; + + const currentProgress = Math.min(0.8, 0.4 + (pageCount * 0.02)); + const currentStatus = `Fetched ${allUserComments.length} comments (page ${pageCount})`; + + // Update UI much less frequently to prevent thread blocking + if (pageCount % 25 === 0 || !cursor) { + // Batch state updates to minimize renders + try { + setProgress(currentProgress); + setStatus(currentStatus); + + // Only update background progress when absolutely necessary + if (pageCount % 50 === 0 || !cursor) { + setBackgroundProgress({ + progress: currentProgress, + status: currentStatus, + phase: 'comments', + postsCount: allUserPosts.length, + commentsCount: allUserComments.length, + pageCount + }); + } + } catch (error) { + console.error('State update failed:', error); + // Continue without crashing + } + + // Check memory pressure less frequently + if (pageCount % 50 === 0) { + const totalData = allUserPosts.length + allUserComments.length; + const memoryStatus = await checkMemoryPressure(totalData); + if (memoryStatus.memoryPressure) { + setStatus(`Memory pressure detected - stopping at ${allUserComments.length} comments`); + break; + } + } + } + + if (pageCount > 200) { + setStatus('Stopping at 200 pages - you have a lot of comments!'); + break; + } + + // Consistent delay to prevent memory pressure on all devices + await new Promise(resolve => setTimeout(resolve, 500)); + + // Frequent yielding to UI thread to prevent blocking + if (pageCount % 3 === 0) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + + // Longer yield every 10 pages for garbage collection + if (pageCount % 10 === 0) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } while (cursor); + + setAllComments(allUserComments); + + // Process analytics + setStatus('Processing analytics...'); + setProgress(0.9); + setBackgroundProgress({ + progress: 0.9, + status: 'Processing analytics...', + phase: 'analytics', + postsCount: allUserPosts.length, + commentsCount: allUserComments.length + }); + + const analytics = processAnalytics(allUserPosts, allUserComments); + setStats(analytics); + + // Update cache timestamp + setCacheTimestamp(Date.now()); + + // Clear background processing state + setIsBackgroundFetching(false); + setBackgroundProgress(null); + + setProgress(1); + setStatus(`Complete! Analyzed ${allUserPosts.length} posts and ${allUserComments.length} comments`); + + crashlytics().log(`User stats completed: ${allUserPosts.length} posts, ${allUserComments.length} comments`); + + } catch (error) { + console.error('Error fetching user stats:', error); + setStatus(`Error: ${error.message}`); + crashlytics().recordError(error); + + // Clean up background state on error + setIsBackgroundFetching(false); + setBackgroundProgress(null); + } finally { + setLoading(false); + } + }; + + const processAnalytics = (posts, comments) => { + const now = new Date(); + // Don't create another massive array - iterate separately to save memory + + // Basic stats + const totalPosts = posts.length; + const totalComments = comments.length; + const totalContent = totalPosts + totalComments; + + // Vote analytics - average and median + const totalPostVotes = posts.reduce((sum, post) => sum + (post.vote_total || 0), 0); + const totalCommentVotes = comments.reduce((sum, comment) => sum + (comment.vote_total || 0), 0); + const avgPostVotes = totalPosts > 0 ? (totalPostVotes / totalPosts).toFixed(1) : 0; + const avgCommentVotes = totalComments > 0 ? (totalCommentVotes / totalComments).toFixed(1) : 0; + + // Calculate median votes + const getMedian = (values) => { + if (values.length === 0) return 0; + const sorted = values.slice().sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 + ? ((sorted[mid - 1] + sorted[mid]) / 2).toFixed(1) + : sorted[mid].toFixed(1); + }; + + const postVotes = posts.map(post => post.vote_total || 0); + const commentVotes = comments.map(comment => comment.vote_total || 0); + const medianPostVotes = getMedian(postVotes); + const medianCommentVotes = getMedian(commentVotes); + + // Vote distribution analysis + const createVoteDistribution = (votes) => { + const buckets = { + '0': 0, + '1-5': 0, + '6-10': 0, + '11-20': 0, + '21-50': 0, + '50+': 0 + }; + + votes.forEach(vote => { + if (vote === 0) buckets['0']++; + else if (vote <= 5) buckets['1-5']++; + else if (vote <= 10) buckets['6-10']++; + else if (vote <= 20) buckets['11-20']++; + else if (vote <= 50) buckets['21-50']++; + else buckets['50+']++; + }); + + return buckets; + }; + + const postVoteDistribution = createVoteDistribution(postVotes); + const commentVoteDistribution = createVoteDistribution(commentVotes); + + // Find top content + const topPost = posts.length > 0 ? posts.reduce((max, post) => + (post.vote_total || 0) > (max.vote_total || 0) ? post : max + ) : null; + + const topComment = comments.length > 0 ? comments.reduce((max, comment) => + (comment.vote_total || 0) > (max.vote_total || 0) ? comment : max + ) : null; + + // Time-based analytics + const hourCounts = Array(24).fill(0); + const monthCounts = Array(12).fill(0); + const dayOfWeekCounts = Array(7).fill(0); + + // Process posts and comments separately to avoid creating massive combined array + posts.forEach(item => { + if (item.created_at) { + const date = new Date(item.created_at); + hourCounts[date.getHours()]++; + monthCounts[date.getMonth()]++; + dayOfWeekCounts[date.getDay()]++; + } + }); + + comments.forEach(item => { + if (item.created_at) { + const date = new Date(item.created_at); + hourCounts[date.getHours()]++; + monthCounts[date.getMonth()]++; + dayOfWeekCounts[date.getDay()]++; + } + }); + + // Find peak activity + const peakHour = hourCounts.indexOf(Math.max(...hourCounts)); + const peakDay = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][ + dayOfWeekCounts.indexOf(Math.max(...dayOfWeekCounts)) + ]; + + // Account age and activity - find oldest from both arrays separately + let oldestContent = null; + + if (posts.length > 0) { + oldestContent = posts.reduce((oldest, item) => { + const itemDate = new Date(item.created_at); + const oldestDate = new Date(oldest.created_at); + return itemDate < oldestDate ? item : oldest; + }); + } + + if (comments.length > 0) { + const oldestComment = comments.reduce((oldest, item) => { + const itemDate = new Date(item.created_at); + const oldestDate = new Date(oldest.created_at); + return itemDate < oldestDate ? item : oldest; + }); + + if (!oldestContent || new Date(oldestComment.created_at) < new Date(oldestContent.created_at)) { + oldestContent = oldestComment; + } + } + + const accountAgeMonths = oldestContent ? + Math.max(1, Math.round((now - new Date(oldestContent.created_at)) / (1000 * 60 * 60 * 24 * 30))) : 0; + + const avgPostsPerMonth = accountAgeMonths > 0 ? (totalPosts / accountAgeMonths).toFixed(1) : 0; + const avgCommentsPerMonth = accountAgeMonths > 0 ? (totalComments / accountAgeMonths).toFixed(1) : 0; + + return { + totalPosts, + totalComments, + totalContent, + totalPostVotes, + totalCommentVotes, + avgPostVotes, + avgCommentVotes, + medianPostVotes, + medianCommentVotes, + postVoteDistribution, + commentVoteDistribution, + topPost, + topComment, + peakHour, + peakDay, + accountAgeMonths, + avgPostsPerMonth, + avgCommentsPerMonth, + hourCounts, + monthCounts, + dayOfWeekCounts, + }; + }; + + // Calculate analytics based on current time period + const currentAnalytics = stats ? (() => { + const filtered = getFilteredContent(); + return processAnalytics(filtered.posts, filtered.comments); + })() : null; + + const formatHour = (hour) => { + if (hour === 0) return '12 AM'; + if (hour < 12) return `${hour} AM`; + if (hour === 12) return '12 PM'; + return `${hour - 12} PM`; + }; + + // Calendar state for monthly heatmap + const [calendarDate, setCalendarDate] = useState(new Date()); + + const StatTile = ({ title, value, subtitle }) => ( + + + {title} + + + {value} + + {subtitle && ( + + {subtitle} + + )} + + ); + + const TimeFilterButtons = () => { + return ( + + + Time Period + + + setTimeFilter('all')} + mode={timeFilter === 'all' ? 'flat' : 'outlined'} + > + All Time + + setTimeFilter('month')} + mode={timeFilter === 'month' ? 'flat' : 'outlined'} + > + Past Month + + setTimeFilter('week')} + mode={timeFilter === 'week' ? 'flat' : 'outlined'} + > + Past Week + + + + ); + }; + + return ( + + + navigation.goBack()} /> + + {isBackgroundFetching && ( + + )} + + + + {/* Fetch Button */} + {!stats && !loading && ( + + + + + This will take a few minutes. Feel free to browse the app while we work in the background! + + + + )} + + {/* Time Filter */} + {stats && ( + + + + + + )} + + {loading && ( + + + + {isBackgroundFetching && appStateRef.current !== 'active' ? 'Processing in Background...' : 'Collecting Data...'} + + + + {status} + + {isBackgroundFetching && ( + + You can navigate away - we'll continue processing in the background + + )} + + + )} + + + {currentAnalytics && ( + <> + {/* Statistics Grid */} + + + + + + + + + + + + + + + + {timeFilter === 'all' && ( + + + + + )} + + {/* Top Post - Full Width */} + {currentAnalytics.topPost && ( + + + Top Post + + {currentAnalytics.topPost.vote_total} votes + + + "{currentAnalytics.topPost.text}" + + + + )} + + {/* Vote Distribution Charts */} + + + Post Vote Distribution + + + {/* Y-axis */} + + {(() => { + const maxCount = Math.max(...Object.values(currentAnalytics.postVoteDistribution)); + const steps = [maxCount, Math.floor(maxCount * 0.75), Math.floor(maxCount * 0.5), Math.floor(maxCount * 0.25), 0]; + return steps.map((value, index) => ( + + {value} + + )); + })()} + + {/* Bars */} + + {Object.entries(currentAnalytics.postVoteDistribution).map(([range, count]) => { + const maxCount = Math.max(...Object.values(currentAnalytics.postVoteDistribution)); + const height = maxCount > 0 ? (count / maxCount) * 70 : 0; + return ( + + + + ); + })} + + + + + {Object.keys(currentAnalytics.postVoteDistribution).map(range => ( + + {range} + + ))} + + + + + + + + + Comment Vote Distribution + + + {/* Y-axis */} + + {(() => { + const maxCount = Math.max(...Object.values(currentAnalytics.commentVoteDistribution)); + const steps = [maxCount, Math.floor(maxCount * 0.75), Math.floor(maxCount * 0.5), Math.floor(maxCount * 0.25), 0]; + return steps.map((value, index) => ( + + {value} + + )); + })()} + + {/* Bars */} + + {Object.entries(currentAnalytics.commentVoteDistribution).map(([range, count]) => { + const maxCount = Math.max(...Object.values(currentAnalytics.commentVoteDistribution)); + const height = maxCount > 0 ? (count / maxCount) * 70 : 0; + return ( + + + + ); + })} + + + + + {Object.keys(currentAnalytics.commentVoteDistribution).map(range => ( + + {range} + + ))} + + + + + + + {/* Hourly Activity Chart */} + + + Hourly Activity + + + {/* Y-axis */} + + {(() => { + const maxCount = Math.max(...currentAnalytics.hourCounts); + const steps = [maxCount, Math.floor(maxCount * 0.75), Math.floor(maxCount * 0.5), Math.floor(maxCount * 0.25), 0]; + return steps.map((value, index) => ( + + {value} + + )); + })()} + + {/* Bars */} + + {currentAnalytics.hourCounts.map((count, hour) => { + const maxCount = Math.max(...currentAnalytics.hourCounts); + const height = maxCount > 0 ? (count / maxCount) * 70 : 0; + return ( + + + + ); + })} + + + + + {[0, 6, 12, 18].map(hour => ( + + {formatHour(hour)} + + ))} + + + + + + + {/* Day of Week Chart */} + + + Day of Week Activity + + + {/* Y-axis */} + + {(() => { + const maxCount = Math.max(...currentAnalytics.dayOfWeekCounts); + const steps = [maxCount, Math.floor(maxCount * 0.75), Math.floor(maxCount * 0.5), Math.floor(maxCount * 0.25), 0]; + return steps.map((value, index) => ( + + {value} + + )); + })()} + + {/* Bars */} + + {currentAnalytics.dayOfWeekCounts.map((count, day) => { + const maxCount = Math.max(...currentAnalytics.dayOfWeekCounts); + const height = maxCount > 0 ? (count / maxCount) * 70 : 0; + return ( + + + + ); + })} + + + + + {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => ( + + {day} + + ))} + + + + + + + {/* Monthly Calendar Heatmap */} + + + + Monthly Activity + + + + {calendarDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })} + + + + + + {/* Simple calendar grid placeholder */} + + + Calendar heatmap coming soon + + + Will show daily post/comment activity + + + + + + {/* Data Management */} + + + Data Management + + + + + Cache: {cacheTimestamp ? getCacheAge() : 'No cached data'} + + + + + + )} + + + ); +} + +export default UserStatsScreen; \ No newline at end of file diff --git a/src/utils/enhancedAPI.js b/src/utils/enhancedAPI.js new file mode 100644 index 0000000..b054a7c --- /dev/null +++ b/src/utils/enhancedAPI.js @@ -0,0 +1,92 @@ +// Enhanced API methods with pagination support for user posts and comments +import { SidechatAPIClient } from 'sidechat.js'; + +/** + * Enhanced version of getUserContent that supports pagination + * @param {SidechatAPIClient} apiClient - The API client instance + * @param {"posts"|"comments"} contentType - type of user content to fetch + * @param {string} [cursor] - pagination cursor for fetching next page + * @returns {Promise<{posts: Array, cursor: string, hasMore: boolean}>} + */ +export async function getUserContentPaginated(apiClient, contentType, cursor = null) { + if (!apiClient.userToken) { + throw new Error("User is not authenticated."); + } + + let type = contentType === "posts" ? "my_posts" : "my_comments"; + + try { + const params = new URLSearchParams({ type }); + if (cursor) { + params.append('cursor', cursor); + } + + const response = await fetch(`${apiClient.apiRoot}/v1/posts?${params}`, { + method: "GET", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "App-Version": "6.0.0", + Dnt: 1, + Authorization: `Bearer ${apiClient.userToken}`, + }, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${await response.text()}`); + } + + const json = await response.json(); + + return { + posts: json.posts || [], + cursor: json.cursor || null, + hasMore: !!json.cursor, // If there's a cursor, there are more posts + }; + } catch (err) { + console.error('Enhanced getUserContent error:', err); + throw new Error(`Failed to get paginated content: ${err.message}`); + } +} + +/** + * Enhanced method to get user updates with better post/comment fetching + * @param {SidechatAPIClient} apiClient - The API client instance + * @param {string} [groupID] - ID of a specific group + * @param {string} [postsCursor] - cursor for posts pagination + * @param {string} [commentsCursor] - cursor for comments pagination + * @returns {Promise<{user: Object, activity_items: Object, user_posts: Object, user_comments: Object}>} + */ +export async function getEnhancedUpdates(apiClient, groupID = "", postsCursor = null, commentsCursor = null) { + if (!apiClient.userToken) { + throw new Error("User is not authenticated."); + } + + try { + // Get the baseline updates (user info, activity, limited posts/comments) + const baseUpdates = await apiClient.getUpdates(groupID); + + // Get paginated posts and comments separately + const [postsData, commentsData] = await Promise.all([ + getUserContentPaginated(apiClient, "posts", postsCursor), + getUserContentPaginated(apiClient, "comments", commentsCursor) + ]); + + return { + ...baseUpdates, + user_posts: { + posts: postsData.posts, + cursor: postsData.cursor, + hasMore: postsData.hasMore, + }, + user_comments: { + posts: commentsData.posts, + cursor: commentsData.cursor, + hasMore: commentsData.hasMore, + }, + }; + } catch (err) { + console.error('Enhanced getUpdates error:', err); + throw new Error(`Failed to get enhanced updates: ${err.message}`); + } +} \ No newline at end of file