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:**
- Created `SkeletonGroupCard` component mimicking the actual group card layout.
- Implemented pulsing opacity animation (`Animated.loop`) for visual feedback.
- Used `useTheme` to ensure placeholders match the active theme (Dark/Light).
- Replaced generic `ActivityIndicator` with a list of skeletons.
- **Technical:** Created `mobile/components/SkeletonGroupCard.js` and updated `mobile/screens/HomeScreen.js`.

- **Mobile Accessibility:** Completed accessibility audit for all mobile screens.
- **Features:**
- Added `accessibilityLabel` to all interactive elements (buttons, inputs, list items).
Expand Down
10 changes: 5 additions & 5 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +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-04
- Files: `mobile/screens/HomeScreen.js`, `mobile/components/SkeletonGroupCard.js`
- Context: Replaced ActivityIndicator with pulsing skeleton cards matching list layout
- Impact: Better loading experience, less jarring
- Size: ~40 lines
- Added: 2026-01-01
- Size: ~70 lines
Comment on lines +60 to +65
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

Fix the completion date (currently in the future).

“Completed: 2026-02-04” is after the PR creation date (2026-02-02) and today (2026-02-02), which makes the log inaccurate. Please set it to the actual completion date or remove it until completion.

🛠️ Possible fix
-  - Completed: 2026-02-04
+  - Completed: 2026-02-02
📝 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
- [x] **[ux]** Complete skeleton loading for HomeScreen groups
- Completed: 2026-02-04
- Files: `mobile/screens/HomeScreen.js`, `mobile/components/SkeletonGroupCard.js`
- Context: Replaced ActivityIndicator with pulsing skeleton cards matching list layout
- Impact: Better loading experience, less jarring
- Size: ~40 lines
- Added: 2026-01-01
- Size: ~70 lines
- [x] **[ux]** Complete skeleton loading for HomeScreen groups
- Completed: 2026-02-02
- Files: `mobile/screens/HomeScreen.js`, `mobile/components/SkeletonGroupCard.js`
- Context: Replaced ActivityIndicator with pulsing skeleton cards matching list layout
- Impact: Better loading experience, less jarring
- Size: ~70 lines
🤖 Prompt for AI Agents
In @.Jules/todo.md around lines 60 - 65, The completion date on the Todo item in
.Jules/todo.md is set to the future (Completed: 2026-02-04); update that line to
the actual completion date (e.g., 2026-02-02) or remove the "Completed:" line
until it is truly complete so the log is accurate; edit the checklist entry for
"Complete skeleton loading for HomeScreen groups" (the same block that lists
Files: mobile/screens/HomeScreen.js, mobile/components/SkeletonGroupCard.js) to
reflect the corrected date or omit the completion date entirely.


- [x] **[a11y]** Complete accessibility labels for all screens
- Completed: 2026-01-29
Expand Down
87 changes: 87 additions & 0 deletions mobile/components/SkeletonGroupCard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useEffect, useRef } from "react";
import { View, StyleSheet, Animated } from "react-native";
import { Card, useTheme } from "react-native-paper";

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

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();
}, []);
Comment on lines +9 to +24
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "SkeletonGroupCard.js" --type f

Repository: Devasy/splitwiser

Length of output: 99


🏁 Script executed:

cat -n mobile/components/SkeletonGroupCard.js

Repository: Devasy/splitwiser

Length of output: 2752


🌐 Web query:

React Native Animated.loop cleanup memory leak unmount

💡 Result:

To avoid “leaks” (and React warnings) with Animated.loop(...) when a component unmounts, treat the loop like any other long‑running subscription: start it in an effect and stop it in the cleanup.

Why this matters

  • Animated.loop returns a CompositeAnimation that can be stopped (and often reset) explicitly. The loop “resets and begins again” each iteration. [1]
  • While RN generally detaches animated props on unmount (often stopping animations implicitly), people have hit real retention issues in some scenarios—e.g., an Animation.start() completion callback being retained when an animated component unmounts mid‑animation. [2] Stopping on unmount is the safest pattern.

Recommended cleanup pattern (hooks)

import { useEffect, useRef } from 'react';
import { Animated, Easing } from 'react-native';

