diff --git a/apps/mobile/src/features/files/SourceFileSurface.tsx b/apps/mobile/src/features/files/SourceFileSurface.tsx index 2eabce998e8e..db37d7f477c9 100644 --- a/apps/mobile/src/features/files/SourceFileSurface.tsx +++ b/apps/mobile/src/features/files/SourceFileSurface.tsx @@ -2,7 +2,14 @@ import { useAtomValue } from "@effect/atom-react"; import { AsyncResult } from "effect/unstable/reactivity"; import type { ComponentType } from "react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { FlatList, ScrollView, Text as NativeText, useWindowDimensions, View } from "react-native"; +import { + FlatList, + RefreshControl, + ScrollView, + Text as NativeText, + useWindowDimensions, + View, +} from "react-native"; import { AppText as Text } from "../../components/AppText"; import { LoadingStrip } from "../../components/LoadingStrip"; @@ -46,7 +53,7 @@ const HighlightedSourceLine = memo(function HighlightedSourceLine(props: { }) { return ( {props.index + 1} - - {props.tokens && props.tokens.length > 0 - ? (() => { - let offset = 0; - return props.tokens.map((token) => { - const start = offset; - offset += token.content.length; + + {props.tokens && props.tokens.length > 0 + ? (() => { + let offset = 0; + return props.tokens.map((token) => { + const start = offset; + offset += token.content.length; - const fontWeight = - token.fontStyle !== null && (token.fontStyle & 2) === 2 - ? ("700" as const) - : ("400" as const); - const fontStyle = - token.fontStyle !== null && (token.fontStyle & 1) === 1 - ? ("italic" as const) - : ("normal" as const); + const fontWeight = + token.fontStyle !== null && (token.fontStyle & 2) === 2 + ? ("700" as const) + : ("400" as const); + const fontStyle = + token.fontStyle !== null && (token.fontStyle & 1) === 1 + ? ("italic" as const) + : ("normal" as const); - return ( - - {token.content.length > 0 ? renderVisibleWhitespace(token.content) : " "} - - ); - }); - })() - : renderVisibleWhitespace(props.line || " ")} - + return ( + + {token.content.length > 0 ? renderVisibleWhitespace(token.content) : " "} + + ); + }); + })() + : renderVisibleWhitespace(props.line || " ")} + + ); }); @@ -212,19 +223,57 @@ function NativeSourceFileSurface( } function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { + const { onRefresh } = props; const { codeSurface, codeWordBreak } = useAppearanceCodeSurface(); const { lines, status, targetIndex, tokens } = useSourceFileModel(props); const listRef = useRef>(null); + const scrollRetryCountRef = useRef(0); + const [isPullRefreshing, setIsPullRefreshing] = useState(false); + const handlePullToRefresh = useCallback(async () => { + if (!onRefresh) { + return; + } + setIsPullRefreshing(true); + try { + await onRefresh(); + } finally { + setIsPullRefreshing(false); + } + }, [onRefresh]); + + const scrollToLine = useCallback((index: number) => { + listRef.current?.scrollToIndex({ index, animated: false, viewPosition: 0.3 }); + }, []); useEffect(() => { if (targetIndex === null) { return; } + scrollRetryCountRef.current = 0; const frame = requestAnimationFrame(() => { - listRef.current?.scrollToIndex({ index: targetIndex, animated: false, viewPosition: 0.3 }); + scrollToLine(targetIndex); }); return () => cancelAnimationFrame(frame); - }, [props.path, targetIndex]); + }, [props.path, scrollToLine, targetIndex]); + + const handleScrollToIndexFailed = useCallback( + (info: { index: number; averageItemLength: number }) => { + if (scrollRetryCountRef.current >= 5) { + return; + } + scrollRetryCountRef.current += 1; + const itemLength = + info.averageItemLength > 0 ? info.averageItemLength : codeSurface.rowHeight; + listRef.current?.scrollToOffset({ + offset: info.index * itemLength, + animated: false, + }); + requestAnimationFrame(() => { + scrollToLine(info.index); + }); + }, + [codeSurface.rowHeight, scrollToLine], + ); const renderLine = useCallback( ({ item, index }: { item: string; index: number }) => ( @@ -263,6 +312,15 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { paddingTop: 8, }} renderItem={renderLine} + onScrollToIndexFailed={handleScrollToIndexFailed} + refreshControl={ + onRefresh ? ( + void handlePullToRefresh()} + /> + ) : undefined + } /> ); @@ -282,7 +340,8 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { export function SourceFileSurface(props: SourceFileSurfaceProps) { const NativeView = resolveNativeReviewDiffView(); - return NativeView ? ( + const { codeWordBreak } = useAppearanceCodeSurface(); + return NativeView && !codeWordBreak ? ( ) : (