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
34 changes: 20 additions & 14 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
threadSearchMatchKey,
type EnvironmentThreadSearchMatch,
} from "@t3tools/client-runtime/state/thread-search";
import {
type ChangeRequestSettlementState,
updateChangeRequestSettlementState,
} from "@t3tools/client-runtime/state/thread-settled";
import type {
EnvironmentId,
SidebarProjectGroupingMode,
Expand Down Expand Up @@ -42,6 +46,7 @@ import {
ThreadListShowMoreRow,
} from "../threads/thread-list-items";
import {
ThreadListV2ChangeRequestLookupPool,
ThreadListV2PendingRow,
ThreadListV2Row,
ThreadListV2SettledShelfHeader,
Expand Down Expand Up @@ -478,23 +483,16 @@ export function HomeScreen(props: HomeScreenProps) {
// Settled threads stay in the live shell stream (settled ≠ archived), so
// the partition works directly off live shells — no snapshot merging or
// optimistic holds.
// PR states stream in per-row (rows own the VCS subscriptions); a merged or
// closed PR auto-settles its thread on the next partition (mirrors web).
// PR states stream independently of virtualized rows so every branch-backed
// thread can reach a definitive state before the partition settles it.
const [changeRequestStateByKey, setChangeRequestStateByKey] = useState<
ReadonlyMap<string, "open" | "closed" | "merged">
ReadonlyMap<string, ChangeRequestSettlementState>
>(() => new Map());
const handleChangeRequestState = useCallback(
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
(threadKey: string, state: "open" | "closed" | "merged" | null) => {
setChangeRequestStateByKey((current) => {
if ((current.get(threadKey) ?? null) === state) return current;
const next = new Map(current);
if (state === null) {
next.delete(threadKey);
} else {
next.set(threadKey, state);
}
return next;
});
(stateKey: string, state: ChangeRequestSettlementState) => {
setChangeRequestStateByKey((current) =>
updateChangeRequestSettlementState(current, stateKey, state),
);
},
[],
);
Expand Down Expand Up @@ -1047,6 +1045,14 @@ export function HomeScreen(props: HomeScreenProps) {
if (threadListV2Enabled) {
return (
<View className="flex-1 bg-screen">
<ThreadListV2ChangeRequestLookupPool
threads={props.threads}
environmentId={props.selectedEnvironmentId}
projectRefs={v2ScopedProjectGroup?.projectRefs ?? null}
projectCwdByKey={projectCwdByKey}
settlementEnvironmentIds={settlementEnvironmentIds}
onChangeRequestState={handleChangeRequestState}
/>
<SwipeableScrollGateProvider enabled={swipeEnabled}>
<FlatList
data={threadListV2Items}
Expand Down
38 changes: 24 additions & 14 deletions apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
threadSearchMatchKey,
type EnvironmentThreadSearchMatch,
} from "@t3tools/client-runtime/state/thread-search";
import {
type ChangeRequestSettlementState,
updateChangeRequestSettlementState,
} from "@t3tools/client-runtime/state/thread-settled";
import { LegendList } from "@legendapp/list/react-native";
import type { MenuAction } from "@react-native-menu/menu";
import { useAtomValue } from "@effect/atom-react";
Expand Down Expand Up @@ -68,6 +72,7 @@ import {
ThreadListShowMoreRow,
} from "./thread-list-items";
import {
ThreadListV2ChangeRequestLookupPool,
ThreadListV2PendingRow,
ThreadListV2Row,
ThreadListV2SettledShelfHeader,
Expand Down Expand Up @@ -404,23 +409,16 @@ function ThreadNavigationSidebarPane(

// Thread List v2 (beta) support — same model as the compact Home list
// (HomeScreen.tsx): flat creation-order card block + settled recency tail.
// PR states stream in per-row; merged/closed PRs auto-settle their thread
// on the next partition.
// PR states stream independently of virtualized rows so every branch-backed
// thread can reach a definitive state before the partition settles it.
const [changeRequestStateByKey, setChangeRequestStateByKey] = useState<
ReadonlyMap<string, "open" | "closed" | "merged">
ReadonlyMap<string, ChangeRequestSettlementState>
>(() => new Map());
const handleChangeRequestState = useCallback(
(threadKey: string, state: "open" | "closed" | "merged" | null) => {
setChangeRequestStateByKey((current) => {
if ((current.get(threadKey) ?? null) === state) return current;
const next = new Map(current);
if (state === null) {
next.delete(threadKey);
} else {
next.set(threadKey, state);
}
return next;
});
(stateKey: string, state: ChangeRequestSettlementState) => {
setChangeRequestStateByKey((current) =>
updateChangeRequestSettlementState(current, stateKey, state),
);
},
[],
);
Expand Down Expand Up @@ -1127,6 +1125,16 @@ function ThreadNavigationSidebarPane(
: "No threads yet"}
</Text>
);
const changeRequestLookupPool = threadListV2Enabled ? (
<ThreadListV2ChangeRequestLookupPool
threads={threads}
environmentId={options.selectedEnvironmentId}
projectRefs={selectedProjectScope?.projectRefs ?? null}
projectCwdByKey={projectCwdByKey}
settlementEnvironmentIds={settlementEnvironmentIds}
onChangeRequestState={handleChangeRequestState}
/>
) : null;

if (props.nativeChrome) {
return (
Expand Down Expand Up @@ -1155,6 +1163,7 @@ function ThreadNavigationSidebarPane(
}}
/>
<View className="flex-1">
{changeRequestLookupPool}
<SwipeableScrollGateProvider enabled={swipeEnabled}>
<GestureDetector gesture={sidebarScrollGesture}>
<LegendList
Expand Down Expand Up @@ -1215,6 +1224,7 @@ function ThreadNavigationSidebarPane(
borderRightWidth: StyleSheet.hairlineWidth,
}}
>
{changeRequestLookupPool}
<View className="flex-1" style={{ paddingBottom: insets.bottom }}>
<SwipeableScrollGateProvider enabled={swipeEnabled}>
<GestureDetector gesture={sidebarScrollGesture}>
Expand Down
118 changes: 105 additions & 13 deletions apps/mobile/src/features/threads/thread-list-v2-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import type {
EnvironmentThreadShell,
} from "@t3tools/client-runtime/state/shell";
import type { EnvironmentThreadSearchMatch } from "@t3tools/client-runtime/state/thread-search";
import { canSnooze, resolveSnoozePresets } from "@t3tools/client-runtime/state/thread-settled";
import {
canSnooze,
type ChangeRequestSettlementState,
resolveChangeRequestSettlementState,
resolveSnoozePresets,
selectChangeRequestLookupWindow,
threadChangeRequestStateKey,
} from "@t3tools/client-runtime/state/thread-settled";
import type { EnvironmentId, ProjectId } from "@t3tools/contracts";
import type { MenuAction } from "@react-native-menu/menu";
import { memo, useCallback, useEffect, useMemo, useState, type ComponentProps } from "react";
import {
Expand All @@ -25,13 +33,17 @@ import { cn } from "../../lib/cn";
import { relativeTime } from "../../lib/time";
import { useThemeColor } from "../../lib/useThemeColor";
import type { PendingNewTask } from "../../state/use-pending-new-tasks";
import { useThreadPr } from "../../state/use-thread-pr";
import { useThreadPrLookup, useThreadVcsStatus } from "../../state/use-thread-pr";
import { ThreadSwipeable } from "../home/thread-swipe-actions";
import {
buildThreadListV2ChangeRequestLookupTargets,
resolveThreadListV2SnoozeMenuSelection,
resolveThreadListV2SnoozeGateExpiryMs,
resolveThreadListV2Status,
resolveThreadListV2SwipeActions,
THREAD_LIST_V2_CHANGE_REQUEST_LOOKUP_LIMIT,
THREAD_LIST_V2_CHANGE_REQUEST_LOOKUP_WINDOW_MS,
type ThreadListV2ChangeRequestLookupTarget,
type ThreadListV2Status,
} from "./threadListV2";
import { ThreadSearchMatchExcerpt } from "./thread-search-match";
Expand Down Expand Up @@ -300,6 +312,89 @@ export const ThreadListV2PendingRow = memo(function ThreadListV2PendingRow(props
);
});

const ThreadListV2ChangeRequestLookupReporter = memo(
function ThreadListV2ChangeRequestLookupReporter(props: {
readonly target: ThreadListV2ChangeRequestLookupTarget;
readonly onChangeRequestState: (stateKey: string, state: ChangeRequestSettlementState) => void;
}) {
const { target, onChangeRequestState } = props;
const gitStatus = useThreadVcsStatus(target.environmentId, target.cwd);
useEffect(() => {
for (const thread of target.threads) {
onChangeRequestState(
threadChangeRequestStateKey(thread),
resolveChangeRequestSettlementState({
threadBranch: thread.branch,
gitStatus: gitStatus.data,
gitStatusError: gitStatus.error,
}),
);
}
}, [gitStatus.data, gitStatus.error, onChangeRequestState, target.threads]);
return null;
},
);

export const ThreadListV2ChangeRequestLookupPool = memo(
function ThreadListV2ChangeRequestLookupPool(props: {
readonly threads: ReadonlyArray<EnvironmentThreadShell>;
readonly environmentId: EnvironmentId | null;
readonly projectRefs?: ReadonlyArray<{
readonly environmentId: EnvironmentId;
readonly projectId: ProjectId;
}> | null;
readonly projectCwdByKey: ReadonlyMap<string, string>;
readonly settlementEnvironmentIds: ReadonlySet<EnvironmentId>;
readonly onChangeRequestState: (stateKey: string, state: ChangeRequestSettlementState) => void;
}) {
const {
threads,
environmentId,
projectRefs,
projectCwdByKey,
settlementEnvironmentIds,
onChangeRequestState,
} = props;
const targets = useMemo(
() =>
buildThreadListV2ChangeRequestLookupTargets({
threads,
environmentId,
projectRefs,
projectCwdByKey,
settlementEnvironmentIds,
}),
[environmentId, projectCwdByKey, projectRefs, settlementEnvironmentIds, threads],
);
const [windowIndex, setWindowIndex] = useState(0);
useEffect(() => {
if (targets.length <= THREAD_LIST_V2_CHANGE_REQUEST_LOOKUP_LIMIT) return;
const interval = setInterval(
() => setWindowIndex((current) => current + 1),
THREAD_LIST_V2_CHANGE_REQUEST_LOOKUP_WINDOW_MS,
);
return () => clearInterval(interval);
}, [targets.length]);
const activeTargets = useMemo(
() =>
selectChangeRequestLookupWindow({
targets,
windowIndex,
limit: THREAD_LIST_V2_CHANGE_REQUEST_LOOKUP_LIMIT,
}),
[targets, windowIndex],
);

return activeTargets.map((target) => (
<ThreadListV2ChangeRequestLookupReporter
key={target.key}
target={target}
onChangeRequestState={onChangeRequestState}
/>
));
},
);

export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
readonly thread: EnvironmentThreadShell;
readonly variant: "card" | "slim";
Expand Down Expand Up @@ -344,12 +439,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
readonly snoozeSupported: boolean;
readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void;
readonly onSwipeableClose: (methods: SwipeableMethods) => void;
/** Reports this row's live PR state up so the partition can auto-settle
merged/closed work (mirrors web's onChangeRequestState). */
readonly onChangeRequestState?: (
threadKey: string,
state: "open" | "closed" | "merged" | null,
) => void;
readonly onChangeRequestState?: (stateKey: string, state: ChangeRequestSettlementState) => void;
readonly projectCwd?: string | null;
readonly searchMatch?: EnvironmentThreadSearchMatch;
readonly searchQuery?: string;
Expand All @@ -372,12 +462,14 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
} = props;
const snoozedRow = props.snoozed === true;

const pr = useThreadPr(thread, props.projectCwd ?? props.project?.workspaceRoot ?? null);
const prState = pr?.state ?? null;
const threadKey = `${thread.environmentId}:${thread.id}`;
const { changeRequestState, pr } = useThreadPrLookup(
thread,
props.projectCwd ?? props.project?.workspaceRoot ?? null,
);
const changeRequestStateKey = threadChangeRequestStateKey(thread);
useEffect(() => {
onChangeRequestState?.(threadKey, prState);
}, [onChangeRequestState, prState, threadKey]);
onChangeRequestState?.(changeRequestStateKey, changeRequestState);
}, [changeRequestState, changeRequestStateKey, onChangeRequestState]);

const screenColor = useThemeColor("--color-screen");
const drawerColor = useThemeColor("--color-drawer");
Expand Down
Loading
Loading