diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index 80ebe1157..c1f8f8e6a 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -23,6 +23,7 @@ import { FlatList, Platform, Pressable, + RefreshControl, ScrollView, type NativeSyntheticEvent, StyleSheet, @@ -401,7 +402,11 @@ export function ReviewSheet(props: ReviewSheetProps) { selectedSection, draftMessage, }); - const NativeReviewDiffView = resolveNativeReviewDiffView()!; + // Resolution returns null while Expo registers the native view (or forever + // when the binary lacks it). Rendering a null component type crashes the + // app, so callers must fall back — ThreadFeed's ReviewCommentCard does the + // same check. + const NativeReviewDiffView = resolveNativeReviewDiffView(); const nativeReviewDiffViewRef = useRef(null); const showcasedReviewDrawRef = useRef(null); // Native pull-to-refresh on the diff surface (replaces the old Refresh menu item). @@ -599,11 +604,27 @@ export function ReviewSheet(props: ReviewSheetProps) { .filter((part): part is string => Boolean(part)) .join(" · "); - // The changed-files navigator lives in the workspace inspector column — - // the single right-hand pane per route — instead of an in-screen panel. + // The changed-files navigator drives the native diff surface via + // scrollToFile, so it is only useful when that surface resolved. In raw + // fallback mode the ref is necessarily null and the raw patch neither + // scrolls nor filters — registering the navigator would present working + // controls that cannot navigate. const showChangedFilesPane = - !showConnectionNotice && selectedSection !== null && parsedDiff.kind === "files"; + !showConnectionNotice && + selectedSection !== null && + parsedDiff.kind === "files" && + NativeReviewDiffView !== null; useRegisterWorkspaceInspector(showChangedFilesPane ? renderInspector : undefined); + // Raw fallback renders the patch inline with no inspector content, so the + // pane toggle would open an empty column — hide it in exactly that case. + const showChangedFilesToggle = + panes.supportsAuxiliaryPane && + !( + !showConnectionNotice && + selectedSection !== null && + parsedDiff.kind === "files" && + NativeReviewDiffView === null + ); const listHeader = useMemo(() => { const children: ReactElement[] = []; @@ -690,7 +711,7 @@ export function ReviewSheet(props: ReviewSheetProps) { {!isAndroid && (showSectionToolbar || panes.supportsAuxiliaryPane || gitMenuAvailable) ? ( - {panes.supportsAuxiliaryPane ? ( + {showChangedFilesToggle ? ( - ) : selectedSection && parsedDiff.kind === "files" ? ( + ) : selectedSection && parsedDiff.kind === "files" && NativeReviewDiffView ? ( void handlePullToRefresh()} + /> + } > {listHeader} {!selectedSection ? ( @@ -869,6 +900,19 @@ export function ReviewSheet(props: ReviewSheetProps) { + ) : parsedDiff.kind === "files" ? ( + // The native diff surface could not be resolved on this binary; + // degrade to the raw patch instead of crashing the app. + + + Native diff view unavailable. Showing the raw patch. + + + + {selectedSection?.diff ?? ""} + + + ) : null} )}