From 8b02cfcb65798512ec3e6f1e5529d6842c5b8e6c Mon Sep 17 00:00:00 2001 From: Gauvain Date: Wed, 15 Jul 2026 18:44:09 +0200 Subject: [PATCH] fix(logs): pin the filter bar on iOS, scroll/expand/copy log entries on Android --- .../(tabs)/(home)/settings/logs/page.tsx | 153 +++++++++++------- bun.lock | 3 + package.json | 1 + translations/en.json | 4 +- 4 files changed, 103 insertions(+), 58 deletions(-) diff --git a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx index 4ce5736cd..ce74d7508 100644 --- a/app/(auth)/(tabs)/(home)/settings/logs/page.tsx +++ b/app/(auth)/(tabs)/(home)/settings/logs/page.tsx @@ -1,4 +1,6 @@ +import { Ionicons } from "@expo/vector-icons"; import { File, Paths } from "expo-file-system"; +import { requireOptionalNativeModule } from "expo-modules-core"; import { useNavigation } from "expo-router"; import type * as SharingType from "expo-sharing"; import { useCallback, useEffect, useId, useMemo, useState } from "react"; @@ -6,6 +8,7 @@ import { useTranslation } from "react-i18next"; import { Platform, ScrollView, TouchableOpacity, View } from "react-native"; import Collapsible from "react-native-collapsible"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { toast } from "sonner-native"; import { Text } from "@/components/common/Text"; import { FilterButton } from "@/components/filters/FilterButton"; import { Loader } from "@/components/Loader"; @@ -72,6 +75,25 @@ export default function Page() { } }, [filteredLogs, Sharing]); + const copyLog = useCallback( + async (log: NonNullable[number]) => { + // Skip on builds that don't ship the expo-clipboard native module + // (probe returns null instead of throwing); same guard as Quick Connect. + if (!requireOptionalNativeModule("ExpoClipboard")) return; + const Clipboard = await import("expo-clipboard"); + const text = [ + `[${log.level}] ${new Date(log.timestamp).toLocaleString()}`, + log.message, + log.data ? JSON.stringify(log.data, null, 2) : null, + ] + .filter(Boolean) + .join("\n"); + await Clipboard.setStringAsync(text); + toast.success(t("home.settings.logs.copied")); + }, + [logs, t], + ); + useEffect(() => { if (Platform.isTV) return; @@ -88,8 +110,15 @@ export default function Page() { }, [share, loading]); return ( - - + + - - - {filteredLogs?.map((log, index) => ( - - - setState((v) => ({ - ...v, - [log.timestamp]: !v[log.timestamp], - })) - } - > - - + {filteredLogs?.map((log, index) => ( + + + setState((v) => ({ + ...v, + [log.timestamp]: !v[log.timestamp], + })) + } + > + + - {log.level} - - - - {new Date(log.timestamp).toLocaleString()} - - - - {log.message} + > + {log.level} - - {log.data && ( - <> - {!state[log.timestamp] && ( - - {t("home.settings.logs.click_for_more_info")} - - )} - - - - {JSON.stringify(log.data, null, 2)} - - - - + + {new Date(log.timestamp).toLocaleString()} + + + {log.message} + {/* Keep the whole collapsed row tappable: the hint lives inside + the toggle so tapping it expands too. */} + {log.data && !state[log.timestamp] && ( + + {t("home.settings.logs.click_for_more_info")} + )} - - ))} - {filteredLogs?.length === 0 && ( - - {t("home.settings.logs.no_logs_available")} - - )} - - - + + + {log.data && ( + + + + {/* Only the raw payload is selectable (per request); the + header/message stay tap-to-toggle. */} + {JSON.stringify(log.data, null, 2)} + + {!Platform.isTV && ( + copyLog(log)} + className='flex flex-row items-center self-end px-2 py-1' + > + + + {t("home.settings.logs.copy")} + + + )} + + + )} + + ))} + {filteredLogs?.length === 0 && ( + + {t("home.settings.logs.no_logs_available")} + + )} + + ); } diff --git a/bun.lock b/bun.lock index 03789f894..11f89b379 100644 --- a/bun.lock +++ b/bun.lock @@ -31,6 +31,7 @@ "expo-brightness": "~56.0.5", "expo-build-properties": "~56.0.18", "expo-camera": "~56.0.8", + "expo-clipboard": "~56.0.4", "expo-constants": "~56.0.18", "expo-crypto": "~56.0.4", "expo-dev-client": "~56.0.20", @@ -1001,6 +1002,8 @@ "expo-camera": ["expo-camera@56.0.8", "", { "dependencies": { "barcode-detector": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-UDOpUUMisFRmCv1XQV1MJCKGAH2CsIC1Rs6P9Bbc6JLVmbxEKAd5dK68y6cScOdWURxVfJ0PRcjYnSuc8ayyIQ=="], + "expo-clipboard": ["expo-clipboard@56.0.4", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-qb4DYlkiowHYHaUYVT2FN9nk/nI1xShXOUYsI7J9dVpQCOHcGFjCBPX1VAvEW4Ye4/Aagd6IuhOVAq/+scBOiA=="], + "expo-constants": ["expo-constants@56.0.18", "", { "dependencies": { "@expo/env": "~2.3.0" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-8AMtbDGl/WVPnWlmbpGmvcdnNCy9E4PFnwdVwj600vljkMDPSxcAcjw8GVXEPk3PpZ+ngTqsrkltWyj0UKYAxw=="], "expo-crypto": ["expo-crypto@56.0.4", "", { "peerDependencies": { "expo": "*" } }, "sha512-fRNEhoXRXgAWBpe3/hq5X+KXTit3OZqdiAGts1YvNEUHQb+H5591mpPac0Yw+sZg9pXcrjRnzo5AxvZaENpc7g=="], diff --git a/package.json b/package.json index 86e7eeb55..cb4307bca 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "expo-brightness": "~56.0.5", "expo-build-properties": "~56.0.18", "expo-camera": "~56.0.8", + "expo-clipboard": "~56.0.4", "expo-constants": "~56.0.18", "expo-crypto": "~56.0.4", "expo-dev-client": "~56.0.20", diff --git a/translations/en.json b/translations/en.json index 692532eb7..ccef177d9 100644 --- a/translations/en.json +++ b/translations/en.json @@ -410,7 +410,9 @@ "click_for_more_info": "Click for more info", "level": "Level", "no_logs_available": "No logs available", - "delete_all_logs": "Delete all logs" + "delete_all_logs": "Delete all logs", + "copy": "Copy", + "copied": "Copied to clipboard" }, "languages": { "title": "Languages",