Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
## [Unreleased]

### Added
- **Mobile Skeleton Loading:** Implemented a reusable skeleton loading system for the mobile app.
- **Features:**
- Created `Skeleton` component with pulsing animation using `Animated` and `useTheme`.
- Created `GroupListSkeleton` component mimicking the layout of group cards.
- Integrated skeleton loading into `HomeScreen`, replacing the generic spinner.
- **Technical:** Created `mobile/components/ui/Skeleton.js` and `mobile/components/skeletons/GroupListSkeleton.js`. Updated `mobile/screens/HomeScreen.js`.

- **Password Strength Meter:** Added a visual password strength indicator to the signup form.
- **Features:**
- Real-time strength calculation (Length, Uppercase, Lowercase, Number, Symbol).
Expand Down
10 changes: 5 additions & 5 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@
- 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-02-09
- Files: `mobile/screens/HomeScreen.js`, `mobile/components/skeletons/GroupListSkeleton.js`, `mobile/components/ui/Skeleton.js`
- Context: Replace ActivityIndicator with skeleton group cards
- Impact: Better loading experience, less jarring
- Size: ~40 lines
- Added: 2026-01-01
- Impact: Professional loading experience that mimics actual content layout
- Size: ~60 lines

- [x] **[a11y]** Complete accessibility labels for all screens
- Completed: 2026-01-29
Expand Down
59 changes: 59 additions & 0 deletions mobile/components/skeletons/GroupListSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Unnecessary default React import — same redundancy as in Skeleton.js given the project's new JSX transform.

-import React from 'react';
+
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React from 'react';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/skeletons/GroupListSkeleton.js` at line 1, Remove the
unnecessary default React import at the top of GroupListSkeleton.js (the line
importing React from 'react'); the project uses the new JSX transform so the
import is redundant—simply delete that import statement from the
GroupListSkeleton component file to mirror the fix applied in Skeleton.js.

import { View, StyleSheet } from 'react-native';
import { Card } from 'react-native-paper';
import Skeleton from '../ui/Skeleton';

const GroupListSkeleton = () => {
// Render 6 skeleton items to fill the screen
return (
<View
style={styles.container}
accessible={true}
accessibilityRole="progressbar"
accessibilityLabel="Loading groups"
>
Comment on lines +8 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Loading container has no scroll capability — 6 cards may be clipped on small screens.

Six skeleton cards at roughly 100 px each (~600 px total) plus padding will exceed the available viewport on smaller devices (e.g., iPhone SE with ~611 px below the appbar). Wrapping in a ScrollView prevents the bottom cards from being clipped during the loading state.

♻️ Proposed fix
-import { View, StyleSheet } from 'react-native';
+import { View, StyleSheet, ScrollView } from 'react-native';
 
 return (
-  <View
+  <ScrollView
+    contentContainerStyle={styles.container}
     accessible={true}
     accessibilityRole="progressbar"
     accessibilityLabel="Loading groups"
   >
     {[...Array(6)].map((_, index) => ( ... ))}
-  </View>
+  </ScrollView>
 );
 
 const styles = StyleSheet.create({
   container: {
     padding: 16,
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/skeletons/GroupListSkeleton.js` around lines 8 - 14, The
loading container in GroupListSkeleton should be scrollable to avoid clipping
the six skeleton cards on small screens; update the GroupListSkeleton component
to replace or wrap the top-level View (using styles.container) with a ScrollView
(or wrap the existing View in a ScrollView) so the skeleton list can scroll,
preserve the accessibility props (accessible, accessibilityRole,
accessibilityLabel) on the ScrollView or move them appropriately, and ensure the
existing styles.container continues to apply to the content/container inside the
ScrollView.

