Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7fb927f
feat(mobile): guided reviews for pull requests
juliusmarminge Oct 11, 2026
9065e46
feat(mobile): put review guides in PR tabs
juliusmarminge Oct 11, 2026
b1cf98f
style(mobile): give review guides a reading hierarchy
juliusmarminge Oct 11, 2026
26592f5
fix(mobile): connect guide excerpts to focused review diffs
juliusmarminge Oct 11, 2026
247d685
fix(mobile): simplify guide diff chrome and focus styling
juliusmarminge Oct 11, 2026
e8e7ba9
fix(mobile): retain scroll state across pull request tabs
juliusmarminge Oct 11, 2026
6748177
feat(mobile): expose word wrap in pull request guides and diffs
juliusmarminge Oct 11, 2026
2864472
fix(mobile): prevent refresh gaps in guide excerpts
juliusmarminge Oct 11, 2026
4763501
fix(mobile): let guide pages scroll over code excerpts
juliusmarminge Oct 11, 2026
3fd9654
style(mobile): remove guide step progress bar
juliusmarminge Oct 11, 2026
bc9719b
style(mobile): put guide before activity in PR tabs
juliusmarminge Oct 11, 2026
1efb0e8
feat(mobile): animate pull request sections and file rows
juliusmarminge Oct 11, 2026
7e30c30
fix(mobile): avoid competing description layout animations
juliusmarminge Oct 11, 2026
618d850
fix(mobile): move description with expanding status rows
juliusmarminge Oct 11, 2026
004defa
style(mobile): format native diff metrics event
juliusmarminge Oct 11, 2026
fd080e9
fix(mobile): refresh guides and preserve incomplete discussions
juliusmarminge Oct 11, 2026
8c6e9ab
feat(mobile): move wide iPad workspace tabs into the sidebar
juliusmarminge Oct 11, 2026
ead2f0c
fix(mobile): synchronize sidebar and native tab selection
juliusmarminge Oct 11, 2026
17cfee6
fix(mobile): label the workspace tab Threads
juliusmarminge Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ class T3ReviewDiffModule : Module() {
Prop("contentWidth") { view: T3ReviewDiffView, contentWidth: Double ->
view.setContentWidth(contentWidth.toFloat())
}
Prop("scrollEnabled") { view: T3ReviewDiffView, enabled: Boolean ->
view.setScrollEnabled(enabled)
}
Prop("initialRowIndex") { view: T3ReviewDiffView, initialRowIndex: Double ->
view.setInitialRowIndex(initialRowIndex)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,21 @@ class T3ReviewDiffView(context: Context, appContext: AppContext) : ExpoView(cont
private val verticalScroller = OverScroller(context)
private val horizontalScroller = OverScroller(context)
private var dragAxis: DragAxis? = null
private var scrollEnabled = true
private var horizontalDragTarget: HorizontalPanTarget? = null
private var lastTouchX = 0f
private var lastTouchY = 0f
private var velocityTracker: VelocityTracker? = null

init {
canvasView.onContentHeightChanged = { height ->
onDebug(
mapOf(
"message" to "metrics",
"contentHeight" to height / resources.displayMetrics.density
)
)
}
canvasView.onRowTap = { row, gesture, target -> handleRowTap(row, gesture, target) }
canvasView.onVisibleRowsChanged = { first, last ->
onDebug(
Expand Down Expand Up @@ -198,6 +207,14 @@ class T3ReviewDiffView(context: Context, appContext: AppContext) : ExpoView(cont
scrollToY(canvasView.rowTop(index), animated)
}

fun setScrollEnabled(enabled: Boolean) {
scrollEnabled = enabled
if (!enabled) {
verticalScroller.forceFinished(true)
canvasView.setVerticalOffset(0)
}
}

fun scrollToTop(animated: Boolean) {
scrollToY(0, animated)
}
Expand All @@ -212,7 +229,7 @@ class T3ReviewDiffView(context: Context, appContext: AppContext) : ExpoView(cont
horizontalDragTarget = canvasView.horizontalPanTarget(event.y)
lastTouchX = event.x
lastTouchY = event.y
parent?.requestDisallowInterceptTouchEvent(true)
parent?.requestDisallowInterceptTouchEvent(scrollEnabled)
return false
}
MotionEvent.ACTION_MOVE -> {
Expand All @@ -221,14 +238,15 @@ class T3ReviewDiffView(context: Context, appContext: AppContext) : ExpoView(cont
val deltaY = event.y - lastTouchY
if (max(abs(deltaX), abs(deltaY)) > touchSlop) {
dragAxis = if (abs(deltaY) >= abs(deltaX)) {
DragAxis.VERTICAL
if (scrollEnabled) DragAxis.VERTICAL else null
} else {
horizontalDragTarget
?.takeIf { canvasView.maxHorizontalOffset(it) > 0 }
?.let { DragAxis.HORIZONTAL }
}
}
}
if (dragAxis == DragAxis.HORIZONTAL) parent?.requestDisallowInterceptTouchEvent(true)
return dragAxis != null
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> return dragAxis != null
Expand Down Expand Up @@ -751,6 +769,7 @@ private class DiffCanvasView(context: Context) : View(context) {
}
var onRowTap: ((DiffRow, String, RowTapTarget) -> Unit)? = null
var onVisibleRowsChanged: ((Int, Int) -> Unit)? = null
var onContentHeightChanged: ((Int) -> Unit)? = null

fun prepareRows() = drawing.prepareRows(tokensByRowId, style, width)
fun useCodeLayouts(layouts: CodeLayoutCache) {
Expand Down Expand Up @@ -876,6 +895,7 @@ private class DiffCanvasView(context: Context) : View(context) {
setVerticalOffset(verticalOffset)
setHorizontalOffset(horizontalOffset)
clampHeaderPathOffsets()
onContentHeightChanged?.invoke(rowOffsets.last())
}

private fun rowHeight(row: DiffRow): Int = when (row.kind) {
Expand Down Expand Up @@ -1198,14 +1218,7 @@ private class DiffCanvasView(context: Context) : View(context) {
}
val selected = selectedRowIds.contains(row.id)
if (selected) {
fill(
canvas,
withAlpha(theme.hunkText, 56),
0f,
top.toFloat(),
width.toFloat(),
bottom.toFloat(),
)
// Keep the diff and syntax colors intact; selection lives in the gutter.
fill(
canvas,
withAlpha(theme.hunkText, 242),
Expand Down
12 changes: 12 additions & 0 deletions apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ public class T3ReviewDiffModule: Module {
view.setInitialRowIndex(initialRowIndex)
}

Prop("scrollEnabled") { (view: T3ReviewDiffView, enabled: Bool) in
view.setScrollEnabled(enabled)
}

Prop("bounces") { (view: T3ReviewDiffView, bounces: Bool) in
view.setBounces(bounces)
}

Prop("refreshEnabled") { (view: T3ReviewDiffView, enabled: Bool) in
view.setRefreshEnabled(enabled)
}

Prop("refreshing") { (view: T3ReviewDiffView, refreshing: Bool) in
view.setRefreshing(refreshing)
}
Expand Down
24 changes: 17 additions & 7 deletions apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,6 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {
scrollView.showsHorizontalScrollIndicator = false
scrollView.backgroundColor = contentView.theme.background
pullRefreshControl.addTarget(self, action: #selector(handlePullToRefresh), for: .valueChanged)
scrollView.refreshControl = pullRefreshControl
addSubview(scrollView)

contentView.backgroundColor = contentView.theme.background
Expand Down Expand Up @@ -701,14 +700,14 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {
applyPendingScrollIfNeeded()
emitVisibleFileIfNeeded()

let debugKey = "\(rows.count):\(Int(bounds.width)):\(Int(bounds.height)):\(Int(height))"
let debugKey = "\(rows.count):\(Int(bounds.width)):\(Int(bounds.height)):\(Int(contentView.contentHeight))"
if debugKey != lastMetricsDebugKey {
lastMetricsDebugKey = debugKey
emitDebug("metrics", [
"rows": rows.count,
"boundsWidth": Double(bounds.width),
"boundsHeight": Double(bounds.height),
"contentHeight": Double(height),
"contentHeight": Double(contentView.contentHeight),
"contentWidth": Double(style.contentWidth),
"fileHeaderHeight": Double(style.fileHeaderHeight),
"rowHeight": Double(style.rowHeight),
Expand Down Expand Up @@ -840,6 +839,20 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate {
setVerticalContentOffset(0, animated: animated)
}

func setScrollEnabled(_ enabled: Bool) {
scrollView.isScrollEnabled = enabled
}

func setBounces(_ bounces: Bool) {
scrollView.bounces = bounces
scrollView.alwaysBounceVertical = bounces
}

func setRefreshEnabled(_ enabled: Bool) {
if !enabled { pullRefreshControl.endRefreshing() }
scrollView.refreshControl = enabled ? pullRefreshControl : nil
}

@objc private func handlePullToRefresh() {
onPullToRefresh([:])
}
Expand Down Expand Up @@ -2429,10 +2442,7 @@ private final class ReviewDiffContentView: UIView, UIGestureRecognizerDelegate {
return
}

let selectionColor = theme.hunkText.withAlphaComponent(0.22)
selectionColor.setFill()
context.fill(rect)

// Keep the diff and syntax colors intact; selection lives in the gutter.
theme.hunkText.withAlphaComponent(0.95).setFill()
context.fill(CGRect(x: 0, y: rect.minY, width: style.changeBarWidth, height: rect.height))
}
Expand Down
5 changes: 5 additions & 0 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PullRequestGuideStopScreen } from "./features/pull-requests/guide/PullRequestGuideScreen";
import { PullRequestInlineCommentScreen } from "./features/pull-requests/PullRequestInlineCommentScreen";
import { PullRequestDiscussionScreen } from "./features/pull-requests/PullRequestDiscussionScreen";
import {
Expand Down Expand Up @@ -722,6 +723,10 @@ const RootStackConfig = createWorkspaceStackNavigator({
},
options: GLASS_HEADER_OPTIONS,
}),
PullRequestGuideStop: createNativeStackScreen({
screen: PullRequestGuideStopScreen,
options: GLASS_HEADER_OPTIONS,
}),
PullRequestFile: createNativeStackScreen({
screen: PullRequestFileRouteScreen,
options: GLASS_HEADER_OPTIONS,
Expand Down
10 changes: 9 additions & 1 deletion apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ export interface NativeReviewDiffViewProps extends ViewProps {
readonly rowHeight: number;
readonly contentWidth: number;
readonly initialRowIndex?: number;
/** Disable the native vertical scroller when an excerpt grows inside a page. */
readonly scrollEnabled?: boolean;
readonly bounces?: boolean;
readonly refreshing?: boolean;
readonly nativeViewRef?: Ref<NativeReviewDiffViewHandle>;
readonly onPullToRefresh?: (event: NativeSyntheticEvent<Record<string, never>>) => void;
Expand Down Expand Up @@ -167,6 +170,7 @@ type NativeReviewDiffRawViewProps = Omit<
NativeReviewDiffViewProps,
"nativeViewRef" | "rowsJson" | "tokensJson" | "tokensPatchJson"
> & {
readonly refreshEnabled: boolean;
readonly ref?: Ref<NativeReviewDiffViewRef>;
};

Expand Down Expand Up @@ -270,7 +274,11 @@ function NativeReviewDiffView(props: NativeReviewDiffViewProps) {
return null;
}

return createElement(RawNativeView, { ...nativeProps, ref: nativeRef });
return createElement(RawNativeView, {
...nativeProps,
refreshEnabled: props.onPullToRefresh !== undefined,
ref: nativeRef,
});
}

export function resolveNativeReviewDiffView(): ComponentType<NativeReviewDiffViewProps> | null {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Pressable, View } from "react-native";
import { AppText as Text } from "../../components/AppText";
import { SymbolView } from "../../components/AppSymbol";
import { ControlPillMenu } from "../../components/ControlPillMenu";
import type { ActivityFilter } from "./pullRequestActivityFilter";

export function PullRequestActivityFilters({
value,
onChange,
hiddenCount,
}: {
readonly value: ActivityFilter;
readonly onChange: (value: ActivityFilter) => void;
readonly hiddenCount: number;
}) {
const kinds = [
{ id: "all", title: "All activity" },
{ id: "comments", title: "Comments" },
{ id: "commits", title: "Commits" },
] as const;
return (
<View className="mb-4 flex-row items-center justify-between gap-3">
<ControlPillMenu
title="Activity"
actions={kinds.map((kind) => ({ ...kind, state: value.kind === kind.id ? "on" : "off" }))}
onPressAction={({ nativeEvent }) => {
const kind = kinds.find((item) => item.id === nativeEvent.event);
if (kind) onChange({ ...value, kind: kind.id });
}}
>
<Pressable
accessibilityRole="button"
accessibilityLabel={`Activity type, ${kinds.find((kind) => kind.id === value.kind)?.title}`}
className="min-h-11 flex-row items-center gap-2 active:opacity-60"
>
<Text className="text-sm font-t3-semibold">
{kinds.find((kind) => kind.id === value.kind)?.title}
</Text>
<SymbolView name="chevron.down" size={11} tintColorClassName="accent-icon-muted" />
</Pressable>
</ControlPillMenu>
<View className="flex-row items-center gap-3">
<ControlPillMenu
title="Show in activity"
actions={[
{ id: "bots", title: "Bot comments", state: value.bots ? "on" : "off" },
{
id: "resolved",
title: "Resolved or dismissed",
state: value.resolved ? "on" : "off",
},
]}
onPressAction={({ nativeEvent: { event } }) => {
if (event === "bots" || event === "resolved")
onChange({ ...value, [event]: !value[event] });
}}
>
<Pressable
accessibilityRole="button"
accessibilityLabel={`Filter activity, ${hiddenCount} hidden`}
className="min-h-11 flex-row items-center gap-1.5 active:opacity-60"
>
{hiddenCount > 0 ? (
<Text className="text-xs text-foreground-muted">{hiddenCount} hidden</Text>
) : null}
<SymbolView
name="line.3.horizontal.decrease"
size={18}
tintColorClassName="accent-icon-muted"
/>
</Pressable>
</ControlPillMenu>
<Pressable
accessibilityRole="button"
accessibilityLabel={value.newestFirst ? "Newest first" : "Oldest first"}
onPress={() => onChange({ ...value, newestFirst: !value.newestFirst })}
className="h-11 flex-row items-center gap-1 active:opacity-60"
>
<SymbolView
name={value.newestFirst ? "arrow.down" : "arrow.up"}
size={18}
tintColorClassName="accent-icon-muted"
/>
<Text className="text-xs text-foreground-muted">
{value.newestFirst ? "Newest" : "Oldest"}
</Text>
</Pressable>
</View>
</View>
);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Animated from "react-native-reanimated";
import { PR_ROW_LAYOUT } from "./pullRequestAnimations";
import { useNavigation } from "@react-navigation/native";
import type { PullRequestDetail } from "@t3tools/contracts";
import type { ReactNode } from "react";
Expand All @@ -21,9 +23,12 @@ import { usePullRequestAgentActions } from "./usePullRequestAgentActions";

export function PullRequestCardGroup(props: { readonly children: ReactNode }) {
return (
<View className="overflow-hidden rounded-[22px] border-continuous bg-grouped-card">
<Animated.View
layout={PR_ROW_LAYOUT}
className="overflow-hidden rounded-[22px] border-continuous bg-grouped-card"
>
{props.children}
</View>
</Animated.View>
);
}

Expand Down
Loading
Loading