| author | |
| committer | |
| log | 97a65a3121c11801a877897928efc51ef40fa49f |
| tree | 6e07cb07aab139199f216316c0358f09b3f4dd35 |
| parent | 57f96e69d39669f47e7310062222cd67adbb4912 |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 32 insertions(+), 25 deletions(-)
apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift+18-11| ... | ... | @@ -563,7 +563,7 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { |
| 563 | 563 | ]) |
| 564 | 564 | } |
| 565 | 565 | |
| 566 | // Source files fill the screen so UIKit can sample content below the glass bar. | |
| 566 | // Code surfaces fill the screen so UIKit can sample content below the glass bar. | |
| 567 | 567 | // Keep the same visible row when header or safe-area reservations change. |
| 568 | 568 | func setContentInsetTop(_ inset: CGFloat) { |
| 569 | 569 | scrollView.automaticallyAdjustsScrollIndicatorInsets = false |
| ... | ... | @@ -571,6 +571,7 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { |
| 571 | 571 | let delta = inset - scrollView.contentInset.top |
| 572 | 572 | guard delta != 0 else { return } |
| 573 | 573 | scrollView.contentInset.top = inset |
| 574 | contentView.topContentInset = inset | |
| 574 | 575 | scrollView.verticalScrollIndicatorInsets.top = inset |
| 575 | 576 | scrollView.contentOffset.y -= delta |
| 576 | 577 | updateViewportFrame() |
| ... | ... | @@ -618,8 +619,12 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { |
| 618 | 619 | if let scrollAnchor, |
| 619 | 620 | let headerOffset = contentView.fileHeaderOffset(forFileId: scrollAnchor.fileId) { |
| 620 | 621 | let targetOffset = headerOffset - scrollAnchor.screenY |
| 621 | let maxOffset = max(scrollView.contentSize.height - scrollView.bounds.height, 0) | |
| 622 | let clampedOffset = min(max(targetOffset, 0), maxOffset) | |
| 622 | let minOffset = -scrollView.contentInset.top | |
| 623 | let maxOffset = max( | |
| 624 | scrollView.contentSize.height - scrollView.bounds.height + scrollView.contentInset.bottom, | |
| 625 | minOffset | |
| 626 | ) | |
| 627 | let clampedOffset = min(max(targetOffset, minOffset), maxOffset) | |
| 623 | 628 | scrollView.setContentOffset(CGPoint(x: 0, y: clampedOffset), animated: false) |
| 624 | 629 | updateViewportFrame() |
| 625 | 630 | } |
| ... | ... | @@ -735,7 +740,8 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { |
| 735 | 740 | // The top of the combined diff is the explicit "All files" destination. |
| 736 | 741 | // Treat it as a first-class selection instead of immediately resolving the |
| 737 | 742 | // first file header and undoing the navigator's optimistic selection. |
| 738 | if scrollView.contentOffset.y <= 0.5 { | |
| 743 | let visibleTop = scrollView.contentOffset.y + scrollView.contentInset.top | |
| 744 | if visibleTop <= 0.5 { | |
| 739 | 745 | guard lastVisibleFileId != nil else { |
| 740 | 746 | return |
| 741 | 747 | } |
| ... | ... | @@ -744,7 +750,7 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { |
| 744 | 750 | return |
| 745 | 751 | } |
| 746 | 752 | |
| 747 | guard let fileId = contentView.visibleFileId(atVerticalOffset: scrollView.contentOffset.y), | |
| 753 | guard let fileId = contentView.visibleFileId(atVerticalOffset: visibleTop), | |
| 748 | 754 | fileId != lastVisibleFileId else { |
| 749 | 755 | return |
| 750 | 756 | } |
| ... | ... | @@ -1025,6 +1031,7 @@ private final class ReviewDiffContentView: UIView, UIGestureRecognizerDelegate { |
| 1025 | 1031 | } |
| 1026 | 1032 | } |
| 1027 | 1033 | var verticalOffset: CGFloat = 0 |
| 1034 | var topContentInset: CGFloat = 0 | |
| 1028 | 1035 | var theme = ReviewDiffNativeTheme.resolve("light") { |
| 1029 | 1036 | didSet { |
| 1030 | 1037 | tokenColorsByHex.removeAll() |
| ... | ... | @@ -1495,7 +1502,7 @@ private final class ReviewDiffContentView: UIView, UIGestureRecognizerDelegate { |
| 1495 | 1502 | |
| 1496 | 1503 | let rowIndex = stickyHeader.rowIndex |
| 1497 | 1504 | let headerTop = rowOffsets[rowIndex] |
| 1498 | guard headerTop < verticalOffset else { | |
| 1505 | guard headerTop < verticalOffset + topContentInset else { | |
| 1499 | 1506 | return nil |
| 1500 | 1507 | } |
| 1501 | 1508 | |
| ... | ... | @@ -1505,12 +1512,12 @@ private final class ReviewDiffContentView: UIView, UIGestureRecognizerDelegate { |
| 1505 | 1512 | : nil |
| 1506 | 1513 | let pushedY: CGFloat |
| 1507 | 1514 | if let nextHeaderRowIndex { |
| 1508 | pushedY = min(0, rowOffsets[nextHeaderRowIndex] - verticalOffset - style.fileHeaderHeight) | |
| 1515 | pushedY = min(topContentInset, rowOffsets[nextHeaderRowIndex] - verticalOffset - style.fileHeaderHeight) | |
| 1509 | 1516 | } else { |
| 1510 | pushedY = 0 | |
| 1517 | pushedY = topContentInset | |
| 1511 | 1518 | } |
| 1512 | 1519 | |
| 1513 | guard pushedY > -style.fileHeaderHeight else { | |
| 1520 | guard pushedY > topContentInset - style.fileHeaderHeight else { | |
| 1514 | 1521 | return nil |
| 1515 | 1522 | } |
| 1516 | 1523 | |
| ... | ... | @@ -1533,7 +1540,7 @@ private final class ReviewDiffContentView: UIView, UIGestureRecognizerDelegate { |
| 1533 | 1540 | while lowerBound < upperBound { |
| 1534 | 1541 | let midpoint = (lowerBound + upperBound) / 2 |
| 1535 | 1542 | let rowIndex = fileHeaderRowIndices[midpoint] |
| 1536 | if rowOffsets[rowIndex] <= verticalOffset { | |
| 1543 | if rowOffsets[rowIndex] <= verticalOffset + topContentInset { | |
| 1537 | 1544 | lowerBound = midpoint + 1 |
| 1538 | 1545 | } else { |
| 1539 | 1546 | upperBound = midpoint |
| ... | ... | @@ -1550,7 +1557,7 @@ private final class ReviewDiffContentView: UIView, UIGestureRecognizerDelegate { |
| 1550 | 1557 | func scrollAnchor(forFileId fileId: String) -> ReviewDiffScrollAnchor? { |
| 1551 | 1558 | if let stickyHeader = stickyFileHeaderTarget(), |
| 1552 | 1559 | resolvedFileId(for: stickyHeader.row) == fileId { |
| 1553 | return ReviewDiffScrollAnchor(fileId: fileId, screenY: 0) | |
| 1560 | return ReviewDiffScrollAnchor(fileId: fileId, screenY: topContentInset) | |
| 1554 | 1561 | } |
| 1555 | 1562 | |
| 1556 | 1563 | guard let headerOffset = fileHeaderOffset(forFileId: fileId) else { |
apps/mobile/src/Stack.tsx+2-3| ... | ... | @@ -158,8 +158,7 @@ const GLASS_HEADER_OPTIONS: AppScreenOptions = { |
| 158 | 158 | unstable_navigationItemStyle: NATIVE_LIQUID_GLASS_SUPPORTED ? "editor" : undefined, |
| 159 | 159 | }; |
| 160 | 160 | |
| 161 | // SOLID: opaque sheet-colored header for surfaces whose content scrolls internally | |
| 162 | // (terminal, review) — there is nothing for glass to sample there. | |
| 161 | // SOLID: opaque sheet-colored header for surfaces that manage their own viewport. | |
| 163 | 162 | const SOLID_HEADER_OPTIONS: AppScreenOptions = { |
| 164 | 163 | headerBackButtonDisplayMode: "minimal", |
| 165 | 164 | headerBackTitle: "", |
| ... | ... | @@ -724,7 +723,7 @@ const RootStackConfig = createWorkspaceStackNavigator({ |
| 724 | 723 | ThreadReview: createNativeStackScreen({ |
| 725 | 724 | screen: ReviewSheet, |
| 726 | 725 | linking: `${THREAD_LINKING_PREFIX}/review`, |
| 727 | options: SOLID_HEADER_OPTIONS, | |
| 726 | options: GLASS_HEADER_OPTIONS, | |
| 728 | 727 | }), |
| 729 | 728 | ThreadReviewComment: createNativeStackScreen({ |
| 730 | 729 | screen: ReviewCommentComposerSheet, |
apps/mobile/src/features/review/ReviewSheet.tsx+12-11| ... | ... | @@ -29,6 +29,7 @@ import { |
| 29 | 29 | View, |
| 30 | 30 | } from "react-native"; |
| 31 | 31 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 32 | import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics"; | |
| 32 | 33 | |
| 33 | 34 | import { AppText as Text } from "../../components/AppText"; |
| 34 | 35 | import { SymbolView } from "../../components/AppSymbol"; |
| ... | ... | @@ -156,7 +157,6 @@ function ReviewHeader( |
| 156 | 157 | ); |
| 157 | 158 | } |
| 158 | 159 | |
| 159 | const REVIEW_HEADER_SPACING = 0; | |
| 160 | 160 | const SHOWCASE_ENABLED = process.env.EXPO_PUBLIC_SHOWCASE === "1"; |
| 161 | 161 | |
| 162 | 162 | const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) { |
| ... | ... | @@ -461,10 +461,11 @@ export function ReviewSheet(props: ReviewSheetProps) { |
| 461 | 461 | const { draftMessage } = useThreadDraftForThread({ environmentId, threadId }); |
| 462 | 462 | const reviewCache = useReviewCacheForThread({ environmentId, threadId }); |
| 463 | 463 | const { selectedThreadCwd } = useSelectedThreadWorktree(); |
| 464 | // With a solid (non-overlay) header the content lays out below the header | |
| 465 | // natively, so no manual top inset is needed. (Android renders its own | |
| 466 | // in-flow AndroidScreenHeader, so it needs no inset either.) | |
| 467 | const topContentInset = 0; | |
| 464 | const columnMetrics = useNativeColumnLayoutMetrics(); | |
| 465 | const safeArea = columnMetrics?.safeArea ?? insets; | |
| 466 | const topContentInset = Platform.OS === "ios" ? safeArea.top : 0; | |
| 467 | const bottomContentInset = | |
| 468 | Platform.OS === "ios" ? safeArea.bottom + 8 : Math.max(insets.bottom, 18) + 18; | |
| 468 | 469 | |
| 469 | 470 | useEffect(() => { |
| 470 | 471 | showAuxiliaryPane("inspector"); |
| ... | ... | @@ -760,7 +761,7 @@ export function ReviewSheet(props: ReviewSheetProps) { |
| 760 | 761 | > |
| 761 | 762 | <View |
| 762 | 763 | className="min-w-0 flex-1" |
| 763 | style={{ paddingTop: topContentInset + REVIEW_HEADER_SPACING }} | |
| 764 | style={listHeader ? { paddingTop: topContentInset } : undefined} | |
| 764 | 765 | > |
| 765 | 766 | {listHeader} |
| 766 | 767 | <View className="min-w-0 flex-1" collapsable={false}> |
| ... | ... | @@ -777,6 +778,8 @@ export function ReviewSheet(props: ReviewSheetProps) { |
| 777 | 778 | collapsedCommentIdsJson={nativeBridge.collapsedCommentIdsJson} |
| 778 | 779 | contentResetKey={`${reviewCache.threadKey}:${selectedSection.id}`} |
| 779 | 780 | contentWidth={NATIVE_REVIEW_DIFF_CONTENT_WIDTH} |
| 781 | contentInsetTop={listHeader ? 0 : topContentInset} | |
| 782 | contentInsetBottom={bottomContentInset} | |
| 780 | 783 | nativeViewRef={nativeReviewDiffViewRef} |
| 781 | 784 | rowHeight={nativeReviewDiffStyle.rowHeight} |
| 782 | 785 | rowsJson={nativeBridge.rowsJson} |
| ... | ... | @@ -804,12 +807,10 @@ export function ReviewSheet(props: ReviewSheetProps) { |
| 804 | 807 | : undefined |
| 805 | 808 | } |
| 806 | 809 | contentInsetAdjustmentBehavior="never" |
| 807 | contentInset={{ top: topContentInset, bottom: Math.max(insets.bottom, 18) + 18 }} | |
| 810 | contentInset={{ top: topContentInset, bottom: bottomContentInset }} | |
| 808 | 811 | contentOffset={{ x: 0, y: -topContentInset }} |
| 809 | scrollIndicatorInsets={{ | |
| 810 | top: topContentInset, | |
| 811 | bottom: Math.max(insets.bottom, 18) + 18, | |
| 812 | }} | |
| 812 | automaticallyAdjustsScrollIndicatorInsets={false} | |
| 813 | scrollIndicatorInsets={{ top: topContentInset, bottom: bottomContentInset }} | |
| 813 | 814 | showsVerticalScrollIndicator={false} |
| 814 | 815 | className="flex-1" |
| 815 | 816 | refreshControl={ |