{[...Array(6)].map((_, index) => (
<Card key={index} style={styles.card}>
<View style={styles.header}>
{/* Avatar Skeleton */}
<Skeleton width={40} height={40} borderRadius={20} />

{/* Title Skeleton */}
<View style={styles.titleContainer}>
<Skeleton width={120} height={20} borderRadius={4} />
</View>
</View>

<Card.Content>
{/* Status Skeleton */}
<Skeleton width={200} height={16} borderRadius={4} style={styles.status} />
Comment on lines +23 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded pixel widths (120, 200) are not responsive.

On narrow devices or with large accessibility font scales these values can exceed the available card width, causing visual overflow or misalignment. Use useWindowDimensions or a percentage of the card width instead.

♻️ Proposed refactor
+import { View, StyleSheet, useWindowDimensions } from 'react-native';
 
 const GroupListSkeleton = () => {
+  const { width: screenWidth } = useWindowDimensions();
+  const cardInnerWidth = screenWidth - 32 - 32; // container padding (16×2) + card padding (16×2)
   ...
   {/* Title Skeleton */}
-  <Skeleton width={120} height={20} borderRadius={4} />
+  <Skeleton width={cardInnerWidth * 0.5} height={20} borderRadius={4} />
   ...
   {/* Status Skeleton */}
-  <Skeleton width={200} height={16} borderRadius={4} style={styles.status} />
+  <Skeleton width={cardInnerWidth * 0.75} height={16} borderRadius={4} style={styles.status} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/skeletons/GroupListSkeleton.js` around lines 23 - 29,
GroupListSkeleton currently uses hardcoded pixel widths for Skeleton components
(width={120} and width={200}) which can overflow on narrow screens or with large
font scales; update the component to compute responsive widths instead (e.g.,
useWindowDimensions or calculate based on container/card width) and replace the
literal values passed to the Skeleton props and styles.status with the computed
responsive values or percentage/flex-based sizing so the Skeleton adapts to
screen size and accessibility settings.

</Card.Content>
</Card>
))}
</View>
);
};

const styles = StyleSheet.create({
container: {
padding: 16,
},
card: {
marginBottom: 16,
},
header: {
flexDirection: 'row',
alignItems: 'center',
padding: 16, // Mimics Card.Title padding
},
titleContainer: {
marginLeft: 16,
flex: 1,
justifyContent: 'center',
},
status: {
marginTop: 4,
}
});

export default GroupListSkeleton;
46 changes: 46 additions & 0 deletions mobile/components/ui/Skeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { useEffect, useRef } from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Unnecessary default React import.

The project uses the new JSX transform (see HomeScreen.js which only imports named hooks from 'react'). The default React import is not needed.

-import React, { useEffect, useRef } from 'react';
+import { useEffect, useRef } from 'react';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import React, { useEffect, useRef } from 'react';
import { useEffect, useRef } from 'react';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/ui/Skeleton.js` at line 1, Remove the unused default React
import from Skeleton.js: the file currently imports "React, { useEffect, useRef
}" but only uses the named hooks; update the import to only pull the named
exports (remove the default "React") so the line becomes "import { useEffect,
useRef } from 'react'". This mirrors the new JSX transform usage in
HomeScreen.js and avoids the unnecessary default import.

import { Animated } from 'react-native';
import { useTheme } from 'react-native-paper';

const Skeleton = ({ width, height, borderRadius = 4, style }) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

width and height are implicitly required but have no default values or validation.

If a caller omits either prop, the Animated.View renders at 0×0 and the skeleton disappears silently with no warning. Add explicit defaults or mark them required.

🛡️ Proposed fix
-const Skeleton = ({ width, height, borderRadius = 4, style }) => {
+const Skeleton = ({ width = 0, height = 0, borderRadius = 4, style }) => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const Skeleton = ({ width, height, borderRadius = 4, style }) => {
const Skeleton = ({ width = 0, height = 0, borderRadius = 4, style }) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/ui/Skeleton.js` at line 5, The Skeleton component currently
treats width and height as implicitly required, causing a 0×0 render if omitted;
update the Skeleton functional component to provide safe defaults for width and
height (e.g., fallback numeric values) or add runtime validation that
throws/warns when width or height are missing, and ensure the Animated.View uses
those validated values; reference the Skeleton component signature and the props
width and height so reviewers can find and update the default values or add
prop-type checks/console.warn as appropriate.

const theme = useTheme();
const opacity = useRef(new Animated.Value(0.5)).current;

useEffect(() => {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(opacity, {
toValue: 1,
duration: 800,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.5,
duration: 800,
useNativeDriver: true,
}),
Comment on lines +12 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Missing easing — the default Easing.inOut(Easing.ease) is non-standard for skeleton pulses.

Skeleton animations conventionally use Easing.linear to produce a steady, uniform shimmer. The current ease-in-out default will produce acceleration/deceleration that doesn't match the typical skeleton UX pattern.

♻️ Proposed refactor
+import { Animated, Easing } from 'react-native';
 
 Animated.timing(opacity, {
   toValue: 1,
   duration: 800,
+  easing: Easing.linear,
   useNativeDriver: true,
 }),
 Animated.timing(opacity, {
   toValue: 0.5,
   duration: 800,
+  easing: Easing.linear,
   useNativeDriver: true,
 }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@mobile/components/ui/Skeleton.js` around lines 12 - 21, The Animated.timing
calls that drive the skeleton pulse (the two Animated.timing(...) usages
operating on the opacity Animated.Value) lack an explicit easing and therefore
use the default ease-in-out behavior; change both timing configs to include
easing: Easing.linear to produce a steady linear pulse, and ensure Easing is
imported (e.g., import { Easing } from 'react-native') so the Skeleton
component's pulse matches standard skeleton UX.

])
);

animation.start();

return () => animation.stop();
}, [opacity]);

return (
<Animated.View
style={[
{
width,
height,
borderRadius,
backgroundColor: theme.colors.surfaceVariant,
opacity,
},
style,
]}
/>
);
};

export default Skeleton;
11 changes: 2 additions & 9 deletions mobile/screens/HomeScreen.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import {
ActivityIndicator,
Appbar,
Avatar,
Modal,
Expand All @@ -13,6 +12,7 @@ import {
import HapticButton from '../components/ui/HapticButton';
import HapticCard from '../components/ui/HapticCard';
import { HapticAppbarAction } from '../components/ui/HapticAppbar';
import GroupListSkeleton from '../components/skeletons/GroupListSkeleton';
import * as Haptics from "expo-haptics";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
Expand Down Expand Up @@ -257,9 +257,7 @@ const HomeScreen = ({ navigation }) => {
</Appbar.Header>

{isLoading ? (
<View style={styles.loaderContainer}>
<ActivityIndicator size="large" />
</View>
<GroupListSkeleton />
) : (
<FlatList
data={groups}
Expand Down Expand Up @@ -289,11 +287,6 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
loaderContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
list: {
padding: 16,
},
Expand Down
Loading