authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-09 06:29:55-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-09 15:29:55+02:00
log8619ef22e9a173678b1ed2d00eae99b329a7c6b6
tree37c1079f72e3897cff9f621ceb21a1b7c4135c41
parent843cf176eae729838b6a2cc8c68cbdadda667d95
signature Signed by PGP key B5690EEEBB952194

Show compact PR number badges in mobile thread rows (#3827)

Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 132 insertions(+), 40 deletions(-)

apps/mobile/src/features/threads/thread-list-items.tsx+54-8
......@@ -6,8 +6,9 @@ import type {
66import type { MenuAction } from "@react-native-menu/menu";
77import { SymbolView } from "expo-symbols";
88import { memo, useCallback, useMemo, type ComponentProps } from "react";
9import { Pressable, useWindowDimensions, View } from "react-native";
9import { Pressable, useColorScheme, useWindowDimensions, View } from "react-native";
1010import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";
11import Svg, { Circle, Path } from "react-native-svg";
1112
1213import { AppText as Text } from "../../components/AppText";
1314import { ControlPillMenu } from "../../components/ControlPill";
......@@ -16,7 +17,7 @@ import { cn } from "../../lib/cn";
1617import { relativeTime } from "../../lib/time";
1718import { useThemeColor } from "../../lib/useThemeColor";
1819import type { PendingNewTask } from "../../state/use-pending-new-tasks";
19import { useThreadPr } from "../../state/use-thread-pr";
20import { useThreadPr, type ThreadPr } from "../../state/use-thread-pr";
2021import type { HomeGroupDisplayAction } from "../home/homeListItems";
2122import { ThreadSwipeable } from "../home/thread-swipe-actions";
2223import { resolveThreadStatus } from "./threadPresentation";
......@@ -33,6 +34,41 @@ export type ThreadListVariant = "compact" | "sidebar";
3334export const THREAD_LIST_COMPACT_INSET = 20;
3435const SIDEBAR_ROW_RADIUS = 12;
3536
37function 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
52function 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
3874export 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;
apps/mobile/src/state/thread-pr-presentation.ts created+36
......@@ -0,0 +1,36 @@
1import type { VcsStatusResult } from "@t3tools/contracts";
2import { resolveChangeRequestPresentation } from "@t3tools/shared/sourceControl";
3
4export type ThreadPr = NonNullable<VcsStatusResult["pr"]>;
5
6export 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
17const 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
23export 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}
apps/mobile/src/state/use-thread-pr.test.ts created+36
......@@ -0,0 +1,36 @@
1import type { VcsStatusResult } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import { presentThreadPr } from "./thread-pr-presentation";
5
6const 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
15describe("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+6-32
......@@ -1,40 +1,14 @@
11import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
2import type { VcsStatusResult } from "@t3tools/contracts";
3import { resolveChangeRequestPresentation } from "@t3tools/shared/sourceControl";
42
53import { useEnvironmentQuery } from "./query";
4import { presentThreadPr, type ThreadPrPresentation } from "./thread-pr-presentation";
65import { vcsEnvironment } from "./vcs";
76
8export type ThreadPr = NonNullable<VcsStatusResult["pr"]>;
9
10export 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
19const 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
25export 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}
7export {
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