Skip to content

Commit 73fdb90

Browse files
author
Quavon-dev
committed
feat(monitors): pull-to-refresh revalidates the active Kuma connection
Pulling down on the Monitors tab now disconnects + reconnects the Kuma socket and re-fetches the monitor list and heartbeats. Implementation - Wires the existing useKumaConnection() hook to a RefreshControl on SafeScrollView (the wrapper already passes through ScrollView props). - handleRefresh() calls manager.revalidateActiveServer() — the same method the background-fetch task uses — so the user-visible behavior is identical: disconnect, reconnect, refetch monitor list and heartbeats. - Holds the spinner for a minimum 400 ms so it doesn't flash imperceptibly fast on a LAN-connection Kuma instance. - Re-entrancy guard: if a refresh is already in flight, subsequent pull gestures are dropped instead of stacking. - Errors are already surfaced by the existing connection-status banner; the refresh handler swallows them silently. iOS hint label - RefreshControl's 'title' prop surfaces 'Pull to refresh' / 'Refreshing…' under the spinner on iOS. Localized in all 5 locales. i18n - New keys: refresh.{title,refreshing} in en/de/fr/ja/es. - Parity test still passes. Quality gates - npm run typecheck: 0 errors - npm run lint: 0 errors (1 pre-existing warning, not from me) - npm test: 283/283 passing
1 parent 79e9f71 commit 73fdb90

6 files changed

Lines changed: 67 additions & 3 deletions

File tree

app/(tabs)/index.tsx

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
* brand tints. Banner uses status-tinted bg/border.
1919
*/
2020

21-
import { useState, useMemo } from 'react';
22-
import { View, Text, ScrollView, Pressable, StyleSheet, TextInput } from 'react-native';
21+
import { useState, useMemo, useCallback } from 'react';
22+
import { View, Text, ScrollView, Pressable, StyleSheet, TextInput, RefreshControl } from 'react-native';
2323
import { useRouter } from 'expo-router';
2424
import { useSafeAreaInsets } from 'react-native-safe-area-context';
2525
import { ChevronDown, Server, WifiOff, Loader, Plus, Search, X } from 'lucide-react-native';
@@ -39,6 +39,7 @@ import {
3939
} from '@/features/monitors/tagFilter';
4040
import { useServers, getActiveServer } from '@/data/store/servers';
4141
import { useMonitors, selectMonitorsForServer } from '@/data/store/monitors';
42+
import { useKumaConnection } from '@/data/connection/manager';
4243
import { colors, spacing, typography, semanticRadius, useAppTheme } from '@/theme';
4344
import { t, tn } from '@/i18n';
4445

@@ -57,6 +58,32 @@ export default function MonitorsScreen() {
5758
// and hostname. Empty string = no search.
5859
const [search, setSearch] = useState('');
5960
const { status: notifyStatus, setStatus: setNotifyStatus } = useNotificationOptIn();
61+
// The connection manager is the same instance the rest of the app
62+
// uses (root layout, background fetch). We use it here for the
63+
// pull-to-refresh handler: revalidateActiveServer() disconnects,
64+
// re-fetches the monitor list + heartbeats, and re-establishes
65+
// the socket. The connection-status banner updates from this same
66+
// call, so the user sees the spinner spin.
67+
const manager = useKumaConnection();
68+
const [refreshing, setRefreshing] = useState(false);
69+
const handleRefresh = useCallback(async () => {
70+
if (refreshing) return;
71+
setRefreshing(true);
72+
try {
73+
// The promise resolves when revalidateActiveServer() returns.
74+
// That happens after the new socket is up; the monitor list
75+
// and heartbeats come in over the socket within a few hundred
76+
// ms after that. We hold the spinner for a minimum 400 ms so
77+
// it doesn't flash too fast to perceive on a fast network.
78+
await manager.revalidateActiveServer();
79+
await new Promise((r) => setTimeout(r, 400));
80+
} catch {
81+
// Error is already reflected in the connection-status banner;
82+
// nothing to do here.
83+
} finally {
84+
setRefreshing(false);
85+
}
86+
}, [manager, refreshing]);
6087

6188
// Live data from the active server.
6289
const active = getActiveServer(servers, activeId);
@@ -165,7 +192,24 @@ export default function MonitorsScreen() {
165192

166193
<SafeScrollView
167194
contentContainerStyle={{ paddingHorizontal: spacing[4] }}
168-
showsVerticalScrollIndicator={false}>
195+
showsVerticalScrollIndicator={false}
196+
// Pull-to-refresh revalidates the active Kuma connection:
197+
// disconnect, reconnect, refetch monitor list + heartbeats.
198+
// The spinner is brand-tinted so it matches the nav bar.
199+
refreshControl={
200+
<RefreshControl
201+
refreshing={refreshing}
202+
onRefresh={handleRefresh}
203+
tintColor={brand}
204+
colors={[brand]}
205+
progressViewOffset={0}
206+
// iOS shows a small hint label under the spinner. We
207+
// surface a localized "Pull to refresh" so the gesture
208+
// is discoverable, and "Refreshing…" while in flight.
209+
title={refreshing ? t('refresh.refreshing') : t('refresh.title')}
210+
titleColor={surface.textMuted}
211+
/>
212+
}>
169213
{/* Connection status banner */}
170214
{(status === 'connecting' || status === 'reconnecting' || status === 'error') && (
171215
<View style={[styles.banner, bannerStyle(status, statusTints)]}>

src/i18n/de.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
"down": "Offline",
8484
"search": "Monitore suchen…"
8585
},
86+
"refresh": {
87+
"title": "Ziehen zum Aktualisieren",
88+
"refreshing": "Wird aktualisiert…"
89+
},
8690
"serverCard": {
8791
"active": "Aktiv",
8892
"monitor": "Monitor",

src/i18n/en.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
"down": "Down",
8484
"search": "Search monitors…"
8585
},
86+
"refresh": {
87+
"title": "Pull to refresh",
88+
"refreshing": "Refreshing…"
89+
},
8690
"serverCard": {
8791
"active": "Active",
8892
"monitor": "monitor",

src/i18n/es.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
"down": "Caído",
8484
"search": "Buscar monitores…"
8585
},
86+
"refresh": {
87+
"title": "Tira para actualizar",
88+
"refreshing": "Actualizando…"
89+
},
8690
"serverCard": {
8791
"active": "Activo",
8892
"monitor": "monitor",

src/i18n/fr.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
"down": "Hors ligne",
8484
"search": "Rechercher des moniteurs…"
8585
},
86+
"refresh": {
87+
"title": "Tirer pour actualiser",
88+
"refreshing": "Actualisation…"
89+
},
8690
"serverCard": {
8791
"active": "Actif",
8892
"monitor": "moniteur",

src/i18n/ja.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
"down": "停止",
8484
"search": "モニターを検索…"
8585
},
86+
"refresh": {
87+
"title": "引いて更新",
88+
"refreshing": "更新中…"
89+
},
8690
"serverCard": {
8791
"active": "アクティブ",
8892
"monitor": "モニター",

0 commit comments

Comments
 (0)