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 @@ -29,6 +29,13 @@
- Toast notification system (`ToastContext`, `Toast` component) for providing non-blocking user feedback.
- Keyboard navigation support for Groups page, enabling accessibility for power users.

- **Mobile App:** Replaced generic loading spinner on Home Screen with a custom `GroupSkeleton` component.
- **Features:**
- Mimics actual list layout (Avatar + Title + Subtitle).
- Pulsing opacity animation using `Animated` API.
- Adapts to theme colors using `react-native-paper`'s `useTheme` (`surfaceVariant`).
- **Technical:** Created `mobile/components/GroupSkeleton.js`, integrated into `mobile/screens/HomeScreen.js`.

### Changed
- **Web App:** Refactored `GroupDetails` destructive actions (Delete Group, Delete Expense, Leave Group, Remove Member) to use the new `ConfirmDialog` instead of `window.confirm`.
- **Accessibility:** Updated `Modal` component to include proper ARIA roles and labels, fixing a long-standing accessibility gap.
Expand Down
19 changes: 19 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,25 @@ Commonly used components:
- `<Portal>` and `<Modal>` for overlays
- `<ActivityIndicator>` for loading states

### Skeleton Loading Pattern (Mobile)

**Date:** 2026-01-22
**Context:** Creating loading states for lists

Use `Animated` API combined with `react-native-paper` theme colors:

```javascript
const theme = useTheme();
const opacity = useRef(new Animated.Value(0.3)).current;
const color = theme.colors.surfaceVariant; // Adapt to theme

Animated.loop(...).start();

return (
<Animated.View style={{ opacity, backgroundColor: color }} />
);
```

### Safe Area Pattern

**Date:** 2026-01-01
Expand Down
12 changes: 6 additions & 6 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@
- Size: ~45 lines
- Added: 2026-01-01

- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- File: `mobile/screens/HomeScreen.js`
- Context: Replace ActivityIndicator with skeleton group cards
- Impact: Better loading experience, less jarring
- Size: ~40 lines
- Added: 2026-01-01
- [x] **[ux]** Complete skeleton loading for HomeScreen groups
- Completed: 2026-01-22
- File: `mobile/screens/HomeScreen.js`, `mobile/components/GroupSkeleton.js`
- Context: Replace ActivityIndicator with skeleton group cards using Animated API and Theme colors
- Impact: Better loading experience, less jarring, theme aware
- Size: ~80 lines

- [ ] **[a11y]** Complete accessibility labels for all screens
- Files: All screens in `mobile/screens/`
Expand Down
110 changes: 110 additions & 0 deletions mobile/components/GroupSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React, { useEffect, useRef } from "react";
import { View, StyleSheet, Animated } from "react-native";
import { Card, useTheme } from "react-native-paper";

const SkeletonItem = () => {
const theme = useTheme();
const opacity = useRef(new Animated.Value(0.3)).current;

// Use surfaceVariant for a neutral placeholder color that adapts to dark/light mode
const skeletonColor = theme.colors.surfaceVariant || "#E0E0E0";

useEffect(() => {
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();
}, [opacity]);
Comment on lines +12 to +27
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 cleanup for the animation loop to prevent memory leaks.

The animation loop is started but never stopped when the component unmounts. This could cause warnings or memory leaks if the skeleton is removed while the animation is running (e.g., when data finishes loading).

🛠️ Proposed fix
   useEffect(() => {
-    Animated.loop(
+    const animation = 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();
+    );
+    animation.start();
+    return () => animation.stop();
   }, [opacity]);
📝 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
useEffect(() => {
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();
}, [opacity]);
useEffect(() => {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(opacity, {
toValue: 0.7,
duration: 800,
useNativeDriver: true,
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: 800,
useNativeDriver: true,
}),
])
);
animation.start();
return () => animation.stop();
}, [opacity]);
🤖 Prompt for AI Agents
In `@mobile/components/GroupSkeleton.js` around lines 12 - 27, The Animated.loop
started inside the useEffect (wrapping Animated.sequence of Animated.timing on
opacity) never gets stopped; fix by capturing the looped animation (e.g., const
anim = Animated.loop(...)), call anim.start() in the effect, and return a
cleanup function that calls anim.stop() to halt the animation when the component
unmounts (update the useEffect that references opacity to return () =>
anim.stop()).


return (
<Card style={styles.card}>
<Card.Content style={styles.content}>
<View style={styles.row}>
{/* Avatar Skeleton */}
<Animated.View
style={[
styles.avatar,
{ opacity, backgroundColor: skeletonColor }
]}
/>

<View style={styles.textContainer}>
{/* Title Skeleton */}
<Animated.View
style={[
styles.title,
{ opacity, backgroundColor: skeletonColor }
]}
/>
{/* Subtitle/Status Skeleton */}
<Animated.View
style={[
styles.subtitle,
{ opacity, backgroundColor: skeletonColor }
]}
/>
</View>
</View>
</Card.Content>
</Card>
);
};

const GroupSkeleton = () => {
return (
<View style={styles.container}>
{[1, 2, 3, 4, 5].map((key) => (
<SkeletonItem key={key} />
))}
</View>
);
};

const styles = StyleSheet.create({
container: {
padding: 16,
},
card: {
marginBottom: 16,
},
content: {
paddingVertical: 8,
},
row: {
flexDirection: "row",
alignItems: "center",
},
avatar: {
width: 40,
height: 40,
borderRadius: 20,
marginRight: 16,
},
textContainer: {
flex: 1,
justifyContent: "center",
},
title: {
width: "60%",
height: 16,
borderRadius: 4,
marginBottom: 8,
},
subtitle: {
width: "40%",
height: 12,
borderRadius: 4,
},
});

export default GroupSkeleton;
6 changes: 2 additions & 4 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, StyleSheet, View } from "react-native";
import {
ActivityIndicator,
Appbar,
Avatar,
Button,
Expand All @@ -14,6 +13,7 @@ import {
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
import { formatCurrency, getCurrencySymbol } from "../utils/currency";
import GroupSkeleton from "../components/GroupSkeleton";

const HomeScreen = ({ navigation }) => {
const { token, logout, user } = useContext(AuthContext);
Expand Down Expand Up @@ -232,9 +232,7 @@ const HomeScreen = ({ navigation }) => {
</Appbar.Header>

{isLoading ? (
<View style={styles.loaderContainer}>
<ActivityIndicator size="large" />
</View>
<GroupSkeleton />
) : (
<FlatList
data={groups}
Expand Down
Loading