Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { useConnectOnboardingNavigation } from "./features/cloud/connectOnboardi
import { AttachmentFileScreen } from "./features/files/AttachmentFileScreen";
import { ThreadFilesTreeScreen, ThreadFileScreen } from "./features/files/ThreadFilesRouteScreen";
import { AdaptiveWorkspaceLayout } from "./features/layout/AdaptiveWorkspaceLayout";
import { selectedPullRequestKey } from "./features/pull-requests/pullRequestSelection";
import {
HardwareKeyboardCommandOverlay,
HardwareKeyboardCommandProvider,
Expand Down Expand Up @@ -658,6 +659,7 @@ function RootStackLayout(props: {
<AdaptiveWorkspaceLayout
pathname={workspaceLocation.pathname}
workspaceRouteKey={workspaceLocation.routeKey}
selectedPullRequestKey={selectedPullRequestKey(props.state)}
>
{props.children}
<HardwareKeyboardCommandOverlay />
Expand Down
7 changes: 6 additions & 1 deletion apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export function AdaptiveWorkspaceLayout(props: {
readonly children: ReactNode;
readonly pathname: string;
readonly workspaceRouteKey: string | undefined;
readonly selectedPullRequestKey?: string | null;
}) {
const preferencesResult = useAtomValue(mobilePreferencesAtom);
if (!AsyncResult.isSuccess(preferencesResult)) {
Expand All @@ -243,6 +244,7 @@ function AdaptiveWorkspaceLayoutContent(
readonly children: ReactNode;
readonly pathname: string;
readonly workspaceRouteKey: string | undefined;
readonly selectedPullRequestKey?: string | null;
} & {
readonly projectGroupingMode: SidebarProjectGroupingMode;
},
Expand Down Expand Up @@ -730,7 +732,10 @@ function AdaptiveWorkspaceLayoutContent(
/>
)}
>
<AndroidWorkspaceSidebar onRequestVisibility={revealPrimarySidebar}>
<AndroidWorkspaceSidebar
onRequestVisibility={revealPrimarySidebar}
selectedPullRequestKey={props.selectedPullRequestKey ?? null}
>
<AndroidHomeFabLayout sidebar onStartNewTask={handleStartNewTask}>
<ThreadNavigationSidebar
width={layout.listPaneWidth}
Expand Down
252 changes: 147 additions & 105 deletions apps/mobile/src/features/pull-requests/PullRequestListRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { ControlPillMenu } from "../../components/ControlPillMenu";
import type { ControlPillMenuProps } from "../../components/ControlPillMenu.types";
import { cn } from "../../lib/cn";
import { relativeTime } from "../../lib/time";
import {
selectedThreadRowColors,
THREAD_LIST_V2_SIDEBAR_ROW_STYLE,
THREAD_LIST_V2_SIDEBAR_ROW_CONTAINER_STYLE,
} from "../threads/thread-list-v2-row-appearance";
import { PullRequestAvatar, PullRequestDiffStats } from "./PullRequestPresentation";
import {
pullRequestLabelColor,
Expand All @@ -20,11 +25,27 @@ export const PullRequestListRow = memo(
readonly item: EnvironmentPullRequestListEntry;
readonly onSelect: (item: EnvironmentPullRequestListEntry) => void;
readonly selected: boolean;
readonly sidebar: boolean;
readonly busy: boolean;
readonly getActions: (item: EnvironmentPullRequestListEntry) => ControlPillMenuProps["actions"];
readonly onAction: (item: EnvironmentPullRequestListEntry, action: string) => void;
}) {
const { item } = props;
const foreground = props.selected
? selectedThreadRowColors.foregroundClassName
: props.sidebar
? "text-drawer-foreground"
: "text-foreground";
const mutedForeground = props.selected
? selectedThreadRowColors.mutedForegroundClassName
: props.sidebar
? "text-drawer-foreground-muted"
: "text-foreground-muted";
const tertiaryForeground = props.selected
? selectedThreadRowColors.mutedForegroundClassName
: props.sidebar
? "text-drawer-foreground-muted"
: "text-foreground-tertiary";
const status = pullRequestStatus(item);
const review = pullRequestListReviewStatus(item);
const checkTint =
Expand All @@ -34,123 +55,144 @@ export const PullRequestListRow = memo(
? "accent-danger-foreground"
: "accent-warning-foreground";
return (
<ControlPillMenu
shouldOpenOnLongPress
title={`#${item.number} · ${item.repository}`}
actions={props.getActions(item)}
onPressAction={({ nativeEvent }) => props.onAction(item, nativeEvent.event)}
>
<Pressable
accessibilityRole="button"
accessibilityState={{ selected: props.selected, busy: props.busy }}
accessibilityLabel={`Pull request ${item.number}, ${item.title}, ${status.label}, by ${item.author?.login ?? "unknown author"}${review ? `, ${review.accessibleLabel}` : ""}${item.checksState ? `, checks ${item.checksState}` : ""}${item.stack ? `, stack ${item.stack.position} of ${item.stack.size}` : ""}${item.labels.length ? `, ${item.labels.map((label) => label.name).join(", ")}` : ""}, ${item.additions} additions, ${item.deletions} deletions`}
accessibilityHint="Double tap to open. Touch and hold for quick actions."
onPress={() => props.onSelect(item)}
disabled={props.busy}
className={cn(
"flex-row gap-3 px-4 active:bg-row-hover",
props.selected && "bg-thread-selected",
props.busy && "opacity-50",
)}
<View style={props.sidebar ? THREAD_LIST_V2_SIDEBAR_ROW_CONTAINER_STYLE : undefined}>
<ControlPillMenu
shouldOpenOnLongPress
title={`#${item.number} · ${item.repository}`}
actions={props.getActions(item)}
onPressAction={({ nativeEvent }) => props.onAction(item, nativeEvent.event)}
>
<View className="pt-2.5">
<SymbolView name={status.icon} size={20} tintColorClassName={status.tint} />
</View>
<View className="min-w-0 flex-1 gap-1 border-b border-border-subtle py-2.5">
<View className="flex-row items-center gap-1.5">
<PullRequestAvatar actor={item.author} size={16} />
<Text className="min-w-0 flex-1 text-xs text-foreground-muted" numberOfLines={1}>
{item.repository} <Text className="text-foreground-tertiary">#{item.number}</Text>
</Text>
{item.stack ? (
<View className="flex-row items-center gap-1">
<SymbolView
name="square.3.layers.3d"
size={12}
tintColorClassName="accent-icon-muted"
/>
<Text className="text-2xs tabular-nums text-foreground-muted">
{item.stack.position}/{item.stack.size}
</Text>
</View>
) : null}
<Text className="text-2xs tabular-nums text-foreground-tertiary">
{relativeTime(item.updatedAt)}
</Text>
<Pressable
accessibilityRole="button"
accessibilityState={{ selected: props.selected, busy: props.busy }}
accessibilityLabel={`Pull request ${item.number}, ${item.title}, ${status.label}, by ${item.author?.login ?? "unknown author"}${review ? `, ${review.accessibleLabel}` : ""}${item.checksState ? `, checks ${item.checksState}` : ""}${item.stack ? `, stack ${item.stack.position} of ${item.stack.size}` : ""}${item.labels.length ? `, ${item.labels.map((label) => label.name).join(", ")}` : ""}, ${item.additions} additions, ${item.deletions} deletions`}
accessibilityHint="Double tap to open. Touch and hold for quick actions."
onPress={() => props.onSelect(item)}
disabled={props.busy}
style={props.sidebar ? THREAD_LIST_V2_SIDEBAR_ROW_STYLE : undefined}
className={cn(
"flex-row gap-3 overflow-hidden",
!props.sidebar && "px-4 active:bg-row-hover",
props.sidebar && "bg-drawer",
props.sidebar && !props.selected && "active:bg-thread-hover",
props.selected && "bg-thread-selected",
props.busy && "opacity-50",
)}
>
<View className={props.sidebar ? "pt-0.5" : "pt-2.5"}>
<SymbolView name={status.icon} size={20} tintColorClassName={status.tint} />
</View>
<Text
className="text-base leading-5 font-t3-medium text-foreground"
numberOfLines={2}
ellipsizeMode="tail"
<View
className={cn(
"min-w-0 flex-1 gap-1",
!props.sidebar && "border-b border-border-subtle py-2.5",
)}
>
{item.title}
</Text>
<View className="flex-row items-center gap-1.5">
<View className="min-w-0 flex-1 flex-row items-center gap-1">
{item.labels.slice(0, 2).map((label) => {
const color = pullRequestLabelColor(label.color);
return (
<View
key={label.name}
className="min-w-0 shrink flex-row items-center gap-1 rounded-md bg-subtle px-1.5 py-0.5"
style={{ maxWidth: 160, ...(color ? { backgroundColor: `${color}20` } : {}) }}
>
{color ? (
<View
style={{ width: 5, height: 5, borderRadius: 3, backgroundColor: color }}
/>
) : null}
<Text
className="shrink text-2xs font-t3-medium text-foreground-muted"
numberOfLines={1}
>
{label.name}
</Text>
</View>
);
})}
{item.labels.length > 2 ? (
<Text className="shrink-0 text-2xs text-foreground-tertiary">
+{item.labels.length - 2}
</Text>
<View className="flex-row items-center gap-1.5">
<PullRequestAvatar actor={item.author} size={16} />
<Text className={cn("min-w-0 flex-1 text-xs", mutedForeground)} numberOfLines={1}>
{item.repository} <Text className={tertiaryForeground}>#{item.number}</Text>
</Text>
{item.stack ? (
<View className="flex-row items-center gap-1">
<SymbolView
name="square.3.layers.3d"
size={12}
tintColorClassName={
props.selected
? selectedThreadRowColors.mutedIconTintClassName
: props.sidebar
? "accent-drawer-foreground-muted"
: "accent-foreground-muted"
}
/>
<Text className={cn("text-2xs tabular-nums", mutedForeground)}>
{item.stack.position}/{item.stack.size}
</Text>
</View>
) : null}
<Text className={cn("text-2xs tabular-nums", tertiaryForeground)}>
{relativeTime(item.updatedAt)}
</Text>
</View>
{item.checksState ? (
<View className="shrink-0" accessibilityLabel={`Checks ${item.checksState}`}>
<SymbolView
name={
item.checksState === "passing"
? "checkmark.circle"
: item.checksState === "failing"
? "xmark.circle.fill"
: "clock"
}
size={13}
tintColorClassName={checkTint}
/>
</View>
) : null}
{review ? (
<View className="shrink-0" accessibilityLabel={review.accessibleLabel}>
<SymbolView name={review.icon} size={13} tintColorClassName={review.tint} />
<Text
className={cn("text-base leading-5 font-t3-medium", foreground)}
numberOfLines={2}
ellipsizeMode="tail"
>
{item.title}
</Text>
<View className="flex-row items-center gap-1.5">
<View className="min-w-0 flex-1 flex-row items-center gap-1">
{item.labels.slice(0, 2).map((label) => {
const color = pullRequestLabelColor(label.color);
return (
<View
key={label.name}
className="min-w-0 shrink flex-row items-center gap-1 rounded-md bg-subtle px-1.5 py-0.5"
style={{
maxWidth: 160,
...(color ? { backgroundColor: `${color}20` } : {}),
}}
>
{color ? (
<View
style={{ width: 5, height: 5, borderRadius: 3, backgroundColor: color }}
/>
) : null}
<Text
className={cn("shrink text-2xs font-t3-medium", mutedForeground)}
numberOfLines={1}
>
{label.name}
</Text>
</View>
);
})}
{item.labels.length > 2 ? (
<Text className={cn("shrink-0 text-2xs", tertiaryForeground)}>
+{item.labels.length - 2}
</Text>
) : null}
</View>
) : null}
{item.additions || item.deletions ? (
<PullRequestDiffStats
additions={item.additions}
deletions={item.deletions}
compact
/>
) : null}
{item.checksState ? (
<View className="shrink-0" accessibilityLabel={`Checks ${item.checksState}`}>
<SymbolView
name={
item.checksState === "passing"
? "checkmark.circle"
: item.checksState === "failing"
? "xmark.circle.fill"
: "clock"
}
size={13}
tintColorClassName={checkTint}
/>
</View>
) : null}
{review ? (
<View className="shrink-0" accessibilityLabel={review.accessibleLabel}>
<SymbolView name={review.icon} size={13} tintColorClassName={review.tint} />
</View>
) : null}
{item.additions || item.deletions ? (
<PullRequestDiffStats
additions={item.additions}
deletions={item.deletions}
compact
/>
) : null}
</View>
</View>
</View>
</Pressable>
</ControlPillMenu>
</Pressable>
</ControlPillMenu>
</View>
);
},
(previous, next) =>
pullRequestListRowsAreEqual(previous.item, next.item) &&
previous.selected === next.selected &&
previous.sidebar === next.sidebar &&
previous.busy === next.busy &&
previous.onSelect === next.onSelect &&
previous.onAction === next.onAction &&
Expand Down
17 changes: 12 additions & 5 deletions apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ function PullRequestScrollView(props: ScrollViewProps) {
);
}

export function PullRequestsScreen() {
export function PullRequestsScreen(
props: {
readonly sidebar?: boolean;
readonly selectedPullRequestKey?: string | null;
} = {},
) {
const navigation = useNavigation();
const primaryColumn = use(NativePrimaryColumnContext);
const workspace = useWorkspaceTabs()!;
Expand Down Expand Up @@ -135,7 +140,8 @@ export function PullRequestsScreen() {
connectedTargets.map((target) => target.environmentId),
result.updateEntry,
);
const selectedKey = primaryColumn?.selectedPullRequestKey;
const sidebar = props.sidebar === true || primaryColumn !== null;
const selectedKey = props.selectedPullRequestKey ?? primaryColumn?.selectedPullRequestKey;
const entries = useMemo(
() =>
filterPullRequestList(
Expand Down Expand Up @@ -179,9 +185,10 @@ export function PullRequestsScreen() {
onAction={select}
busy={busyKey === pullRequestKey(item)}
selected={selectedKey === pullRequestKey(item)}
sidebar={sidebar}
/>
),
[selectPullRequest, selectedKey, actions, select, busyKey],
[selectPullRequest, selectedKey, actions, select, busyKey, sidebar],
);
const previousSearchMode = useRef(tab.searchMode);
useEffect(() => {
Expand All @@ -199,11 +206,11 @@ export function PullRequestsScreen() {
: -1;

return (
<View className={primaryColumn ? "flex-1 bg-drawer" : "flex-1 bg-screen"}>
<View className={sidebar ? "flex-1 bg-drawer" : "flex-1 bg-screen"}>
<LegendList
ref={listRef}
data={entries}
extraData={`${primaryColumn?.selectedPullRequestKey}:${busyKey}`}
extraData={`${selectedKey}:${busyKey}`}
keyExtractor={pullRequestKey}
itemsAreEqual={pullRequestListRowsAreEqual}
// Default-size rows measure about 78pt with one title line and 98pt with two.
Expand Down
Loading
Loading