From 3ef97fbec46a9fad5112d9c14b71ab965ca246eb Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sun, 8 Mar 2026 20:08:42 +0000
Subject: [PATCH] [jules] enhance: Add skeleton loading to mobile Home screen
Creates reusable Skeleton and GroupListSkeleton components for the React Native App. Replaces the default ActivityIndicator on the Home screen and refactors the Friends screen to use the generic Skeleton instead of hardcoded Views.
Co-authored-by: Devasy23 <110348311+Devasy23@users.noreply.github.com>
---
.Jules/changelog.md | 5 ++
.Jules/knowledge.md | 5 ++
.Jules/todo.md | 9 ++-
.../components/skeletons/GroupListSkeleton.js | 44 ++++++++++++++
mobile/components/ui/Skeleton.js | 51 ++++++++++++++++
mobile/screens/FriendsScreen.js | 58 ++-----------------
mobile/screens/HomeScreen.js | 6 +-
7 files changed, 120 insertions(+), 58 deletions(-)
create mode 100644 mobile/components/skeletons/GroupListSkeleton.js
create mode 100644 mobile/components/ui/Skeleton.js
diff --git a/.Jules/changelog.md b/.Jules/changelog.md
index 11fc864..0f04b65 100644
--- a/.Jules/changelog.md
+++ b/.Jules/changelog.md
@@ -123,3 +123,8 @@
- `.jules/todo.md`
- `.jules/knowledge.md`
- `.jules/changelog.md`
+
+### 2026-03-08
+- Added generic `Skeleton` component and `GroupListSkeleton` component to mobile app
+- Replaced basic ActivityIndicator loading screen with `GroupListSkeleton` on mobile `HomeScreen`
+- Refactored `FriendsScreen` inline skeleton logic to use the new generic `Skeleton` component
diff --git a/.Jules/knowledge.md b/.Jules/knowledge.md
index 43a9ab0..a5062c3 100644
--- a/.Jules/knowledge.md
+++ b/.Jules/knowledge.md
@@ -756,3 +756,8 @@ _Document errors and their solutions here as you encounter them._
- react-native-paper: UI components
- axios: API calls (via api/client.js)
- expo: Platform SDK
+
+### Development Workflows
+- The mobile project is configured with `react-native-web`. You can test it in a browser using `npx expo start --web`.
+- Use `AsyncStorage` values (`auth_token`, `refresh_token`, `user_data`) directly into Playwright's `window.localStorage` to bypass login flows for Playwright testing via react-native-web.
+- When creating files in new subdirectories inside `/mobile/`, use `mkdir -p` before the write operation to ensure the path exists.
diff --git a/.Jules/todo.md b/.Jules/todo.md
index ebb0c7a..326aef8 100644
--- a/.Jules/todo.md
+++ b/.Jules/todo.md
@@ -57,8 +57,9 @@
- Impact: Native feel, users can easily refresh data
- Size: ~150 lines
-- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- - File: `mobile/screens/HomeScreen.js`
+- [x] **[ux]** Complete skeleton loading for HomeScreen groups
+ - Completed: 2026-03-08
+ - File: `mobile/screens/HomeScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js`
- Context: Replace ActivityIndicator with skeleton group cards
- Impact: Better loading experience, less jarring
- Size: ~40 lines
@@ -168,3 +169,7 @@
- Completed: 2026-02-08
- Files modified: `web/components/ui/PasswordStrength.tsx`, `web/pages/Auth.tsx`
- Impact: Provides visual feedback on password complexity during signup
+- [x] **[ux]** Complete skeleton loading for HomeScreen groups
+ - Completed: 2026-03-08
+ - Files modified: `mobile/screens/HomeScreen.js`, `mobile/screens/FriendsScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js`
+ - Impact: Better loading experience across the mobile app via generic skeleton components.
diff --git a/mobile/components/skeletons/GroupListSkeleton.js b/mobile/components/skeletons/GroupListSkeleton.js
new file mode 100644
index 0000000..02d0d1e
--- /dev/null
+++ b/mobile/components/skeletons/GroupListSkeleton.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import { StyleSheet, View } from 'react-native';
+import { Card } from 'react-native-paper';
+import Skeleton from '../ui/Skeleton';
+
+const GroupListSkeleton = ({ count = 5 }) => {
+ return (
+
+ {Array.from({ length: count }).map((_, index) => (
+
+ }
+ left={(props) => (
+
+ )}
+ />
+
+
+
+
+ ))}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ padding: 16,
+ },
+ card: {
+ marginBottom: 16,
+ },
+});
+
+export default GroupListSkeleton;
diff --git a/mobile/components/ui/Skeleton.js b/mobile/components/ui/Skeleton.js
new file mode 100644
index 0000000..e66aafd
--- /dev/null
+++ b/mobile/components/ui/Skeleton.js
@@ -0,0 +1,51 @@
+import React, { useEffect, useRef } from 'react';
+import { Animated, StyleSheet, View } from 'react-native';
+import { useTheme } from 'react-native-paper';
+
+const Skeleton = ({ width, height, borderRadius = 4, style }) => {
+ const theme = useTheme();
+ const opacityAnim = useRef(new Animated.Value(0.3)).current;
+
+ useEffect(() => {
+ const loop = Animated.loop(
+ Animated.sequence([
+ Animated.timing(opacityAnim, {
+ toValue: 1,
+ duration: 700,
+ useNativeDriver: true,
+ }),
+ Animated.timing(opacityAnim, {
+ toValue: 0.3,
+ duration: 700,
+ useNativeDriver: true,
+ }),
+ ])
+ );
+ loop.start();
+ return () => loop.stop();
+ }, [opacityAnim]);
+
+ return (
+
+ );
+};
+
+const styles = StyleSheet.create({
+ skeleton: {
+ overflow: 'hidden',
+ },
+});
+
+export default Skeleton;
diff --git a/mobile/screens/FriendsScreen.js b/mobile/screens/FriendsScreen.js
index c778a95..3c8b87c 100644
--- a/mobile/screens/FriendsScreen.js
+++ b/mobile/screens/FriendsScreen.js
@@ -1,6 +1,6 @@
import { useIsFocused } from "@react-navigation/native";
-import { useContext, useEffect, useRef, useState } from "react";
-import { Alert, Animated, FlatList, RefreshControl, StyleSheet, View } from "react-native";
+import { useContext, useEffect, useState } from "react";
+import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import {
Appbar,
Avatar,
@@ -15,6 +15,7 @@ import { triggerPullRefreshHaptic } from '../components/ui/hapticUtils';
import { getFriendsBalance, getGroups } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
import { formatCurrency } from "../utils/currency";
+import Skeleton from "../components/ui/Skeleton";
const FriendsScreen = () => {
const { token, user } = useContext(AuthContext);
@@ -167,42 +168,12 @@ const FriendsScreen = () => {
);
};
- // Shimmer skeleton components
- const opacityAnim = useRef(new Animated.Value(0.3)).current;
- useEffect(() => {
- const loop = Animated.loop(
- Animated.sequence([
- Animated.timing(opacityAnim, {
- toValue: 1,
- duration: 700,
- useNativeDriver: true,
- }),
- Animated.timing(opacityAnim, {
- toValue: 0.3,
- duration: 700,
- useNativeDriver: true,
- }),
- ])
- );
- loop.start();
- return () => loop.stop();
- }, [opacityAnim]);
-
const SkeletonRow = () => (
-
+
-
-
+
+
);
@@ -315,23 +286,6 @@ const styles = StyleSheet.create({
alignItems: "center",
marginBottom: 14,
},
- skeletonAvatar: {
- width: 48,
- height: 48,
- borderRadius: 24,
- backgroundColor: "#e0e0e0",
- },
- skeletonLine: {
- height: 14,
- backgroundColor: "#e0e0e0",
- borderRadius: 6,
- marginBottom: 6,
- },
- skeletonLineSmall: {
- height: 12,
- backgroundColor: "#e0e0e0",
- borderRadius: 6,
- },
});
export default FriendsScreen;
diff --git a/mobile/screens/HomeScreen.js b/mobile/screens/HomeScreen.js
index d2f3c38..7f5cf14 100644
--- a/mobile/screens/HomeScreen.js
+++ b/mobile/screens/HomeScreen.js
@@ -1,7 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import {
- ActivityIndicator,
Appbar,
Avatar,
Modal,
@@ -17,6 +16,7 @@ import * as Haptics from "expo-haptics";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
import { formatCurrency, getCurrencySymbol } from "../utils/currency";
+import GroupListSkeleton from "../components/skeletons/GroupListSkeleton";
const HomeScreen = ({ navigation }) => {
const { token, logout, user } = useContext(AuthContext);
@@ -257,9 +257,7 @@ const HomeScreen = ({ navigation }) => {
{isLoading ? (
-
-
-
+
) : (