Skip to content

Commit eb60dbf

Browse files
committed
feat(mobile): pull-to-refresh on Profile, Saved, My submissions, Mosque detail
New usePullToRefresh hook invalidates the TanStack Query cache when the user drags a ScrollView past its edge, which triggers every active query on the screen to refetch. Applied to the four screens where stale data is most visible — stats, saved list, submission list, and a mosque's details/reviews/events. Green tint matches the app palette.
1 parent 4eda1c4 commit eb60dbf

5 files changed

Lines changed: 88 additions & 4 deletions

File tree

apps/mobile/features/mosques/components/mosque-detail-content.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useState } from "react";
2-
import { ScrollView, View } from "react-native";
2+
import { RefreshControl, ScrollView, View } from "react-native";
33
import { Text } from "@/components/ui/text";
44
import { PrayerTimesTab } from "@/features/prayer-times";
5+
import { usePullToRefresh } from "@/lib/use-pull-to-refresh";
56
import type { MosqueDetail } from "../lib/types";
67
import { MosqueDetailTabs, type MosqueTab } from "./mosque-detail-tabs";
78
import { MosqueEventsTab } from "./mosque-events-tab";
@@ -12,12 +13,21 @@ import { MosqueReviewsTab } from "./mosque-reviews-tab";
1213
export function MosqueDetailContent({ data }: { data: MosqueDetail }) {
1314
const [tab, setTab] = useState<MosqueTab>("overview");
1415
const { mosque, imam, events, reviews } = data;
16+
const { refreshing, onRefresh } = usePullToRefresh();
1517

1618
return (
1719
<ScrollView
1820
className="flex-1"
1921
contentContainerStyle={{ paddingBottom: 120 }}
2022
showsVerticalScrollIndicator={false}
23+
refreshControl={
24+
<RefreshControl
25+
refreshing={refreshing}
26+
onRefresh={onRefresh}
27+
tintColor="#2e5d45"
28+
colors={["#2e5d45"]}
29+
/>
30+
}
2131
>
2232
<View className="px-s-6 pt-s-2">
2333
<Text variant="display-lg">{mosque.name}</Text>

apps/mobile/features/mosques/components/saved-mosques-screen.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { StatusBar } from "expo-status-bar";
2-
import { ScrollView, View } from "react-native";
2+
import { RefreshControl, ScrollView, View } from "react-native";
33
import { Text } from "@/components/ui/text";
4+
import { usePullToRefresh } from "@/lib/use-pull-to-refresh";
45
import { useSavedMosques } from "../hooks/use-mosques";
56
import { SavedMosqueCard } from "./saved-mosque-card";
67
import { SavedMosquesEmpty } from "./saved-mosques-empty";
@@ -19,6 +20,7 @@ export function SavedMosquesScreen() {
1920
const { data, isLoading } = useSavedMosques();
2021
const rows = data?.data ?? [];
2122
const pairs = chunkPairs(rows);
23+
const { refreshing, onRefresh } = usePullToRefresh();
2224

2325
return (
2426
<View className="flex-1 bg-cream">
@@ -46,6 +48,14 @@ export function SavedMosquesScreen() {
4648
paddingBottom: 32,
4749
}}
4850
showsVerticalScrollIndicator={false}
51+
refreshControl={
52+
<RefreshControl
53+
refreshing={refreshing}
54+
onRefresh={onRefresh}
55+
tintColor="#2e5d45"
56+
colors={["#2e5d45"]}
57+
/>
58+
}
4959
>
5060
<View className="gap-s-3">
5161
{pairs.map(([left, right]) => (

apps/mobile/features/profile/components/profile-screen.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import * as Linking from "expo-linking";
22
import { router } from "expo-router";
33
import { StatusBar } from "expo-status-bar";
4-
import { ScrollView, Share, View } from "react-native";
4+
import { RefreshControl, ScrollView, Share, View } from "react-native";
55
import { useHydrateSettings, useSettingsStore } from "@/features/settings";
66
import { useSession } from "@/lib/auth";
7+
import { usePullToRefresh } from "@/lib/use-pull-to-refresh";
78
import { useProfileStats } from "../hooks/use-profile-stats";
89
import { ProfileHeader } from "./profile-header";
910
import { ProfileHeroCard } from "./profile-hero-card";
@@ -20,6 +21,7 @@ export function ProfileScreen() {
2021

2122
const name = session?.user?.name ?? "Guest";
2223
const email = session?.user?.email ?? "";
24+
const { refreshing, onRefresh } = usePullToRefresh();
2325

2426
return (
2527
<View className="flex-1 bg-cream">
@@ -33,6 +35,14 @@ export function ProfileScreen() {
3335
paddingBottom: 48,
3436
}}
3537
showsVerticalScrollIndicator={false}
38+
refreshControl={
39+
<RefreshControl
40+
refreshing={refreshing}
41+
onRefresh={onRefresh}
42+
tintColor="#2e5d45"
43+
colors={["#2e5d45"]}
44+
/>
45+
}
3646
>
3747
<ProfileHeroCard name={name} email={email} />
3848

apps/mobile/features/submissions/components/my-submissions-screen.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { router } from "expo-router";
22
import { StatusBar } from "expo-status-bar";
3-
import { Pressable, ScrollView, View } from "react-native";
3+
import { Pressable, RefreshControl, ScrollView, View } from "react-native";
44
import { SafeAreaView } from "react-native-safe-area-context";
55
import { Button } from "@/components/ui/button";
66
import { Icon } from "@/components/ui/icon";
77
import { IconButton } from "@/components/ui/icon-button";
88
import { Skeleton } from "@/components/ui/skeleton";
99
import { Text } from "@/components/ui/text";
10+
import { usePullToRefresh } from "@/lib/use-pull-to-refresh";
1011
import { useMySubmissions } from "../hooks/use-submissions";
1112

1213
type SubmissionRow = NonNullable<
@@ -16,6 +17,7 @@ type SubmissionRow = NonNullable<
1617
export function MySubmissionsScreen() {
1718
const { data, isLoading } = useMySubmissions();
1819
const submissions = data?.data ?? [];
20+
const { refreshing, onRefresh } = usePullToRefresh();
1921

2022
return (
2123
<View className="flex-1 bg-cream">
@@ -41,6 +43,14 @@ export function MySubmissionsScreen() {
4143
paddingBottom: 48,
4244
}}
4345
showsVerticalScrollIndicator={false}
46+
refreshControl={
47+
<RefreshControl
48+
refreshing={refreshing}
49+
onRefresh={onRefresh}
50+
tintColor="#2e5d45"
51+
colors={["#2e5d45"]}
52+
/>
53+
}
4454
>
4555
{isLoading ? (
4656
<View className="gap-s-3">
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useQueryClient } from "@tanstack/react-query";
2+
import { useCallback, useState } from "react";
3+
4+
/**
5+
* Hook to wire up pull-to-refresh on a ScrollView or FlatList.
6+
*
7+
* Pass the query keys you want invalidated on pull. Pass an empty array
8+
* (or no argument) to invalidate every TanStack Query in the cache — cheap
9+
* and simple for screens that show data from several disparate sources.
10+
*
11+
* Usage:
12+
* const { refreshing, onRefresh } = usePullToRefresh([
13+
* ["mosques", "saved"],
14+
* ["my-submissions"],
15+
* ]);
16+
* <ScrollView
17+
* refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
18+
* >
19+
*/
20+
export function usePullToRefresh(
21+
queryKeys: ReadonlyArray<readonly unknown[]> = [],
22+
) {
23+
const qc = useQueryClient();
24+
const [refreshing, setRefreshing] = useState(false);
25+
26+
const onRefresh = useCallback(async () => {
27+
setRefreshing(true);
28+
try {
29+
if (queryKeys.length === 0) {
30+
await qc.invalidateQueries();
31+
} else {
32+
await Promise.all(
33+
queryKeys.map((key) => qc.invalidateQueries({ queryKey: key })),
34+
);
35+
}
36+
} finally {
37+
setRefreshing(false);
38+
}
39+
// queryKeys is assumed stable — pass a module-level constant from the
40+
// caller to avoid recreating the callback every render.
41+
}, [qc, queryKeys]);
42+
43+
return { refreshing, onRefresh };
44+
}

0 commit comments

Comments
 (0)