Skip to content
Open
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
64 changes: 29 additions & 35 deletions apps/mobile/src/app/inbox/[...id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import * as Haptics from "expo-haptics";
import { useLocalSearchParams, useRouter } from "expo-router";
import {
CaretDown,
CaretRight,
ChatCircle,
Lightning,
Play,
Expand Down Expand Up @@ -47,6 +46,7 @@ import {
} from "@/features/inbox/components/DismissReportSheet";
import { ReportActivity } from "@/features/inbox/components/ReportActivity";
import { ReportFeedbackFooter } from "@/features/inbox/components/ReportFeedbackFooter";
import { ReportSection } from "@/features/inbox/components/ReportSection";
import { SignalCard } from "@/features/inbox/components/SignalCard";
import {
type ReviewerActionExtra,
Expand Down Expand Up @@ -156,6 +156,7 @@ export default function ReportDetailScreen() {
const [dismissOpen, setDismissOpen] = useState(false);
const [discussOpen, setDiscussOpen] = useState(false);
const [createPrFeedbackOpen, setCreatePrFeedbackOpen] = useState(false);
const [summaryExpanded, setSummaryExpanded] = useState(true);
const [signalsExpanded, setSignalsExpanded] = useState(false);

const artefactsQuery = useInboxReportArtefacts(reportId ?? null);
Expand Down Expand Up @@ -519,11 +520,17 @@ export default function ReportDetailScreen() {

{/* Summary */}
{report.summary && (
<View className="mb-4" style={{ opacity: isReady ? 1 : 0.7 }}>
<MarkdownText
content={formatSignalReportSummaryMarkdown(report.summary)}
/>
</View>
<ReportSection
title="Summary"
expanded={summaryExpanded}
onToggle={() => setSummaryExpanded((prev) => !prev)}
>
<View style={{ opacity: isReady ? 1 : 0.7 }}>
<MarkdownText
content={formatSignalReportSummaryMarkdown(report.summary)}
/>
</View>
</ReportSection>
)}

{/* Suggested reviewers */}
Expand All @@ -538,35 +545,22 @@ export default function ReportDetailScreen() {

{/* Signals */}
{signals.length > 0 && (
<View className="mb-4">
<Pressable
onPress={handleToggleSignals}
hitSlop={6}
accessibilityRole="button"
accessibilityState={{ expanded: signalsExpanded }}
className="mb-2 flex-row items-center gap-1.5 self-start py-1 active:opacity-60"
>
{signalsExpanded ? (
<CaretDown size={14} color={themeColors.gray[12]} />
) : (
<CaretRight size={14} color={themeColors.gray[12]} />
)}
<Text className="font-semibold text-[14px] text-gray-12">
Signals ({signals.length})
</Text>
</Pressable>
{signalsExpanded && (
<View className="gap-2">
{signals.map((signal) => (
<SignalCard
key={signal.signal_id}
signal={signal}
finding={findingsBySignalId.get(signal.signal_id)}
/>
))}
</View>
)}
</View>
<ReportSection
title="Signals"
count={signals.length}
expanded={signalsExpanded}
onToggle={handleToggleSignals}
>
<View className="gap-2">
{signals.map((signal) => (
<SignalCard
key={signal.signal_id}
signal={signal}
finding={findingsBySignalId.get(signal.signal_id)}
/>
))}
</View>
</ReportSection>
)}
{signalsQuery.isLoading && (
<Text className="text-[12px] text-gray-9">Loading signals…</Text>
Expand Down
70 changes: 70 additions & 0 deletions apps/mobile/src/features/inbox/components/ReportSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Text } from "@components/text";
import { CaretDown, CaretRight } from "phosphor-react-native";
import type { ReactNode } from "react";
import { Pressable, View } from "react-native";
import { useThemeColors } from "@/lib/theme";

interface ReportSectionProps {
title: string;
/** Appended to the title as `(n)` – omit for sections without a count. */
count?: number;
/** Icon rendered before the title (e.g. `<ClockCounterClockwise size={14} />`). */
icon?: ReactNode;
expanded: boolean;
onToggle: () => void;
/**
* Controls rendered at the end of the header row, outside the disclosure so
* they stay independently tappable.
*/
rightSlot?: ReactNode;
children: ReactNode;
}

/**
* Collapsible section on the report detail screen (Summary, Reviewers,
* Signals). Mirrors the desktop `DetailSection` disclosure so the same report
* can be scanned the same way on either host. Expansion state is owned by the
* caller – the Signals disclosure fires analytics when it opens.
*/
export function ReportSection({
title,
count,
icon,
expanded,
onToggle,
rightSlot,
children,
}: ReportSectionProps) {
const themeColors = useThemeColors();

return (
<View className="mb-4">
<View className="mb-2 flex-row items-center gap-2">
<Pressable
onPress={onToggle}
hitSlop={6}
accessibilityRole="button"
accessibilityState={{ expanded }}
className="shrink flex-row items-center gap-1.5 py-1 active:opacity-60"
>
{expanded ? (
<CaretDown size={14} color={themeColors.gray[12]} />
) : (
<CaretRight size={14} color={themeColors.gray[12]} />
)}
{icon}
<Text className="font-semibold text-[14px] text-gray-12">
{count === undefined ? title : `${title} (${count})`}
</Text>
</Pressable>
{rightSlot ? (
<>
<View className="flex-1" />
{rightSlot}
</>
) : null}
</View>
{expanded ? children : null}
</View>
);
}
183 changes: 94 additions & 89 deletions apps/mobile/src/features/inbox/components/SuggestedReviewers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { openExternalUrl } from "@/lib/openExternalUrl";
import { useThemeColors } from "@/lib/theme";
import { useUpdateSuggestedReviewers } from "../hooks/useInboxReports";
import { EditReviewersSheet } from "./EditReviewersSheet";
import { ReportSection } from "./ReportSection";

export type ReviewerActionExtra = Pick<
InboxReportActionProperties,
Expand All @@ -50,6 +51,7 @@ export function SuggestedReviewers({
}: SuggestedReviewersProps) {
const themeColors = useThemeColors();
const [editOpen, setEditOpen] = useState(false);
const [expanded, setExpanded] = useState(true);
const { mutate: updateReviewers, isPending } =
useUpdateSuggestedReviewers(reportId);

Expand Down Expand Up @@ -108,96 +110,99 @@ export function SuggestedReviewers({
};

return (
<View className="mb-4">
<View className="mb-2 flex-row items-center gap-2">
<Text className="font-semibold text-[12px] text-gray-10 uppercase tracking-wide">
Suggested reviewers
</Text>
{isPending && (
<ActivityIndicator size="small" color={themeColors.gray[9]} />
)}
<View className="flex-1" />
<Pressable
onPress={() => setEditOpen(true)}
disabled={isPending}
accessibilityLabel="Add suggested reviewer"
hitSlop={6}
className="flex-row items-center gap-1 rounded-full border border-gray-6 px-2.5 py-1 active:opacity-70 disabled:opacity-50"
>
<Plus size={12} color={themeColors.gray[11]} weight="bold" />
<Text className="text-[12px] text-gray-11">Add</Text>
</Pressable>
</View>

{displayReviewers.length === 0 ? (
<Text className="text-[13px] text-gray-9">
No reviewers assigned. Use “Add” to suggest one.
</Text>
) : (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ gap: 8 }}
>
{displayReviewers.map((reviewer) => {
const isMe =
!!reviewer.user?.uuid &&
!!meUuid &&
reviewer.user.uuid === meUuid;
const displayName =
reviewer.user?.first_name ??
reviewer.github_name ??
reviewer.github_login;
return (
<View
key={reviewer.user?.uuid ?? reviewer.github_login}
className="flex-row items-center gap-2 rounded-full border border-gray-6 bg-gray-2 py-1.5 pr-1.5 pl-1.5"
>
<Pressable
onPress={() => {
fireAction("click_suggested_reviewer", {
suggested_reviewer_login: reviewer.github_login,
});
openExternalUrl(
`https://github.com/${reviewer.github_login}`,
);
}}
hitSlop={4}
className="flex-row items-center gap-2 active:opacity-70"
<>
<ReportSection
title="Suggested reviewers"
expanded={expanded}
onToggle={() => setExpanded((prev) => !prev)}
rightSlot={
<View className="flex-row items-center gap-2">
{isPending && (
<ActivityIndicator size="small" color={themeColors.gray[9]} />
)}
<Pressable
onPress={() => setEditOpen(true)}
disabled={isPending}
accessibilityLabel="Add suggested reviewer"
hitSlop={6}
className="flex-row items-center gap-1 rounded-full border border-gray-6 px-2.5 py-1 active:opacity-70 disabled:opacity-50"
>
<Plus size={12} color={themeColors.gray[11]} weight="bold" />
<Text className="text-[12px] text-gray-11">Add</Text>
</Pressable>
</View>
}
>
{displayReviewers.length === 0 ? (
<Text className="text-[13px] text-gray-9">
No reviewers assigned. Use “Add” to suggest one.
</Text>
) : (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ gap: 8 }}
>
{displayReviewers.map((reviewer) => {
const isMe =
!!reviewer.user?.uuid &&
!!meUuid &&
reviewer.user.uuid === meUuid;
const displayName =
reviewer.user?.first_name ??
reviewer.github_name ??
reviewer.github_login;
return (
<View
key={reviewer.user?.uuid ?? reviewer.github_login}
className="flex-row items-center gap-2 rounded-full border border-gray-6 bg-gray-2 py-1.5 pr-1.5 pl-1.5"
>
<Image
source={{
uri: `https://github.com/${reviewer.github_login}.png?size=48`,
<Pressable
onPress={() => {
fireAction("click_suggested_reviewer", {
suggested_reviewer_login: reviewer.github_login,
});
openExternalUrl(
`https://github.com/${reviewer.github_login}`,
);
}}
className="h-6 w-6 rounded-full bg-gray-4"
/>
<Text className="text-[13px] text-gray-12">
{displayName}
</Text>
{isMe && (
<View className="rounded bg-status-warning/20 px-1 py-0.5">
<Eye
size={10}
color={themeColors.status.warning}
weight="bold"
/>
</View>
)}
</Pressable>
<Pressable
onPress={() => removeReviewer(reviewer)}
disabled={isPending}
accessibilityLabel={`Remove ${displayName}`}
hitSlop={6}
className="h-5 w-5 items-center justify-center rounded-full active:bg-gray-4 disabled:opacity-50"
>
<X size={12} color={themeColors.gray[10]} weight="bold" />
</Pressable>
</View>
);
})}
</ScrollView>
)}
hitSlop={4}
className="flex-row items-center gap-2 active:opacity-70"
>
<Image
source={{
uri: `https://github.com/${reviewer.github_login}.png?size=48`,
}}
className="h-6 w-6 rounded-full bg-gray-4"
/>
<Text className="text-[13px] text-gray-12">
{displayName}
</Text>
{isMe && (
<View className="rounded bg-status-warning/20 px-1 py-0.5">
<Eye
size={10}
color={themeColors.status.warning}
weight="bold"
/>
</View>
)}
</Pressable>
<Pressable
onPress={() => removeReviewer(reviewer)}
disabled={isPending}
accessibilityLabel={`Remove ${displayName}`}
hitSlop={6}
className="h-5 w-5 items-center justify-center rounded-full active:bg-gray-4 disabled:opacity-50"
>
<X size={12} color={themeColors.gray[10]} weight="bold" />
</Pressable>
</View>
);
})}
</ScrollView>
)}
</ReportSection>

<EditReviewersSheet
visible={editOpen}
Expand All @@ -206,6 +211,6 @@ export function SuggestedReviewers({
onClose={() => setEditOpen(false)}
onToggle={toggleReviewer}
/>
</View>
</>
);
}
2 changes: 1 addition & 1 deletion packages/ui/src/features/inbox/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ The current UI is single-column, route-based, and card/list oriented. Do not rei
Shared primitives exist to keep the surfaces consistent:

- `InboxDetailPageHeader` for detail headers.
- `DetailSection` for content sections inside detail screens.
- `DetailSection` for main-column content sections inside detail screens, and `RightColumnSection` for the slimmer supporting sections beside them. Both take `collapsible` (plus `defaultCollapsed`) to turn the header into a disclosure button; `rightSlot` stays outside that button so its own controls remain clickable. Mobile's equivalent is `apps/mobile/src/features/inbox/components/ReportSection.tsx`.
- `SignalsList` and the existing detail `SignalCard` for contributing signals.
- Badge and metadata helpers in `components/utils/` and `InboxMetaRow`.
- `SOURCE_PRODUCT_META` for source-product labels and icons.
Expand Down
Loading
Loading