export function useLoopingSpin() {
  const value = useRef(new Animated.Value(0)).current;
  const loopRef = useRef<Animated.CompositeAnimation | null>(null);

  useEffect(() => {
    const anim = Animated.loop(
      Animated.timing(value, {
        toValue: 1,
        duration: 1000,
        easing: Easing.linear,
        useNativeDriver: true,
        isInteraction: false, // helps avoid blocking VirtualizedList rendering while running [1]
      })
    );

    loopRef.current = anim;
    anim.start(); // avoid passing a completion callback unless you truly need it [2]

    return () => {
      loopRef.current?.stop();   // stop the loop explicitly
      loopRef.current = null;
      value.stopAnimation();     // extra safety: stop any driver/tracking on the value
      value.setValue(0);         // optional: reset for next mount
    };
  }, [value]);

  return value;
}

Also clean up listeners (common real leak source)

If you used value.addListener(...), remove it in the same cleanup (value.removeListener(id) / value.removeAllListeners()), since listeners are async callbacks that can outlive the screen. [3]

Sources: React Native Animated.loop docs (and isInteraction note) [1]; RN issue documenting callback retention when unmounting mid‑animation [2]; RN animations docs on addListener / async updates [3].


Stop the animation loop on unmount to prevent memory leaks.

The Animated.loop(...).start() call runs indefinitely without cleanup. On component unmount, the animation continues running in the background, retaining references and potentially blocking garbage collection. Store the animation reference and explicitly call .stop() in the effect cleanup function.

🧩 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();
   }, []);
📝 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();
}, []);
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();
}, []);
🤖 Prompt for AI Agents
In `@mobile/components/SkeletonGroupCard.js` around lines 9 - 24, The
Animated.loop started in the useEffect for the opacity animation never gets
stopped, causing a memory leak; capture the returned animation instance from
Animated.loop(Animated.sequence(...)) into a variable (e.g., anim or loopAnim)
before calling .start(), and in the useEffect cleanup return a function that
calls .stop() on that saved animation instance (reference the useEffect,
Animated.loop, Animated.sequence, opacity and .start() to locate the code) so
the animation is explicitly stopped on unmount.


const styles = StyleSheet.create({
card: {
marginBottom: 16,
backgroundColor: theme.colors.surface,
},
row: {
flexDirection: "row",
alignItems: "center",
padding: 16,
},
avatarPlaceholder: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: theme.colors.surfaceVariant,
marginRight: 16,
},
textContainer: {
flex: 1,
justifyContent: "center",
},
titlePlaceholder: {
height: 20,
width: "60%",
backgroundColor: theme.colors.surfaceVariant,
borderRadius: 4,
},
cardContent: {
paddingHorizontal: 16,
paddingBottom: 16,
},
statusPlaceholder: {
height: 14,
width: "40%",
backgroundColor: theme.colors.surfaceVariant,
borderRadius: 4,
},
});

return (
<Card
style={styles.card}
accessible={true}
accessibilityRole="progressbar"
accessibilityLabel="Loading group"
>
<Animated.View style={{ opacity }}>
<View style={styles.row}>
<View style={styles.avatarPlaceholder} />
<View style={styles.textContainer}>
<View style={styles.titlePlaceholder} />
</View>
</View>
<View style={styles.cardContent}>
<View style={styles.statusPlaceholder} />
</View>
</Animated.View>
</Card>
);
};

export default SkeletonGroupCard;
10 changes: 6 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, RefreshControl, StyleSheet, View } from "react-native";
import {
ActivityIndicator,
Appbar,
Avatar,
Button,
Expand All @@ -12,6 +11,7 @@ import {
TextInput,
useTheme,
} from "react-native-paper";
import SkeletonGroupCard from "../components/SkeletonGroupCard";
import * as Haptics from "expo-haptics";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
Expand All @@ -33,7 +33,7 @@ const HomeScreen = ({ navigation }) => {
const showModal = () => setModalVisible(true);
const hideModal = () => setModalVisible(false);

// Calculate settlement status for a group
// Calculate settlement status for a group (owes/owed)
const calculateSettlementStatus = async (groupId, userId) => {
try {
const response = await getOptimizedSettlements(groupId);
Expand Down Expand Up @@ -256,8 +256,10 @@ const HomeScreen = ({ navigation }) => {
</Appbar.Header>

{isLoading ? (
<View style={styles.loaderContainer}>
<ActivityIndicator size="large" />
<View style={styles.list}>
{[1, 2, 3, 4].map((i) => (
<SkeletonGroupCard key={i} />
))}
</View>
) : (
<FlatList
Expand Down
Loading