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

### Added
- **Mobile Skeleton Loading:** Implemented a skeleton loading state for the Home screen groups list.
- **Features:**
- Created a reusable `Skeleton` component using `Animated` for pulsing opacity.
- Created `GroupListSkeleton` to mimic the exact layout of group cards.
- Replaced the generic `ActivityIndicator` spinner with the skeleton loader for a smoother UX.
- Matches `react-native-paper` theme colors (uses `surfaceVariant`).
- **Technical:** Created `mobile/components/ui/Skeleton.js`, `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
21 changes: 21 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,27 @@ Commonly used components:
- `<Portal>` and `<Modal>` for overlays
- `<ActivityIndicator>` for loading states

### Skeleton Loading Pattern

**Date:** 2026-02-12
**Context:** Implemented in `HomeScreen` for better perceived performance.

Use `mobile/components/ui/Skeleton.js` for building loading states:
1. **Base Component:** `<Skeleton width={100} height={20} />`
2. **Animation:** Uses `Animated.loop` for pulsing opacity (0.3 <-> 1.0).
3. **Theming:** Automatically uses `theme.colors.surfaceVariant`.
4. **Layout:** Combine multiple `Skeleton` components inside a container (e.g., `GroupListSkeleton`) that mimics the actual content layout (e.g., inside a `Card`).

**Example:**
```jsx
<Card>
<Card.Title
title={<Skeleton width={150} height={20} />}
left={(props) => <Skeleton width={40} height={40} borderRadius={20} />}
/>
</Card>
```
Comment on lines +313 to +321
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

Add a blank line before the fenced code block to fix the MD031 lint warning.

markdownlint requires blank lines surrounding fenced code blocks.

📝 Proposed fix
 **Example:**
+
 ```jsx
 <Card>
📝 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
**Example:**
```jsx
<Card>
<Card.Title
title={<Skeleton width={150} height={20} />}
left={(props) => <Skeleton width={40} height={40} borderRadius={20} />}
/>
</Card>
```
**Example:**
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 314-314: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.Jules/knowledge.md around lines 313 - 321, Add a blank line immediately
before the fenced code block that begins with ```jsx so the example block is
separated from the preceding text; update the section containing the
Card/Card.Title/Skeleton example to insert one empty line before the opening
```jsx fence to satisfy markdownlint MD031.


### Safe Area Pattern

**Date:** 2026-01-01
Expand Down
9 changes: 5 additions & 4 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`
- Context: Replace ActivityIndicator with skeleton group cards
- [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/GroupListSkeleton.js`
- Context: Created reusable Skeleton component and GroupListSkeleton to replace spinner
- 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/GroupListSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { View } from 'react-native';
import { Card } from 'react-native-paper';
import Skeleton from '../ui/Skeleton';

const GroupListSkeleton = () => {
// Render 4 skeleton items to fill the screen
return (
<View style={{ padding: 16 }} accessibilityLabel="Loading groups">
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

accessibilityLabel on a plain View requires accessible={true} to be reliably announced.

Without accessible={true}, React Native may not treat this View as a focusable accessibility element and the label may be ignored on both iOS and Android. Add accessible={true} so the container is announced as a single element and screen readers skip traversal into children.

♿ Proposed fix
-      <View style={{ padding: 16 }} accessibilityLabel="Loading groups">
+      <View style={{ padding: 16 }} accessible={true} accessibilityLabel="Loading groups">
📝 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
<View style={{ padding: 16 }} accessibilityLabel="Loading groups">
<View style={{ padding: 16 }} accessible={true} accessibilityLabel="Loading groups">
🤖 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 9, The View in
GroupListSkeleton that sets accessibilityLabel="Loading groups" must also
include accessible={true} so screen readers reliably announce the container as a
single element; update the View in the GroupListSkeleton component to add
accessible={true} alongside the existing accessibilityLabel prop so assistive
tech skips its children and announces "Loading groups".

{[1, 2, 3, 4].map((i) => (
<Card key={i} style={{ marginBottom: 16 }}>
<View style={{ flexDirection: 'row', alignItems: 'center', padding: 16 }}>
<Skeleton width={40} height={40} borderRadius={20} style={{ marginRight: 16 }} />
<Skeleton width={150} height={20} />
</View>
<Card.Content>
<Skeleton width={220} height={16} style={{ marginTop: 4 }} />
</Card.Content>
</Card>
))}
</View>
);
};

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

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

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

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

return (
<Animated.View
style={[
{
width,
height,
borderRadius: borderRadius || 4,
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

Use nullish coalescing ?? instead of || for the borderRadius default.

borderRadius || 4 evaluates to 4 when borderRadius={0} is explicitly passed (e.g., sharp corners), silently ignoring the caller's intent. Use ?? to only fall back when the value is null/undefined.

🐛 Proposed fix
-          borderRadius: borderRadius || 4,
+          borderRadius: borderRadius ?? 4,
📝 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
borderRadius: borderRadius || 4,
borderRadius: borderRadius ?? 4,
🤖 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 35, In the Skeleton component
replace the fallback for borderRadius so it doesn't override explicit zero
values: update the style assignment that currently uses "borderRadius:
borderRadius || 4" to use nullish coalescing (borderRadius ?? 4) so only
null/undefined fall back to 4; locate this in the Skeleton component's
render/style object where borderRadius is referenced.

backgroundColor: theme.colors.surfaceVariant,
opacity,
},
style,
]}
/>
Comment on lines +30 to +41
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

Hide the skeleton placeholder from screen readers.

Without accessible={false}, screen readers may traverse into this animated view and announce nothing useful (or confuse users). Add accessible={false} to suppress it, and importantForAccessibility="no-hide-descendants" on Android for full suppression.

♿ Proposed fix
     <Animated.View
+      accessible={false}
+      importantForAccessibility="no-hide-descendants"
       style={[
🤖 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 30 - 41, The Animated.View
used in the Skeleton component should be hidden from screen readers: update the
Animated.View (in mobile/components/ui/Skeleton.js) to include
accessible={false} and importantForAccessibility="no-hide-descendants" so
assistive tech ignores the placeholder (importantForAccessibility applies on
Android for full suppression).

);
};

export default Skeleton;
10 changes: 2 additions & 8 deletions mobile/screens/HomeScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,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 +258,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 +288,6 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
loaderContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
list: {
padding: 16,
},
Expand Down
Loading