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 skeleton loading state for HomeScreen.
- **Features:**
- Replaced `ActivityIndicator` with `GroupListSkeleton` component.
- Mimics exact layout of group cards (Avatar, Title, Status).
- Uses pulsing animation for smooth transition.
- Uses theme colors (`surfaceVariant`) for dark/light mode support.
- **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
12 changes: 7 additions & 5 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,10 @@
- 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-14
- Files modified: `mobile/screens/HomeScreen.js`, `mobile/components/skeletons/GroupListSkeleton.js`, `mobile/components/ui/Skeleton.js`
- Impact: Better loading experience, less jarring
- Size: ~40 lines
- Added: 2026-01-01

- [x] **[a11y]** Complete accessibility labels for all screens
- Completed: 2026-01-29
Expand Down Expand Up @@ -168,3 +166,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-02-14
- Files modified: `mobile/screens/HomeScreen.js`, `mobile/components/skeletons/GroupListSkeleton.js`, `mobile/components/ui/Skeleton.js`
- Impact: Better loading experience, less jarring
66 changes: 66 additions & 0 deletions mobile/components/skeletons/GroupListSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import { View, StyleSheet, FlatList } from 'react-native';
import { Card } from 'react-native-paper';
import Skeleton from '../ui/Skeleton';

const GroupListSkeleton = () => {
// Create 6 dummy items to fill the screen
const dummyData = Array(6).fill(null);

const renderItem = () => (
<Card style={styles.card}>
{/* Header Area (Avatar + Title) */}
<View style={styles.header}>
<Skeleton width={40} height={40} borderRadius={20} />
<View style={styles.titleContainer}>
<Skeleton width="50%" height={20} borderRadius={4} />
</View>
</View>

{/* Content Area (Status Text) */}
<View style={styles.content}>
<Skeleton width="75%" height={16} borderRadius={4} />
</View>
</Card>
);

return (
<View style={styles.container} accessible={true} accessibilityLabel="Loading groups">
<FlatList
data={dummyData}
renderItem={renderItem}
keyExtractor={(_, index) => index.toString()}
contentContainerStyle={styles.list}
scrollEnabled={false}
/>
</View>
);
};
Comment on lines +6 to +38
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

Prefer a simple map over FlatList for this static, non-scrollable list.

FlatList is designed for virtualized, scrollable lists. Here it's used with scrollEnabled={false} and a fixed item count of 6 — the virtualization machinery (scroll listeners, layout measurement passes, window sizing) runs entirely for nothing. Additionally, dummyData is recreated on every render inside the component.

♻️ Proposed refactor
-import { View, StyleSheet, FlatList } from 'react-native';
+import { View, StyleSheet } from 'react-native';
 import { Card } from 'react-native-paper';
 import Skeleton from '../ui/Skeleton';

+const SKELETON_COUNT = 6;
+const SKELETON_KEYS = Array.from({ length: SKELETON_COUNT }, (_, i) => i);

 const GroupListSkeleton = () => {
-  // Create 6 dummy items to fill the screen
-  const dummyData = Array(6).fill(null);
-
-  const renderItem = () => (
+  const renderCard = (key) => (
+    <Card key={key} style={styles.card}>
-    <Card style={styles.card}>
       {/* Header Area (Avatar + Title) */}
       <View style={styles.header}>
         <Skeleton width={40} height={40} borderRadius={20} />
         <View style={styles.titleContainer}>
           <Skeleton width="50%" height={20} borderRadius={4} />
         </View>
       </View>
       {/* Content Area (Status Text) */}
       <View style={styles.content}>
         <Skeleton width="75%" height={16} borderRadius={4} />
       </View>
     </Card>
   );

   return (
     <View style={styles.container} accessible={true} accessibilityLabel="Loading groups">
-      <FlatList
-        data={dummyData}
-        renderItem={renderItem}
-        keyExtractor={(_, index) => index.toString()}
-        contentContainerStyle={styles.list}
-        scrollEnabled={false}
-      />
+      <View style={styles.list}>
+        {SKELETON_KEYS.map(renderCard)}
+      </View>
     </View>
   );
 };
🤖 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 6 - 38,
Replace the unnecessary FlatList in GroupListSkeleton with a simple map over a
memoized dummyData array: move or memoize the dummyData (e.g., define
Array(6).fill(null) outside the component or use useMemo inside) to avoid
recreating it each render, remove renderItem as a FlatList prop and instead map
dummyData.map((_, index) => ( ... )) to return the Card JSX (use the existing
JSX in renderItem), ensure each mapped Card has a stable key (index.toString()),
and keep existing accessibility props and styles (styles.card, styles.header,
styles.titleContainer, styles.content) intact.


const styles = StyleSheet.create({
container: {
flex: 1,
},
list: {
padding: 16,
},
card: {
marginBottom: 16,
// Card has default elevation and background color from theme
},
header: {
flexDirection: 'row',
alignItems: 'center',
padding: 16, // Match Card.Title padding
},
titleContainer: {
marginLeft: 16,
flex: 1,
},
content: {
paddingHorizontal: 16,
paddingBottom: 16, // Match Card.Content padding
},
});

export default GroupListSkeleton;
48 changes: 48 additions & 0 deletions mobile/components/ui/Skeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet } from 'react-native';
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

Remove unused StyleSheet import.

StyleSheet is imported but never referenced — there is no StyleSheet.create() call in this file; all styles are inline objects.

🔧 Proposed fix
-import { Animated, StyleSheet } from 'react-native';
+import { Animated } from 'react-native';
📝 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 { Animated, StyleSheet } from 'react-native';
import { Animated } from 'react-native';
🤖 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 2, Remove the unused StyleSheet
import from the module import line in Skeleton.js: update the import that
currently reads "import { Animated, StyleSheet } from 'react-native';" to only
import Animated. Ensure no other references to StyleSheet exist in
functions/components such as the Skeleton component so the linter no longer
reports an unused import.

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: 0.7, // Don't go to fully opaque so it looks like a placeholder
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

borderRadius || 4 silently ignores borderRadius={0}.

The falsy OR operator means passing borderRadius={0} (a legitimate value for a sharp-edged skeleton) falls through to 4. Prefer nullish coalescing.

🔧 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 36, The style currently uses the
falsy OR operator which causes a passed borderRadius={0} to be ignored; update
the Skeleton component's style assignment (the borderRadius usage in
mobile/components/ui/Skeleton.js) to use the nullish coalescing operator (??) so
that only null or undefined fall back to 4 (i.e., replace borderRadius:
borderRadius || 4 with a nullish-coalescing equivalent) to preserve explicit
zero values.

backgroundColor: theme.colors.surfaceVariant, // Adapts to light/dark mode
opacity,
},
style,
]}
accessibilityRole="progressbar"
accessibilityLabel="Loading..."
/>
);
};

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