Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Copy Markdown
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
27 changes: 27 additions & 0 deletions mobile/components/skeletons/GroupListSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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
Copy Markdown
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 }}>
<Card.Title
title={<Skeleton width={150} height={20} />}
left={(props) => (
<Skeleton width={40} height={40} borderRadius={20} />
)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Prefix the unused props parameter with _ to signal intentional non-use.

The left render prop receives { size: number } from Card.Title, but Skeleton uses fixed dimensions. Renaming to _props (or just _) prevents lint warnings and communicates intent clearly.

♻️ Proposed fix
-            left={(props) => (
+            left={(_props) => (
               <Skeleton width={40} height={40} 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
left={(props) => (
<Skeleton width={40} height={40} borderRadius={20} />
)}
left={(_props) => (
<Skeleton width={40} height={40} borderRadius={20} />
)}
🤖 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 14 - 16, The
render-prop parameter named props in the left prop of Card.Title is unused and
should be prefixed to avoid linter warnings; rename props to _props (or _) in
the left={(props) => (...) } arrow function so it becomes left={(_props) =>
(<Skeleton width={40} height={40} borderRadius={20} />)} to signal intentional
non-use while keeping the Skeleton usage unchanged in GroupListSkeleton.js.

/>
Comment thread
Devasy marked this conversation as resolved.
Outdated
<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
Copy Markdown
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
Copy Markdown
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