From dac109cce1af95d9132c538f53eb0b22d1f8eaed Mon Sep 17 00:00:00 2001 From: wSedlacek Date: Fri, 6 Nov 2026 22:52:22 -0800 Subject: [PATCH 1/2] fix(mobile): thread list rows always open the thread Tapping the PR badge used to jump straight into the PR. The whole row is now one touch target for the thread. --- apps/mobile/src/features/home/HomeScreen.tsx | 11 ------ .../workspace/WorkspaceThreadSidebar.tsx | 3 -- .../threads/ThreadNavigationSidebar.tsx | 15 +------- .../features/threads/thread-list-v2-items.tsx | 35 +++---------------- .../threads/use-open-thread-pull-requests.ts | 34 ------------------ .../src/state/thread-pr-presentation.ts | 6 ---- 6 files changed, 5 insertions(+), 99 deletions(-) delete mode 100644 apps/mobile/src/features/threads/use-open-thread-pull-requests.ts diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index dac883341cc10..233878f4b6540 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -69,8 +69,6 @@ import { ThreadListV2WorkingShelfHeader, } from "../threads/thread-list-v2-items"; import { useThreadRowProviderInstanceResolver } from "../threads/thread-provider-instance"; -import { useOpenThreadPullRequests } from "../threads/use-open-thread-pull-requests"; -import type { ThreadPrPresentation } from "../../state/thread-pr-presentation"; import { threadListV2ListItemsAreEqual, THREAD_LIST_V2_SETTLED_INITIAL_COUNT, @@ -311,14 +309,6 @@ export function HomeScreen(props: HomeScreenProps) { const handleScrollBeginDrag = () => { openSwipeableRef.current?.close(); }; - const openThreadPullRequests = useOpenThreadPullRequests(); - const handleOpenThreadPullRequests = ( - thread: EnvironmentThreadShell, - pr: ThreadPrPresentation, - ) => { - openSwipeableRef.current?.close(); - openThreadPullRequests(thread, pr); - }; const onFabScroll = useFabScroll(); const listRef = useRef(null); const searchMode = workspace?.state.tabs.threads.searchMode; @@ -609,7 +599,6 @@ export function HomeScreen(props: HomeScreenProps) { } searchQuery={threadSearch.query} onSelectThread={selectThread} - onOpenThreadPullRequests={handleOpenThreadPullRequests} onDeleteThread={handleDeleteThread} onArchiveThread={props.onArchiveThread} onRenameThread={handleRenameThread} diff --git a/apps/mobile/src/features/layout/workspace/WorkspaceThreadSidebar.tsx b/apps/mobile/src/features/layout/workspace/WorkspaceThreadSidebar.tsx index f2602bd596017..13984be51abc0 100644 --- a/apps/mobile/src/features/layout/workspace/WorkspaceThreadSidebar.tsx +++ b/apps/mobile/src/features/layout/workspace/WorkspaceThreadSidebar.tsx @@ -11,7 +11,6 @@ import { usePendingTaskListActions } from "../../home/usePendingTaskListActions" import { useAdaptiveWorkspaceLayout } from "../adaptive-workspace-context"; import { openSettings } from "../../settings/settings-navigation"; import { ThreadNavigationSidebar } from "../../threads/ThreadNavigationSidebar"; -import { useOpenThreadPullRequests } from "../../threads/use-open-thread-pull-requests"; import { useSelectWorkspaceDetail } from "./useSelectWorkspaceDetail"; type Route = { readonly name: string; readonly params?: object }; @@ -51,7 +50,6 @@ export function WorkspaceThreadSidebar() { openWorkspaceTab, } = useAdaptiveWorkspaceLayout(); const selectDetail = useSelectWorkspaceDetail(); - const openThreadPullRequests = useOpenThreadPullRequests(); const selectedThread = useNavigationState(selectedThreadKey); const selectThreadRoute = (params: ReactNavigation.RootParamList["Thread"]) => selectDetail({ name: "Thread", params }); @@ -92,7 +90,6 @@ export function WorkspaceThreadSidebar() { threadId: String(thread.id), }) } - onOpenThreadPullRequests={openThreadPullRequests} onSearchQueryChange={setPrimarySidebarSearchQuery} searchQuery={primarySidebarSearchQuery} /> diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index dce389c836669..d58fc1adfe94b 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -59,7 +59,6 @@ import { ThreadListV2WorkingShelfHeader, } from "./thread-list-v2-items"; import { useThreadRowProviderInstanceResolver } from "./thread-provider-instance"; -import type { ThreadPrPresentation } from "../../state/thread-pr-presentation"; import { ThreadPrDemandProvider } from "../../state/thread-pr-demand"; import { isThreadListV2ListItem, @@ -91,10 +90,6 @@ interface ThreadNavigationSidebarProps { readonly onOpenPendingTask: (pendingTask: PendingNewTask) => void; readonly onSearchQueryChange: (query: string) => void; readonly onSelectThread: (thread: EnvironmentThreadShell) => void; - readonly onOpenThreadPullRequests: ( - thread: EnvironmentThreadShell, - pr: ThreadPrPresentation, - ) => void; readonly onRequestVisibility: () => void; readonly searchQuery: string; } @@ -315,7 +310,7 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { } }; // Destructure callback props; capturing all props would invalidate these row-closing handlers on unrelated changes. - const { onSelectThread, onOpenThreadPullRequests } = props; + const { onSelectThread } = props; // A row listed for a message match opens the thread at that message, by // tap or by shortcut. const handleSelectThread = (thread: EnvironmentThreadShell) => { @@ -329,13 +324,6 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { onSelectThread(thread); openSwipeableRef.current?.close(); }; - const handleOpenThreadPullRequests = ( - thread: EnvironmentThreadShell, - pr: ThreadPrPresentation, - ) => { - onOpenThreadPullRequests(thread, pr); - openSwipeableRef.current?.close(); - }; const handleScrollBeginDrag = () => { openSwipeableRef.current?.close(); }; @@ -447,7 +435,6 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { } fullSwipeWidth={props.width - 20} onSelectThread={handleSelectThread} - onOpenThreadPullRequests={handleOpenThreadPullRequests} onDeleteThread={confirmDeleteThread} onArchiveThread={archiveThread} onRenameThread={renameThread} diff --git a/apps/mobile/src/features/threads/thread-list-v2-items.tsx b/apps/mobile/src/features/threads/thread-list-v2-items.tsx index c9043a8d6c181..04e65f0235b14 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -54,10 +54,6 @@ import { } from "../../state/pending-new-tasks-model"; import { useBranches } from "../../state/queries"; import { useNewTaskProjectDefaults } from "./use-new-task-project-defaults"; -import { - resolveThreadPrBadgeAction, - type ThreadPrPresentation, -} from "../../state/thread-pr-presentation"; import { useThreadPr } from "../../state/use-thread-pr"; import { useSwipeRowDormant } from "../home/swipe-row-activation"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; @@ -641,10 +637,6 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { readonly selected?: boolean; readonly fullSwipeWidth?: number; readonly onSelectThread: (thread: EnvironmentThreadShell) => void; - readonly onOpenThreadPullRequests: ( - thread: EnvironmentThreadShell, - pr: ThreadPrPresentation, - ) => void; readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; readonly onNewThreadOnBranch: (thread: EnvironmentThreadShell) => void; readonly onRenameThread: (thread: EnvironmentThreadShell) => void; @@ -692,7 +684,6 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { thread, variant, onSelectThread, - onOpenThreadPullRequests, onDeleteThread, onRenameThread, onRegenerateThreadTitle, @@ -726,10 +717,6 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { const providerInstance = props.providerInstance; const pr = useThreadPr(thread); - const prBadgeLabel = pr ? resolveThreadPrBadgeAction(pr).accessibilityLabel : null; - const handleOpenPullRequests = useCallback(() => { - if (pr) onOpenThreadPullRequests(thread, pr); - }, [onOpenThreadPullRequests, pr, thread]); const theme = useUniwindTheme(); const sidebarPane = props.pane === "sidebar"; @@ -759,11 +746,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { }, [props.wokeAt, thread.environmentId, thread.id, visitThread]); // VoiceOver cannot focus buttons inside the accessible row. const rowAccessibilityActions = useMemo( - () => [ - ...(prBadgeLabel ? [{ name: "openPullRequests", label: prBadgeLabel }] : []), - ...(showsWoke ? [{ name: "dismissWoke", label: "Dismiss Woke notification" }] : []), - ], - [prBadgeLabel, showsWoke], + () => (showsWoke ? [{ name: "dismissWoke", label: "Dismiss Woke notification" }] : []), + [showsWoke], ); const handleDelete = useCallback(() => onDeleteThread(thread), [onDeleteThread, thread]); @@ -1298,17 +1282,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { )} {pr ? ( - undefined} - style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })} - > + {pr.label} - + ) : null} {providerInstance ? ( @@ -1383,7 +1357,6 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { accessibilityState={{ selected }} accessibilityActions={rowAccessibilityActions} onAccessibilityAction={(event) => { - if (event.nativeEvent.actionName === "openPullRequests") handleOpenPullRequests(); if (event.nativeEvent.actionName === "dismissWoke") handleAcknowledgeWoke(); }} onPress={() => { diff --git a/apps/mobile/src/features/threads/use-open-thread-pull-requests.ts b/apps/mobile/src/features/threads/use-open-thread-pull-requests.ts deleted file mode 100644 index 034719d5dd19e..0000000000000 --- a/apps/mobile/src/features/threads/use-open-thread-pull-requests.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; -import { useNavigation } from "@react-navigation/native"; - -import * as Dialog from "../../components/ui/Dialog"; -import { useNavigateToPullRequest } from "../../native/useOpenPullRequest"; -import { - resolveThreadPrBadgeAction, - type ThreadPrPresentation, -} from "../../state/thread-pr-presentation"; - -/** Called once per thread list so virtualized rows share one stable handler. */ -export function useOpenThreadPullRequests() { - const navigation = useNavigation(); - const navigateToPullRequest = useNavigateToPullRequest(); - return (thread: EnvironmentThreadShell, pr: ThreadPrPresentation) => { - if (resolveThreadPrBadgeAction(pr).kind === "list") { - navigation.navigate("ThreadPullRequests", { - environmentId: String(thread.environmentId), - threadId: String(thread.id), - }); - return; - } - void navigateToPullRequest(pr.url, { - environmentId: String(thread.environmentId), - projectId: String(thread.projectId), - }).then((opened) => { - if (!opened) - Dialog.alert( - `Unable to open ${pr.changeRequest.shortLabel}`, - `The ${pr.changeRequest.singular} could not be opened.`, - ); - }); - }; -} diff --git a/apps/mobile/src/state/thread-pr-presentation.ts b/apps/mobile/src/state/thread-pr-presentation.ts index a44f410b54ca3..236058e61b61e 100644 --- a/apps/mobile/src/state/thread-pr-presentation.ts +++ b/apps/mobile/src/state/thread-pr-presentation.ts @@ -127,12 +127,6 @@ export function resolveThreadPrSource( return { linkedPresentation, pullRequestRef }; } -export function resolveThreadPrBadgeAction(pr: ThreadPrPresentation) { - return pr.kind === "stack" || pr.others > 0 - ? { kind: "list" as const, accessibilityLabel: `Show ${pr.accessibilityLabel}` } - : { kind: "open" as const, accessibilityLabel: `Open ${pr.accessibilityLabel}` }; -} - /** Records `value` as the newest entry, evicting the least recently answered beyond `limit`. */ export function rememberBounded( current: ReadonlyMap, From 3c6cc9cead30db80d3dfdac4f892768a4fc126d3 Mon Sep 17 00:00:00 2001 From: wSedlacek Date: Sun, 8 Nov 2026 09:42:59 -0800 Subject: [PATCH 2/2] fix(mobile): announce PR badge on thread list rows VoiceOver focuses the row, not the non-interactive badge, so include the PR description in the row accessibility label. --- apps/mobile/src/features/threads/thread-list-v2-items.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/src/features/threads/thread-list-v2-items.tsx b/apps/mobile/src/features/threads/thread-list-v2-items.tsx index 04e65f0235b14..d080f429586a5 100644 --- a/apps/mobile/src/features/threads/thread-list-v2-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-v2-items.tsx @@ -733,7 +733,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: { : workingLabel) ?? (isUnread ? DONE_STATUS_LABEL : undefined); const recede = shouldRecedeThreadRow({ status, selected }); const isRegeneratingTitle = thread.titleRegeneration != null; - const rowAccessibilityLabel = `${thread.title}${props.hasQueuedMessages ? ", messages queued to send" : ""}${isRegeneratingTitle ? ", regenerating title" : ""}`; + const rowAccessibilityLabel = `${thread.title}${props.hasQueuedMessages ? ", messages queued to send" : ""}${isRegeneratingTitle ? ", regenerating title" : ""}${pr ? `, ${pr.accessibilityLabel}` : ""}`; const timeLabel = props.timeLabel; const visitThread = useAtomCommand(threadEnvironment.visit, { reportFailure: false });