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
5 changes: 5 additions & 0 deletions .Jules/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,8 @@
- `.jules/todo.md`
- `.jules/knowledge.md`
- `.jules/changelog.md`

### 2026-03-08
- Added generic `Skeleton` component and `GroupListSkeleton` component to mobile app
- Replaced basic ActivityIndicator loading screen with `GroupListSkeleton` on mobile `HomeScreen`
- Refactored `FriendsScreen` inline skeleton logic to use the new generic `Skeleton` component
Comment on lines +127 to +130
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

Make this a standalone dated changelog section.

This March 8 entry is currently nested under ## [2026-01-01] - Initial Setup, which makes the timeline misleading and also triggers the heading-spacing lint. It should be promoted to its own top-level dated section, with a blank line after the heading.

Suggested fix
-### 2026-03-08
+## [2026-03-08] - Mobile skeleton loading
+
 - Added generic `Skeleton` component and `GroupListSkeleton` component to mobile app
 - Replaced basic ActivityIndicator loading screen with `GroupListSkeleton` on mobile `HomeScreen`
 - Refactored `FriendsScreen` inline skeleton logic to use the new generic `Skeleton` component
📝 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
### 2026-03-08
- Added generic `Skeleton` component and `GroupListSkeleton` component to mobile app
- Replaced basic ActivityIndicator loading screen with `GroupListSkeleton` on mobile `HomeScreen`
- Refactored `FriendsScreen` inline skeleton logic to use the new generic `Skeleton` component
## [2026-03-08] - Mobile skeleton loading
- Added generic `Skeleton` component and `GroupListSkeleton` component to mobile app
- Replaced basic ActivityIndicator loading screen with `GroupListSkeleton` on mobile `HomeScreen`
- Refactored `FriendsScreen` inline skeleton logic to use the new generic `Skeleton` component
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 127-127: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.Jules/changelog.md around lines 127 - 130, The "### 2026-03-08" entry is
nested under the earlier "## [2026-01-01] - Initial Setup" section and lacks
proper spacing; change the heading "### 2026-03-08" to a top-level dated section
(use "## 2026-03-08" or the same format as other top-level dated headings) and
ensure there is a blank line immediately after that heading, and move the three
bullet lines so they are outside and below the initial setup section to form a
standalone dated changelog entry.

5 changes: 5 additions & 0 deletions .Jules/knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -756,3 +756,8 @@ _Document errors and their solutions here as you encounter them._
- react-native-paper: UI components
- axios: API calls (via api/client.js)
- expo: Platform SDK

### Development Workflows
- The mobile project is configured with `react-native-web`. You can test it in a browser using `npx expo start --web`.
- Use `AsyncStorage` values (`auth_token`, `refresh_token`, `user_data`) directly into Playwright's `window.localStorage` to bypass login flows for Playwright testing via react-native-web.
- When creating files in new subdirectories inside `/mobile/`, use `mkdir -p` before the write operation to ensure the path exists.
Comment on lines +760 to +763
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 a blank line below this heading.

markdownlint is right here: the new ### Development Workflows heading is immediately followed by a list, so this block currently violates MD022.

Suggested fix
 ### Development Workflows
+
 - The mobile project is configured with `react-native-web`. You can test it in a browser using `npx expo start --web`.
 - Use `AsyncStorage` values (`auth_token`, `refresh_token`, `user_data`) directly into Playwright's `window.localStorage` to bypass login flows for Playwright testing via react-native-web.
 - When creating files in new subdirectories inside `/mobile/`, use `mkdir -p` before the write operation to ensure the path exists.
