| ... | ... | @@ -23,6 +23,7 @@ import { |
| 23 | 23 | FlatList, |
| 24 | 24 | Platform, |
| 25 | 25 | Pressable, |
| 26 | RefreshControl, |
| 26 | 27 | ScrollView, |
| 27 | 28 | type NativeSyntheticEvent, |
| 28 | 29 | StyleSheet, |
| ... | ... | @@ -399,7 +400,11 @@ export function ReviewSheet(props: ReviewSheetProps) { |
| 399 | 400 | selectedSection, |
| 400 | 401 | draftMessage, |
| 401 | 402 | }); |
| 402 | | const NativeReviewDiffView = resolveNativeReviewDiffView()!; |
| 403 | // Resolution returns null while Expo registers the native view (or forever |
| 404 | // when the binary lacks it). Rendering a null component type crashes the |
| 405 | // app, so callers must fall back — ThreadFeed's ReviewCommentCard does the |
| 406 | // same check. |
| 407 | const NativeReviewDiffView = resolveNativeReviewDiffView(); |
| 403 | 408 | const nativeReviewDiffViewRef = useRef<NativeReviewDiffViewHandle>(null); |
| 404 | 409 | const showcasedReviewDrawRef = useRef<string | null>(null); |
| 405 | 410 | // Native pull-to-refresh on the diff surface (replaces the old Refresh menu item). |
| ... | ... | @@ -597,11 +602,27 @@ export function ReviewSheet(props: ReviewSheetProps) { |
| 597 | 602 | .filter((part): part is string => Boolean(part)) |
| 598 | 603 | .join(" · "); |
| 599 | 604 | |
| 600 | | // The changed-files navigator lives in the workspace inspector column — |
| 601 | | // the single right-hand pane per route — instead of an in-screen panel. |
| 605 | // The changed-files navigator drives the native diff surface via |
| 606 | // scrollToFile, so it is only useful when that surface resolved. In raw |
| 607 | // fallback mode the ref is necessarily null and the raw patch neither |
| 608 | // scrolls nor filters — registering the navigator would present working |
| 609 | // controls that cannot navigate. |
| 602 | 610 | const showChangedFilesPane = |
| 603 | | !showConnectionNotice && selectedSection !== null && parsedDiff.kind === "files"; |
| 611 | !showConnectionNotice && |
| 612 | selectedSection !== null && |
| 613 | parsedDiff.kind === "files" && |
| 614 | NativeReviewDiffView !== null; |
| 604 | 615 | useRegisterWorkspaceInspector(showChangedFilesPane ? renderInspector : undefined); |
| 616 | // Raw fallback renders the patch inline with no inspector content, so the |
| 617 | // pane toggle would open an empty column — hide it in exactly that case. |
| 618 | const showChangedFilesToggle = |
| 619 | panes.supportsAuxiliaryPane && |
| 620 | !( |
| 621 | !showConnectionNotice && |
| 622 | selectedSection !== null && |
| 623 | parsedDiff.kind === "files" && |
| 624 | NativeReviewDiffView === null |
| 625 | ); |
| 605 | 626 | |
| 606 | 627 | const listHeader = useMemo(() => { |
| 607 | 628 | const children: ReactElement[] = []; |
| ... | ... | @@ -688,7 +709,7 @@ export function ReviewSheet(props: ReviewSheetProps) { |
| 688 | 709 | |
| 689 | 710 | {!isAndroid && (showSectionToolbar || panes.supportsAuxiliaryPane || gitMenuAvailable) ? ( |
| 690 | 711 | <NativeHeaderToolbar placement="right"> |
| 691 | | {panes.supportsAuxiliaryPane ? ( |
| 712 | {showChangedFilesToggle ? ( |
| 692 | 713 | <NativeHeaderToolbar.Button |
| 693 | 714 | accessibilityLabel={ |
| 694 | 715 | panes.auxiliaryPaneVisible ? "Hide changed files" : "Show changed files" |
| ... | ... | @@ -781,7 +802,7 @@ export function ReviewSheet(props: ReviewSheetProps) { |
| 781 | 802 | onRetry={handleRetryEnvironment} |
| 782 | 803 | /> |
| 783 | 804 | </View> |
| 784 | | ) : selectedSection && parsedDiff.kind === "files" ? ( |
| 805 | ) : selectedSection && parsedDiff.kind === "files" && NativeReviewDiffView ? ( |
| 785 | 806 | <View |
| 786 | 807 | className="flex-1" |
| 787 | 808 | style={{ |
| ... | ... | @@ -835,6 +856,16 @@ export function ReviewSheet(props: ReviewSheetProps) { |
| 835 | 856 | }} |
| 836 | 857 | showsVerticalScrollIndicator={false} |
| 837 | 858 | className="flex-1" |
| 859 | refreshControl={ |
| 860 | // The native diff surface owns pull-to-refresh via onPullToRefresh; |
| 861 | // the raw fallback (and empty states) need an explicit control — |
| 862 | // iOS has no other refresh affordance here (the explicit |
| 863 | // "Refresh current diff" menu is Android-only). |
| 864 | <RefreshControl |
| 865 | refreshing={isPullRefreshing} |
| 866 | onRefresh={() => void handlePullToRefresh()} |
| 867 | /> |
| 868 | } |
| 838 | 869 | > |
| 839 | 870 | {listHeader} |
| 840 | 871 | {!selectedSection ? ( |
| ... | ... | @@ -867,6 +898,19 @@ export function ReviewSheet(props: ReviewSheetProps) { |
| 867 | 898 | </Text> |
| 868 | 899 | </ScrollView> |
| 869 | 900 | </View> |
| 901 | ) : parsedDiff.kind === "files" ? ( |
| 902 | // The native diff surface could not be resolved on this binary; |
| 903 | // degrade to the raw patch instead of crashing the app. |
| 904 | <View className="gap-3 border-b border-border bg-card px-4 py-4"> |
| 905 | <Text className="text-xs leading-normal text-foreground-muted"> |
| 906 | Native diff view unavailable. Showing the raw patch. |
| 907 | </Text> |
| 908 | <ScrollView horizontal showsHorizontalScrollIndicator={false} bounces={false}> |
| 909 | <Text selectable className="font-mono text-xs leading-relaxed text-foreground"> |
| 910 | {selectedSection?.diff ?? ""} |
| 911 | </Text> |
| 912 | </ScrollView> |
| 913 | </View> |
| 870 | 914 | ) : null} |
| 871 | 915 | </ScrollView> |
| 872 | 916 | )} |