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 ? (
) : (