📝 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
### Development Workflows
- The mobile project is configured with `react-native-web`. You can test it in a browser using `npx expo start --web`.
- Use `AsyncStorage` values (`auth_token`, `refresh_token`, `user_data`) directly into Playwright's `window.localStorage` to bypass login flows for Playwright testing via react-native-web.
- When creating files in new subdirectories inside `/mobile/`, use `mkdir -p` before the write operation to ensure the path exists.
### Development Workflows
- The mobile project is configured with `react-native-web`. You can test it in a browser using `npx expo start --web`.
- Use `AsyncStorage` values (`auth_token`, `refresh_token`, `user_data`) directly into Playwright's `window.localStorage` to bypass login flows for Playwright testing via react-native-web.
- When creating files in new subdirectories inside `/mobile/`, use `mkdir -p` before the write operation to ensure the path exists.
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 760-760: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.Jules/knowledge.md around lines 760 - 763, The Markdown heading "###
Development Workflows" currently has no blank line before the list which
triggers markdownlint MD022; insert a single blank line immediately after the
"### Development Workflows" heading so the subsequent bulleted list is separated
from the heading (update the block around the heading in .Jules/knowledge.md
where "### Development Workflows" appears).

9 changes: 7 additions & 2 deletions .Jules/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@
- Impact: Native feel, users can easily refresh data
- Size: ~150 lines

- [ ] **[ux]** Complete skeleton loading for HomeScreen groups
- File: `mobile/screens/HomeScreen.js`
- [x] **[ux]** Complete skeleton loading for HomeScreen groups
- Completed: 2026-03-08
- File: `mobile/screens/HomeScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js`
- Context: Replace ActivityIndicator with skeleton group cards
- Impact: Better loading experience, less jarring
- Size: ~40 lines
Expand Down Expand Up @@ -168,3 +169,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-03-08
- Files modified: `mobile/screens/HomeScreen.js`, `mobile/screens/FriendsScreen.js`, `mobile/components/ui/Skeleton.js`, `mobile/components/skeletons/GroupListSkeleton.js`
- Impact: Better loading experience across the mobile app via generic skeleton components.
44 changes: 44 additions & 0 deletions mobile/components/skeletons/GroupListSkeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Card } from 'react-native-paper';
import Skeleton from '../ui/Skeleton';

const GroupListSkeleton = ({ count = 5 }) => {
return (
<View
style={styles.container}
accessibilityRole="progressbar"
accessibilityLabel="Loading groups list"
>
{Array.from({ length: count }).map((_, index) => (
<Card key={index} style={styles.card}>
<Card.Title
title={<Skeleton width={120} height={20} borderRadius={4} />}
Comment on lines +15 to +16
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

🌐 Web query:

In react-native-paper 5.14.5, how does Card.Titlerender thetitle prop, and when supplying a custom component instead of text should it be passed as a render function?

💡 Result:

In react-native-paper@5.14.5, Card.Title renders title as children of a text component:

  • It picks TitleComponent = theme.isV3 ? Text : Title, then renders:
    • <TitleComponent ...>{title}</TitleComponent> (github.com)

Because title is placed inside Text/Title, it will only work with a string or <Text>-based node (anything that ultimately renders as React Native <Text>). Passing a component that renders a <View> (or contains one) will fail with the usual “<View> within <Text>” nesting error. (callstack.github.io)

For a “custom component” title:

  • Do not pass a render functiontitle is not a render-prop; it’s a React.ReactNode rendered directly as children. (callstack.github.io)
  • If you want custom layout (icon + text, multiple views, etc.), use left/right (which are render callbacks) or build your own header row instead of relying on title. (callstack.github.io)

Citations:


Remove Skeleton from title; use left/right render props or build a custom header row instead.

Skeleton renders an Animated.View, but Card.Title's title prop is rendered directly as children of a Text component. Passing a view component here violates React Native's text nesting rules and causes a runtime error. The title prop does not accept render functions.

Use the left or right render callbacks (which accept functions) to place the Skeleton component, or build a custom header row outside of Card.Title if you need finer control over layout.

🤖 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 15 - 16,
Remove the Skeleton from the Card.Title title prop (currently using <Card.Title
title={<Skeleton ... />}>), because title is rendered inside a Text and cannot
contain a View; instead move the Skeleton into Card.Title's left or right render
prop (use Card.Title's left={props => <Skeleton ... />} or right={...}) or
replace Card.Title with a custom header row component outside of Card.Title that
composes a Text for the title and a Skeleton View for the placeholder; update
the GroupListSkeleton component to render the Skeleton via the left/right
callback or a custom header so no Animated/View is passed into the title prop.

left={(props) => (
<Skeleton
width={props.size || 40}
height={props.size || 40}
borderRadius={(props.size || 40) / 2}
style={{ marginLeft: -8 }}
/>
)}
/>
<Card.Content>
<Skeleton width={180} height={16} borderRadius={4} style={{ marginTop: 4 }} />
</Card.Content>
</Card>
))}
</View>
);
};

const styles = StyleSheet.create({
container: {
padding: 16,
},
card: {
marginBottom: 16,
},
});

export default GroupListSkeleton;
51 changes: 51 additions & 0 deletions mobile/components/ui/Skeleton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet, View } from 'react-native';
import { useTheme } from 'react-native-paper';

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

