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