From e720e34dd119d0e33334573cc0fc369496bad4d1 Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Mon, 1 Jun 2026 11:37:16 +0100 Subject: [PATCH 1/5] feat(mobile): highlight and surface current user in suggested reviewers (port #2435) Ports desktop PR #2435 to the mobile inbox report detail. - Wire the current user's uuid (via useUserQuery) into SuggestedReviewers so the existing isMe highlight shows. - Move the logged-in user to the front of the suggested-reviewers list when present and not already first, via a pure orderSuggestedReviewers util. - Add unit tests covering reorder, already-first no-op, absent user, and missing uuid. Generated-By: PostHog Code Task-Id: 6570d0da-5363-4355-94b2-4576776d02aa --- apps/mobile/src/app/inbox/[id].tsx | 17 +++++-- apps/mobile/src/features/inbox/utils.test.ts | 50 ++++++++++++++++++++ apps/mobile/src/features/inbox/utils.ts | 11 +++++ 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 apps/mobile/src/features/inbox/utils.test.ts diff --git a/apps/mobile/src/app/inbox/[id].tsx b/apps/mobile/src/app/inbox/[id].tsx index 05808e6277..e95bee4e6e 100644 --- a/apps/mobile/src/app/inbox/[id].tsx +++ b/apps/mobile/src/app/inbox/[id].tsx @@ -16,6 +16,7 @@ import { usePostHog } from "posthog-react-native"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ActivityIndicator, Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useUserQuery } from "@/features/auth/hooks/useUserQuery"; import { MarkdownText } from "@/features/chat/components/MarkdownText"; import { getReportRepository } from "@/features/inbox/api"; import { DiscussReportSheet } from "@/features/inbox/components/DiscussReportSheet"; @@ -34,7 +35,10 @@ import type { SignalReportStatus, SuggestedReviewer, } from "@/features/inbox/types"; -import { inboxStatusLabel } from "@/features/inbox/utils"; +import { + inboxStatusLabel, + orderSuggestedReviewers, +} from "@/features/inbox/utils"; import { useThemeColors } from "@/lib/theme"; const statusColorMap: Record = { @@ -114,6 +118,7 @@ export default function ReportDetailScreen() { const insets = useSafeAreaInsets(); const posthog = usePostHog(); const { data: report, isLoading, error } = useInboxReport(reportId ?? null); + const { data: user } = useUserQuery(); const [reportRepo, setReportRepo] = useState(null); const [dismissOpen, setDismissOpen] = useState(false); const [discussOpen, setDiscussOpen] = useState(false); @@ -151,11 +156,12 @@ export default function ReportDetailScreen() { const suggestedReviewers = useMemo((): SuggestedReviewer[] => { for (const a of artefacts) { if (a.type === "suggested_reviewers") { - return (a.content as SuggestedReviewer[]) ?? []; + const reviewers = (a.content as SuggestedReviewer[]) ?? []; + return orderSuggestedReviewers(reviewers, user?.uuid); } } return []; - }, [artefacts]); + }, [artefacts, user?.uuid]); const findingsBySignalId = useMemo(() => { const map = new Map(); @@ -358,7 +364,10 @@ export default function ReportDetailScreen() { )} {/* Suggested reviewers */} - + {/* Signals */} {signals.length > 0 && ( diff --git a/apps/mobile/src/features/inbox/utils.test.ts b/apps/mobile/src/features/inbox/utils.test.ts new file mode 100644 index 0000000000..9836d11ae5 --- /dev/null +++ b/apps/mobile/src/features/inbox/utils.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import type { SuggestedReviewer } from "./types"; +import { orderSuggestedReviewers } from "./utils"; + +function reviewer(login: string, uuid?: string): SuggestedReviewer { + return { + github_login: login, + github_name: login, + relevant_commits: [], + user: uuid + ? { + id: 1, + uuid, + email: `${login}@posthog.com`, + first_name: login, + last_name: "", + } + : null, + }; +} + +describe("orderSuggestedReviewers", () => { + it("moves the current user to the front", () => { + const reviewers = [ + reviewer("a", "uuid-a"), + reviewer("me", "uuid-me"), + reviewer("c", "uuid-c"), + ]; + const ordered = orderSuggestedReviewers(reviewers, "uuid-me"); + expect(ordered.map((r) => r.github_login)).toEqual(["me", "a", "c"]); + }); + + it("is a no-op when the current user is already first", () => { + const reviewers = [reviewer("me", "uuid-me"), reviewer("a", "uuid-a")]; + const ordered = orderSuggestedReviewers(reviewers, "uuid-me"); + expect(ordered).toBe(reviewers); + }); + + it("is a no-op when the current user is absent", () => { + const reviewers = [reviewer("a", "uuid-a"), reviewer("b", "uuid-b")]; + const ordered = orderSuggestedReviewers(reviewers, "uuid-me"); + expect(ordered).toBe(reviewers); + }); + + it("is a no-op when meUuid is missing", () => { + const reviewers = [reviewer("a", "uuid-a"), reviewer("me", "uuid-me")]; + expect(orderSuggestedReviewers(reviewers, null)).toBe(reviewers); + expect(orderSuggestedReviewers(reviewers, undefined)).toBe(reviewers); + }); +}); diff --git a/apps/mobile/src/features/inbox/utils.ts b/apps/mobile/src/features/inbox/utils.ts index 588978b49f..41ee173117 100644 --- a/apps/mobile/src/features/inbox/utils.ts +++ b/apps/mobile/src/features/inbox/utils.ts @@ -2,6 +2,7 @@ import type { SignalReport, SignalReportOrderingField, SignalReportStatus, + SuggestedReviewer, } from "./types"; export function inboxStatusLabel(status: SignalReportStatus): string { @@ -87,3 +88,13 @@ export function getActionableReports(reports: SignalReport[]): SignalReport[] { !r.already_addressed, ); } + +export function orderSuggestedReviewers( + reviewers: SuggestedReviewer[], + meUuid: string | null | undefined, +): SuggestedReviewer[] { + if (!meUuid) return reviewers; + const meIndex = reviewers.findIndex((r) => r.user?.uuid === meUuid); + if (meIndex <= 0) return reviewers; + return [reviewers[meIndex], ...reviewers.filter((_, i) => i !== meIndex)]; +} From d37169710926cb433cef9d8c638c4f441f6ec866 Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Mon, 1 Jun 2026 11:53:09 +0100 Subject: [PATCH 2/5] test(mobile): parameterise suggested-reviewer no-op cases with it.each Generated-By: PostHog Code Task-Id: 6570d0da-5363-4355-94b2-4576776d02aa --- apps/mobile/src/features/inbox/utils.test.ts | 39 ++++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/apps/mobile/src/features/inbox/utils.test.ts b/apps/mobile/src/features/inbox/utils.test.ts index 9836d11ae5..a67748efac 100644 --- a/apps/mobile/src/features/inbox/utils.test.ts +++ b/apps/mobile/src/features/inbox/utils.test.ts @@ -30,21 +30,28 @@ describe("orderSuggestedReviewers", () => { expect(ordered.map((r) => r.github_login)).toEqual(["me", "a", "c"]); }); - it("is a no-op when the current user is already first", () => { - const reviewers = [reviewer("me", "uuid-me"), reviewer("a", "uuid-a")]; - const ordered = orderSuggestedReviewers(reviewers, "uuid-me"); - expect(ordered).toBe(reviewers); - }); - - it("is a no-op when the current user is absent", () => { - const reviewers = [reviewer("a", "uuid-a"), reviewer("b", "uuid-b")]; - const ordered = orderSuggestedReviewers(reviewers, "uuid-me"); - expect(ordered).toBe(reviewers); - }); - - it("is a no-op when meUuid is missing", () => { - const reviewers = [reviewer("a", "uuid-a"), reviewer("me", "uuid-me")]; - expect(orderSuggestedReviewers(reviewers, null)).toBe(reviewers); - expect(orderSuggestedReviewers(reviewers, undefined)).toBe(reviewers); + it.each([ + { + label: "already first", + reviewers: [reviewer("me", "uuid-me"), reviewer("a", "uuid-a")], + meUuid: "uuid-me" as string | null | undefined, + }, + { + label: "absent", + reviewers: [reviewer("a", "uuid-a"), reviewer("b", "uuid-b")], + meUuid: "uuid-me" as string | null | undefined, + }, + { + label: "null meUuid", + reviewers: [reviewer("a", "uuid-a"), reviewer("me", "uuid-me")], + meUuid: null as string | null | undefined, + }, + { + label: "undefined meUuid", + reviewers: [reviewer("a", "uuid-a"), reviewer("me", "uuid-me")], + meUuid: undefined as string | null | undefined, + }, + ])("is a no-op when $label", ({ reviewers, meUuid }) => { + expect(orderSuggestedReviewers(reviewers, meUuid)).toBe(reviewers); }); }); From ffff6266e4f8dce5a285972b6562f3f016e34389 Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Tue, 2 Jun 2026 14:53:21 +0100 Subject: [PATCH 3/5] fix(mobile): rename inbox route to match base branch catch-all rename Resolves the modify/rename conflict with main, which renamed inbox/[id].tsx to inbox/[...id].tsx. Generated-By: PostHog Code Task-Id: f65eb1e6-78ba-4c40-8077-b4cd78485c93 --- apps/mobile/src/app/inbox/{[id].tsx => [...id].tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/mobile/src/app/inbox/{[id].tsx => [...id].tsx} (100%) diff --git a/apps/mobile/src/app/inbox/[id].tsx b/apps/mobile/src/app/inbox/[...id].tsx similarity index 100% rename from apps/mobile/src/app/inbox/[id].tsx rename to apps/mobile/src/app/inbox/[...id].tsx From ff52fad9e1f0ea0c060ef4eb9c2b537e22b82eb3 Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Tue, 2 Jun 2026 14:54:17 +0100 Subject: [PATCH 4/5] chore(mobile): align inbox route file with base before merge Temporarily matches main's inbox/[...id].tsx so the base branch merges cleanly; PR feature edits are re-applied after the merge. Generated-By: PostHog Code Task-Id: f65eb1e6-78ba-4c40-8077-b4cd78485c93 --- apps/mobile/src/app/inbox/[...id].tsx | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/apps/mobile/src/app/inbox/[...id].tsx b/apps/mobile/src/app/inbox/[...id].tsx index e95bee4e6e..5da1282c78 100644 --- a/apps/mobile/src/app/inbox/[...id].tsx +++ b/apps/mobile/src/app/inbox/[...id].tsx @@ -16,7 +16,6 @@ import { usePostHog } from "posthog-react-native"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ActivityIndicator, Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { useUserQuery } from "@/features/auth/hooks/useUserQuery"; import { MarkdownText } from "@/features/chat/components/MarkdownText"; import { getReportRepository } from "@/features/inbox/api"; import { DiscussReportSheet } from "@/features/inbox/components/DiscussReportSheet"; @@ -35,10 +34,7 @@ import type { SignalReportStatus, SuggestedReviewer, } from "@/features/inbox/types"; -import { - inboxStatusLabel, - orderSuggestedReviewers, -} from "@/features/inbox/utils"; +import { inboxStatusLabel } from "@/features/inbox/utils"; import { useThemeColors } from "@/lib/theme"; const statusColorMap: Record = { @@ -112,13 +108,18 @@ function ActionabilityBadge({ value }: { value: string }) { } export default function ReportDetailScreen() { - const { id: reportId } = useLocalSearchParams<{ id: string }>(); + // Catch-all route: `id` arrives as string[] for `/inbox//` and + // we only read the first segment (the UUID). The slug is purely cosmetic; + // receivers ignore everything past the UUID, matching the desktop contract + // in `apps/code/src/shared/deeplink.ts`. Expo-router can hand us either a + // string or string[] depending on the URL shape, so tolerate both. + const { id: idParam } = useLocalSearchParams<{ id: string | string[] }>(); + const reportId = Array.isArray(idParam) ? idParam[0] : idParam; const router = useRouter(); const themeColors = useThemeColors(); const insets = useSafeAreaInsets(); const posthog = usePostHog(); const { data: report, isLoading, error } = useInboxReport(reportId ?? null); - const { data: user } = useUserQuery(); const [reportRepo, setReportRepo] = useState(null); const [dismissOpen, setDismissOpen] = useState(false); const [discussOpen, setDiscussOpen] = useState(false); @@ -156,12 +157,11 @@ export default function ReportDetailScreen() { const suggestedReviewers = useMemo((): SuggestedReviewer[] => { for (const a of artefacts) { if (a.type === "suggested_reviewers") { - const reviewers = (a.content as SuggestedReviewer[]) ?? []; - return orderSuggestedReviewers(reviewers, user?.uuid); + return (a.content as SuggestedReviewer[]) ?? []; } } return []; - }, [artefacts, user?.uuid]); + }, [artefacts]); const findingsBySignalId = useMemo(() => { const map = new Map(); @@ -364,10 +364,7 @@ export default function ReportDetailScreen() { )} {/* Suggested reviewers */} - + {/* Signals */} {signals.length > 0 && ( @@ -464,6 +461,7 @@ export default function ReportDetailScreen() { setDiscussOpen(false)} onSubmit={handleDiscussSubmit} /> From 7e455e65816840d7c9dc6457ccae917e7c1d058b Mon Sep 17 00:00:00 2001 From: Tom Owers Date: Tue, 2 Jun 2026 14:54:43 +0100 Subject: [PATCH 5/5] feat(mobile): re-apply suggested-reviewer ordering after base merge Re-applies the current-user highlighting/ordering edits to inbox/[...id].tsx on top of the merged base. Generated-By: PostHog Code Task-Id: f65eb1e6-78ba-4c40-8077-b4cd78485c93 --- apps/mobile/src/app/inbox/[...id].tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/app/inbox/[...id].tsx b/apps/mobile/src/app/inbox/[...id].tsx index 5da1282c78..d68ae5b0ce 100644 --- a/apps/mobile/src/app/inbox/[...id].tsx +++ b/apps/mobile/src/app/inbox/[...id].tsx @@ -16,6 +16,7 @@ import { usePostHog } from "posthog-react-native"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ActivityIndicator, Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useUserQuery } from "@/features/auth/hooks/useUserQuery"; import { MarkdownText } from "@/features/chat/components/MarkdownText"; import { getReportRepository } from "@/features/inbox/api"; import { DiscussReportSheet } from "@/features/inbox/components/DiscussReportSheet"; @@ -34,7 +35,10 @@ import type { SignalReportStatus, SuggestedReviewer, } from "@/features/inbox/types"; -import { inboxStatusLabel } from "@/features/inbox/utils"; +import { + inboxStatusLabel, + orderSuggestedReviewers, +} from "@/features/inbox/utils"; import { useThemeColors } from "@/lib/theme"; const statusColorMap: Record = { @@ -120,6 +124,7 @@ export default function ReportDetailScreen() { const insets = useSafeAreaInsets(); const posthog = usePostHog(); const { data: report, isLoading, error } = useInboxReport(reportId ?? null); + const { data: user } = useUserQuery(); const [reportRepo, setReportRepo] = useState(null); const [dismissOpen, setDismissOpen] = useState(false); const [discussOpen, setDiscussOpen] = useState(false); @@ -157,11 +162,12 @@ export default function ReportDetailScreen() { const suggestedReviewers = useMemo((): SuggestedReviewer[] => { for (const a of artefacts) { if (a.type === "suggested_reviewers") { - return (a.content as SuggestedReviewer[]) ?? []; + const reviewers = (a.content as SuggestedReviewer[]) ?? []; + return orderSuggestedReviewers(reviewers, user?.uuid); } } return []; - }, [artefacts]); + }, [artefacts, user?.uuid]); const findingsBySignalId = useMemo(() => { const map = new Map(); @@ -364,7 +370,10 @@ export default function ReportDetailScreen() { )} {/* Suggested reviewers */} - + {/* Signals */} {signals.length > 0 && (