useEffect(() => {
const loop = Animated.loop(
Animated.sequence([
Animated.timing(opacityAnim, {
toValue: 1,
duration: 700,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 0.3,
duration: 700,
useNativeDriver: true,
}),
])
);
loop.start();
return () => loop.stop();
}, [opacityAnim]);

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

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

export default Skeleton;
58 changes: 6 additions & 52 deletions mobile/screens/FriendsScreen.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useIsFocused } from "@react-navigation/native";
import { useContext, useEffect, useRef, useState } from "react";
import { Alert, Animated, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import { useContext, useEffect, useState } from "react";
import { Alert, FlatList, RefreshControl, StyleSheet, View } from "react-native";
import {
Appbar,
Avatar,
Expand All @@ -15,6 +15,7 @@ import { triggerPullRefreshHaptic } from '../components/ui/hapticUtils';
import { getFriendsBalance, getGroups } from "../api/groups";
import { AuthContext } from "../context/AuthContext";
import { formatCurrency } from "../utils/currency";
import Skeleton from "../components/ui/Skeleton";

const FriendsScreen = () => {
const { token, user } = useContext(AuthContext);
Expand Down Expand Up @@ -167,42 +168,12 @@ const FriendsScreen = () => {
);
};

// Shimmer skeleton components
const opacityAnim = useRef(new Animated.Value(0.3)).current;
useEffect(() => {
const loop = Animated.loop(
Animated.sequence([
Animated.timing(opacityAnim, {
toValue: 1,
duration: 700,
useNativeDriver: true,
}),
Animated.timing(opacityAnim, {
toValue: 0.3,
duration: 700,
useNativeDriver: true,
}),
])
);
loop.start();
return () => loop.stop();
}, [opacityAnim]);

const SkeletonRow = () => (
<View style={styles.skeletonRow}>
<Animated.View
style={[styles.skeletonAvatar, { opacity: opacityAnim }]}
/>
<Skeleton width={48} height={48} borderRadius={24} />
<View style={{ flex: 1, marginLeft: 12 }}>
<Animated.View
style={[styles.skeletonLine, { width: "60%", opacity: opacityAnim }]}
/>
<Animated.View
style={[
styles.skeletonLineSmall,
{ width: "40%", opacity: opacityAnim },
]}
/>
<Skeleton width="60%" height={14} borderRadius={6} style={{ marginBottom: 6 }} />
<Skeleton width="40%" height={12} borderRadius={6} />
</View>
</View>
);
Expand Down Expand Up @@ -315,23 +286,6 @@ const styles = StyleSheet.create({
alignItems: "center",
marginBottom: 14,
},
skeletonAvatar: {
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: "#e0e0e0",
},
skeletonLine: {
height: 14,
backgroundColor: "#e0e0e0",
borderRadius: 6,
marginBottom: 6,
},
skeletonLineSmall: {
height: 12,
backgroundColor: "#e0e0e0",
borderRadius: 6,
},
});

export default FriendsScreen;
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 @@ -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 GroupListSkeleton from "../components/skeletons/GroupListSkeleton";

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

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