Skip to content

Commit 8619ef2

Browse files
Show compact PR number badges in mobile thread rows (#3827)
Co-authored-by: codex <codex@users.noreply.github.com>
1 parent 843cf17 commit 8619ef2

4 files changed

Lines changed: 132 additions & 40 deletions

File tree

apps/mobile/src/features/threads/thread-list-items.tsx

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import type {
66
import type { MenuAction } from "@react-native-menu/menu";
77
import { SymbolView } from "expo-symbols";
88
import { memo, useCallback, useMemo, type ComponentProps } from "react";
9-
import { Pressable, useWindowDimensions, View } from "react-native";
9+
import { Pressable, useColorScheme, useWindowDimensions, View } from "react-native";
1010
import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";
11+
import Svg, { Circle, Path } from "react-native-svg";
1112

1213
import { AppText as Text } from "../../components/AppText";
1314
import { ControlPillMenu } from "../../components/ControlPill";
@@ -16,7 +17,7 @@ import { cn } from "../../lib/cn";
1617
import { relativeTime } from "../../lib/time";
1718
import { useThemeColor } from "../../lib/useThemeColor";
1819
import type { PendingNewTask } from "../../state/use-pending-new-tasks";
19-
import { useThreadPr } from "../../state/use-thread-pr";
20+
import { useThreadPr, type ThreadPr } from "../../state/use-thread-pr";
2021
import type { HomeGroupDisplayAction } from "../home/homeListItems";
2122
import { ThreadSwipeable } from "../home/thread-swipe-actions";
2223
import { resolveThreadStatus } from "./threadPresentation";
@@ -33,6 +34,41 @@ export type ThreadListVariant = "compact" | "sidebar";
3334
export const THREAD_LIST_COMPACT_INSET = 20;
3435
const SIDEBAR_ROW_RADIUS = 12;
3536

37+
function pullRequestTintColor(
38+
state: ThreadPr["state"],
39+
colorScheme: ReturnType<typeof useColorScheme>,
40+
) {
41+
const dark = colorScheme === "dark";
42+
switch (state) {
43+
case "open":
44+
return dark ? "#34d399" : "#059669";
45+
case "merged":
46+
return dark ? "#a78bfa" : "#7c3aed";
47+
case "closed":
48+
return dark ? "#a1a1aa" : "#71717a";
49+
}
50+
}
51+
52+
function PullRequestIcon(props: { readonly size: number; readonly color: string }) {
53+
return (
54+
<Svg
55+
width={props.size}
56+
height={props.size}
57+
viewBox="0 0 24 24"
58+
fill="none"
59+
stroke={props.color}
60+
strokeWidth={2}
61+
strokeLinecap="round"
62+
strokeLinejoin="round"
63+
>
64+
<Circle cx={18} cy={18} r={3} />
65+
<Circle cx={6} cy={6} r={3} />
66+
<Path d="M13 6h3a2 2 0 0 1 2 2v7" />
67+
<Path d="M6 9v12" />
68+
</Svg>
69+
);
70+
}
71+
3672
/* ─── Project group header ───────────────────────────────────────────── */
3773

3874
export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props: {
@@ -394,6 +430,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
394430
>["simultaneousWithExternalGesture"];
395431
}) {
396432
const { width: windowWidth } = useWindowDimensions();
433+
const colorScheme = useColorScheme();
397434
const compact = props.variant === "compact";
398435
const selected = props.selected === true;
399436
// Recycling-safe: resets when the list container is reused for another
@@ -479,13 +516,22 @@ export const ThreadListRow = memo(function ThreadListRow(props: {
479516
</>
480517
) : null}
481518
{pr !== null ? (
482-
<Text
483-
className={`${compact ? "text-sm" : "text-xs"} font-t3-medium ${
484-
selected ? "text-white" : pr.textClassName
485-
}`}
519+
<View
520+
accessibilityLabel={pr.accessibilityLabel}
521+
className="flex-row items-center gap-0.5"
486522
>
487-
{pr.label}
488-
</Text>
523+
<PullRequestIcon
524+
size={compact ? 13 : 11}
525+
color={selected ? "#ffffff" : pullRequestTintColor(pr.state, colorScheme)}
526+
/>
527+
<Text
528+
className={`${compact ? "text-sm" : "text-xs"} font-t3-medium ${
529+
selected ? "text-white" : pr.textClassName
530+
}`}
531+
>
532+
{pr.label}
533+
</Text>
534+
</View>
489535
) : null}
490536
</View>
491537
) : null;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { VcsStatusResult } from "@t3tools/contracts";
2+
import { resolveChangeRequestPresentation } from "@t3tools/shared/sourceControl";
3+
4+
export type ThreadPr = NonNullable<VcsStatusResult["pr"]>;
5+
6+
export interface ThreadPrPresentation {
7+
readonly number: number;
8+
readonly state: ThreadPr["state"];
9+
readonly url: string;
10+
/** Compact desktop-style label, e.g. "#3774". */
11+
readonly label: string;
12+
/** Full, provider-aware label for assistive technologies. */
13+
readonly accessibilityLabel: string;
14+
readonly textClassName: string;
15+
}
16+
17+
const PR_STATE_TEXT_CLASS: Record<ThreadPr["state"], string> = {
18+
open: "text-emerald-600 dark:text-emerald-400",
19+
merged: "text-violet-600 dark:text-violet-400",
20+
closed: "text-zinc-500 dark:text-zinc-400",
21+
};
22+
23+
export function presentThreadPr(
24+
pr: ThreadPr,
25+
provider: VcsStatusResult["sourceControlProvider"] | null | undefined,
26+
): ThreadPrPresentation {
27+
const presentation = resolveChangeRequestPresentation(provider);
28+
return {
29+
number: pr.number,
30+
state: pr.state,
31+
url: pr.url,
32+
label: `#${pr.number}`,
33+
accessibilityLabel: `#${pr.number} ${presentation.longName} ${pr.state}`,
34+
textClassName: PR_STATE_TEXT_CLASS[pr.state],
35+
};
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { VcsStatusResult } from "@t3tools/contracts";
2+
import { describe, expect, it } from "vite-plus/test";
3+
4+
import { presentThreadPr } from "./thread-pr-presentation";
5+
6+
const pullRequest: NonNullable<VcsStatusResult["pr"]> = {
7+
number: 3774,
8+
title: "Desktop-style pull request indicator",
9+
url: "https://github.com/t3tools/t3code/pull/3774",
10+
baseRef: "main",
11+
headRef: "codex/desktop-style-pr-indicator",
12+
state: "merged",
13+
};
14+
15+
describe("presentThreadPr", () => {
16+
it("uses the compact desktop-style pull request number label", () => {
17+
expect(presentThreadPr(pullRequest, undefined)).toMatchObject({
18+
label: "#3774",
19+
accessibilityLabel: "#3774 pull request merged",
20+
textClassName: "text-violet-600 dark:text-violet-400",
21+
});
22+
});
23+
24+
it("uses merge-request terminology for GitLab", () => {
25+
expect(
26+
presentThreadPr(pullRequest, {
27+
kind: "gitlab",
28+
name: "GitLab",
29+
baseUrl: "https://gitlab.com",
30+
}),
31+
).toMatchObject({
32+
label: "#3774",
33+
accessibilityLabel: "#3774 merge request merged",
34+
});
35+
});
36+
});

apps/mobile/src/state/use-thread-pr.ts

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,14 @@
11
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
2-
import type { VcsStatusResult } from "@t3tools/contracts";
3-
import { resolveChangeRequestPresentation } from "@t3tools/shared/sourceControl";
42

53
import { useEnvironmentQuery } from "./query";
4+
import { presentThreadPr, type ThreadPrPresentation } from "./thread-pr-presentation";
65
import { vcsEnvironment } from "./vcs";
76

8-
export type ThreadPr = NonNullable<VcsStatusResult["pr"]>;
9-
10-
export interface ThreadPrPresentation {
11-
readonly number: number;
12-
readonly state: ThreadPr["state"];
13-
readonly url: string;
14-
/** Compact chip label, e.g. "PR open" / "MR merged". */
15-
readonly label: string;
16-
readonly textClassName: string;
17-
}
18-
19-
const PR_STATE_TEXT_CLASS: Record<ThreadPr["state"], string> = {
20-
open: "text-emerald-600 dark:text-emerald-400",
21-
merged: "text-violet-600 dark:text-violet-400",
22-
closed: "text-zinc-500 dark:text-zinc-400",
23-
};
24-
25-
export function presentThreadPr(
26-
pr: ThreadPr,
27-
provider: VcsStatusResult["sourceControlProvider"] | null | undefined,
28-
): ThreadPrPresentation {
29-
const shortName = resolveChangeRequestPresentation(provider).shortName;
30-
return {
31-
number: pr.number,
32-
state: pr.state,
33-
url: pr.url,
34-
label: `${shortName} ${pr.state}`,
35-
textClassName: PR_STATE_TEXT_CLASS[pr.state],
36-
};
37-
}
7+
export {
8+
presentThreadPr,
9+
type ThreadPr,
10+
type ThreadPrPresentation,
11+
} from "./thread-pr-presentation";
3812

3913
/**
4014
* Live PR status for a thread's branch. Subscriptions are deduplicated per

0 commit comments

Comments
 (0)