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 @@ -7,6 +7,13 @@
## [Unreleased]

### Added
- **Mobile Skeleton Loading:** Implemented skeleton loading state for the Home screen group list.
- **Features:**
- Created reusable `Skeleton` component with pulsing opacity animation.
- Created `GroupCardSkeleton` mimicking the exact layout of group cards.
- Replaced the full-screen spinner with a list of skeletons to reduce perceived latency.
- Wrapped in accessible container (`accessibilityLabel="Loading groups"`).
- **Technical:** Uses `Animated.loop` for performance and `useTheme` for dark mode support.
- **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
26 changes: 26 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,32 @@ Commonly used components:
- `<Portal>` and `<Modal>` for overlays
- `<ActivityIndicator>` for loading states

### Mobile Skeleton Loading Pattern

**Date:** 2026-02-14
**Context:** Creating loading states for mobile lists

To create smooth skeleton loaders in React Native without external libraries:

```javascript
// 1. Create base Skeleton component
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();
}, []);
return <Animated.View style={{ opacity, backgroundColor: theme.colors.surfaceVariant }} />;
```

**Key Implementation Detail:**
- Use `Animated.loop` with `Animated.sequence` for the pulse effect.
- Use `react-native-paper`'s `useTheme` to ensure the skeleton color works in both light and dark modes (`surfaceVariant` is a good choice).
- Compose these primitives into `CardSkeleton` components that match the structure of the real content card.

### Safe Area Pattern

**Date:** 2026-01-01
Expand Down
11 changes: 6 additions & 5 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
- Impact: Better loading experience, less jarring
- Size: ~40 lines
- [x] **[ux]** Complete skeleton loading for HomeScreen groups
- Completed: 2026-02-14
- Files: `mobile/screens/HomeScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupCardSkeleton.js`
- Context: Replaced ActivityIndicator with pulsing skeleton cards
- Impact: Professional loading state, prevents layout shift
- Size: ~70 lines
- Added: 2026-01-01

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

const GroupCardSkeleton = () => {
return (
<Card style={{ marginBottom: 16 }}>
<Card.Title
title={<Skeleton width={120} height={20} />}
left={(props) => <Skeleton width={40} height={40} borderRadius={20} />}
/>
Comment on lines +8 to +11
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:

# Find and check mobile/package.json for react-native-paper version
find . -name "package.json" -path "*/mobile/*" -type f | head -5

Repository: Devasy/splitwiser

Length of output: 82


🏁 Script executed:

# Check the full GroupCardSkeleton.js file
find . -name "GroupCardSkeleton.js" -type f

Repository: Devasy/splitwiser

Length of output: 111


🌐 Web query:

react-native-paper Card.Title title prop accept React elements component rendering

💡 Result:

In react-native-paper, Card.Title’s title prop is typed as React.ReactNode, but it’s effectively limited to text content: it “will only accept a string or <Text>-based node.” That means you can pass "My title" or <Text>My title</Text>, but you can’t pass arbitrary components that render a <View> (e.g., an icon wrapped in a View) because the title is rendered inside a Text component, which triggers the common RN error about nesting <View> inside <Text>. [1], [2]

If you need fully custom rendering (icon + text layout, multiple components, etc.), use left / right slots (they’re intended to return React elements), or skip Card.Title and build your own header using Card.Content + your own layout. [1]

Sources: [1] [2]


🏁 Script executed:

# Check react-native-paper version
cat ./mobile/package.json | grep -A2 -B2 "react-native-paper"

Repository: Devasy/splitwiser

Length of output: 241


🏁 Script executed:

# Read the full GroupCardSkeleton.js file
cat -n ./mobile/components/skeletons/GroupCardSkeleton.js

Repository: Devasy/splitwiser

Length of output: 716


Remove <Skeleton> from Card.Title title prop — this will cause a runtime error.

react-native-paper's Card.Title title prop cannot render arbitrary View-based components like Skeleton (an Animated.View). The title is internally rendered inside a Text component, which triggers the React Native error: "Cannot add a child that is not a Text to a Text." Use left or right slots instead for custom elements.

Additionally, the left callback ignores props.size and hardcodes 40. Use props.size to ensure the skeleton adapts if the Card.Title size changes:

Suggested fix
      <Card.Title
-       title={<Skeleton width={120} height={20} />}
        left={(props) => <Skeleton width={props.size} height={props.size} borderRadius={props.size / 2} />}
      />

If you need a skeleton title, render it separately in Card.Content instead.

🤖 Prompt for AI Agents
In `@mobile/components/skeletons/GroupCardSkeleton.js` around lines 8 - 11,
Card.Title is being passed a Skeleton into the title prop which causes "Cannot
add a child that is not a Text to a Text" at runtime; remove the Skeleton from
the title prop and instead render a Skeleton placeholder inside Card.Content or
use the left/right slots for non-Text elements. Also update the left slot
callback (the left prop) to use the incoming props.size instead of the hardcoded
40 so the avatar skeleton scales correctly (adjust the Skeleton borderRadius to
props.size / 2). Ensure Card.Title only receives strings or Text-compatible
content for title and move any View/Animated.View skeletons out of the title
prop.

<Card.Content>
<Skeleton width={200} height={16} style={{ marginTop: 4 }} />
</Card.Content>
</Card>
);
};

export default GroupCardSkeleton;
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 } from 'react-native';
import { useTheme } from 'react-native-paper';

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

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

return () => pulse.stop();
}, [opacity]);

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

export default Skeleton;
13 changes: 5 additions & 8 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 @@ -17,6 +16,7 @@ import * as Haptics from "expo-haptics";
import { createGroup, getGroups, getOptimizedSettlements } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
import { formatCurrency, getCurrencySymbol } from "../utils/currency";
import GroupCardSkeleton from '../components/skeletons/GroupCardSkeleton';

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

{isLoading ? (
<View style={styles.loaderContainer}>
<ActivityIndicator size="large" />
<View style={styles.list} accessible={true} accessibilityLabel="Loading groups">
{[1, 2, 3, 4, 5, 6].map((i) => (
<GroupCardSkeleton key={i} />
))}
</View>
) : (
<FlatList
Expand Down Expand Up @@ -289,11 +291,6 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
loaderContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
list: {
padding: 16,
},
Expand Down
Loading