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
9 changes: 9 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
## [Unreleased]

### Added
- **Mobile Skeleton Loading:** Implemented skeleton loading states for the Home screen group list.
- **Features:**
- Replaced `ActivityIndicator` with a list of `GroupCardSkeleton` components.
- Mimics the exact layout of the group cards (Avatar + Title + Subtitle).
- Uses `Animated` for a smooth pulsing opacity effect.
- Automatically adapts to light/dark mode using `react-native-paper` theme.
- Accessible container with `accessibilityLabel="Loading groups"`.
- **Technical:** Created `mobile/components/ui/Skeleton.js` and `mobile/components/skeletons/GroupCardSkeleton.js`. Integrated into `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
25 changes: 25 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,31 @@ When building mobile screens with React Native Paper:
3. **Hints:** Use `accessibilityHint` for non-obvious actions (e.g., "Double tap to delete").
4. **State:** For custom checkboxes or toggles, use `accessibilityState={{ checked: boolean }}`.

### Skeleton Loading Pattern

**Date:** 2026-02-12
**Context:** Implementing HomeScreen skeletons

To create professional loading states in React Native:

1. **Base Component:** Create a `Skeleton` component using `Animated` for pulsing opacity.
* Use `useTheme` to set background color (e.g., `theme.colors.surfaceVariant`).
* Use `useNativeDriver: true` for performance.
2. **Layout Mirroring:** Create a specific skeleton component (e.g., `GroupCardSkeleton`) that mirrors the actual component's layout using `Card`, `View`, and spacing.
3. **Integration:**
* Replace `ActivityIndicator` with a list of skeletons.
* Wrap in a `View` with `accessible={true}` and `accessibilityLabel="Loading..."`.

```javascript
// Animation Logic
Animated.loop(
Animated.sequence([
Animated.timing(opacity, { toValue: 0.7, duration: 800, useNativeDriver: true }),
Animated.timing(opacity, { toValue: 0.3, duration: 800, useNativeDriver: true }),
])
).start();
```

---

## API Response Patterns
Expand Down
7 changes: 4 additions & 3 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +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-12
- Files: `mobile/screens/HomeScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupCardSkeleton.js`
- Context: Replace ActivityIndicator with skeleton group cards
- Impact: Better loading experience, less jarring
- Size: ~40 lines
- Size: ~80 lines
- Added: 2026-01-01

- [x] **[a11y]** Complete accessibility labels for all screens
Expand Down
25 changes: 25 additions & 0 deletions mobile/components/skeletons/GroupCardSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { Card } from 'react-native-paper';
import Skeleton from '../ui/Skeleton';

const GroupCardSkeleton = () => {
return (
<Card style={{ marginBottom: 16 }}>
<Card.Title
title={<Skeleton width={120} height={20} />}
left={(props) => (
<Skeleton
width={40}
height={40}
borderRadius={20}
/>
)}
/>
<Card.Content>
<Skeleton width={200} height={16} style={{ marginTop: 4 }} />
</Card.Content>
</Card>
Comment on lines +7 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

Hardcoded pixel widths won't adapt to varying screen sizes or content.

The skeleton widths (120, 200, 40) are fixed. On narrower devices, the 200px content skeleton could overflow, and on wider devices it may look undersized. Consider using percentage-based widths or measuring the container.

Also, the left callback receives props (which includes size from Card.Title) but doesn't use it. Using props.size for the avatar skeleton dimensions would keep it consistent with the actual Card.Title avatar sizing.

Proposed improvement
       left={(props) => (
         <Skeleton
-          width={40}
-          height={40}
+          width={props.size}
+          height={props.size}
           borderRadius={20}
         />
       )}
📝 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
<Card style={{ marginBottom: 16 }}>
<Card.Title
title={<Skeleton width={120} height={20} />}
left={(props) => (
<Skeleton
width={40}
height={40}
borderRadius={20}
/>
)}
/>
<Card.Content>
<Skeleton width={200} height={16} style={{ marginTop: 4 }} />
</Card.Content>
</Card>
<Card style={{ marginBottom: 16 }}>
<Card.Title
title={<Skeleton width={120} height={20} />}
left={(props) => (
<Skeleton
width={props.size}
height={props.size}
borderRadius={20}
/>
)}
/>
<Card.Content>
<Skeleton width={200} height={16} style={{ marginTop: 4 }} />
</Card.Content>
</Card>
🤖 Prompt for AI Agents
In `@mobile/components/skeletons/GroupCardSkeleton.js` around lines 7 - 21, The
skeleton uses fixed pixel widths (120, 200, 40) which don't adapt to
screen/container size; update the Skeleton sizing in GroupCardSkeleton.js to use
responsive values (percentages or measured container width) for the title and
content Skeletons and use the Card.Title left callback's props.size for the
avatar Skeleton dimensions so the avatar skeleton matches Card.Title's
configured size (replace the hardcoded 40/borderRadius 20 with values derived
from props.size). Ensure the title/content Skeleton widths are relative (e.g.,
"60%" / "100%" of Card.Content or calculated from a container measurement)
rather than fixed pixels.

);
};

export default GroupCardSkeleton;
44 changes: 44 additions & 0 deletions mobile/components/ui/Skeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { useEffect, useRef } from 'react';
import { Animated } from 'react-native';
import { useTheme } from 'react-native-paper';

const Skeleton = ({ width, height, borderRadius = 4, style }) => {
const theme = useTheme();
const opacity = useRef(new Animated.Value(0.3)).current;

useEffect(() => {
const pulse = Animated.loop(
Animated.sequence([
Animated.timing(opacity, {
toValue: 0.7,
duration: 800,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: 800,
useNativeDriver: true,
}),
])
);
pulse.start();
return () => pulse.stop();
}, [opacity]);

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

export default Skeleton;
17 changes: 9 additions & 8 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 @@ -12,6 +11,7 @@ import {
} from "react-native-paper";
import HapticButton from '../components/ui/HapticButton';
import HapticCard from '../components/ui/HapticCard';
import GroupCardSkeleton from '../components/skeletons/GroupCardSkeleton';
import { HapticAppbarAction } from '../components/ui/HapticAppbar';
import * as Haptics from "expo-haptics";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
Expand Down Expand Up @@ -257,8 +257,14 @@ const HomeScreen = ({ navigation }) => {
</Appbar.Header>

{isLoading ? (
<View style={styles.loaderContainer}>
<ActivityIndicator size="large" />
<View
style={styles.list}
accessible={true}
accessibilityLabel="Loading groups"
>
{[1, 2, 3, 4, 5].map((key) => (
<GroupCardSkeleton key={key} />
))}
</View>
) : (
<FlatList
Expand Down Expand Up @@ -289,11 +295,6 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
loaderContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
list: {
padding: 16,
},
Expand Down
Loading