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:** Replaced basic ActivityIndicator with a sophisticated `GroupListSkeleton` in `HomeScreen`.
- **Features:**
- Created reusable animated `Skeleton` primitive.
- Created `GroupListSkeleton` mimicking actual `HapticCard` layout.
- Implemented subtle pulse animation matching theme variants.
- Fully accessible with appropriate `progressbar` roles and labels.
- **Technical:** Created `mobile/components/ui/Skeleton.js` and `mobile/components/skeletons/GroupListSkeleton.js`. Integrated into `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
3 changes: 2 additions & 1 deletion .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,13 @@
- Impact: Native feel, users can easily refresh data
- Size: ~150 lines

- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- [x] **[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
- Completed: 2026-03-18

- [x] **[a11y]** Complete accessibility labels for all screens
- Completed: 2026-01-29
Expand Down
63 changes: 63 additions & 0 deletions mobile/components/skeletons/GroupListSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import { View, StyleSheet, FlatList } from 'react-native';
import { Card } from 'react-native-paper';
import Skeleton from '../ui/Skeleton';

const GroupListSkeleton = () => {
const renderSkeletonItem = ({ item }) => (
<Card style={styles.card}>
<Card.Title
title={<Skeleton width={120} height={20} borderRadius={4} />}
left={(props) => (
<View style={[props.style, styles.avatarPlaceholder]}>
<Skeleton width={40} height={40} borderRadius={20} />
</View>
)}
/>
<Card.Content>
<Skeleton width={180} height={16} borderRadius={4} style={styles.contentSkeleton} />
Comment on lines +10 to +18
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for Avatar dimensions in HapticCard or related components to verify consistency
rg -n "Avatar\.(Image|Text)" --type=js -A2 -B2 mobile/
# Check Card.Title usage patterns for dimension hints
ast-grep --pattern 'Card.Title
  $$$
'

Repository: Devasy/splitwiser

Length of output: 3573


🏁 Script executed:

# Find HapticCard component definition
fd -type f -name "*.js" | xargs grep -l "HapticCard" | head -5

Repository: Devasy/splitwiser

Length of output: 231


🏁 Script executed:

# Search for Card.Title in HomeScreen to see actual card structure
rg -n "Card\." mobile/screens/HomeScreen.js -A3 -B1

Repository: Devasy/splitwiser

Length of output: 472


🏁 Script executed:

# Find the actual GroupListSkeleton usage and HapticCard definition
fd -type f -name "*Skeleton*" mobile/components/

Repository: Devasy/splitwiser

Length of output: 231


🏁 Script executed:

# Find HapticCard component definition
find mobile -name "*.js" | xargs grep -l "HapticCard" | head -10

Repository: Devasy/splitwiser

Length of output: 161


🏁 Script executed:

# Search for HapticCard exports or definitions
rg -n "export.*HapticCard|const HapticCard|function HapticCard" mobile/

Repository: Devasy/splitwiser

Length of output: 234


🏁 Script executed:

# Check for HapticCard component file directly
find mobile -name "*HapticCard*" -o -name "*Card*"

Repository: Devasy/splitwiser

Length of output: 95


🏁 Script executed:

# Look at the actual dimensions in HomeScreen card rendering
rg -n "const GroupCard\|function GroupCard" mobile/screens/HomeScreen.js -A 50

Repository: Devasy/splitwiser

Length of output: 43


🏁 Script executed:

# Read HapticCard component to understand its structure
cat mobile/components/ui/HapticCard.js

Repository: Devasy/splitwiser

Length of output: 413


🏁 Script executed:

# Read HomeScreen GroupCard function to see actual dimensions
sed -n '180,230p' mobile/screens/HomeScreen.js

Repository: Devasy/splitwiser

Length of output: 1586


🏁 Script executed:

# Read the complete GroupListSkeleton file
cat mobile/components/skeletons/GroupListSkeleton.js

Repository: Devasy/splitwiser

Length of output: 1635


🏁 Script executed:

# Check React Paper version to determine default Avatar size
cat mobile/package.json | grep -A2 -B2 "react-native-paper"

Repository: Devasy/splitwiser

Length of output: 241


🏁 Script executed:

# Search for Avatar default size documentation or any size specifications in HomeScreen
rg -n "Avatar" mobile/screens/HomeScreen.js -B3 -A3

Repository: Devasy/splitwiser

Length of output: 518


🏁 Script executed:

# Check styles.card in HomeScreen to see actual card dimensions/padding
rg -n "styles\." mobile/screens/HomeScreen.js | grep -i "card\|avatar"

Repository: Devasy/splitwiser

Length of output: 92


🏁 Script executed:

# Find the StyleSheet definition in HomeScreen
rg -n "const styles = StyleSheet" mobile/screens/HomeScreen.js -A 100

Repository: Devasy/splitwiser

Length of output: 849


🏁 Script executed:

# Search for React Paper Avatar default size information or any comments about it
rg -n "Avatar" mobile/ --type-list | head -20

Repository: Devasy/splitwiser

Length of output: 450


🏁 Script executed:

# Check if there are any size references in component files or constants
rg -n "size.*=" mobile/components/ | grep -i avatar

Repository: Devasy/splitwiser

Length of output: 43


🏁 Script executed:

# Search React Paper documentation for Avatar default size - check node_modules or look for any size-related comments
find mobile -name "*.js" -exec grep -l "Avatar.*size" {} \; | head -5

Repository: Devasy/splitwiser

Length of output: 198


🏁 Script executed:

# Look at the React Paper v5 Avatar documentation through web search or check if there's any inline documentation
# For now, check if any dev comments mention dimensions or let's see Card.Title default height
rg -n "Card\.Title\|height.*20\|width.*120" mobile/screens/ -i

Repository: Devasy/splitwiser

Length of output: 43


🌐 Web query:

React Native Paper v5 Avatar default size when not specified

💡 Result:

The default size for Avatar components (Avatar.Image, Avatar.Icon, Avatar.Text) in React Native Paper v5, when the size prop is not specified, is 64 pixels.

Citations:


🌐 Web query:

React Native Paper Card.Title default height dimensions

💡 Result:

React Native Paper's Card.Title component does not have a fixed default height. Its height is content-driven and auto-adjusts based on the title text (default variant: bodyLarge, 1 line), optional subtitle (default variant: bodyMedium, 1 line), and optional left/right icons/avatars. Key dimensions from official theme (MD3): - bodyLarge: fontSize 16, lineHeight 24 - bodyMedium: fontSize 14, lineHeight 20 The component uses a flex row layout with padding (likely similar to CardContent's 16 horizontal, vertical varies by position). Without subtitle or icons, height approximates the title's lineHeight (~24dp) plus minimal padding. With subtitle stacked below, height sums both lineHeights plus spacing (~48-56dp total, depending on padding). No explicit height is defined in docs or source snippets; use style={{ height: X }} to set custom height. For precise measurement, render in app and inspect with React Native Debugger.

Citations:


🏁 Script executed:

# Let's check if there are any related issues or TODOs about layout shift or dimensions
rg -n "layout.*shift\|dimension\|size.*match" mobile/ -i

Repository: Devasy/splitwiser

Length of output: 43


🏁 Script executed:

# Check if Avatar has a default size constant anywhere
rg -n "const.*Avatar\|Avatar.*40\|Avatar.*size" mobile/components/ -i

Repository: Devasy/splitwiser

Length of output: 43


Avatar skeleton dimensions incorrect—causing layout shift instead of preventing it.

The Avatar skeleton is 40×40, but HomeScreen's Avatar components use the React Native Paper v5 default size of 64×64 (no explicit size prop is passed). When the skeleton is replaced with real content, this 24px difference causes layout shift. Update the skeleton to use 64×64 for the avatar to match the actual component:

<Skeleton width={40} height={40} borderRadius={20} />

should be:

<Skeleton width={64} height={64} borderRadius={32} />

Also consider extracting these dimensions (64 for avatar, 32 for borderRadius) as shared constants to prevent future mismatches.

📝 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
title={<Skeleton width={120} height={20} borderRadius={4} />}
left={(props) => (
<View style={[props.style, styles.avatarPlaceholder]}>
<Skeleton width={40} height={40} borderRadius={20} />
</View>
)}
/>
<Card.Content>
<Skeleton width={180} height={16} borderRadius={4} style={styles.contentSkeleton} />
title={<Skeleton width={120} height={20} borderRadius={4} />}
left={(props) => (
<View style={[props.style, styles.avatarPlaceholder]}>
<Skeleton width={64} height={64} borderRadius={32} />
</View>
)}
/>
<Card.Content>
<Skeleton width={180} height={16} borderRadius={4} style={styles.contentSkeleton} />
🤖 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 10 - 18,
Update the avatar skeleton in GroupListSkeleton.js so its Skeleton matches the
real Avatar size: change the Skeleton inside the left render (currently width=40
height=40 borderRadius=20) to use 64×64 with borderRadius 32, and update
styles.avatarPlaceholder if it assumes the smaller size; also extract the avatar
size (64) and avatar borderRadius (32) into shared constants (e.g., AVATAR_SIZE,
AVATAR_RADIUS) so both the Skeleton and the real Avatar use the same values to
avoid future mismatches.

</Card.Content>
</Card>
);
Comment on lines +7 to +21
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

Unused item parameter in renderSkeletonItem.

The item parameter is destructured but never used since all skeleton items are identical. Consider using an underscore prefix to indicate it's intentionally unused, or simplify to just use the index.

♻️ Proposed fix
-  const renderSkeletonItem = ({ item }) => (
+  const renderSkeletonItem = ({ item: _item }) => (
     <Card style={styles.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
const renderSkeletonItem = ({ item }) => (
<Card style={styles.card}>
<Card.Title
title={<Skeleton width={120} height={20} borderRadius={4} />}
left={(props) => (
<View style={[props.style, styles.avatarPlaceholder]}>
<Skeleton width={40} height={40} borderRadius={20} />
</View>
)}
/>
<Card.Content>
<Skeleton width={180} height={16} borderRadius={4} style={styles.contentSkeleton} />
</Card.Content>
</Card>
);
const renderSkeletonItem = ({ item: _item }) => (
<Card style={styles.card}>
<Card.Title
title={<Skeleton width={120} height={20} borderRadius={4} />}
left={(props) => (
<View style={[props.style, styles.avatarPlaceholder]}>
<Skeleton width={40} height={40} borderRadius={20} />
</View>
)}
/>
<Card.Content>
<Skeleton width={180} height={16} borderRadius={4} style={styles.contentSkeleton} />
</Card.Content>
</Card>
);
🤖 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 7 - 21, The
renderSkeletonItem function currently destructures an unused item parameter;
update its signature to reflect it's intentionally unused by renaming it to
_item (or replace with a single unused placeholder like _), e.g., change "const
renderSkeletonItem = ({ item }) =>" to "const renderSkeletonItem = ({ _item })
=>" (or "const renderSkeletonItem = (_) =>") so linters won’t flag the unused
variable; keep the body of renderSkeletonItem and Card rendering unchanged.


// Render 5 placeholder items
const skeletonData = Array.from({ length: 5 }).map((_, i) => ({ id: `skeleton-${i}` }));

return (
<View
style={styles.container}
accessible={true}
accessibilityRole="progressbar"
accessibilityLabel="Loading groups"
>
<FlatList
data={skeletonData}
renderItem={renderSkeletonItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
scrollEnabled={false} // Disable scrolling for skeleton list
/>
</View>
);
};

const styles = StyleSheet.create({
container: {
flex: 1,
},
list: {
padding: 16,
},
card: {
marginBottom: 16,
},
avatarPlaceholder: {
justifyContent: 'center',
alignItems: 'center',
},
contentSkeleton: {
marginTop: 4,
},
});

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

The View import from react-native is not used in this component—only Animated.View is rendered.

🧹 Proposed fix
 import React, { useEffect, useRef } from 'react';
-import { Animated, View, StyleSheet } from 'react-native';
+import { Animated, StyleSheet } from 'react-native';
 import { useTheme } from 'react-native-paper';
📝 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 React, { useEffect, useRef } from 'react';
import { Animated, View, StyleSheet } from 'react-native';
import { useTheme } from 'react-native-paper';
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet } from 'react-native';
import { useTheme } from 'react-native-paper';
🤖 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 1 - 3, The import list in the
Skeleton component includes an unused symbol `View`; remove `View` from the
react-native import (adjust the line importing `Animated` and `StyleSheet`) so
only `Animated` and `StyleSheet` are imported, and verify the `Skeleton`
component (and any helpers) do not reference `View` before committing.


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

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

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

return (
<Animated.View
style={[
styles.skeleton,
{
width,
height,
borderRadius,
backgroundColor: theme.colors.surfaceVariant,
opacity: animatedValue,
},
style,
]}
/>
);
};

const styles = StyleSheet.create({
skeleton: {
overflow: 'hidden',
},
});

export default Skeleton;
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, 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
Loading